View
@@ -206,11 +206,39 @@ def GetTraceback(self, token):
pass
class _ExecError(RuntimeError):
"""Internal to Executor."""
class _FatalError(RuntimeError):
"""Internal exception for fatal errors."""
pass
class _ControlFlow(RuntimeError):
"""Internal execption for control flow.
break and continue are caught by loops, return is caught by functions.
"""
def __init__(self, token, arg):
"""
Args:
token: the keyword token
"""
self.token = token
self.arg = arg
def IsReturn(self):
return self.token.id == Id.ControlFlow_Return
def IsBreak(self):
return self.token.id == Id.ControlFlow_Break
def IsContinue(self):
return self.token.id == Id.ControlFlow_Continue
def ReturnValue(self):
assert self.IsReturn()
return self.arg
class Executor(object):
"""Executes the program by tree-walking.
@@ -373,7 +401,7 @@ def _EvalHelper(self, code_str):
if not node:
print('Error parsing code %r' % code_str)
return 1
status, cflow = self._Execute(node)
status = self._Execute(node)
return status
def _Eval(self, argv):
@@ -398,7 +426,6 @@ def _Exec(self, argv):
return 0
def RunBuiltin(self, builtin_id, argv):
cflow = EBuiltin.NONE # default value
restore_fd_state = True
# TODO: Just test Type() == COMMAND word, and then if it's a command word,
@@ -424,20 +451,6 @@ def RunBuiltin(self, builtin_id, argv):
# TODO: Should this be turned into our own SystemExit exception?
sys.exit(code)
elif builtin_id == EBuiltin.BREAK:
status = 0
cflow = EBuiltin.BREAK
elif builtin_id == EBuiltin.CONTINUE:
status = 0
cflow = EBuiltin.CONTINUE
elif builtin_id == EBuiltin.RETURN:
# TODO: Hook up the rest of this! Need to know when you're in a function
# body.
status = 0
cflow = EBuiltin.RETURN
elif builtin_id in (EBuiltin.SOURCE, EBuiltin.DOT):
status = self._Source(argv)
@@ -466,7 +479,7 @@ def RunBuiltin(self, builtin_id, argv):
else:
raise AssertionError('Unhandled builtin: %d' % builtin_id)
return status, cflow
return status
def RunFunc(self, func_node, argv):
"""Called by FuncThunk."""
@@ -477,9 +490,16 @@ def RunFunc(self, func_node, argv):
# Redirects still valid for functions.
# Here doc causes a pipe and Process(SubProgramThunk).
status, cflow = self._Execute(func_body)
try:
status = self._Execute(func_body)
except _ControlFlow as e:
if e.IsReturn():
status = e.ReturnValue()
else:
# break/continue used in the wrong place
raise AssertionError('Invalid control flow')
self.mem.Pop()
return status, cflow
return status
def _GetThunkForSimpleCommand(self, argv, more_env):
"""
@@ -508,6 +528,7 @@ def _GetThunkForSimpleCommand(self, argv, more_env):
builtin_id = self.builtins.Resolve(argv[0])
if builtin_id != EBuiltin.NONE:
return BuiltinThunk(self, builtin_id, argv)
func_node = self.funcs.get(argv[0])
if func_node is not None:
return FuncThunk(self, func_node, argv)
@@ -526,9 +547,19 @@ def _GetProcessForNode(self, node):
raise AssertionError("Error evaluating words: %s" % err)
more_env = self.ev.EvalEnv(node.more_env)
if more_env is None:
# TODO:
# TODO: proper error
raise AssertionError()
thunk = self._GetThunkForSimpleCommand(argv, more_env)
elif node.tag == command_e.ControlFlow:
# TODO: Raise _FatalError
# Pipeline or subshells with control flow are invalid, e.g.:
# - break | less
# - continue | less
# - ( return )
# NOTE: This could be done at parse time too.
raise AssertionError('Invalid control flow %s' % node)
else:
thunk = SubProgramThunk(self, node)
@@ -550,8 +581,8 @@ def _EvalRedirects(self, node):
"""
# No redirects
if node.tag in (
command_e.NoOp, command_e.Assignment, command_e.Pipeline,
command_e.AndOr, command_e.CommandList,
command_e.NoOp, command_e.Assignment, command_e.ControlFlow,
command_e.Pipeline, command_e.AndOr, command_e.CommandList,
command_e.Sentence):
return []
@@ -631,7 +662,7 @@ def _RunPipeline(self, node):
else:
status = pipe_status[-1] # last one determines status
return status, EBuiltin.NONE # no control flow
return status
def _Execute(self, node):
"""
@@ -640,23 +671,14 @@ def _Execute(self, node):
"""
redirects = self._EvalRedirects(node)
# TODO: Change this to its own enum?
# or add EBuiltin.THROW _throw? For testing.
# Is this different han exit? exit should really be throw. Because we
# want to be able to unwind the stack, show stats, etc. Exiting in the
# middle is bad.
# exit and _throw could be the same, except _throw takes an error message,
# and exits 1, and shows traceback.
cflow = EBuiltin.NONE
# TODO: Only eval argv[0] once. It can have side effects!
if node.tag == command_e.SimpleCommand:
words = braces.BraceExpandWords(node.words)
argv = self.ev.EvalWords(words)
if argv is None:
self.error_stack.extend(self.ev.Error())
raise _ExecError()
raise _FatalError()
more_env = self.ev.EvalEnv(node.more_env)
if more_env is None:
print(self.error_stack)
@@ -682,7 +704,7 @@ def _Execute(self, node):
for r in redirects:
r.ApplyInParent(self.fd_state)
status, cflow = thunk.RunInParent()
status = thunk.RunInParent()
restore_fd_state = thunk.ShouldRestoreFdState()
# Special case for exec 1>&2 (with no args): we permanently change the
@@ -695,11 +717,11 @@ def _Execute(self, node):
self.fd_state.ForgetAll()
elif node.tag == command_e.Sentence:
# TODO: Compile this away
status, cflow = self._Execute(node.command)
# TODO: Compile this away.
status = self._Execute(node.command)
elif node.tag == command_e.Pipeline:
status, cflow = self._RunPipeline(node)
status = self._RunPipeline(node)
elif node.tag == command_e.Subshell:
# This makes sure we don't waste a process if we'd launch one anyway.
@@ -735,7 +757,7 @@ def _Execute(self, node):
ok, val = self.ev.EvalCompoundWord(pair.rhs)
if not ok:
self.error_stack.extend(self.ev.Error())
raise _ExecError()
raise _FatalError()
pairs.append((pair.lhs, val))
flags = 0 # TODO: Calculate from keyword/flags
@@ -747,40 +769,57 @@ def _Execute(self, node):
# TODO: This should be eval of RHS, unlike bash!
status = 0
elif node.tag == command_e.ControlFlow:
if node.arg_word: # Evaluate the argument
ok, val = self.ev.EvalCompoundWord(node.arg_word)
if not ok:
self.error_stack.extend(self.ev.Error())
raise _FatalError()
is_str, arg_str = val.AsString()
assert is_str
arg = int(arg_str) # They all take integers
else:
arg = 0 # return 0, break 0 levels, etc.
raise _ControlFlow(node.token, arg)
# The only difference between these two is that CommandList has no
# redirects. We already took care of that above.
elif node.tag in (command_e.CommandList, command_e.BraceGroup):
status = 0 # for empty list
for child in node.children:
status, cflow = self._Execute(child) # last status wins
if cflow in (EBuiltin.BREAK, EBuiltin.CONTINUE):
break
status = self._Execute(child) # last status wins
elif node.tag == command_e.AndOr:
#print(node.children)
left, right = node.children
status, cflow = self._Execute(left)
status = self._Execute(left)
if node.op_id == Id.Op_DPipe:
if status != 0:
status, cflow = self._Execute(right)
status = self._Execute(right)
elif node.op_id == Id.Op_DAmp:
if status == 0:
status, cflow = self._Execute(right)
status = self._Execute(right)
else:
raise AssertionError
elif node.tag == command_e.While:
while True:
status, _ = self._Execute(node.cond)
status = self._Execute(node.cond)
if status != 0:
break
status, cflow = self._Execute(node.body) # last one wins
if cflow == EBuiltin.BREAK:
cflow = EBuiltin.NONE # reset since we respected it
break
if cflow == EBuiltin.CONTINUE:
cflow = EBuiltin.NONE # reset since we respected it
try:
status = self._Execute(node.body) # last one wins
except _ControlFlow as e:
if e.IsBreak():
status = 0
break
elif e.IsContinue():
status = 0
continue
else: # return needs to pop up more
raise
elif node.tag == command_e.ForEach:
iter_name = node.iter_name
@@ -793,22 +832,25 @@ def _Execute(self, node):
# NOTE: This expands globs too. TODO: We should pass in a Globber()
# object.
status = 0 # in case we don't loop
cflow = EBuiltin.NONE
for x in iter_list:
self.mem.SetSimpleVar(iter_name, Value.FromString(x))
status, cflow = self._Execute(node.body)
if cflow == EBuiltin.BREAK:
cflow = EBuiltin.NONE # reset since we respected it
break
if cflow == EBuiltin.CONTINUE:
cflow = EBuiltin.NONE # reset since we respected it
try:
status = self._Execute(node.body) # last one wins
except _ControlFlow as e:
if e.IsBreak():
status = 0
break
elif e.IsContinue():
status = 0
continue
else: # return needs to pop up more
raise
elif node.tag == command_e.DoGroup:
# Delegate to command list
# TODO: This should be compiled out!
status, cflow = self._Execute(node.child)
status = self._Execute(node.child)
elif node.tag == command_e.FuncDef:
self.funcs[node.name] = node
@@ -817,14 +859,14 @@ def _Execute(self, node):
elif node.tag == command_e.If:
done = False
for arm in node.arms:
status, _ = self._Execute(arm.cond)
status = self._Execute(arm.cond)
if status == 0:
status, _ = self._Execute(arm.action)
status = self._Execute(arm.action)
done = True
break
# TODO: The compiler should flatten this
if not done and node.else_action is not None:
status, _ = self._Execute(node.else_action)
status = self._Execute(node.else_action)
elif node.tag == command_e.NoOp:
status = 0 # make it true
@@ -843,33 +885,29 @@ def _Execute(self, node):
tb = self.mem.GetTraceback(token)
self._SetException(tb,
"Command %s exited with code %d" % ('TODO', status))
# cflow should be EXCEPT
# TODO: raise _ControlFlow? Except?
# Dummy?
# TODO: Is this the right place to put it? Does it need a stack for
# function calls?
self.mem.last_status = status
return status, cflow
return status
def Execute(self, node):
"""Execute an LST node."""
"""Execute a top level LST node."""
# Use exceptions internally, but exit codes externally.
try:
status, cflow = self._Execute(node)
except _ExecError:
status = self._Execute(node)
except _ControlFlow as e:
# TODO: Make this error message better.
print('Break/continue/return bubbled up to top level', file=sys.stderr)
status = 1
except _FatalError:
# TODO: Nicer runtime error message.
print(self.error_stack, file=sys.stderr)
status = 1
cflow = EBuiltin.NONE
return status, cflow
def ExecuteTop(self, node):
"""
Execute from the top level.
TODO: This is wrong; we can't only check here.
"""
status, cflow = self.Execute(node)
if cflow != EBuiltin.NONE:
print('break / continue can only be used inside loop')
status = 129 # TODO: Fix this. Use correct macros
return status, cflow
# TODO: Hook this up
#print('break / continue can only be used inside loop')
#status = 129 # TODO: Fix this. Use correct macros
return status
View
@@ -355,6 +355,10 @@ def _AddKinds(spec):
# "None" could either be a global variable or assignment to a local.
spec.AddKind('Assign', ['Declare', 'Export', 'Local', 'Readonly', 'None'])
# Unlike bash, we parse control flow statically. They're not
# dynamically-resolved builtins.
spec.AddKind('ControlFlow', ['Break', 'Continue', 'Return'])
# Id -> OperandType
BOOL_OPS = {} # type: dict
View
@@ -54,7 +54,7 @@ def ParseAndExecute(code_str):
print(node)
ex = cmd_exec_test.InitExecutor()
status, cflow = ex.Execute(node)
status = ex.Execute(node)
# TODO: Can we capture output here?
return status
View
@@ -98,7 +98,7 @@ def PrintError(error_stack, arena, f):
# - although parse errors happen at runtime because of 'source'
# - should there be a distinction then?
for parse_error in error_stack:
print(parse_error)
#print(parse_error)
if parse_error.token:
span_id = parse_error.token.span_id
elif parse_error.word:
@@ -121,19 +121,19 @@ def PrintError(error_stack, arena, f):
col = line_span.col
length = line_span.length
print('Line %d of %r' % (line_num+1, path))
print(' ' + line.rstrip())
print('Line %d of %r' % (line_num+1, path), file=f)
print(' ' + line.rstrip(), file=f)
if col == -1:
print('NO COL')
print('NO COL', file=f)
else:
sys.stdout.write(' ')
f.write(' ')
# preserve tabs
for c in line[:col]:
sys.stdout.write('\t' if c == '\t' else ' ')
sys.stdout.write('^')
sys.stdout.write('~' * (length-1))
sys.stdout.write('\n')
f.write('\t' if c == '\t' else ' ')
f.write('^')
f.write('~' * (length-1))
f.write('\n')
print(parse_error.UserErrorString(), file=f)
print('---')
print('---', file=f)
#print(error_stack, file=f)
View
@@ -398,25 +398,27 @@ def LooksLikeAssignment(w):
return name, rhs
# Interpret the words as 4 kinds of ID: Assignment, Arith, Bool, Command.
# TODO: Might need other builtins.
def AssignmentBuiltinId(w):
"""Tests if word is an assignment builtin."""
def KeywordToken(w):
"""Tests if a word is an assignment or control flow word.
Returns:
kind, token
"""
assert w.tag == word_e.CompoundWord
# has to be a single literal part
err = (Kind.Undefined, None)
if len(w.parts) != 1:
return Id.Undefined_Tok, -1
return err
token_type = _LiteralPartId(w.parts[0])
if token_type == Id.Undefined_Tok:
return Id.Undefined_Tok, -1
return err
token_kind = LookupKind(token_type)
if token_kind == Kind.Assign:
return token_type, w.parts[0].token.span_id
if token_kind in (Kind.Assign, Kind.ControlFlow):
return token_kind, w.parts[0].token
return Id.Undefined_Tok, -1
return err
#
View
@@ -9,6 +9,8 @@
cmd_parse.py - Parse high level shell commands.
"""
import sys
from core import base
from core import braces
from core import word
@@ -40,7 +42,7 @@ def _GetHereDocsToFill(node):
# EOF2
# Leaf nodes: no redirects and no children.
if node.tag in (command_e.NoOp, command_e.Assignment):
if node.tag in (command_e.NoOp, command_e.Assignment, command_e.ControlFlow):
return []
# Leaf nodes: these have redirects but not children.
@@ -560,30 +562,57 @@ def ParseSimpleCommand(self):
node.spids.append(left_spid) # no keyword spid to skip past
return node
assign_kw, keyword_spid = word.AssignmentBuiltinId(suffix_words[0])
# NOTE: Could also detect break/continue/return here? Parse time or
# runtime?
if assign_kw == Id.Undefined_Tok:
node = self._MakeSimpleCommand(prefix_bindings, suffix_words, redirects)
kind, kw_token = word.KeywordToken(suffix_words[0])
if kind == Kind.Assign:
if redirects:
self.AddErrorContext('Got redirects in assignment: %s', redirects)
return None
if prefix_bindings: # FOO=bar local spam=eggs not allowed
# Use the location of the first value. TODO: Use the whole word before
# splitting.
_, v0, _ = prefix_bindings[0]
self.AddErrorContext(
'Invalid prefix bindings in assignment: %s', prefix_bindings,
word=v0)
return None
node = self._MakeAssignment(kw_token.id, suffix_words)
if not node: return None
node.spids.append(kw_token.span_id)
return node
if redirects:
# TODO: Make it a warning, or do it in the second stage?
print(
'WARNING: Got redirects in assignment: %s' % redirects, file=sys.stderr)
elif kind == Kind.ControlFlow:
if redirects:
self.AddErrorContext('Got redirects in control flow: %s', redirects)
return None
if prefix_bindings: # FOO=bar local spam=eggs not allowed
# Use the location of the first value. TODO: Use the whole word before
# splitting.
_, v0, _ = prefix_bindings[0]
self.AddErrorContext(
'Invalid prefix bindings in assignment: %s', prefix_bindings,
word=v0)
return None
if prefix_bindings: # FOO=bar local spam=eggs not allowed
# Use the location of the first value. TODO: Use the whole word before
# splitting.
_, v0, _ = prefix_bindings[0]
self.AddErrorContext(
'Invalid prefix bindings in control flow: %s', prefix_bindings,
word=v0)
return None
node = self._MakeAssignment(assign_kw, suffix_words)
if not node: return None
node.spids.append(keyword_spid)
return node
# Attach the token for errors. (Assignment may not need it.)
if len(suffix_words) == 1:
arg_word = None
elif len(suffix_words) == 2:
arg_word = suffix_words[1]
else:
self.AddErrorContext('Too many arguments')
return None
return ast.ControlFlow(kw_token, arg_word)
else:
node = self._MakeSimpleCommand(prefix_bindings, suffix_words, redirects)
return node
def ParseBraceGroup(self):
"""
View
@@ -179,6 +179,10 @@
C('export', Id.Assign_Export),
C('local', Id.Assign_Local),
C('readonly', Id.Assign_Readonly),
C('break', Id.ControlFlow_Break),
C('continue', Id.ControlFlow_Continue),
C('return', Id.ControlFlow_Return),
]
# These two can must be recognized in the OUTER state, but can't nested within
View
@@ -145,6 +145,7 @@ module osh
| Sentence(command command, token terminator)
-- TODO: parse flags -r -x; -a and -A aren't needed
| Assignment(id keyword, assign_pair* pairs)
| ControlFlow(token token, word? arg_word)
| Pipeline(command* children, bool negated, int* stderr_indices)
-- TODO: Should be left and right
| AndOr(command* children, id op_id)
View
@@ -872,7 +872,8 @@ def _ReadCompoundWord(self, eof_type=Id.Undefined_Tok, lex_mode=LexMode.OUTER,
# Keywords like "for" are treated like literals
elif self.token_kind in (
Kind.Lit, Kind.KW, Kind.Assign, Kind.BoolUnary, Kind.BoolBinary):
Kind.Lit, Kind.KW, Kind.Assign, Kind.ControlFlow, Kind.BoolUnary,
Kind.BoolBinary):
if self.token_type == Id.Lit_EscapedChar:
part = ast.EscapedLiteralPart(self.cur_token)
else:
@@ -1038,8 +1039,8 @@ def _ReadWord(self, lex_mode):
return None, True # tell Read() to try again
elif self.token_kind in (
Kind.VSub, Kind.Lit, Kind.Left, Kind.KW, Kind.Assign, Kind.BoolUnary,
Kind.BoolBinary):
Kind.VSub, Kind.Lit, Kind.Left, Kind.KW, Kind.Assign, Kind.ControlFlow,
Kind.BoolUnary, Kind.BoolBinary):
# We're beginning a word. If we see Id.Lit_Pound, change to
# LexMode.COMMENT and read until end of line. (TODO: How to add comments
# to AST?)
View
@@ -79,7 +79,7 @@ run-task-with-status-test() {
test "$(wc -l < _tmp/status.txt)" = '1' || die "Expected only one line"
}
readonly NUM_TASKS=40
readonly NUM_TASKS=400
# TODO:
#
View
14 spec.sh
@@ -217,6 +217,14 @@ builtins-special() {
${REF_SHELLS[@]} $OSH "$@"
}
command-parsing() {
sh-spec tests/command-parsing.test.sh ${REF_SHELLS[@]} $OSH "$@"
}
func-parsing() {
sh-spec tests/func-parsing.test.sh ${REF_SHELLS[@]} $OSH "$@"
}
func() {
sh-spec tests/func.test.sh ${REF_SHELLS[@]} $OSH "$@"
}
@@ -254,7 +262,7 @@ here-doc() {
# - On Debian, the whole process hangs.
# Is this due to Python 3.2 vs 3.4? Either way osh doesn't implement the
# functionality, so it's probably best to just implement it.
sh-spec tests/here-doc.test.sh --osh-failures-allowed 10 --range 1-27 \
sh-spec tests/here-doc.test.sh --osh-failures-allowed 9 --range 1-27 \
${REF_SHELLS[@]} $OSH "$@"
}
@@ -277,7 +285,7 @@ tilde() {
var-sub() {
# NOTE: ZSH has interesting behavior, like echo hi > "$@" can write to TWO
# FILES! But ultimately we don't really care, so I disabled it.
sh-spec tests/var-sub.test.sh --osh-failures-allowed 22 \
sh-spec tests/var-sub.test.sh --osh-failures-allowed 21 \
${REF_SHELLS[@]} $OSH "$@"
}
@@ -310,7 +318,7 @@ arith-context() {
# TODO: array= (a b c) vs array=(a b c). I think LookAheadForOp might still be
# messed up.
array() {
sh-spec tests/array.test.sh --osh-failures-allowed 27 \
sh-spec tests/array.test.sh --osh-failures-allowed 26 \
$BASH $MKSH $OSH "$@"
}
View
@@ -0,0 +1,53 @@
#!/bin/bash
#
# Some nonsensical combinations which can all be detected at PARSE TIME.
# All shells allow these, but right now OSH disallowed.
# TODO: Run the parser on your whole corpus, and then if there are no errors,
# you should make OSH the OK behavior, and others are OK.
### Prefix env on assignment
f() {
# NOTE: local treated like a special builtin!
E=env local v=var
echo $E $v
}
f
# status: 0
# stdout: env var
# OK bash stdout: var
# OK osh status: 2
# OK osh stdout-json: ""
### Redirect on assignment
f() {
# NOTE: local treated like a special builtin!
local E=env > _tmp/r.txt
}
rm -f _tmp/r.txt
f
test -f _tmp/r.txt && echo REDIRECTED
# status: 0
# stdout: REDIRECTED
# OK osh status: 2
# OK osh stdout-json: ""
### Prefix env on control flow
for x in a b c; do
echo $x
E=env break
done
# status: 0
# stdout: a
# OK osh status: 2
# OK osh stdout-json: ""
### Redirect on control flow
rm -f _tmp/r.txt
for x in a b c; do
break > _tmp/r.txt
done
test -f _tmp/r.txt && echo REDIRECTED
# status: 0
# stdout: REDIRECTED
# OK osh status: 2
# OK osh stdout-json: ""
View
@@ -0,0 +1,97 @@
#!/bin/bash
### Incomplete Function
# code: foo()
# status: 2
# BUG mksh status: 0
### Incomplete Function 2
# code: foo() {
# status: 2
# OK mksh status: 1
### Bad function
# code: foo(ls)
# status: 2
# OK mksh status: 1
### Unbraced function body.
# dash allows this, but bash does not. The POSIX grammar might not allow
# this? Because a function body needs a compound command.
# function_body : compound_command
# | compound_command redirect_list /* Apply rule 9 */
# code: one_line() ls; one_line;
### Function with spaces, to see if ( and ) are separate tokens.
# NOTE: Newline after ( is not OK.
func ( ) { echo in-func; }; func
# stdout: in-func
### subshell function
# bash allows this.
i=0
j=0
inc() { i=$((i+5)); }
inc_subshell() ( j=$((j+5)); )
inc
inc_subshell
echo $i $j
# stdout: 5 0
### Hard case, function with } token in it
rbrace() { echo }; }; rbrace
# stdout: }
### . in function name
# bash accepts; dash doesn't
func-name.ext ( ) { echo func-name.ext; }
func-name.ext
# stdout: func-name.ext
# OK dash status: 2
# OK dash stdout-json: ""
### = in function name
# WOW, bash is so lenient. foo=bar is a command, I suppose. I think I'm doing
# to disallow this one.
func-name=ext ( ) { echo func-name=ext; }
func-name=ext
# stdout: func-name=ext
# OK dash status: 2
# OK dash stdout-json: ""
# OK mksh status: 1
# OK mksh stdout-json: ""
### Function name with $
# bash allows this; dash doesn't.
foo$bar() { ls ; }
# status: 2
# OK bash/mksh status: 1
### Function name with !
# bash allows this; dash doesn't.
foo!bar() { ls ; }
# status: 0
# OK dash status: 2
### Function name with -
# bash allows this; dash doesn't.
foo-bar() { ls ; }
# status: 0
# OK dash status: 2
### Break after ) is OK.
# newline is always a token in "normal" state.
echo hi; func ( )
{ echo in-func; }
func
# stdout-json: "hi\nin-func\n"
### Nested definition
# A function definition is a command, so it can be nested
func() {
nested_func() { echo nested; }
nested_func
}
func
# stdout: nested
View
136 tests/func.test.sh 100755 → 100644
@@ -1,97 +1,49 @@
#!/bin/bash
### Incomplete Function
# code: foo()
# status: 2
# BUG mksh status: 0
### Incomplete Function 2
# code: foo() {
# status: 2
# OK mksh status: 1
### Bad function
# code: foo(ls)
# status: 2
# OK mksh status: 1
### Unbraced function body.
# dash allows this, but bash does not. The POSIX grammar might not allow
# this? Because a function body needs a compound command.
# function_body : compound_command
# | compound_command redirect_list /* Apply rule 9 */
# code: one_line() ls; one_line;
### Function with spaces, to see if ( and ) are separate tokens.
# NOTE: Newline after ( is not OK.
func ( ) { echo in-func; }; func
# stdout: in-func
### subshell function
# bash allows this.
i=0
j=0
inc() { i=$((i+5)); }
inc_subshell() ( j=$((j+5)); )
inc
inc_subshell
echo $i $j
# stdout: 5 0
### Hard case, function with } token in it
rbrace() { echo }; }; rbrace
# stdout: }
### . in function name
# bash accepts; dash doesn't
func-name.ext ( ) { echo func-name.ext; }
func-name.ext
# stdout: func-name.ext
# OK dash status: 2
# OK dash stdout-json: ""
### = in function name
# WOW, bash is so lenient. foo=bar is a command, I suppose. I think I'm doing
# to disallow this one.
func-name=ext ( ) { echo func-name=ext; }
func-name=ext
# stdout: func-name=ext
# OK dash status: 2
# OK dash stdout-json: ""
# OK mksh status: 1
# OK mksh stdout-json: ""
### Function name with $
# bash allows this; dash doesn't.
foo$bar() { ls ; }
# status: 2
# OK bash/mksh status: 1
### Function name with !
# bash allows this; dash doesn't.
foo!bar() { ls ; }
# status: 0
# OK dash status: 2
### Function name with -
# bash allows this; dash doesn't.
foo-bar() { ls ; }
# status: 0
# OK dash status: 2
### Break after ) is OK.
# newline is always a token in "normal" state.
echo hi; func ( )
{ echo in-func; }
func
# stdout-json: "hi\nin-func\n"
### Locals don't leak
f() {
local f_var=f_var
}
f
echo $f_var
# stdout:
### Nested definition
# A function definition is a command, so it can be nested
func() {
nested_func() { echo nested; }
nested_func
### Globals leak
f() {
f_var=f_var
}
func
# stdout: nested
f
echo $f_var
# stdout: f_var
### Return statement
f() {
echo one
return 42
echo two
}
f
# stdout: one
# status: 42
### Return at top level is error
return
echo bad
# N-I dash/mksh status: 0
# N-I bash status: 0
# N-I bash stdout: bad
# status: 1
# stdout-json: ""
### Dynamic Scope
f() {
echo $g_var
}
g() {
local g_var=g_var
f
}
g
# stdout: g_var
View
@@ -49,22 +49,46 @@ func() {
echo $i
}
func
# status: 0
# stdout-json: "a\nb\nc\nc\n"
### continue
for i in a b c; do
echo $i
continue
if test $i = b; then
continue
fi
echo $i
done
# stdout-json: "a\nb\nc\n"
# status: 0
# stdout-json: "a\na\nb\nc\nc\n"
### break
for i in a b c; do
echo $i
break
if test $i = b; then
break
fi
done
# stdout: a
# status: 0
# stdout-json: "a\nb\n"
### continue at top level is error
# NOTE: bash and mksh both print warnings, but don't exit with an error.
continue
echo bad
# N-I bash/dash/mksh status: 0
# N-I bash/dash/mksh stdout: bad
# stdout-json: ""
# status: 1
### break at top level is error
break
echo bad
# N-I bash/dash/mksh status: 0
# N-I bash/dash/mksh stdout: bad
# stdout-json: ""
# status: 1
### while in while condition
# This is a consequence of the grammar