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" | grep "not supported"
checkwinsize	on	("off" not supported)
cmdhist	on	("off" not supported)
complete_fullquote	on	("off" not supported)
extquote	on	("off" not supported)
force_fignore	on	("off" not supported)
// .. cut for brevity
xpg_echo	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 mvdan#877
  • Loading branch information
riacataquian committed Jun 26, 2022
1 parent 5146d3e commit 55b2812
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 30 deletions.
146 changes: 131 additions & 15 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -366,28 +366,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"},
Expand All @@ -399,11 +408,118 @@ var shellOptsTable = [...]struct {
{' ', "pipefail"},
}

var bashOptsTable = [...]string{
// sorted alphabetically by name
"expand_aliases",
"globstar",
"nullglob",
var bashOptsTable = [...]bashOpt{
// Bash options that are "on" by default, sorted alphabetically by name
{
name: "checkwinsize",
default_state: true,
supported: false,
},
{
name: "cmdhist",
default_state: true,
supported: false,
},
{
name: "complete_fullquote",
default_state: true,
supported: false,
},
{
name: "expand_aliases",
default_state: true,
supported: true,
},
{
name: "extquote",
default_state: true,
supported: false,
},
{
name: "force_fignore",
default_state: true,
supported: false,
},
{
name: "globstar",
default_state: true,
supported: true,
},
{
name: "hostcomplete",
default_state: true,
supported: false,
},
{
name: "inherit_errexit",
default_state: true,
supported: false,
},
{
name: "interactive_comments",
default_state: true,
supported: false,
},
{
name: "nullglob",
default_state: true,
supported: true,
},
{
name: "progcomp",
default_state: true,
supported: false,
},
{
name: "promptvars",
default_state: true,
supported: false,
},
{
name: "sourcepath",
default_state: true,
supported: false,
},
// unsupported and non-default options, 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"},
}

// To access the shell options arrays without a linear search when we
Expand Down
44 changes: 30 additions & 14 deletions interp/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
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 := 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, optStatuses[!enabled], state)
}

func (r *Runner) readLine(raw bool) ([]byte, error) {
Expand Down
14 changes: 14 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,20 @@ 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 '\"off\" not supported' | wc -l | tr -d ' '", "1\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"},
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 55b2812

Please sign in to comment.