A gallery of copy-paste recipes. Every command on this page is run in CI-style verification before release — the output shown is the real output.
New to exform? Read the README first. The one rule to remember: one example is often enough; two removes ambiguity. If exform warns that it had to hardcode part of your input, add a second example that varies that part.
Each recipe shows the program exform inferred (--dry-run) so you can see
there's no black box. Jump to a section:
- Names & people
- Numbers & IDs
- Text case & slugs
- Web & files
- CSV, columns & key/values
- Dates
- Wrapping & templating
- JSON lines — "jq by example"
- Rename files by example
"First Last" → "Last, Initial."
$ printf 'John Smith\nGrace Hopper\nAlan Turing\n' | exform \
-e 'John Smith => Smith, J.' -e 'Grace Hopper => Hopper, G.'
Smith, J.
Hopper, G.
Turing, A."Last, First" → "First Last" · field(ws,1) + ' ' + field(,,0)
$ printf 'Smith, John\nDoe, Jane\n' | exform \
-e 'Smith, John => John Smith' -e 'Doe, Jane => Jane Doe' -q
John Smith
Jane DoeInitials · needs two examples so nothing is memorised
$ printf 'Grace Hopper\nAlan Turing\n' | exform \
-e 'Grace Hopper => G.H.' -e 'Alan Turing => A.T.' -q
G.H.
A.T.Acronym / initials that generalize to any number of words · line.acronym
Give examples of different lengths and exform folds over every word instead of memorising a fixed number of fields:
$ printf 'Ada King Lovelace\nCher\nJohn Ronald Reuel Tolkien\n' | exform \
-e 'John Ronald Tolkien => JRT' -e 'Alan Turing => AT' -q
AKL
C
JRRTCorporate username from a name · line.first_ + field(ws,1).lower
$ printf 'Grace Hopper\nAlan Turing\n' | exform \
-e 'John Smith => jsmith' -e 'Ada Lovelace => alovelace' -q
ghopper
aturingJust the first name · field(ws,0) · just the last · field(ws,-1)
$ printf 'John Smith\nGrace Hopper\n' | exform -e 'John Smith => John'
John
GraceExtract the number from noisy text · one example is enough
$ printf 'Order #12345 shipped\nOrder #42 shipped\n' | exform -e 'Order #12345 shipped => 12345'
12345
42Add thousands separators · line.group, · like a spreadsheet
$ printf '1234567\n89012\n42\n' | exform -e '1234567 => 1,234,567'
1,234,567
89,012
42Money-style: prefix a $ and group · '$' + line.group,
$ printf '1234567\n89012\n' | exform -e '1234567 => $1,234,567' -q
$1,234,567
$89,012Group a number buried in text
$ printf 'Total: 1234567 units\n' | exform -e 'Total: 1234567 units => 1,234,567'
1,234,567Zero-pad IDs to a fixed width · line.zpad3
$ printf '7\n42\n1000\n' | exform -e '7 => 007'
007
042
1000Title-case a line · line.title · UPPERCASE · line.upper
$ printf 'hello world\nfoo bar baz\n' | exform -e 'hello world => Hello World'
Hello World
Foo Bar BazSlugify for URLs/anchors · line.slug · works for any number of words
$ printf 'Hello World\nMy Post: Part 2\nQuick Brown Fox Jumps\n' | exform -e 'Hello World => hello-world'
hello-world
my-post-part-2
quick-brown-fox-jumpssnake_case → kebab-case · (also line.snake to go the other way)
$ printf 'my_var_name\nfoo_bar\n' | exform -e 'my_var_name => my-var-name'
my-var-name
foo-barsnake_case → camelCase · line.camel · folds over any number of words
Two examples of different word counts remove ambiguity; exform reshapes each
word rather than memorising a fixed number of fields (line.pascal gives
MyVarName). The word splitter accepts snake_case, kebab-case, spaced and
camelCase input alike.
$ printf 'my_var_name\nhttp_request_id\nx\n' | exform \
-e 'my_var_name => myVarName' -e 'foo_bar => fooBar' -q
myVarName
httpRequestId
xcamelCase → snake_case · line.snakecase · the reverse direction, splits on case boundaries
The same word splitter runs backwards: camelCase / PascalCase / ACRONYM
input folds to lower-cased snake_case (line.snakecase), kebab-case
(line.kebabcase), or spaced Title Case (line.titlecase).
$ printf 'firstName\nuserId\ngetHTTPResponse\n' | exform \
-e 'myVariableName => my_variable_name' -e 'firstName => first_name' -q
first_name
user_id
get_http_responseDomain from a URL · field(/,2)
$ printf 'https://example.com/x\nhttp://foo.org/y\n' | exform \
-e 'https://example.com/x => example.com' -e 'http://foo.org/y => foo.org'
example.com
foo.orgUsername from an email · field(@,0)
$ printf 'jane.doe@corp.com\nbob.lee@corp.com\n' | exform -e 'jane.doe@corp.com => jane.doe'
jane.doe
bob.leeLowercase a whole email · line.lower
$ printf 'JANE@X.COM\nBob@Y.com\n' | exform -e 'JANE@X.COM => jane@x.com'
jane@x.com
bob@y.comDrop a file extension · two examples pin the multi-dot case
$ printf 'report.pdf\nphoto.jpeg\n' | exform \
-e 'report.pdf => report' -e 'photo.jpeg => photo' -q
report
photoStrip a leading @ from handles · two examples so _ is preserved
$ printf '@alice\n@bob_dev\n' | exform -e '@alice => alice' -e '@bob_dev => bob_dev' -q
alice
bob_devReorder / relabel columns · two examples pin down which fields move
$ printf '2021,apple,5\n2022,pear,9\n' | exform \
-e '2021,apple,5 => apple: 5' -e '2022,pear,9 => pear: 9'
apple: 5
pear: 9Swap two space-separated columns
$ printf 'a b\nc d\n' | exform -e 'a b => b a' -e 'c d => d c' -q
b a
d cValue from key=value · field(=,1)
$ printf 'name=jane\ncity=oslo\n' | exform -e 'name=jane => jane'
jane
osloISO → US date, keep it simple
$ printf '2021-05-01\n2022-12-31\n' | exform \
-e '2021-05-01 => 05/01/2021' -e '2022-12-31 => 12/31/2022' -q
05/01/2021
12/31/2022Reformat a date and drop a field from a log line
$ printf '2021-05-01 ERROR boom\n2022-12-31 WARN cold\n' | exform \
-e '2021-05-01 ERROR boom => 01/05/2021 boom' \
-e '2022-12-31 WARN cold => 31/12/2022 cold'
01/05/2021 boom
31/12/2022 coldWrap each line in an HTML tag
$ printf 'hi\nyo\n' | exform -e 'hi => <li>hi</li>' -e 'yo => <li>yo</li>' -q
<li>hi</li>
<li>yo</li>Turn URLs into Markdown links
$ printf 'https://a.com\nhttps://b.org\n' | exform \
-e 'https://a.com => [a.com](https://a.com)' \
-e 'https://b.org => [b.org](https://b.org)' -q
[a.com](https://a.com)
[b.org](https://b.org)Quote a CSV field
$ printf 'a,1\nb,2\n' | exform -e 'a,1 => "a"' -e 'b,2 => "b"' -q
"a"
"b"These leave the rest of each line untouched, like sed but by example.
Reformat a date inside a line
$ printf 'commit 2021-03-05 ok\npush 1999-12-31 x\n' \
| exform --in-line -e 'commit 2021-03-05 ok => commit 2021/03/05 ok'
commit 2021/03/05 ok
push 1999/12/31 xUppercase a keyed value, nothing else
$ printf 'level=info a\nlevel=warn b\n' \
| exform --in-line -e 'level=info a => level=INFO a' -e 'level=warn b => level=WARN b'
level=INFO a
level=WARN bWrap a word in brackets
$ printf 'see foo here\nsee bar there\n' \
| exform --in-line -e 'see foo here => see <foo> here' -e 'see bar there => see <bar> there'
see <foo> here
see <bar> thereRewrite every match on a line (--all)
$ printf 'a 1 2 3 b\n' | exform --in-line --all -e 'a 1 b => a [1] b'
a [1] [2] [3] bReshape one column of a CSV/TSV (--field)
$ printf 'id,date,amt\n1,2021-05-01,100\n2,2022-12-31,250\n' \
| exform --field 2 -e '2021-05-01 => 05/01/2021' -e '2022-12-31 => 12/31/2022'
id,date,amt
1,05/01/2021,100
2,12/31/2022,250Only column 2 changes; the other columns (and the header) are left exactly as
they were. Use --field-sep $'\t' for TSV, and --field 2,4 to reshape several
columns at once.
$ printf 'name\tcity\nJOHN\tnyc\nJANE\tla\n' \
| exform --field 1 --field-sep $'\t' -e 'JOHN => John' -e 'JANE => Jane'
name city
John nyc
Jane laEmit a standalone, dependency-free script (--emit python)
$ exform -e 'my_var_name => myVarName' -e 'home_address => homeAddress' --emit python > camelize.py
$ printf 'deep_nested_key\n' | python3 camelize.py
deepNestedKeyThe generated camelize.py has no dependency on exform — commit it and run it
anywhere Python is available. exform verifies the script reproduces your
examples before printing it.
Emit a portable awk one-liner (--emit awk) · for the syntax nobody remembers
$ exform -e '2021,apple => apple,2021' -e '2022,pear => pear,2022' --emit awk
# text transform generated by exform (https://github.com/ingrid-owusu/exform)
# inferred program: field(,,1) + ',' + field(,,0)
# reads stdin, writes stdout; lines missing an expected field are printed best-effort
awk -F, '{ print $2 "," $1 }'Paste the last line into any pipeline — no exform, no Python, no install needed:
$ printf 'apple,2021\npear,2022\n' | awk -F, '{ print $2 "," $1 }'
2021,apple
2022,pearOther transforms map to the awk idiom you'd have had to look up — last field, uppercase, zero-pad:
$ exform -e 'a/b/c => c' -e 'x/y/z => z' --emit awk | tail -1
awk -F/ '{ print $NF }'
$ exform -e 'hello => HELLO' -e 'world => WORLD' --emit awk | tail -1
awk '{ print toupper($0) }'exform runs the generated awk against your examples and refuses to print it if
the output disagrees — so a one-liner it hands you is one it has checked. When
a transform can't be expressed faithfully in awk (e.g. regex extraction or
word-case folding), it says so; use --emit python for those.
Reshape one string field of each JSON object by example and leave every other key, nested value and type exactly as it was — no jq syntax to learn.
Fix a name field
$ printf '{"user": "Smith, John", "age": 30}\n{"user": "Doe, Jane", "age": 25}\n' \
| exform --json-field user -e 'Smith, John => John Smith' -e 'Doe, Jane => Jane Doe'
{"user": "John Smith", "age": 30}
{"user": "Jane Doe", "age": 25}The age field and the object structure are untouched — only the targeted
string changes.
Reach into a nested field with a dotted path · two examples generalise to unseen records
$ printf '{"u": {"name": "Smith, John"}}\n{"u": {"name": "Doe, Jane"}}\n{"u": {"name": "Turing, Alan"}}\n' \
| exform --json-field u.name -e 'Smith, John => John Smith' -e 'Doe, Jane => Jane Doe'
{"u": {"name": "John Smith"}}
{"u": {"name": "Jane Doe"}}
{"u": {"name": "Alan Turing"}}The third record was never given as an example — exform inferred the rule and
applied it. Records missing the key (or where the value isn't a string) pass
through unchanged; use --on-error to control genuinely untransformable values.
Pipe exform a list of paths and a couple of old => new examples; it renames the
batch. Dry-run by default — add --apply to actually rename. Only the
basename is transformed, so the directory is preserved.
Strip a camera prefix and lower-case the extension · two examples generalise to unseen files
$ ls
DSC_1001.JPG DSC_1002.JPG DSC_1099.JPG
$ ls | exform --rename -e 'DSC_1001.JPG => 1001.jpg' -e 'DSC_1002.JPG => 1002.jpg'
DSC_1001.JPG -> 1001.jpg
DSC_1002.JPG -> 1002.jpg
DSC_1099.JPG -> 1099.jpg
exform: 3 files would be renamed (dry-run). Re-run with --apply to perform the renames.
$ ls | exform --rename -e 'DSC_1001.JPG => 1001.jpg' -e 'DSC_1002.JPG => 1002.jpg' --apply
exform: renamed 3 files.DSC_1099.JPG was never given as an example — exform inferred the rule and
applied it. If two files would map to the same name, or a target already exists,
exform refuses the whole batch without touching anything. Combine with find to
rename across a tree: find . -name '*.txt' | exform --rename -e '...' --apply.
Got a recipe exform can't learn, or one worth adding? Open an issue — the maintainer (an AI agent) reads them. If a mapping is genuinely ambiguous from your examples, exform will tell you what it had to assume; add one more varied example and it usually locks on.