wildling 2.0.1__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.
- wildling-2.0.1/PKG-INFO +109 -0
- wildling-2.0.1/README.md +100 -0
- wildling-2.0.1/pyproject.toml +24 -0
- wildling-2.0.1/setup.cfg +4 -0
- wildling-2.0.1/wildling/__init__.py +65 -0
- wildling-2.0.1/wildling/__main__.py +4 -0
- wildling-2.0.1/wildling/cli.py +245 -0
- wildling-2.0.1/wildling/generator.py +36 -0
- wildling-2.0.1/wildling/help.txt +43 -0
- wildling-2.0.1/wildling/parse_pattern.py +161 -0
- wildling-2.0.1/wildling/token.py +58 -0
- wildling-2.0.1/wildling.egg-info/PKG-INFO +109 -0
- wildling-2.0.1/wildling.egg-info/SOURCES.txt +14 -0
- wildling-2.0.1/wildling.egg-info/dependency_links.txt +1 -0
- wildling-2.0.1/wildling.egg-info/entry_points.txt +2 -0
- wildling-2.0.1/wildling.egg-info/top_level.txt +1 -0
wildling-2.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wildling
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Pattern based string generator library and CLI
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/dotmonk/wildling
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# wildling
|
|
11
|
+
|
|
12
|
+
Python library and CLI for pattern-based string generation. **Zero third-party dependencies** (stdlib only). Requires Python 3.9+.
|
|
13
|
+
|
|
14
|
+
<!-- wildling:preamble -->
|
|
15
|
+
**Docs:** [Website](https://dotmonk.github.io/wildling/) · [Sandbox](https://dotmonk.github.io/wildling/sandbox.html) · [Syntax](https://dotmonk.github.io/wildling/syntax.html) · [Source](https://github.com/dotmonk/wildling/tree/main/python)
|
|
16
|
+
|
|
17
|
+
**Registry:** [PyPI](https://pypi.org/project/wildling/)
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
http://${'dev,stage,prod'}\-${'api,web'}#{0-2}.example.${'com,net,org'}/@.html
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
(The `\-` is a literal hyphen; bare `-` would mean “one letter or digit”. `@` is one lowercase letter.)
|
|
26
|
+
|
|
27
|
+
That builds **URL-shaped** candidates: scheme `http://`, then environment × service × optional digits × TLD, then a one-letter path page. Three environments, two services, zero–two digits (`''`, `0`–`9`, `00`–`99`), three TLDs, and `a`–`z` → **51948** strings — the kind of list you generate for fuzzing links or probing staging hosts, not type out.
|
|
28
|
+
|
|
29
|
+
A few of them:
|
|
30
|
+
|
|
31
|
+
- `http://dev-api.example.com/a.html` / `http://stage-web.example.com/z.html`
|
|
32
|
+
- `http://dev-api0.example.net/a.html` / `http://prod-web9.example.org/m.html`
|
|
33
|
+
- `http://dev-api00.example.com/a.html` / `http://prod-web99.example.org/z.html`
|
|
34
|
+
|
|
35
|
+
Named dictionaries (`%{'hosts'}`) work the same way when the word lists live in files.
|
|
36
|
+
|
|
37
|
+
Try it in the [sandbox](https://dotmonk.github.io/wildling/sandbox.html?pattern=http%3A%2F%2F%24%7B%27dev%2Cstage%2Cprod%27%7D%5C-%24%7B%27api%2Cweb%27%7D%23%7B0-2%7D.example.%24%7B%27com%2Cnet%2Corg%27%7D%2F%40.html), or see [pattern syntax](https://dotmonk.github.io/wildling/syntax.html) for length ranges, dictionaries, and escapes.
|
|
38
|
+
<!-- /wildling:preamble -->
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
From this repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd python
|
|
46
|
+
./build.sh
|
|
47
|
+
# or without Docker:
|
|
48
|
+
cp ../docs/help.txt wildling/help.txt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The CLI is `./bin/wildling`.
|
|
52
|
+
|
|
53
|
+
**Registry:** `pip install wildling`
|
|
54
|
+
|
|
55
|
+
**Git:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install "git+https://github.com/dotmonk/wildling.git@v2.0.0#subdirectory=python"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Library
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from wildling import create_wildling
|
|
65
|
+
|
|
66
|
+
wildling = create_wildling(
|
|
67
|
+
patterns=["abrakadabra", "Year 19##"],
|
|
68
|
+
dictionaries={"colors": ["red", "blue"]},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
wildling.count() # 101
|
|
72
|
+
wildling.get(0) # "abrakadabra"
|
|
73
|
+
|
|
74
|
+
value = wildling.next()
|
|
75
|
+
while value is not False:
|
|
76
|
+
print(value)
|
|
77
|
+
value = wildling.next()
|
|
78
|
+
|
|
79
|
+
wildling.reset()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### API
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `next()` | Next combination, or `False` when exhausted |
|
|
87
|
+
| `get(index)` | Combination at `index`, or `False` if out of range |
|
|
88
|
+
| `count()` | Total combinations across all patterns |
|
|
89
|
+
| `index()` | Current position (after `next` calls) |
|
|
90
|
+
| `reset()` | Reset iteration to the start |
|
|
91
|
+
| `generators()` | Per-pattern generators |
|
|
92
|
+
|
|
93
|
+
## CLI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
./bin/wildling "Year 19##"
|
|
97
|
+
./bin/wildling --dictionary planets:../dictionaries/planets.txt "%{'planets'}"
|
|
98
|
+
./bin/wildling --template ./config.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Help text and `--check` output follow [`docs/cli.md`](../docs/cli.md) / [`docs/help.txt`](../docs/help.txt).
|
|
102
|
+
|
|
103
|
+
## Build
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
./build.sh # Docker: copy help.txt + byte-compile
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Project tests live in `../tests/` and are run with `../test.sh`.
|
wildling-2.0.1/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# wildling
|
|
2
|
+
|
|
3
|
+
Python library and CLI for pattern-based string generation. **Zero third-party dependencies** (stdlib only). Requires Python 3.9+.
|
|
4
|
+
|
|
5
|
+
<!-- wildling:preamble -->
|
|
6
|
+
**Docs:** [Website](https://dotmonk.github.io/wildling/) · [Sandbox](https://dotmonk.github.io/wildling/sandbox.html) · [Syntax](https://dotmonk.github.io/wildling/syntax.html) · [Source](https://github.com/dotmonk/wildling/tree/main/python)
|
|
7
|
+
|
|
8
|
+
**Registry:** [PyPI](https://pypi.org/project/wildling/)
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
http://${'dev,stage,prod'}\-${'api,web'}#{0-2}.example.${'com,net,org'}/@.html
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(The `\-` is a literal hyphen; bare `-` would mean “one letter or digit”. `@` is one lowercase letter.)
|
|
17
|
+
|
|
18
|
+
That builds **URL-shaped** candidates: scheme `http://`, then environment × service × optional digits × TLD, then a one-letter path page. Three environments, two services, zero–two digits (`''`, `0`–`9`, `00`–`99`), three TLDs, and `a`–`z` → **51948** strings — the kind of list you generate for fuzzing links or probing staging hosts, not type out.
|
|
19
|
+
|
|
20
|
+
A few of them:
|
|
21
|
+
|
|
22
|
+
- `http://dev-api.example.com/a.html` / `http://stage-web.example.com/z.html`
|
|
23
|
+
- `http://dev-api0.example.net/a.html` / `http://prod-web9.example.org/m.html`
|
|
24
|
+
- `http://dev-api00.example.com/a.html` / `http://prod-web99.example.org/z.html`
|
|
25
|
+
|
|
26
|
+
Named dictionaries (`%{'hosts'}`) work the same way when the word lists live in files.
|
|
27
|
+
|
|
28
|
+
Try it in the [sandbox](https://dotmonk.github.io/wildling/sandbox.html?pattern=http%3A%2F%2F%24%7B%27dev%2Cstage%2Cprod%27%7D%5C-%24%7B%27api%2Cweb%27%7D%23%7B0-2%7D.example.%24%7B%27com%2Cnet%2Corg%27%7D%2F%40.html), or see [pattern syntax](https://dotmonk.github.io/wildling/syntax.html) for length ranges, dictionaries, and escapes.
|
|
29
|
+
<!-- /wildling:preamble -->
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
From this repository:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd python
|
|
37
|
+
./build.sh
|
|
38
|
+
# or without Docker:
|
|
39
|
+
cp ../docs/help.txt wildling/help.txt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The CLI is `./bin/wildling`.
|
|
43
|
+
|
|
44
|
+
**Registry:** `pip install wildling`
|
|
45
|
+
|
|
46
|
+
**Git:**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "git+https://github.com/dotmonk/wildling.git@v2.0.0#subdirectory=python"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Library
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from wildling import create_wildling
|
|
56
|
+
|
|
57
|
+
wildling = create_wildling(
|
|
58
|
+
patterns=["abrakadabra", "Year 19##"],
|
|
59
|
+
dictionaries={"colors": ["red", "blue"]},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
wildling.count() # 101
|
|
63
|
+
wildling.get(0) # "abrakadabra"
|
|
64
|
+
|
|
65
|
+
value = wildling.next()
|
|
66
|
+
while value is not False:
|
|
67
|
+
print(value)
|
|
68
|
+
value = wildling.next()
|
|
69
|
+
|
|
70
|
+
wildling.reset()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### API
|
|
74
|
+
|
|
75
|
+
| Method | Description |
|
|
76
|
+
|--------|-------------|
|
|
77
|
+
| `next()` | Next combination, or `False` when exhausted |
|
|
78
|
+
| `get(index)` | Combination at `index`, or `False` if out of range |
|
|
79
|
+
| `count()` | Total combinations across all patterns |
|
|
80
|
+
| `index()` | Current position (after `next` calls) |
|
|
81
|
+
| `reset()` | Reset iteration to the start |
|
|
82
|
+
| `generators()` | Per-pattern generators |
|
|
83
|
+
|
|
84
|
+
## CLI
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
./bin/wildling "Year 19##"
|
|
88
|
+
./bin/wildling --dictionary planets:../dictionaries/planets.txt "%{'planets'}"
|
|
89
|
+
./bin/wildling --template ./config.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Help text and `--check` output follow [`docs/cli.md`](../docs/cli.md) / [`docs/help.txt`](../docs/help.txt).
|
|
93
|
+
|
|
94
|
+
## Build
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
./build.sh # Docker: copy help.txt + byte-compile
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Project tests live in `../tests/` and are run with `../test.sh`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wildling"
|
|
7
|
+
version = "2.0.1"
|
|
8
|
+
description = "Pattern based string generator library and CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Repository = "https://github.com/dotmonk/wildling"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
wildling = "wildling.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["wildling*"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
wildling = ["help.txt"]
|
wildling-2.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from .generator import Generator, create_generator
|
|
6
|
+
from .parse_pattern import Dictionaries
|
|
7
|
+
|
|
8
|
+
__version__ = "2.0.1"
|
|
9
|
+
|
|
10
|
+
WildlingResult = Union[str, bool]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Wildling:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
patterns: List[str],
|
|
17
|
+
dictionaries: Optional[Dictionaries] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self._dictionaries: Dictionaries = dictionaries or {}
|
|
20
|
+
self._generators = [
|
|
21
|
+
create_generator(pattern, self._dictionaries) for pattern in patterns
|
|
22
|
+
]
|
|
23
|
+
self._pattern_count = sum(generator.count() for generator in self._generators)
|
|
24
|
+
self._internal_index = 0
|
|
25
|
+
|
|
26
|
+
def index(self) -> int:
|
|
27
|
+
return self._internal_index
|
|
28
|
+
|
|
29
|
+
def count(self) -> int:
|
|
30
|
+
return self._pattern_count
|
|
31
|
+
|
|
32
|
+
def reset(self) -> None:
|
|
33
|
+
self._internal_index = 0
|
|
34
|
+
|
|
35
|
+
def next(self) -> WildlingResult:
|
|
36
|
+
if self._internal_index == self._pattern_count:
|
|
37
|
+
return False
|
|
38
|
+
self._internal_index += 1
|
|
39
|
+
return self.get(self._internal_index - 1)
|
|
40
|
+
|
|
41
|
+
def generators(self) -> List[Generator]:
|
|
42
|
+
return self._generators
|
|
43
|
+
|
|
44
|
+
def get(self, index: int) -> WildlingResult:
|
|
45
|
+
if index > self._pattern_count - 1 or index < 0:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
segment_index = 0
|
|
49
|
+
for generator in self._generators:
|
|
50
|
+
pattern_index = index - segment_index
|
|
51
|
+
if pattern_index < generator.count():
|
|
52
|
+
return generator.get(pattern_index)
|
|
53
|
+
segment_index += generator.count()
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_wildling(
|
|
58
|
+
patterns: List[str],
|
|
59
|
+
dictionaries: Optional[Dictionaries] = None,
|
|
60
|
+
) -> Wildling:
|
|
61
|
+
return Wildling(patterns, dictionaries)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Alias matching the JavaScript default export name in docs
|
|
65
|
+
createWildling = create_wildling
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
from . import __version__, create_wildling
|
|
10
|
+
from .generator import Generator
|
|
11
|
+
|
|
12
|
+
Dictionary = Dict[str, List[str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CliArgs:
|
|
17
|
+
selects: List[int] = field(default_factory=list)
|
|
18
|
+
ranges: List[Tuple[int, int]] = field(default_factory=list)
|
|
19
|
+
check: bool = False
|
|
20
|
+
dictionaries: Dictionary = field(default_factory=dict)
|
|
21
|
+
patterns: List[str] = field(default_factory=list)
|
|
22
|
+
help: bool = False
|
|
23
|
+
version: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_range(value: str) -> Optional[Tuple[int, int]]:
|
|
27
|
+
parts = value.split("-", 1)
|
|
28
|
+
if len(parts) != 2 or not parts[0].isdigit() or not parts[1].isdigit():
|
|
29
|
+
return None
|
|
30
|
+
start = int(parts[0])
|
|
31
|
+
end = int(parts[1])
|
|
32
|
+
return (start, end) if start <= end else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_dictionary_file(path: str) -> List[str]:
|
|
36
|
+
with open(path, encoding="utf-8") as handle:
|
|
37
|
+
return [line.strip() for line in handle.read().splitlines() if line.strip()]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply_dictionary(result: CliArgs, name: str, value: Union[str, List[str]]) -> None:
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
result.dictionaries[name] = [str(item) for item in value]
|
|
43
|
+
return
|
|
44
|
+
if isinstance(value, str) and os.path.exists(value):
|
|
45
|
+
try:
|
|
46
|
+
result.dictionaries[name] = load_dictionary_file(value)
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def apply_template(result: CliArgs, path: str) -> None:
|
|
52
|
+
if not os.path.exists(path):
|
|
53
|
+
print(f"Template file not found: {path}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with open(path, encoding="utf-8") as handle:
|
|
58
|
+
template = json.load(handle)
|
|
59
|
+
except (OSError, json.JSONDecodeError):
|
|
60
|
+
print(f"Invalid JSON template: {path}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
if not isinstance(template, dict):
|
|
64
|
+
print(f"Invalid JSON template: {path}", file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
if template.get("check") is True:
|
|
68
|
+
result.check = True
|
|
69
|
+
|
|
70
|
+
select = template.get("select")
|
|
71
|
+
if isinstance(select, list):
|
|
72
|
+
for val in select:
|
|
73
|
+
try:
|
|
74
|
+
number = int(val)
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
continue
|
|
77
|
+
if number >= 0:
|
|
78
|
+
result.selects.append(number)
|
|
79
|
+
|
|
80
|
+
ranges = template.get("range")
|
|
81
|
+
if isinstance(ranges, list):
|
|
82
|
+
for range_str in ranges:
|
|
83
|
+
parsed = parse_range(str(range_str))
|
|
84
|
+
if parsed is not None:
|
|
85
|
+
result.ranges.append(parsed)
|
|
86
|
+
|
|
87
|
+
dictionaries = template.get("dictionaries")
|
|
88
|
+
if isinstance(dictionaries, dict):
|
|
89
|
+
for name, value in dictionaries.items():
|
|
90
|
+
if isinstance(value, (str, list)):
|
|
91
|
+
apply_dictionary(result, str(name), value)
|
|
92
|
+
|
|
93
|
+
patterns = template.get("patterns")
|
|
94
|
+
if isinstance(patterns, list):
|
|
95
|
+
for pattern in patterns:
|
|
96
|
+
result.patterns.append(str(pattern))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_args(args: List[str]) -> CliArgs:
|
|
100
|
+
result = CliArgs()
|
|
101
|
+
i = 0
|
|
102
|
+
while i < len(args):
|
|
103
|
+
arg = args[i]
|
|
104
|
+
|
|
105
|
+
if arg in ("--help", "-h"):
|
|
106
|
+
result.help = True
|
|
107
|
+
i += 1
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if arg in ("--version", "-v"):
|
|
111
|
+
result.version = True
|
|
112
|
+
i += 1
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if arg == "--check":
|
|
116
|
+
result.check = True
|
|
117
|
+
i += 1
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if arg == "--select":
|
|
121
|
+
i += 1
|
|
122
|
+
if i >= len(args):
|
|
123
|
+
break
|
|
124
|
+
try:
|
|
125
|
+
val = int(args[i])
|
|
126
|
+
except ValueError:
|
|
127
|
+
val = -1
|
|
128
|
+
if val >= 0:
|
|
129
|
+
result.selects.append(val)
|
|
130
|
+
i += 1
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if arg == "--range":
|
|
134
|
+
i += 1
|
|
135
|
+
if i >= len(args):
|
|
136
|
+
break
|
|
137
|
+
parsed = parse_range(args[i])
|
|
138
|
+
if parsed is not None:
|
|
139
|
+
result.ranges.append(parsed)
|
|
140
|
+
i += 1
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if arg == "--dictionary":
|
|
144
|
+
i += 1
|
|
145
|
+
if i >= len(args):
|
|
146
|
+
break
|
|
147
|
+
name, sep, path = args[i].partition(":")
|
|
148
|
+
if sep and name and path:
|
|
149
|
+
apply_dictionary(result, name, path)
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
if arg == "--template":
|
|
154
|
+
i += 1
|
|
155
|
+
if i >= len(args):
|
|
156
|
+
print("Missing path for --template", file=sys.stderr)
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
apply_template(result, args[i])
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
result.patterns.append(arg)
|
|
163
|
+
i += 1
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_help_text() -> str:
|
|
169
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
170
|
+
candidates = [
|
|
171
|
+
os.path.join(here, "help.txt"),
|
|
172
|
+
os.path.join(here, "..", "..", "docs", "help.txt"),
|
|
173
|
+
]
|
|
174
|
+
for path in candidates:
|
|
175
|
+
if os.path.exists(path):
|
|
176
|
+
with open(path, encoding="utf-8") as handle:
|
|
177
|
+
return handle.read()
|
|
178
|
+
return "wildling - pattern based string generator\n\nHelp text unavailable.\n"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def format_list(values: List[Union[str, int]]) -> str:
|
|
182
|
+
return "" if not values else " " + " ".join(str(value) for value in values)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def format_check_output(args: CliArgs, total: int, generators: List[Generator]) -> str:
|
|
186
|
+
lines = [
|
|
187
|
+
f"patterns:{format_list(args.patterns)}",
|
|
188
|
+
f"dictionaries:{format_list(list(args.dictionaries.keys()))}",
|
|
189
|
+
f"select:{format_list(args.selects)}",
|
|
190
|
+
f"range:{format_list([f'{start}-{end}' for start, end in args.ranges])}",
|
|
191
|
+
f"total: {total}",
|
|
192
|
+
]
|
|
193
|
+
for gen in generators:
|
|
194
|
+
lines.append(f"generator: {gen.source} {gen.count()}")
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main(argv: Optional[List[str]] = None) -> None:
|
|
199
|
+
args = parse_args(sys.argv[1:] if argv is None else argv)
|
|
200
|
+
|
|
201
|
+
if args.help:
|
|
202
|
+
print(load_help_text().rstrip())
|
|
203
|
+
sys.exit(0)
|
|
204
|
+
|
|
205
|
+
if args.version:
|
|
206
|
+
print(f"wildling {__version__}")
|
|
207
|
+
sys.exit(0)
|
|
208
|
+
|
|
209
|
+
if not args.patterns:
|
|
210
|
+
print("No pattern provided. Use --help for usage information.", file=sys.stderr)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
wildcard = create_wildling(args.patterns, args.dictionaries)
|
|
214
|
+
|
|
215
|
+
if args.check:
|
|
216
|
+
print(format_check_output(args, wildcard.count(), wildcard.generators()))
|
|
217
|
+
sys.exit(0)
|
|
218
|
+
|
|
219
|
+
if args.selects or args.ranges:
|
|
220
|
+
oor = False
|
|
221
|
+
for index in args.selects:
|
|
222
|
+
value = wildcard.get(index)
|
|
223
|
+
if value is False:
|
|
224
|
+
print(f"out of range: {index}", file=sys.stderr)
|
|
225
|
+
oor = True
|
|
226
|
+
else:
|
|
227
|
+
print(value)
|
|
228
|
+
for start, end in args.ranges:
|
|
229
|
+
for index in range(start, end + 1):
|
|
230
|
+
value = wildcard.get(index)
|
|
231
|
+
if value is False:
|
|
232
|
+
print(f"out of range: {index}", file=sys.stderr)
|
|
233
|
+
oor = True
|
|
234
|
+
else:
|
|
235
|
+
print(value)
|
|
236
|
+
sys.exit(1 if oor else 0)
|
|
237
|
+
|
|
238
|
+
value = wildcard.next()
|
|
239
|
+
while value is not False:
|
|
240
|
+
print(value)
|
|
241
|
+
value = wildcard.next()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from .parse_pattern import Dictionaries, parse_pattern
|
|
6
|
+
from .token import Token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Generator:
|
|
10
|
+
def __init__(self, input_pattern: str, dictionaries: Dictionaries) -> None:
|
|
11
|
+
self.source = input_pattern
|
|
12
|
+
self._tokens = parse_pattern(input_pattern, dictionaries)
|
|
13
|
+
self._count = 1
|
|
14
|
+
for token in self._tokens:
|
|
15
|
+
self._count *= token.count()
|
|
16
|
+
|
|
17
|
+
def count(self) -> int:
|
|
18
|
+
return self._count
|
|
19
|
+
|
|
20
|
+
def tokens(self) -> List[Token]:
|
|
21
|
+
return self._tokens
|
|
22
|
+
|
|
23
|
+
def get(self, index: int) -> str:
|
|
24
|
+
if index > self._count - 1 or index < 0:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
string_array: List[str] = []
|
|
28
|
+
index_with_offset = index
|
|
29
|
+
for token in self._tokens:
|
|
30
|
+
string_array.append(token.get(index_with_offset % token.count()))
|
|
31
|
+
index_with_offset //= token.count()
|
|
32
|
+
return "".join(string_array)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_generator(input_pattern: str, dictionaries: Dictionaries) -> Generator:
|
|
36
|
+
return Generator(input_pattern, dictionaries)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
wildling - pattern based string generator
|
|
2
|
+
|
|
3
|
+
USAGE
|
|
4
|
+
wildling [options] [pattern ...]
|
|
5
|
+
|
|
6
|
+
OPTIONS
|
|
7
|
+
--select # Print only combination number # (repeatable)
|
|
8
|
+
--range #-# Print combinations from # to # inclusive (repeatable)
|
|
9
|
+
--check Print generation info instead of results
|
|
10
|
+
--dictionary <name>:<path> Load a dictionary file as <name> (repeatable)
|
|
11
|
+
--template <path> Load options from a JSON template file
|
|
12
|
+
--help, -h Show this help message
|
|
13
|
+
--version, -v Show version
|
|
14
|
+
|
|
15
|
+
PATTERNS
|
|
16
|
+
# #{N} #{N-M} digits 0-9; optional fixed or ranged length
|
|
17
|
+
@ @{N} @{N-M} lowercase a-z
|
|
18
|
+
* *{N} *{N-M} lowercase a-z and digits 0-9
|
|
19
|
+
& &{N} &{N-M} lowercase and uppercase letters
|
|
20
|
+
! !{N} !{N-M} uppercase A-Z
|
|
21
|
+
? ?{N} ?{N-M} uppercase A-Z and digits 0-9
|
|
22
|
+
- -{N} -{N-M} letters and digits
|
|
23
|
+
${'a,b',N-M} combinations from a comma-separated word list
|
|
24
|
+
%{'name'} %{'name',N-M} words from a named dictionary
|
|
25
|
+
\# \@ \$ ... escape a wildcard character
|
|
26
|
+
|
|
27
|
+
TEMPLATE JSON
|
|
28
|
+
{
|
|
29
|
+
"patterns": ["Year 19##", "%{'colors'}"],
|
|
30
|
+
"dictionaries": {
|
|
31
|
+
"colors": "path/to/colors.txt",
|
|
32
|
+
"inline": ["red", "blue"]
|
|
33
|
+
},
|
|
34
|
+
"select": [0, 2],
|
|
35
|
+
"range": ["5-7"],
|
|
36
|
+
"check": false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
EXAMPLES
|
|
40
|
+
wildling --dictionary colors:./colors.txt "%{'colors'}#"
|
|
41
|
+
wildling --template ./config.json
|
|
42
|
+
wildling --select 0 --range 8-9 "##"
|
|
43
|
+
wildling "${'blue,red',1-2}"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Callable, Dict, List, Union
|
|
5
|
+
|
|
6
|
+
from .token import Token, TokenOptions, create_token
|
|
7
|
+
|
|
8
|
+
Dictionaries = Dict[str, List[str]]
|
|
9
|
+
|
|
10
|
+
TOKEN_PARSING_REGEX = re.compile(
|
|
11
|
+
r"(\\[%@$*#&?!-]|[%@$*#&?!-]\{.*?\}|[%@$*#&?!-])"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_length_with_variants(part: str, variants: List[str]) -> TokenOptions:
|
|
16
|
+
length_arg_regex = re.compile(r"\{((\d+)-(\d+)|(\d+))\}")
|
|
17
|
+
match = length_arg_regex.search(part)
|
|
18
|
+
|
|
19
|
+
start_length = 1
|
|
20
|
+
end_length = 1
|
|
21
|
+
|
|
22
|
+
if match is not None and match.group(2):
|
|
23
|
+
start_length = int(match.group(2))
|
|
24
|
+
end_length = int(match.group(3))
|
|
25
|
+
elif match is not None and match.group(1):
|
|
26
|
+
start_length = int(match.group(1))
|
|
27
|
+
end_length = start_length
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"variants": variants,
|
|
31
|
+
"startLength": start_length,
|
|
32
|
+
"endLength": end_length,
|
|
33
|
+
"src": part,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_length_with_string(part: str) -> Union[TokenOptions, bool]:
|
|
38
|
+
length_arg_regex = re.compile(r"\{'(.*)'(?:,(\d+)-(\d+))?(?:,(\d+))?\}")
|
|
39
|
+
match = length_arg_regex.search(part)
|
|
40
|
+
|
|
41
|
+
if match is None:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
if match.group(2) is not None and match.group(3) is not None:
|
|
45
|
+
return {
|
|
46
|
+
"string": match.group(1) or "",
|
|
47
|
+
"startLength": int(match.group(2)),
|
|
48
|
+
"endLength": int(match.group(3)),
|
|
49
|
+
"src": part,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if match.group(4) is not None:
|
|
53
|
+
length = int(match.group(4))
|
|
54
|
+
return {
|
|
55
|
+
"string": match.group(1) or "",
|
|
56
|
+
"startLength": length,
|
|
57
|
+
"endLength": length,
|
|
58
|
+
"src": part,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"string": match.group(1) or "",
|
|
63
|
+
"startLength": 1,
|
|
64
|
+
"endLength": 1,
|
|
65
|
+
"src": part,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def simple_tokenizer(variants_string: str) -> Callable[[str], Token]:
|
|
70
|
+
variants = list(variants_string)
|
|
71
|
+
|
|
72
|
+
def tokenizer(part: str) -> Token:
|
|
73
|
+
return create_token(parse_length_with_variants(part, variants))
|
|
74
|
+
|
|
75
|
+
return tokenizer
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _dictionary_tokenizer(part: str, dictionaries: Dictionaries) -> Token:
|
|
79
|
+
options = parse_length_with_string(part)
|
|
80
|
+
if options is False or (
|
|
81
|
+
isinstance(options, dict)
|
|
82
|
+
and options.get("string")
|
|
83
|
+
and options["string"] not in dictionaries
|
|
84
|
+
):
|
|
85
|
+
options = {
|
|
86
|
+
"variants": [part],
|
|
87
|
+
"startLength": 1,
|
|
88
|
+
"endLength": 1,
|
|
89
|
+
"src": part,
|
|
90
|
+
}
|
|
91
|
+
else:
|
|
92
|
+
assert isinstance(options, dict)
|
|
93
|
+
options["variants"] = dictionaries.get(options.get("string") or "", [])
|
|
94
|
+
return create_token(options)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _words_tokenizer(part: str) -> Token:
|
|
98
|
+
options = parse_length_with_string(part)
|
|
99
|
+
|
|
100
|
+
if options is False:
|
|
101
|
+
options = {
|
|
102
|
+
"variants": [part],
|
|
103
|
+
"startLength": 1,
|
|
104
|
+
"endLength": 1,
|
|
105
|
+
"src": part,
|
|
106
|
+
}
|
|
107
|
+
else:
|
|
108
|
+
assert isinstance(options, dict)
|
|
109
|
+
variants: List[str] = []
|
|
110
|
+
work_string = options.get("string") or ""
|
|
111
|
+
index = 0
|
|
112
|
+
while index < len(work_string):
|
|
113
|
+
if work_string[index : index + 2] == "\\,":
|
|
114
|
+
index += 2
|
|
115
|
+
elif work_string[index] == ",":
|
|
116
|
+
variants.append(work_string[:index])
|
|
117
|
+
work_string = work_string[index + 1 :]
|
|
118
|
+
index = 0
|
|
119
|
+
else:
|
|
120
|
+
index += 1
|
|
121
|
+
variants.append(work_string)
|
|
122
|
+
options["variants"] = [variant.replace("\\,", ",") for variant in variants]
|
|
123
|
+
|
|
124
|
+
return create_token(options)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def part_to_token(part: str, dictionaries: Dictionaries) -> Token:
|
|
128
|
+
tokenizers: Dict[str, Callable[[str], Token]] = {
|
|
129
|
+
"#": simple_tokenizer("0123456789"),
|
|
130
|
+
"@": simple_tokenizer("abcdefghijklmnopqrstuvwxyz"),
|
|
131
|
+
"*": simple_tokenizer("abcdefghijklmnopqrstuvwxyz0123456789"),
|
|
132
|
+
"-": simple_tokenizer(
|
|
133
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
134
|
+
),
|
|
135
|
+
"!": simple_tokenizer("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
|
136
|
+
"?": simple_tokenizer("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),
|
|
137
|
+
"&": simple_tokenizer("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
|
138
|
+
"%": lambda p: _dictionary_tokenizer(p, dictionaries),
|
|
139
|
+
"$": _words_tokenizer,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
tokenizer = tokenizers.get(part[0]) if part else None
|
|
143
|
+
is_escaped_token = (
|
|
144
|
+
len(part) > 1 and part[0] == "\\" and part[1] in tokenizers
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if tokenizer is not None:
|
|
148
|
+
return tokenizer(part)
|
|
149
|
+
if is_escaped_token:
|
|
150
|
+
return create_token(
|
|
151
|
+
{
|
|
152
|
+
"variants": [re.sub(r"^\\", "", part)],
|
|
153
|
+
"src": part,
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
return create_token({"variants": [part], "src": part})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_pattern(input_pattern: str, dictionaries: Dictionaries) -> List[Token]:
|
|
160
|
+
parts = [part for part in TOKEN_PARSING_REGEX.split(input_pattern) if part]
|
|
161
|
+
return [part_to_token(part, dictionaries) for part in parts]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TokenOptions(TypedDict, total=False):
|
|
7
|
+
string: str
|
|
8
|
+
startLength: int
|
|
9
|
+
endLength: int
|
|
10
|
+
variants: List[str]
|
|
11
|
+
src: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _default_integer_option(option: object, fallback: int) -> int:
|
|
15
|
+
return option if isinstance(option, int) and option >= 0 else fallback
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Token:
|
|
19
|
+
def __init__(self, options: TokenOptions) -> None:
|
|
20
|
+
self._src = options.get("src", "")
|
|
21
|
+
self._start_length = _default_integer_option(options.get("startLength"), 1)
|
|
22
|
+
self._end_length = _default_integer_option(options.get("endLength"), 1)
|
|
23
|
+
self._variants = options.get("variants") or []
|
|
24
|
+
self._count = 0
|
|
25
|
+
for length in range(self._start_length, self._end_length + 1):
|
|
26
|
+
self._count += len(self._variants) ** length
|
|
27
|
+
|
|
28
|
+
def count(self) -> int:
|
|
29
|
+
return self._count
|
|
30
|
+
|
|
31
|
+
def src(self) -> str:
|
|
32
|
+
return self._src
|
|
33
|
+
|
|
34
|
+
def get(self, index: int) -> str:
|
|
35
|
+
if index > self._count - 1 or index < 0:
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
if index == 0 and self._start_length == 0:
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
index_with_offset = index
|
|
42
|
+
string_length = self._start_length
|
|
43
|
+
for string_length in range(self._start_length, self._end_length + 1):
|
|
44
|
+
offset_count = len(self._variants) ** string_length
|
|
45
|
+
if index_with_offset < offset_count:
|
|
46
|
+
break
|
|
47
|
+
index_with_offset -= offset_count
|
|
48
|
+
|
|
49
|
+
string_array: List[str] = []
|
|
50
|
+
for _ in range(string_length):
|
|
51
|
+
variant_index = index_with_offset % len(self._variants)
|
|
52
|
+
index_with_offset //= len(self._variants)
|
|
53
|
+
string_array.append(self._variants[variant_index])
|
|
54
|
+
return "".join(string_array)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_token(options: TokenOptions) -> Token:
|
|
58
|
+
return Token(options)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wildling
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Pattern based string generator library and CLI
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/dotmonk/wildling
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# wildling
|
|
11
|
+
|
|
12
|
+
Python library and CLI for pattern-based string generation. **Zero third-party dependencies** (stdlib only). Requires Python 3.9+.
|
|
13
|
+
|
|
14
|
+
<!-- wildling:preamble -->
|
|
15
|
+
**Docs:** [Website](https://dotmonk.github.io/wildling/) · [Sandbox](https://dotmonk.github.io/wildling/sandbox.html) · [Syntax](https://dotmonk.github.io/wildling/syntax.html) · [Source](https://github.com/dotmonk/wildling/tree/main/python)
|
|
16
|
+
|
|
17
|
+
**Registry:** [PyPI](https://pypi.org/project/wildling/)
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
http://${'dev,stage,prod'}\-${'api,web'}#{0-2}.example.${'com,net,org'}/@.html
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
(The `\-` is a literal hyphen; bare `-` would mean “one letter or digit”. `@` is one lowercase letter.)
|
|
26
|
+
|
|
27
|
+
That builds **URL-shaped** candidates: scheme `http://`, then environment × service × optional digits × TLD, then a one-letter path page. Three environments, two services, zero–two digits (`''`, `0`–`9`, `00`–`99`), three TLDs, and `a`–`z` → **51948** strings — the kind of list you generate for fuzzing links or probing staging hosts, not type out.
|
|
28
|
+
|
|
29
|
+
A few of them:
|
|
30
|
+
|
|
31
|
+
- `http://dev-api.example.com/a.html` / `http://stage-web.example.com/z.html`
|
|
32
|
+
- `http://dev-api0.example.net/a.html` / `http://prod-web9.example.org/m.html`
|
|
33
|
+
- `http://dev-api00.example.com/a.html` / `http://prod-web99.example.org/z.html`
|
|
34
|
+
|
|
35
|
+
Named dictionaries (`%{'hosts'}`) work the same way when the word lists live in files.
|
|
36
|
+
|
|
37
|
+
Try it in the [sandbox](https://dotmonk.github.io/wildling/sandbox.html?pattern=http%3A%2F%2F%24%7B%27dev%2Cstage%2Cprod%27%7D%5C-%24%7B%27api%2Cweb%27%7D%23%7B0-2%7D.example.%24%7B%27com%2Cnet%2Corg%27%7D%2F%40.html), or see [pattern syntax](https://dotmonk.github.io/wildling/syntax.html) for length ranges, dictionaries, and escapes.
|
|
38
|
+
<!-- /wildling:preamble -->
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
From this repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd python
|
|
46
|
+
./build.sh
|
|
47
|
+
# or without Docker:
|
|
48
|
+
cp ../docs/help.txt wildling/help.txt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The CLI is `./bin/wildling`.
|
|
52
|
+
|
|
53
|
+
**Registry:** `pip install wildling`
|
|
54
|
+
|
|
55
|
+
**Git:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install "git+https://github.com/dotmonk/wildling.git@v2.0.0#subdirectory=python"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Library
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from wildling import create_wildling
|
|
65
|
+
|
|
66
|
+
wildling = create_wildling(
|
|
67
|
+
patterns=["abrakadabra", "Year 19##"],
|
|
68
|
+
dictionaries={"colors": ["red", "blue"]},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
wildling.count() # 101
|
|
72
|
+
wildling.get(0) # "abrakadabra"
|
|
73
|
+
|
|
74
|
+
value = wildling.next()
|
|
75
|
+
while value is not False:
|
|
76
|
+
print(value)
|
|
77
|
+
value = wildling.next()
|
|
78
|
+
|
|
79
|
+
wildling.reset()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### API
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `next()` | Next combination, or `False` when exhausted |
|
|
87
|
+
| `get(index)` | Combination at `index`, or `False` if out of range |
|
|
88
|
+
| `count()` | Total combinations across all patterns |
|
|
89
|
+
| `index()` | Current position (after `next` calls) |
|
|
90
|
+
| `reset()` | Reset iteration to the start |
|
|
91
|
+
| `generators()` | Per-pattern generators |
|
|
92
|
+
|
|
93
|
+
## CLI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
./bin/wildling "Year 19##"
|
|
97
|
+
./bin/wildling --dictionary planets:../dictionaries/planets.txt "%{'planets'}"
|
|
98
|
+
./bin/wildling --template ./config.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Help text and `--check` output follow [`docs/cli.md`](../docs/cli.md) / [`docs/help.txt`](../docs/help.txt).
|
|
102
|
+
|
|
103
|
+
## Build
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
./build.sh # Docker: copy help.txt + byte-compile
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Project tests live in `../tests/` and are run with `../test.sh`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
wildling/__init__.py
|
|
4
|
+
wildling/__main__.py
|
|
5
|
+
wildling/cli.py
|
|
6
|
+
wildling/generator.py
|
|
7
|
+
wildling/help.txt
|
|
8
|
+
wildling/parse_pattern.py
|
|
9
|
+
wildling/token.py
|
|
10
|
+
wildling.egg-info/PKG-INFO
|
|
11
|
+
wildling.egg-info/SOURCES.txt
|
|
12
|
+
wildling.egg-info/dependency_links.txt
|
|
13
|
+
wildling.egg-info/entry_points.txt
|
|
14
|
+
wildling.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wildling
|