views:

4985

answers:

9

When using os.system() it's often necessary to escape filenames and other arguments passed as parameters to commands. How can I do this? Preferably something that would work on multiple operating systems/shells but in particular for bash.

I'm currently doing the following, but am sure there must be a library function for this, or at least a more elegant/robust/efficient option:

def sh_escape(s):
   return s.replace("(","\\(").replace(")","\\)").replace(" ","\\ ")

os.system("cat %s | grep something | sort > %s" 
          % (sh_escape(in_filename), 
             sh_escape(out_filename)))

Edit: I've accepted the simple answer of using quotes, don't know why I didn't think of that; I guess because I came from Windows where ' and " behave a little differently.

Regarding security, I understand the concern, but, in this case, I'm interested in a quick and easy solution which os.system() provides, and the source of the strings is either not user-generated or at least entered by a trusted user (me).

+2  A: 

Beware of the security issue! For instance if out_filename is

foo.txt; rm -rf /

The malicious user can add more command directly interpreted by the shell.

Steve Gury
That's the whole point of escaping it: note how (for example) pipes.quote("ab") differs from pipes.quote("a b")
Roger Pate
@Steve: An ideal `sh_escape` function would escape out the `;` and spaces and remove the security problem by simply creating a file called something like `foo.txt\;\ rm\ -rf\ /`.
Tom
+1  A: 

I believe that os.system just invokes whatever command shell is configured for the user, so I don't think you can do it in a platform independent way. My command shell could be anything from bash, emacs, ruby, or even quake3. Some of these programs aren't expecting the kind of arguments you are passing to them and even if they did there is no guarantee they do their escaping the same way.

pauldoo
It's not unreasonable to expect a mostly or fully POSIX-compliant shell (at least everywhere but with Windows, and you know what "shell" you have then, anyway). os.system doesn't use $SHELL, at least not here.
Roger Pate
+5  A: 

This is what I use:

def shellquote(s):
    return "'" + s.replace("'", "'\\''") + "'"

The shell will always accept a quoted filename and remove the surrounding quotes before passing it to the program in question. Notably, this avoids problems with filenames that contain spaces or any other kind of nasty shell metacharacter.

Greg Hewgill
escaped singles quotes are not valid within single quotes.
pixelbeat
@pixelbeat: which is exactly why he closes his single quotes, adds an escaped literal single quote, and then reopens his single quotes again.
lhunath
While this is hardly the responsibility of the shellquote function, it might be interesting to note that this will still fail if an unquoted backslash appears just before the return value of this function.Morale: make sure you use this in code that you can trust as safe - (such as part of hardcoded commands) - don't append it to other unquoted user input.
lhunath
Note that unless you absolutely need shell features, you should probably be using Jamie's suggestion instead.
lhunath
+23  A: 

Perhaps you have a specific reason for using os.system(). But if not you should probably be using the subprocess module. You can specify the pipes directly and avoid using the shell.

The following is from PEP324

Replacing shell pipe line
-------------------------

output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
Jamie
A: 

If you do use the system command, I would try and whitelist what goes into the os.system() call.. For example..

clean_user_input re.sub("[^a-zA-Z]", "", user_input)
os.system("ls %s" % (clean_user_input))

The subprocess module is a better option, and I would recommend trying to avoid using anything like os.system/subprocess wherever possible.

dbr
+16  A: 

pipes.quote() (available since python 1.6) does what you want.

pixelbeat
There's also `commands.mkarg`. It also adds a leading space (outside the quotes) which may or may not be desirable.It's interesting how their implementations are quite different from each other, and also much more complicated than Greg Hewgill's answer.
Laurence Gonsalves
+3  A: 

pipes.quote() (available since python 1.6) does what you want. ~~~pixelbeat

thanks, pixelbeat!

>>> from pipes import quote
>>> args=['a a',r'b<\">B', '''c"''C''']
>>> print ' '.join( quote( arg ) for arg in args )
'a a' 'b<\">B' "c\"''C"
zowers
A: 

Note that pipes.quote is actually broken and not safe to use--It doesn't handle zero-length arguments.

>>> from pipes import quote
>>> args = ['arg1', '', 'arg3']
>>> print 'mycommand %s' % (' '.join(quote(arg) for arg in args))
mycommand arg1  arg3
John Wiseman
What version of Python are you using? Version 2.6 seems to produce the correct output: mycommand arg1 '' arg3 (Those are two single-quotes together, though the font on Stack Overflow makes that hard to tell!)
Brandon Craig Rhodes
A: 

The function I use is:

def quote_argument(argument):
    return '"%s"' % (
        argument
        .replace('\\', '\\\\')
        .replace('"', '\"')
        .replace('$', '\$')
        .replace('`', '\`')
    )

that is: I always enclose the argument in double quotes, and then backslash-quote the only characters special inside double quotes.

ΤΖΩΤΖΙΟΥ