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 26, 2022
1 parent 5146d3e commit 85f4362
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 30 deletions.
142 changes: 127 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.default_state
}

// 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,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 +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
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, state, optStatuses[!enabled])
}

func (r *Runner) readLine(raw bool) ([]byte, error) {
Expand Down
18 changes: 18 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
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 85f4362

Please sign in to comment.