Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c1f974c

Browse files
committed
Closes #1521950: Made shlex parsing more shell-like.
1 parent d2f8747 commit c1f974c

3 files changed

Lines changed: 265 additions & 32 deletions

File tree

Doc/library/shlex.rst

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ The :mod:`shlex` module defines the following functions:
7373
The :mod:`shlex` module defines the following class:
7474

7575

76-
.. class:: shlex(instream=None, infile=None, posix=False)
76+
.. class:: shlex(instream=None, infile=None, posix=False, punctuation_chars=False)
7777

7878
A :class:`~shlex.shlex` instance or subclass instance is a lexical analyzer
7979
object. The initialization argument, if present, specifies where to read
80-
characters from. It must be a file-/stream-like object with
80+
characters from. It must be a file-/stream-like object with
8181
:meth:`~io.TextIOBase.read` and :meth:`~io.TextIOBase.readline` methods, or
8282
a string. If no argument is given, input will be taken from ``sys.stdin``.
8383
The second optional argument is a filename string, which sets the initial
@@ -87,8 +87,19 @@ The :mod:`shlex` module defines the following class:
8787
when *posix* is not true (default), the :class:`~shlex.shlex` instance will
8888
operate in compatibility mode. When operating in POSIX mode,
8989
:class:`~shlex.shlex` will try to be as close as possible to the POSIX shell
90-
parsing rules.
91-
90+
parsing rules. The *punctuation_chars* argument provides a way to make the
91+
behaviour even closer to how real shells parse. This can take a number of
92+
values: the default value, ``False``, preserves the behaviour seen under
93+
Python 3.5 and earlier. If set to ``True``, then parsing of the characters
94+
``();<>|&`` is changed: any run of these characters (considered punctuation
95+
characters) is returned as a single token. If set to a non-empty string of
96+
characters, those characters will be used as the punctuation characters. Any
97+
characters in the :attr:`wordchars` attribute that appear in
98+
*punctuation_chars* will be removed from :attr:`wordchars`. See
99+
:ref:`improved-shell-compatibility` for more information.
100+
101+
.. versionchanged:: 3.6
102+
The `punctuation_chars` parameter was added.
92103

93104
.. seealso::
94105

@@ -191,7 +202,13 @@ variables which either control lexical analysis or can be used for debugging:
191202
.. attribute:: shlex.wordchars
192203

193204
The string of characters that will accumulate into multi-character tokens. By
194-
default, includes all ASCII alphanumerics and underscore.
205+
default, includes all ASCII alphanumerics and underscore. In POSIX mode, the
206+
accented characters in the Latin-1 set are also included. If
207+
:attr:`punctuation_chars` is not empty, the characters ``~-./*?=``, which can
208+
appear in filename specifications and command line parameters, will also be
209+
included in this attribute, and any characters which appear in
210+
``punctuation_chars`` will be removed from ``wordchars`` if they are present
211+
there.
195212

196213

197214
.. attribute:: shlex.whitespace
@@ -222,9 +239,13 @@ variables which either control lexical analysis or can be used for debugging:
222239

223240
.. attribute:: shlex.whitespace_split
224241

225-
If ``True``, tokens will only be split in whitespaces. This is useful, for
242+
If ``True``, tokens will only be split in whitespaces. This is useful, for
226243
example, for parsing command lines with :class:`~shlex.shlex`, getting
227-
tokens in a similar way to shell arguments.
244+
tokens in a similar way to shell arguments. If this attribute is ``True``,
245+
:attr:`punctuation_chars` will have no effect, and splitting will happen
246+
only on whitespaces. When using :attr:`punctuation_chars`, which is
247+
intended to provide parsing closer to that implemented by shells, it is
248+
advisable to leave ``whitespace_split`` as ``False`` (the default value).
228249

229250

230251
.. attribute:: shlex.infile
@@ -245,10 +266,9 @@ variables which either control lexical analysis or can be used for debugging:
245266
This attribute is ``None`` by default. If you assign a string to it, that
246267
string will be recognized as a lexical-level inclusion request similar to the
247268
``source`` keyword in various shells. That is, the immediately following token
248-
will be opened as a filename and input will
249-
be taken from that stream until EOF, at which
250-
point the :meth:`~io.IOBase.close` method of that stream will be called and
251-
the input source will again become the original input stream. Source
269+
will be opened as a filename and input will be taken from that stream until
270+
EOF, at which point the :meth:`~io.IOBase.close` method of that stream will be
271+
called and the input source will again become the original input stream. Source
252272
requests may be stacked any number of levels deep.
253273

254274

@@ -275,6 +295,16 @@ variables which either control lexical analysis or can be used for debugging:
275295
(``''``), in non-POSIX mode, and to ``None`` in POSIX mode.
276296

277297

298+
.. attribute:: shlex.punctuation_chars
299+
300+
Characters that will be considered punctuation. Runs of punctuation
301+
characters will be returned as a single token. However, note that no
302+
semantic validity checking will be performed: for example, '>>>' could be
303+
returned as a token, even though it may not be recognised as such by shells.
304+
305+
.. versionadded:: 3.6
306+
307+
278308
.. _shlex-parsing-rules:
279309

280310
Parsing Rules
@@ -327,3 +357,62 @@ following parsing rules.
327357
* EOF is signaled with a :const:`None` value;
328358

329359
* Quoted empty strings (``''``) are allowed.
360+
361+
.. _improved-shell-compatibility:
362+
363+
Improved Compatibility with Shells
364+
----------------------------------
365+
366+
.. versionadded:: 3.6
367+
368+
The :class:`shlex` class provides compatibility with the parsing performed by
369+
common Unix shells like ``bash``, ``dash``, and ``sh``. To take advantage of
370+
this compatibility, specify the ``punctuation_chars`` argument in the
371+
constructor. This defaults to ``False``, which preserves pre-3.6 behaviour.
372+
However, if it is set to ``True``, then parsing of the characters ``();<>|&``
373+
is changed: any run of these characters is returned as a single token. While
374+
this is short of a full parser for shells (which would be out of scope for the
375+
standard library, given the multiplicity of shells out there), it does allow
376+
you to perform processing of command lines more easily than you could
377+
otherwise. To illustrate, you can see the difference in the following snippet::
378+
379+
import shlex
380+
381+
for punct in (False, True):
382+
if punct:
383+
message = 'Old'
384+
else:
385+
message = 'New'
386+
text = "a && b; c && d || e; f >'abc'; (def \"ghi\")"
387+
s = shlex.shlex(text, punctuation_chars=punct)
388+
print('%s: %s' % (message, list(s)))
389+
390+
which prints out::
391+
392+
Old: ['a', '&', '&', 'b', ';', 'c', '&', '&', 'd', '|', '|', 'e', ';', 'f', '>', "'abc'", ';', '(', 'def', '"ghi"', ')']
393+
New: ['a', '&&', 'b', ';', 'c', '&&', 'd', '||', 'e', ';', 'f', '>', "'abc'", ';', '(', 'def', '"ghi"', ')']
394+
395+
Of course, tokens will be returned which are not valid for shells, and you'll
396+
need to implement your own error checks on the returned tokens.
397+
398+
Instead of passing ``True`` as the value for the punctuation_chars parameter,
399+
you can pass a string with specific characters, which will be used to determine
400+
which characters constitute punctuation. For example::
401+
402+
>>> import shlex
403+
>>> s = shlex.shlex("a && b || c", punctuation_chars="|")
404+
>>> list(s)
405+
['a', '&', '&', 'b', '||', 'c']
406+
407+
.. note:: When ``punctuation_chars`` is specified, the :attr:`~shlex.wordchars`
408+
attribute is augmented with the characters ``~-./*?=``. That is because these
409+
characters can appear in file names (including wildcards) and command-line
410+
arguments (e.g. ``--color=auto``). Hence::
411+
412+
>>> import shlex
413+
>>> s = shlex.shlex('~/a && b-c --color=auto || d *.py?',
414+
... punctuation_chars=True)
415+
>>> list(s)
416+
['~/a', '&&', 'b-c', '--color=auto', '||', 'd', '*.py?']
417+
418+

Lib/shlex.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# push_source() and pop_source() made explicit by ESR, January 2001.
66
# Posix compliance, split(), string arguments, and
77
# iterator interface by Gustavo Niemeyer, April 2003.
8+
# changes to tokenize more like Posix shells by Vinay Sajip, July 2016.
89

910
import os
1011
import re
@@ -17,7 +18,8 @@
1718

1819
class shlex:
1920
"A lexical analyzer class for simple shell-like syntaxes."
20-
def __init__(self, instream=None, infile=None, posix=False):
21+
def __init__(self, instream=None, infile=None, posix=False,
22+
punctuation_chars=False):
2123
if isinstance(instream, str):
2224
instream = StringIO(instream)
2325
if instream is not None:
@@ -49,6 +51,19 @@ def __init__(self, instream=None, infile=None, posix=False):
4951
self.token = ''
5052
self.filestack = deque()
5153
self.source = None
54+
if not punctuation_chars:
55+
punctuation_chars = ''
56+
elif punctuation_chars is True:
57+
punctuation_chars = '();<>|&'
58+
self.punctuation_chars = punctuation_chars
59+
if punctuation_chars:
60+
# _pushback_chars is a push back queue used by lookahead logic
61+
self._pushback_chars = deque()
62+
# these chars added because allowed in file names, args, wildcards
63+
self.wordchars += '~-./*?='
64+
#remove any punctuation chars from wordchars
65+
t = self.wordchars.maketrans(dict.fromkeys(punctuation_chars))
66+
self.wordchars = self.wordchars.translate(t)
5267

5368
def push_token(self, tok):
5469
"Push a token onto the stack popped by the get_token method"
@@ -115,12 +130,15 @@ def read_token(self):
115130
quoted = False
116131
escapedstate = ' '
117132
while True:
118-
nextchar = self.instream.read(1)
133+
if self.punctuation_chars and self._pushback_chars:
134+
nextchar = self._pushback_chars.pop()
135+
else:
136+
nextchar = self.instream.read(1)
119137
if nextchar == '\n':
120-
self.lineno = self.lineno + 1
138+
self.lineno += 1
121139
if self.debug >= 3:
122-
print("shlex: in state", repr(self.state), \
123-
"I see character:", repr(nextchar))
140+
print("shlex: in state %r I see character: %r" % (self.state,
141+
nextchar))
124142
if self.state is None:
125143
self.token = '' # past end of file
126144
break
@@ -137,13 +155,16 @@ def read_token(self):
137155
continue
138156
elif nextchar in self.commenters:
139157
self.instream.readline()
140-
self.lineno = self.lineno + 1
158+
self.lineno += 1
141159
elif self.posix and nextchar in self.escape:
142160
escapedstate = 'a'
143161
self.state = nextchar
144162
elif nextchar in self.wordchars:
145163
self.token = nextchar
146164
self.state = 'a'
165+
elif nextchar in self.punctuation_chars:
166+
self.token = nextchar
167+
self.state = 'c'
147168
elif nextchar in self.quotes:
148169
if not self.posix:
149170
self.token = nextchar
@@ -166,17 +187,17 @@ def read_token(self):
166187
raise ValueError("No closing quotation")
167188
if nextchar == self.state:
168189
if not self.posix:
169-
self.token = self.token + nextchar
190+
self.token += nextchar
170191
self.state = ' '
171192
break
172193
else:
173194
self.state = 'a'
174-
elif self.posix and nextchar in self.escape and \
175-
self.state in self.escapedquotes:
195+
elif (self.posix and nextchar in self.escape and self.state
196+
in self.escapedquotes):
176197
escapedstate = self.state
177198
self.state = nextchar
178199
else:
179-
self.token = self.token + nextchar
200+
self.token += nextchar
180201
elif self.state in self.escape:
181202
if not nextchar: # end of file
182203
if self.debug >= 2:
@@ -185,12 +206,12 @@ def read_token(self):
185206
raise ValueError("No escaped character")
186207
# In posix shells, only the quote itself or the escape
187208
# character may be escaped within quotes.
188-
if escapedstate in self.quotes and \
189-
nextchar != self.state and nextchar != escapedstate:
190-
self.token = self.token + self.state
191-
self.token = self.token + nextchar
209+
if (escapedstate in self.quotes and
210+
nextchar != self.state and nextchar != escapedstate):
211+
self.token += self.state
212+
self.token += nextchar
192213
self.state = escapedstate
193-
elif self.state == 'a':
214+
elif self.state in ('a', 'c'):
194215
if not nextchar:
195216
self.state = None # end of file
196217
break
@@ -204,7 +225,7 @@ def read_token(self):
204225
continue
205226
elif nextchar in self.commenters:
206227
self.instream.readline()
207-
self.lineno = self.lineno + 1
228+
self.lineno += 1
208229
if self.posix:
209230
self.state = ' '
210231
if self.token or (self.posix and quoted):
@@ -216,15 +237,26 @@ def read_token(self):
216237
elif self.posix and nextchar in self.escape:
217238
escapedstate = 'a'
218239
self.state = nextchar
219-
elif nextchar in self.wordchars or nextchar in self.quotes \
220-
or self.whitespace_split:
221-
self.token = self.token + nextchar
240+
elif self.state == 'c':
241+
if nextchar in self.punctuation_chars:
242+
self.token += nextchar
243+
else:
244+
if nextchar not in self.whitespace:
245+
self._pushback_chars.append(nextchar)
246+
self.state = ' '
247+
break
248+
elif (nextchar in self.wordchars or nextchar in self.quotes
249+
or self.whitespace_split):
250+
self.token += nextchar
222251
else:
223-
self.pushback.appendleft(nextchar)
252+
if self.punctuation_chars:
253+
self._pushback_chars.append(nextchar)
254+
else:
255+
self.pushback.appendleft(nextchar)
224256
if self.debug >= 2:
225257
print("shlex: I see punctuation in word state")
226258
self.state = ' '
227-
if self.token:
259+
if self.token or (self.posix and quoted):
228260
break # emit current token
229261
else:
230262
continue

0 commit comments

Comments
 (0)