From 1b055b15452dcce1844a8f09900c174ced9a78f1 Mon Sep 17 00:00:00 2001 From: riacataquian Date: Sun, 19 Jun 2022 18:31:38 +0800 Subject: [PATCH] interp: display all Bash's `shopt` option Trying to set an unsupported but valid Bash option leads to a potentially confusing error message: ``` $ gosh -c "shopt -s extglob" shopt: invalid option name "extglob" ``` This commit lists all of the Bash options when `shopt` without arguments is called and explicitly identify the unsupported options: ``` $ gosh -c "shopt | grep unsupported" cmdhist off (unsupported) complete_fullquote off (unsupported) extquote off (unsupported) force_fignore off (unsupported) hostcomplete off (unsupported) assoc_expand_once off (unsupported) autocd off (unsupported) ``` Also, rewrite the `bashOptsTable` so that it can keep two option states: - Supported options; and - Options that we may or may not allow users to override using the `-s` and `-u` flags We kept that distinction so we could error if one attempts to override the default options. In practice, toggling these defaults are generally okay, but in our case, it does not produce any side-effect and we wanted to be explicit about it when toggled. Fixes #877 --- interp/api.go | 140 +++++++++++++++++++++++++++++++++++++----- interp/builtin.go | 43 +++++++++---- interp/interp_test.go | 13 ++++ interp/test.go | 2 +- 4 files changed, 171 insertions(+), 27 deletions(-) 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