bracex 3.2__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 → bracex-3.2.2}/PKG-INFO +1 -1
  2. {bracex-3.2 → bracex-3.2.2}/bracex/__init__.py +177 -19
  3. {bracex-3.2 → bracex-3.2.2}/bracex/__main__.py +1 -1
  4. {bracex-3.2 → bracex-3.2.2}/bracex/__meta__.py +1 -1
  5. bracex-3.2.2/docs/src/dictionary/en-custom.txt +45 -0
  6. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/about/changelog.md +15 -0
  7. {bracex-3.2 → bracex-3.2.2}/pyproject.toml +2 -3
  8. {bracex-3.2 → bracex-3.2.2}/tests/brace-cases.txt +73 -0
  9. {bracex-3.2 → bracex-3.2.2}/tests/brace-results.txt +172 -1
  10. bracex-3.2.2/tests/generate.sh +22 -0
  11. {bracex-3.2 → bracex-3.2.2}/tests/test_brace.py +19 -2
  12. bracex-3.2.2/zensical.yml +164 -0
  13. bracex-3.2/tests/generate.sh +0 -21
  14. {bracex-3.2 → bracex-3.2.2}/.gitignore +0 -0
  15. {bracex-3.2 → bracex-3.2.2}/.pyspelling.yml +0 -0
  16. {bracex-3.2 → bracex-3.2.2}/LICENSE.md +0 -0
  17. {bracex-3.2 → bracex-3.2.2}/README.md +0 -0
  18. {bracex-3.2 → bracex-3.2.2}/bracex/py.typed +0 -0
  19. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/.snippets/abbr.md +0 -0
  20. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/.snippets/links.md +0 -0
  21. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/.snippets/refs.md +0 -0
  22. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/about/contributing.md +0 -0
  23. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/about/development.md +0 -0
  24. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/about/license.md +0 -0
  25. {bracex-3.2 → bracex-3.2.2}/docs/src/markdown/index.md +0 -0
  26. {bracex-3.2 → bracex-3.2.2}/docs/theme/announce.html +0 -0
  27. {bracex-3.2 → bracex-3.2.2}/hatch_build.py +0 -0
  28. {bracex-3.2 → bracex-3.2.2}/tests/__init__.py +0 -0
  29. {bracex-3.2 → bracex-3.2.2}/tests/test_main.py +0 -0
  30. {bracex-3.2 → bracex-3.2.2}/tests/test_versions.py +0 -0
  31. {bracex-3.2 → bracex-3.2.2}/tools/pycon.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bracex
3
- Version: 3.2
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>
@@ -33,8 +33,13 @@ __version_info__ = __meta__.__version_info__
33
33
  _alpha = [chr(x) if x != 0x5c else '' for x in range(ord('A'), ord('z') + 1)]
34
34
  _nalpha = list(reversed(_alpha))
35
35
 
36
- RE_INT_ITER = re.compile(r'(-?((?:0(?=\d))*)\d+)\.{2}(-?((?:0(?=\d))*)\d+)(?:\.{2}-?(((?:0(?=\d))*)\d+))?(?=\})')
37
- RE_CHR_ITER = re.compile(r'([A-Za-z])\.{2}([A-Za-z])(?:\.{2}-?(((?:0(?=\d))*)\d+))?(?=\})')
36
+ RE_INT_ITER = re.compile(
37
+ r'([-+]?((?:0(?=\d))*)\d+)\.{2}([-+]?((?:0(?=\d))*)\d+)(?:\.{2}[-+]?(((?:0(?=\d))*)\d+))?(?=\})', re.ASCII
38
+ )
39
+ RE_CHR_ITER = re.compile(r'([A-Za-z])\.{2}([A-Za-z])(?:\.{2}[-+]?(((?:0(?=\d))*)\d+))?(?=\})', re.ASCII)
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)
38
43
 
39
44
  DEFAULT_LIMIT = 1000
40
45
 
@@ -97,6 +102,8 @@ class StringIter:
97
102
 
98
103
  self._string = string
99
104
  self._index = 0
105
+ self._last_seq = string.rfind('..')
106
+ self.scan_memo = {} # type: dict[int, tuple[int, bool]]
100
107
 
101
108
  def __iter__(self) -> "StringIter": # pragma: no cover
102
109
  """Iterate."""
@@ -122,6 +129,18 @@ class StringIter:
122
129
 
123
130
  return self._index
124
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
+
125
144
  def previous(self) -> str: # pragma: no cover
126
145
  """Get previous char."""
127
146
 
@@ -196,6 +215,84 @@ class ExpandBrace:
196
215
  if release:
197
216
  self.expanding = False
198
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
+
199
296
  def get_escape(self, c: str, i: StringIter) -> str:
200
297
  """Get an escape."""
201
298
 
@@ -241,7 +338,7 @@ class ExpandBrace:
241
338
  i: StringIter,
242
339
  depth: int,
243
340
  ignore_end: bool = False
244
- ) -> tuple[list[str | Iterator[str]], int]:
341
+ ) -> tuple[list[str | Iterator[str]], int, bool]:
245
342
  """
246
343
  Get a string literal.
247
344
 
@@ -250,9 +347,12 @@ class ExpandBrace:
250
347
  """
251
348
 
252
349
  result = [] # type: list[str | Iterator[str]]
350
+ word_start = 0
253
351
  is_dollar = False
352
+ dollar_depth = 0
254
353
  count = 1
255
354
  literal = ''
355
+ has_seq = False
256
356
 
257
357
  try:
258
358
  while c:
@@ -266,7 +366,11 @@ class ExpandBrace:
266
366
  elif c == '\\':
267
367
  literal += self.get_escape(c, i)
268
368
 
269
- elif not ignore_brace and c == '{':
369
+ elif (ignore_brace and c == '{') or (dollar_depth and c == '}'):
370
+ literal += c
371
+ dollar_depth += 1 if c == '{' else -1
372
+
373
+ elif c == '{':
270
374
 
271
375
  if literal:
272
376
  result.append(literal)
@@ -274,7 +378,10 @@ class ExpandBrace:
274
378
 
275
379
  # Try and get the group
276
380
  try:
277
- seq, scount = self.get_sequence(next(i), i, depth + 1)
381
+ seq, scount, expanded = self.get_sequence(next(i), i, depth + 1, word_start)
382
+ if expanded:
383
+ # Bash expands each postamble as a new word.
384
+ word_start = i.index
278
385
  count *= scount
279
386
  result.append(seq)
280
387
  except StopIteration:
@@ -283,7 +390,7 @@ class ExpandBrace:
283
390
  literal += c
284
391
  raise
285
392
 
286
- elif self.is_expanding() and (c == ',' or (c == '}' and not ignore_end)):
393
+ elif self.is_expanding() and not dollar_depth and (c == ',' or (c == '}' and not ignore_end)):
287
394
  # We are Expanding within a group and found a group delimiter
288
395
  # Return what we gathered before the group delimiters.
289
396
 
@@ -294,6 +401,14 @@ class ExpandBrace:
294
401
 
295
402
  i.rewind(1)
296
403
  break
404
+
405
+ elif not dollar_depth and c == '.' and i.match(RE_SEQ_SEP):
406
+ # Bash counts `..` like a comma when looking for the `}` that ends a top level group,
407
+ # unless `}` follows it, so the next `}` can end the group.
408
+ ignore_end = False
409
+ has_seq = True
410
+ literal += '..'
411
+
297
412
  else:
298
413
  literal += c
299
414
 
@@ -303,9 +418,9 @@ class ExpandBrace:
303
418
  if literal:
304
419
  result.append(literal)
305
420
 
306
- return result, self.account(count)
421
+ return result, self.account(count), has_seq
307
422
 
308
- def get_sequence(self, c: str, i: StringIter, depth: int) -> tuple[Iterator[str], int]:
423
+ def get_sequence(self, c: str, i: StringIter, depth: int, word_start: int) -> tuple[Iterator[str], int, bool]:
309
424
  """
310
425
  Get the sequence.
311
426
 
@@ -314,8 +429,10 @@ class ExpandBrace:
314
429
  """
315
430
 
316
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.
317
433
  release = self.set_expanding()
318
434
  has_comma = False # Used to indicate validity of group (`{1..2}` are an exception).
435
+ has_seq = False # Used to indicate a top level `..` that lets the next `}` end the group.
319
436
  is_empty = True # Tracks whether the current slot is empty `{slot,slot,slot}`.
320
437
  counts = []
321
438
 
@@ -325,15 +442,34 @@ class ExpandBrace:
325
442
  i.advance(1)
326
443
  if item is not None:
327
444
  self.release_expanding(release)
328
- return item, self.account(count)
445
+ return item, self.account(count), True
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
329
465
 
330
466
  try:
331
467
  while True:
332
468
  # Bash has some special top level logic. if `}` follows `{` but hasn't matched
333
- # a group yet, keep going except when the first 2 bytes are `{}` which gets
334
- # completely ignored.
335
- keep_looking = depth == 1 and not has_comma
336
- if (c == '}' and (not keep_looking or i.index == 2)):
469
+ # a group yet, keep going except when the first 2 bytes of the current
470
+ # postamble are `{}` which gets completely ignored.
471
+ keep_looking = depth == 1 and not has_comma and not has_seq
472
+ if (c == '}' and (not keep_looking or i.index == word_start + 2)):
337
473
  self.release_expanding(release)
338
474
 
339
475
  # Handle empty slot
@@ -349,7 +485,7 @@ class ExpandBrace:
349
485
  for r in self.flatten(result):
350
486
  temp = self.squash(temp, [r] if isinstance(r, str) else r)
351
487
  temp = self.squash(temp, ['}'])
352
- return temp, self.account(math.prod(counts, start=1))
488
+ return temp, self.account(math.prod(counts, start=1)), False
353
489
 
354
490
  # Format return for a sequence
355
491
  fin = iter([]) # type: Iterator[str]
@@ -369,7 +505,7 @@ class ExpandBrace:
369
505
  start = e + 1
370
506
  if start < l:
371
507
  fin = self.chain(fin, result[start:])
372
- return fin, self.account(sum(counts))
508
+ return fin, self.account(sum(counts)), True
373
509
 
374
510
  elif c == ',':
375
511
  # Must be the first element in the list.
@@ -382,7 +518,7 @@ class ExpandBrace:
382
518
 
383
519
  else:
384
520
  # Lower level: Try to find group, but give up if cannot acquire.
385
- value, lcount = self.get_literals(c, i, depth, keep_looking)
521
+ value, lcount, has_seq = self.get_literals(c, i, depth, keep_looking)
386
522
  counts.append(lcount)
387
523
  if value is not None:
388
524
  if len(value) > 1:
@@ -408,7 +544,7 @@ class ExpandBrace:
408
544
  for r in self.flatten([slot]):
409
545
  member = self.squash(member, [r] if isinstance(r, str) else r)
410
546
  temp2 = self.squash(temp2, member)
411
- return temp2, self.account(math.prod(counts, start=1))
547
+ return temp2, self.account(math.prod(counts, start=1)), False
412
548
 
413
549
  def get_range(self, i: StringIter) -> tuple[Iterator[str] | None, int]:
414
550
  """
@@ -461,9 +597,15 @@ class ExpandBrace:
461
597
  last = int64(end)
462
598
  inc = max(1, int64(increment))
463
599
 
600
+ # Bash's zero-padding rule: padding is only enabled when an operand, as written,
601
+ # starts with `0` (or `-0`). A unary `+` sign never enables padding, but once
602
+ # padding is enabled by either operand, the full written width of both operands
603
+ # (signs included) is honored.
604
+ arm = bool((spad[1] - spad[0]) and not start.startswith('+'))
605
+ arm = arm or bool((epad[1] - epad[0]) and not end.startswith('+'))
464
606
  spad_len = spad[1] - spad[0]
465
607
  epad_len = epad[1] - epad[0]
466
- padding = max(spad_len + len(start), epad_len + len(end)) if spad_len or epad_len else 0
608
+ padding = max(spad_len + len(start), epad_len + len(end)) if arm else 0
467
609
 
468
610
  if first < last:
469
611
  span = abs(last - first + 1)
@@ -509,11 +651,27 @@ class ExpandBrace:
509
651
  r = range(first, last - 1, -inc)
510
652
  return (alpha[i] for i in r), count
511
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
+
512
670
  def expand_str(self, string: str) -> Iterator[str]:
513
671
  """Expand the string."""
514
672
 
515
673
  i = StringIter(string)
516
- values, _ = self.get_literals(next(i), i, 0)
674
+ values, _, _ = self.get_literals(next(i), i, 0)
517
675
 
518
676
  # Squash the nested list by calculating the combinations
519
677
  results = iter([EMPTY]) # type: Iterator[str]
@@ -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, 0, "final")
196
+ __version_info__ = Version(3, 2, 2, "final")
197
197
  __version__ = __version_info__._get_canonical()
@@ -0,0 +1,45 @@
1
+ API
2
+ Accessors
3
+ Backport
4
+ Bracex
5
+ Changelog
6
+ Formatter
7
+ GitHub
8
+ Gitter
9
+ KiB
10
+ MERCHANTABILITY
11
+ MkDocs
12
+ NONINFRINGEMENT
13
+ NUL
14
+ Preprocess
15
+ PyPI
16
+ Setuptools
17
+ TODO
18
+ Tox
19
+ Twemoji
20
+ Virtualenv
21
+ Willmer
22
+ Zensical
23
+ accessor
24
+ backend
25
+ deprecations
26
+ expander
27
+ formatter
28
+ globbing
29
+ incrementer
30
+ iterables
31
+ linter
32
+ postamble
33
+ pre
34
+ prerelease
35
+ prereleases
36
+ pytest
37
+ regex
38
+ santhreal
39
+ stdout
40
+ sublicense
41
+ tuple
42
+ uncatchable
43
+ validator
44
+ wordlist
45
+ unary
@@ -1,5 +1,20 @@
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
+
9
+ ## 3.2.1
10
+
11
+ - **FIX**: Keep numeric sequences with non-ASCII digits literal, matching Bash.
12
+ - **FIX**: Preserve an initial empty brace group after a valid expansion.
13
+ - **FIX**: Fix `${...}` inside a brace group ending the group early.
14
+ - **FIX**: Fix an invalid sequence such as `{1..a}` being split at a later comma instead of being kept as text.
15
+ - **FIX**: Fix numeric and character sequences with unary `+` signs (`{+1..3}`, `{1..+3..2}`, `{a..z..+2}`)
16
+ being left as literal text instead of expanded like Bash, including Bash's zero-padding rule for `+` operands.
17
+
3
18
  ## 3.2
4
19
 
5
20
  - **NEW**: Drop Python 3.10 support.
@@ -40,11 +40,10 @@ include = [
40
40
  "/docs/src/markdown/**/*.md",
41
41
  "/docs/src/markdown/**/*.gif",
42
42
  "/docs/src/markdown/**/*.png",
43
- "/docs/src/markdown/dictionary/*.txt",
43
+ "/docs/src/dictionary/*.txt",
44
44
  "/docs/theme/**/*.css",
45
45
  "/docs/theme/**/*.js",
46
46
  "/docs/theme/**/*.html",
47
- "/requirements/*.txt",
48
47
  "/bracex/**/*.py",
49
48
  "/bracex/py.typed",
50
49
  "/tests/**/*.py",
@@ -53,7 +52,7 @@ include = [
53
52
  "/tools/**/*.py",
54
53
  "/.pyspelling.yml",
55
54
  "/.coveragerc",
56
- "/mkdocs.yml"
55
+ "/zensical.yml"
57
56
  ]
58
57
 
59
58
  [tool.mypy]
@@ -289,3 +289,76 @@ a{b},
289
289
  {a,b{c,d}
290
290
  {a,b{
291
291
  {2{
292
+ # A top level `..` that is not followed by `}` lets the next `}` end the group
293
+ {1..a},b}
294
+ {a...},c}
295
+ a{1..a},b}c
296
+ {1..a},{b,c}}
297
+ {1..},b}
298
+ # An initial empty group is literal in the postamble after a valid expansion
299
+ {a,b}{},c}
300
+ {1..2}{},c}
301
+ prefix{a,b}{},tail}
302
+ {a,b}x{},c}
303
+ {a,b}{}{},c}
304
+ {word}{},c}
305
+ {a{b,c}}{},d}
306
+ {+1..3}
307
+ {+1..+3}
308
+ {3..+1}
309
+ {+1..+3..2}
310
+ {1..+3..2}
311
+ {+1..+3..+2}
312
+ {+3..1..+2}
313
+ {+05..8}
314
+ {+005..8}
315
+ {05..+8}
316
+ {+5..+08}
317
+ {+05..+05}
318
+ {+05..08}
319
+ {+10..08}
320
+ {-05..+2}
321
+ {+5..05}
322
+ {+5..08}
323
+ {08..+5}
324
+ {+003..1}
325
+ {a..z..+2}
326
+ a{+1..2}b
327
+ {a,{+1..2},b}
328
+ {+9223372036854775806..+9223372036854775807}
329
+ {١..٣}
330
+ {1..3}
331
+ {1..٣}
332
+ {١..3}
333
+ {1..3..٢}
334
+ {a..e..٢}
335
+ {0١..03}
336
+ x{١..٣}y
337
+ {a,{١..٣},b}
338
+ {+١..+٣}
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}
@@ -1262,4 +1262,175 @@ A{b,{d,e},{f,g}}Z
1262
1262
  [{a,bc]
1263
1263
  [{a,bd]><><><><{a,b{
1264
1264
  [{a,b{]><><><><{2{
1265
- [{2{]><><><><
1265
+ [{2{]><><><><{1..a},b}
1266
+ [{1..a},b}]><><><><{a...},c}
1267
+ [{a...},c}]><><><><a{1..a},b}c
1268
+ [a{1..a},b}c]><><><><{1..a},{b,c}}
1269
+ [{1..a},b}]
1270
+ [{1..a},c}]><><><><{1..},b}
1271
+ [1..}]
1272
+ [b]><><><><{a,b}{},c}
1273
+ [a{},c}]
1274
+ [b{},c}]><><><><{1..2}{},c}
1275
+ [1{},c}]
1276
+ [2{},c}]><><><><prefix{a,b}{},tail}
1277
+ [prefixa{},tail}]
1278
+ [prefixb{},tail}]><><><><{a,b}x{},c}
1279
+ [ax}]
1280
+ [axc]
1281
+ [bx}]
1282
+ [bxc]><><><><{a,b}{}{},c}
1283
+ [a{}}]
1284
+ [a{}c]
1285
+ [b{}}]
1286
+ [b{}c]><><><><{word}{},c}
1287
+ [word}{}]
1288
+ [c]><><><><{a{b,c}}{},d}
1289
+ [ab}{}]
1290
+ [ac}{}]
1291
+ [d]><><><><{+1..3}
1292
+ [1]
1293
+ [2]
1294
+ [3]><><><><{+1..+3}
1295
+ [1]
1296
+ [2]
1297
+ [3]><><><><{3..+1}
1298
+ [3]
1299
+ [2]
1300
+ [1]><><><><{+1..+3..2}
1301
+ [1]
1302
+ [3]><><><><{1..+3..2}
1303
+ [1]
1304
+ [3]><><><><{+1..+3..+2}
1305
+ [1]
1306
+ [3]><><><><{+3..1..+2}
1307
+ [3]
1308
+ [1]><><><><{+05..8}
1309
+ [5]
1310
+ [6]
1311
+ [7]
1312
+ [8]><><><><{+005..8}
1313
+ [5]
1314
+ [6]
1315
+ [7]
1316
+ [8]><><><><{05..+8}
1317
+ [05]
1318
+ [06]
1319
+ [07]
1320
+ [08]><><><><{+5..+08}
1321
+ [5]
1322
+ [6]
1323
+ [7]
1324
+ [8]><><><><{+05..+05}
1325
+ [5]><><><><{+05..08}
1326
+ [005]
1327
+ [006]
1328
+ [007]
1329
+ [008]><><><><{+10..08}
1330
+ [010]
1331
+ [009]
1332
+ [008]><><><><{-05..+2}
1333
+ [-05]
1334
+ [-04]
1335
+ [-03]
1336
+ [-02]
1337
+ [-01]
1338
+ [000]
1339
+ [001]
1340
+ [002]><><><><{+5..05}
1341
+ [05]><><><><{+5..08}
1342
+ [05]
1343
+ [06]
1344
+ [07]
1345
+ [08]><><><><{08..+5}
1346
+ [08]
1347
+ [07]
1348
+ [06]
1349
+ [05]><><><><{+003..1}
1350
+ [3]
1351
+ [2]
1352
+ [1]><><><><{a..z..+2}
1353
+ [a]
1354
+ [c]
1355
+ [e]
1356
+ [g]
1357
+ [i]
1358
+ [k]
1359
+ [m]
1360
+ [o]
1361
+ [q]
1362
+ [s]
1363
+ [u]
1364
+ [w]
1365
+ [y]><><><><a{+1..2}b
1366
+ [a1b]
1367
+ [a2b]><><><><{a,{+1..2},b}
1368
+ [a]
1369
+ [1]
1370
+ [2]
1371
+ [b]><><><><{+9223372036854775806..+9223372036854775807}
1372
+ [9223372036854775806]
1373
+ [9223372036854775807]><><><><{١..٣}
1374
+ [{١..٣}]><><><><{1..3}
1375
+ [{1..3}]><><><><{1..٣}
1376
+ [{1..٣}]><><><><{١..3}
1377
+ [{١..3}]><><><><{1..3..٢}
1378
+ [{1..3..٢}]><><><><{a..e..٢}
1379
+ [{a..e..٢}]><><><><{0١..03}
1380
+ [{0١..03}]><><><><x{١..٣}y
1381
+ [x{١..٣}y]><><><><{a,{١..٣},b}
1382
+ [a]
1383
+ [{١..٣}]
1384
+ [b]><><><><{+١..+٣}
1385
+ [{+١..+٣}]><><><><{-١..٣}
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
@@ -50,7 +50,18 @@ class TestBraces:
50
50
  dollar_cases = [
51
51
  ['${1..3}', ['${1..3}']],
52
52
  ['${a,b}${c,d}', ['${a,b}${c,d}']],
53
- ['x${a,b}x${c,d}x', ['x${a,b}x${c,d}x']]
53
+ ['x${a,b}x${c,d}x', ['x${a,b}x${c,d}x']],
54
+ ['{a,${b}}', ['a', '${b}']],
55
+ ['{${a,b},c}', ['${a,b}', 'c']],
56
+ ['{a,${b{c}d}e}', ['a', '${b{c}d}e']],
57
+ [r'{a,${b\}c}}', ['a', '${b}c}']],
58
+ ['${x{1..2}}', ['${x1}', '${x2}']],
59
+ ['${x{a,b}}', ['${xa}', '${xb}']],
60
+ ['{a,${x{1..2}}}', ['a', '${x1}', '${x2}']],
61
+ ['{a,${x{c,d}}}', ['a', '${xc}', '${xd}']],
62
+ ['x${y{a,b}}z', ['x${ya}z', 'x${yb}z']],
63
+ ['q{b}${b,c}z', ['q{b}${b,c}z']],
64
+ ['{a${x:-..b}},c}', ['a${x:-..b}}', 'c']]
54
65
  ]
55
66
 
56
67
  empty_cases = [
@@ -72,7 +83,13 @@ class TestBraces:
72
83
  nested_cases = [
73
84
  ['{a,b{1..3},c}', ['a', 'b1', 'b2', 'b3', 'c']],
74
85
  ['{{A..Z},{a..z}}', list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')],
75
- ['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}']]
76
93
  ]
77
94
 
78
95
  order_cases = [
@@ -0,0 +1,164 @@
1
+ site_name: Bracex
2
+ site_url: https://facelessuser.github.io/bracex
3
+ repo_url: https://github.com/facelessuser/bracex
4
+ edit_uri: tree/master/docs/src/markdown
5
+ site_description: Python spell checker.
6
+ copyright: |
7
+ Copyright &copy; 2018 - 2026 <a href="https://github.com/facelessuser" target="_blank" rel="noopener">Isaac Muse</a>
8
+
9
+ docs_dir: docs/src/markdown
10
+ theme:
11
+ name: zensical_pymdownx
12
+ custom_dir: docs/theme
13
+ features:
14
+ - announce.dismiss
15
+ - navigation.tabs
16
+ - navigation.top
17
+ - navigation.instant
18
+ - navigation.indexes
19
+ - navigation.path
20
+ - navigation.footer
21
+ - toc.follow
22
+ - content.tabs.link
23
+ - search.share
24
+ - search.highlight
25
+ - search.suggest
26
+ - content.code.copy
27
+ - content.tabs.link
28
+ - content.code.annotations
29
+
30
+ nav:
31
+ - Home:
32
+ - Usage: index.md
33
+ - About:
34
+ - Contributing &amp; Support: about/contributing.md
35
+ - Development: about/development.md
36
+ - Changelog: about/changelog.md
37
+ - License: about/license.md
38
+
39
+ markdown_extensions:
40
+ - toc:
41
+ slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}}
42
+ permalink: ""
43
+ - smarty:
44
+ smart_quotes: false
45
+ - pymdownx.betterem:
46
+ - attr_list:
47
+ - tables:
48
+ - abbr:
49
+ - footnotes:
50
+ - md_in_html:
51
+ - pymdownx.superfences:
52
+ preserve_tabs: true
53
+ relaxed_headers: true
54
+ custom_fences:
55
+ - name: py
56
+ class: 'highlight'
57
+ format: !!python/object/apply:tools.pycon.py_command_formatter
58
+ kwds:
59
+ init: |
60
+ import bracex
61
+ validator: !!python/name:tools.pycon.py_command_validator
62
+ reset: !!python/name:tools.pycon.py_command_reset
63
+ - name: python
64
+ class: 'highlight'
65
+ format: !!python/object/apply:tools.pycon.py_command_formatter
66
+ kwds:
67
+ init: |
68
+ import bracex
69
+ validator: !!python/name:tools.pycon.py_command_validator
70
+ reset: !!python/name:tools.pycon.py_command_reset
71
+ - pymdownx.highlight:
72
+ line_spans: __codeline
73
+ line_anchors: __codelineno
74
+ anchor_linenums: True
75
+ - pymdownx.inlinehilite:
76
+ - pymdownx.magiclink:
77
+ repo_url_shortener: true
78
+ repo_url_shorthand: true
79
+ social_url_shorthand: true
80
+ user: facelessuser
81
+ repo: bracex
82
+ - pymdownx.tilde:
83
+ - pymdownx.caret:
84
+ - pymdownx.smartsymbols:
85
+ - pymdownx.emoji:
86
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
87
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
88
+ - pymdownx.escapeall:
89
+ hardbreak: True
90
+ nbsp: True
91
+ - pymdownx.tasklist:
92
+ custom_checkbox: true
93
+ - pymdownx.progressbar:
94
+ - pymdownx.striphtml:
95
+ - pymdownx.snippets:
96
+ base_path:
97
+ - docs/src/markdown/.snippets
98
+ - LICENSE.md
99
+ auto_append:
100
+ - refs.md
101
+ - pymdownx.keys:
102
+ separator: "\uff0b"
103
+ - pymdownx.saneheaders:
104
+ - pymdownx.blocks.admonition:
105
+ types:
106
+ - new
107
+ - settings
108
+ - note
109
+ - abstract
110
+ - info
111
+ - tip
112
+ - success
113
+ - question
114
+ - warning
115
+ - failure
116
+ - danger
117
+ - bug
118
+ - example
119
+ - quote
120
+ - pymdownx.blocks.details:
121
+ types:
122
+ - name: details-new
123
+ class: new
124
+ - name: details-settings
125
+ class: settings
126
+ - name: details-note
127
+ class: note
128
+ - name: details-abstract
129
+ class: abstract
130
+ - name: details-info
131
+ class: info
132
+ - name: details-tip
133
+ class: tip
134
+ - name: details-success
135
+ class: success
136
+ - name: details-question
137
+ class: question
138
+ - name: details-warning
139
+ class: warning
140
+ - name: details-failure
141
+ class: failure
142
+ - name: details-danger
143
+ class: danger
144
+ - name: details-bug
145
+ class: bug
146
+ - name: details-example
147
+ class: example
148
+ - name: details-quote
149
+ class: quote
150
+ - pymdownx.blocks.html:
151
+ - pymdownx.blocks.definition:
152
+ - pymdownx.blocks.tab:
153
+ alternate_style: True
154
+ combine_header_slug: True
155
+ slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}}
156
+ - pymdownx.quotes:
157
+ callouts: true
158
+
159
+ extra:
160
+ social:
161
+ - icon: fontawesome/brands/github
162
+ link: https://github.com/facelessuser
163
+ zensical_pymdownx:
164
+ sponsor: https://github.com/sponsors/facelessuser
@@ -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