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 the jekyll file.
  • Allows you to define your own commands. If you type p hello, the script will look for the cmd_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 includes jekyll serve, or jekyll build.
  • The cmd_jekyll_new command calls bundle add webrick immediately after we create the new site to fix the error cannot load such file -- webrick. You can learn more about this in bug 361.