diff --git a/interp/api.go b/interp/api.go index 0648bd9db..8b8fca5d0 100644 --- a/interp/api.go +++ b/interp/api.go @@ -185,6 +185,12 @@ func New(opts ...RunnerOption) (*Runner, error) { return nil, err } } + + // turn "on" the default Bash options + for i, opt := range bashOptsTable { + r.opts[len(shellOptsTable)+i] = opt.default_state + } + // Set the default fallbacks, if necessary. if r.Env == nil { Env(nil)(r) @@ -274,7 +280,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 +294,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 +372,37 @@ 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 + default_state bool // Bash's default value for this option + supported bool // whether we support the option's non-default state +} + +var shellOptsTable = [...]shellOpt{ // sorted alphabetically by name; use a space for the options // that have no flag form {'a', "allexport"}, @@ -399,11 +414,108 @@ var shellOptsTable = [...]struct { {' ', "pipefail"}, } -var bashOptsTable = [...]string{ - // sorted alphabetically by name - "expand_aliases", - "globstar", - "nullglob", +var bashOptsTable = [...]bashOpt{ + // supported options, sorted alphabetically by name + { + name: "expand_aliases", + default_state: false, + supported: true, + }, + { + name: "globstar", + default_state: false, + supported: true, + }, + { + name: "nullglob", + default_state: false, + supported: true, + }, + // unsupported options, sorted alphabetically by name + {name: "assoc_expand_once"}, + {name: "autocd"}, + {name: "cdable_vars"}, + {name: "cdspell"}, + {name: "checkhash"}, + {name: "checkjobs"}, + { + name: "checkwinsize", + default_state: true, + }, + { + name: "cmdhist", + default_state: true, + }, + {name: "compat31"}, + {name: "compat32"}, + {name: "compat40"}, + {name: "compat41"}, + {name: "compat42"}, + {name: "compat44"}, + {name: "compat43"}, + {name: "compat44"}, + { + name: "complete_fullquote", + default_state: true, + }, + {name: "direxpand"}, + {name: "dirspell"}, + {name: "dotglob"}, + {name: "execfail"}, + {name: "extdebug"}, + {name: "extglob"}, + { + name: "extquote", + default_state: true, + }, + {name: "failglob"}, + { + name: "force_fignore", + default_state: true, + }, + {name: "globasciiranges"}, + {name: "gnu_errfmt"}, + {name: "histappend"}, + {name: "histreedit"}, + {name: "histverify"}, + { + name: "hostcomplete", + default_state: true, + }, + {name: "huponexit"}, + { + name: "inherit_errexit", + default_state: true, + }, + { + name: "interactive_comments", + default_state: true, + }, + {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", + default_state: true, + }, + {name: "progcomp_alias"}, + { + name: "promptvars", + default_state: true, + }, + {name: "restricted_shell"}, + {name: "shift_verbose"}, + { + name: "sourcepath", + default_state: true, + }, + {name: "xpg_echo"}, } // To access the shell options arrays without a linear search when we diff --git a/interp/builtin.go b/interp/builtin.go index 4cb726446..52b0ed383 100644 --- a/interp/builtin.go +++ b/interp/builtin.go @@ -19,6 +19,12 @@ import ( "mvdan.cc/sh/v3/syntax" ) +// optStatuses are the option statuses' text display +var optStatuses = map[bool]string{ + true: "on", + false: "off", +} + func isBuiltin(name string) bool { switch name { case "true", ":", "false", "exit", "set", "shift", "unset", @@ -668,29 +674,38 @@ 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 { + r.errf("bash: line %d: shopt: %s %s (%q not supported)\n", pos.Line(), arg, optStatuses[opt.default_state], optStatuses[!opt.default_state]) + 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 +905,13 @@ func mapfileSplit(delim byte, dropDelim bool) func(data []byte, atEOF bool) (adv } } -func (r *Runner) printOptLine(name string, enabled bool) { - status := "off" - if enabled { - status = "on" +func (r *Runner) printOptLine(name string, enabled, supported bool) { + state := optStatuses[enabled] + if supported { + r.outf("%s\t%s\n", name, state) + return } - r.outf("%s\t%s\n", name, status) + r.outf("%s\t%s\t(%q not supported)\n", name, state, optStatuses[!enabled]) } func (r *Runner) readLine(raw bool) ([]byte, error) { diff --git a/interp/interp_test.go b/interp/interp_test.go index 99317a607..44f16f029 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -2070,6 +2070,24 @@ 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 inherit_errexit | grep 'on' | wc -l | tr -d ' '", "1\n"}, + { + "shopt inherit_errexit", + "inherit_errexit\ton\t(\"off\" not supported)\n", + }, + { + "shopt -s extglob", + "bash: line 1: shopt: extglob off (\"on\" not supported)\nexit status 1 #IGNORE", + }, + { + "shopt -s interactive_comments", + "bash: line 1: shopt: interactive_comments on (\"off\" not supported)\nexit status 1 #IGNORE", + }, + { + "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