Skip to content

Commit fbaeb48

Browse files
committed
Assignments/redirects identified during parsing
We were half-parsing these (and redirects) during the construction of the Command object. Do this during parsing instead; it's required to identify heredocs.
1 parent 5625eb3 commit fbaeb48

File tree

4 files changed

+57
-33
lines changed

4 files changed

+57
-33
lines changed

psh/model.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import attr
12
import contextlib
23
import fcntl
34
import io
@@ -106,6 +107,18 @@ def __getitem__(self, key):
106107
return super().__getitem__(key)
107108

108109

110+
@attr.s
111+
class Assignment:
112+
var = attr.ib()
113+
expr = attr.ib()
114+
115+
@staticmethod
116+
def run(env, assignments):
117+
for a in assignments:
118+
assert isinstance(a.var, str)
119+
env[str(a.var)] = a.expr.evaluate(env)
120+
121+
109122
class ConstantString(Comparable):
110123
""" An uninterpreted piece of string """
111124
def __init__(self, s, *args, **kwargs):
@@ -350,27 +363,9 @@ def __init__(self, *args, **kwargs):
350363
super().__init__(*args, **kwargs)
351364
# Extract assignments and redirects
352365
self.assignments = []
353-
items = []
354-
might_be_assignment = True
355-
356-
for item in self:
357-
if isinstance(item, Word):
358-
if might_be_assignment and item.matches_assignment():
359-
self.with_assignment(item)
360-
continue
361-
might_be_assignment = False
362-
363-
redirect = Redirect.from_word(item)
364-
if redirect is not None:
365-
self.with_redirect(redirect)
366-
continue
367-
368-
items.append(item)
369-
370-
self[:] = items
371366

372-
def with_assignment(self, assignment):
373-
self.assignments.append(assignment)
367+
def with_assignment(self, *assignment):
368+
self.assignments.extend(assignment)
374369
return self
375370

376371
def is_null(self):
@@ -379,9 +374,7 @@ def is_null(self):
379374
def execute(self, env, input=None, output=None, error=None):
380375
LOG.debug("executing: %s", self)
381376
assert env.permit_execution
382-
for var, _, *rest in self.assignments:
383-
assert isinstance(var, Id)
384-
env[str(var)] = Word(rest).evaluate(env)
377+
Assignment.run(env, self.assignments)
385378
if env.permit_execution and len(self) > 0:
386379
args = [item.evaluate(env) for item in self]
387380
if args[0] in env.builtins:

psh/parser.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,57 @@
11
from functools import partial
2-
from parsy import eof, regex, generate, string, whitespace, ParseError, fail, seq, success, string_from, eof, any_char
3-
from .model import (ConstantString, Token, Id, VarRef, Word, Arith,
2+
from parsy import eof, regex, generate, string, ParseError, fail, seq, success, string_from, eof, any_char
3+
from parsy_extn import monkeypatch_parsy
4+
5+
from .model import (ConstantString, Token, Id, VarRef, Word, Arith, Assignment,
46
Command, CommandSequence, CommandPipe, While, If, Function,
5-
RedirectFrom, RedirectTo, RedirectDup,
7+
Redirect, RedirectFrom, RedirectTo, RedirectDup,
68
MaybeDoubleQuoted,)
79

810

11+
# All our parser use may need to carry notes forward.
12+
# This cost is only paid if we actually muck around with heredocs.
13+
monkeypatch_parsy()
14+
15+
916
ws = regex('([ \t]|\\\\\n)+')
1017
eol = string('\n')
1118

19+
whitespace = ws | eol
20+
21+
1222
# End of statement
1323
EOF = object()
14-
eos = ws.optional() >> (regex('[;\n]') | eof.result(EOF))
24+
eos = ws.optional() >> (string(";") | eol | eof.result(EOF))
1525

1626
@generate("command")
1727
def command():
1828
words = []
29+
assignments = []
30+
redirs = []
31+
assignments_possible = True
1932
while True:
2033
yield ws.optional()
21-
w = yield word
34+
if assignments_possible:
35+
w = yield assignment | redirect | word
36+
else:
37+
w = yield redirect | word
38+
if isinstance(w, Word):
39+
assignments_possible = False
40+
elif isinstance(w, Redirect):
41+
redirs.append(w)
42+
continue
43+
elif isinstance(w, Assignment):
44+
assignments.append(w)
45+
continue
46+
2247
if not w:
2348
break
2449
if len(words) == 0 and w.matches_reserved("while", "do", "done", "if", "then", "elif", "else", "fi"):
2550
return fail("can't have a reserved word here")
2651

2752
words.append(w)
2853

29-
cmd = Command(words)
54+
cmd = Command(words).with_assignment(*assignments).with_redirect(*redirs)
3055
return cmd
3156

3257

@@ -224,6 +249,7 @@ def expr_add():
224249
lambda x: x[0] if len(x) == 1 and isinstance(x[0], Word) else
225250
Word([i for i in x if i != Token("")]))
226251

252+
assignment = seq(variable_id, string("="), word).map(lambda vew: Assignment(vew[0], vew[2]))
227253

228254
redirect_dup_from_n = seq(regex("[0-9]+"), string("<&") >> word).combine(RedirectDup)
229255
redirect_dup_from = (string("<&") >> word).map(partial(RedirectDup, 0))

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def read_file(fn):
2525

2626
install_requires=[
2727
"argcomplete",
28+
"attrs",
2829
"parsy",
2930
"parsy-extn",
3031
"prompt_toolkit",

test/test_parser.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pytest
33

44
from psh.parser import command, command_sequence
5-
from psh.model import Word, ConstantString, Command, VarRef, Id, Token, CommandSequence, CommandPipe, While, If
5+
from psh.model import Word, ConstantString, Assignment, Command, VarRef, Id, Token, CommandSequence, CommandPipe, While, If
66
from psh.local import make_env
77

88

@@ -18,11 +18,15 @@
1818
("$(cat $foo $bar)", Command([Word([CommandSequence([Command([Word([ConstantString("cat")]),
1919
Word([VarRef("foo")]),
2020
Word([VarRef("bar")])])])])])),
21-
("a=2", Command([]).with_assignment(Word([Id("a"), Token("="), ConstantString("2")]))),
21+
("a=2", Command([]).with_assignment(Assignment("a", Word([ConstantString("2")])))),
2222
("a=1 b=2 echo $a$b", Command([Word([Id("echo")]),
2323
Word([VarRef("a"), VarRef("b")])]).
24-
with_assignment(Word([Id("a"), Token("="), ConstantString("1")])).
25-
with_assignment(Word([Id("b"), Token("="), ConstantString("2")]))),
24+
with_assignment(Assignment("a", Word([ConstantString("1")])),
25+
Assignment("b", Word([ConstantString("2")])))),
26+
("a=2 echo b=1", Command([Word([Id("echo")]),
27+
Word([Id("b"), Token("="), ConstantString("1")])]).
28+
with_assignment(Assignment("a", Word([ConstantString("2")])))),
29+
2630
))
2731
def test_basic(text, expected):
2832
cmd = command.parse(text)

0 commit comments

Comments
 (0)