Introduction
Ruby is a better Perl and in my opinion is an essential language for system administrators. If you are still writing scripts in Bash, I hope this inspires you to start integrating Ruby in to your shell scripts. I will show you how you can ease in to it and make the transition very smoothly.
The idea with 'enhanced shell scripting' is to create hybrid Ruby/Bash scripts. The reason for this is to take advantage of both worlds. Ruby makes it seamless to pass data back and forth with shell commands.
There are many times when running shell commands is easier or required when there is an external command-line utilities you need to run.
On the other hand, Bash syntax is quite ugly and difficult to remember, and it has very limited features. Ruby offers two tons of object-oriented power, along with tons of libraries and better syntax.
We will look at how to write 'enhanced shell scripts' using Ruby and other tips on taking advantage of both worlds.
Execute Bash commands in Ruby
You can execute shell commands in Ruby, making it easy to run external programs.
Backticks
In Ruby, backticks allow shell execution to provide seamless back and forth with shell. This makes it easy to convert an existing shell script to Ruby. You can simply wrap everything in back ticks and start porting over the sections needed to Ruby.
#!/usr/bin/ruby
`cd ~`
puts `pwd`
puts `uname -a`.split
The $? object
Check for return codes and errors with the $?
special object. This is
similar to the $?
used in Bash except it is a Ruby object with more information.
This is critical for error checking and handling Bash scripts
#!/usr/bin/ruby
# Execute some shell command
`pwd`
# Was the app run successful?
puts $?.success?
# Process id of the exited app
puts $?.pid
# The actual exit status code
puts $?.exitstatus
The $?
object will work properly from commands run using backticks.
For example:
#!/usr/bin/ruby
`touch /root/test.txt`
if $?.exitstatus != 0
puts 'Error happened!'
end
Execute Ruby from Bash
If you have a Bash script and you only want to run a little bit of Ruby
code, you have a couple options. You can pass the code as an argument using
the ruby -e
flag or you can create a heredoc and pass a block of code.
Execute Ruby in a single line
#!/bin/bash
echo "This is a Bash script, but may want to call Ruby."
ruby -e 'puts "you can jam"; puts "a whole script in one line";'
Execute a block of code with heredoc
If you are working in a bash script but want to execute a block of Ruby code, you can use a heredoc
#!/bin/bash
echo "This is a Bash script executing some Ruby"
/usr/bin/ruby <<EOF
puts 'Put some Ruby'
puts 'code here'
EOF
Get input in Ruby
There are several ways to get input in to Ruby. You can take command-line arguments, read environment variables, use interactive prompts, read in config files like JSON, or use STDIN to receive piped input. We will look at each of these options and more.
Command line arguments
Command-line arguments are passed in through the ARGV
object. Unlike
some other languages, Ruby does not include the name of the script being
executed as an argument. So if you ran ruby myscript.rb
the ARGV
object
would be empty. The first argument that gets passed to the script
happens.
#!/usr/bin/ruby
ARGV.each { |arg|
puts arg
}
# Or access individual elements
# No error will be thrown if the arg does not exist
puts ARGV[1]
Environment variables
You can get and set environment variables using the ENV
object. Note
that any environment variables you set are lost when the Ruby script is
done running.
#!/usr/bin/ruby
# Get an environment variable
puts ENV['SHELL']
# Set an environment variable (only lasts during Ruby session)
ENV['SOME_VAR'] = 'test'
Prompt user for input
The gets
prompt is good for getting interactive input from the user.
#!/usr/bin/ruby
print "Enter something: "
x = gets.chomp.upcase
puts "You entered: #{x}"
Prompt user for password
This is similar to prompting the user for input, only the terminal output should not be echoed back for security purposes. This is only available in Ruby 2.3 and newer.
#!/usr/bin/ruby
require 'io/console'
# The prompt is optional
password = IO::console.getpass "Enter Password: "
puts "Your password was #{password.length} characters long."
To learn more see my tutorial Get Password in Console with Ruby.
The ARGF object
The ARGF
object is a virtual file that either takes input from STDIN
or takes the command-line arguments from ARGV
and loads files by name.
This allows flexible usage of your script with zero effort.
You can pass a file to it by piping it a couple ways:
#!/bin/bash
# ARGF will process these from STDIN
cat myfile.txt | ruby myscript.rb
ruby myscript.rb < myfile.txt
# ARGF will load these files by name (as if one big input was provided to STDIN)
ruby myscript.rb myfile.txt myfile2.txt
In Ruby, you use ARGF
like this:
#!/usr/bin/ruby
# ARGF will take in a file argument or use STDIN if no file
# If multiple file names are provided, it cats them all together
# and treats it as one big input.
ARGF.each do |line|
puts line
end
There are a few more options but that is the basic idea.
Read more about ARGF
at
https://ruby-doc.org/core-2.6.3/ARGF.html.
Reading JSON config files
JSON files are a convenient way to store settings information. This is a simple example of how to read a JSON config file to pull some data.
#!/usr/bin/ruby
require 'json'
# If there is a file named `settings.json` that contains:
# {"data": 42}
json_object = JSON.parse(File.read('settings.json'))
puts json_object['data']
# Outputs: 42
Output from Ruby
There are several methods of outputting information from Ruby. You can
use the obvious STDOUT
and STDERR
and you can also specify an exit
status code to pass on to any calling program. We will also
Writing to STDOUT
You can write to STDOUT
using puts
and print
.
#!/usr/bin/ruby
# STDOUT is default output target
puts 'Text with newline'
print 'Text without newline'
STDOUT.puts 'Equivalent to puts'
STDOUT.print 'Equivalent to print'
Writing to STDERR
It is important to separate STDOUT
and STDERR
output to allow proper
piping of applications. Debug output, and anything that does not belong
in the output of the application should go to STDERR
and only data
that should be piped to anothe application or stored should go to STDOUT
#!/usr/bin/ruby
STDERR.puts 'This will go to STDERR instead of STDOUT'
STDERR.print 'This will print with no newline at the end.'
Writing to a file
You can easily write to a file in Ruby like this:
#!/usr/bin/ruby
open('test.txt', 'w') { |output_file|
output_file.print 'Write to it just like STDOUT or STDERR'
output_file.puts 'print(), puts(), and write() all work.'
}
Colorize terminal output
You can easily add color to your output using the colorize
module.
# In the system terminal
gem install colorize
gem install win32console # For windows
#!/usr/bin/ruby
require 'colorize'
puts "Blue text".blue
puts 'Bold cyan on blue text'.cyan.on_blue.bold
puts "This is #{"fancy".red} text"
Learn more in my full tutorial Colorize Ruby Terminal Output.
Specifying exit status code
To play well with other programs, your Ruby script should return a proper exit status indicating wether it exited with success or other status.
#!/usr/bin/ruby
# Equivalent success exit codes
exit(0)
exit(true)
# Error or other status
exit(3)
Built-in regular expressions
Ruby has first-class regular expressions and can be used directly in the language
with the =~
operator. For example:
#!/usr/bin/ruby
if `whoami`.upcase =~ /^NANODANO$/
puts 'You are nanodano!'
end
This makes Ruby a lot more like Perl. Regular expressions in Python are very ugly compared to this. Since regular expressions are so common in shell scripting, it feels right at home in Ruby.
Processes
Ruby provides modules for interacting with processes. For example, forking or getting information about its own process ID. Read more about the Process module at https://ruby-doc.org/core-2.6.3/Process.html.
Get Ruby's process ID
You can get your current process ID with Process.pid
.
puts Process.pid
Fork a process
Forking can be confusing, but essentially it creates an exact duplicate of the
current process in memory that gets assigned a unique PID as the child process
or the original. Both processes will then continue running the rest of the code.
You can use the process IDs to determine if the running process is the parent
or child. You can simply call fork
and you will have two processes.
Here is a simple example of forking:
#!/usr/bin/ruby
# There is only one process at this time, the parent.
parent_pid = Process.pid
# After this line executes, there will be two copies of
# this program running with separate pids.
# child_pid will be empty in the child process, since it
# hadn't started yet and parent process will have a non-nil child_pid value
child_pid = fork
# The parent and child will print out different pids at this point
puts Process.pid
puts "Child pid: #{child_pid}"
if Process.pid == parent_pid
puts 'I am the parent!'
else
puts 'I am a child!'
end
Alternatively, you can put the code to be executed in the forked process inside a block so it only executes a specific task.
#!/usr/bin/ruby
fork do # Limit forked process to a block of code
puts 'Starting child and working for one second...'
sleep 1
puts 'Finishing child.'
end
puts 'Waiting for child processes to finish...'
Process.wait # Wait for child processes to finish
puts 'Child processes finished. Closing.'
Kill a process
Here is an example of forking a child process and then sending it a signal to kill it and waiting for the child process to complete before exiting cleanly.
Read more about the Process.kill()
function at
https://ruby-doc.org/core-2.6.3/Process.html#method-c-kill.
#!/usr/bin/ruby
parent_pid = Process.pid
child_pid = fork do
puts "My child pid is #{Process.pid} and my parent is #{parent_pid}"
Signal.trap("HUP") {
puts 'Signal caught. Exiting cleanly.'
exit(true)
}
while true do end
end
puts "Child pid is #{child_pid}"
puts "My pid is #{Process.pid}"
puts 'Killing child process now.'
Process.kill("HUP", child_pid)
puts 'Waiting for child process to finish.'
Process.wait # Waits for child processes to finish
Trap an interrupt signal
Just as shown in the previous example, you can catch signals using
Signal.trap()
and in this case we want to watch for the SIGINT
interrupt
signal caused by pressing CTRL-C
key combination.
#!/usr/bin/ruby
Signal.trap("SIGINT") {
puts 'Caught a CTRL-C / SIGINT. Shutting down cleanly.'
exit(true)
}
puts 'Running forever until CTRL-C / SIGINT signal is recieved.'
while true do end
Read more about Signal.trap()
at https://ruby-doc.org/core-2.6.3/Signal.html#method-c-trap.
Working with files and directories
Some basic examples for working with common directory and file tasks like getting a user home directory, walking directories, globbing contents, and joining file paths.
Basics file functions
This is only a few of the functions, not an extensive list, but you will see all the expected functions.
Dir.pwd
Dir.mkdir
Dir.rmdir
Dir.chdir
Dir.exist?
Dir.empty?
File.chmod
File.delete
File.empty?
File.executable?
File.exists?
File.readable?
File.size
File.new
File.open
File.mkfifo
See more at https://ruby-doc.org/core-2.6.3/Dir.html and https://ruby-doc.org/core-2.6.3/File.html.
Get user home dir
A common task is getting the path of the user's home directory.
#!/usr/bin/ruby
# Get user home dir
puts Dir.home
# or a specific user
puts Dir.home 'nanodano'
Join file paths
To join file paths using the proper slashes for the operating system, use
File.join
. This is the equivalent of Python's os.path.join()
.
# Create the path to `~/.config` in a cross-platform way.
File.join(Dir.home, '.config')
Glob a directory
You can easily glob a directory (get a list of objects) using Dir[]
syntax.
You can also use Dir.glob()
.
Dir['*.png']
Dir['**/*'] ## Recursively go through directories and get all files
# Or
Dir.glob('*.png')
Rake
Rake is a task execution tool for any kind of project, Ruby or not. Rake is great for managing several related tasks that may share some common code. It can make managing and executing scripts much simpler.
Here is a simple example Rakefile
demonstrating how to create basic tasks:
#!/usr/bin/ruby
# Rakefile
task default: [:build, :install] # Will run :build then :install
task :clean do
puts "Cleaning"
end
task :build => [:clean] do # Will run :clean first
puts "Building"
end
task :install do
puts "Installing"
end
These tasks can be run using the following commands in the system terminal:
rake
rake clean
rake build
rake install
Also check out my Ruby Rake Tutorial.
Conclusion
Hopefully this document has provided some inspiration to start using Ruby to enhance shell scripts. Ruby provides so much power over the Bash shell yet we still can't live without it so we might as well catalyze that synergy (tm).
References
- Colorize Ruby Terminal Output
- Get Password in Console with Ruby
- Ruby Rake Tutorial
- http://www.tweetegy.com/2012/04/forking-ruby-processes-or-how-to-fork-ruby/
- https://ruby-doc.org/core-2.6.3/Dir.html
- https://ruby-doc.org/core-2.6.3/File.html
- https://ruby-doc.org/core-2.6.3/Process.html
- https://ruby-doc.org/core-2.6.3/ARGF.html
- https://ruby-doc.org/core-2.6.3/Process.html#method-c-kill
- https://ruby-doc.org/core-2.6.3/Signal.html#method-c-trap