Skip to content

Commit 68dd540

Browse files
authored
[3.13] gh-154007: Improve test coverage for the shlex module (GH-154009) (#154458)
(cherry picked from commit 0eac28e)
1 parent d249782 commit 68dd540

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lib/test/test_shlex.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import io
22
import itertools
3+
import os
34
import shlex
45
import string
6+
import tempfile
57
import unittest
8+
from unittest.mock import patch
69

710

811
# The original test data set was from shellwords, by Hartmut Goebel.
@@ -363,6 +366,203 @@ def testPunctuationCharsReadOnly(self):
363366
with self.assertRaises(AttributeError):
364367
shlex_instance.punctuation_chars = False
365368

369+
def testLinenoAfterNewLine(self):
370+
s = shlex.shlex("line 1\nline 2")
371+
self.assertEqual(s.lineno, 1) # before consumption
372+
list(s)
373+
self.assertEqual(s.lineno, 2)
374+
375+
def testLinenoAfterComment(self):
376+
"""Comment handler increments lineno even without a trailing newline."""
377+
s = shlex.shlex("line 1 # line 2")
378+
list(s)
379+
self.assertEqual(s.lineno, 2)
380+
381+
def testPushToken(self):
382+
s = shlex.shlex("b c")
383+
s.push_token("a")
384+
self.assertListEqual(list(s), ["a", "b", "c"])
385+
386+
def testPushTokenLifo(self):
387+
s = shlex.shlex("")
388+
s.push_token("first")
389+
s.push_token("last")
390+
self.assertListEqual(list(s), ["last", "first"])
391+
392+
def testPushTokenDebug(self):
393+
s = shlex.shlex("")
394+
s.debug = 1
395+
tok = "a"
396+
with patch("builtins.print") as mock_print:
397+
s.push_token(tok)
398+
mock_print.assert_called_once_with(f"shlex: pushing token {tok!r}")
399+
400+
def testPushSourceString(self):
401+
s = shlex.shlex("world")
402+
s.push_source("hello")
403+
self.assertListEqual(list(s), ["hello", "world"])
404+
405+
def testPushSourceStream(self):
406+
s = shlex.shlex("world")
407+
s.push_source(io.StringIO("hello"))
408+
self.assertListEqual(list(s), ["hello", "world"])
409+
410+
def testPushSourceStreamDebug(self):
411+
s = shlex.shlex("")
412+
stream = io.StringIO("hello")
413+
s.debug = 1
414+
with patch("builtins.print") as mock_print:
415+
s.push_source(stream)
416+
mock_print.assert_called_once_with(f"shlex: pushing to stream {stream}")
417+
418+
def testPushSourceNewfile(self):
419+
"""shlex.push_source sets infile to newfile; pop_source restores the original on exhaustion."""
420+
original_file = "original.sh"
421+
new_file = "new.sh"
422+
s = shlex.shlex("b", infile=original_file)
423+
s.debug = 1
424+
with patch("builtins.print") as mock_print:
425+
s.push_source("a", newfile=new_file)
426+
mock_print.assert_called_once_with(f"shlex: pushing to file {new_file}")
427+
self.assertEqual(s.infile, new_file)
428+
s.debug = 0
429+
list(s)
430+
self.assertEqual(s.infile, original_file)
431+
432+
def testPopSourceDebug(self):
433+
"""pop_source emits debug output when debug is set."""
434+
s = shlex.shlex("b")
435+
original_stream = s.instream
436+
s.push_source("a")
437+
s.debug = 1
438+
with patch("builtins.print") as mock_print:
439+
list(s) # exhausts pushed source and triggers pop_source internally
440+
mock_print.assert_any_call(f"shlex: popping to {original_stream}, line 1")
441+
442+
def testErrorLeaderTracksPosition(self):
443+
infile_label = "test.sh"
444+
s = shlex.shlex("line 1\nline 2", infile=infile_label)
445+
list(s)
446+
result = s.error_leader()
447+
self.assertEqual(result, f'"{infile_label}", line 2: ')
448+
449+
def testErrorLeaderOverrides(self):
450+
s = shlex.shlex("foo", infile="original.sh")
451+
infile_label_override = "override.sh"
452+
lineno_override = 42
453+
result = s.error_leader(infile=infile_label_override, lineno=lineno_override)
454+
self.assertEqual(result, f'"{infile_label_override}", line {lineno_override}: ')
455+
456+
def testNoClosingQuotation(self):
457+
s = shlex.shlex('"foo')
458+
with self.assertRaisesRegex(ValueError, "No closing quotation"):
459+
list(s)
460+
461+
def testNoEscapedCharacter(self):
462+
s = shlex.shlex("\\", posix=True)
463+
with self.assertRaisesRegex(ValueError, "No escaped character"):
464+
list(s)
465+
466+
def testSourcehookStripsQuotes(self):
467+
with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete_on_close=False) as f:
468+
f.write("hello")
469+
f.close()
470+
s = shlex.shlex("")
471+
newfile, stream = s.sourcehook(f'"{f.name}"')
472+
stream.close()
473+
self.assertEqual(newfile, f.name)
474+
475+
def testSourcehookAbsolutePath(self):
476+
with tempfile.NamedTemporaryFile(mode="w", delete_on_close=False) as f:
477+
f.close()
478+
s = shlex.shlex("", infile="/some/dir/main.sh")
479+
newfile, stream = s.sourcehook(f.name)
480+
stream.close()
481+
self.assertEqual(newfile, f.name)
482+
483+
def testSourcehookRelativePath(self):
484+
with tempfile.TemporaryDirectory() as d:
485+
fpath = os.path.join(d, "included.sh")
486+
with open(fpath, "w"):
487+
pass
488+
s = shlex.shlex("", infile=os.path.join(d, "main.sh"))
489+
newfile, stream = s.sourcehook("included.sh")
490+
stream.close()
491+
self.assertEqual(newfile, fpath)
492+
493+
def testSourceInclusion(self):
494+
"""shlex.source sets a trigger keyword: when the lexer reads a token equal
495+
to it, the next token is consumed as a filename and passed to
496+
sourcehook, which returns a stream to push onto the input stack.
497+
Tokens flow from that stream first, then resume from the original.
498+
"""
499+
s = shlex.shlex("trigger filename remaining")
500+
s.source = "trigger"
501+
s.sourcehook = lambda f: (f, io.StringIO("included"))
502+
self.assertEqual(list(s), ["included", "remaining"])
503+
504+
def testGetTokenPopsPushbackDebug(self):
505+
s = shlex.shlex("")
506+
s.push_token("hello")
507+
s.debug = 1 # set after push_token to isolate the pop-token branch
508+
with patch("builtins.print") as mock_print:
509+
tok = s.get_token()
510+
self.assertEqual(tok, "hello")
511+
mock_print.assert_called_once_with("shlex: popping token 'hello'")
512+
513+
def testDebugWhitespaceInWhitespaceState(self):
514+
s = shlex.shlex(" a")
515+
s.debug = 2
516+
with patch("builtins.print") as mock_print:
517+
list(s)
518+
mock_print.assert_any_call("shlex: I see whitespace in whitespace state")
519+
520+
def testDebugWhitespaceInWordState(self):
521+
s = shlex.shlex("a b")
522+
s.debug = 2
523+
with patch("builtins.print") as mock_print:
524+
list(s)
525+
mock_print.assert_any_call("shlex: I see whitespace in word state")
526+
527+
def testDebugPunctuationInWordState(self):
528+
s = shlex.shlex("a(")
529+
s.debug = 2
530+
with patch("builtins.print") as mock_print:
531+
list(s)
532+
mock_print.assert_any_call("shlex: I see punctuation in word state")
533+
534+
def testDebugRawToken(self):
535+
s = shlex.shlex("hello")
536+
s.debug = 2
537+
with patch("builtins.print") as mock_print:
538+
list(s)
539+
mock_print.assert_any_call("shlex: raw token='hello'")
540+
541+
def testDebugEOFInQuote(self):
542+
s = shlex.shlex('"oops', posix=True)
543+
s.debug = 2
544+
with patch('builtins.print') as mock_print:
545+
with self.assertRaises(ValueError):
546+
list(s)
547+
msgs = [call.args[0] for call in mock_print.call_args_list]
548+
self.assertTrue(any("EOF in quotes" in m for m in msgs))
549+
550+
def testDebugEOFInEscape(self):
551+
s = shlex.shlex("oops\\", posix=True)
552+
s.debug = 2
553+
with patch("builtins.print") as mock_print:
554+
with self.assertRaises(ValueError):
555+
list(s)
556+
msgs = [call.args[0] for call in mock_print.call_args_list]
557+
self.assertTrue(any("EOF in escape" in m for m in msgs))
558+
559+
def testDebugStateTrace(self):
560+
s = shlex.shlex("a")
561+
s.debug = 3
562+
with patch("builtins.print") as mock_print:
563+
list(s)
564+
mock_print.assert_any_call("shlex: in state ' ' I see character: 'a'")
565+
366566

367567
# Allow this test to be used with old shlex.py
368568
if not getattr(shlex, "split", None):

0 commit comments

Comments
 (0)