Skip to content

Commit 8afc311

Browse files
committed
gh-154007: Improve test coverage for the shlex module
1 parent 9adef68 commit 8afc311

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

Lib/test/test_shlex.py

Lines changed: 202 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
from test.support import cpython_only
710
from test.support import import_helper
811

@@ -376,6 +379,205 @@ def testPunctuationCharsReadOnly(self):
376379
with self.assertRaises(AttributeError):
377380
shlex_instance.punctuation_chars = False
378381

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

0 commit comments

Comments
 (0)