|
1 | 1 | import io |
2 | 2 | import itertools |
| 3 | +import os |
3 | 4 | import shlex |
4 | 5 | import string |
| 6 | +import tempfile |
5 | 7 | import unittest |
| 8 | +from unittest.mock import patch |
6 | 9 | from test.support import cpython_only |
7 | 10 | from test.support import import_helper |
8 | 11 |
|
@@ -376,9 +379,206 @@ def testPunctuationCharsReadOnly(self): |
376 | 379 | with self.assertRaises(AttributeError): |
377 | 380 | shlex_instance.punctuation_chars = False |
378 | 381 |
|
| 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.assertRaisesRegex(ValueError, "No closing quotation"): |
| 472 | + list(s) |
| 473 | + |
| 474 | + def testNoEscapedCharacter(self): |
| 475 | + s = shlex.shlex("\\", posix=True) |
| 476 | + with self.assertRaisesRegex(ValueError, "No escaped character"): |
| 477 | + list(s) |
| 478 | + |
| 479 | + def testSourcehookStripsQuotes(self): |
| 480 | + with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete_on_close=False) as f: |
| 481 | + f.write("hello") |
| 482 | + f.close() |
| 483 | + s = shlex.shlex("") |
| 484 | + newfile, stream = s.sourcehook(f'"{f.name}"') |
| 485 | + stream.close() |
| 486 | + self.assertEqual(newfile, f.name) |
| 487 | + |
| 488 | + def testSourcehookAbsolutePath(self): |
| 489 | + with tempfile.NamedTemporaryFile(mode="w", delete_on_close=False) as f: |
| 490 | + f.close() |
| 491 | + s = shlex.shlex("", infile="/some/dir/main.sh") |
| 492 | + newfile, stream = s.sourcehook(f.name) |
| 493 | + stream.close() |
| 494 | + self.assertEqual(newfile, f.name) |
| 495 | + |
| 496 | + def testSourcehookRelativePath(self): |
| 497 | + with tempfile.TemporaryDirectory() as d: |
| 498 | + fpath = os.path.join(d, "included.sh") |
| 499 | + with open(fpath, "w"): |
| 500 | + pass |
| 501 | + s = shlex.shlex("", infile=os.path.join(d, "main.sh")) |
| 502 | + newfile, stream = s.sourcehook("included.sh") |
| 503 | + stream.close() |
| 504 | + self.assertEqual(newfile, fpath) |
| 505 | + |
| 506 | + def testSourceInclusion(self): |
| 507 | + """shlex.source sets a trigger keyword: when the lexer reads a token equal |
| 508 | + to it, the next token is consumed as a filename and passed to |
| 509 | + sourcehook, which returns a stream to push onto the input stack. |
| 510 | + Tokens flow from that stream first, then resume from the original. |
| 511 | + """ |
| 512 | + s = shlex.shlex("trigger filename remaining") |
| 513 | + s.source = "trigger" |
| 514 | + s.sourcehook = lambda f: (f, io.StringIO("included")) |
| 515 | + self.assertEqual(list(s), ["included", "remaining"]) |
| 516 | + |
| 517 | + def testGetTokenPopsPushbackDebug(self): |
| 518 | + s = shlex.shlex("") |
| 519 | + s.push_token("hello") |
| 520 | + s.debug = 1 # set after push_token to isolate the pop-token branch |
| 521 | + with patch("builtins.print") as mock_print: |
| 522 | + tok = s.get_token() |
| 523 | + self.assertEqual(tok, "hello") |
| 524 | + mock_print.assert_called_once_with("shlex: popping token 'hello'") |
| 525 | + |
| 526 | + def testDebugWhitespaceInWhitespaceState(self): |
| 527 | + with patch("builtins.print") as mock_print: |
| 528 | + s = shlex.shlex(" a") |
| 529 | + s.debug = 2 |
| 530 | + list(s) |
| 531 | + mock_print.assert_any_call("shlex: I see whitespace in whitespace state") |
| 532 | + |
| 533 | + def testDebugWhitespaceInWordState(self): |
| 534 | + with patch("builtins.print") as mock_print: |
| 535 | + s = shlex.shlex("a b") |
| 536 | + s.debug = 2 |
| 537 | + list(s) |
| 538 | + mock_print.assert_any_call("shlex: I see whitespace in word state") |
| 539 | + |
| 540 | + def testDebugPunctuationInWordState(self): |
| 541 | + with patch("builtins.print") as mock_print: |
| 542 | + s = shlex.shlex("a(") |
| 543 | + s.debug = 2 |
| 544 | + list(s) |
| 545 | + mock_print.assert_any_call("shlex: I see punctuation in word state") |
| 546 | + |
| 547 | + def testDebugRawToken(self): |
| 548 | + with patch("builtins.print") as mock_print: |
| 549 | + s = shlex.shlex("hello") |
| 550 | + s.debug = 2 |
| 551 | + list(s) |
| 552 | + mock_print.assert_any_call("shlex: raw token='hello'") |
| 553 | + |
| 554 | + def testDebugEOFInQuote(self): |
| 555 | + s = shlex.shlex('"oops', posix=True) |
| 556 | + s.debug = 2 |
| 557 | + with patch('builtins.print') as mock_print: |
| 558 | + with self.assertRaises(ValueError): |
| 559 | + list(s) |
| 560 | + msgs = [call.args[0] for call in mock_print.call_args_list] |
| 561 | + self.assertTrue(any("EOF in quotes" in m for m in msgs)) |
| 562 | + |
| 563 | + def testDebugEOFInEscape(self): |
| 564 | + s = shlex.shlex("oops\\", posix=True) |
| 565 | + s.debug = 2 |
| 566 | + with patch("builtins.print") as mock_print: |
| 567 | + with self.assertRaises(ValueError): |
| 568 | + list(s) |
| 569 | + msgs = [call.args[0] for call in mock_print.call_args_list] |
| 570 | + self.assertTrue(any("EOF in escape" in m for m in msgs)) |
| 571 | + |
| 572 | + def testDebugStateTrace(self): |
| 573 | + s = shlex.shlex("a") |
| 574 | + s.debug = 3 |
| 575 | + with patch("builtins.print") as mock_print: |
| 576 | + list(s) |
| 577 | + mock_print.assert_any_call("shlex: in state ' ' I see character: 'a'") |
| 578 | + |
379 | 579 | @cpython_only |
380 | 580 | def test_lazy_imports(self): |
381 | | - import_helper.ensure_lazy_imports('shlex', {'collections', 're', 'os'}) |
| 581 | + import_helper.ensure_lazy_imports("shlex", {"collections", "re", "os"}) |
382 | 582 |
|
383 | 583 |
|
384 | 584 | # Allow this test to be used with old shlex.py |
|
0 commit comments