natsort_port · binary

One binary,
six ways to ask it
to sort.

The compiled Rust binary is both the product and the test subject — every differential harness in this project drives it through these same six subcommands.

Human-facing commands
sort [flags] Read lines from stdin, print them naturally sorted
printf 'file10\nfile2\nfile1\n' | natsort_port sort
stdinOne item per line
stdoutfile1, file2, file10 — one per line
compare A B [flags] Print -1 / 0 / 1, like a comparator function
natsort_port compare num2 num10
stdout-1 (num2 sorts before num10)
key STRING [flags] Print the parsed natsort key — how a string splits into chunks
natsort_port key "a-5.034e2"
stdoutT:a- I:5 T:. I:34 T:e I:2

Each token is T:ext, I:nteger, or R:eal (float) — tab-separated so a caller can rebuild the tuple exactly (this is how the differential harnesses compare keys byte-for-byte against Python's natsort_keygen()).

os-sort Sort stdin the way a file manager would (case-insensitive natural order)
printf 'Banana\napple\nCherry\n' | natsort_port os-sort
stdoutapple, Banana, Cherry
Machine-facing commands

Used internally by the Python test harnesses — batch mode avoids spawning one process per comparison.

batch-sort Sort many lists in one process invocation
printf 'real\t1.10\t1.2\n' | natsort_port batch-sort
stdinOne line per list: flagspec item1 item2 ...
stdoutOne sorted, tab-joined line per input line

This is what fuzz_harness.py, fuzz_60s.py, and the adapter's natsorted() shim call — thousands of comparisons without the ~1ms process-spawn cost each.

bench N Sort N generated filenames in-process, print elapsed microseconds
natsort_port bench 50000
stdoutA single number: microseconds elapsed for the sort itself (excludes process startup)

Called by bench.py, which wraps it with /usr/bin/time -v for peak RSS and computes p50/p95/p99 across multiple runs.

Algorithm flags

Appended to sort, compare, or key. Omit for the default (INT).

--realSigned floats: "1.10" parses as 1.1, not text-then-10
--signedA leading +/- attaches to the following number
--floatUnsigned floats
--ignorecaseFold case before comparing (uses casefold, not lowercase)
--lowercasefirstSwap the case of every letter before comparing
--grouplettersExpand each letter to casefold(c) + c
--noexpUnder REAL/FLOAT, don't treat e/E + digits as an exponent -- "1e5" stays 1.0, "e", 5.0
--presortPre-sort lexicographically first, so ties in the natural key break by string order, not input order

batch-sort takes the same flags as comma-separated words in the line's first field instead: real,ignorecase.

How it's wired together
> Python calls this binary, never the reverse
> adapter/natsort/__init__.py — Python package, same names as real natsort
>  every function shells out to natsort_port via subprocess
>  lets natsort's own, unmodified test files import "natsort" and hit Rust
>
> fuzz_harness.py / fuzz_60s.py — random strings → both sides → diff
> cli_difftest.py — same idea, but through the CLI surface directly
> bench.py — timing + memory, Python vs Rust, same workload