Automating Our Workstation Setup

Learn how to automate our workstation setup.

Throughout this course, we’ve created a customized environment. We’ve installed some tools on our machine, created some directories, and wrote some configuration files. Once we get something just how we like it, we might want to keep it that way. We can create a script that’ll set things up for us. That way, when we get a new machine, we can run our script and have everything set up exactly how we want it.

Let’s create a script that does just that. We’ll install the handful of utilities we’ve installed throughout the course and create a ~/.bashrc file with some basic options. To keep this exercise short, the script won’t contain everything we’ve done, but it’ll contain enough that we’ll be able to add everything else on our own. We’ll start by creating a script to set up an Ubuntu environment, which we can test with the Ubuntu virtual machine we created. If you’re on a Mac, follow along anyway because once we’re done, we’ll alter the script so that it works on macOS.

To build this script, we’ll leverage many of the techniques we learned in this course, along with a few new ones. We’ll use some conditional statements to check exit codes from commands, we’ll install programs with the package manager, and we’ll create files with cat. We’ll also use variable expansion throughout the script, a small bit of command substitution, and a for loop to iterate over a collection of packages.

Setting up the OS

Let’s create a new file named ubuntu_setup.sh and make it executable:

touch ubuntu_setup.sh
chmod +x ubuntu_setup.sh

We start our script with the shebang line and declare a variable that holds a datestamp to use as a suffix when creating backups of files. Then, we add a call to sudo, which will cause the script to prompt for the sudo password right away:

#!/usr/bin/env bash
datestamp=$(date +"%Y%m%d%H%M") 
sudo -v

We’re not using set -e this time because we’re going to trap errors ourselves.

Setting output colors

The commands in this script will generate a lot of text output. Let’s colorize information messages, error messages, and success messages so they’ll stand out from the other text.We add this code to create variables to hold the color values:

ERRORCOLOR=$(tput setaf 1) # Red
SUCCESSCOLOR=$(tput setaf 2) # Green 
INFOCOLOR=$(tput setaf 3) # Yellow 
RESET=$(tput sgr0)

This is similar to how we defined the colors for our shell prompt in our .bashrc file, except this time, we don’t need to include square braces in the output since these won’t be used as part of the prompt. And just like with the prompt, we include a variable named RESET so we can put the shell colors back to normal.

To use these colors, we print them out using variable expansion inside of echo statements. That’ll get messy quickly, so we define functions we can call instead to print info, success, and error messages:

function info() { echo "${INFOCOLOR}${@}${RESET}"; } 
function success() { echo "${SUCCESSCOLOR}${@}${RESET}"; } 
function error() { echo "${ERRORCOLOR}${@}${RESET}" >&2; }

Each function prints the color code to the terminal, then prints all the arguments passed to the function (${@}), and then resets the terminal’s text settings. The error function is slightly different, though. It redirects the output to STDERR instead of STDOUT, the default output of echo.

Starting message

Next, we display a message that informs the user we’re starting the script. We use the info function we just defined and use command substitution with the date command to get the date to display:

info "Starting install at $(date)"

Installing packages

Now, we add this code to the script. It installs some of the software packages we used in this course.

declare -a apps=(tree curl unzip make) info "Updating Package list"
sudo apt-get update
info "Installing apps:" 
for app in ${apps[@]}; do
info "Installing ${app}" 
sudo apt-get -y install $app 
result=$?
if [ $result -ne 0 ]; then
error "Error: failed to install ${app}"
exit 1
else
success "Installed ${app}"
fi 
done

First, we define a variable named apps, which contains a list of applications we want to install. We then iterate over each entry in the list using a for loop, displaying the application’s name and passing the application to the apt-get command to install it. The -y option tells apt-get to install the program without confirmation, which is perfect for a script.

Throughout the course, we’ve used apt install instead of apt-get install. The apt command is a newer high-level user interface for installing packages interactively. It sets some default options for package installation and looks a little nicer. The apt-get command is the legacy command and is a good fit for scripts because it has a more stable user interface. In fact, if we use apt in a script, we see this warning:

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

We could install these packages without using the list of packages and the for loop because the apt-get command supports supplying a list of packages to install all at once. But with this structure, we have a little more control.

Let’s add installing Node.js to this setup script. We use curl to grab the installation script and execute it directly this time. This “pipe a script from the internet to Bash” approach does pose a security risk. Still, we’re going to do it here because we’re trying to automate the process anyway, so downloading it and inspecting it as we did previously doesn’t make sense. We have to trust that the servers holding the script haven’t been compromised.

We add this code to the script:

echo "Installing Node.js from Nodesource"
curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash - sudo apt-get -y install nodejs
result=$?
if [ $result -ne 0 ]; then
error "Error: Couldn't install Node.js."
exit 1
else
success "Installed Node.js" 
fi

Once again, we check the exit status from the apt-get command and display an error message if it failed.

Creating a .bashrc file

Now that the software is installed, let’s create a ~/.bashrc file containing some of the settings we configured in The Shell and Environment. The code that follows won’t have everything we did previously, but we can add more as we work through this. Let’s back up any existing ~/.bashrc file first. We add this code to check if one exists. If it does, we back it up:

info "Setting up ~/.bashrc"
if [ -f ~/.bashrc ]; then
oldbashrc=~/.bashrc.${datestamp}
info "Found existing ~/.bashrc file. Backing up to ${oldbashrc}." 
mv ~/.bashrc ${oldbashrc}
fi

To create the backup file name, we use the datestamp variable we defined at the top of the script as a suffix for the file. Notice that we aren’t using any quotes around the value we are creating. The tilde isn’t expanded to the full path in a double-quoted string. It’s stored as a literal character. If we used double quotes here, the command to move the script would fail because the destination path would be invalid. As an alternative, we could replace all occurrences of the tilde with ${HOME}. That value would be expanded in the double-quoted string.

Now, we use cat to create the new ~/.bashrc file and then check the status of the command:

cat << 'EOF' > ~/.bashrc [ -z "$PS1" ] && return
shopt -s histappend
HISTCONTROL=ignoreboth
HISTSIZE=1000
HISTFILESIZE=2000
HISTIGNORE="exit:clear"
export EDITOR=nano
export VISUAL=nano
export PATH=~/bin:$PATH EOF
result=$?
if [ $result -ne 0 ]; then
error "Error: failed to create ~/.bashrc"
exit 1
else
success "Created ~/.bashrc" 
fi

Using single quotes around the EOF in the cat line ensures that the variables in the body of the heredoc aren’t interpreted.

Finally, we add a message at the end of the script that confirms things are finished and tells the user what to do next:

success "Done! Run source ~/.bashrc to apply changes."

Now, we save our file. To test it out, we run the script with ./ubuntu_setup.sh. It’ll produce a lot of output.

Run the complete code on the terminal below for practice.

cat << "END" > ubuntu_setup.sh
#!/usr/bin/env bash
#---
# Excerpted from "Small, Sharp, Software Tools",
# published by The Pragmatic Bookshelf.
# Copyrights apply to this code. It may not be used to create training material,
# courses, books, articles, and the like. Contact us if you are in doubt.
# We make no guarantees that this code is fit for any purpose.
# Visit http://www.pragmaticprogrammer.com/titles/bhcldev for more book information.
#---
datestamp=$(date +"%Y%m%d%H%M")

sudo -v

ERRORCOLOR=$(tput setaf 1)    # Red
SUCCESSCOLOR=$(tput setaf 2)  # Green
INFOCOLOR=$(tput setaf 3)     # Yellow
RESET=$(tput sgr0)

function info()    { echo "${INFOCOLOR}${@}${RESET}"; }
function success() { echo "${SUCCESSCOLOR}${@}${RESET}"; }
function error()   { echo "${ERRORCOLOR}${@}${RESET}" >&2; }

info "Starting install at $(date)"

declare -a apps=(tree curl unzip make)

info "Updating Package list"
sudo apt-get update

info "Installing apps:"
for app in ${apps[@]}; do
  info "Installing ${app}"
  sudo apt-get -y install $app
  result=$?
  if [ $result -ne 0 ]; then
    error "Error: failed to install ${app}"
    exit 1
  else
    success "Installed ${app}"
  fi
done


echo "Installing Node.js from Nodesource"
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get -y install nodejs
result=$?
if [ $result -ne 0 ]; then
  error "Error: Couldn't install Node.js."
  exit 1
else
  success "Installed Node.js"
fi

info "Setting up ~/.bashrc"

if [ -f ~/.bashrc ]; then
  oldbashrc=~/.bashrc.${datestamp}
  info "Found existing ~/.bashrc file. Backing up to ${oldbashrc}."
  mv ~/.bashrc ${oldbashrc}
fi

cat << 'EOF' > ~/.bashrc
[ -z "$PS1" ] && return

shopt -s histappend
HISTCONTROL=ignoreboth
HISTSIZE=1000
HISTFILESIZE=2000
HISTIGNORE="exit:clear"

export EDITOR=nano
export VISUAL=nano

export PATH=~/bin:$PATH
EOF

result=$?
if [ $result -ne 0 ]; then
  error "Error: failed to create ~/.bashrc"
  exit 1
else
  success "Created ~/.bashrc"
fi

success "Done! Run   source ~/.bashrc    to apply changes."
END
chmod +x ubuntu_setup.sh
./ubuntu_setup.sh

Get hands-on with 1200+ tech skills courses.