pureshellcheck 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {pureshellcheck-0.2.1/src/pureshellcheck.egg-info → pureshellcheck-0.2.2}/PKG-INFO +1 -1
  2. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/pyproject.toml +1 -1
  3. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/__init__.py +1 -1
  4. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/parser.py +73 -0
  5. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2/src/pureshellcheck.egg-info}/PKG-INFO +1 -1
  6. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/LICENSE +0 -0
  7. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/MANIFEST.in +0 -0
  8. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/README.md +0 -0
  9. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/setup.cfg +0 -0
  10. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/analyzer.py +0 -0
  11. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/astlib.py +0 -0
  12. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/checks/__init__.py +0 -0
  13. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/checks/commands.py +0 -0
  14. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/checks/misc.py +0 -0
  15. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/checks/quoting.py +0 -0
  16. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/checks/variables.py +0 -0
  17. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/cli.py +0 -0
  18. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/shast.py +0 -0
  19. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/varflow.py +0 -0
  20. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck/varscan.py +0 -0
  21. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck.egg-info/SOURCES.txt +0 -0
  22. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck.egg-info/dependency_links.txt +0 -0
  23. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck.egg-info/entry_points.txt +0 -0
  24. {pureshellcheck-0.2.1 → pureshellcheck-0.2.2}/src/pureshellcheck.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pureshellcheck
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A pure Python reimplementation of ShellCheck's most common checks
5
5
  Author: adam2go
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pureshellcheck"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A pure Python reimplementation of ShellCheck's most common checks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -6,7 +6,7 @@ common checks.
6
6
  ... print(finding.line, finding.column, finding.code, finding.message)
7
7
  """
8
8
 
9
- __version__ = "0.2.1"
9
+ __version__ = "0.2.2"
10
10
 
11
11
  from .analyzer import Finding, run_checks # noqa: F401
12
12
  from .parser import ParseError, parse # noqa: F401
@@ -22,6 +22,15 @@ END_KEYWORDS = {
22
22
  # Characters that terminate an unquoted word.
23
23
  METACHARS = " \t\n;&|<>()"
24
24
 
25
+ # Cap on nested constructs - subshells "(", command substitutions "$(",
26
+ # brace/arithmetic groups - which re-enter read_command or read_arith_primary.
27
+ # Arithmetic nesting is the most frame-hungry (its precedence chain adds ~15
28
+ # interpreter frames per level), so 40 trips at ~600 frames: comfortably under
29
+ # CPython's default 1000 limit on every platform (including Windows' ~1 MB
30
+ # stack) even from a deep call site, yet far above any real script. Deeply
31
+ # nested input like "$( $( $(" or "(((((" raises a clean ParseError.
32
+ MAX_NESTING = 40
33
+
25
34
  UNQUOTED_RUN = re.compile(r"""[^\\'"$`*?\[\](){}<>|&;\s!#~]+""")
26
35
  DQUOTE_RUN = re.compile(r'[^"\\$`]+')
27
36
  SQUOTE_END = re.compile(r"'")
@@ -81,6 +90,17 @@ class ParseError(Exception):
81
90
  self.code = code
82
91
 
83
92
 
93
+ class _NestingLimit(Exception):
94
+ """Raised when input nests deeper than MAX_NESTING. Deliberately NOT a
95
+ ParseError so the parser's backtracking (which catches ParseError to try
96
+ alternatives) cannot swallow it; parse() turns it into a ParseError."""
97
+
98
+ def __init__(self, message, pos):
99
+ super().__init__(message)
100
+ self.message = message
101
+ self.pos = pos
102
+
103
+
84
104
  class Directive:
85
105
  """A `# shellcheck key=value` comment."""
86
106
 
@@ -104,6 +124,11 @@ class Parser:
104
124
  self.pending_heredocs = []
105
125
  self.directives = []
106
126
  self.line_had_command = False
127
+ # Smallest index from which a glob-class scan reached EOF with no
128
+ # closing ']' or metachar; from there on every '[' is a literal.
129
+ # Caps the otherwise O(n^2) rescan on input like "[1,[1,[1,...".
130
+ self._glob_eof_from = self.n + 1
131
+ self._nesting = 0 # current compound-command nesting depth
107
132
 
108
133
  # ------------------------------------------------------------------
109
134
  # Low-level helpers
@@ -192,6 +217,13 @@ class Parser:
192
217
  # Script / lists
193
218
 
194
219
  def parse(self):
220
+ try:
221
+ return self._parse()
222
+ except _NestingLimit as e:
223
+ # Surface the internal depth-limit signal as the public error type.
224
+ raise ParseError(e.message, e.pos) from None
225
+
226
+ def _parse(self):
195
227
  shebang = None
196
228
  if self.src.startswith("#!"):
197
229
  end = self.src.find("\n")
@@ -340,6 +372,20 @@ class Parser:
340
372
  # Commands
341
373
 
342
374
  def read_command(self):
375
+ # Every nested compound command - subshell, command substitution,
376
+ # brace/arithmetic group - re-enters here, so this bounds parser
377
+ # recursion and keeps adversarial input from overflowing the stack.
378
+ self._nesting += 1
379
+ if self._nesting > MAX_NESTING:
380
+ self._nesting -= 1
381
+ raise _NestingLimit("commands nested too deeply (over %d levels)"
382
+ % MAX_NESTING, self.i)
383
+ try:
384
+ return self._read_command()
385
+ finally:
386
+ self._nesting -= 1
387
+
388
+ def _read_command(self):
343
389
  self.skip_inline_ws()
344
390
  if self.at_end():
345
391
  return None
@@ -1366,11 +1412,17 @@ class Parser:
1366
1412
  """Parse [...] as a glob character class, else literal '['."""
1367
1413
  src, n = self.src, self.n
1368
1414
  start = self.i
1415
+ # We already proved the rest of the input has no closing ']' (nor a
1416
+ # metachar) from an earlier '['; this one can't open a class either.
1417
+ if start >= self._glob_eof_from:
1418
+ self.i = start + 1
1419
+ return Node("T_Literal", start, self.i, text="[")
1369
1420
  j = self.i + 1
1370
1421
  if j < n and src[j] in "!^":
1371
1422
  j += 1
1372
1423
  if j < n and src[j] == "]":
1373
1424
  j += 1
1425
+ jumped = False
1374
1426
  while j < n:
1375
1427
  c = src[j]
1376
1428
  if c == "]":
@@ -1384,8 +1436,15 @@ class Parser:
1384
1436
  if end == -1:
1385
1437
  break
1386
1438
  j = end + 2
1439
+ jumped = True
1387
1440
  continue
1388
1441
  j += 1
1442
+ else:
1443
+ # Reached EOF with no ']' or metachar. Unless a [:class:] jump
1444
+ # skipped over text, no later '[' can find one either - remember
1445
+ # it so the next '[' bails in O(1).
1446
+ if not jumped:
1447
+ self._glob_eof_from = start
1389
1448
  self.i = start + 1
1390
1449
  return Node("T_Literal", start, self.i, text="[")
1391
1450
 
@@ -1842,6 +1901,20 @@ class Parser:
1842
1901
  return operand
1843
1902
 
1844
1903
  def read_arith_primary(self):
1904
+ # Nested "(...)" in arithmetic re-enters here, a separate recursion
1905
+ # from read_command, so it shares the same nesting guard to keep
1906
+ # "(((((..." / "$((((..." from overflowing the stack.
1907
+ self._nesting += 1
1908
+ if self._nesting > MAX_NESTING:
1909
+ self._nesting -= 1
1910
+ raise _NestingLimit("arithmetic nested too deeply (over %d levels)"
1911
+ % MAX_NESTING, self.i)
1912
+ try:
1913
+ return self._read_arith_primary()
1914
+ finally:
1915
+ self._nesting -= 1
1916
+
1917
+ def _read_arith_primary(self):
1845
1918
  self.skip_arith_ws()
1846
1919
  start = self.i
1847
1920
  src, n = self.src, self.n
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pureshellcheck
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A pure Python reimplementation of ShellCheck's most common checks
5
5
  Author: adam2go
6
6
  License: MIT
File without changes
File without changes
File without changes