Skip to content

Commit

Permalink
interp: display all Bash's shopt option
Browse files Browse the repository at this point in the history
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"
```

Fix that by handling the unsupported options differently from the
invalid ones:
```
$ gosh -c "shopt -s extglob"
bash: line 1: shopt: extglob off ("on" not supported)
exit status 1
```

Additionally, this commit lists all of the Bash options when `shopt`
without arguments is called and explicitly identify the unsupported
options, for example:
```
$ gosh -c "shopt"
expand_aliases	off
globstar	off
nullglob	off
// .. cut for brevity
hostcomplete	on	("off" not supported)
inherit_errexit	on	("off" not supported)
interactive_comments	on	("off" not supported)
```

While at it, rewrite the `bashOptsTable` so that it can keep two option
states: 1) Bash's default options and 2) whether we support it

Fixes #877
  • Loading branch information
riacataquian committed Jun 29, 2022
1 parent 3d2e729 commit 7a93c0c
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 28 deletions.
143 changes: 128 additions & 15 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.defaultState
}

// Set the default fallbacks, if necessary.
if r.Env == nil {
Env(nil)(r)
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -366,28 +372,38 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption {
}
}

func (r *Runner) optByName(name string, bash bool) *bool {
// optByName returns the matching runner's option index and status
func (r *Runner) optByName(name string, bash bool) (index int, status *bool) {
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 {
index = len(shellOptsTable) + i
return index, &r.opts[index]
}
}
}
for i, opt := range &shellOptsTable {
if opt.name == name {
return &r.opts[i]
return i, &r.opts[i]
}
}
return nil
return 0, nil
}

type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool

var shellOptsTable = [...]struct {
type shellOpt struct {
flag byte
name string
}{
}

type bashOpt struct {
name string
defaultState 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"},
Expand All @@ -399,11 +415,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",
defaultState: false,
supported: true,
},
{
name: "globstar",
defaultState: false,
supported: true,
},
{
name: "nullglob",
defaultState: 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",
defaultState: true,
},
{
name: "cmdhist",
defaultState: true,
},
{name: "compat31"},
{name: "compat32"},
{name: "compat40"},
{name: "compat41"},
{name: "compat42"},
{name: "compat44"},
{name: "compat43"},
{name: "compat44"},
{
name: "complete_fullquote",
defaultState: true,
},
{name: "direxpand"},
{name: "dirspell"},
{name: "dotglob"},
{name: "execfail"},
{name: "extdebug"},
{name: "extglob"},
{
name: "extquote",
defaultState: true,
},
{name: "failglob"},
{
name: "force_fignore",
defaultState: true,
},
{name: "globasciiranges"},
{name: "gnu_errfmt"},
{name: "histappend"},
{name: "histreedit"},
{name: "histverify"},
{
name: "hostcomplete",
defaultState: true,
},
{name: "huponexit"},
{
name: "inherit_errexit",
defaultState: true,
},
{
name: "interactive_comments",
defaultState: 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",
defaultState: true,
},
{name: "progcomp_alias"},
{
name: "promptvars",
defaultState: true,
},
{name: "restricted_shell"},
{name: "shift_verbose"},
{
name: "sourcepath",
defaultState: true,
},
{name: "xpg_echo"},
}

// To access the shell options arrays without a linear search when we
Expand Down
46 changes: 35 additions & 11 deletions interp/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,29 +668,44 @@ 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)
i, opt := r.optByName(arg, bash)
if opt == nil {
r.errf("shopt: invalid option name %q\n", arg)
return 1
}

var (
bo *bashOpt
supported = true // default for shell options
)
if bash {
bo = &bashOptsTable[i-len(shellOptsTable)]
supported = bo.supported
}

switch mode {
case "-s", "-u":
if bash && !supported {
r.errf("shopt: invalid option name %q %q (%q not supported)\n", arg, r.optStatusText(bo.defaultState), r.optStatusText(!bo.defaultState))
return 1
}
*opt = mode == "-s"
default: // ""
r.printOptLine(arg, *opt)
r.printOptLine(arg, *opt, supported)
}
}
r.updateExpandOpts()
Expand Down Expand Up @@ -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 := r.optStatusText(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, r.optStatusText(!enabled))
}

func (r *Runner) readLine(raw bool) ([]byte, error) {
Expand Down Expand Up @@ -1070,3 +1086,11 @@ func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, d

return opt, optarg, false
}

// optStatusText returns a shell option's status text display
func (r *Runner) optStatusText(status bool) string {
if status {
return "on"
}
return "off"
}
19 changes: 18 additions & 1 deletion interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,7 @@ var runTests = []runTest{
{`mkdir d; old=$PWD; cd d & wait; [[ $old == "$PWD" ]]`, ""},
{
"f() { echo 1; }; { sleep 0.01s; f; } & f() { echo 2; }; wait",
"1\n",
"1\n #IGNORE",
},

// bash test
Expand Down Expand Up @@ -2072,6 +2072,23 @@ 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",
"inherit_errexit\ton\t(\"off\" not supported)\n #JUSTERR",
},
{
"shopt -s extglob",
"shopt: invalid option name \"extglob\" \"off\" (\"on\" not supported)\nexit status 1 #IGNORE",
},
{
"shopt -s interactive_comments",
"shopt: invalid option name \"interactive_comments\" \"on\" (\"off\" not supported)\nexit status 1 #IGNORE",
},
{
"shopt -s foo",
"shopt: invalid option name \"foo\"\nexit status 1 #JUSTERR",
},

// IFS
{`echo -n "$IFS"`, " \t\n"},
Expand Down
2 changes: 1 addition & 1 deletion interp/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7a93c0c

Please sign in to comment.