Show HN: Rb – Turns Ruby into a command line utility
github.com
github.com
A few examples:
Capitalizing a line:
echo hello world | rb -l capitalize
Printing only unique lines: printf 'hello\nworld\nhello\n' | rb uniq
Computing the sum of numbers in a table: printf '1\n2\n3\n' | rb 'map(&:to_i).sum'
Personally I think it's a really cool idea. echo hello world | ruby -pe '$_.capitalize!'
printf 'hello\nworld\nhello\n' | ruby -le 'puts STDIN.to_a.uniq
ps | ruby -lane 'BEGIN { b = 0 }; b += $F[0].to_i; END { print "sum of PIDs: #{b}" }
select whatever column you want, to sum up.Really what Ruby needs is a flag that adds some capabilities to NilClass, so that these BEGIN blocks aren't needed.
Does it, though?
ps | ruby -le 'b = $<.reduce(0) {|s, l| s + l.split(0).to_i}; print "sum of PIDs: #{b}"'
or: ps | ruby -lane 'b = (b || 0) + $F[0].to_i; END { print "sum of PIDs: #{b}" }'Each of your examples use different flags, inputs and print methods. The last is particularly good at proving the point!
docker ps | rb drop 1 | rb -l split[1]
docker ps -a | rb grep /Exited/ | rb -l 'split.last.ljust(20) + " => " + split(/ {2,}/)[-2]'
df -h | rb 'drop(1).sort_by { |l| l.split[-2].to_f }'
Command line tools in zero lines of ruby (using ruby): docker ps | ruby -lane 'next if $. == 1; print $F[1]
docker ps -a | ruby -lne 'print $1 if /(Exited .*? ago)/'
df | ruby -lane 'BEGIN { lines = [] }; lines.push [$F[4].to_i, $_] if $. > 0; END { lines.sort { |a,b|b[0] <=> a[0] }.each{|k| print k[1] } }
Bit of a pain as ++ is lacking, as is autovivication. a /bin/sort at the end usually beats `<=>` for terseness.It's a cool script though.
It's normal for a oneliner to be somewhat inscrutable though, since they're often extremely dependent on their inputs. Like if it's important to fetch the 3rd, 5th, and 7th column--why? You can't tell without the input.
df -h | ruby -e 'puts readlines.drop(1).sort_by { |l| l.split[-2].to_f }'
similarly, the `uniq` example from your other comment could be shortened to printf 'hello\nworld\nhello\n' | ruby -e 'puts readlines.uniq' code = ARGV[0]
STDIN.each_line do |l|
puts l.instance_eval(code)
STDOUT.flush
end execute(STDIN.each_line, code)
which gives you a lazy enumerator, in place of: execute(STDIN.readlines, code)
which gives you an Array.That said, brew install foo is a normal part of many development workflows, which essentially just curls a file from someone else's git repo.
I've [submitted a PR][2] to inline the install script in the README.
Am I way off base here?
Most recent discussion: https://news.ycombinator.com/item?id=17636032
All things perfectly manageable inside a PORO. Bundler even has an inline mode, so you can put everything in one file, close to your data, Bundler just handles dep management under the hood like a boss. Check out bundler-inline.
Sure, you can do all that with bash. But you can also make system() calls with Ruby, with full string interpolation power tools. If you're a Rubyist, and you want to do data analysis, this is the workflow you want.
The other thing that keeps me back is scale. If everything fits in memory, cool, but sometimes I need to sort or uniq something that won't.
There's also the option of pushing the workload off onto AWS that I may look into.
If you're wondering how this is at-all fast (and it is), in Linux at least /tmp is a tmpfs, meaning that these tempfiles are actually ending up as memory after all. They get "spilled to disk" in the sense that the memory associated with the files spills to swap; but, importantly, tmpfs pages know that they're from files, so they have much better swapping semantics than regular memory for the case where you're writing the file as a stream, and then reading it as a stream. (Basically, juggling streams under a tmpfs is equivalent in overhead to managing memory when you've got it, and managing files when you don't, but without any of the dev-time overhead of having to write code for both cases.)
I love ruby but for throw away jobs I'd be more likely to do it on the command line than a ruby script simply because it would be quicker for me to write and run in a shell. Unless I'm wrong at the very least you wouldn't have to deal with the GIL.
Not my experience. More often than not, if I'm writing a pipeline (in a shell or in a REPL), it's because someone handed me a file they made in a one-off effort that has nonsensical nonstandard formatting, and I'm trying to normalize it.
If they made another file, it'd have different nonsensical nonstandard formatting, so there's no reuse here.
Meanwhile, writing a pipeline at a REPL (or shell) enables me to just work on it expression by expression, keeping a variable-assignment at the beginning of the expression so that I can then work on the intermediate created data to figure out how to munge it just a little bit more, and then add that back to the pipeline once it's right.
If there was a way to edit a Ruby script such that, each time I save it, my existing REPL session re-runs the script and loads the bindings from it into my existing session—without destroying any other bindings I've made—then I'd be happy to use that. But that's less like "using files", and much closer to using something like a Jupyter notebook.
$ docker ps | rb drop 1 | rb -l split[1]
$ docker ps | perl -anE 'say $F[1] if $.>1'
perl solved this problem a long time ago, people awk 'NR>1{print $2}'
or ruby[1] ruby -ane 'puts $F[1] if $.>1'
which one to use depends on lots of factor - speed, features, availability, etc as well as whether user already knows perl[2]/ruby/etcFurther reading(disclosure: I wrote these)
[0]: https://github.com/learnbyexample/Command-line-text-processi...
[1]: https://github.com/learnbyexample/Command-line-text-processi...
[2]: https://github.com/learnbyexample/Command-line-text-processi...
Look again at your code. I'm not 100% sure what the Ruby version does, but once I do figure out the exact semantics of drop and split, it's going to be waaaaaaaaaaaaaaaaay easier to remember, understand and modify the Ruby version than the Perl one.
Your post comes off as something from reddit's /r/nottheonion, you're just making the case against Perl for Perl-haters :)
-a autosplits each line by whitespace and puts each element into the array F (this was inspired by AWK).
-n loops through each line of the file, and -E executes the perl.
$. (NR in AWK) is the line number.
As others have noted, you can write the same thing in ruby on the command line already with `ruby -ane 'puts $F[1] if $.>1'`
(Notice the similarities?)
If you write one liners in AWK, Perl, or Ruby often the "odd" variables look more like useful shortcuts.
*edit You could also write the perl without any of the special variables, but it would be much more verbose, hence the special characters and flags.
Granted, anything really nontrivial I'll usually just write a Python script for, but the point stands.
cat your-file.txt | ruby -ne '$_.your_ruby_stuff'
Just syntactic sugar? (it does look cleaner!)
I though it was great, knowing python better than bash, plus it was portable to windows.
But eventually I always end up using built in GNU tools.
Don't know why. It just rolls better.
Well, it’s these 10 lines, plus an unlimited amount of Ruby that you have to compose on the fly for each operation.
I think that's relative and it certainly hasn't been my experience. If I program in Ruby, Node, Python, etc for 8 hours of every day, it makes sense that I would reach for that over a command line tool with a syntax that looks a bit arcane. The best tool for the job sometimes is the tool you know best.
That's like hammering in screws because you know your hammer and screwdrivers seem arcane to you.
For ad-hoc stuff `ruby -e` is more than enough, and if you want to write a program once and use it a lot, it's worth investing the time in doing it the way it works best, which in this case, would be a (fast) language like awk that makes it easier to process line-by-line instead of reading until EOF and making the rest of the pipeline wait.
docker ps | rb drop 1 | rb -l split[1]
The output of docker ps is passed as an array of lines to ruby, and the first line is dropped, and then that output is fed to ruby again, but line by line, so each line is split (which defaults to split on whitespace), and returns the second element. With parentheses for function calls it would be: docker ps | rb drop(1) | rb -l split()[1] rb () {
[[ $1 == -l ]] && shift
case $? in
0 ) ruby -e "STDIN.each_line { |l| puts l.chomp.instance_eval(&eval('Proc.new { $* }')) }";;
* ) ruby -e "puts STDIN.each_line.instance_eval(&eval('Proc.new { $* }'))";;
esac
}I'm not saying I hate Ruby. I just haven't take the time (nor had a reason to, frankly) become a fan the way I have with Python. Being similar languages in terms of both being dynamic, malleable, mostly single threaded and REPL-able they both seem to occupy the same spot in my toolbox.
Not to knock Ruby-the-language, but the one hitch is that Python's ecosystem is quite a bit vaster into fields I don't interact with (biology, physics, etc.) but also with many that I do (numpy/sympy, cli utilities and scripting, opencv, curl bindings). Where I'm at, it's most everyone's secondary or tertiary language.
Nothing against Ruby, it's just circumstance. If anything I might pick it up soon if only to access JRuby/TruffleRuby worlds which are much ahead of Jython/GraalPython.
Oh, except for function calls without (). And I'm not huge on DSLs. They seem icky but I grew to love significant whitespace in Python so maybe I'll grow to love those too.
The example to sort df -h by the percent:
df -h | p 'sorted({}[1:], key=lambda x: int(x.split()[-2][:-1]))'
I saved this as ~/bin/p #!/usr/bin/env python3
import sys
single_line = sys.argv[1] == '-l'
code = ' '.join(sys.argv[2 if single_line else 1:])
if single_line:
for line in sys.stdin:
print(eval(code.format('line.strip()')))
else:
print('\n'.join(eval(code.format('sys.stdin.read().splitlines()'))))- It's a bit unintiuitive because you don't really know what strings to escape on the bash command line. In the example, some are escaped and others aren't.
- In the end I think this could be achieved with a simple bash function:
$ rb() { ruby -lane "print \$_.instance_eval(\"$@\")"; }
$ echo hello | rb capitalize
$ Hello