dark mode light mode Search Menu
Search

Bash Scripting with CLI

JD Lasica on Flickr

So far in this series we’ve spent some time talking about how to use the command line to do things like how to chain commands together and do basic file system tasks. Now we’re ready to talk about writing programs for the command line!

If you have a Mac or a Raspberry Pi, look for the Terminal application. If you’re on Windows 10, you’ll want to follow the steps in the following article on how to set up a terminal with bash on Windows!

Now, this isn’t the old-school command line for Windows. That’s a program called Powershell. Powershell is a bit different than anything else, so we’ll cover it separately in the future.

First, let’s throw out a couple of definitions that are relevant. The program that you’re interacting with when you’re on the command line is a shell. The shell is what interprets the commands you type and turns them into commands the operating system understands.

There’s quite a few shells that exist but the one that is used by default on Linux and OSX is bash. So bash stands for “Bourne Again SHell”, and it’s named this because it’s a revamp of an even older shell program called the “Bourne shell” that dates back to the 1970s. By comparison, bash is a spry 29 years old.

A cool thing about shells like bash is that they have their own programming language built in. For example, here’s a tiny little script to play entire directories of music from the Raspberry Pi I have hooked up to some speakers:

#!/bin/bash
dirName=$1
for song in "$dirName"/*.mp3
do
	omxplayer "$song"
done

This is small and simple but all the elements of bash scripting are here: variables, loops, command line arguments, and calling other programs. You might be able to sound out this example.

You’re creating some kind of variable that stores the directory name. Then for every song, which is a file in the directory that ends in .mp3, run the program omxplayer — which is the main music and video program for the Raspberry pi—on that song to play it.

Now, let’s cover those topics by going through a few examples you can try out for yourself..
The very first thing we’ll cover is reading in command line arguments. By “command line arguments” I mean something like the file and directory names in:

mv fatdog.mp3 dogthemedsongs/

The command is mv and fatdog.mp3 and dogthemedsongs/. are both arguments.

If you’re writing a bash script you can read the command line arguments as $1, $2, $3, etc. for the first, second, third, and other arguments. To test this out, let’s do a little bash script version of “Hello World”. We’ll use the nano editor and type a few commands:

nano hello.sh

In the nano editor, type this text:

#!/bin/bash
echo “Hello” $1

Next, press the Control key and type X to exit nano. Type Y and press enter to save the hello.sh file.

Now we need to make the file executable by the bash command line software, to tell the operating system “yes, I want to run this as a program”:

chmod +x hello.sh

And, finally, let’s feed our little program a word:

./hello.sh world

Your command line software should output “Hello world” If you try this, what happens?

./hello.sh fred

In the hello.sh file, $1 is the value we pass into the script. The value is added to “Hello” with a space between that word and the $1.

Now, what the heck is this #!/bin/bash business that’s at the top of the script? That makes sure that your operating system knows to treat this program as a bash script and feed the text of the code into the bash program to run it.

Below that, we use the echo command which prints whatever string you give it. In our hello world program, we give it “Hello” and the first command line argument.

[…]

Okay, so that’s not very interesting. What else could we do? Well, a really easy program would be a “backup” program that makes backup copies of all the files in whatever directories you give it. Open a file called createbackup.sh and type in the following:

#!/bin/bash
 
for dir in $@
do
    for file in $dir/*
    do
	cp $file ${file}_backup
    done
done

Let’s talk out what’s happening in this program.

The first thing to discuss is how for loops work in bash. If you’ve played around with Python before, this kind of for … in loop might look familiar. It’s how you do something, you iterate over every element in a list of data. Every go-round of the loop it puts the next element on the list in the loop variable, which is dir for the outer loop.

What’s the list in the first loop? It’s a special variable called @ which is a list of all the command line arguments you gave the program when you ran it. In our case, it’ll be all the directories you want to backup. In the second loop, the list you’re iterating over is every file in the directory stored in the variable dir. To get the value stored in the variable dir you need to put a $ in front of the variable name. So if you ran this program like:

./createbackup.sh my_stories

The first time the loop runs dir is going to have the value my_stories. This means that the expression $dir/* will have the value my_stories/*, which we know from our prior experiments on the command line means “every filename that is in the directory my_stories”. In this example, the inner loop will go through every file in the directory my_stories and then copy that file to a new file with the same name but _backup stuck on the end.

So if there was a file called TheFattestDog.txt in the directory my_stories this program would copy it to a file called TheFattestDog.txt_backup. Why do we need those curly-brackets around ${file}, though? Bash needs to distinguish between a variable called file and a variable called file_backup in this case, so the curly brackets are a way of telling bash “no, the variable is called file and I want to stick ‘backup’ to the end of whatever string is stored in file.”

Okay, now you have a program that can make backups. What about making a program that restores the old backup in the directory? We can do that by just some slight modifications of our previous code!

#!/bin/bash
 
for dir in $@
do
    for file in $dir/*_backup
    do
	cp $file ${file%_backup}
	rm $file
    done
done

The big thing that’s different is the line that says ${file%_backup}. Bash has a lot of built in things to help deal with file name manipulation: it’s a programming language for administering computers, after all! This is one of the more unique ones. What ${name%text} means to bash is “take the string stored in the name variable and remove text from the end of the string.” So in our program it removes the _backup from filename. In fact, the %operator is a lot more powerful than that: you can cut the part of the file name that matches any regular expression you give it, if you happen to have learned what those are.

That’s our first intro to making a real command line utility as a bash script! It’s actually pretty useful, though no replacement for real version control like git.

Next time, we’ll be talking more about bash scripting, the cron utility, and more about secure shell to control other computers and administer them. Until then, try playing around on the command line and coming up with tasks you might want to automate or fun things to try. There’s so much you can do in bash with just what we’ve covered so far.

Learn More

Bash

The command line has a programming language built into it so you can write programs that behave like the built in commands.
https://ryanstutorials.net/bash-scripting-tutorial/

A long-form tutorial on bash scripting

http://tldp.org/LDP/abs/html/

Another longer guide to bash scripting

https://guide.bash.academy/

An alternative to bash

https://rootnroll.com/d/fish-shell/