If you use Linux or Mac, every time you open a terminal you are using a shell application.
A shell is an interface that helps you execute commands in your system.
The shell hosts environment variables & has useful features like a command history and auto-completion.
If you are the kind of person that likes to learn how things work under the hood, this post will be perfect for you!
How Does a Shell Work?
To build our own shell application let’s think about what a shell really is:
First, there is a prompt, usually with some extra information like your current user & current directory, then you type a command & when you press enter the results are displayed on your screen.
Yeah, that sounds pretty basic, but doesn’t this remind you of something?
If you are thinking of pry
then you are right!
A shell in basically a REPL (Read-Eval-Print-Loop) for your operating system.
Knowing that we can write our first version of your shell:
prompt = "> " print prompt while (input = gets.chomp) break if input == "exit" system(input) print prompt end
This will give us a minimal, but functional shell. We can improve this by using a library that many other REPL-like applications use.
That library is called Readline
.
Using The Readline Library
Readline is part of the Ruby Standard Library, so there is nothing to install, you just need to require
it.
One of the advantages of using Readline
is that it can keep a command history automatically for us.
It can also take care of printing the command prompt & many other things.
Here is v2 of our shell, this time using Readline
:
require 'readline' while input = Readline.readline("> ", true) break if input == "exit" system(input) end
This is great, we got rid of the two puts
for the prompt & now we have access to some powerful capabilities from Readline
. For example, we can use keyboard shortcuts to delete a word (CTRL + W
) or even search the history (CTRL + R
)!
Let’s add a new command to print the full history:
require 'readline' while input = Readline.readline("> ", true) break if input == "exit" puts Readline::HISTORY.to_a if input == "hist" # Remove blank lines from history Readline::HISTORY.pop if input == "" system(input) end
Fun fact: If you try this code in pry you will get pry’s command history! The reason is that pry is also using
Readline
, andReadline::HISTORY
is shared state.
Now you can type hist
to get your command history 🙂
Adding Auto-Completion
Thanks to the auto-completion feature of your favorite shell you will be able to save a lot of typing. Readline makes it really easy to integrate this feature into your shell.
Let’s start by auto-completing commands from our history.
Example:
comp = proc { |s| Readline::HISTORY.grep(/^#{Regexp.escape(s)}/) } Readline.completion_append_character = " " Readline.completion_proc = comp ## rest of the code goes here ##
With this code you should be able to auto-complete previously typed commands by pressing the <tab>
key. Now let’s take this a step further & add directory auto-completion.
Example:
comp = proc do |s| directory_list = Dir.glob("#{s}*") if directory_list.size > 0 directory_list else Readline::HISTORY.grep(/^#{Regexp.escape(s)}/) end end
The completion_proc
returns the list of possible candidates, in this case we just need to check if the typed string is part of a directory name by using Dir.glob
. Readline will take care of the rest!
Implementing The System Method
Now you should have a working shell, with history & auto-completion, not too bad for 25 lines of code 🙂
But there is something that I want to dig deeper into, so you can get some insights on what is going on behind the scenes of actually executing a command.
This is done by the system
method, in C this method just sends your command to /bin/sh
, which is a shell application. Let’s see how you can implement what /bin/sh
does in Ruby.
Note: This will only work on Linux / Mac 🙂
The system method:
def system(command) fork { exec(command) } end
What happens here is that fork
creates a new copy of the current process, then this process is replaced by the command we want to run via the exec
method. This is a very common pattern in Linux programming.
If you don’t fork then the current process is replaced, which means that when the command you are running (ls
, cd
or anything else) is done then your Ruby program will terminate with it.
You can see that happening here:
def system(command) exec(command) end system('ls') # This code will never run! puts "after system"
Conclusion
In this post you learned that a shell is a REPL-like interface (think irb
/ pry
) for interacting with your system. You also learned how to build your own shell by using the powerful Readline
library, which provides many built-in features like history & auto-completion (but you have to define how that works).
And after that you learned about the fork
+ exec
pattern commonly used in Linux programming projects.
If you enjoyed this post could you do me a favor & share it with all your Ruby friends? It will help the blog grow & more people will be able to learn 🙂
How can one handle piping?
Good question Joe 🙂
If you use the built-in
system
method you don’t have to worry about it. But if you are implementing your own you could use the IO.pipe method.