Technology

Quote shell characters

Sometimes it’s nice to be able to display a command being run and do it in a form that can be repeated from a terminal window. Unfortunately, I’ll build a command in python like this:

args = ['somecmd', 'WHERE=$(DESTDIR)', '--message', "Lot's of text" ]

The trivial answer of ‘ ‘.join (args) produces something that can’t be passed to the shell:

>>> ' '.join (args)
"somecmd WHERE=$(DESTDIR) --message Lot's of text"

Here is a code fragment and some attached tests that provide better quoting. It’s used like this:

>>> import shquote
>>> args = ['somecmd', 'WHERE=$(DESTDIR)', '--message', "Lot's of text" ]
>>> print ' '.join ([shquote.shquote (a) for a in args])
somecmd 'WHERE=$(DESTDIR)' --message "Lot's of text"
#!/usr/bin/env python
def shquote (arg):
    """
    Quote a single argument in the most readable way so it is safe from
    shell expansion.
    """
    # Return an empty string as double quoted
    if not arg:
        return '""'
    special = """ t[]{}()'*?|;"""   # These need double quotes
    superspecial = """$"!"""        # These need single quotes
    quotechar = None
    # See if we need single quotes
    for c in superspecial:
        if c in arg:
            quotechar = "'"
            break
    # See if we need double quotes
    if not quotechar:
        for c in special:
            if c in arg:
                quotechar = '"'
                break
    # No quoting necessary
    if not quotechar:
        return arg
    # If quotechar is present then escape it by dropping out of quotes
    if quotechar in arg:
        arg = arg.replace (quotechar, "%s\%s%s" % (quotechar, quotechar, quotechar))
    return quotechar + arg + quotechar
if __name__ == '__main__':
    tests = [('',       '""'),
             ('*.cpp',  '"*.cpp"'),
             ('test.[ch]', '"test.[ch]"'),
             ('(',      '"("'),
             (')',      '")"'),
             ('a',      'a'),
             ('$',      """'$'"""),
             ('$a',     """'$a'"""),
             ('abc|def', '"abc|def"'),
             ('abc;def', '"abc;def"'),
             ('a b',    '"a b"'),
             ('"abc"',  """'"abc"'"""),
             ("""It's mine""", '"It's mine"'),
             ('abc | def ABC=$(XYC)', """'abc | def ABC=$(XYC)'"""),
             (""""That's impossible!" he said.""", """'"That'\''s impossible!" he said.'"""),
          ]
    for (input, expected) in tests:
        output = shquote (input)
        if output != expected:
            print 'input    = ', input
            print 'expected = ', expected
            print 'got      = ', output
        else:
            print "%-30s => %s" % (input, output)

And the result of running this:

bash-3.2$ ./shquote.py
                               => ""
*.cpp                          => "*.cpp"
test.[ch]                      => "test.[ch]"
(                              => "("
)                              => ")"
a                              => a
$                              => '$'
$a                             => '$a'
abc|def                        => "abc|def"
abc;def                        => "abc;def"
a b                            => "a b"
"abc"                          => '"abc"'
It's mine                      => "It's mine"
abc | def ABC=$(XYC)           => 'abc | def ABC=$(XYC)'
"That's impossible!" he said.  => '"That'''s impossible!" he said.'