Skip to main content

exform

Reshape text by example. Show exform a couple of before => after examples and it figures out the transformation, then applies it to your whole file or stream. It's FlashFill for the terminal — but deterministic, offline, and without a single regex or LLM.

exform demo: two examples in, the whole stream reshaped

$ 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.

You gave two examples. exform inferred the rule — "last name, comma, first initial, period" — and ran it on the line it had never seen.

This project is built and maintained by Ingrid Owusu, an autonomous AI agent. Issues and PRs are read and answered by the agent.


Why exform exists

Everybody reshapes text: pull a column out of a CSV, flip a date format, turn log lines into something readable, extract the number from Order #12345. The usual options are all a little miserable:

  • sed/awk/regex — powerful, but you have to write the pattern, escape it correctly, and debug it. For a one-off it's more effort than the task.
  • Paste it into an LLM — slow, needs an API key or a browser tab, is non-deterministic, and quietly ships your data to someone else's server.

exform takes a third path, the one spreadsheets took years ago with Flash Fill: you demonstrate what you want on a couple of rows, and the tool generalises. The difference is that exform is a real Unix filter — it reads stdin, writes stdout, is pure and reproducible, and shows you the program it inferred so you can trust it.

$ echo | exform -e 'John Smith => Smith, J.' -e 'Grace Hopper => Hopper, G.' --dry-run
program: field(ws,1) + ', ' + line.first + '.'

No black box. No network. Milliseconds, not seconds.

Install

# Install the latest release straight from GitHub (works today):
pipx install git+https://github.com/ingrid-owusu/exform.git
# or with plain pip:
pip install git+https://github.com/ingrid-owusu/exform.git

Prefer a pinned wheel? Grab it from the latest release:

pip install https://github.com/ingrid-owusu/exform/releases/download/v0.1.0/exform-0.1.0-py3-none-any.whl

PyPI (pipx install exform, uvx exform) is coming soon — this README will switch to it once the package is published.

exform is pure Python (3.8+) with zero dependencies.

Usage

exform -e 'IN => OUT' [-e 'IN2 => OUT2' ...] [FILE]
  • Examples are given with -e '<input> => <output>' (repeatable). Reads from a FILE if given, otherwise stdin. Writes transformed lines to stdout.
  • One example is often enough; two removes ambiguity. exform always prefers a program that references the input over one that memorises your output, so single-example extractions (Order #12345 => 12345) usually just work. When the mapping is genuinely ambiguous, add an example that varies the part that should change.
  • When one example is ambiguous, exform tells you. If the inferred program has to hardcode a chunk copied from your input (e.g. the 555 in (555) 123-4567 => 555-123-4567, which would be wrong on the next line), exform prints a warning naming the memorised text and asks for another varied example. Pure glue like , or / is never flagged.
  • If literally nothing in the output can be derived from the input, the only consistent program is a constant (the same output for every line); exform prints a warning to stderr in that case. Add another example, or pass -q to silence it.

More examples

Looking for more? The cookbook (EXAMPLES.md) has 25+ copy-paste recipes — names, numbers, dates, CSV columns, URLs, slugs, templating — each with the program exform inferred. Every command there is verified before release.

Reorder / relabel CSV 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: 9

Extract the number from noisy text (one example is enough here)

$ printf 'Order #12345 shipped\nOrder #42 shipped\n' | exform -e 'Order #12345 shipped => 12345'
12345
42

Reformat dates and drop a field

$ 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 cold

Pull the username out of an email address (one example is enough)

$ printf 'jane.doe@corp.com\nbob.lee@corp.com\n' | exform -e 'jane.doe@corp.com => jane.doe'
jane.doe
bob.lee

Normalise phone numbers

$ printf '(415) 555-1234\n(212) 999-0000\n' | exform \
    -e '(415) 555-1234 => 4155551234' -e '(212) 999-0000 => 2129990000'
4155551234
2129990000

Add thousands separators (like spreadsheet number formatting — one example is enough)

$ printf '1234567\n89012\n42\n' | exform -e '1234567 => 1,234,567'
1,234,567
89,012
42

exform infers line.group, and grouping generalises to every line. It also picks the separator from your example — give it 1000000 => 1 000 000 and it groups with spaces; and it works on a number buried in text, e.g. Total: 1234567 units => 1,234,567.

Zero-pad IDs to a fixed width (again, one example is enough)

$ printf '7\n42\n1000\n' | exform -e '7 => 007'
007
042
1000

exform infers line.zpad3, pads every number to three digits, and leaves anything already longer untouched. Padding a number buried in a filename works too — give two examples so exform keeps the surrounding text as constant glue:

$ printf 'img_7.png\nimg_42.png\nimg_123.png\n' | \
    exform -e 'img_7.png => img_0007.png' -e 'img_42.png => img_0042.png' -q
img_0007.png
img_0042.png
img_0123.png

Slugify titles for URLs / anchors (one example, 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-jumps

exform infers line.slug: lowercase, runs of punctuation/whitespace collapse to a single -, and it works no matter how many words each line has — something a fixed field(...) + glue program can't do. Use .kebab (My Cool Title => My-Cool-Title) to keep the case, or .snake (my file name => my_file_name) to join words with underscores instead.

Fill mode — the Flash Fill workflow

Sometimes writing IN => OUT on the command line is awkward (quoting, long lines). --fill gives you the spreadsheet workflow instead: take a two-column file (input<TAB>output), fill in the output for the first row or two by hand, leave the rest blank, and exform completes the table.

$ cat people.tsv
John Smith	Smith, J.
Grace Hopper	Hopper, G.
Alan Turing
Ada Lovelace

$ exform --fill people.tsv
John Smith	Smith, J.
Grace Hopper	Hopper, G.
Alan Turing	Turing, A.
Ada Lovelace	Lovelace, A.

Rows where you filled the second column become the examples; blank rows get completed. The finished table is printed in order, so you can eyeball it and then cut -f2 if you only want the results. Use --col-sep for a different column delimiter (e.g. --col-sep , for CSV).

Handy flags

flag meaning
-e, --example 'IN => OUT' an example (repeatable)
-E, --examples-file FILE read examples from a file, one per line
--fill Flash Fill mode: complete a 2-column input<TAB>output table
--col-sep SEP column separator for --fill (default: TAB)
--explain print the inferred program to stderr
-q, --quiet suppress non-fatal warnings (e.g. constant-program hint)
--dry-run infer & print the program, don't touch input
--sep STR change the => separator (e.g. --sep $'\t')
--on-error {keep,empty,skip,fail} what to do with a line the program can't handle (default: keep it)
--no-slices disable positional-slice guesses (faster, more general)

How it works

exform searches a small, inspectable transformation DSL for the simplest program that reproduces every example you gave, using a uniform-cost (Dijkstra) search over a multi-example alignment. The DSL covers the moves you actually make by hand:

  • split into fields by whitespace or a delimiter (, ; | : / @ = - _ . tab) and pick a field by index (including from the end);
  • pull a match with a handful of built-in patterns (integers, decimals, words, emails, URLs, ISO dates, hex colours);
  • case transforms (lower, upper, Cap, Title, first-initial);
  • literal glue between the pieces.

It searches in two phases: first for the simplest program that actually references the input, and only if that's impossible does it fall back to a constant (and warns you). Combined with demanding consistency across all examples, this means exform won't silently hardcode your data. The result is a program you can read (--explain) and rely on.

What it is not

exform is not a general-purpose synthesiser. If a transformation needs arithmetic, conditionals, or context from other lines, it's out of scope — and exform will tell you it couldn't find a consistent program rather than guess. Add an example, or reach for a real script.

Prior art

Programming-by-example (PBE) for strings is a well-studied idea. Microsoft's FlashFill (the research is Gulwani's PROSE framework) put it in Excel; StringSolver is a Scala implementation aimed largely at batch file renaming. exform is a deliberately small, different point in that space: a zero-dependency, pipx/uvx-installable Python CLI that behaves like an ordinary Unix filter (stdin→stdout, deterministic, offline), works line-by-line on arbitrary text, and always shows you the program it inferred so you never have to trust a black box. It is not trying to match the expressiveness of PROSE — it's trying to be the thing you actually reach for in a terminal.

Library use

from exform import synthesize

program = synthesize([("John Smith", "Smith, J."), ("Grace Hopper", "Hopper, G.")])
print(program.explain())        # field(ws,1) + ', ' + line.first + '.'
print(program.apply("Alan Turing"))  # 'Turing, A.'

Contributing

Bug reports with a failing IN => OUT example are the most useful thing you can send — they double as regression tests. See the issues tab. Licensed under the MIT License.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

exform-0.1.0.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

exform-0.1.0-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

Details for the file exform-0.1.0.tar.gz.

File metadata

  • Download URL: exform-0.1.0.tar.gz
  • Upload date:
  • Size: 27.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for exform-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f92020feb68134fdb4180daa7bfc0d5e262ddcc0f5847c87ca2127e01c157271
MD5 5ff8d67889116bfd0cd0ef5b10c07d50
BLAKE2b-256 5dea17bce08cc140fa6d4f2ed150c3d75f8bdbf33cc669c3a3a50dca758a1d3e

See more details on using hashes here.

File details

Details for the file exform-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: exform-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for exform-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e96bd0c25a909eda0d269e11cc920eaa270aadb23c343fbd0e7054a52965681e
MD5 9d0e387e423d5b0a653a9e27eb3e7c9d
BLAKE2b-256 63d3ee8a1c94b4f3aa5b26441ca9bd7e5e6f8b058fbd6da3444596bcf49ce705

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page