diff --git a/Doc/library/shlex.rst b/Doc/library/shlex.rst --- a/Doc/library/shlex.rst +++ b/Doc/library/shlex.rst @@ -34,6 +34,17 @@ The :mod:`shlex` module defines the foll passing ``None`` for *s* will read the string to split from standard input. + +.. function:: quote(filename) + + Return a shell-escaped version of the string *filename*. The returned value + is a string that can safely be used as one token in a shell command line. + Example:: + + >>> quote('somefile; rm -rf /home') + "'somefile; rm -rf /home'" + + The :mod:`shlex` module defines the following class: diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -92,7 +92,8 @@ This module defines one class called :cl >>> call("cat " + filename, shell=True) # Uh-oh. This will end badly... *shell=False* does not suffer from this vulnerability; the above Note may be - helpful in getting code using *shell=False* to work. + helpful in getting code using *shell=False* to work. See also + :func:`shlex.quote` for a function useful to quote filenames. On Windows: the :class:`Popen` class uses CreateProcess() to execute the child program, which operates on strings. If *args* is a sequence, it will @@ -871,3 +872,8 @@ 5. If backslashes immediately precede a described in rule 3. +.. seealso:: + + :mod:`shlex` + Module which provides function to parse command lines and to quote + filenames. diff --git a/Lib/pipes.py b/Lib/pipes.py --- a/Lib/pipes.py +++ b/Lib/pipes.py @@ -62,7 +62,9 @@ For an example, see the function test() import re import os import tempfile -import string +# we import the quote function rather than the module for backward compat +# (quote used to be an undocumented but used function in pipes) +from shlex import quote __all__ = ["Template"] @@ -245,22 +247,3 @@ def makepipeline(infile, steps, outfile) cmdlist = trapcmd + '\n' + cmdlist + '\n' + rmcmd # return cmdlist - - -# Reliably quote a string as a single argument for /bin/sh - -# Safe unquoted -_safechars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./') - -def quote(file): - """Return a shell-escaped version of the file string.""" - for c in file: - if c not in _safechars: - break - else: - if not file: - return "''" - return file - # use single quotes, and put single quotes into double quotes - # the string $'b is then quoted as '$'"'"'b' - return "'" + file.replace("'", "'\"'\"'") + "'" diff --git a/Lib/shlex.py b/Lib/shlex.py --- a/Lib/shlex.py +++ b/Lib/shlex.py @@ -6,13 +6,14 @@ # Posix compliance, split(), string arguments, and # iterator interface by Gustavo Niemeyer, April 2003. -import os.path +import os +import re import sys from collections import deque from io import StringIO -__all__ = ["shlex", "split"] +__all__ = ["shlex", "split", "quote"] class shlex: "A lexical analyzer class for simple shell-like syntaxes." @@ -274,6 +275,20 @@ def split(s, comments=False, posix=True) lex.commenters = '' return list(lex) + +_find_unsafe = re.compile(r'[^\w\d@%_\-\+=:,\./]').search + +def quote(filename): + """Return a shell-escaped version of the filename string.""" + if not filename: + return "''" + if _find_unsafe(filename) is None: + return filename + + # use single quotes, and put single quotes into double quotes + # the string $'b is then quoted as '$'"'"'b' + return "'" + filename.replace("'", "'\"'\"'") + "'" + if __name__ == '__main__': if len(sys.argv) == 1: lexer = shlex() diff --git a/Lib/test/test_pipes.py b/Lib/test/test_pipes.py --- a/Lib/test/test_pipes.py +++ b/Lib/test/test_pipes.py @@ -79,20 +79,6 @@ class SimplePipeTests(unittest.TestCase) with open(TESTFN) as f: self.assertEqual(f.read(), d) - def testQuoting(self): - safeunquoted = string.ascii_letters + string.digits + '@%_-+=:,./' - unsafe = '"`$\\!' - - self.assertEqual(pipes.quote(''), "''") - self.assertEqual(pipes.quote(safeunquoted), safeunquoted) - self.assertEqual(pipes.quote('test file name'), "'test file name'") - for u in unsafe: - self.assertEqual(pipes.quote('test%sname' % u), - "'test%sname'" % u) - for u in unsafe: - self.assertEqual(pipes.quote("test%s'name'" % u), - "'test%s'\"'\"'name'\"'\"''" % u) - def testRepr(self): t = pipes.Template() self.assertEqual(repr(t), "