Development

A Beginner’s Guide to Writing Bash & Shell Scripts

Optimizing your development workflow is one of the most important things a developer can do. Being able to leverage your tools quickly and efficiently lets you spend your time working on actual development–instead of fighting your tools or doing boring, repetitive things.

A few months ago, I wrote a blog post about getting set up with the command line and starting to use some of the tools that are available. If you have not read that post yet, I’d recommend doing so, or at least glancing through it before getting into this post.

The main reason I built vv was because I was tired of manually setting up sites on VVV for every new project. Most features inside of vv were built to automate something that I or someone else did manually with every new project. This mindset is important to get into when developing; it’s why we learn keyboard shortcuts, why we use things like theme frameworks, and even why we use WordPress. Doing things over and over again is boring.

writing bash scripts, writing shell scripts, writing bash and shell scripts, brad parbs, WebDevStudios, web development tutorials, web development, development tips, programmer tips, programming tips, programming, programming tutorials, speed up your development, speed up my development, learn to develop faster

Getting Started

To get started writing our first Bash script, we simply need to make a file in a position where our shell can locate it. We can easily do this by checking which locations are in the $PATH environmental variable, as that’s where our shell looks for executable files. To view this, simply open up a terminal and do echo $PATH. This should give you an output that looks something like: /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/go/bin

This is a list of folders, separated by ‘:‘s. All of these folders are locations where your shell looks for executable scripts when you run them by name. So placing a ‘hello-world’ script in any of these locations would let you run hello-world in your terminal.

When writing scripts that you want to use in more than one place on your computer, you’ll want to either save the script into one of those folders or you’ll want to add a new folder to your $PATH.

Bash Basics

Writing our first Bash script

To begin writing our first Bash script, we need to make a new file and add a special line to it. The ‘shebang’ is always the first line in a Bash script, and it defines what shell should run this script. This is usually always going to be Bash, but in some cases in might be something else.

#! /bin/bash is usually what is used here, but I recommend using #! /usr/bin/env bash because it is better for portability. /bin/Bash is the exact location of the Bash executable, whereas /usr/bin/env bash is grabbing the environmental variable that contains Bash. This means Bash can be located in a different location.

Now that we’ve added this this first line, you’ll want to save your file with a name. You can either have no extension, or a .sh extension. If you save it, and go into the directory where it is saved, you can run ‘./your-file-name’ and it should execute the script. If you get a permission denied error, you’ll need to run ‘chmod +x your-file-name‘ to allow it to be executable.

Earlier, we talked about the $PATH. You can store Bash scripts in any of those locations, or add a new location, and then you’ll be able to run your script anywhere on your system by just running ‘your-file-name‘. I recommend this for any scripts that aren’t project-specific.

Basic Bash

The way Bash works is pretty simple. A Bash script is set of commands. Anything you run on the command line, you’ll be able to run in a Bash script. Most Bash scripts are structured as a list of commands, usually with some helper functions, and some checks on whether or not to run different things.

Let’s start writing a very basic ‘Hello, World,’ script, as is the norm for learning a new language. We’ll start off with a shebang, and then it’s simply echo "Hello, World". So your script would look like this:

   #!/usr/bin/env bash
   
   echo "Hello, World"

Now that we’ve written our required ‘Hello, World,’ program, let’s get into something that can be a but more useful. First, we want to understand some of the flow and constructs in Bash programming.

Functions

Like most languages, bash programs do have functions. You can define these one of two ways. You can use the ‘function’ keyword, or you can leave it off. A function looks like this:

    function my_cool_function() {
        # Some code here
        echo "Hello!"
    }
    
    #calling our function:
    my_cool_function

One thing you’ll notice is that I left off the () and the semicolon when calling my function. In Bash, semicolons at the end of lines are optional. I tend to not use them, but others prefer having them in there. The same with the parentheses–I choose to leave them off if there are no arguments, for ease of readability.

One important note about functions: In Bash, you must define your function before calling it. Bash scripts are executed in the order of the lines in the file, so calling a function before you define it will cause an error.

In a bit, we’ll talk about passing arguments to functions, as well as returning values from your functions.

Control flow

One of the things that really tripped me up when I started learning Bash was if/else statements. These are treated a bit oddly in Bash.

Your basic if / else looks like this:

    if [[ "$value" == "check" ]]; then
        # do something
    else
        # do something else
    fi

Seems pretty straightforward, but the two things you’ll want to note are the fi at the end, signifying the end of the if statement, as well as the double [[ inside the check. The double brackets is a bit confusing, so for simplicity sake, just remember that using two brackets instead of one is safer and has a few nice features.

Variables

Now that we’ve talked about if / else statements, let’s chat about variables for a minute. Variables in Bash need some care when you work with them.

First, assigning a basic variable, and echoing it out:

    my_var="my text"
    
    echo "$my_var"

You’ll notice when we echoed it (and anytime we’d use it for anything) that there is a dollar sign in front of it. You don’t need that when you’re assigning the variable though. The next important thing is that we put quotes around it when we used it.

This is something that most people don’t do, but it is very important when you start accepting user input or deal with filenames/file paths. The quoting prevents what’s called “word splitting.” If you were to set the variable to be a filename, and that file name happened to have a space in it, Bash would assume that space meant you were passing in two arguments, rather than one. Most of the time you want to treat your variable as exactly what it is, so its a good rule of thumb to always put your variables in quotes when using them.

Passing in values

When calling your script from the command line, you can easily pass in values.

    $./myscript.sh val1 val2

These arguments are quite easy to read in Bash. They automatically get assigned to the variables $1, $2, $3 for each passed in argument. You can also use the handy $@ variable to access everything.

Running this:

   #!/usr/bin/env bash
    echo "$1"
    echo "$2"
    echo "$@"

…as this:

    $./myscript.sh val1 val2

…would output this:

val1
val2
val1 val2

You can also access arguments passed into functions inside of your script the same way, as well as passing in values inside your script the same way.

Reading variables from a prompt

In your scripts, you most likely will at some point want to prompt for some information. Be it a file name, path, times to run, etc. This is quite simple.

    read -r variable_name

This will put the results of the prompt into the variable.

Generally, you’ll want to echo out your question before it, like so:

    echo "What is your favorite color?"
    read -r color
    echo "Your favorite color is $color."

Subshells

Inside of your Bash script, you’ll probably want to return values in your functions. In most programming languages, this is as simple as doing ‘return $x.’ For a lot of built-in shell commands, we probably want to get what they normally output. Saving the current directory we are in, for example. The way we do it is by echoing our return value inside of our function, and then calling that function inside of what is called a subshell, and capturing the output.

This is handy, as it lets us use any available bash function to return a value. For example, we can caputure the output of lsby calling it in a subshell.

    directories="$(ls)"

This would set the variable $directories to contain a listing of files in the current directory. Behind the scenes, a subshell executes the code in a different processing thread, so all output is captured, and not printed to the screen.

Helpful tools for writing scripts

Shellcheck

My favorite tool for Bash scripting is shellcheck. Available at http://shellcheck.net, as well as a downloadable command for every platform from Github. Shellcheck is basically a linter for scripts. Running your script through it will surface everything from parse errors, syntax mistakes, and a whole lot more.

The best part of Shellcheck is that not only does it tell you what is wrong, but it will also clue you in on why it is wrong and what that means. For example, if we had this code, which accepts one argument, switches to that directory, and then echoes out what directory we are in.

   #! /usr/bin/env bash
   
    cd $1
    $path = $( pwd )
    echo $path

Running this through Shellcheck gives us these errors:

$ shellcheck myscript
 
Line 3:
        cd $1
        ^-- SC2164: Use cd ... || exit in case cd fails.
           ^-- SC2086: Double quote to prevent globbing and word splitting.
 
Line 4:
        $path = $( pwd )
         ^-- SC1066: Don't use $ on the left side of assignments.
              ^-- SC1068: Don't put spaces around the = in assignments.
 
Line 5:
        echo $path
             ^-- SC2086: Double quote to prevent globbing and word splitting.

As you can see, there are a lot of mistakes here, but they all are fixable in a few seconds. Following those directions, we end up with this block of code, which throws no Shellcheck errors.

    #! /usr/bin/env bash
   
    cd "$1" || exit
    path=$( pwd )
    echo "$path"    

As you noticed in the above error example, the errors started with “SC####”. All errors that Shellcheck shows have relevant pages with information on Github. For example, SC2086 will explain the rationale behind why you should do it the way they suggest.

Explain Shell

ExplainShell.com is another great resource. By pasting in a full Shell command with flags and options, it will parse out what everything means and explain it. For example, this command will list all files in the directory:

    find . -type f -print0

Running this through ExplainShell will out what each of those options / arguments do.

explainshell

Writing your first script

Although not everything in Bash was covered in this post, you should have enough knowledge now to start making a basic script. Once you start writing scripts, you’ll learn more and more about the language, as you try to automate more and more tasks.

Let’s write a script together!

One thing I have to do sometimes when getting spun up on a new project is running a few dependency installation scripts. Most of the time it is npm install, bower install, and maybe composer install. Let’s write a script to do that for us:

    #! /usr/bin/env bash
    
    echo "Installing all the things..."
        
    echo "Installing npm requirements..."
    npm install

    echo "Installing bower packages..."
    bower install
    
    echo "Installing composer packages..."
    composer install
    
    echo "Finished installing."

This script above will run all the commands we want. However, maybe there are some projects where you might not use composer, so rather than just attempting to run the install commands, let’s first check to see if the relevant .json files exist first.

    #! /usr/bin/env bash
    
    echo "Installing all the things..."
    
    if [ -f "package.json" ]; then
        echo "Installing npm requirements..."
        npm install
    fi
    
    if [ -f ".bowerrc" ]; then
        echo "Installing bower packages..."
        bower install
    fi
    
    if [ -f "composer.json" ]; then
        echo "Installing composer packages..."
        composer install
    fi
    
    echo "Finished installing."

That’s much better. Now, if there isn’t anything to install, we won’t even try to. The last step is saving this somewhere into our $PATH folder listing and then we can run it in a project directory.

That was a pretty simple script to write, and now we can easily install all our project dependencies very easily. Another good change might be to also allow for passing in arguments that will toggle between an install and an update. I’ll leave that as an exercise for you.

There are a ton of things you can do to automate your workflow. Hopefully with this very basic knowledge of Bash, you’ll be able to start making your life a bit easier.

Comments

2 thoughts on “A Beginner’s Guide to Writing Bash & Shell Scripts

  1. Hey, Brad!
    Thanks for writing this awesome tutorial. I build shell scripts almost every other week. Yet, didn’t know about ExplainShell. It looks like an incredible resource.

Have a comment?

Your email address will not be published. Required fields are marked *

accessibilityadminaggregationanchorarrow-rightattach-iconbackupsblogbookmarksbuddypresscachingcalendarcaret-downcartunifiedcouponcrediblecredit-cardcustommigrationdesigndevecomfriendsgallerygoodgroupsgrowthhostingideasinternationalizationiphoneloyaltymailmaphealthmessagingArtboard 1migrationsmultiple-sourcesmultisitenewsnotificationsperformancephonepluginprofilesresearcharrowscalablescrapingsecuresecureseosharearrowarrowsourcestreamsupporttwitchunifiedupdatesvaultwebsitewordpress