# pru turns a pipeline stage into a piece of Ruby you can read

> pru is a small Ruby command line filter that replaces grep, sed, awk and wc in shell pipelines with Ruby expressions, with a map mode over each line, a reduce mode over all lines, and JSON and Kubernetes input modes. It suits a developer who writes shell but thinks in Ruby, and it is the wrong tool for a pipeline that has to run at a scale where starting a Ruby process per stage matters.

**grosser/pru** — Pipeable Ruby - forget about grep / sed / awk / wc ... use pure, readable Ruby!

- Repository: https://github.com/grosser/pru
- Stars: 588 · Forks: 19
- Language: Ruby
- License: not declared
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/grosser-pru

## The same expression, three ways of applying it

pru is described as pipeable Ruby: forget about grep, sed, awk and wc, use pure, readable Ruby instead. The tool is a filter that takes Ruby expressions and applies them to a stream of input.

The design has one idea and two modes. In the default map mode, each line of input is one item. In the reduce mode, all the lines are collected into an array and the expression runs once over that array.

```
    # count letters of each line
    ls -1 | pru size

    # select lines longer than five letters
    ls -1 | pru 'size > 5'

    # 2nd to last character
    ls -1 | pru self[2..-1]
```

And the reduce variants of the same three ideas.

```
    # count lines
    ls -1 | pru -r size

    # are the more than five lines?
    ls -1 | pru -r 'size > 5'

    # get 2nd to last line
    ls -1 | pru -r self[2..-1]
```

The vocabulary is small. `self` is the current line or the array, `size` is its length, and the expression is plain Ruby. What you can write is what Ruby can do.

The naming of the two modes is borrowed from functional programming, and the pairing is the point: map and reduce compose, so the same expression can be used either way.

## Composing map and reduce in one pipeline

The examples show that pru accepts more than one expression, and that the modes mix.

```
    # count letters in each line, then sum
    ls -1 | pru size sum

    # select lines longer than 5 letters, then join with commas
    ls -1 | pru 'size > 5' 'join(",")'
```

The first is a map followed by a reduce: count the letters of each line, then sum the counts. The second is a filter followed by a reduce: keep the long lines, then join them with commas. The mode is inferred from the expression, and you can change the mode mid pipeline with the -r flag or by using an expression that operates on the collection.

This is where pru earns its keep over sed and awk, because the awkward parts of shell text processing are the parts where awk's field-splitting and sed's substitution syntax stop being readable. The project shows the replacements directly.

```
    # grep --- all lines including foo
    ls -al | grep foo
    ls -al | pru /foo/

    # awk --- return second item
    ls -al | awk '{print $2}'
    ls -al | pru 'split(" ")[1]'

    # wc --- count lines
    ls -al | wc -l
    ls -al | pru --reduce 'size'
```

Note the last one uses --reduce spelled out, and that the grep replacement uses a regular expression literal rather than a string, because the expression is Ruby and Ruby has regex literals.

## JSON and Kubernetes modes for machine output

Two of the newer modes are the reason to reach for pru rather than a jq habit, though the project does not mention jq.

The --json flag parses a stream of JSON values, newline-delimited or pretty-printed multiline, into items. Hash and Array results are pretty-printed, and everything else is printed plainly so it pipes nicely into other tools.

```
    # pull a field out of each object
    printf '{"a":1}\n{"a":2}\n' | pru --json 'self["a"]'

    # filter objects
    printf '{"n":1}\n{"n":5}\n' | pru --json 'self["n"] > 3'

    # map then reduce
    printf '{"a":1}\n{"a":2}\n' | pru --json 'self["a"]' 'sum'
```

The --k8s flag is the same idea with one difference: it iterates the "items" key, which is the shape kubectl returns with -o json.

```
    # list pod names
    kubectl get pods -A -o json | pru --k8s 'dig("metadata", "name")'
```

The dig call is the right way to reach into a nested hash in Ruby, and the whole example is one expression that replaces the usual kubectl plus jq plus template pipeline.

One caveat is worth stating. Ruby hashes are not the same as JSON objects when it comes to string keys and symbol access, and the --json mode prints Hash results pretty-printed while printing everything else plainly. That formatting decision is what makes the output pipeable, and it is also why the mode is a slightly different shape from what a JSON tool gives you.

## In-place editing and the other options

Beyond filtering, pru can edit a file in place, which turns it into a rewriting tool rather than a filter.

```
    pru -i Gemfile 'sub /ruby/, "foo"'
```

The -i flag takes the file and applies the expression to its contents, writing the result back. The documentation's own example is a cleanup pass.

```
    pru -i Rakefile 'gsub(/\r\n/,"\n").gsub(/\t/,"  ")'
```

That one normalises line endings and replaces tabs with spaces, and it is the kind of change you would otherwise write a one-off script for.

The full option list is short. There is -r, --reduce CODE to reduce via the given code, -j, --json to parse input as a JSON stream where each value is an item, and -k, --k8s which behaves like --json but iterates the items array if present. There is -I, --libdir DIR to add a directory to the load path, and --require LIB to require a library, which also accepts a comma-separated list. There is -i, --inplace-edit FILE, plus -h, --help and -v, --version.

The --require option is the escape hatch. Several documented examples rely on it, such as pulling the bundler versions feed and parsing it with JSON before matching a gem requirement.

```
    curl http://rubygems.org/api/v1/versions/bundler | pru --require json 'JSON.parse(self).map{|g|g["number"]}.find{|v| Gem::Requirement.new("~>1.0.1").satisfied_by? Gem::Version.new(v) }'
```

That single expression fetches a version list, parses it, and evaluates a version constraint. The equivalent shell would need a temp file, a second tool, and some quoting discipline.

## Installing it as a gem or as a standalone binary

There are two installs, and the second one exists for a reason.

As a gem it is one command.

```
gem install pru
```

Or as a standalone script.

```
curl https://rubinjam.herokuapp.com/pack/pru > pru && chmod +x pru
./pru -v
```

The standalone route is the interesting one, because it removes Ruby from the requirement. The README explains the context: working with rvm, mise or rbenv means installing once per Ruby version, so the gem route needs care.

```
    rvm 2.7.7 exec gem install pru
    echo 'alias pru="rvm 2.7.2 exec pru"' >> ~/.bash_profile
```

With several Ruby versions on a machine, a per-version gem install and an alias to pin the version is the documented pattern. The alternative is rubinjam, which builds the tool as a standalone binary, and that is linked from the README with its own one-liner.

```
    curl -sSfL https://rubinjam.onrender.com/pack/pru > pru && chmod +x pru
    ./pru --version
```

Two different hosts for the packed binary appear in the README, an older herokuapp.com URL and a newer onrender.com one, which is the kind of detail that tells you the project has outlived a couple of hosting arrangements.

## The limitations come from what the expressions can reach

Two limits are worth understanding before you rewrite a script with pru.

The first is performance, and it is structural rather than fixable. Every pru stage is a Ruby process, so a pipeline with several stages starts several interpreters. The documentation does not claim any speed advantage, and the tool's argument is readability. For a script that runs occasionally, on a developer's machine, that is the right trade. For a hot loop over a large file in a cron job, a single Ruby script that reads the file once will beat a chain of pru stages, and the tool is the wrong shape for that.

The second is that the expressions are code, and they are evaluated. That is the entire point, since you can call any Ruby method, and it is also the risk: a pipeline that pipes untrusted input into pru is piping it into eval. The examples that reach the network, like the one that shells out to curl to count watchers, show how much Ruby is in scope.

There is also a small correctness trap in the examples worth noting. The awk replacement for counting and averaging is written as a separate reduce expression using a string with interpolation and escaped newlines, and it is the least readable line in the whole set of examples. That is not an accident of the documentation, it is what happens when a shell idiom does not map cleanly onto the map and reduce model.

The project is also small in the way that matters for maintenance. It has no GitHub releases, the last push was on 2026-07-01, and the repository is not archived. The gemspec, the bin/ directory, lib/ and spec/ are the whole project, with a Rakefile and a Gemfile.

## Who maintains it and under what terms

pru is by Michael Grosser, and the project page gives the author contact and a licensing line: MIT.

That is the project as a whole, and the distinction from the per-file terms is worth checking in your own repository, because the project metadata records the licence as one that GitHub cannot classify.

The contributors list names John Hobbs, Vasiliy Ermolovich, Jens Wille and SixArm, and the project links a build badge from a Travis CI configuration, alongside a .github/ directory in the repository, so the build moved between the two services over the life of the project.

The repository tree is minimal and tells you what kind of tool this is. There is a .ruby-version file, a Gemfile and a Gemfile.lock, a Rakefile for the tasks, a pru.gemspec, and the code in bin/ and lib/ with the tests in spec/. A Gemfile.lock is checked in, which for a gem is unusual and suggests the lock is there to keep the development environment reproducible rather than to be consumed by applications.

The tool has been around long enough to have a stable shape and a small surface. That is the trade: you get one executable with a handful of flags and a vocabulary of Ruby expressions, and you give up the ecosystem of tools around grep, sed and awk.

## Conclusion

pru fits a developer or a small team whose shell scripts are becoming unreadable one-liners and who would rather express a filter in Ruby than in awk, since the same expression language covers selection, replacement, counting and aggregation. It does not fit a pipeline where process startup dominates, because each pru stage is a Ruby process, and it does not fit a team that does not want Ruby on the machine at all, which is the case the standalone binary exists to solve. Before adopting it, check that the Ruby version on the box is one you can run, since the project documents installing it under rvm, mise or rbenv, and remember that the expressions are evaluated with eval, so anything you pipe through it is code you are choosing to run.

## FAQ

### How do I install pru?

As a gem with gem install pru, or as a standalone binary by downloading the packed script with curl, making it executable and running ./pru -v. The README also covers installing under rvm, mise or rbenv, where only one install per Ruby version is needed.

### What is the difference between pru map and reduce mode?

In the default map mode the expression runs once per input line with self bound to that line, and in reduce mode all the lines are collected into an array and the expression runs once over the whole array. The -r or --reduce flag selects the reduce mode.

### How do I process JSON with pru?

Use the --json flag to parse a stream of JSON values into items, and --k8s to do the same but iterate the items array as returned by kubectl -o json. Hash and Array results are pretty-printed and other values are printed plainly so the output pipes into other tools.

### Can pru edit a file in place?

Yes, with the -i flag, for example pru -i Gemfile 'sub /ruby/, "foo"'. The documented example uses it to normalise line endings and replace tabs with spaces in a Rakefile.

## Sources

- [grosser/pru on GitHub](https://github.com/grosser/pru)
- [Issues](https://github.com/grosser/pru/issues)
- [README](https://github.com/grosser/pru/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/grosser-pru
