Write modular shell scripts
- Introduction
- The main script
- Download the main script
- Alias the main script
- Writing global commands
- Writing sub commands
- Special commands
- Appendix A - Jekyll script (jekyll.sh)
Introduction
When I start working at a new company, I will often create a set of scripts script to automate repetitive tasks. I also keep a set of scripts that I use for my own personal projects. While a lot of the scripts I write are specific for that company, there is aalso some amount of overlap. This document describes how I am able to reuse this code.
I often bundle commands into sub commands. For example I might have a jekyll
command that has make
sub command that makes the site and a new
command that creates a new site. This makes it easier to remember the commands. I also write inline documentation so that if I forget the exact subcommand, I can call the command and it will tell me the available subcommands.
I also include documentation so that if I remember that jekyll has a make subcommand but I can’t remember exactly what it does, I can call the help command and it will show me the documentation like a man page.
All of this is a lot easier then trying to browse several files or one large file lookig for the command that I wrote some time back and use infrequently.
The main script
At the heart of this effort is the main script. The main script provides the following functionalty:
- Provides basic functionality like like
error
which allow you to code a command that displays an error in red and exits the program. - Includes scripts in the
includes
subdirectory. I organize subcommands into a single file, so all the jekyll subcommands, for example, are stored in thejekyll
file. - Allows you to define your own commands. If you type
p hello
, the script will look for thecmd_hello
function. - Adds support for documentation. If you start a line with
#+
then that information will be included in the help command.
You can view the main script in GitHub.
Download the main script
This command downloads the main script. I recommend putting this in devops > bin. First you download the script, then you need to add the execute permission, then you should add a symlink to the first letter of the commpay. For example, if I’m working at a company named Acme, then I would name the script a.sh.
curl https://github.com/patrick-melo/script/blob/main/bin/p.sh > p.sh
chmod u+x p.sh
Alias the main script
I also add an alias to the script. That allows me to call the script with a simple one letter shortcut without having to specify the path to the script. To add an alias, I add a line to ~/.zshrc
. This file is in my home directory and is loaded every time I start a terminal. I name the alias the first name of the company I’m working at. So if I’m working at Acme, I would name the alias as follows:
alias p="~/repos/devops/bin/p.sh"
Writing global commands
To create a global command, create a file to hold that global command if you don’t already have one. The file should have a a name that begins with _global
and ends in .sh
for eaxmple _global_example.sh
. This file can store all your global commands in one place.
Create a command that starts with cmd_
and ends with the name you’d like to use in the command line. For example, this command echos text to the screen. If you type p echo hello world
, this function will be called and the terminal will display hello world
.
#+ echo [text]
#+ Echos text to the terminal.
#+
cmd_echo() { echo $@ ; }
Writing sub commands
To create sub commands, create a file with the name of the main command. In this example, we’re going to define a series of echo subcommands so we named the file echo.sh
. The command p echo terminal "hello world"
echos text to the terminal, commandp echo notify "hello world"
displays a notification, and command p echo dialog "hello world"
which displays a dialog.
#+
#+ EEcchhoo ccoommmmaannddss
#+
cmd_echo() { cmd echo $@; }
cmd_echo_@default() { cmd_echo_terminal "$@"; }
cmd_echo_@usage() { usage "$SCRIPT echo ['terminal'|'notify'|'dialog']"; }
#+ example echo [text]
#+ Echos the given text to the terminal.
#+
cmd_echo_terminal() { echo $@ ; }
#+ example notify [text]
#+ Displays a notification with the given text.
#+
cmd_echo_notify() { osascript -e 'display notification "'"$@"'" with title "Terminal"'; }
#+ example dialog [text]
#+ Displays a dialog with the given text.
#+
cmd_echo_dialog() { osascript -e 'tell app "System Events" to display dialog "'"$@"'" with title "Terminal"' >/dev/null 2>&1; }
Special commands
The main script offers support for two special functions @default
and @usage
.
If you uncomment cmd_echo_@default
, and you type p echo "hello world"
, you will see that the text is displayed on the terminal. This helper function allows you to define a default behavior for subcommands.
If you uncomment cmd_echo_@usage
, and type p echo
, the script will display Usage: p echo ['terminal'|'notify'|'dialog']
.
Appendix A - Jekyll script (jekyll.sh)
The following is an example of the jekyll command which allows you to run Jekyll from a Docker container.
cmd_j() { cmd_jekyll "$@" ; }
cmd_jekyll() { cmd jekyll "$@" ; }
cmd_jekyll_config() {
[ ! -f _config.yml ] && error "Cannot open _config.yml"
}
cmd_jekyll_volume() {
destination=$(awk '/destination/ {print $2}' _config.yml)
[ ! -z "$destination" ] && volume="--volume $PWD/$destination:/srv/jekyll/$destination"
echo $volume
}
cmd_jekyll_default() {
cmd_jekyll_config
docker run --platform linux/amd64 --volume "$PWD:/srv/jekyll" $(cmd_jekyll_volume) -it jekyll/jekyll jekyll $cmd $@
}
# JEKYLL_ENV is for whether to include google-analytics.html
cmd_jekyll_make() {
cmd_jekyll_config
docker run --platform linux/amd64 --volume "$PWD:/srv/jekyll" $(cmd_jekyll_volume) -it jekyll/jekyll /bin/bash -c "JEKYLL_ENV=production jekyll build $@"
}
cmd_jekyll_new() {
[ -z "$1" ] && usage "p jekyll new [site]"
docker run --platform linux/amd64 --volume "$PWD:/srv/jekyll" -it jekyll/jekyll /bin/bash -c "chmod -R 777 /usr/gem ; jekyll new $1 ; cd $1 ; bundle add webrick"
}
cmd_jekyll_serve() {
cmd_jekyll_config
docker run --platform linux/amd64 --volume "$PWD:/srv/jekyll" $(cmd_jekyll_volume) -it -p 4000:4000 jekyll/jekyll jekyll serve $@
}
cmd_jekyll_shell() {
cmd_jekyll_config
docker run --platform linux/amd64 --volume "$PWD:/srv/jekyll" $(cmd_jekyll_volume) -it jekyll/jekyll /bin/bash $@
}
cmd_jekyll_stop() {
docker ps -q --filter "ancestor=jekyll/jekyll" | xargs docker stop
}
These commands are centered around the docker run
command. You can read more about this on the Docker website. Here is an explanation of some of the options we use:
- The
--platform linux/amd64
option indicates that we’re running on a Macbook with Apple silicon. - The
--volume "$PWD:/srv/jekyll"
option indicates that we want to map our current working directory to the/src/jekyll
directory in the container. -
cmd_jekyll_volume
supports cases where the destination folder is not inside the source folder. - The
--it
option indicates that we want an interactive session. The-t
tells Docker to allocate a pseudo-tty and the-i
tells Docker to keep STDIN open. - The Docker container is specified with
jekyll/jekyll
. -
The minima theme has code in head.html has the following code. We set
JEKYLL_ENV=production
so that the analytics is included when we build the site for production.{%- if jekyll.environment == 'production' and site.google_analytics -%} {%- include google-analytics.html -%} {%- endif -%}
- The
docker run
are specified at the end. This includesjekyll serve
, orjekyll build
. - The
cmd_jekyll_new
command callsbundle add webrick
immediately after we create the new site to fix the errorcannot load such file -- webrick
. You can learn more about this in bug 361.
Comments
Join the discussion for this article on this ticket. Comments appear on this page instantly.