Ru
Ruby in your shell!
Overview
Ru brings Ruby's expressiveness, cleanliness, and readability to the command line.
It lets you avoid looking up pesky options in man pages and Googling how to write a transformation in bash that would take you approximately 1s to write in Ruby.
For example, to center a file's lines, use String#center:
ru 'map(:center, 80)' myfile
Using traditional tools, this isn't as easy or readable:
awk 'printf "%" int(40+length($0)/2) "s\n", $0' myfile
For another example, let's compare summing the lines of a list of integers using Ru vs. a traditional approach:
ru 'map(:to_i).sum' myfile
awk '{s+=$1} END {print s}' myfile
Any method from Ruby Core and Active Support can be used. Ru also provides new methods (and modifies #map) to make transformations easier. Here are some variations on the above example:
ru 'map(:to_i, 10).sum' myfile
ru 'map(:to_i).reduce(&:+)' myfile
ru 'each_line.to_i.to_a.sum' myfile
ru 'grep(/^\d+$/).map(:to_i).sum' myfile
ru 'map { |n| n.to_i }.reduce(&:+)' myfile
ru 'reduce(0) { |sum, n| sum + n.to_i }' myfile
ru 'each_line.match(/(\d+)/)[1].to_i.to_a.sum' myfile
ru 'map { |n| n.to_i }.reduce(0) { |sum, n| sum + n }' myfile
See Examples and Methods for more.
Installation
gem install ru2
You can now use Ruby in your shell!
For example, to sum a list of integers:
$ printf "2\n3" | ru 'map(:to_i).sum'
5
Usage
See Examples below, too!
Ru reads from stdin:
$ printf "2\n3" | ru 'map(:to_i).sum'
5
$ cat myfile | ru 'map(:to_i).sum'
5
Or from file(s):
$ ru 'map(:to_i).sum' myfile
5
$ ru 'map(:to_i).sum' myfile myfile
10
You can also run Ruby code without any input by prepending a =
:
$ ru '=2 + 3'
5
The code argument is run as if it has $stdin.each_line.map(&:chomp).
prepended to it. The result is converted to a string and printed. So, if you run ru 'map(&:to_i).sum'
, you can think of it as running puts $stdin.each_line.map(&:chomp).map(&:to_i).sum
.
In addition to the methods provided by Ruby Core and Active Support, Ru provides other methods for performing transformations, like each_line
, files
, and grep
, and it improves map
. See Methods for more.
Stream mode
If input data is very large or of undefined size it may be better to process it line by line without loading the whole data into memory.
This can be done by activating stream mode (which utilizes Enumerator::Lazy) by passing -s
or --stream
flag.
Note, that in stream mode Ru can process only one file or input stream at a time.
For example, let's count how many lines there are in the /dev/urandom :)
$ cat /dev/urandom | ru -s 'inject(0){|a| puts a if a % 100000 == 0; a+1 }'
Or how many zeros there are in the /dev/zero :)
Note, that there are no lines in the stream, so we have to active binary mode by passing -b
or --binary
flag.
$ cat /dev/zero | ru -s -b 'inject(0){|a| puts a if a % 10000000 == 0; a+1 }'
As you can see, this allows to read stream or file byte by byte.
$ echo 'test' > /tmp/test && ru -b 'join(" ")' /tmp/test
116 101 115 116 10
Note, that memory consumption is constant no matter how long those commands are running. You can interrupt them with Ctrl+C.
Examples
Let's compare the readability and conciseness of Ru relative to existing tools:
Center lines
ru
ru 'map(:center, 80)' myfile
awk
awk 'printf "%" int(40+length($0)/2) "s\n", $0' myfile
sed
Sum a list of integers
ru
ru 'map(:to_i).sum' myfile
awk
awk '{s+=$1} END {print s}' myfile
paste
paste -s -d+ myfile | bc
Print the 5th line
ru
ru '[4]' myfile
sed
sed '5q;d' myfile
Print all lines except the first and last
ru
ru '[1..-2]' myfile
sed
sed '1d;$d' myfile
Sort an Apache access log by response time (decreasing, with time prepended)
ru
ru 'map { |line| [line[/(\d+)( ".+"){2}$/, 1].to_i, line] }.sort.reverse.map(:join, " ")' access.log
awk
awk --re-interval '{ match($0, /(([^[:space:]]+|\[[^\]]+\]|"[^"]+")[[:space:]]+){7}/, m); print m[2], $0 }' access.log | sort -nk 1
Methods
In addition to the methods provided by Ruby Core and Active Support, Ru provides other methods for performing transformations.
each_line
Provides a shorthand for calling methods on each iteration of the input. Best explained by example:
ru 'each_line.strip.center(80)' myfile
If you'd like to transform it back into a list, call to_a
:
ru 'each_line.strip.to_a.map(:center, 80)' myfile
files
Converts the lines to Ru::File
objects (see Ru::File below).
$ printf "foo.txt" | ru 'files.map(:updated_at).map(:strftime, ""%Y-%m-%d")'
2014-11-08
format(format='l')
Formats a list of Ru::File
s. You'll typically call this after calling files
to transform them into strings:
$ ru 'files.format'
644 tom staff 3 2014-10-26 09:06 bar.txt
644 tom staff 11 2014-11-04 08:29 foo.txt
The default format, 'l'
, is shown above. It prints [omode, owner, group, size, date, name]
.
grep
Selects lines which match the given regex.
$ printf "john\npaul\ngeorge" | ru 'grep(/o[h|r]/)'
john
george
map
This is the same as Array#map, but it adds a new syntax that allows you to easily pass arguments to a method. For example:
$ printf "john\npaul" | ru 'map(:[], 0)'
j
p
$ printf "john\npaul" | ru 'map(:center, 8, ".")'
..john..
..paul..
Note that the examples above can also be performed with each_line
:
$ printf "john\npaul" | ru 'each_line[0]'
$ printf "john\npaul" | ru 'each_line.center(8, ".")'
Ru::File
The files
method returns an enumerable of Ru::File
s, which are similar to Ruby Core's File
. Each one has the following methods:
basename
-
created_at
(alias for ctime) ctime
extname
-
format
(see theformat
method above) ftype
gid
group
mode
mtime
-
name
(alias for basename) omode
owner
size
-
to_s
(alias for name) uid
-
updated_at
(alias for mtime) world_readable?
Options
-h, --help
Print a help page.
-v, --version
Print the installed version of Ru.
Testing
Ru is tested against Active Support 3, 4 and 5. If you'd like to submit a PR, please be sure to use Appraisal to test your changes in all contexts:
appraisal rspec
License
Ru is released under the MIT License. Please see the MIT-LICENSE file for details.