bracex 3.2.1__tar.gz → 3.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 (31) hide show
  1. {bracex-3.2.1 → bracex-3.2.2}/PKG-INFO +1 -1
  2. {bracex-3.2.1 → bracex-3.2.2}/bracex/__init__.py +130 -0
  3. {bracex-3.2.1 → bracex-3.2.2}/bracex/__main__.py +1 -1
  4. {bracex-3.2.1 → bracex-3.2.2}/bracex/__meta__.py +1 -1
  5. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/about/changelog.md +6 -0
  6. {bracex-3.2.1 → bracex-3.2.2}/tests/brace-cases.txt +25 -0
  7. {bracex-3.2.1 → bracex-3.2.2}/tests/brace-results.txt +51 -1
  8. bracex-3.2.2/tests/generate.sh +22 -0
  9. {bracex-3.2.1 → bracex-3.2.2}/tests/test_brace.py +7 -1
  10. bracex-3.2.1/tests/generate.sh +0 -21
  11. {bracex-3.2.1 → bracex-3.2.2}/.gitignore +0 -0
  12. {bracex-3.2.1 → bracex-3.2.2}/.pyspelling.yml +0 -0
  13. {bracex-3.2.1 → bracex-3.2.2}/LICENSE.md +0 -0
  14. {bracex-3.2.1 → bracex-3.2.2}/README.md +0 -0
  15. {bracex-3.2.1 → bracex-3.2.2}/bracex/py.typed +0 -0
  16. {bracex-3.2.1 → bracex-3.2.2}/docs/src/dictionary/en-custom.txt +0 -0
  17. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/.snippets/abbr.md +0 -0
  18. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/.snippets/links.md +0 -0
  19. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/.snippets/refs.md +0 -0
  20. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/about/contributing.md +0 -0
  21. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/about/development.md +0 -0
  22. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/about/license.md +0 -0
  23. {bracex-3.2.1 → bracex-3.2.2}/docs/src/markdown/index.md +0 -0
  24. {bracex-3.2.1 → bracex-3.2.2}/docs/theme/announce.html +0 -0
  25. {bracex-3.2.1 → bracex-3.2.2}/hatch_build.py +0 -0
  26. {bracex-3.2.1 → bracex-3.2.2}/pyproject.toml +0 -0
  27. {bracex-3.2.1 → bracex-3.2.2}/tests/__init__.py +0 -0
  28. {bracex-3.2.1 → bracex-3.2.2}/tests/test_main.py +0 -0
  29. {bracex-3.2.1 → bracex-3.2.2}/tests/test_versions.py +0 -0
  30. {bracex-3.2.1 → bracex-3.2.2}/tools/pycon.py +0 -0
  31. {bracex-3.2.1 → bracex-3.2.2}/zensical.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bracex
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Bash style brace expander.
5
5
  Project-URL: Homepage, https://github.com/facelessuser/bracex
6
6
  Author-email: Isaac Muse <Isaac.Muse@gmail.com>
@@ -38,6 +38,8 @@ RE_INT_ITER = re.compile(
38
38
  )
39
39
  RE_CHR_ITER = re.compile(r'([A-Za-z])\.{2}([A-Za-z])(?:\.{2}[-+]?(((?:0(?=\d))*)\d+))?(?=\})', re.ASCII)
40
40
  RE_SEQ_SEP = re.compile(r'\.(?!\})')
41
+ RE_SEQ_TERM = re.compile(r'(?:[-+]?\d|[A-Za-z]\.)', re.ASCII)
42
+ RE_SEQ_TERM_END = re.compile(r'(?:[-+]?\d|[A-Za-z][.}])', re.ASCII)
41
43
 
42
44
  DEFAULT_LIMIT = 1000
43
45
 
@@ -100,6 +102,8 @@ class StringIter:
100
102
 
101
103
  self._string = string
102
104
  self._index = 0
105
+ self._last_seq = string.rfind('..')
106
+ self.scan_memo = {} # type: dict[int, tuple[int, bool]]
103
107
 
104
108
  def __iter__(self) -> "StringIter": # pragma: no cover
105
109
  """Iterate."""
@@ -125,6 +129,18 @@ class StringIter:
125
129
 
126
130
  return self._index
127
131
 
132
+ @property
133
+ def last_seq(self) -> int:
134
+ """Get the index of the last `..`, or -1 if there are none."""
135
+
136
+ return self._last_seq
137
+
138
+ @property
139
+ def string(self) -> str:
140
+ """Get the string being iterated."""
141
+
142
+ return self._string
143
+
128
144
  def previous(self) -> str: # pragma: no cover
129
145
  """Get previous char."""
130
146
 
@@ -199,6 +215,84 @@ class ExpandBrace:
199
215
  if release:
200
216
  self.expanding = False
201
217
 
218
+ def scan_group(self, i: StringIter, index: int, bounded: bool) -> tuple[int, bool]:
219
+ """
220
+ Scan ahead, like Bash to find the `}` that ends the group starting at `index`.
221
+
222
+ Returns the index of the closing `}` (or -1 if there is none) and whether Bash would treat the group
223
+ as a sequence candidate: a top level `..` (not directly followed by `}`) and no top level comma.
224
+
225
+ Bash only ever sees the slot a nested group is in, so when `bounded`, a top level `}` that doesn't end the
226
+ group can only be the end of the slot, which means there is no closing `}`.
227
+
228
+ Groups nested in the one being scanned are tracked as they are passed, as their outcome (as a bounded
229
+ group) is known as soon as they close. These are remembered so they don't have to be scanned again.
230
+ """
231
+
232
+ string = i.string
233
+ memo = i.scan_memo
234
+ if bounded:
235
+ result = memo.get(index)
236
+ if result is not None:
237
+ return result
238
+
239
+ # Open nested groups as `[start, found a separator, is a sequence]`, and `None` for a `${`.
240
+ stack = [] # type: list[list[Any] | None]
241
+ found = False # A top level comma or `..` has been found, which allows a top level `}` to end the group.
242
+ is_seq = False
243
+ size = len(string)
244
+ while index < size:
245
+ c = string[index]
246
+ if c == '\\':
247
+ index += 1
248
+ elif c == '$' and string.startswith('{', index + 1):
249
+ stack.append(None)
250
+ index += 1
251
+ elif c == '{':
252
+ stack.append([index + 1, False, False])
253
+ elif c == '}':
254
+ if stack:
255
+ nested = stack.pop()
256
+ if nested is not None:
257
+ memo[nested[0]] = (index, nested[2]) if nested[1] else (-1, False)
258
+ elif found:
259
+ return index, is_seq
260
+ elif bounded: # pragma: no cover
261
+ # A nested group is normally already known from the scan of the group around it.
262
+ break
263
+ elif c == ',' or (c == '.' and string.startswith('..', index) and string[index + 2:index + 3] != '}'):
264
+ if not stack:
265
+ if c == ',' or not found:
266
+ found = True
267
+ is_seq = c == '.'
268
+ else:
269
+ top = stack[-1]
270
+ if top is not None and (c == ',' or not top[1]):
271
+ top[1:] = [True, c == '.']
272
+ index += 1
273
+ return -1, False
274
+
275
+ def looks_like_sequence(self, group: str) -> bool:
276
+ """
277
+ Check whether the group (with its closing `}`) starts like a sequence.
278
+
279
+ This is Bash's loose `valid_seqterm` check: both sides of the first `..` must start like the same kind
280
+ of term (integer or letter). Whether the sequence is really valid is not considered here.
281
+ """
282
+
283
+ index = group.find('..')
284
+ if index < 1 or group[0] == '.':
285
+ return False
286
+ left = RE_SEQ_TERM.match(group)
287
+ right = RE_SEQ_TERM_END.match(group, index + 2)
288
+ return bool(left and right and group[0].isalpha() == group[index + 2].isalpha())
289
+
290
+ def literal_text(self, text: str) -> str:
291
+ """Resolve escapes in text that is kept literally."""
292
+
293
+ i = StringIter(text)
294
+ return ''.join(self.get_escape(c, i) if c == '\\' else c for c in i)
295
+
202
296
  def get_escape(self, c: str, i: StringIter) -> str:
203
297
  """Get an escape."""
204
298
 
@@ -335,6 +429,7 @@ class ExpandBrace:
335
429
  """
336
430
 
337
431
  result = [] # type: list[str | Iterator[str] | Iterable[str | Iterator[str]]]
432
+ group_start = i.index - 1 # Index of the first character within the group.
338
433
  release = self.set_expanding()
339
434
  has_comma = False # Used to indicate validity of group (`{1..2}` are an exception).
340
435
  has_seq = False # Used to indicate a top level `..` that lets the next `}` end the group.
@@ -349,6 +444,25 @@ class ExpandBrace:
349
444
  self.release_expanding(release)
350
445
  return item, self.account(count), True
351
446
 
447
+ # Bash decides up front whether a group is a sequence candidate and, if so, whether it looks like a sequence.
448
+ # A group that looks like one but isn't valid is kept as text, nested groups and all. A group that doesn't
449
+ # even look like one is abandoned: Bash skips just the `{` and tries again with what follows.
450
+ # Without a `..` ahead, the group can't be a sequence candidate, so skip the scan.
451
+ end, is_seq = self.scan_group(i, group_start, depth > 1) if i.last_seq >= group_start else (-1, False)
452
+ if is_seq and end != -1:
453
+ self.release_expanding(release)
454
+ if self.looks_like_sequence(i.string[group_start:end + 1]):
455
+ i.advance(end + 1 - i.index)
456
+ return iter(['{' + self.literal_text(i.string[group_start:end]) + '}']), 1, False
457
+ if depth == 1:
458
+ # Continue the current word from just after the `{`.
459
+ i.rewind(i.index - group_start)
460
+ return iter(['{']), 1, False
461
+ # Nested groups are confined to their slot, so expand what follows the `{` on its own.
462
+ sub, scount = self.expand_isolated(i.string[group_start:end + 1])
463
+ i.advance(end + 1 - i.index)
464
+ return self.squash(iter(['{']), sub), scount, False
465
+
352
466
  try:
353
467
  while True:
354
468
  # Bash has some special top level logic. if `}` follows `{` but hasn't matched
@@ -537,6 +651,22 @@ class ExpandBrace:
537
651
  r = range(first, last - 1, -inc)
538
652
  return (alpha[i] for i in r), count
539
653
 
654
+ def expand_isolated(self, string: str) -> tuple[Iterator[str], int]:
655
+ """Expand a piece of text on its own, as though it were not inside a group."""
656
+
657
+ expanding = self.expanding
658
+ self.expanding = False
659
+ try:
660
+ i = StringIter(string)
661
+ values, count, _ = self.get_literals(next(i), i, 0)
662
+ finally:
663
+ self.expanding = expanding
664
+
665
+ results = iter([EMPTY]) # type: Iterator[str]
666
+ for v in values:
667
+ results = self.squash(results, [v] if isinstance(v, str) else v)
668
+ return results, count
669
+
540
670
  def expand_str(self, string: str) -> Iterator[str]:
541
671
  """Expand the string."""
542
672
 
@@ -24,7 +24,7 @@ import argparse
24
24
  import bracex
25
25
 
26
26
 
27
- def main(argv: str | None = None) -> None:
27
+ def main(argv: list[str] | None = None) -> None:
28
28
  """Accept command line arguments and output brace expansion to stdout."""
29
29
  parser = argparse.ArgumentParser(
30
30
  prog='python -m bracex',
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
193
193
  return Version(major, minor, micro, release, pre, post, dev)
194
194
 
195
195
 
196
- __version_info__ = Version(3, 2, 1, "final")
196
+ __version_info__ = Version(3, 2, 2, "final")
197
197
  __version__ = __version_info__._get_canonical()
@@ -1,5 +1,11 @@
1
1
  # Changes
2
2
 
3
+ ## 3.2.2
4
+
5
+ - **FIX**: Match Bash 5.3 for groups that contain a top level `..` but are not valid sequences. If the group starts
6
+ like a sequence, such as `{1..3{a,b}}`, it is kept as text without expanding nested groups. If it doesn't, such as
7
+ `{a..x{b}},}`, only the opening `{` is skipped and what follows is parsed again, as in Bash.
8
+
3
9
  ## 3.2.1
4
10
 
5
11
  - **FIX**: Keep numeric sequences with non-ASCII digits literal, matching Bash.
@@ -337,3 +337,28 @@ x{١..٣}y
337
337
  {a,{١..٣},b}
338
338
  {+١..+٣}
339
339
  {-١..٣}
340
+ {1..3{a,b}}
341
+ {1..3{a,b}}x
342
+ x{1..3{a,b}}y{c,d}
343
+ {1..3{a,b}c}
344
+ {1..3{1..3}}
345
+ {a..c{1,2}}
346
+ {1{a,b}..3}
347
+ {a{b,c}..d}
348
+ {1..{a,b}}
349
+ {1..3{a,b},z}
350
+ {{a,b}..c}
351
+ {{1..3}..b}
352
+ x{{1..3}..b}y
353
+ {x{1..3}..y}
354
+ {{1..3}...}
355
+ {{11}..3{1..3}b}
356
+ {{11}..3{1..3}b}z
357
+ {x,{{11}..3{1..3}b}}
358
+ {a..b{c}}x,y}
359
+ {a..x{b}},}
360
+ {..x{b}}1x,}
361
+ {x,{a..x{b}},y}
362
+ {1..3{a,b}}{c,d}
363
+ {a,{b}..c}
364
+ {x,{b}..c,y}
@@ -1383,4 +1383,54 @@ A{b,{d,e},{f,g}}Z
1383
1383
  [{١..٣}]
1384
1384
  [b]><><><><{+١..+٣}
1385
1385
  [{+١..+٣}]><><><><{-١..٣}
1386
- [{-١..٣}]><><><><
1386
+ [{-١..٣}]><><><><{1..3{a,b}}
1387
+ [{1..3{a,b}}]><><><><{1..3{a,b}}x
1388
+ [{1..3{a,b}}x]><><><><x{1..3{a,b}}y{c,d}
1389
+ [x{1..3{a,b}}yc]
1390
+ [x{1..3{a,b}}yd]><><><><{1..3{a,b}c}
1391
+ [{1..3{a,b}c}]><><><><{1..3{1..3}}
1392
+ [{1..3{1..3}}]><><><><{a..c{1,2}}
1393
+ [{a..c1}]
1394
+ [{a..c2}]><><><><{1{a,b}..3}
1395
+ [{1{a,b}..3}]><><><><{a{b,c}..d}
1396
+ [{ab..d}]
1397
+ [{ac..d}]><><><><{1..{a,b}}
1398
+ [{1..a}]
1399
+ [{1..b}]><><><><{1..3{a,b},z}
1400
+ [1..3a]
1401
+ [1..3b]
1402
+ [z]><><><><{{a,b}..c}
1403
+ [{a..c}]
1404
+ [{b..c}]><><><><{{1..3}..b}
1405
+ [{1..b}]
1406
+ [{2..b}]
1407
+ [{3..b}]><><><><x{{1..3}..b}y
1408
+ [x{1..b}y]
1409
+ [x{2..b}y]
1410
+ [x{3..b}y]><><><><{x{1..3}..y}
1411
+ [{x1..y}]
1412
+ [{x2..y}]
1413
+ [{x3..y}]><><><><{{1..3}...}
1414
+ [{1...}]
1415
+ [{2...}]
1416
+ [{3...}]><><><><{{11}..3{1..3}b}
1417
+ [{{11}..3{1..3}b}]><><><><{{11}..3{1..3}b}z
1418
+ [{{11}..3{1..3}b}z]><><><><{x,{{11}..3{1..3}b}}
1419
+ [x]
1420
+ [{{11}..3{1..3}b}]><><><><{a..b{c}}x,y}
1421
+ [{a..bc}}x]
1422
+ [{a..by]><><><><{a..x{b}},}
1423
+ [{a..xb}}]
1424
+ [{a..x]><><><><{..x{b}}1x,}
1425
+ [{..xb}}1x]
1426
+ [{..x]><><><><{x,{a..x{b}},y}
1427
+ [x]
1428
+ [{a..x{b}}]
1429
+ [y]><><><><{1..3{a,b}}{c,d}
1430
+ [{1..3{a,b}}c]
1431
+ [{1..3{a,b}}d]><><><><{a,{b}..c}
1432
+ [a]
1433
+ [{b}..c]><><><><{x,{b}..c,y}
1434
+ [x]
1435
+ [{b}..c]
1436
+ [y]><><><><
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ # Bash 5.3 changed how brace groups that look like sequences are validated (invalid candidates are skipped
6
+ # and later braces are still expanded), so results differ from 5.2 and earlier. Generate against 5.3 only.
7
+ if [[ "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" != "5.3" ]]; then
8
+ echo "this script requires bash 5.3" >&2
9
+ exit 1
10
+ fi
11
+
12
+ CDPATH= cd "$(dirname "$0")"
13
+
14
+ cat brace-cases.txt | \
15
+ while read case; do
16
+ if [ "${case:0:1}" = "#" ]; then
17
+ continue;
18
+ fi;
19
+ b="$($BASH -c 'for c in '"$case"'; do echo "[$c]"; done')"
20
+ echo "$case"
21
+ echo -n "$b><><><><";
22
+ done > brace-results.txt
@@ -83,7 +83,13 @@ class TestBraces:
83
83
  nested_cases = [
84
84
  ['{a,b{1..3},c}', ['a', 'b1', 'b2', 'b3', 'c']],
85
85
  ['{{A..Z},{a..z}}', list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')],
86
- ['ppp{,config,oe{,conf}}', ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']]
86
+ ['ppp{,config,oe{,conf}}', ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']],
87
+ # Bash keeps a group that starts like a sequence as text, nested groups included
88
+ ['{1..3{a,b}}', ['{1..3{a,b}}']],
89
+ ['x{1..3{a,b}}y{c,d}', ['x{1..3{a,b}}yc', 'x{1..3{a,b}}yd']],
90
+ ['{1..3{a,b\\}}}', ['{1..3{a,b}}}']],
91
+ # But if it doesn't even start like a sequence, nested groups still expand
92
+ ['{1..{a,b}}', ['{1..a}', '{1..b}']]
87
93
  ]
88
94
 
89
95
  order_cases = [
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- set -e
4
-
5
- # Bash 4.3 - 5.0 because of arbitrary need to pick a single standard.
6
- if ! [[ "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}" =~ (4\.[^1-2]|5\.[0-3])(\.\d+)? ]]; then
7
- echo "this script requires bash 4.3, 4.4, or 5.0" >&2
8
- exit 1
9
- fi
10
-
11
- CDPATH= cd "$(dirname "$0")"
12
-
13
- cat brace-cases.txt | \
14
- while read case; do
15
- if [ "${case:0:1}" = "#" ]; then
16
- continue;
17
- fi;
18
- b="$($BASH -c 'for c in '"$case"'; do echo "[$c]"; done')"
19
- echo "$case"
20
- echo -n "$b><><><><";
21
- done > brace-results.txt
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes