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.
This commit is contained in:
parent
fcdaaa9011
commit
9bce311ea4
|
@ -34,6 +34,22 @@ The :mod:`shlex` module defines the following functions:
|
||||||
passing ``None`` for *s* will read the string to split from standard
|
passing ``None`` for *s* will read the string to split from standard
|
||||||
input.
|
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:
|
The :mod:`shlex` module defines the following class:
|
||||||
|
|
||||||
|
|
||||||
|
@ -282,5 +298,4 @@ parsing rules.
|
||||||
|
|
||||||
* EOF is signaled with a :const:`None` value;
|
* EOF is signaled with a :const:`None` value;
|
||||||
|
|
||||||
* Quoted empty strings (``''``) are allowed;
|
* Quoted empty strings (``''``) are allowed.
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,8 @@ This module defines one class called :class:`Popen`:
|
||||||
>>> call("cat " + filename, shell=True) # Uh-oh. This will end badly...
|
>>> call("cat " + filename, shell=True) # Uh-oh. This will end badly...
|
||||||
|
|
||||||
*shell=False* does not suffer from this vulnerability; the above Note may be
|
*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
|
On Windows: the :class:`Popen` class uses CreateProcess() to execute the
|
||||||
child program, which operates on strings. If *args* is a sequence, it will
|
child program, which operates on strings. If *args* is a sequence, it will
|
||||||
|
@ -871,3 +872,7 @@ runtime):
|
||||||
described in rule 3.
|
described in rule 3.
|
||||||
|
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:mod:`shlex`
|
||||||
|
Module which provides function to parse and escape command lines.
|
||||||
|
|
23
Lib/pipes.py
23
Lib/pipes.py
|
@ -62,7 +62,9 @@ For an example, see the function test() at the end of the file.
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import tempfile
|
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"]
|
__all__ = ["Template"]
|
||||||
|
|
||||||
|
@ -245,22 +247,3 @@ def makepipeline(infile, steps, outfile):
|
||||||
cmdlist = trapcmd + '\n' + cmdlist + '\n' + rmcmd
|
cmdlist = trapcmd + '\n' + cmdlist + '\n' + rmcmd
|
||||||
#
|
#
|
||||||
return cmdlist
|
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("'", "'\"'\"'") + "'"
|
|
||||||
|
|
20
Lib/shlex.py
20
Lib/shlex.py
|
@ -6,13 +6,14 @@
|
||||||
# Posix compliance, split(), string arguments, and
|
# Posix compliance, split(), string arguments, and
|
||||||
# iterator interface by Gustavo Niemeyer, April 2003.
|
# iterator interface by Gustavo Niemeyer, April 2003.
|
||||||
|
|
||||||
import os.path
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
__all__ = ["shlex", "split"]
|
__all__ = ["shlex", "split", "quote"]
|
||||||
|
|
||||||
class shlex:
|
class shlex:
|
||||||
"A lexical analyzer class for simple shell-like syntaxes."
|
"A lexical analyzer class for simple shell-like syntaxes."
|
||||||
|
@ -274,6 +275,21 @@ def split(s, comments=False, posix=True):
|
||||||
lex.commenters = ''
|
lex.commenters = ''
|
||||||
return list(lex)
|
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 __name__ == '__main__':
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
lexer = shlex()
|
lexer = shlex()
|
||||||
|
|
|
@ -79,20 +79,6 @@ class SimplePipeTests(unittest.TestCase):
|
||||||
with open(TESTFN) as f:
|
with open(TESTFN) as f:
|
||||||
self.assertEqual(f.read(), d)
|
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):
|
def testRepr(self):
|
||||||
t = pipes.Template()
|
t = pipes.Template()
|
||||||
self.assertEqual(repr(t), "<Template instance, steps=[]>")
|
self.assertEqual(repr(t), "<Template instance, steps=[]>")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import unittest
|
import io
|
||||||
import os, sys, io
|
|
||||||
import shlex
|
import shlex
|
||||||
|
import string
|
||||||
|
import unittest
|
||||||
|
|
||||||
from test import support
|
from test import support
|
||||||
|
|
||||||
|
@ -173,6 +174,21 @@ class ShlexTest(unittest.TestCase):
|
||||||
"%s: %s != %s" %
|
"%s: %s != %s" %
|
||||||
(self.data[i][0], l, self.data[i][1:]))
|
(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
|
# Allow this test to be used with old shlex.py
|
||||||
if not getattr(shlex, "split", None):
|
if not getattr(shlex, "split", None):
|
||||||
for methname in dir(ShlexTest):
|
for methname in dir(ShlexTest):
|
||||||
|
|
|
@ -237,6 +237,9 @@ Core and Builtins
|
||||||
Library
|
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
|
- Issue #12607: In subprocess, fix issue where if stdin, stdout or stderr is
|
||||||
given as a low fd, it gets overwritten.
|
given as a low fd, it gets overwritten.
|
||||||
|
|
||||||
|
@ -6674,4 +6677,4 @@ Docs
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
**(For information about older versions, consult the HISTORY file.)**
|
**(For information about older versions, consult the HISTORY file.)**
|
||||||
|
|
Loading…
Reference in New Issue