Shell friendy Ruby scripts Sep 24 2018

When we build Ruby scripts, generally, we build them to be run independently but being able to compose them with other scripts makes them even better. How can we make our Ruby scripts be easy to compose? This is what this post is about, I will show you how to build your scripts so they can be composed or used independently.

Let’s see a basic Ruby script that receives a list of files and displays all lines capitalized.

1
2
3
4
5
ARGV.each do |file|
  File.open(file).each_line do |line|
    puts line.capitalize
  end
end

You can try it and see if it works:

1
ruby capitalize.rb words1.txt words2.txt

You’ll see a display of all the lines capitalized. Let’s make the script act more like a command so we don’t have to call the Ruby interpreter each time:

1
2
3
4
5
6
#!/usr/bin/env ruby 
ARGV.each do |file|
  File.open(file).each_line do |line|
    puts line.capitalize
  end
end

Let’s add execution permissions and rename it to just capitalize:

1
2
chmod u+x capitalize.rb
mv capitalize.rb capitalize

Now we can call it like any other command on the shell:

1
./capitalize words1.txt words2.txt

Our script currently works well on its own, but we want it to be composable. Bash, or any other modern shell, has the capability to redirect the output of a command and send it as the input of another command, allowing us to compose complex workflows using programs that excel on doing one task. We do this by using the special character |, called pipe.

For example, if we would like to obtain 10 random words from our system dictionary we could use the following command:

1
sort -R /usr/share/dict/words  | head -n 10

This will display to the screen ten random words, but not all of them are capitalized so we would like to use our script to display them all capitalized. This is how we would like to compose our commands:

1
sort -R /usr/share/dict/words  | head -n 10 | ./capitalize

And get a list of 10 names on our screen all capitalized. If we run it with our current script we get nothing, this is because our script is expecting to receive a list of files to process, but we are getting our input from the pipe, so we need to modify our script to be able to handle the case where no files are being sent as parameters and read from the standard input. Our script will look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env ruby

if ARGV.length > 0
ARGV.each do |file|
  File.open(file).each_line do |line|
    puts line.capitalize
  end
end
else
  $stdin.each_line do |line|
    puts line.capitalize
  end
end

We are telling our script, if the arguments number is greater than zero, it should treat those arguments as the names of the files to process. And if the number of arguments is zero, it should read the lines from the standard input. This solution works. You can try it again:

1
sort -R /usr/share/dict/words  | head -n 10 | ./capitalize

And this time you should get a list of capitalized words. But Ruby makes it easier for us to make scripts that handle this behaviour, it provides the global variable ARGF.

ARGF is an abstraction, it is an input stream that we can iterate through and it will handle both cases when the script receives a list of arguments or when it should read from the standard input. Let’s change our script to use ARGF instead, our script will look like this:

1
2
3
4
5
#!/usr/bin/env ruby

ARGF.each_line do |line|
    puts line.capitalize
end

Now we can call our script with filenames as arguments:

1
./capitalize words1.txt words2.txt

or by getting the content from standard input without a problem:

1
sort -R /usr/share/dict/words  | head -n 10 | ./capitalize

ARGF allows us to write more compact scripts and handles the case when we would like to compose our scripts and get the information not only from arguments but from standard input. Notice that it can only handle one case you can’t at the same time send arguments and expect it to read from the standard input, it will raise an error.

Hope this post was helpful and now you have another tool to make your scripts play nice with other commands. You can focus on writing scripts that perform one task well and be sure you can reuse them by composing them in any workflow.

If you have any questions from this post or suggestions for topics you want me to explore send me a message.


** If you want to check what else I'm currently doing, be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message, you can send it to derik@rderik.com.