Welcome, guest | Sign In | My Account | Store | Cart

This Python module quotes a Python string so that it will be treated as a single argument to commands ran via os.system() (assuming bash is the underlying shell). In other words, this module makes arbitrary strings "command line safe" (for bash command lines anyway, YMMV if you're using Windows or one of the (less fine) posix shells).

Python, 99 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#! /usr/bin/env python
######################################################################
#  Written by Kevin L. Sitze on 2006-12-03
#  This code may be used pursuant to the MIT License.
######################################################################

import re

__all__ = ( 'quote', )

_bash_reserved_words = {
    'case'     : True,
    'coproc'   : True,
    'do'       : True,
    'done'     : True,
    'elif'     : True,
    'else'     : True,
    'esac'     : True,
    'fi'       : True,
    'for'      : True,
    'function' : True,
    'if'       : True,
    'in'       : True,
    'select'   : True,
    'then'     : True,
    'until'    : True,
    'while'    : True,
    'time'     : True
}

####
#  _quote_re1 escapes double-quoted special characters.
#  _quote_re2 escapes unquoted special characters.

_quote_re1 = re.compile( r"([\!\"\$\\\`])" )
_quote_re2 = re.compile( r"([\t\ \!\"\#\$\&\'\(\)\*\:\;\<\>\?\@\[\\\]\^\`\{\|\}\~])" )

def quote( *args ):
    """Combine the arguments into a single string and escape any and
    all shell special characters or (reserved) words.  The shortest
    possible string (correctly quoted suited to pass to a bash shell)
    is returned.
    """
    s = "".join( args )
    if _bash_reserved_words.has_key( s ):
        return "\\" + s
    elif s.find( '\'' ) >= 0:
        s1 = '"' + _quote_re1.sub( r"\\\1", s ) + '"'
    else:
        s1 = "'" + s + "'"
    s2 = _quote_re2.sub( r"\\\1", s )
    if len( s1 ) <= len( s2 ):
        return s1
    else:
        return s2

if __name__ == '__main__':

    import sys
    import traceback
    from types import FloatType, ComplexType

    def assertEquals( exp, got ):
        """assertEquals( exp, got )

        Two objects test as "equal" if:
        
        * they are the same object as tested by the 'is' operator.
        * either object is a float or complex number and the absolute
          value of the difference between the two is less than 1e-8.
        * applying the equals operator ('==') returns True.
        """
        if exp is got:
            r = True
        elif ( type( exp ) in ( FloatType, ComplexType ) or
               type( got ) in ( FloatType, ComplexType ) ):
            r = abs( exp - got ) < 1e-8
        else:
            r = ( exp == got )
        if not r:
            print >>sys.stderr, "Error: expected <%s> but got <%s>" % ( repr( exp ), repr( got ) )
            traceback.print_stack()

    for word in _bash_reserved_words:
        assertEquals( "\\" + word, quote( word ) )

    for char in ( '\t',
                  ' ', '!', '"', '#',
                  '$', '&', "'", '(',
                  ')', '*', ':', ';',
                  '<', '>', '?', '@',
                  '[', ']', '^', '`',
                  '{', '|', '}', '~' ):
        assertEquals( "\\" + char, quote( char ) )

    assertEquals( "'this is a simple path with spaces'",
                  quote( 'this is a simple path with spaces' ) )
    assertEquals( "don\\'t", quote( "don't" ) )
    assertEquals( '"don\'t do it"', quote( "don't do it" ) )

Yes, there are several modules out there that will quote strings in a form suitable for being passed to another program on a command line. Yes, I know that there are better ways of doing shell commands (i.e., using the subprocess package). However, I still find this recipe useful for logging what my Python scripts are doing by emitting a "shell command" even if what it is actually doing underneath is more detailed and/or refined.

What do I like about this package? The resulting quoted strings are the shortest possible string that the bash shell will accept as a single CLI argument. This means they're easy for humans to eyeball, which makes dealing with verbose logs much more pleasant. The other thing I like about this package? It does the right thing when quoting strings (for bash).

How do I use it? I stick the above code into a file "shell.py" and then in my programs I'll shove things to stderr like this:

import shell
import shutil

verbose = True
def copyFile( source, target ):
    if verbose:
        print >>sys.stderr, 'mv', '-f', shell.quote( source ), shell.quote( target )
    shutil.copyfile( source, target )

copyFile( 'a source filename', 'the target file to copy stuff to...' )

You'll get output that looks like this:

mv -f 'a source filename' 'the target file to copy stuff to...'

Or if you're getting fancier, you can do something similar with the logging package.

This is so much nicer to read than the kinds of output produced by "the other module."

mv -f a\ source\ filename the\ target\ file\ to\ copy\ stuff\ to...

Where the heck is the break between the source and target file name (who wants to play "Where's Waldo")?

WARNING: this module is specifically designed for the bash shell and if used with or under other shells will likely cause problems and give you indigestion.

WARNING: Please don't try using this to building command lines to pass to os.system(), especially if you are NOT using the bash shell. It will probably do what you want 99 times out of 100, but doing this kind of thing (even in the bash shell) is pretty much guaranteed to be a security risk. Use the subprocess package to spawn off your commands, it is so much safer, especially in a web environment.

WARNING: use of this module is intended to facilitate human readable logs, it is NOT intended for building command lines to feed to os.system().

WARNING: if you're still thinking of using this to build CLI commands for os.system() then you must first place your hand over your heart and swear the following oath:

"I know I'm an idiot, I know I'm lazy and I understand that what I'm about to attempt is a potential security risk and I swear that I shall not complain when my website/system/box and/or (insert appropriate system name here) gets hacked because I know up front that what I'm doing is stupid and likely to blow up in my face in all sorts of unforeseen ways: including but not limited to having arbitrarily upgraded to a new version of bash, randomly setting my shell to be something other than bash in the future, having another user that is not using the officially sanctioned bash run my program, or have all sorts of other changes to the system outside my understanding and control occur that will cause miscellaneous problems and bugs that will be difficult for me or those whom come after me to track down and fix. I also promise to hold the author of this script harmless because he told me in many words that what I am about to do will void my warranty and possibly cause untold harm and devastation to countless others."

Now, go out there and blow your foot off.