[Python-checkins] cpython: Add shlex.quote function, to escape filenames and command lines (#9723).

eric.araujo python-checkins at python.org
Fri Jul 29 14:35:22 CEST 2011


http://hg.python.org/cpython/rev/5966eeb0457d
changeset:   71559:5966eeb0457d
parent:      71528:842494d73f69
user:        Éric Araujo <merwok at netwok.org>
date:        Wed Jul 27 18:29:31 2011 +0200
summary:
  Add shlex.quote function, to escape filenames and command lines (#9723).

This function used to live as pipes.quote, where it was undocumented but
used anyway.  (An alias still exists for backward compatibility.)  The
tests have been moved as is, but the code of the function was changed to
use a regex instead of a loop with string comparisons (at Ian Bicking’s
suggestion).  I’m terrible at regexes, so any feedback is welcome.

files:
  Doc/library/shlex.rst      |  19 +++++++++++++++++--
  Doc/library/subprocess.rst |   7 ++++++-
  Lib/pipes.py               |  23 +++--------------------
  Lib/shlex.py               |  20 ++++++++++++++++++--
  Lib/test/test_pipes.py     |  14 --------------
  Lib/test/test_shlex.py     |  20 ++++++++++++++++++--
  Misc/NEWS                  |   5 ++++-
  7 files changed, 66 insertions(+), 42 deletions(-)


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,22 @@
       passing ``None`` for *s* will read the string to split from standard
       input.
 
+
+.. function:: quote(s)
+
+   Return a shell-escaped version of the string *s*.  The returned value is a
+   string that can safely be used as one token in a shell command line.
+   Examples::
+
+      >>> filename = 'somefile; rm -rf /home'
+      >>> command = 'ls -l {}'.format(quote(filename))
+      >>> print(command)
+      ls -l 'somefile; rm -rf /home'
+      >>> remote_command = 'ssh home {}'.format(quote(command))
+      >>> print(remote_command)
+      ssh home 'ls -l '"'"'somefile; rm -rf /home'"'"''
+
+
 The :mod:`shlex` module defines the following class:
 
 
@@ -282,5 +298,4 @@
 
 * EOF is signaled with a :const:`None` value;
 
-* Quoted empty strings (``''``) are allowed;
-
+* Quoted empty strings (``''``) are allowed.
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 @@
          >>> 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 and commands.
 
    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,7 @@
    described in rule 3.
 
 
+.. seealso::
+
+   :mod:`shlex`
+      Module which provides function to parse and escape command lines.
diff --git a/Lib/pipes.py b/Lib/pipes.py
--- a/Lib/pipes.py
+++ b/Lib/pipes.py
@@ -62,7 +62,9 @@
 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 @@
         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,21 @@
         lex.commenters = ''
     return list(lex)
 
+
+_find_unsafe = re.compile(r'[^\w\d@%_\-\+=:,\./]').search
+
+def quote(s):
+    """Return a shell-escaped version of the string *s*."""
+    if not s:
+        return "''"
+    if _find_unsafe(s) is None:
+        return s
+
+    # use single quotes, and put single quotes into double quotes
+    # the string $'b is then quoted as '$'"'"'b'
+    return "'" + s.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 @@
         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), "<Template instance, steps=[]>")
diff --git a/Lib/test/test_shlex.py b/Lib/test/test_shlex.py
--- a/Lib/test/test_shlex.py
+++ b/Lib/test/test_shlex.py
@@ -1,6 +1,7 @@
+import io
+import shlex
+import string
 import unittest
-import os, sys, io
-import shlex
 
 from test import support
 
@@ -173,6 +174,21 @@
                              "%s: %s != %s" %
                              (self.data[i][0], l, self.data[i][1:]))
 
+    def testQuote(self):
+        safeunquoted = string.ascii_letters + string.digits + '@%_-+=:,./'
+        unsafe = '"`$\\!'
+
+        self.assertEqual(shlex.quote(''), "''")
+        self.assertEqual(shlex.quote(safeunquoted), safeunquoted)
+        self.assertEqual(shlex.quote('test file name'), "'test file name'")
+        for u in unsafe:
+            self.assertEqual(shlex.quote('test%sname' % u),
+                             "'test%sname'" % u)
+        for u in unsafe:
+            self.assertEqual(shlex.quote("test%s'name'" % u),
+                             "'test%s'\"'\"'name'\"'\"''" % u)
+
+
 # Allow this test to be used with old shlex.py
 if not getattr(shlex, "split", None):
     for methname in dir(ShlexTest):
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -237,6 +237,9 @@
 Library
 -------
 
+- Issue #9723: Add shlex.quote functions, to escape filenames and command
+  lines.
+
 - Issue #12607: In subprocess, fix issue where if stdin, stdout or stderr is
   given as a low fd, it gets overwritten.
 
@@ -6674,4 +6677,4 @@
 ----
 
 
-**(For information about older versions, consult the HISTORY file.)**
\ No newline at end of file
+**(For information about older versions, consult the HISTORY file.)**

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list