# HG changeset patch # User Bernhard Herzog # Date 1174331647 -3600 # Node ID a2ce575ce82b0676bc10a383c3788a57f5033064 # Parent 3c5ab7a65384db3e2556d0b7d28603855ccc59e2 add cmdexpand function and tests diff -r 3c5ab7a65384 -r a2ce575ce82b test/test_cmdexpand.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/test_cmdexpand.py Mon Mar 19 20:14:07 2007 +0100 @@ -0,0 +1,124 @@ +# Copyright (C) 2007 by Intevation GmbH +# Authors: +# Bernhard Herzog +# +# This program is free software under the GPL (>=v2) +# Read the file COPYING coming with the software for details. + +"""Tests for the cmdexpand function""" + +import unittest + +from treepkg.cmdexpand import cmdexpand + + +class TestCMDExpand(unittest.TestCase): + + def test_words(self): + """Test cmdexpand with simple whitespace separated words""" + self.assertEquals(cmdexpand("abc defg xyz zy"), + ['abc', 'defg', 'xyz', 'zy']) + + def test_single_quoted(self): + """Test cmdexpand with some single quoted words""" + self.assertEquals(cmdexpand("abc 'defg xyz' zy"), + ['abc', 'defg xyz', 'zy']) + + def test_double_quoted(self): + """Test cmdexpand with some double quoted words""" + self.assertEquals(cmdexpand('abc "defg xyz" zy'), + ['abc', 'defg xyz', 'zy']) + + def test_word_expansion(self): + """Test cmdexpand with simple word expansion""" + self.assertEquals(cmdexpand('abc $foo ghi', foo="def"), + ['abc', 'def', 'ghi']) + self.assertEquals(cmdexpand('abc $foo ghi $bar', foo="def", bar="X"), + ['abc', 'def', 'ghi', 'X']) + + def test_word_expansion_braced_name(self): + """Test cmdexpand with word expansion using braced names""" + self.assertEquals(cmdexpand('abc ${foo} x${foo}y ghi', foo="def"), + ['abc', 'def', 'xdefy', 'ghi']) + + def test_word_expansion_non_byte_string(self): + """Test cmdexpand quoting of dollar signs""" + self.assertEquals(cmdexpand('abc $foo $bar ghi', foo=123, bar=u"1 2 3"), + ['abc', '123', '1 2 3', 'ghi']) + + def test_word_expansion_non_identifier(self): + """Test cmdexpand word expansion if dollar not followed by identifier""" + # $ immediately followed by a non-identifier character + self.assertRaises(ValueError, cmdexpand, 'abc $#foo bar', foo="def") + + def test_word_expansion_inside_words(self): + """Test cmdexpand word expansions in parts of words""" + self.assertEquals(cmdexpand("$foo x$bar y$baz.", + foo="abc", bar="yz", baz="zx"), + ["abc", "xyz", "yzx."]) + self.assertEquals(cmdexpand("$foo x$bar-$baz.", + foo="abc", bar="yz", baz="zx"), + ["abc", "xyz-zx."]) + + def test_case_sensitivity(self): + """Test case sensitivity of expansion keys""" + self.assertEquals(cmdexpand('abc $foo $Foo $FOO', + foo="def", Foo="DEF", FOO="Def"), + ['abc', 'def', 'DEF', 'Def']) + + def test_list_expansion(self): + """Test cmdexpand with list expansion""" + self.assertEquals(cmdexpand('abc @foo ghi', foo=["d", "e", "f"]), + ['abc', 'd', 'e', 'f', 'ghi']) + + def test_list_expansion_non_string(self): + """Test cmdexpand with list expansion""" + self.assertEquals(cmdexpand('abc @foo ghi', foo=[1, 1.0, None]), + ['abc', '1', '1.0', 'None', 'ghi']) + + def test_list_expansion_with_iterators(self): + """Test cmdexpand with list expansion using an iterator""" + self.assertEquals(cmdexpand('abc @foo ghi', + foo=(i**2 for i in range(3))), + ['abc', '0', '1', '4', 'ghi']) + + def test_list_expansion_non_identifier(self): + """Test cmdexpand with at-sign not followed by identifier""" + # @+identifier do not cover entire word + self.assertRaises(ValueError, cmdexpand, 'abc @foo, ghi', + foo=["d", "e", "f"]) + + # @ immediately followed by a non-identifier character + self.assertRaises(ValueError, cmdexpand, 'abc @. z') + + def test_list_expansion_inside_word(self): + """Test whether cmdexpand raises ValueError for at-signs inside words""" + self.assertRaises(ValueError, cmdexpand, 'abc x@foo ghi', + foo=["d", "e", "f"]) + + + def test_dollar_quoting(self): + """Test cmdexpand quoting of dollar signs""" + self.assertEquals(cmdexpand('abc $$foo $foo g$$hi', foo="def"), + ['abc', '$foo', 'def', 'g$hi']) + + def test_atsign_quoting(self): + """Test cmdexpand quoting of at-signs""" + self.assertEquals(cmdexpand('abc @foo $@foo g$@i', foo=["d", "e", "f"]), + ['abc', 'd', 'e', 'f', '@foo', 'g@i']) + + def test_interaction_with_shlex_quoting(self): + """Test cmdexpand's interaction with shlex's quoting""" + # Unlike unix-shells the expansion isn't influenced much by + # shell quoting as supported by shlex. + self.assertEquals(cmdexpand('abc "@foo" \'@foo\' ghi', + foo=["d", "e", "f"]), + ['abc', 'd', 'e', 'f', 'd', 'e', 'f', 'ghi']) + self.assertEquals(cmdexpand('abc "$foo" \'$foo\' ghi', foo="def"), + ['abc', 'def', 'def', 'ghi']) + self.assertEquals(cmdexpand('abc " $foo" \'a $foo\' ghi', foo="def"), + ['abc', ' def', 'a def', 'ghi']) + + +if __name__ == "__main__": + unittest.main() diff -r 3c5ab7a65384 -r a2ce575ce82b treepkg/cmdexpand.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/treepkg/cmdexpand.py Mon Mar 19 20:14:07 2007 +0100 @@ -0,0 +1,73 @@ +# Copyright (C) 2007 by Intevation GmbH +# Authors: +# Bernhard Herzog +# +# This program is free software under the GPL (>=v2) +# Read the file COPYING coming with the software for details. + +"""Shell like string splitting and expansion""" + +import re +import shlex + + +# helper for the other regular expression matching a python identifier +match_identifier = "[_a-zA-Z][_a-zA-Z0-9]*" + +# regular expression to use for word expansion matching a dollar +# followed by exactly one of these: +# a) another dollar sign or the at-sign (for quoting of these characters) +# b) a python identifier +# c) a python identifier enclosed in braces +# d) something else which indicates invalid use of the dollar sign +rx_word_expansion = re.compile(r"\$((?P[$@])" + r"|(?P%(identifier)s)" + r"|\{(?P%(identifier)s)\}" + r"|(?P))" + % dict(identifier=match_identifier)) + +# regular expression matching an entire word that has to be list +# expanded. The regex matches if the word starts with an at-sign. The +# part of the word that followes the at-sign either matches an +# identifier with the named group "named" or anything else which +# indicates invalid use the at-sign. +rx_list_expansion = re.compile(r"^@((?P%(identifier)s)|(?P.+))$" + % dict(identifier=match_identifier)) + +# match an unquoted at-sign. +rx_unquoted_at = re.compile("[^$]@") + +def expandword(word, mapping): + def replacment(match): + key = match.group("named") or match.group("braced") + if key: + return str(mapping[key]) + + delim = match.group("delim") + if delim: + return delim + + # otherwise invalid has matched and we raise a value error + assert match.group("invalid") != None + raise ValueError + + return rx_word_expansion.sub(replacment, word) + +def cmdexpand(string, **kw): + words = shlex.split(string) + for index, word in reversed(list(enumerate(words))): + match = rx_unquoted_at.search(word) + if match: + raise ValueError("%r contains an unquoted '@'" % word) + match = rx_list_expansion.match(word) + if match: + key = match.group("named") + if key: + words[index:index + 1] = (str(item) for item in kw[key]) + else: + assert match.group("invalid") != None + raise ValueError("In %r the characters after the '@'" + " do not match a python identifier" % word) + else: + words[index] = expandword(word, kw) + return words