]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-154007: Improve test coverage for the `shlex` module (#154009)
authorPiotr Kaznowski <piotr@kazno.dev>
Wed, 22 Jul 2026 08:08:03 +0000 (10:08 +0200)
committerGitHub <noreply@github.com>
Wed, 22 Jul 2026 08:08:03 +0000 (10:08 +0200)
Lib/test/test_shlex.py

index 2adaee81b063085d05809e620a131dfc77780469..4c0cd88bbcda14fc469b77cf9156fb714abe20ae 100644 (file)
@@ -1,8 +1,11 @@
 import io
 import itertools
+import os
 import shlex
 import string
+import tempfile
 import unittest
+from unittest.mock import patch
 from test.support import cpython_only
 from test.support import import_helper
 
@@ -376,6 +379,203 @@ class ShlexTest(unittest.TestCase):
         with self.assertRaises(AttributeError):
             shlex_instance.punctuation_chars = False
 
+    def testLinenoAfterNewLine(self):
+        s = shlex.shlex("line 1\nline 2")
+        self.assertEqual(s.lineno, 1)  # before consumption
+        list(s)
+        self.assertEqual(s.lineno, 2)
+
+    def testLinenoAfterComment(self):
+        """Comment handler increments lineno even without a trailing newline."""
+        s = shlex.shlex("line 1 # line 2")
+        list(s)
+        self.assertEqual(s.lineno, 2)
+
+    def testPushToken(self):
+        s = shlex.shlex("b c")
+        s.push_token("a")
+        self.assertListEqual(list(s), ["a", "b", "c"])
+
+    def testPushTokenLifo(self):
+        s = shlex.shlex("")
+        s.push_token("first")
+        s.push_token("last")
+        self.assertListEqual(list(s), ["last", "first"])
+
+    def testPushTokenDebug(self):
+        s = shlex.shlex("")
+        s.debug = 1
+        tok = "a"
+        with patch("builtins.print") as mock_print:
+            s.push_token(tok)
+        mock_print.assert_called_once_with(f"shlex: pushing token {tok!r}")
+
+    def testPushSourceString(self):
+        s = shlex.shlex("world")
+        s.push_source("hello")
+        self.assertListEqual(list(s), ["hello", "world"])
+
+    def testPushSourceStream(self):
+        s = shlex.shlex("world")
+        s.push_source(io.StringIO("hello"))
+        self.assertListEqual(list(s), ["hello", "world"])
+
+    def testPushSourceStreamDebug(self):
+        s = shlex.shlex("")
+        stream = io.StringIO("hello")
+        s.debug = 1
+        with patch("builtins.print") as mock_print:
+            s.push_source(stream)
+        mock_print.assert_called_once_with(f"shlex: pushing to stream {stream}")
+
+    def testPushSourceNewfile(self):
+        """shlex.push_source sets infile to newfile; pop_source restores the original on exhaustion."""
+        original_file = "original.sh"
+        new_file = "new.sh"
+        s = shlex.shlex("b", infile=original_file)
+        s.debug = 1
+        with patch("builtins.print") as mock_print:
+            s.push_source("a", newfile=new_file)
+        mock_print.assert_called_once_with(f"shlex: pushing to file {new_file}")
+        self.assertEqual(s.infile, new_file)
+        s.debug = 0
+        list(s)
+        self.assertEqual(s.infile, original_file)
+
+    def testPopSourceDebug(self):
+        """pop_source emits debug output when debug is set."""
+        s = shlex.shlex("b")
+        original_stream = s.instream
+        s.push_source("a")
+        s.debug = 1
+        with patch("builtins.print") as mock_print:
+            list(s)  # exhausts pushed source and triggers pop_source internally
+        mock_print.assert_any_call(f"shlex: popping to {original_stream}, line 1")
+
+    def testErrorLeaderTracksPosition(self):
+        infile_label = "test.sh"
+        s = shlex.shlex("line 1\nline 2", infile=infile_label)
+        list(s)
+        result = s.error_leader()
+        self.assertEqual(result, f'"{infile_label}", line 2: ')
+
+    def testErrorLeaderOverrides(self):
+        s = shlex.shlex("foo", infile="original.sh")
+        infile_label_override = "override.sh"
+        lineno_override = 42
+        result = s.error_leader(infile=infile_label_override, lineno=lineno_override)
+        self.assertEqual(result, f'"{infile_label_override}", line {lineno_override}: ')
+
+    def testNoClosingQuotation(self):
+        s = shlex.shlex('"foo')
+        with self.assertRaisesRegex(ValueError, "No closing quotation"):
+            list(s)
+
+    def testNoEscapedCharacter(self):
+        s = shlex.shlex("\\", posix=True)
+        with self.assertRaisesRegex(ValueError, "No escaped character"):
+            list(s)
+
+    def testSourcehookStripsQuotes(self):
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete_on_close=False) as f:
+            f.write("hello")
+            f.close()
+            s = shlex.shlex("")
+            newfile, stream = s.sourcehook(f'"{f.name}"')
+            stream.close()
+        self.assertEqual(newfile, f.name)
+
+    def testSourcehookAbsolutePath(self):
+        with tempfile.NamedTemporaryFile(mode="w", delete_on_close=False) as f:
+            f.close()
+            s = shlex.shlex("", infile="/some/dir/main.sh")
+            newfile, stream = s.sourcehook(f.name)
+            stream.close()
+        self.assertEqual(newfile, f.name)
+
+    def testSourcehookRelativePath(self):
+        with tempfile.TemporaryDirectory() as d:
+            fpath = os.path.join(d, "included.sh")
+            with open(fpath, "w"):
+                pass
+            s = shlex.shlex("", infile=os.path.join(d, "main.sh"))
+            newfile, stream = s.sourcehook("included.sh")
+            stream.close()
+            self.assertEqual(newfile, fpath)
+
+    def testSourceInclusion(self):
+        """shlex.source sets a trigger keyword: when the lexer reads a token equal
+        to it, the next token is consumed as a filename and passed to
+        sourcehook, which returns a stream to push onto the input stack.
+        Tokens flow from that stream first, then resume from the original.
+        """
+        s = shlex.shlex("trigger filename remaining")
+        s.source = "trigger"
+        s.sourcehook = lambda f: (f, io.StringIO("included"))
+        self.assertEqual(list(s), ["included", "remaining"])
+
+    def testGetTokenPopsPushbackDebug(self):
+        s = shlex.shlex("")
+        s.push_token("hello")
+        s.debug = 1  # set after push_token to isolate the pop-token branch
+        with patch("builtins.print") as mock_print:
+            tok = s.get_token()
+        self.assertEqual(tok, "hello")
+        mock_print.assert_called_once_with("shlex: popping token 'hello'")
+
+    def testDebugWhitespaceInWhitespaceState(self):
+        s = shlex.shlex(" a")
+        s.debug = 2
+        with patch("builtins.print") as mock_print:
+            list(s)
+        mock_print.assert_any_call("shlex: I see whitespace in whitespace state")
+
+    def testDebugWhitespaceInWordState(self):
+        s = shlex.shlex("a b")
+        s.debug = 2
+        with patch("builtins.print") as mock_print:
+            list(s)
+        mock_print.assert_any_call("shlex: I see whitespace in word state")
+
+    def testDebugPunctuationInWordState(self):
+        s = shlex.shlex("a(")
+        s.debug = 2
+        with patch("builtins.print") as mock_print:
+            list(s)
+        mock_print.assert_any_call("shlex: I see punctuation in word state")
+
+    def testDebugRawToken(self):
+        s = shlex.shlex("hello")
+        s.debug = 2
+        with patch("builtins.print") as mock_print:
+            list(s)
+        mock_print.assert_any_call("shlex: raw token='hello'")
+
+    def testDebugEOFInQuote(self):
+        s = shlex.shlex('"oops', posix=True)
+        s.debug = 2
+        with patch('builtins.print') as mock_print:
+            with self.assertRaises(ValueError):
+                list(s)
+        msgs = [call.args[0] for call in mock_print.call_args_list]
+        self.assertTrue(any("EOF in quotes" in m for m in msgs))
+
+    def testDebugEOFInEscape(self):
+        s = shlex.shlex("oops\\", posix=True)
+        s.debug = 2
+        with patch("builtins.print") as mock_print:
+            with self.assertRaises(ValueError):
+                list(s)
+        msgs = [call.args[0] for call in mock_print.call_args_list]
+        self.assertTrue(any("EOF in escape" in m for m in msgs))
+
+    def testDebugStateTrace(self):
+        s = shlex.shlex("a")
+        s.debug = 3
+        with patch("builtins.print") as mock_print:
+            list(s)
+        mock_print.assert_any_call("shlex: in state ' ' I see character: 'a'")
+
     @cpython_only
     def test_lazy_imports(self):
         import_helper.ensure_lazy_imports('shlex', {'collections', 're', 'os'})