Skip to content

Commit

Permalink
Merge pull request #583 from jkloetzke/fix-substitutions
Browse files Browse the repository at this point in the history
Fix variable substitutions with unused parts
  • Loading branch information
jkloetzke authored Sep 3, 2024
2 parents 60164e5 + 363df79 commit 99b2c94
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 14 deletions.
59 changes: 45 additions & 14 deletions pym/bob/stringparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def isTrue(val):
class StringParser:
"""Utility class for complex string parsing/manipulation"""

__slots__ = ('env', 'funs', 'funArgs', 'nounset', 'text', 'index', 'end')

def __init__(self, env, funs, funArgs, nounset):
self.env = env
self.funs = funs
Expand Down Expand Up @@ -86,6 +88,7 @@ def nextToken(self, extra=None):
return "".join(tok)

def getRestOfName(self):
"""Get remainder of bare variable name"""
ret = ''
i = self.index
while i < self.end:
Expand All @@ -98,6 +101,7 @@ def getRestOfName(self):
return ret

def getSingleQuoted(self):
"""Get remainder of single quoted string."""
i = self.index
while i < self.end:
if self.text[i] == "'":
Expand All @@ -109,22 +113,33 @@ def getSingleQuoted(self):
self.index = i+1
return ret

def getString(self, delim=[None], keep=False):
def getString(self, delim=[None], keep=False, subst=True):
"""Interpret as string from current parsing position.
Do any necessary substitutions until either the string ends or hits one
of the additional delimiters.
:param delim: Additional delimiter characters where parsing should stop
:param keep: Keep the additional delimiter if hit. By default the
delimier is swallowed.
:param subst: Do variable or command substitution. If false, skip over
such substitutions.
"""
s = []
tok = self.nextToken(delim)
while tok not in delim:
if tok == '"':
s.append(self.getString(['"']))
s.append(self.getString(['"'], False, subst))
elif tok == '\'':
s.append(self.getSingleQuoted())
elif tok == '$':
tok = self.nextChar()
if tok == '{':
s.append(self.getVariable())
s.append(self.getVariable(subst))
elif tok == '(':
s.append(self.getCommand())
s.append(self.getCommand(subst))
elif tok in NAME_START:
s.append(self.getBareVariable(tok))
s.append(self.getBareVariable(tok, subst))
else:
raise ParseError("Invalid $-subsitituion")
elif tok == None:
Expand All @@ -138,9 +153,13 @@ def getString(self, delim=[None], keep=False):
if keep: self.index -= 1
return "".join(s)

def getVariable(self):
def getVariable(self, subst):
"""Substitute variable at current position.
:param subst: Bail out if substitution fails?
"""
# get variable name
varName = self.getString([':', '-', '+', '}'], True)
varName = self.getString([':', '-', '+', '}'], True, subst)

# process?
op = self.nextChar()
Expand All @@ -151,46 +170,58 @@ def getVariable(self):
op = self.nextChar()

if op == '-':
default = self.getString(['}'])
default = self.getString(['}'], False, subst and unset)
if unset:
return default
else:
return self.env[varName]
elif op == '+':
alternate = self.getString(['}'])
alternate = self.getString(['}'], False, subst and not unset)
if unset:
return ""
else:
return alternate
elif op == '}':
if varName not in self.env:
if self.nounset:
if subst and self.nounset:
raise ParseError("Unset variable: " + varName)
else:
return ""
return self.env[varName]
else:
raise ParseError("Unterminated variable: " + str(op))

def getBareVariable(self, varName):
def getBareVariable(self, varName, subst):
"""Substitute base variable at current position.
:param varName: Initial character of variable name
:param subst: Bail out if substitution fails?
"""
varName += self.getRestOfName()
varValue = self.env.get(varName)
if varValue is None:
if self.nounset:
if subst and self.nounset:
raise ParseError("Unset variable: " + varName)
return ""
else:
return varValue

def getCommand(self):
def getCommand(self, subst):
"""Substitute string function at current position.
:param subst: Actually call function or just skip?
"""
words = []
delim = [",", ")"]
while True:
word = self.getString(delim, True)
word = self.getString(delim, True, subst)
words.append(word)
end = self.nextChar()
if end == ")": break

if not subst:
return ""

if len(words) < 1:
raise ParseError("Expected function name")
cmd = words[0]
Expand Down
20 changes: 20 additions & 0 deletions test/unit/test_input_stringparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ def testFails(self):
self.assertRaises(ParseError, u.parse, "$1")
self.assertRaises(ParseError, u.parse, "$%%")

def testSkipUnused(self):
"""Unused branches must not be substituted.
Syntax error must still be detected, though.
"""

self.assertEqual(self.p.parse("${asdf:-$unset}"), "qwer")
self.assertEqual(self.p.parse("${asdf:-${unset}}"), "qwer")
self.assertEqual(self.p.parse("${asdf:-${${double-unset}}}"), "qwer")
self.assertEqual(self.p.parse("${asdf:-$(unknown)}"), "qwer")
self.assertEqual(self.p.parse("${asdf:-$($fn,$unset)}"), "qwer")
self.assertRaises(ParseError, self.p.parse, "${asdf:-$($fn}")

self.assertEqual(self.p.parse("${unset:+$unset}"), "")
self.assertEqual(self.p.parse("${unset:+${unset}}"), "")
self.assertEqual(self.p.parse("${unset:+${${double-unset}}}"), "")
self.assertEqual(self.p.parse("${unset:+$(unknown)}"), "")
self.assertEqual(self.p.parse("${unset:+$($fn,$unset)}"), "")
self.assertRaises(ParseError, self.p.parse, "${unset:+${${double-unset}}")

class TestStringFunctions(TestCase):

def testEqual(self):
Expand Down

0 comments on commit 99b2c94

Please sign in to comment.