Want to see the full-length video right now for free?
To start, when we say "CLI" or "command line utility," we're talking about a
program that we can run at the command line by simply typing the command name.
git
is a great example of the sort of commands we could write (although ours
will of course be a good bit simpler).
To begin, we'll work with a very simple bash script with the following contents
echo 'hello world'
We can then run this very basic shell script by specifying the sh
interpreter (similar to using ruby script.rb
):
$ sh script.sh
This is a great start, but our goal is be able to run this without specifying
the interpreter, so we can try that as follows (for now we'll need to specify
the current path with ./
)
$ ./script.sh
zsh: permission denied: ./script.sh
Here we hit our first wall since file can not be executed directly.
By default, the text files we create are not executable and we have to
explicitly mark them as executable if we want to use them without always
needing to specify the language / interpreter to use. We can mark our script
as executable using the chmod
command:
$ chmod +x script.sh
And now we can directly run our script with ./script.sh
.
Shell is fine, but it is far from my favorite language to write in. Instead,
I'd rather write in a language like Ruby. We can rename our script file to
script.rb
and update the contents to:
puts 'hello world'
but now we've broken our script. If we try to run it based on its new executable mode, we'll get an error:
$ ./script.rb
./script.rb: line 2: puts: command not found
This error occurs because the shell, by default, is running our script with
the sh
(shell) interpreter. It doesn't much care about the file extension
being "rb", that's for humans and text editors.
Instead, we can specify the interpreter to use, ruby
in this case, using a
"shebang" line. This is a special comment that must be the first line in our
file, and specifies the interpreter to use. The contents of our script.rb
file are now:
#!/usr/bin/env ruby
puts 'hello world'
Here, we've added the "shebang" line to specify that we want to run via
ruby
. In fact, we're going a bit further and using /usr/bin/env
which is a
command to which we pass the argument ruby
, and this command will then
provide a specific version of ruby
. This is useful if we're working with
multiple rubies via rbenv
, chruby
, or other tool to manage multiple
version of ruby.
Now, we can run our script directly without needing to re-specify ruby
, but
we can even go a bit further and remove the ".rb" extension:
$ ./script.rb
hello world
$ mv script.rb script
$ ./script
hello world
These all work, but just to go crazy, we can actually change our shebang line
to specify many different interpreters, even node
!
#!/usr/bin/env node
console.log('hello world from JavaScript')
$ ./script
hello world from JavaScript
The final step in building up our command line script is to free ourselves from having to know the location of the script, and instead be able to run it from anywhere.
In order to do this, we can hook into a special shell variable called $PATH
.
The $PATH
is an ordered list of directories, separated by :
s, that the
shell will search through to find executables.
As an example, here is the (abbreviated) value of $PATH
on my system,
splitting to show one directory per line:
$ echo $PATH | tr \: \\n
/Users/christoomey/.gem/ruby/2.1.5/bin
/Users/christoomey/.rubies/ruby-2.1.5/lib/ruby/gems/2.1.0/bin
/Users/christoomey/.rubies/ruby-2.1.5/bin
/Users/christoomey/Library/Haskell/bin
/Users/christoomey/bin
/usr/local/bin
/usr/bin
/bin
/sbin
/opt/X11/bin
/usr/local/go/bin
In this case, I can take our script
file and move it into my ~/bin/
directory which is on my path, and from there I can run the script from
anywhere!
$ mv script ~/bin/
$ script
hello world
You can add a directory like ~/bin
to your path by adding a snippet like the
following to your shell startup file (~/.bashrc
, ~/.zshrc
, etc):
export PATH="$HOME/bin:$PATH"
For a more in depth overview of $PATH
setting, check out [this great Unix
StackExchange answer][], or take a peek at how the [thoughtbot dotfiles work
with the PATH][].
[this great Unix StackExchange answer]: http://unix.stackexchange.com/a/26059 [thoughtbot dotfiles work with the PATH]: https://github.com/thoughtbot/dotfiles/blob/2d9e8bf0e84a835f300bd519faef4b6bfe289a47/zsh/configs/post/path.zsh#L1-L12
Now that we know how to build these command line scripts and make them available everywhere, the question becomes, "What can we do with them?" It turns out we can do a whole lot.
The first and likely largest category these sort of command line scripts fall into are general purpose helper scripts. A few examples are:
bin/setup
, this script automates deploying
Upcase.[tat]: https://github.com/thoughtbot/dotfiles/blob/2d9e8bf0e84a835f300bd519faef4b6bfe289a47/bin/tat [bin/setup]: https://github.com/thoughtbot/upcase/blob/bc01657d935a3df515267b5cd91746255d504ea6/bin/setup [bin/deploy]: https://github.com/thoughtbot/upcase/blob/bc01657d935a3df515267b5cd91746255d504ea6/bin/deploy
One specific use case for command line scripts is to automate Git workflows.
git
will automatically look for executable scripts on your $PATH
that
match the command name, so running git churn
will search for a script named
git-churn
that is marked as executable and available on your path.
As an example, I have the script called [git-shalector][] in my ~/bin
directory which I can then run as:
$ git shalector
git-shalector
is written in Bash, but like with all our command line
scripts, we can choose any interpreted language to use, so [git-publish][] is
written in Ruby.
[git-shalector]: https://github.com/christoomey/dotfiles/blob/77fb4084bd3f207aace80aa93a49769a6a298ddb/bin/git-shalector [git-publish]: https://github.com/christoomey/dotfiles/blob/77fb4084bd3f207aace80aa93a49769a6a298ddb/bin/git-publish
Similar to Git, [Ruby's Bundler][] has the same sort of path lookup for
subcommand magic built in. The thoughtbot dotfiles includes a custom
[bundler-search subcommand][] that uses bundle and ag
([the_silver_searcher][]) to search for a pattern in all of your active gems.
$ bundle search 'semantic_form_for'
[Ruby's Bundler]: http://bundler.io/ [bundler-search subcommand]: https://github.com/thoughtbot/dotfiles/blob/2d9e8bf0e84a835f300bd519faef4b6bfe289a47/bin/bundler-search [the_silver_searcher]: https://github.com/ggreer/the_silver_searcher
Often we want to chain together a number of commands using &&
, knowing that
if any command in the sequence fails, the whole sequence will stop. In order
to support this, we need to return a specific exit code from our script.
0
as the exit code means "success" and the next command will run, any other
value means "failure" and that no further commands in the &&
sequence should
run. Also, we can always check $?
as it will contain the exit code of the
previous command:
$ true && echo hello
hello
$ false && echo hello
# no output as `false` returned non-zero exit code
$ echo $?
1
By default our scripts will exit with 0
as the status code indicating
success, but we can explicitly set the exit code to indicate failure as
needed:
# new script file called 'hello'
puts 'hello world'
exit 1
With the addition of exit 1
, this command now is viewed as failing, so if we
chain it:
$ ./hello && echo after
hello world
This is a bit more advanced, but since this often confuses newer users, we
wanted to cover how to work with error text. By default, all output is sent to
the standard output, aka stdout
, stream. In the event of an error, we often
want to alert the user by presenting some sort of message, but we don't want
to muck up the normal output of the script.
Luckily, there is a second output stream known as standard error or stderr
,
and we can send any error text to it.
In bash this would done by echoing and redirecting to 2
, which is the
numeric way to reference stderr
:
echo 'this is an error' >&2
Similarly, in Ruby we have a special IO global we can use called $stderr
:
$stderr.puts 'this is an error'
In some cases, we just want to silence both the output and error messages. We
can do this by redirecting stdout
and stderr
. Given we have the following
contents in our hello
script file:
#!/usr/bin/env ruby
puts 'hello world'
$stderr.puts 'errroorrrrrr'
exit 1
We can run the script, redirecting the output to the file output.txt
, but
allowing the error message to be displayed:
$ ./hello > output.txt
errroorrrrrr
$ cat output.txt
hello world
Similarly, we can combine stderr
into stdout
and redirect both into our
file:
$ ./hello > output.txt 2>&1
$ cat output.txt
errroorrrrrr
hello world
Lastly, we can explicitly ignore the error output by redirecting stderr
to
/dev/null
which is essentially a black hole:
$ ./hello > output.txt 2>/dev/null
$ cat output.txt
hello world