View
@@ -8,6 +8,9 @@
"""
reader.py - Read lines of input.
"""
import cStringIO
from core import util
@@ -18,6 +21,9 @@ def __init__(self, arena):
def GetLine(self):
line = self._GetLine()
if line is None:
return -1, None
if self.arena:
line_id = self.arena.AddLine(line, self.line_num)
else:
@@ -55,32 +61,34 @@ def Reset(self):
# free vector...
class StringLineReader(_Reader):
class FileLineReader(_Reader):
"""For -c and stdin?"""
def __init__(self, s, arena=None):
def __init__(self, f, arena=None):
"""
Args:
lines: List of (line_id, line) pairs
"""
_Reader.__init__(self, arena)
self.lines = s.splitlines(True)
self.pos = 0
self.f = f
def _GetLine(self):
if self.pos == len(self.lines):
line = self.f.readline()
if not line:
return None
line = self.lines[self.pos]
# The last line should be passed to the Lexer with a '\n', even if it
# didn't have one.
if not line.endswith('\n'):
line += '\n'
self.pos += 1
return line
def StringLineReader(s, arena=None):
return FileLineReader(cStringIO.StringIO(s), arena=arena)
# C++ ownership notes:
# - used for file input (including source)
# - used for -c arg (NUL terminated, likely no newline)
@@ -91,18 +99,20 @@ def _GetLine(self):
# internally.
class VirtualLineReader(object):
class VirtualLineReader(_Reader):
"""Used for here docs."""
def __init__(self, lines):
def __init__(self, lines, arena):
"""
Args:
lines: List of (line_id, line) pairs
"""
_Reader.__init__(self, arena)
self.lines = lines
self.num_lines = len(lines)
self.pos = 0
def GetLine(self):
if self.pos == len(self.lines):
if self.pos == self.num_lines:
return -1, None
line_id, line = self.lines[self.pos]
View
@@ -0,0 +1,50 @@
#!/usr/bin/python -S
"""
reader_test.py: Tests for reader.py
"""
import cStringIO
import unittest
from core import alloc
from core import reader # module under test
class ReaderTest(unittest.TestCase):
def setUp(self):
self.pool = alloc.Pool()
#self.arena = pool.NewArena()
def testStringLineReader(self):
# No Arena, gives -1
r = reader.StringLineReader('one\ntwo')
self.assertEqual((-1, 'one\n'), r.GetLine())
self.assertEqual((-1, 'two\n'), r.GetLine())
self.assertEqual((-1, None), r.GetLine())
def testLineReadersAreEquivalent(self):
a1 = self.pool.NewArena()
r1 = reader.StringLineReader('one\ntwo', arena=a1)
a2 = self.pool.NewArena()
f = cStringIO.StringIO('one\ntwo')
r2 = reader.FileLineReader(f, arena=a2)
a3 = self.pool.NewArena()
lines = [(0, 'one\n'), (1, 'two\n')]
r3 = reader.VirtualLineReader(lines, a3)
for a in [a1, a2, a3]:
a.PushSource('reader_test.py')
for r in [r1, r2, r3]:
print(r)
# Lines are added to the arena with a line_id.
self.assertEqual((0, 'one\n'), r.GetLine())
self.assertEqual((1, 'two\n'), r.GetLine())
self.assertEqual((-1, None), r.GetLine())
if __name__ == '__main__':
unittest.main()
View
@@ -102,10 +102,11 @@ class CommandParser(object):
lexer: for lookahead in function def, PushHint of ()
line_reader: for here doc
"""
def __init__(self, w_parser, lexer, line_reader):
def __init__(self, w_parser, lexer, line_reader, arena=None):
self.w_parser = w_parser # for normal parsing
self.lexer = lexer # for fast lookahead to (, for function defs
self.line_reader = line_reader # for here docs
self.arena = arena
self.Reset()
@@ -234,10 +235,11 @@ def _MaybeReadHereDocs(self, node):
# TODO: Move this import
from osh import parse_lib
# TODO: Thread arena. need self.arena
w_parser = parse_lib.MakeWordParserForHereDoc(lines)
w_parser = parse_lib.MakeWordParserForHereDoc(lines, self.arena)
word = w_parser.ReadHereDocBody()
if not word:
self.AddErrorContext('Error reading here doc body: %s', w_parser.Error())
self.AddErrorContext(
'Error reading here doc body: %s', w_parser.Error())
return False
h.body = word
h.was_filled = True
View
@@ -25,7 +25,7 @@
def InitCommandParser(code_str):
pool = Pool()
arena = pool.NewArena()
arena.AddSourcePath('<unit test>')
arena.PushSource('<unit test>')
line_reader, lexer = parse_lib.InitLexer(code_str, arena=arena)
w_parser = WordParser(lexer, line_reader)
c_parser = CommandParser(w_parser, lexer, line_reader)
View
@@ -39,6 +39,7 @@
module osh
{
-- A portion of a line, used for error messages.
-- TODO: Need arena_id, for different files
line_span = (int line_id, int col, int length)
-- A primitive token. NOTE: val is redundant with 'loc' for now. If we
View
@@ -20,26 +20,40 @@ def InitLexer(s, arena=None):
return line_reader, lx
# New API:
# - MakeParser(reader, arena) - for top level, 'source'
# - eval: MakeParser(StringLineReader(), arena)
# - source: MakeParser(FileLineReader(), arena)
# - MakeParserForCommandSub(reader, lexer) -- arena is inside lexer/reader
# - MakeParserForCompletion(code_str) # no arena? no errors?
# - MakeWordParserForHereDoc(lines, arena) # arena is lost
# - althoguh you want to AddLine
# - line_id = arena.AddLine()
# TODO:
# - Does it make sense to create ParseState objects? They have no dependencies
# -- just pure data. Or just recreate them every time? One issue is that
# you need somewhere to store the side effects -- errors for parsers, and the
# actual values for the evaluators/executors.
def MakeParserForTop(line_reader, arena=None):
def MakeParser(line_reader, arena):
"""Top level parser."""
# AtEnd() is true
line_lexer = lexer.LineLexer(lex.LEXER_DEF, '', arena=arena)
lx = lexer.Lexer(line_lexer, line_reader)
w_parser = word_parse.WordParser(lx, line_reader)
c_parser = cmd_parse.CommandParser(w_parser, lx, line_reader)
c_parser = cmd_parse.CommandParser(w_parser, lx, line_reader, arena=arena)
return w_parser, c_parser
# TODO: We could reuse w_parser with Reset() each time. That's what the REPL
# does.
# But LineLexer and Lexer are also stateful! So that might not be worth it.
# Hm the REPL only does line_reader.Reset()?
#
# NOTE: It probably needs to take a VirtualLineReader for $PS1, $PS2, ...
# values.
def MakeParserForCompletion(code_str):
"""Parser for partial lines."""
# NOTE: We don't need to use a arena here? Or we need a "scratch arena" that
@@ -52,25 +66,21 @@ def MakeParserForCompletion(code_str):
return w_parser, c_parser
def MakeParserForExecutor(code_str):
"""Parser for source / eval."""
_, c_parser = MakeParserForCompletion(code_str)
return c_parser
# TODO: This has to take an arena so it gets the spans.
def MakeWordParserForHereDoc(lines):
line_reader = reader.VirtualLineReader(lines)
line_lexer = lexer.LineLexer(lex.LEXER_DEF, '')
def MakeWordParserForHereDoc(lines, arena):
line_reader = reader.VirtualLineReader(lines, arena)
line_lexer = lexer.LineLexer(lex.LEXER_DEF, '', arena=arena)
lx = lexer.Lexer(line_lexer, line_reader)
return word_parse.WordParser(lx, line_reader)
def MakeParserForCommandSub(line_reader, lexer):
"""To parse command sub, we want a fresh word parser state."""
# new instance based on same lexer
"""To parse command sub, we want a fresh word parser state.
It's a new instance based on same lexer and arena.
"""
arena = line_reader.arena
w_parser = word_parse.WordParser(lexer, line_reader)
c_parser = cmd_parse.CommandParser(w_parser, lexer, line_reader)
c_parser = cmd_parse.CommandParser(w_parser, lexer, line_reader, arena)
return c_parser
View
@@ -27,6 +27,7 @@
def _InitWordParserWithArena(s):
pool = alloc.Pool()
arena = pool.NewArena()
arena.PushSource('word_parse_test.py')
line_reader, lexer = parse_lib.InitLexer(s, arena=arena)
w_parser = WordParser(lexer, line_reader)
return arena, w_parser
View
@@ -62,6 +62,11 @@ pwd
# status: 0
# N-I dash/mksh status: 127
### Eval
eval "a=3"
echo $a
# stdout: 3
### Source
lib=$TMP/spec-test-lib.sh
echo 'LIBVAR=libvar' > $lib
View
@@ -63,11 +63,6 @@ EOF
wc -c _tmp/smoke1.txt
# stdout: 8 _tmp/smoke1.txt
### Eval
eval "a=3"
echo $a
# stdout: 3
### "$@" "$*"
func () {
argv.py "$@" "$*"
View
@@ -5,6 +5,28 @@
#
# Run with bash/dash/mksh/zsh.
#
# PARSE ERRORS
#
source_bad_syntax() {
cat >_tmp/bad-syntax.sh <<EOF
if foo; echo ls; fi
EOF
. _tmp/bad-syntax.sh
}
# NOTE:
# - bash correctly reports line 25 (24 would be better)
# - mksh: no line number
# - zsh: line 2 of eval, which doesn't really help.
# - dash: ditto, line 2 of eval
eval_bad_syntax() {
local code='if foo; echo ls; fi'
eval "echo --
$code"
}
#
# COMMAND ERRORS
#
@@ -16,6 +38,26 @@ no_such_command() {
echo 'SHOULD NOT GET HERE'
}
no_such_command_commandsub() {
set -o errexit
echo $(ZZZZZ)
echo 'SHOULD NOT GET HERE'
}
no_such_command_heredoc() {
set -o errexit
# Note: bash gives the line of the beginning of the here doc! Not the actual
# line.
# TODO: osh doesn't give any psition info.
cat <<EOF
one
$(ZZZZZ)
three
EOF
echo 'SHOULD NOT GET HERE'
}
failed_command() {
set -o errexit
false
@@ -33,7 +75,7 @@ pipefail() {
echo 'SHOULD NOT GET HERE'
}
pipefail-func() {
pipefail_func() {
set -o errexit -o pipefail
f() {
cat
@@ -48,23 +90,23 @@ pipefail-func() {
# TODO: point to {. It's the same sas a subshell so you don't know exactly
# which command failed.
pipefail-group() {
pipefail_group() {
set -o errexit -o pipefail
echo hi | { cat; sh -c 'exit 42'; } | wc
echo 'SHOULD NOT GET HERE'
}
# TODO: point to (
pipefail-subshell() {
pipefail_subshell() {
set -o errexit -o pipefail
echo hi | (cat; sh -c 'exit 42') | wc
echo 'SHOULD NOT GET HERE'
}
# TODO: point to 'while'
pipefail-while() {
pipefail_while() {
set -o errexit -o pipefail
seq 3 | while true; do
read line
@@ -78,7 +120,7 @@ pipefail-while() {
}
# Multiple errors from multiple processes
pipefail-multiple() {
pipefail_multiple() {
set -o errexit -o pipefail
{ echo 'four'; sh -c 'exit 4'; } |
{ echo 'five'; sh -c 'exit 5'; } |
@@ -195,8 +237,9 @@ all() {
_run_test control_flow
for t in \
no_such_command failed_command \
pipefail pipefail-group pipefail-subshell pipefail-func pipefail-while \
no_such_command no_such_command_commandsub no_such_command_heredoc \
failed_command \
pipefail pipefail_group pipefail_subshell pipefail_func pipefail_while \
nonexistent nounset \
nounset_arith divzero divzero_var \
string_to_int_arith string_to_hex string_to_octal \