February 22nd, 2019

Exploring Bash Scripting



Command Line Scripting

Back in the summer I wrote an article about Batch scripting on Windows. In today's article, I'm looking at Bash scripting. I've used Bash quite a bit recently. At work I've used Bash scripts to automate the conversion of Subversion repositories to Git. In my personal work I've used Bash scripts to assist setting up AWS infrastructure. The rest of this article explores basic Bash features and how they compare to Batch.

Bash stands for Bourne-Again SHell, and is derived from the original Bourne shell1. The Bourne shell was distributed with Unix operating systems starting in 1979, written by Stephen Bourne in Bell Labs2. Bash began replacing the original Bourne shell in 19893.

Bash is a command line scripting language originally used on Unix-like operating systems. Nowadays, Bash is found on most major operating systems, including Linux distributions, MacOS, and Windows 10. Bash commands are written from a terminal or collected into a script. Today I'm exploring the creation of Bash scripts.

Bash scripts have the file extension .sh. A typical bash file begins with a shebang line pointing to the bash interpreter. Script input arguments are handled with the $<argument-number> syntax.

#!/usr/bin/env bash echo "The first argument passed: $1"

Assigning values to variables is very simple in Bash. It's equally simple to use these variables in future commands.

Name="Andrew Jarombek" echo ${Name}

The ${...} syntax is called a parameter expansion. Parameter expansions are used to substitute variables or expressions with their values. Therefore echo ${Name} is substituted with echo "Andrew Jarombek".

Interestingly Bash variables are untyped (unlike most programming languages I use). This has interesting consequences which I'll explore in a future post. Bash variables contain character strings which are used in different ways depending on the context4.

Parameter expansions can manipulate existing variables. The following examples alter the Name variable. In this context Name is a string.

Name="Andrew Jarombek" # Extract the characters after index 7 LastName=${Name:7} echo ${LastName} # "Jarombek" # Extract the characters from index 0 to index 6 FirstName=${Name:0:6} echo ${FirstName} # "Andrew" # Replace Jarombek with Jarbek AltName=${Name/Jarombek/Jarbek} echo ${AltName} # "Andrew Jarbek"

In some contexts, variables can represent integers. In the following example, an arithmetic substitution is used to create an integer context.

Age=23 AgeInTenYears=$(($Age + 10)) echo ${AgeInTenYears} # 33

The $((...)) syntax creates an arithmetic substitution5. There are other ways to create integer contexts for performing arithmetic, including backticks and the let construct6.

Bash also has an easy construct for creating functions. This is much improved from the hacky labeling and call statements in Batch.

Age=23 func() { Age=24 echo ${Age} } # Prints: 24 func

One interesting aspect of Bash functions is that variables defined in functions leak into the global scope. Luckily if this is not what you intend, there are keywords for altering this behavior. The local keyword defines a variable scoped to the function.

func2() { Global="Global Variable" local Local="Local Variable" # Both global and local print echo "${Global}, ${Local}" } func2 # Only global prints echo "${Global}, ${Local}"
Global Variable, Local Variable Global Variable,

As you can see, Local is not accessible outside the function.

Arrays are also available in Bash. They are simpler than their Batch equivalents because complicated delayed expansion logic isn't needed. Bash also provides associative arrays in newer versions.

# Create an array and assign values to indices Towns=() Towns[0]=Greenwich Towns[1]="New Canaan" Towns[2]=Darien Towns[3]=Wilton Towns[4]=Ridgefield echo ${Towns[0]} # Bash also has associative arrays (These only work in Bash Version 4) declare -A SkiLocations=([sat]="Catamount" [sun]="Jiminy Peak") echo ${SkiLocations[sat]} # Create an array of integers NumberList=(1 2 3 4 5) # Get all the items from NumberList echo ${NumberList[*]}
Greenwich Catamount 1 2 3 4 5

Bash also has simple for and if statements:

# Accumulate all of the strings in the town list for i in ${NumberList[*]} do AllTowns="${AllTowns} ${Towns[$((i - 1))]}" done echo ${AllTowns} # Accumulate some of the strings in the town list for i in 2 4 do SomeTowns="${SomeTowns} ${Towns[$((i - 1))]}" done echo ${SomeTowns} # Create a unix timestamp of a date on MacOS DateWritten=$(date -j -f "%F" 2019-01-28 +"%s") MonthsEnd=$(date -j -f "%F" 2019-01-31 +"%s") if [ ${DateWritten} -lt ${MonthsEnd} ] then echo "Date is before Jan. 31st, 2019" else echo "Date is equal to or after Jan. 31st, 2019" fi
Greenwich New Canaan Darien Wilton Ridgefield New Canaan Wilton Date is before Jan. 31st, 2019

I really enjoy working with Bash due to its simplicity. It's commands are short and easy to use, with common utilities such as echo, cat, grep, head, and tail being good examples. I much prefer it over Batch scripting. You can check out more Bash code on GitHub.

[1] Arnold Robbins, Bash Pocket Reference, 2nd ed (Beijing: O'Reilly, 2016), 2

[2] "Bourne shell",

[3] "Bash (Unix shell)",

[4] "Bash Variables Are Untyped",

[5] Simpson., 16

[6] "Arithmetic Expansion",