Open-source project
theryangeary/choose avatar
theryangeary/choose

choose: Python-style field slices for cut and awk pipelines

A human-friendly and fast alternative to cut (and sometimes awk)

2,287 stars46 forksRustGPL-3.0

At a glance

What is it?
choose rewrites field selection as Python-style slices, with zero-based indexes, negative offsets and Rust regex separators, in place of cut's comma lists and awk's quoted program text. It stays a slicer on purpose, and the help output in its README is two minor versions behind the crate it ships.
Who is it for?
Adopt choose for one-line field extraction in scripts and shell history, where zero-based slices and negative offsets buy more than awk's programmability, and pass on it where a pipeline needs aggregation, pattern branching, byte offsets or empty fields kept intact.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 134 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The grammar behind every choose invocation

One argument list, `<choices>...`, carries the whole selection, and every element of it is a field spec rather than a plain field number. The help output gives the grammar as "Either a, a:b, a..b, or a..=b, where a and b are integers". Counting starts at zero, so `choose 5` prints the sixth item on a line, and either end of a range may be dropped: an omitted start reaches the beginning of the line, an omitted end runs to the end of it. Nine worked examples cover those shapes.

The two range forms are not interchangeable. According to the help text, a:b is inclusive of b unless -x overrides it, a..b is exclusive of b, and a..=b is inclusive again, so the colon is already the closed range and the dot forms exist for the half-open range you would write in Python or Rust. Passing -x, --exclusive opens only the colon form, which is why the two lines below differ by one item:

bash
choose 2:5              # print everything from the 2nd to 5th item on the line,
                        # inclusive of the 5th

choose -x 2:5           # print everything from the 2nd to 5th item on the line,
                        # exclusive of the 5th

Indexing backwards works too, and a pair of negative offsets gives you a tail without counting fields first:

bash
choose -1               # print the last item from a line

choose -3:-1            # print the last three items from a line

Several specs sit side by side in one command, which is how a comma-separated field list gets typed without commas. A pipe character reaches the program as an ordinary argument instead of acting as a range operator, so the syntax does not fight the shell around it.

Rust regex separators and the non-greedy switch

Without a separator flag, items are split on whitespace. The example comments describe `choose -f ':' 0 3 5` as picking the 0th, 3rd and 5th items "where items are separated by ':' instead of whitespace", so the separator decides what an index counts; change it and the same numbers land somewhere else on the line.

bash
choose -f ':' 0 3 5     # print the 0th, 3rd, and 5th item from a line, where
                        # items are separated by ':' instead of whitespace

A pattern rather than a literal character: character classes and alternation are available where cut accepts a single delimiter, and the help spells out the dialect as Rust regex syntax. Two switches shape the split further: `-o, --output-field-separator` sets what the printed fields get joined with, and `-n, --non-greedy` makes the separator itself non-greedy, which matters for a pattern able to match more than once per gap. What -o falls back to when you leave it out is not stated, and no example shows a non-greedy run, so both get settled by looking at output rather than by reading.

Under the default whitespace separator, the treatment of empty fields is left unstated as well, and nothing on the page covers a line with leading spaces. Four runtime dependencies and nothing else appear in the manifest:

toml
[dependencies]
structopt = "0.3"
regex = "1"
lazy_static = "1"
backslash = "0"

Pattern syntax is attributed to Rust's regex crate. What the `backslash` dependency is for is not described anywhere.

Six flags, three options, and no expression language

Counting what is documented gives 6 flags (-c, -d, -x, -h, -n, -V), 3 options (-f, -i, -o) and 1 argument list. Knowing what is absent from that inventory matters as much as knowing what sits in it: no byte-oriented mode, no list of acceptable delimiters, no complement flag, and no switch that keeps empty fields when whitespace is the separator. `-c, --character-wise` is the sole change of unit on offer, and it moves the count from fields to characters.

Every example reads from standard input, since none of them pass `-i, --input <input>`. When you do want a file, that option takes a path, and nothing further about it is explained. `-d, --debug` is listed as "Activate debug mode" with no sample output anywhere on the page, so what it prints belongs to the short list of things a new user has to discover by running it.

For a pipeline the consequence is blunt: anything needing a computed field, a conditional or a running total stays in awk. choose selects slices, prints them and exits, which earns its place when you are reading one line of process listing output and stops earning it the moment a script grows a second rule.

The help dump in the README is an older build

Copying the flag list out of the README gets you a different program from the one Cargo.toml builds. That usage block opens with choose 1.2.0, while the manifest declares version 1.3.7 and the newest dated build listed for the project is 1.3.7 on 2025-08-26. The last push was on 2026-06-11 and the repository is not archived.

Any flag added between those two builds is simply missing from the page, and nothing there dates the help text or claims it is current. The install section repeats the problem in a different shape: none of the 5 documented paths is pinned to a version. The Cargo route serves whatever crates.io currently holds, so two machines installing months apart can end up with different flag sets and no record of which is which.

One line in the old block still explains the design. `-x, --exclusive` is described as "Use exclusive ranges, similar to array indexing in many programming languages", which is the clearest statement on the page about where the slice syntax comes from.

Package names drift from the binary name

Building from source takes four lines, and the last one copies the binary to a destination you pick:

bash
git clone https://github.com/theryangeary/choose.git
cd choose
cargo build --release
install target/release/choose <DESTDIR>

The sentence after that block is "Just make sure DESTDIR is in your path", so the directory is yours to choose and no uninstall step comes with it. Cargo handles the same job in one command with no arguments:

sh
cargo install choose

System packages use three different names for one program. Homebrew installs `brew install choose-rust`, Arch Linux users install `yay -S choose-rust-git`, MacPorts installs `sudo port install choose`, and Fedora and CentOS go through a COPR repository:

sh
dnf copr enable atim/choose
dnf install choose

Nothing on the page says what the Homebrew formula and the Arch package call the installed binary, or whether either drops a symlink, so on those two systems the first command worth running afterwards is the one that checks which answers. A source build leaves its binary inside the directory the toolchain creates, and the manifest targets Rust edition 2024 while excluding the test fixtures from the packaged crate.

perf, cargo test, and a 997 Hz sample rate

Development lives in the Makefile, and that is where the speed claims get produced:

bash
test:
	cargo test
	test/e2e_test.sh

bench: release
	test/bench.sh working

Testing runs in two layers, a Rust test pass plus an end-to-end shell script. Benchmarking comes in two flavours as well: the bench target hands `test/bench.sh` the word working to measure the tree you have, while the bench_commit variant passes the short commit hash from git so one build can be measured on its own.

The flamegraph target is the more interesting one. Debug symbols are forced on with `RUSTFLAGS=-g`, and the profiler run itself is one line:

bash
perf record --call-graph dwarf,16384 -e cpu-clock -F 997 target/release/choose -i test/long_long_long_long.txt 3:5

The resulting stack goes through stackcollapse-perf.pl, stackcollapse-recursive.pl, c++filt and flamegraph.pl into an SVG. That call-graph argument ends in 16384 and the sample rate is 997 Hz, an odd number to pick. The fixture file is named for its own length rather than for the data inside it, so profiling here targets long synthetic lines instead of realistic records, and the commit variant writes the short hash into the filename.

Claims about speed point onward to benchmarking.md, and none of those figures appear here. A `.github/` directory sits in the repository root, but the page says nothing about which of those workflows run on each push.

What awk and cut still do better

awk is a language with fields, patterns and arithmetic, and the project's own case for leaving it out of a quick command is `awk '{print $1}'`: four layers of quoting wrapped around a single field. choose drops all of that, giving up variables, conditionals, counters and any output other than the selected slices. Its README states the scope directly, saying choose "is not meant to be a drop-in or complete replacement" for either tool.

cut is the closer comparison, and the gaps are concrete. Field lists there are one-based and comma-separated, the delimiter option takes a single character, and a byte mode has no counterpart here. On this side indexes start at zero, each field spec is its own argument, and separators match with a regex. On a tab or colon separated line cut preserves empty fields, while whitespace defaulting in choose leaves their handling unstated, and that is the one case where swapping a script over changes output without an error.

The manifest declares `license = "GPL-3.0-or-later"`, a copyleft licence whose obligations attach to code derived from it. Scripts that shell out to the binary carry none of that, but anything vendoring the source into a larger program falls under those terms, so read them before going past the binary.

Editorial conclusion

Adopt choose for one-line field extraction in scripts and shell history, where zero-based slices and negative offsets buy more than awk's programmability, and pass on it where a pipeline needs aggregation, pattern branching, byte offsets or empty fields kept intact. Before switching a script over, run the exclusive range form against real input and watch the join character, since neither the whitespace empty-field rule nor the default for -o is written down anywhere, and confirm which binary name your package manager actually installed.

Frequently asked questions

How do I install choose on macOS or Linux?

The README lists `cargo install choose` as the shortest path, with Homebrew installing `brew install choose-rust`, MacPorts installing `sudo port install choose`, and Arch Linux installing `yay -S choose-rust-git`. Building from source ends with copying target/release/choose into a directory on your path.

How is choose different from cut?

choose counts fields from zero, accepts negative indexes that count back from the end of the line, and takes each field spec as its own argument instead of a comma-separated list. Its separator option takes a Rust regex rather than a single delimiter character, and it has no byte mode.

Does choose support regular expressions for field separators?

Yes. The help text specifies Rust regex syntax for the separator option, and a separate flag switches the pattern to non-greedy matching. A second option controls the separator used to join the printed fields, though its default is not documented.

Can choose replace awk in my shell scripts?

No. The README says it is not meant to be a drop-in or complete replacement, and the documented surface has no way to compute a field, branch on a match or keep a running total. It selects slices of a line and prints them.

Why does choose 5 print the sixth field?

Indexes are zero-based, so 5 is the sixth item on the line. Ranges use the same numbering, and a negative index counts back from the end, which is why -1 prints the last item.

Official sources

  1. Issues
  2. License: GPL-3.0
  3. README
  4. Releases
  5. theryangeary/choose on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/theryangeary-choose.svg)](https://hysenlabs.com/projects/theryangeary-choose)

Was this useful?