Skip to content

Commit 21e9d18

Browse files
committed
bpo-24869: Add startline and endline attributes to the tokens returned by shlex.
1 parent 2c8a5e4 commit 21e9d18

3 files changed

Lines changed: 56 additions & 11 deletions

File tree

‎Doc/library/shlex.rst‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ The :mod:`shlex` module defines the following functions:
3030
in POSIX mode by default, but uses non-POSIX mode if the *posix* argument is
3131
false.
3232

33+
The returned tokens have attributes ``startline`` giving the line
34+
number of the first character of the token, and ``endline`` giving
35+
the line number of the last character of the token.
36+
3337
.. note::
3438

3539
Since the :func:`split` function instantiates a :class:`~shlex.shlex`
@@ -124,6 +128,10 @@ A :class:`~shlex.shlex` instance has the following methods:
124128
encounters an immediate end-of-file, :attr:`eof` is returned (the empty
125129
string (``''``) in non-POSIX mode, and ``None`` in POSIX mode).
126130

131+
Tokens read from the input stream have attributes ``startline`` giving the
132+
line number of the first character of the token, and ``endline`` giving the
133+
line number of the last character of the token.
134+
127135

128136
.. method:: shlex.push_token(str)
129137

@@ -136,6 +144,10 @@ A :class:`~shlex.shlex` instance has the following methods:
136144
requests. (This is not ordinarily a useful entry point, and is documented here
137145
only for the sake of completeness.)
138146

147+
The returned token has attributes ``startline`` giving the line
148+
number of the first character of the token, and ``endline`` giving
149+
the line number of the last character of the token.
150+
139151

140152
.. method:: shlex.sourcehook(filename)
141153

‎Lib/shlex.py‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@
1616

1717
__all__ = ["shlex", "split", "quote"]
1818

19+
20+
class Token(str):
21+
"A string with startline and endline attributes."
22+
__slots__ = ['startline', 'endline']
23+
24+
def __new__(cls, token, startline, endline):
25+
self = super().__new__(cls, token)
26+
self.startline = startline
27+
self.endline = endline
28+
return self
29+
30+
1931
class shlex:
2032
"A lexical analyzer class for simple shell-like syntaxes."
2133
def __init__(self, instream=None, infile=None, posix=False,
@@ -129,6 +141,18 @@ def get_token(self):
129141
def read_token(self):
130142
quoted = False
131143
escapedstate = ' '
144+
startline = endline = self.lineno
145+
146+
def start_token(s):
147+
self.token = s
148+
nonlocal startline, endline
149+
startline = endline = self.lineno
150+
151+
def continue_token(s):
152+
self.token += s
153+
nonlocal endline
154+
endline = self.lineno
155+
132156
while True:
133157
if self.punctuation_chars and self._pushback_chars:
134158
nextchar = self._pushback_chars.pop()
@@ -140,7 +164,7 @@ def read_token(self):
140164
print("shlex: in state %r I see character: %r" % (self.state,
141165
nextchar))
142166
if self.state is None:
143-
self.token = '' # past end of file
167+
start_token('') # past end of file
144168
break
145169
elif self.state == ' ':
146170
if not nextchar:
@@ -160,20 +184,20 @@ def read_token(self):
160184
escapedstate = 'a'
161185
self.state = nextchar
162186
elif nextchar in self.wordchars:
163-
self.token = nextchar
187+
start_token(nextchar)
164188
self.state = 'a'
165189
elif nextchar in self.punctuation_chars:
166-
self.token = nextchar
190+
start_token(nextchar)
167191
self.state = 'c'
168192
elif nextchar in self.quotes:
169193
if not self.posix:
170-
self.token = nextchar
194+
start_token(nextchar)
171195
self.state = nextchar
172196
elif self.whitespace_split:
173-
self.token = nextchar
197+
start_token(nextchar)
174198
self.state = 'a'
175199
else:
176-
self.token = nextchar
200+
start_token(nextchar)
177201
if self.token or (self.posix and quoted):
178202
break # emit current token
179203
else:
@@ -187,7 +211,7 @@ def read_token(self):
187211
raise ValueError("No closing quotation")
188212
if nextchar == self.state:
189213
if not self.posix:
190-
self.token += nextchar
214+
continue_token(nextchar)
191215
self.state = ' '
192216
break
193217
else:
@@ -197,7 +221,7 @@ def read_token(self):
197221
escapedstate = self.state
198222
self.state = nextchar
199223
else:
200-
self.token += nextchar
224+
continue_token(nextchar)
201225
elif self.state in self.escape:
202226
if not nextchar: # end of file
203227
if self.debug >= 2:
@@ -209,7 +233,7 @@ def read_token(self):
209233
if (escapedstate in self.quotes and
210234
nextchar != self.state and nextchar != escapedstate):
211235
self.token += self.state
212-
self.token += nextchar
236+
continue_token(nextchar)
213237
self.state = escapedstate
214238
elif self.state in ('a', 'c'):
215239
if not nextchar:
@@ -234,7 +258,7 @@ def read_token(self):
234258
continue
235259
elif self.state == 'c':
236260
if nextchar in self.punctuation_chars:
237-
self.token += nextchar
261+
continue_token(nextchar)
238262
else:
239263
if nextchar not in self.whitespace:
240264
self._pushback_chars.append(nextchar)
@@ -247,7 +271,7 @@ def read_token(self):
247271
self.state = nextchar
248272
elif (nextchar in self.wordchars or nextchar in self.quotes
249273
or self.whitespace_split):
250-
self.token += nextchar
274+
continue_token(nextchar)
251275
else:
252276
if self.punctuation_chars:
253277
self._pushback_chars.append(nextchar)
@@ -264,6 +288,8 @@ def read_token(self):
264288
self.token = ''
265289
if self.posix and not quoted and result == '':
266290
result = None
291+
else:
292+
result = Token(result, startline, endline)
267293
if self.debug > 1:
268294
if result:
269295
print("shlex: raw token=" + repr(result))

‎Lib/test/test_shlex.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,13 @@ def testQuote(self):
308308
self.assertEqual(shlex.quote("test%s'name'" % u),
309309
"'test%s'\"'\"'name'\"'\"''" % u)
310310

311+
def testLineNumbers(self):
312+
"""Test that tokens have correct line number attributes."""
313+
source = '1\n2 "2\n3" 3\n4 "4\n5\n6"'
314+
expected = [(1, 1), (2, 2), (2, 3), (3, 3), (4, 4), (4, 6)]
315+
found = [(t.startline, t.endline) for t in shlex.split(source)]
316+
self.assertEqual(expected, found)
317+
311318
# Allow this test to be used with old shlex.py
312319
if not getattr(shlex, "split", None):
313320
for methname in dir(ShlexTest):

0 commit comments

Comments
 (0)