Automating Our Workstation Setup
Learn how to automate our workstation setup.
We'll cover the following
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.