BASH From Scratch
BASH From Scratch
The basics
Creating an executable file
In order to write a script and execute it, the script file must have execution permission. So let's
create a file and give it execution permission
cd into a test directory and run
# create a new file called `script.sh`
touch script.sh
# add `execution` permission to the script to make it executable
chmod +x script.sh
the ./ is necessary to tell your terminal to look in the current directory and not the
default script directory.
now open that file in your favorite editor and let's get to learning the syntax.
The shebang!
TL;DR; add #!/usr/bin/env bash in the first line of your script file to mark that file as a
bash executable.
you could use #!/usr/bin/env sh for portability, but we are working specifically
with bash here.
printing stuff
to print things to the terminal, you can use echo or printf
for example lets make that script.sh, from above, print "hello world"
#!/usr/bin/env bash
echo hello world
example:
#!/usr/bin/env bash
printf "hello\nworld\n"
prints:
hello
world
Comments
Any line starting with # is a comment.
Declaring Variables
simple:
MY_VARIABLE="my value"
IMPORTANT: no space before or after =. Shell uses space as a delimiter for command
arguments.
Parameter Expansion
see this and this to learn more.
FAV_FRUIT="apples"
echo FAV_FRUIT
# prints `FAV_FRUIT`
while:
#!/usr/bin/env bash
FAV_FRUIT="apples"
echo $FAV_FRUIT
# prints `apples`
FAV_FRUIT="apples"
I_LIKE_BLANK="I like $FAV_FRUIT"
echo $I_LIKE_BLANK
Note: in all examples above, you can replace $ with ${variable name} for the
same effect. so $FAV_FRUIT is the same as ${FAV_FRUIT}
Second note: notice how we used $FAV_FRUIT inside of a string? that's string
templating for ya!
SHIRT_COLOR=""
COLOR="${SHIRT_COLOR:-red}"
echo $color
another example:
#!/usr/bin/env bash
DEFAULT_COLOR="red"
SHIRT_COLOR=""
MY_SHIRT_COLOR="My shirt color is ${SHIRT_COLOR:-$DEFAULT_COLOR}"
echo $MY_SHIRT_COLOR
DEFAULT_FRUIT="Apricot"
FRUIT=${FRUIT:-$DEFAULT_FRUIT}
echo "I Like $FRUIT"
If then
if [[ <some test> ]]; then
<commands>
fi
If else
if [[ <some test> ]]; then
<commands>
else
<other commands>
fi
If elseif else
if [[ <some test> ]]; then
<commands>
elif [[ <some test> ]]; then
<different commands>
else
<other commands>
fi
Tests
in the above <some test> can be replaced with a test condition. You can see all available test
conditions by typing man test in your terminal
Test Description
[[2 -gt 1]] 2 is greater than 1. counterpart -lt: less,than.
[[2 -eq 2]] 2 equals 2
[[3 -ge 3]] 3 is greater than,or equal to 3. counterpart -le: less than or equal
[[-n "hello"]] Length of "hello" is greater than 0
[[-z ""]] Length of "" is 0
[["apple"= String,"apple" equals String "apple". (-eq compares numbers while =
"apple"]] compares charachters)
[["apple"!=
"apple1"]] String,"apple" does no equal String "apple1"
Examples using wildcards:
- [[ "watermelon" = *"melon"* ]]: String "watermelon" contains "melon"
Since this article is about bash and not shell in general, we use bash's double square
brackets [[]]. Read this and this answer for more information.
The following table shows the commands and the outputs for above script:
Command Output
FRUIT="Apple" [Apple]: yay! your fruit has more than 4
./script.sh characters!
FRUIT="Fig" [Fig]: Unbelievable... your fruit has less
./script.sh than 4 characters...
FRUIT="Pear" A fruit with exactly 4 characters, how
./script.sh precious!
Note the parameter expansion ${#FRUIT} gets the characters length of FRUIT
echo $#
echo $#
shift 2
echo $#
shift
echo $#
echo $@
running ./script arg1 arg2 arg3 arg4 arg5 (total 5 arguments) prints:
5
3
2
arg4 arg5
huh? so shift 2 basically removed arg1 and arg2 then we executed shift again and
removed arg3. Note that $@ is another builtin that prints the arguments.
Now is a good time to tell you how to access arguments sort-of like an array. You can use $1 for
first arg, $2 for second arg, $3 for third arg.. and so on.
example:
#!/usr/bin/env bash
echo $1
echo $3
echo $2
running:
./script arg1 arg2 arg3
prints:
arg1
arg3
arg2
how do we use all that info? take a look at this:
while [[ $# -gt 0 ]]; do
key="$1"
echo $key
shift
done
It might have clicked by now! Using the while loop above, with exit condition [[ $# -gt
0 ]] and shift to reduce $# we can loop over all passed args!
let's make it better with a case statement, let's start simple and parse name and height only
#!/usr/bin/env bash
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
# parse name arg
-n|--name)
NAME="$2" # the value is right after the key
shift # past argument (`--name`)
shift # past value
;;
# parse height arg
-h|--height)
HEIGHT="$2"
shift # past argument
shift # past value
;;
# unknown option
*)
echo "Unknown argument: $key"
shift # past argument
;;
esac
done
Notes:
Rut it:
./run.sh --name Ahmed --height 6ft
or shorter
./run.sh -n Ahmed -h 6ft
prints:
NAME: Ahmed
HEIGHT: 6ft
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
# parse name arg
-n|--name)
NAME="$2" # the value is right after the key
shift # past argument (`--name`)
shift # past value
;;
# parse height arg
-h|--height)
HEIGHT="$2"
shift # past argument
shift # past value
;;
# parse user arg
-u|--user)
USER="$2"
shift # past argument
shift # past value
;;
# parse occupation argument
-o|--occupation)
OCCUPATION="$2"
shift # past argument
shift # past value
;;
# parse code quality argument
-u|--username)
USERNAME="$2"
shift # past argument
shift # past value
;;
# unknown option
*)
echo "Unknown argument: $key"
shift # past argument
;;
esac
done
or
./run.sh -n ahmed -h 6ft -o "professional procrastinator" -u coolestGuyEver23
result:
Hello, my name is ahmed. I'm 6ft tall.
I work as a professional procrastinator and my username is coolestGuyEver23
Note, remember Using a default value section above? you can use that to add default
values in case a param was not passed.
Evaluating Commands
you can assemble a a command as a string then use eval to execute it. For example, remember that
long maven command from the beginning of the post?
mvn clean install -PautoInstallPackage -Dcrx.user=user1 -Dcrx.password=pass1
PROFILE="${PROFILE:-autoInstallPackage}"
USER="${USER:-user1}"
PASSWORD="${PASSWORD:-pass1}"
eval $COMMAND
Thanks @lietux
Functions
Functions are simple and straightforward. Here is a function that prints the first argument passed to
it:
#!/usr/bin/env bash
printFirstOnly(){
echo $1
}
# execute it
printFirstOnly hello world
Function arguments are accessed with the $N variable where N is the argument number.
I like to use functions to pretty print things:
pretty() {
printf "***\n$1\n***\n"
}
prints:
***
hi there
***
Conclusion
If you've reached this, then I've done something right :) I'd love to hear your feedback on the post in
general. Was it organized? scattered? did it make sense? was it helpful? and what can I do to
improve it?
DISCUSS (19)
Ahmed Musallam
Developer, Father and Husband. Currently working in the Adobe Experience Manager space.
REPLY
Brad Robertson
Excellent article, and very helpful comment discussions as well! The concise descriptions with
supporting examples makes it a quick and effective read.
Anyway, wish I had read an article like this 9 months ago-- the information about shift, loops and
functions would have gone a long way in the scripts I've been writing to provision our Ubuntu
Vagrant VMs for use as localhost web dev environments.
Looking forward to applying some of this. Thx for posting!
REPLY
Janne "Lietu" Enberg
Just as a quick comment - you should check out actual provisioning tools, such as SaltStack and
Ansible if you want reproducible environments (or event driven infrastructure actions, secret
management, easily distributable remote command execution, etc. etc.)
REPLY
Ahmed Musallam
Author
Glad you liked it! Honestly, I wrote it after struggling with bash for a few weeks. I was copying and
pasting things I did not understand exactly how they worked.. mainly that while-shift loop.
REPLY
Janne "Lietu" Enberg
Instead of referring to some online articles on test conditions, you can just check out man test.
Conditions and most other such things are good to shorthand a bit, and if you're planning on using
BASH use [[ ... ]] instead of [ ... ] as they also function differently. Stick to one style
for predictability. serverfault.com/a/52050
Slightly compacted formats
if [[ "$FOO" == "1" ]]; then
...
else
...
fi
for i in 1 2 3 4; do
echo $i
done
Also shellcheck.net/ is an excellent resource to use to check your scripts for common bugs etc.
There's even a decent unit testing framework for BASH scripts nowadays: github.com/sstephenson/
bats
REPLY
Ahmed Musallam
Author
This is fantastic! I’m still new to bash scripting :) but will include this in the article. Great
feedback!!
REPLY
Ahmed Musallam
I have updated the post, thank you again for the feedback!
REPLY
Janne "Lietu" Enberg
Oh, another thing I just noticed. Generally people like to think "eval is evil", and while imo eval
has it's place, especially in BASH, you can avoid using eval with various tricks.
JAVA=$(which java)
APP=$1
EXTRA=""
if [[ "$NEED_EXTRA" == "1" ]]; then
EXTRA="--extra arg"
fi