diff --git a/interp/api.go b/interp/api.go index 0648bd9db..3c2fa50fd 100644 --- a/interp/api.go +++ b/interp/api.go @@ -274,7 +274,7 @@ func Params(args ...string) RunnerOption { value := fp.value() if value == "" && enable { for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } continue } @@ -288,7 +288,7 @@ func Params(args ...string) RunnerOption { } continue } - opt := r.optByName(value, false) + opt, _, _ := r.optByName(value, false) if opt == nil { return fmt.Errorf("invalid option: %q", value) } @@ -366,28 +366,39 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption { } } -func (r *Runner) optByName(name string, bash bool) *bool { +func (r *Runner) optByName(name string, bash bool) (*bool, *shellOpt, *bashOpt) { if bash { - for i, optName := range bashOptsTable { - if optName == name { - return &r.opts[len(shellOptsTable)+i] + for i, opt := range bashOptsTable { + if opt.name == name { + return &r.opts[len(shellOptsTable)+i], nil, &opt } } } for i, opt := range &shellOptsTable { if opt.name == name { - return &r.opts[i] + return &r.opts[i], &opt, nil } } - return nil + + return nil, nil, nil } type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool -var shellOptsTable = [...]struct { +type shellOpt struct { flag byte name string -}{ +} + +type bashOpt struct { + name string + // explicitly state and treat these option's status differently to avoid confusion; + // although some Bash options are supported and enabled by default, + // toggling and untoggling of these options does not produce a side-effect + allow_override, supported bool +} + +var shellOptsTable = [...]shellOpt{ // sorted alphabetically by name; use a space for the options // that have no flag form {'a', "allexport"}, @@ -399,13 +410,112 @@ var shellOptsTable = [...]struct { {' ', "pipefail"}, } -var bashOptsTable = [...]string{ - // sorted alphabetically by name - "expand_aliases", - "globstar", - "nullglob", +var bashOptsTable = [...]bashOpt{ + // supported flags, sorted alphabetically by name + { + name: "expand_aliases", + supported: true, + allow_override: true, + }, + { + name: "globstar", + supported: true, + allow_override: true, + }, + { + name: "nullglob", + supported: true, + allow_override: true, + }, + // options that are enabled by default in Bash, sorted alphabetically by name + { + name: "checkwinsize", + supported: true, + }, + { + name: "cmdhist", + supported: false, + }, + { + name: "complete_fullquote", + supported: false, + }, + { + name: "extquote", + supported: false, + }, + { + name: "force_fignore", + supported: false, + }, + { + name: "hostcomplete", + supported: false, + }, + { + name: "inherit_errexit", + supported: true, + }, + { + name: "interactive_comments", + supported: true, + }, + { + name: "progcomp", + supported: true, + }, + { + name: "promptvars", + supported: true, + }, + { + name: "sourcepath", + supported: true, + }, + // unsupported flags, sorted alphabetically by name + {name: "assoc_expand_once"}, + {name: "autocd"}, + {name: "cdable_vars"}, + {name: "cdspell"}, + {name: "checkhash"}, + {name: "checkjobs"}, + {name: "compat31"}, + {name: "compat32"}, + {name: "compat40"}, + {name: "compat41"}, + {name: "compat42"}, + {name: "compat44"}, + {name: "compat43"}, + {name: "direxpand"}, + {name: "dirspell"}, + {name: "dotglob"}, + {name: "execfail"}, + {name: "extdebug"}, + {name: "extglob"}, + {name: "failglob"}, + {name: "globasciiranges"}, + {name: "gnu_errfmt"}, + {name: "histappend"}, + {name: "histreedit"}, + {name: "histverify"}, + {name: "huponexit"}, + {name: "lastpipe"}, + {name: "lithist"}, + {name: "localvar_inherit"}, + {name: "localvar_unset"}, + {name: "login_shell"}, + {name: "mailwarn"}, + {name: "no_empty_cmd_completion"}, + {name: "nocaseglob"}, + {name: "nocasematch"}, + {name: "progcomp_alias"}, + {name: "restricted_shell"}, + {name: "shift_verbose"}, + {name: "xpg_echo"}, } +var bashOptsTableLen int = len(bashOptsTable) + // To access the shell options arrays without a linear search when we // know which option we're after at compile time. First come the shell options, // then the bash options. diff --git a/interp/builtin.go b/interp/builtin.go index 4cb726446..d49744fc5 100644 --- a/interp/builtin.go +++ b/interp/builtin.go @@ -668,29 +668,46 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } } args := fp.args() + bash := !posixOpts if len(args) == 0 { - if !posixOpts { - for i, name := range bashOptsTable { - r.printOptLine(name, r.opts[len(shellOptsTable)+i]) + if bash { + for i, opt := range bashOptsTable { + r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported) } break } for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } break } for _, arg := range args { - opt := r.optByName(arg, !posixOpts) - if opt == nil { - r.errf("shopt: invalid option name %q\n", arg) + v, _, opt := r.optByName(arg, bash) + if v == nil { + r.errf("bash: line %d: shopt: %s: invalid shell option name\n", pos.Line(), arg) return 1 } switch mode { case "-s", "-u": - *opt = mode == "-s" + if bash && opt.supported == false { + r.errf("bash: line %d: shopt: %s: unsupported option name\n", pos.Line(), arg) + return 1 + } + if bash && opt.allow_override == false { + status := "off" + if opt.supported { + status = "on" + } + r.errf("bash: line %d: shopt: %s (%v): setting/unsetting unsupported for option name\n", pos.Line(), arg, status) + return 1 + } + *v = mode == "-s" default: // "" - r.printOptLine(arg, *opt) + supported := true + if bash { + supported = opt.supported + } + r.printOptLine(arg, *v, supported) } } r.updateExpandOpts() @@ -890,12 +907,16 @@ func mapfileSplit(delim byte, dropDelim bool) func(data []byte, atEOF bool) (adv } } -func (r *Runner) printOptLine(name string, enabled bool) { +func (r *Runner) printOptLine(name string, enabled, supported bool) { status := "off" if enabled { status = "on" } - r.outf("%s\t%s\n", name, status) + if supported { + r.outf("%s\t%s\n", name, status) + return + } + r.outf("%s\t%s\t(unsupported)\n", name, status) } func (r *Runner) readLine(raw bool) ([]byte, error) { diff --git a/interp/interp_test.go b/interp/interp_test.go index 99317a607..db3175af6 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -2070,6 +2070,19 @@ set +o pipefail {"shopt -u -o noexec; echo foo_interp_missing", "foo_interp_missing\n"}, {"shopt -u globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '", "1\n"}, {"shopt -s globstar; shopt globstar | grep 'off$' | wc -l | tr -d ' '", "0\n"}, + {"shopt extglob | grep 'off' | wc -l | tr -d ' '", "1\n"}, + { + "shopt -s extglob", + "bash: line 1: shopt: extglob: unsupported option name\nexit status 1 #IGNORE only gosh returns unsupported", + }, + { + "shopt -s inherit_errexit", + "bash: line 1: shopt: inherit_errexit (on): setting/unsetting unsupported for option name\nexit status 1 #IGNORE default bash options cant be toggled", + }, + { + "shopt -s foo", + "bash: line 1: shopt: foo: invalid shell option name\nexit status 1", + }, // IFS {`echo -n "$IFS"`, " \t\n"}, diff --git a/interp/test.go b/interp/test.go index fb45699c4..9a61a68eb 100644 --- a/interp/test.go +++ b/interp/test.go @@ -190,7 +190,7 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) case syntax.TsNempStr: return x != "" case syntax.TsOptSet: - if opt := r.optByName(x, false); opt != nil { + if opt, _, _ := r.optByName(x, false); opt != nil { return *opt } return false