Automating Your Workstation Setup

Throughout this book, you’ve created a customized environment. You’ve installed some tools on your machine, created some directories, and wrote some configuration files. Once you get something just the way you like it, you might want to keep it that way. You can create a script that will set things up for you. That way, when you get a new machine, you can run your script and hit the ground running.

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

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

Create a new file named ubuntu_setup.sh and make it executable:

 $ ​​touch​​ ​​ubuntu_setup.sh
 $ ​​chmod​​ ​​+x​​ ​​ubuntu_setup.sh

Now open the file with nano:

 $ ​​nano​​ ​​ubuntu_setup.sh

Start out your script with the shebang line and declare a variable that holds a datestamp you’ll use as a suffix when creating backups of files. Then 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

You’re not using set -e this time because you’re going to trap errors yourself.

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. 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 you defined the colors for your shell prompt in your .bashrc file, except this time you 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, include a variable named RESET so you can put the shell colors back to normal.

To use these colors, you’ll print them out using variable expansion inside of echo statements. That’ll get messy quickly, so define functions you 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 to STDOUT, which is echo’s default output.

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

 info ​"Starting install at ​​$(​date​)​​"

Now add this code to the script which installs some of the software packages you used in this book.

 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, you define a variable named apps which contains a list of apps you want to install. You then iterate over each entry in the list using a for loop, displaying the app’s name and passing the app 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 book, you’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 you use apt in a script, you’ll see this warning:

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

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

In Working with Web APIs, you installed Node.js. Let’s add that to this setup script as well. You’ll 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, but you’re going to roll with it here because you’re trying to automate the process anyway, so downloading it and inspecting it like you did previously doesn’t make sense here. You’ll just have to trust that the servers holding the script haven’t been compromised.

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, you check the exit status from the apt-get command and display an error message if it failed.

Now that the software is installed, let’s create a ~/.bashrc file containing some of the settings you configured in Chapter 6, The Shell and Environment. The code that follows won’t have everything you did previously, but as you work through this, feel free to add more. Let’s back up any existing ~/.bashrc file first. Add this code to check if one exists, if it does, 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 filename, use the datestamp variable that you defined at the top of the script as a suffix for the file. Notice that we are not 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 you used double quotes here, the command to move the script would fail because the destination path would be invalid. As an alternative, you could replace all occurrences of the tilde with ${HOME}. That value would be expanded in the double-quoted string.

Now 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

By using single quotes around the EOF in the cat line, you ensure that the variables in the body of the heredoc aren’t interpreted.

Finally, 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."

Save your file. To test it out, suspend nano (Ctrl+z). Then, at the prompt, run the script with ./ubuntu_setup.sh. It’ll produce a lot of output, truncated here to save space:

 $ ​​./ubuntu_setup.sh
 Starting install at Sun Mar 3 03:01:55 IST 2019
 Updating Package list
 Hit:1 http://in.archive.ubuntu.com/ubuntu bionic InRelease
 Get:2 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
 Hit:3 https://deb.nodesource.com/node_11.x bionic InRelease
 ...
 
 Installed Node.js
 Setting up bashrc
 Found existing bashrc file. Backing up
 to /home/brian/.bashrc.201903030301.
 Created ~/.bashrc
 Done! Run source ~/.bashrc to apply changes.

Your logging statements throughout the script display with color, and you can see the responses from the other commands as well.

If you want to run your script silently, redirect all output to a log file:

 $ ​​./ubuntu_setup.sh​​ ​​>​​ ​​log.txt​​ ​​2>&1

This won’t capture the commands, but you can add set -x to the script and Bash will print all of the commands executed. Alternatively, use bash to execute the script and pass the -x option, like this:

 $ ​​bash​​ ​​-x​​ ​​ubuntu_setup.sh​​ ​​>​​ ​​log.txt​​ ​​2>&1

The log now contain all executed commands.

Bash scripting offers so much more that you haven’t covered yet. Bash has while loops you can use either in scripts or on the command line, which you saw an example of in Serving Files with Netcat:

 $ ​​while​​ ​​true;​​ ​​do​​ ​​nc​​ ​​-l​​ ​​8000​​ ​​<​​ ​​hello.txt;​​ ​​done

And remember that you can take advantage of tools like sed, cut, or awk to manipulate input and output in your scripts. And instead of using cat to write your files, you could host your files online and use curl to pull them down directly. You can even host your script online, making it even easier to use on a new machine.

Unfortunately, this installation script only works on an Ubuntu system. But read on to find out how to make things work on macOS. It’s not much different.

Making a Mac Version

If you’re using a Mac, you may want a similar script. To make it, replace apt-get update with brew update, and apt-get install with brew install. Node.js is available through Homebrew, so add nodejs to the list of packages to install and remove the manual installation steps. And since macOS uses ./bash_profile instead of .bashrc, you’ll want to make that change as well. Let’s walk through it.

Copy the existing script and open it in your editor:

 $ ​​cp​​ ​​ubuntu_setup.sh​​ ​​mac_setup.sh
 $ ​​nano​​ ​​mac_setup.sh

Since Homebrew isn’t installed by default on a Mac, let’s add it as a step to the script:

 info ​"Starting install at ​​$(​date​)​​"
 
»info ​"Checking for Homebrew"
»if ​which -s brew; ​then
» ​info ​"Homebrew installed"
»else
» ​info ​"Installing Homebrew"
» /usr/bin/ruby -e ​
»"​​$(​curl -fsSL
» https://raw.githubusercontent.com/Homebrew/install/master/install​)​​"
»fi

You first check for the existence of the brew command. which -s will silently look for the command, so you won’t see output. Then you check the exit status and install Homebrew if it’s not available. We’ll use the method that Homebrew recommends,[27] which involves downloading a script from their site and sending it to the Ruby interpreter installed on macOS. Like the Node.js install for Ubuntu, we’re trading security for the ability to automate the installation.

Next, modify the section where the script installs apps. Add nodejs to the list of packages and change the apt-get references to brew references.

»declare -a apps=(tree curl unzip make nodejs)
 
 info ​"Updating Package list"
»brew update
 
 info ​"Installing apps:"
 for ​app ​in​ ​${​apps[@]​}​; ​do
  ​info ​"Installing ​​${​app​}​​"
»if ​brew list $app > /dev/null; ​then
» ​info ​"​​${​app​}​​ is already installed"
»else
» ​brew install $app
»fi
  result=$?
 if​ [ $result -ne 0 ]; ​then
  ​error ​"Error: failed to install ​​${​app​}​​"
  exit 1
 else
  ​success ​"Installed ​​${​app​}​​"
 fi
 done

The apt-get command returned an exit status of 0 if the package was already installed, so a nonzero exit status would only happen if something else went wrong. Unfortunately, brew returns an exit status of 1 if the package is already installed, so you first check to see if the package is already available on the system using the brew list command. You can suppress its output since all you care about is its exit status. If the package is installed, you move on. If it’s not, you install it.

You can completely remove the Node.js section from the script now that you’ve added the nodejs package to the list of packages in the previous step. Find this section and comment it out or remove it:

»# 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

The rest of the script stays the same, except that macOS uses ~/.bash_profile by default, so you’ll modify that file instead. Change the references for the backups and change the output of the cat command:

»info ​"Setting up ~/bash_profile"
»
»if​ [ -f ~/.bash_profile ]; ​then
» ​oldprofile=~/.bash_profile.​${​datestamp​}
» info ​"Found existing ~/.bash_profile file. Backing up to ​​${​oldprofile​}​​."
» mv ~/.bash_profile ​${​oldprofile​}
»fi
»
»cat << ​'​EOF​' > ~/.bash_profile

Then change the check at the end of the file as well as the confirmation message:

 result=$?
 if​ [ $result -ne 0 ]; ​then
» error ​"Error: failed to create ~/.bash_profile"
  exit 1
 else
» success ​"Created ~/.bash_profile"
 fi
 
»success ​"Done! Run source ~/.bash_profile to apply changes."

You now have a script that lets you set up a Mac. Save the file, exit the editor, and run the script:

 $ ​​./mac_setup.sh
 Starting install at Sun Mar 3 20:13:31 CST 2019
 Homebrew installed
 Updating Package list
 ...
 Done! Run source ~/.bash_profile to apply changes.

You can go further than this with macOS. You can manipulate a ton of macOS settings through the terminal. For example, if you always want to show hidden files in Finder, you can add this line:

 defaults write com.apple.finder AppleShowAllFiles -bool YES

Since it’s a change to Finder’s settings, you’ll have to restart Finder, which you can do with this line:

 killall Finder

You can set a lot more options like this through a script, but you’ll have to find them on your own as Apple doesn’t maintain a master list. However, you can execute defaults read | less and read through its output for some clues.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.214.155