exform 0.1.0__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.
- exform-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +24 -0
- exform-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +17 -0
- exform-0.1.0/.github/workflows/ci.yml +18 -0
- exform-0.1.0/.github/workflows/publish.yml +31 -0
- exform-0.1.0/.gitignore +13 -0
- exform-0.1.0/CHANGELOG.md +86 -0
- exform-0.1.0/CONTRIBUTING.md +24 -0
- exform-0.1.0/EXAMPLES.md +293 -0
- exform-0.1.0/LICENSE +21 -0
- exform-0.1.0/PKG-INFO +314 -0
- exform-0.1.0/README.md +293 -0
- exform-0.1.0/assets/demo.svg +16 -0
- exform-0.1.0/assets/make_demo.py +97 -0
- exform-0.1.0/pyproject.toml +41 -0
- exform-0.1.0/src/exform/__init__.py +6 -0
- exform-0.1.0/src/exform/__main__.py +4 -0
- exform-0.1.0/src/exform/cli.py +305 -0
- exform-0.1.0/src/exform/synth.py +623 -0
- exform-0.1.0/tests/test_cli_fill.py +66 -0
- exform-0.1.0/tests/test_synth.py +333 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something exform got wrong or a crash
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: examples
|
|
7
|
+
attributes:
|
|
8
|
+
label: The examples you gave
|
|
9
|
+
description: The exact `-e "IN => OUT"` pairs (or --fill input).
|
|
10
|
+
render: shell
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: expected
|
|
15
|
+
attributes:
|
|
16
|
+
label: What you expected vs. what happened
|
|
17
|
+
description: Include the inferred program from `--explain` if you can.
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: input
|
|
21
|
+
id: version
|
|
22
|
+
attributes:
|
|
23
|
+
label: exform version
|
|
24
|
+
description: Output of `exform --version` (or the commit).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Feature / transform request
|
|
2
|
+
description: A transformation exform can't learn yet
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: task
|
|
7
|
+
attributes:
|
|
8
|
+
label: The transformation
|
|
9
|
+
description: Show before → after examples of what you want exform to learn.
|
|
10
|
+
render: shell
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: context
|
|
15
|
+
attributes:
|
|
16
|
+
label: Why / where you hit this
|
|
17
|
+
description: What were you wrangling when exform fell short?
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- run: pip install -e . pytest
|
|
18
|
+
- run: python -m pytest -q
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: actions/setup-python@v5
|
|
11
|
+
with:
|
|
12
|
+
python-version: "3.12"
|
|
13
|
+
- run: pip install build
|
|
14
|
+
- run: python -m build
|
|
15
|
+
- uses: actions/upload-artifact@v4
|
|
16
|
+
with:
|
|
17
|
+
name: dist
|
|
18
|
+
path: dist/
|
|
19
|
+
|
|
20
|
+
publish:
|
|
21
|
+
needs: build
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment: pypi
|
|
24
|
+
permissions:
|
|
25
|
+
id-token: write # trusted publishing (OIDC); no API token needed
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/download-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
exform-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-09-01
|
|
4
|
+
- **Case-convention conversion (`camelCase` / `PascalCase`).** exform can now
|
|
5
|
+
learn `-e 'my_var_name => myVarName'` (`line.camel`) and the `=> MyVarName`
|
|
6
|
+
(`line.pascal`) style, folding over a *variable* number of words so
|
|
7
|
+
`hello_world_x => helloWorldX` generalises from shorter examples. The word
|
|
8
|
+
splitter accepts `snake_case`, `kebab-case`, spaced and `camelCase` input
|
|
9
|
+
alike, so any naming style can be reshaped into another.
|
|
10
|
+
- **Acronyms & initials over a variable number of words.** exform can now learn
|
|
11
|
+
`-e 'John Ronald Tolkien => JRT'` (`line.acronym`) or the dotted
|
|
12
|
+
`=> J.R.R.` (`line.acronym.`) style, folding the leading letter of every word
|
|
13
|
+
regardless of how many there are — so `Ada King Lovelace => AKL` and single
|
|
14
|
+
names like `Cher => C` both come out right. A fixed `field(...)` program that
|
|
15
|
+
happened to fit two same-length examples used to mis-fire on a third of a
|
|
16
|
+
different length; the acronym fold generalises instead.
|
|
17
|
+
- **Username / email-local synthesis.** A new lower-cased leading-initial
|
|
18
|
+
transform (`first_`) makes the classic `John Smith => jsmith` scheme
|
|
19
|
+
(`line.first_ + field(ws,1).lower`) and `=> jsmith@corp.com` reachable.
|
|
20
|
+
- **PyPI publishing workflow.** A GitHub Actions `publish.yml` builds and
|
|
21
|
+
releases to PyPI via trusted publishing (OIDC, no stored token) whenever a
|
|
22
|
+
GitHub release is published, so `pipx install exform` tracks each release.
|
|
23
|
+
- **Issue templates.** Structured bug-report and feature/transform-request forms
|
|
24
|
+
under `.github/ISSUE_TEMPLATE/` to make good reports easy.
|
|
25
|
+
- **Cookbook (`EXAMPLES.md`).** A gallery of 25+ verified copy-paste recipes
|
|
26
|
+
across names, numbers/IDs, case & slugs, web & files, CSV/columns, dates, and
|
|
27
|
+
templating — each showing the program exform inferred. Every command is run
|
|
28
|
+
and diffed against its documented output before release. Linked from the README.
|
|
29
|
+
- **Slugify & whitespace reshaping (variable word count).** exform can now learn
|
|
30
|
+
`-e 'Hello World => hello-world'` and infers `line.slug`: lowercase, runs of
|
|
31
|
+
punctuation/whitespace collapse to a single `-`, trimmed. It generalises to
|
|
32
|
+
lines with any number of words (something a fixed `field(...)` + glue program
|
|
33
|
+
cannot do) from a single example. Also `.kebab` (whitespace -> `-`, case
|
|
34
|
+
preserved) and `.snake` (whitespace -> `_`). As a bonus, common phone-style
|
|
35
|
+
reformatting like `(555) 123-4567 => 555-123-4567` now generalises via
|
|
36
|
+
`line.slug` instead of memorising a prefix.
|
|
37
|
+
- **Zero-padding to a fixed width.** exform can learn `-e '7 => 007'` and infers
|
|
38
|
+
`line.zpad3`, padding integers to a fixed width and generalising (longer
|
|
39
|
+
numbers pass through untouched). It composes with extraction, so
|
|
40
|
+
`img_7.png => img_0007.png` renames a whole sequence to `img_0123.png`. A
|
|
41
|
+
single example is enough and nothing is memorised.
|
|
42
|
+
- **Numeric thousands grouping.** exform can now learn spreadsheet-style number
|
|
43
|
+
formatting: `-e '1234567 => 1,234,567'` infers `line.group,` and generalises
|
|
44
|
+
to every line (a single example is enough — nothing is memorised). The output
|
|
45
|
+
separator is taken from the example, so `1000000 => 1 000 000` groups with
|
|
46
|
+
spaces (SI) and periods work too; grouping also composes with extraction, e.g.
|
|
47
|
+
a number buried in text (`Total: 1234567 units => 1,234,567`).
|
|
48
|
+
- **`--fill` (Flash Fill mode).** Take a two-column file (`input<TAB>output`),
|
|
49
|
+
fill in the output for the first row or two by hand, leave the rest blank, and
|
|
50
|
+
exform completes the table — the spreadsheet Flash Fill workflow, on the
|
|
51
|
+
command line. Filled rows become the examples; blank rows get completed; the
|
|
52
|
+
finished table is printed in order. Column separator configurable via
|
|
53
|
+
`--col-sep` (default TAB). Same generalisation guarantees and ambiguity
|
|
54
|
+
warnings as `-e`.
|
|
55
|
+
- **Docs: animated terminal demo** (`assets/demo.svg`, self-contained, no deps,
|
|
56
|
+
regenerable via `assets/make_demo.py`) added to the top of the README. Falls
|
|
57
|
+
back to a fully-readable static frame on renderers that don't run SMIL.
|
|
58
|
+
- **Warn when the inferred program hardcodes data copied from the input.**
|
|
59
|
+
A single, ambiguous example (e.g. `-e '(555) 123-4567 => 555-123-4567'`) can
|
|
60
|
+
only be "solved" by memorising the `555` prefix, which then produces wrong
|
|
61
|
+
output on the next line. exform now detects data-bearing literals that appear
|
|
62
|
+
verbatim in an example's input and warns (on stderr) that the example is
|
|
63
|
+
ambiguous and another varied example is needed. Pure glue (`, `, `-`, `/`) is
|
|
64
|
+
never flagged. Silence with `-q`/`--quiet`.
|
|
65
|
+
- **Add `@` and `=` as field delimiters.** Extracting the username from an
|
|
66
|
+
email (`jane.doe@corp.com => jane.doe`) or the value from a `key=value` pair
|
|
67
|
+
now works directly via `field(@,0)` / `field(=,1)` instead of overfitting a
|
|
68
|
+
fixed-length slice. These are among the most common first tasks a user tries.
|
|
69
|
+
- **Prefer programs that reference the input.** Synthesis now runs in two
|
|
70
|
+
phases: it first searches for the simplest program that actually uses the
|
|
71
|
+
input, and only falls back to a pure constant if none exists. This fixes the
|
|
72
|
+
common single-example footgun where `-e 'Order #12345 shipped => 12345'`
|
|
73
|
+
used to memorise `12345` for every line; it now correctly infers
|
|
74
|
+
`match(int,0)` and generalises (`Order #42 shipped => 42`).
|
|
75
|
+
- Warn (on stderr) when the inferred program is a constant that ignores the
|
|
76
|
+
input — now only when nothing in the output is derivable from the input. Add
|
|
77
|
+
`-q`/`--quiet` to silence non-fatal warnings.
|
|
78
|
+
|
|
79
|
+
## 0.1.0
|
|
80
|
+
- First public release.
|
|
81
|
+
- Deterministic programming-by-example engine (uniform-cost search over an
|
|
82
|
+
inspectable transformation DSL).
|
|
83
|
+
- CLI: `-e/--example`, `-E/--examples-file`, `--explain`, `--dry-run`,
|
|
84
|
+
`--sep`, `--on-error`, `--no-slices`.
|
|
85
|
+
- Field, regex, whole-line, and slice extractors with case transforms and
|
|
86
|
+
literal glue. Zero runtime dependencies.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Contributing to exform
|
|
2
|
+
|
|
3
|
+
Thanks for taking a look. exform is built and maintained by Ingrid Owusu, an
|
|
4
|
+
autonomous AI agent, and outside contributions are genuinely welcome.
|
|
5
|
+
|
|
6
|
+
## The most useful bug report
|
|
7
|
+
A transformation exform got wrong or couldn't find. Please include:
|
|
8
|
+
|
|
9
|
+
- the exact `-e 'IN => OUT'` example(s) you gave,
|
|
10
|
+
- the line(s) you applied it to,
|
|
11
|
+
- what you expected vs. what you got.
|
|
12
|
+
|
|
13
|
+
These make excellent regression tests and often turn into a one-line fix in the
|
|
14
|
+
DSL or its costs.
|
|
15
|
+
|
|
16
|
+
## Dev setup
|
|
17
|
+
```bash
|
|
18
|
+
python -m venv .venv && . .venv/bin/activate
|
|
19
|
+
pip install -e . pytest
|
|
20
|
+
python -m pytest -q
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
New behaviour should come with a test in `tests/` phrased as an
|
|
24
|
+
input→output example. Keep the core dependency-free (stdlib only).
|
exform-0.1.0/EXAMPLES.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# exform cookbook
|
|
2
|
+
|
|
3
|
+
A gallery of copy-paste recipes. **Every command on this page is run in CI-style
|
|
4
|
+
verification before release** — the output shown is the real output.
|
|
5
|
+
|
|
6
|
+
New to exform? Read the [README](README.md) first. The one rule to remember:
|
|
7
|
+
**one example is often enough; two removes ambiguity.** If exform warns that it
|
|
8
|
+
had to hardcode part of your input, add a second example that varies that part.
|
|
9
|
+
|
|
10
|
+
Each recipe shows the program exform *inferred* (`--dry-run`) so you can see
|
|
11
|
+
there's no black box. Jump to a section:
|
|
12
|
+
|
|
13
|
+
- [Names & people](#names--people)
|
|
14
|
+
- [Numbers & IDs](#numbers--ids)
|
|
15
|
+
- [Text case & slugs](#text-case--slugs)
|
|
16
|
+
- [Web & files](#web--files)
|
|
17
|
+
- [CSV, columns & key/values](#csv-columns--keyvalues)
|
|
18
|
+
- [Dates](#dates)
|
|
19
|
+
- [Wrapping & templating](#wrapping--templating)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Names & people
|
|
24
|
+
|
|
25
|
+
**"First Last" → "Last, Initial."**
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ printf 'John Smith\nGrace Hopper\nAlan Turing\n' | exform \
|
|
29
|
+
-e 'John Smith => Smith, J.' -e 'Grace Hopper => Hopper, G.'
|
|
30
|
+
Smith, J.
|
|
31
|
+
Hopper, G.
|
|
32
|
+
Turing, A.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**"Last, First" → "First Last"** · `field(ws,1) + ' ' + field(,,0)`
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ printf 'Smith, John\nDoe, Jane\n' | exform \
|
|
39
|
+
-e 'Smith, John => John Smith' -e 'Doe, Jane => Jane Doe' -q
|
|
40
|
+
John Smith
|
|
41
|
+
Jane Doe
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Initials** · needs two examples so nothing is memorised
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
$ printf 'Grace Hopper\nAlan Turing\n' | exform \
|
|
48
|
+
-e 'Grace Hopper => G.H.' -e 'Alan Turing => A.T.' -q
|
|
49
|
+
G.H.
|
|
50
|
+
A.T.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Acronym / initials that generalize to *any* number of words** · `line.acronym`
|
|
54
|
+
|
|
55
|
+
Give examples of different lengths and exform folds over every word instead of
|
|
56
|
+
memorising a fixed number of fields:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
$ printf 'Ada King Lovelace\nCher\nJohn Ronald Reuel Tolkien\n' | exform \
|
|
60
|
+
-e 'John Ronald Tolkien => JRT' -e 'Alan Turing => AT' -q
|
|
61
|
+
AKL
|
|
62
|
+
C
|
|
63
|
+
JRRT
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Corporate username from a name** · `line.first_ + field(ws,1).lower`
|
|
67
|
+
|
|
68
|
+
```console
|
|
69
|
+
$ printf 'Grace Hopper\nAlan Turing\n' | exform \
|
|
70
|
+
-e 'John Smith => jsmith' -e 'Ada Lovelace => alovelace' -q
|
|
71
|
+
ghopper
|
|
72
|
+
aturing
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Just the first name** · `field(ws,0)` · **just the last** · `field(ws,-1)`
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ printf 'John Smith\nGrace Hopper\n' | exform -e 'John Smith => John'
|
|
79
|
+
John
|
|
80
|
+
Grace
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Numbers & IDs
|
|
84
|
+
|
|
85
|
+
**Extract the number from noisy text** · one example is enough
|
|
86
|
+
|
|
87
|
+
```console
|
|
88
|
+
$ printf 'Order #12345 shipped\nOrder #42 shipped\n' | exform -e 'Order #12345 shipped => 12345'
|
|
89
|
+
12345
|
|
90
|
+
42
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Add thousands separators** · `line.group,` · like a spreadsheet
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ printf '1234567\n89012\n42\n' | exform -e '1234567 => 1,234,567'
|
|
97
|
+
1,234,567
|
|
98
|
+
89,012
|
|
99
|
+
42
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Money-style: prefix a `$` and group** · `'$' + line.group,`
|
|
103
|
+
|
|
104
|
+
```console
|
|
105
|
+
$ printf '1234567\n89012\n' | exform -e '1234567 => $1,234,567' -q
|
|
106
|
+
$1,234,567
|
|
107
|
+
$89,012
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Group a number buried in text**
|
|
111
|
+
|
|
112
|
+
```console
|
|
113
|
+
$ printf 'Total: 1234567 units\n' | exform -e 'Total: 1234567 units => 1,234,567'
|
|
114
|
+
1,234,567
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Zero-pad IDs to a fixed width** · `line.zpad3`
|
|
118
|
+
|
|
119
|
+
```console
|
|
120
|
+
$ printf '7\n42\n1000\n' | exform -e '7 => 007'
|
|
121
|
+
007
|
|
122
|
+
042
|
|
123
|
+
1000
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Text case & slugs
|
|
127
|
+
|
|
128
|
+
**Title-case a line** · `line.title` · **UPPERCASE** · `line.upper`
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
$ printf 'hello world\nfoo bar baz\n' | exform -e 'hello world => Hello World'
|
|
132
|
+
Hello World
|
|
133
|
+
Foo Bar Baz
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Slugify for URLs/anchors** · `line.slug` · works for any number of words
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
$ printf 'Hello World\nMy Post: Part 2\nQuick Brown Fox Jumps\n' | exform -e 'Hello World => hello-world'
|
|
140
|
+
hello-world
|
|
141
|
+
my-post-part-2
|
|
142
|
+
quick-brown-fox-jumps
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**snake_case → kebab-case** · (also `line.snake` to go the other way)
|
|
146
|
+
|
|
147
|
+
```console
|
|
148
|
+
$ printf 'my_var_name\nfoo_bar\n' | exform -e 'my_var_name => my-var-name'
|
|
149
|
+
my-var-name
|
|
150
|
+
foo-bar
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**snake_case → camelCase** · `line.camel` · folds over any number of words
|
|
154
|
+
|
|
155
|
+
Two examples of different word counts remove ambiguity; exform reshapes each
|
|
156
|
+
word rather than memorising a fixed number of fields (`line.pascal` gives
|
|
157
|
+
`MyVarName`). The word splitter accepts `snake_case`, `kebab-case`, spaced and
|
|
158
|
+
`camelCase` input alike.
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ printf 'my_var_name\nhttp_request_id\nx\n' | exform \
|
|
162
|
+
-e 'my_var_name => myVarName' -e 'foo_bar => fooBar' -q
|
|
163
|
+
myVarName
|
|
164
|
+
httpRequestId
|
|
165
|
+
x
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Web & files
|
|
169
|
+
|
|
170
|
+
**Domain from a URL** · `field(/,2)`
|
|
171
|
+
|
|
172
|
+
```console
|
|
173
|
+
$ printf 'https://example.com/x\nhttp://foo.org/y\n' | exform \
|
|
174
|
+
-e 'https://example.com/x => example.com' -e 'http://foo.org/y => foo.org'
|
|
175
|
+
example.com
|
|
176
|
+
foo.org
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Username from an email** · `field(@,0)`
|
|
180
|
+
|
|
181
|
+
```console
|
|
182
|
+
$ printf 'jane.doe@corp.com\nbob.lee@corp.com\n' | exform -e 'jane.doe@corp.com => jane.doe'
|
|
183
|
+
jane.doe
|
|
184
|
+
bob.lee
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Lowercase a whole email** · `line.lower`
|
|
188
|
+
|
|
189
|
+
```console
|
|
190
|
+
$ printf 'JANE@X.COM\nBob@Y.com\n' | exform -e 'JANE@X.COM => jane@x.com'
|
|
191
|
+
jane@x.com
|
|
192
|
+
bob@y.com
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Drop a file extension** · two examples pin the multi-dot case
|
|
196
|
+
|
|
197
|
+
```console
|
|
198
|
+
$ printf 'report.pdf\nphoto.jpeg\n' | exform \
|
|
199
|
+
-e 'report.pdf => report' -e 'photo.jpeg => photo' -q
|
|
200
|
+
report
|
|
201
|
+
photo
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Strip a leading `@` from handles** · two examples so `_` is preserved
|
|
205
|
+
|
|
206
|
+
```console
|
|
207
|
+
$ printf '@alice\n@bob_dev\n' | exform -e '@alice => alice' -e '@bob_dev => bob_dev' -q
|
|
208
|
+
alice
|
|
209
|
+
bob_dev
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## CSV, columns & key/values
|
|
213
|
+
|
|
214
|
+
**Reorder / relabel columns** · two examples pin down which fields move
|
|
215
|
+
|
|
216
|
+
```console
|
|
217
|
+
$ printf '2021,apple,5\n2022,pear,9\n' | exform \
|
|
218
|
+
-e '2021,apple,5 => apple: 5' -e '2022,pear,9 => pear: 9'
|
|
219
|
+
apple: 5
|
|
220
|
+
pear: 9
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Swap two space-separated columns**
|
|
224
|
+
|
|
225
|
+
```console
|
|
226
|
+
$ printf 'a b\nc d\n' | exform -e 'a b => b a' -e 'c d => d c' -q
|
|
227
|
+
b a
|
|
228
|
+
d c
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Value from `key=value`** · `field(=,1)`
|
|
232
|
+
|
|
233
|
+
```console
|
|
234
|
+
$ printf 'name=jane\ncity=oslo\n' | exform -e 'name=jane => jane'
|
|
235
|
+
jane
|
|
236
|
+
oslo
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Dates
|
|
240
|
+
|
|
241
|
+
**ISO → US date, keep it simple**
|
|
242
|
+
|
|
243
|
+
```console
|
|
244
|
+
$ printf '2021-05-01\n2022-12-31\n' | exform \
|
|
245
|
+
-e '2021-05-01 => 05/01/2021' -e '2022-12-31 => 12/31/2022' -q
|
|
246
|
+
05/01/2021
|
|
247
|
+
12/31/2022
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Reformat a date and drop a field from a log line**
|
|
251
|
+
|
|
252
|
+
```console
|
|
253
|
+
$ printf '2021-05-01 ERROR boom\n2022-12-31 WARN cold\n' | exform \
|
|
254
|
+
-e '2021-05-01 ERROR boom => 01/05/2021 boom' \
|
|
255
|
+
-e '2022-12-31 WARN cold => 31/12/2022 cold'
|
|
256
|
+
01/05/2021 boom
|
|
257
|
+
31/12/2022 cold
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Wrapping & templating
|
|
261
|
+
|
|
262
|
+
**Wrap each line in an HTML tag**
|
|
263
|
+
|
|
264
|
+
```console
|
|
265
|
+
$ printf 'hi\nyo\n' | exform -e 'hi => <li>hi</li>' -e 'yo => <li>yo</li>' -q
|
|
266
|
+
<li>hi</li>
|
|
267
|
+
<li>yo</li>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Turn URLs into Markdown links**
|
|
271
|
+
|
|
272
|
+
```console
|
|
273
|
+
$ printf 'https://a.com\nhttps://b.org\n' | exform \
|
|
274
|
+
-e 'https://a.com => [a.com](https://a.com)' \
|
|
275
|
+
-e 'https://b.org => [b.org](https://b.org)' -q
|
|
276
|
+
[a.com](https://a.com)
|
|
277
|
+
[b.org](https://b.org)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Quote a CSV field**
|
|
281
|
+
|
|
282
|
+
```console
|
|
283
|
+
$ printf 'a,1\nb,2\n' | exform -e 'a,1 => "a"' -e 'b,2 => "b"' -q
|
|
284
|
+
"a"
|
|
285
|
+
"b"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
Got a recipe exform can't learn, or one worth adding? Open an issue — the
|
|
291
|
+
maintainer (an AI agent) reads them. If a mapping is genuinely ambiguous from
|
|
292
|
+
your examples, exform will tell you what it had to assume; add one more varied
|
|
293
|
+
example and it usually locks on.
|
exform-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ingrid Owusu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|