Permalink
Browse files

Implement the getopts builtin, with tests.

Uncovered a fundamental design flaw with the global variable OPTIND.

Didn't implement some stuff like extra args and OPTERR.  Alpine doesn't
seem to use these.

Fixes issue #39.
  • Loading branch information...
Andy Chu
Andy Chu committed Sep 26, 2017
1 parent fc3fc85 commit a36fe9a45f404db7f7ba65d378987842557d52d6
Showing with 293 additions and 2 deletions.
  1. +1 −0 core/args.py
  2. +106 −1 core/builtin.py
  3. +3 −0 core/cmd_exec.py
  4. +2 −0 core/state.py
  5. +180 −0 spec/builtin-getopts.test.sh
  6. +1 −1 test/spec.sh
View
@@ -472,3 +472,4 @@ def Parse(self, argv):
break
return out, state.i
View
@@ -42,6 +42,9 @@
scope = runtime.scope
var_flags = runtime.var_flags
log = util.log
e_die = util.e_die
# NOTE: NONE is a special value.
# TODO:
@@ -57,7 +60,7 @@
COMPLETE COMPGEN DEBUG_LINE
TRUE FALSE
COLON
TEST BRACKET
TEST BRACKET GETOPTS
TYPE HELP
""".split())
@@ -197,6 +200,9 @@ def Resolve(argv0):
elif argv0 == "[":
return EBuiltin.BRACKET
elif argv0 == "getopts":
return EBuiltin.GETOPTS
elif argv0 == "type":
return EBuiltin.TYPE
@@ -746,6 +752,105 @@ def Umask(argv):
raise args.UsageError('umask: unexpected arguments')
def _ParseOptSpec(spec_str):
spec = {}
i = 0
n = len(spec_str)
while True:
if i >= n:
break
c = spec_str[i]
key = '-' + c
spec[key] = False
i += 1
if i >= n:
break
# If the next character is :, change the value to True.
if spec_str[i] == ':':
spec[key] = True
i += 1
return spec
def _GetOpts(spec, mem, optind):
optarg = '' # not set by default
v2 = mem.GetArgNum(optind)
if v2.tag == value_e.Undef: # No more arguments.
return 1, '?', optarg, optind
assert v2.tag == value_e.Str
current = v2.s
if not current.startswith('-'): # The next arg doesn't look like a flag.
return 1, '?', optarg, optind
# It looks like an argument. Stop iteration by returning 1.
if current not in spec: # Invalid flag
optind += 1
return 0, '?', optarg, optind
optind += 1
opt_char = current[-1]
needs_arg = spec[current]
if needs_arg:
v3 = mem.GetArgNum(optind)
if v3.tag == value_e.Undef:
util.error('getopts: option %r requires an argument', current)
# Hm doesn't cause status 1?
return 0, '?', optarg, optind
assert v3.tag == value_e.Str
optarg = v3.s
optind += 1
return 0, opt_char, optarg, optind
# spec string -> {flag, arity}
_GETOPTS_CACHE = {}
def GetOpts(argv, mem):
"""
Vars to set:
OPTIND - initialized to 1 at startup
OPTARG - argument
Vars used:
OPTERR: disable printing of error messages
"""
try:
# NOTE: If first char is a colon, error reporting is different. Alpine
# might not use that?
spec_str = argv[0]
var_name = argv[1]
except IndexError:
raise args.UsageError('getopts optstring name [arg]')
try:
spec = _GETOPTS_CACHE[spec_str]
except KeyError:
spec = _ParseOptSpec(spec_str)
_GETOPTS_CACHE[spec_str] = spec
# These errors are fatal errors, not like the builtin exiting with code 1.
# Because the invariants of the shell have been violated!
v = mem.GetVar('OPTIND')
if v.tag != value_e.Str:
e_die('OPTIND should be a string, got %r', v)
try:
optind = int(v.s)
except ValueError:
e_die("OPTIND doesn't look like an integer, got %r", v.s)
status, opt_char, optarg, optind = _GetOpts(spec, mem, optind)
state.SetGlobalString(mem, var_name, opt_char)
state.SetGlobalString(mem, 'OPTARG', optarg)
state.SetGlobalString(mem, 'OPTIND', str(optind))
return status
def Help(argv, loader):
# TODO: Need $VERSION inside all pages?
try:
View
@@ -297,6 +297,9 @@ def _RunBuiltin(self, builtin_id, argv):
elif builtin_id == EBuiltin.BRACKET:
status = test_builtin.Test(argv, True) # need_right_bracket
elif builtin_id == EBuiltin.GETOPTS:
status = builtin.GetOpts(argv, self.mem)
elif builtin_id == EBuiltin.TYPE:
path = self.mem.GetVar('PATH')
status = builtin.Type(argv, self.funcs, path)
View
@@ -245,6 +245,8 @@ def _InitDefaults(self):
# NOTE: Should we put these in a namespace for Oil?
SetGlobalString(self, 'UID', str(os.getuid()))
SetGlobalString(self, 'EUID', str(os.geteuid()))
# For getopts builtin
SetGlobalString(self, 'OPTIND', '1')
def _InitEnviron(self, environ):
# This is the way dash and bash work -- at startup, they turn everything in
@@ -4,6 +4,36 @@
#
# NOTE: Aliases don't work in batch mode! Interactive only.
### getopts empty
set --
getopts 'a:' opt
echo "status=$? opt=$opt OPTARG=$OPTARG"
# stdout: status=1 opt=? OPTARG=
### getopts sees unknown arg
set -- -Z
getopts 'a:' opt
echo "status=$? opt=$opt OPTARG=$OPTARG"
# stdout: status=0 opt=? OPTARG=
### getopts three invocations
set -- -h -c foo
getopts 'hc:' opt
echo status=$? opt=$opt
getopts 'hc:' opt
echo status=$? opt=$opt
getopts 'hc:' opt
echo status=$? opt=$opt
# stdout-json: "status=0 opt=h\nstatus=0 opt=c\nstatus=1 opt=?\n"
### getopts resets OPTARG
set -- -c foo -h
getopts 'hc:' opt
echo status=$? opt=$opt OPTARG=$OPTARG
getopts 'hc:' opt
echo status=$? opt=$opt OPTARG=$OPTARG
# stdout-json: "status=0 opt=c OPTARG=foo\nstatus=0 opt=h OPTARG=\n"
### Basic getopts invocation
set -- -h -c foo x y z
FLAG_h=0
@@ -17,3 +47,153 @@ done
shift $(( OPTIND - 1 ))
echo h=$FLAG_h c=$FLAG_c optind=$OPTIND argv=$@
# stdout: h=1 c=foo optind=4 argv=x y z
### getopts with invalid flag
set -- -h -x
while getopts "hc:" opt; do
case $opt in
h) FLAG_h=1 ;;
c) FLAG_c="$OPTARG" ;;
'?') echo ERROR $OPTIND; exit 2; ;;
esac
done
echo status=$?
# stdout: ERROR 3
# status: 2
### getopts missing required argument
set -- -h -c
while getopts "hc:" opt; do
case $opt in
h) FLAG_h=1 ;;
c) FLAG_c="$OPTARG" ;;
'?') echo ERROR $OPTIND; exit 2; ;;
esac
done
echo status=$?
# stdout: ERROR 3
# status: 2
### getopts doesn't look for flags after args
set -- x -h -c y
FLAG_h=0
FLAG_c=''
while getopts "hc:" opt; do
case $opt in
h) FLAG_h=1 ;;
c) FLAG_c="$OPTARG" ;;
esac
done
shift $(( OPTIND - 1 ))
echo h=$FLAG_h c=$FLAG_c optind=$OPTIND argv=$@
# stdout: h=0 c= optind=1 argv=x -h -c y
### getopts with explicit args
# NOTE: Alpine doesn't appear to use this
FLAG_h=0
FLAG_c=''
arg=''
while getopts "hc:" opt -h -c foo x y z; do
case $opt in
h) FLAG_h=1 ;;
c) FLAG_c="$OPTARG" ;;
esac
done
echo h=$FLAG_h c=$FLAG_c optind=$OPTIND argv=$@
# stdout: h=1 c=foo optind=4 argv=
### OPTIND
echo $OPTIND
# stdout: 1
### OPTIND after multiple getopts with same spec
while getopts "hc:" opt; do
echo '-'
done
echo $OPTIND
set -- -h -c foo x y z
while getopts "hc:" opt; do
echo '-'
done
echo $OPTIND
set --
while getopts "hc:" opt; do
echo '-'
done
echo $OPTIND
# stdout-json: "1\n-\n-\n4\n1\n"
# BUG mksh/osh stdout-json: "1\n-\n-\n4\n4\n"
### OPTIND after multiple getopts with different spec
# Wow this is poorly specified! A fundamental design problem with the global
# variable OPTIND.
set -- -a
while getopts "ab:" opt; do
echo '.'
done
echo $OPTIND
set -- -c -d -e foo
while getopts "cde:" opt; do
echo '-'
done
echo $OPTIND
set -- -f
while getopts "f:" opt; do
echo '_'
done
echo $OPTIND
# stdout-json: ".\n2\n-\n-\n5\n2\n"
# BUG ash/dash stdout-json: ".\n2\n-\n-\n-\n5\n_\n2\n"
# BUG mksh/osh stdout-json: ".\n2\n-\n-\n5\n5\n"
### OPTIND narrowed down
FLAG_a=
FLAG_b=
FLAG_c=
FLAG_d=
FLAG_e=
set -- -a
while getopts "ab:" opt; do
case $opt in
a) FLAG_a=1 ;;
b) FLAG_b="$OPTARG" ;;
esac
done
# Bash doesn't reset optind! It skips over c! mksh at least warns about this!
# You have to reset OPTIND yourself.
set -- -c -d -e E
while getopts "cde:" opt; do
case $opt in
c) FLAG_c=1 ;;
d) FLAG_d=1 ;;
e) FLAG_e="$OPTARG" ;;
esac
done
echo a=$FLAG_a b=$FLAG_b c=$FLAG_c d=$FLAG_d e=$FLAG_e
# stdout: a=1 b= c=1 d=1 e=E
# BUG bash/mksh/osh stdout: a=1 b= c= d=1 e=E
### Getopts parses the function's arguments
# NOTE: GLOBALS are set, not locals! Bad interface.
FLAG_h=0
FLAG_c=''
myfunc() {
while getopts "hc:" opt; do
case $opt in
h) FLAG_h=1 ;;
c) FLAG_c="$OPTARG" ;;
esac
done
}
set -- -h -c foo x y z
myfunc -c bar
echo h=$FLAG_h c=$FLAG_c opt=$opt optind=$OPTIND argv=$@
# stdout: h=0 c=bar opt=? optind=3 argv=-h -c foo x y z
View
@@ -259,7 +259,7 @@ builtin-vars() {
builtin-getopts() {
sh-spec spec/builtin-getopts.test.sh --osh-failures-allowed 1 \
${REF_SHELLS[@]} $OSH "$@"
${REF_SHELLS[@]} $BUSYBOX_ASH $OSH "$@"
}
builtin-test() {

0 comments on commit a36fe9a

Please sign in to comment.