Shell Scripting

Shell scripting is a way to automate tasks by writing a series of commands in a text file that the shell (command-line interpreter) can execute. In a Unix-like environment, the shell (e.g., Bash) reads and runs each command in the script sequentially. Shell scripts are commonly used for system administration, task automation, and running batches of commands. In this lecture, we will cover the fundamentals of shell scripting using Bash (the Bourne Again Shell), including working with environment variables, basic Bash syntax for variables, conditional statements, loops, functions, and handling script input parameters.

Shebang (#!)

The shebang is the character sequence #! at the very top of a script file. It indicates which interpreter should execute the script. When you run a script directly (for example, by making it executable and running ./myscript.sh), the operating system looks at the first line. If it begins with #!, the rest of that line is treated as the path to the interpreter program. The kernel will then launch that interpreter and pass the script file to it as an argument. In effect, running the script becomes equivalent to running the interpreter with the script’s filename. For example, if a script file myscript.sh starts with #!/bin/bash, executing ./myscript.sh is the same as executing /bin/bash myscript.sh.

The shebang line’s purpose is to ensure the correct interpreter is used for the script’s content. In Bash scripts, you’ll often see #!/bin/bash. In Python scripts, you might see #!/usr/bin/python3 or use the env utility: #!/usr/bin/env python3. Using /usr/bin/env is a common trick to make scripts more portable – it finds the interpreter in the system’s PATH rather than hard-coding a specific location. For instance, a Python script with #!/usr/bin/env python3 will work on systems where Python 3 is installed in different directories, as env will locate the first python3 in the PATH. The shebang line is ignored by the interpreter itself (treated as a comment in Bash or Python), but it’s crucial for the OS to know how to run the script.

#!/usr/bin/env bash
# Example Bash script using shebang
echo "This Bash script is running under the Bash interpreter!"
#!/usr/bin/env python3
# Example Python script using shebang
print("This Python script is running under the Python interpreter!")

In practice, including a shebang and making the script executable (chmod +x myscript.sh) allows you to run it directly. Without a shebang, you would have to run the script by explicitly calling the interpreter (e.g., bash myscript.sh or python myscript.py). The shebang simply streamlines script execution by linking the file to the appropriate interpreter.

Accessing Environmental Variables

Environment variables are global name-value pairs in the operating system that can affect the behavior of processes. They typically store configuration settings or system information (for example, HOME for your home directory, PATH for executable search paths, USER for the current username, etc.). These variables are accessible to programs and scripts that run in that environment. In a shell scripting context, environment variables are inherited by child processes, including other programs or scripts that you run from your shell.

In Bash, accessing an environment variable is as simple as using $VAR_NAME. For example, $HOME will expand to the path of your home directory, and $USER will expand to your username. You can use the echo command or other commands to utilize these values:

#!/bin/bash
echo "Current user: $USER"
echo "Home directory: $HOME"
echo "Path locations: $PATH"

If a variable is not already in the environment, Bash will return an empty string for it. You can create or modify environment variables in Bash by assigning to them and exporting them with export, so that subprocesses can see them. For instance, export MYVAR="hello" makes MYVAR available to any commands or scripts executed after that in the same session.

In Python, environment variables can be accessed through the os module. The os.environ dictionary contains all environment variables accessible to the program, or you can use os.getenv("VAR_NAME") to get a specific variable’s value. If the variable is not set, os.getenv will return None (or a default value if you provide one). Here’s an example of accessing environment variables in Python:

import os
user = os.getenv("USER")
home = os.environ.get("HOME")
print(f"Current user: {user}")
print(f"Home directory: {home}")

Running this Python snippet would output your username and home directory path, analogous to the Bash example above. Remember that environment variables must be set in the environment (for example, in the shell or parent process) before the script runs, otherwise you’ll just get an empty value or a default. In summary, both Bash and Python provide simple ways to retrieve environment variables: Bash uses $VARIABLE syntax directly in the shell, and Python uses the os module’s environment access (like os.getenv or os.environ).

Bash Scripting Basics

Now let’s dive into the core concepts of Bash scripting. We will look at how to use variables, conditional statements, loops, functions, and how to handle input parameters in a Bash script. Understanding these basics will enable you to write useful shell scripts for a variety of tasks.

Assigning Variables

In Bash, you can create and assign values to variables very simply. Unlike some languages, you do not need to declare a type – Bash treats all variables as strings, but you can manipulate them as numbers in arithmetic contexts. To assign a value to a variable, use the syntax NAME=value with no spaces around the equals sign. For example:

#!/bin/bash
# Assigning variables
greeting="Hello"
name="Alice"
echo "$greeting, $name!"   # This will output: Hello, Alice!

Notice that when assigning, we do not use a $ before the variable name, but when retrieving or using the value, we prefix the name with $ (like $greeting or $name). Variable names by convention use uppercase for environment variables and lowercase for local script variables, but this is just a style guideline. If a variable’s value contains spaces or special characters, you should quote the value (e.g., message="Hello world") to ensure it is interpreted as a single value.

Bash variables are essentially untyped – they hold text. However, if the text is a number, you can treat it as such in arithmetic contexts. Bash provides ways to do arithmetic, such as the $(( )) expansion or the expr command. For example:

x=5
y=3
# Arithmetic expansion to compute x+y:
sum=$(( x + y ))
echo "Sum: $sum"          # Outputs: Sum: 8

In this example, $(( x + y )) computes the arithmetic sum of x and y. We store that result in sum. You could also use let, as in let sum=x+y, or in an if or loop condition you might use the double-parentheses notation. For most uses, $(( )) is a convenient way to do integer math in scripts.

Conditional Statements (if/elif/else)

Shell scripts often need to make decisions and execute commands conditionally. Bash uses an if ... elif ... else ... fi structure for conditional execution. The general syntax is:

if condition; then
    # commands if condition is true
elif other_condition; then
    # commands if the first condition was false, but this condition is true
else
    # commands if none of the above conditions are true
fi

The if keyword is followed by a condition and a semicolon (or a newline), then then. The block of commands to execute if the condition is true comes next. An optional elif (else-if) can follow to check another condition if the previous if was false. You can have multiple elif branches. Finally, an optional else covers the “none of the above” case. The entire if structure is closed with fi (which is “if” backwards).

The condition in an if statement is usually a command that returns an exit status (0 for true/success, non-zero for false/failure). In practice, this is often the [ ... ] (test) command or the double-bracket [[ ... ]] construct for evaluations. For example, you can use [ "$var1" = "$var2" ] to test string equality and [ $a -gt 10 ] to test numeric comparisons.

You can also combine conditions. One way is using the shell operators && (AND) and || (OR) between separate test commands. For instance, if [ $x -gt 0 ] && [ $x -lt 10 ]; then ... fi will execute the then-block only if both conditions are true. Another way is using [[ ... && ... ]] in a double-bracket test, which allows && and || inside. For simplicity, we’ll use the [ ] style and logical operators as separate commands.

Here’s an example of a conditional in a script that greets a particular user differently and uses string comparisons:

#!/bin/bash
name="Bob"
if [ "$name" = "Alice" ]; then
    echo "Hello, Alice!"
elif [ "$name" = "Bob" ]; then
    echo "Hi Bob, long time no see."
else
    echo "Hello, $name."
fi

If name is "Alice", the first branch executes; if it’s "Bob", the elif branch executes; otherwise the else runs. Note the use of quotes around $name in the test – this is good practice to handle cases where the variable might be empty or contain spaces.

Now, an example with numeric conditions and a logical combination. Suppose we want to check if someone is eligible to vote given their age and citizenship status:

age=20
citizen="yes"
if [ $age -ge 18 ] && [ "$citizen" = "yes" ]; then
    echo "You are eligible to vote."
else
    echo "You are not eligible to vote."
fi

In this snippet, the if condition uses [ $age -ge 18 ] (age is 18 or older) and [ "$citizen" = "yes" ]. Only if both are true will it print the eligible message. This demonstrates using a numeric comparison and a string comparison together with a logical AND.

Bash also offers an arithmetic condition check using double parentheses. For example, if (( x % 2 == 0 )); then ... is a valid way to test if x is even. Inside (( )), you can use C-like operators without needing the $ on variable names. This is handy for purely numeric conditions.

Quiz: Which of the following conditions will correctly check that the variable count is equal to 5 (assuming count is an integer)?




Loops

Loops allow your script to execute a block of commands repeatedly. Bash primarily offers for loops and while loops (and also until loops, which are like the opposite of while). We’ll focus on for and while, which are the most common.

While Loops: A while loop continues to execute as long as its condition remains true. It has the form:

while condition; do
    # commands
done

Just like with if, the condition in a while loop is usually a test command that returns an exit status. The loop will run the body, then check the condition again, and repeat until the condition returns false. Here’s an example of a while loop that counts from 1 to 5:

#!/bin/bash
count=1
while [ $count -le 5 ]; do
    echo "Count is $count"
    count=$(( count + 1 ))
done
echo "Done! Final count is $count"

This script initializes count to 1. The while condition [ $count -le 5 ] is true as long as count is 5 or less. Inside the loop, we print the current count and then increment count by 1. Once count becomes 6, the condition fails and the loop exits, then the script prints “Done!”.

For Loops (Iterating over a list): Bash for loops often iterate over a list of items. The syntax is:

for var in list; do
    # commands using $var
done

The list can be a series of values separated by spaces, literal values, or generated using brace expansion (like {1..10}) or command substitution (like $(ls *.txt)). For each item in the list, the variable var takes that value and the loop body executes once.

Example of iterating over a fixed list of strings:

#!/bin/bash
for color in red green blue; do
    echo "Color: $color"
done
echo "All colors printed."

You can replace the list with a brace expansion to iterate over numbers. For example:

for i in {1..5}; do
    echo "Number $i"
done

You can also loop over a sequence of numbers using the seq command (e.g., for i in $(seq 1 5)) or a C-style loop:

for (( i=1; i<=5; i++ )); do
    echo "Number $i"
done

Inside loops, you can use break to exit the loop entirely, or continue to skip to the next iteration.

Functions

Functions in Bash allow you to group a set of commands and reuse them. A function is like a mini-script within a script – it has its own parameter list and can be called multiple times. Defining a function in Bash is straightforward:

function_name() {
    commands
}
# or equivalently:
function function_name {
    commands
}

The first style is more commonly used. You simply write the name of the function followed by () and then a block of commands in braces { }. By convention, we define functions at the top of the script (before they are used), though Bash will let you define them anywhere as long as they are defined before you call them.

Once defined, you call a function by simply using its name like a command. You can also pass arguments to a function; inside the function, those are accessed as $1, $2, etc. In fact, inside the function, the positional parameter $1 refers to the function’s first argument, shadowing the script’s own $1 while in that function’s scope.

Here’s a simple function example:

#!/bin/bash
greet() {
    echo "Hello, $1!"
}
greet "Alice"
greet "Bob"

This defines a function greet that takes one argument (accessed as $1) and echoes a greeting. After calling greet "Alice" and greet "Bob", you’ll see greeting messages for each.

Functions can also return an exit status (success/failure indicator) like a command. By default, the exit status of a function is that of its last command. Use the return keyword to return a specific status (0 for success, non-zero for failure). Note that Bash functions do not return arbitrary values – to return a result (like a calculated value or a string), have the function print (echo) the value and capture it via command substitution.

For example, to “return” the sum of two numbers:

add() {
    local a=$1
    local b=$2
    echo $(( a + b ))
}
result=$(add 5 7)
echo "The sum is $result"

Or, to return a success/failure status using a function that checks if a number is even:

is_even() {
    local num=$1
    if (( num % 2 == 0 )); then
        return 0   # 0 indicates success/true
    else
        return 1   # non-zero indicates false
    fi
}
if is_even 42; then
    echo "42 is even"
else
    echo "42 is odd"
fi

Quiz: How can a Bash function return a value (such as a calculated number or string) to the script that called it?




Accessing Script Parameters (Arguments)

One of the things that makes scripts useful is the ability to accept input from the command line when the script is run. These inputs are called positional parameters or arguments. In Bash, when you run a script, any words you put after the script name are passed in as arguments. The script can access them via special variables: $1 is the first argument, $2 is the second, and so on. $0 is the name of the script itself. If you have more than 9 arguments, you can access beyond $9 by using braces (e.g., ${10} for the tenth argument).

Related special parameters include $#, which gives the number of arguments passed to the script, and $@ (or $*) which expands to all the arguments. The difference is that when quoted ("$@"), each argument is preserved as a separate word, while "$*" puts all arguments into a single string.

Here’s an example script that uses arguments. This script expects at least one argument (a name) and greets that name. It also prints how many arguments were given and lists them:

#!/bin/bash
if [ $# -eq 0 ]; then
    echo "Usage: $0  [other names...]"
    exit 1
fi

echo "Hello, $1!"
echo "You provided $# arguments: $@"

You can iterate over all arguments using a loop. For example, for arg in "$@" will process each argument one by one.

Exercise: Write a shell script evenodd.sh that takes one number as an argument and tells you whether it’s even or odd. For example, running ./evenodd.sh 4 should output “4 is even”, and ./evenodd.sh 7 should output “7 is odd”.


Answer:


#!/bin/bash
if [ $# -lt 1 ]; then
    echo "Usage: $0 "
    exit 1
fi

num=$1
if (( num % 2 == 0 )); then
    echo "$num is even"
else
    echo "$num is odd"
fi
      

Exercise: Write a shell script sum.sh that calculates the sum of all numbers from 1 up to a given number N (passed as an argument). For example, ./sum.sh 5 should output 15. Use a loop to accumulate the sum.


Answer:


#!/bin/bash
if [ $# -lt 1 ]; then
    echo "Usage: $0 "
    exit 1
fi

N=$1
if [ $N -lt 1 ]; then
    echo "Please provide a positive integer."
    exit 1
fi

sum=0
for (( i=1; i<=N; i++ )); do
    sum=$(( sum + i ))
done

echo "The sum of numbers 1 through $N is $sum."
      

Exercise: Write a shell script max.sh that takes two numbers as arguments and uses a function to determine which one is larger. The function should be named max and echo the larger number. For example, ./max.sh 8 12 should output “12 is the larger number.”


Answer:


#!/bin/bash
max() {
    if [ $1 -ge $2 ]; then
        echo $1
    else
        echo $2
    fi
}

if [ $# -lt 2 ]; then
    echo "Usage: $0  "
    exit 1
fi

larger=$(max $1 $2)
echo "$larger is the larger number."
      

Google doc