The Art of Writing a Fully Self-contained Bash Script


Bash is amazingly powerful and yet not the easiest thing to learn. When learning any new computer language it’s natural to write everything in a very long single file. As you get better with the language you start to break things out into separate files as reusable modules. Bash is no different. One exception is when writing a Bash script which is intended to be used on a remote server via SSH. 

Breaking up common tasks into self-contained bash scripts for use over SSH.

When managing WordPress sites there are various tasks I repeat. Here is a short list of such tasks.

  • Deploy plugins or other predefined configurations
  • Launches sites – updates URL from dev to live, enable search engine and clears WordPress object cache
  • Update themes and plugins
  • Update URLs for HTTPS after SSL is installed

Many of these tasks are a combination of WP-CLI commands and bash scripting. Breaking them up into separate files allows them to be run over SSH with no other prerequisites. 

Reducing everything to the bare necessity.

Within my toolkit CaptainCore I’ve written over a dozen Bash scripts which are used remotely over SSH. If they were written for local use they would be structured differently. Locally I’d break them apart into an organized hierarchy of files and include other dependencies as needed. 

When used remotely over SSH only the single file is available. All prerequisites for the script to run need to be self contained. Keeping this file somewhat readable requires stripping out anything that is not necessary. If the script doesn’t require any arguments then don’t both including code to parse arguments.

Handling argument support manually

I like to pass arguments into a bash script in a similar format to WP-CLI. That looks like script-name.sh --name=<value>. Bash out of the box doesn’t support this format. Rather than rely on a separate library to parse arguments I’ve used the following bash loop which transforms arguments into useable bash variables. 

# Loop through arguments and separate regular arguments from flags
for arg in "$@"; do

  # Add to arguments array. (Does not starts with "--")
  if [[ $arg != --* ]]; then
    count=1+${#arguments[*]}
    arguments[$count]=$arg
    continue
  fi

  # Remove leading "--"
  flag_name=$( echo $arg | cut -c 3- )

  # Add to flags array
  count=1+${#flags[*]}
  flags[$count]=$arg

  # Process flags without data (Assign to variable)
  if [[ $arg != *"="* ]]; then
    flag_name=${flag_name//-/_}
    declare "$flag_name"=true
  fi

  # Process flags with data (Assign to variable)
  if [[ $arg == *"="* ]]; then
    flag_value=$( echo $flag_name | perl -n -e '/.+?=(.+)/&& print $1' ) # extract value
    flag_name=$( echo $flag_name | perl -n -e '/(.+?)=.+/&& print $1' ) # extract name
    flag_name=${flag_name/-/_}

    # Remove first and last quote if found
    flag_value="${flag_value%\"}"
    flag_value="${flag_value#\"}"

    declare "$flag_name"="$flag_value"
    continue
  fi

done

This is how it looks to pass arguments remotely over SSH. 

ssh sitename@sitename.net "bash -s -- --name='<value>' < script-name.sh

Inside the script-name.sh file the bash varible $name will be set to the value passed in the arguments. 

Heredocs help break apart other languages. 

Since referencing other files isn’t possible using heredocs helps to keep scripts somewhat readable. This works well for generating mu-plugins as shown in my deploy-fathom.sh script. The PHP code for the generated mu-plugin starts out within a bash heredoc section.