View
@@ -0,0 +1,116 @@
#!/usr/bin/python
from __future__ import print_function
"""
main_loop.py
main_loop.Interactive()
main_loop.Batch()
They should call
ex.ExecuteOne() -- and catch FatalRuntimeError or ControlFlow?
For interactive, you keep going on an error, but exit on 'exit'
For batch, you fail at the first error
Get rid of:
ex.Execute() -- used for ExitTrap
ex.ExecuteAndCatch()
ParseWholeFile -- needs to check the here doc.
"""
from core import ui
from core import util
log = util.log
def Interactive(opts, ex, c_parser, arena):
status = 0
while True:
# Reset internal newline state. NOTE: It would actually be correct to
# reinitialize all objects (except Env) on every iteration.
c_parser.Reset()
c_parser.ResetInputObjects()
try:
node = c_parser.ParseOne()
except util.ParseError as e:
ui.PrettyPrintError(e, arena)
# NOTE: This should set the status interactively! Bash does this.
status = 2
continue
if node is None: # EOF
# NOTE: We don't care if there are pending here docs in the interative case.
break
is_control_flow, is_fatal = ex.ExecuteAndCatch(node)
status = ex.LastStatus()
if is_control_flow: # e.g. 'exit' in the middle of a script
break
if is_fatal: # e.g. divide by zero
continue
if opts.print_status:
print('STATUS', repr(status))
if ex.MaybeRunExitTrap():
return ex.LastStatus()
else:
return status # could be a parse error
def Batch(opts, ex, c_parser, arena, nodes_out=None):
"""Loop for batch execution.
Args:
nodes_out: if set to a list, the input lines are parsed, and LST nodes are
appended to it instead of executed. For 'sh -n'.
Can this be combined with interative loop? Differences:
- Handling of parse errors.
- Have to detect here docs at the end?
Not a problem:
- Get rid of --print-status and --show-ast for now
- Get rid of EOF difference
TODO:
- Do source / eval need this?
- 'source' needs to parse incrementally so that aliases are respected
- I doubt 'eval' does! You can test it.
- In contrast, 'trap' should parse up front?
- What about $() ?
"""
status = 0
while True:
try:
node = c_parser.ParseOne() # can raise ParseError
if node is None: # EOF
c_parser.CheckForPendingHereDocs() # can raise ParseError
break
except util.ParseError as e:
ui.PrettyPrintError(e, arena)
status = 2
break
if nodes_out is not None:
nodes_out.append(node)
continue
#log('parsed %s', node)
is_control_flow, is_fatal = ex.ExecuteAndCatch(node)
status = ex.LastStatus()
# e.g. divide by zero or 'exit' in the middle of a script
if is_control_flow or is_fatal:
break
if ex.MaybeRunExitTrap():
return ex.LastStatus()
else:
return status # could be a parse error
View
@@ -368,8 +368,8 @@ def Run(self):
# NOTE: may NOT return due to exec().
if self.disable_errexit:
self.ex.exec_opts.errexit.Disable()
status = self.ex.Execute(self.node, fork_external=False)
sys.exit(status) # Must exit!
self.ex.ExecuteAndCatch(self.node, fork_external=False)
sys.exit(self.ex.LastStatus()) # Must exit!
class _HereDocWriterThunk(Thunk):
View
@@ -13,7 +13,11 @@
import sys
from asdl import const
from asdl import encode
from asdl import format as fmt
from core import word
from osh import ast_lib
from osh.meta import ast
def Clear():
@@ -144,3 +148,39 @@ def PrintErrorStack(error_stack, arena, f=sys.stderr):
for err in error_stack:
PrettyPrintError(err, arena, f=f)
print('---', file=f)
def PrintAst(nodes, opts):
if len(nodes) == 1:
node = nodes[0]
else:
node = ast.CommandList(nodes)
if opts.ast_format == 'none':
print('AST not printed.', file=sys.stderr)
elif opts.ast_format == 'oheap':
# TODO: Make this a separate flag?
if sys.stdout.isatty():
raise RuntimeError('ERROR: Not dumping binary data to a TTY.')
f = sys.stdout
enc = encode.Params()
out = encode.BinOutput(f)
encode.EncodeRoot(node, enc, out)
else: # text output
f = sys.stdout
if opts.ast_format in ('text', 'abbrev-text'):
ast_f = fmt.DetectConsoleOutput(f)
elif opts.ast_format in ('html', 'abbrev-html'):
ast_f = fmt.HtmlOutput(f)
else:
raise AssertionError
abbrev_hook = (
ast_lib.AbbreviateNodes if 'abbrev-' in opts.ast_format else None)
tree = fmt.MakeTree(node, abbrev_hook=abbrev_hook)
ast_f.FileHeader()
fmt.PrintTree(tree, ast_f)
ast_f.FileFooter()
ast_f.write('\n')
View
@@ -1322,26 +1322,32 @@ def ParseAndOr(self):
node = ast.AndOr(ops, children)
return node
def ParseCommandLine(self):
"""
NOTE: This is only called in InteractiveLoop. Oh crap I need to really
read and execute a line at a time then?
"""
NOTE: ParseCommandLine and ParseCommandTerm are similar, but different.
At the top level, We want to execute after every line:
- to process alias
- to process 'exit', because invalid syntax might appear after it
BUG: sleep 1 & sleep 1 & doesn't work here, when written in REPL. But it
does work with '-c', because that calls ParseFile and not ParseCommandLine
over and over.
But for say a while loop body, we want to parse the whole thing at once, and
then execute it. We don't want to parse it over and over again!
TODO: Get rid of ParseFile and stuff? Shouldn't be used for -c and so
forth. Just have an ExecuteLoop for now. But you still need
ParseCommandList, for internal nodes.
COMPARE
command_line : and_or (sync_op and_or)* trailer? ; # TOP LEVEL
command_term : and_or (trailer and_or)* ; # CHILDREN
"""
def ParseCommandLine(self):
"""
Called from the top level InteractiveLoop or ExecutionLoop.
command_line : and_or (sync_op and_or)* trailer? ;
trailer : sync_op newline_ok
| NEWLINES;
sync_op : '&' | ';';
This rule causes LL(k > 1) behavior. We would have to peek to see if there
is another command word after the sync op.
NOTE: This rule causes LL(k > 1) behavior. We would have to peek to see if
there is another command word after the sync op.
But it's easier to express imperatively. Do the following in a loop:
1. ParseAndOr
@@ -1350,12 +1356,6 @@ def ParseCommandLine(self):
line.)
b. If there's a sync_op, process it. Then look for a newline and
return. Otherwise, parse another AndOr.
COMPARE
command_line : and_or (sync_op and_or)* trailer? ; # TOP LEVEL
command_term : and_or (trailer and_or)* ; # CHILDREN
I think you should be able to factor these out.
"""
children = []
done = False
@@ -1384,7 +1384,30 @@ def ParseCommandLine(self):
children.append(child)
return ast.CommandList(children)
# Simplify the AST.
if len(children) > 1:
return ast.CommandList(children)
else:
return children[0]
def ParseOne(self):
"""A wrapper around ParseCommandLine for main_loop.
We want to be able catch ParseError all in one place.
Raises:
ParseError
"""
self._NewlineOk()
self._Peek()
if self.c_id == Id.Eof_Real:
# TODO: Assert that there are no pending here docs
return None
node = self.ParseCommandLine()
assert node is not None
return node
def ParseCommandTerm(self):
""""
@@ -1405,14 +1428,12 @@ def ParseCommandTerm(self):
Id.Eof_Real, Id.Eof_RParen, Id.Eof_Backtick, Id.Right_Subshell,
Id.Lit_RBrace, Id.Op_DSemi)
# NOTE: This is similar to ParseCommandLine, except there is a lot of stuff
# about here docs. Here docs are inherently line-oriented.
# NOTE: This is similar to ParseCommandLine.
#
# - Why aren't we doing END_LIST in ParseCommandLine?
# - Because you will never be inside $() at the top level.
# - We also know it will end in a newline. It can't end in "fi"!
# - example: if true; then { echo hi; } fi
# - Why aren't we doing 'for c in children' too?
children = []
done = False
@@ -1448,7 +1469,7 @@ def ParseCommandTerm(self):
# Test if we should keep going. There might be another command after
# the semi and newline.
self._Peek()
if self.c_id in END_LIST:
if self.c_id in END_LIST: # \n EOF
done = True
elif self.c_id in END_LIST: # ; EOF
@@ -1482,6 +1503,13 @@ def ParseCommandList(self):
assert node is not None
return node
def CheckForPendingHereDocs(self):
# NOTE: This happens when there is no newline at the end of a file, like
# osh -c 'cat <<EOF'
if self.pending_here_docs:
node = self.pending_here_docs[0] # Just show the first one?
p_die('Unterminated here doc began here', word=node.here_begin)
def ParseWholeFile(self):
"""Entry point for main() in non-interactive shell.
View
@@ -74,11 +74,7 @@ def assertParseAndOr(test, code_str):
return _assertParseMethod(test, code_str, 'ParseAndOr')
def assertParseCommandLine(test, code_str):
node = _assertParseMethod(test, code_str, 'ParseCommandLine')
if len(node.children) == 1:
return node.children[0]
else:
return node
return _assertParseMethod(test, code_str, 'ParseCommandLine')
def assertParseCommandList(test, code_str):
node = _assertParseMethod(test, code_str, 'ParseCommandList')
View
@@ -273,3 +273,32 @@ LBRACE echo one; echo two; }
one
two
## END
#### Alias not respected inside $()
# This could be parsed correctly, but it is only defined in a child process.
shopt -s expand_aliases
echo $(alias sayhi='echo hello')
sayhi
## status: 127
#### Alias doesn't work on a single line!
alias sayhi='echo hello'; sayhi same line
sayhi other line
## STDOUT:
hello other line
## END
## BUG bash stdout-json: ""
## BUG bash status: 127
#### Alias is respected inside eval
shopt -s expand_aliases
eval "alias sayhi='echo hello'
sayhi inside"
sayhi outside
## STDOUT:
hello inside
hello outside
## END
## BUG zsh STDOUT:
hello outside
## END
View
@@ -53,9 +53,11 @@ echo status=$?
#### trap EXIT
cleanup() {
echo "cleanup [$@]"
exit 42
}
trap 'cleanup x y z' EXIT
## stdout: cleanup [x y z]
## status: 42
#### trap DEBUG
debuglog() {
@@ -139,3 +141,16 @@ err [x y] 1
2
3
## END
#### trap with PARSE error (implicit exit)
trap 'echo FAILED' EXIT
for
## stdout: FAILED
## status: 2
## OK mksh status: 1
#### trap with PARSE error with explicit exit
trap 'echo FAILED; exit 0' EXIT
for
## stdout: FAILED
## status: 0
View
@@ -37,6 +37,7 @@ _compare() {
if test $expected_status != $osh_status; then
echo "FAIL: Got status $osh_status but expected $expected_status"
echo "in test case: $@"
exit 1
fi
View
@@ -94,6 +94,7 @@ EOF
}
osh-interactive() {
set +o errexit
echo 'echo hi' | $OSH -i
echo 'exit' | $OSH -i
View
@@ -232,7 +232,7 @@ blog-other1() {
}
alias() {
sh-spec spec/alias.test.sh --osh-failures-allowed 18 \
sh-spec spec/alias.test.sh --osh-failures-allowed 20 \
${REF_SHELLS[@]} $ZSH $OSH_LIST "$@"
}
@@ -385,7 +385,7 @@ explore-parsing() {
}
parse-errors() {
sh-spec spec/parse-errors.test.sh --osh-failures-allowed 5 \
sh-spec spec/parse-errors.test.sh --osh-failures-allowed 4 \
${REF_SHELLS[@]} $OSH_LIST "$@"
}
View
@@ -1,6 +1,13 @@
# A list of files that are known not to conform to the OSH language.
# `# comment` trick doesn't parse because comments go to EOL. Could change
# this?
cloud/sandstorm/install.sh
distro/crankshaft/rpi_buildsystem/7_build_gpio2kbd.sh
esoteric/setup.shl/bin/setup # associative array ambiguity (key should be quoted)
exp/shootout/timing.sh # the $(( )) ambiguity
linux-4.8.7/tools/perf/perf-with-kcore.sh # another $(( )) ambiguity