Bash syntax

Abstract

Programmers often use Bash scripts for various automation requirements, such as handling software builds, applying system configurations, file manipulations, and network-related operations. Assume that you are developing a software framework, then you can write a Bash script to check and install all required developer dependencies. As a result, new contributors can easily set up your project on their computers without doing time-consuming manual tasks.

Bash is a command language — not a modern general-purpose programming language that comes with a full-featured standard library and developer-friendly English-like grammar. But, in recent Bash versions, we can find various language features that we can use for general-purpose programming. Therefore, now Bash is becoming flexible and suitable for developing general-purpose computer programs.

I will explain several Bash language syntaxes and native features that you can use for general-purpose computing in this story. You can either use these syntaxes to make traditional Bash scripts modern and readable or for building general programs that you may develop with Python or JavaScript-like modern dynamic languages.

1. String Manipulation Techniques

Every general-purpose programming language typically provides various string manipulation methods on string objects. Languages that don’t natively support the OOP pattern provide string manipulation functions via the particular language’s standard library. For example, C offers string manipulation functions via the string.h header file, and Go offers them via the strings package.

Bash is a Shell interpreter and a simple command language, so it can’t offer you string methods or a full-featured standard library with an English-like grammar. Besides, extending Bash with many built-ins adds unwanted complexity to the Shell interpreter (i.e., PowerShell is complicated). Therefore, Bash supported string manipulation with the parameter expansion syntax (${}).

We can use the parameter expansion syntax to find substrings as follows:

string="Bash is great!"
echo ${string:8} # great!  
echo ${string:0:4} # Bash  
echo ${string:8:-1} # great (-1: upto 1st element from right)

We can get string lengths, as shown below.

string="Bash is great!";  
len=${#string}
echo "len = $len"

Replacing a specific substring is also a piece of cake:

string="Bash is great!!"
echo ${string/Bash/GNU Bash} # GNU Bash is great!!  
echo ${string//!/.} # Bash is great..

Uppercase and lowercase string transformations are also possible with built-in syntaxes:

string="Bash"
echo ${string^^} # BASH  
echo ${string,,} # bash

Accessing characters by the index is not natively supported in Bash, unlike Python and JavaScript, but you can always use the substring-based workaround:

function charAt() {  
    string=$1  
    i=$2  
    echo ${string:i:1}  
}
echo $(charAt "Hello" 2) # l  
echo $(charAt "Hello" 1) # e

Here I used the command-substitution syntax for capturing the function output — I will explain it further in a separate section below.

2. Creating Arrays and Dictionaries

Bash has untyped variables, so it lets you set any character sequence inside variables. It tries to treat everything as character streams (strings) and manipulates variables according to the context. Even though it doesn’t offer you a type system, it lets you work with array and dictionary structures. These data structures are undoubtedly helpful for general-purpose programming.

You can initialize an array and loop through it with the following code snippet:

A=(2 4 5 6 4)
for i in ${A[@]}; do  
    echo -n "${i} " # 2 4 5 6 4  
done

You can use the substring syntax for array slicing as well, as shown below:

A=(2 4 5 6 4)
echo ${A[@]:2:4} # 5 6 4  
echo ${A[@]:0:1} # 2

A Bash array identifier always refers to the first array element similar to C arrays with pointers. Therefore, we need to use the [@] notation to refer to the entire array for processing, as shown in the above example code.

Appending array elements works as Python:

A=(1 2 3)
A+=(4)  
echo ${A[@]} # 1 2 3 4
A+=(5 6)  
echo ${A[@]} # 1 2 3 4 5 6

Working with dictionaries is easy as Python and JavaScript. Look at the following example code that iterates through a dictionary and prints the key-value data:

declare -A D  
D=(["one"]=1 ["two"]=2 ["three"]=3)
for i in ${!D[@]}; do  
    echo "$i -> ${D[$i]}" # one -> 1 ....  
done

3. Powerful One-Liners with Command Substitution

Bash offers a way to run commands and capture the standard output with oneliners via the command substitution syntax. The Bash command substitution syntax ($(…)) supports developers to combine multiple commands productively. For example, you can sendfind command results to another command as parameters as follows:

another-command $(find *.txt)

This command substitution syntax helps us work with functions in a developer-friendly manner. The issue with standard Bash functions is that the return value only accepts valid process exit codes (because Bash is command-oriented). For example, the following function works fine in Bash with the return keyword:

function getNumber() {  
    return 200  
}
getNumber  
echo $? # 200

But, if you change the above code snippet’s return value to a higher number than 255, it won’t work correctly since the highest valid exit code value is 255. We can output (not the standard return) any value from a Bash function, thanks to command substitution as follows:

function getMessage() {  
    echo "Hello Bash!"  
}
msg=$(getMessage)  
echo $msg # Hello Bash!

Here we used the echo command instead of the return keyword since the command substitution syntax captures the standard output — not the exit code. The above approach creates a subshell, but you can find various workarounds to avoid the second Bash instance.

4. Arithmetic Operations and Random Values Generation

Every general-purpose programming language typically supports basic arithmetic operations with built-in grammar and advanced mathematical operations via standard library functions. Bash offers the built-in arithmetic expansion syntax ( $((…)) ) and the let built-in command for basic arithmetic operations. Look at the following example:

a=10  
b=5
echo "$a + $b = $((a + b))" # 10 + 5 = 15  
echo "$a - $b = $((a - b))" # 10 - 5 = 5  
echo "$a x $b = $((a * b))" # 10 x 5 = 50  
echo "$a / $b = $((a / b))" # 10 / 5 = 2  
echo "$a % $b = $((a % b))" # 10 % 5 = 0

You can use any basic operation like bitwise operators, exponentiation (**), and even ternary operators within an arithmetic expansion block. KornShell supports floating-point numbers within arithmetic expansions, but you have to use the bc command with Bash as it doesn’t support floating-point calculations natively:

a=10  
b=5.7
echo "$a / $b = $(echo "scale = 2; $a / $b" | bc)" # 10 / 5.7 = 1.75

Generating random numbers in Bash is more minimal than in Python because of the $RANDOM shell variable:

echo $(($RANDOM % 10)) # Random number between 0 and 10

You can use the let built-in command the same as arithmetic expansions too.

5. Handling Inputs Is Easier than Python and Node.js

Handling console input is different according to the programming language runtime environment. For example, you can use the built-in readline package for capturing user inputs with Node.js. On the other hand, Python offers minimal input built-in. Bash offers the built-in read command for capturing user input with a prompt message. The read command behaves somewhat syntactically similar to C’s scanf since it accepts the variable name as a parameter.

Look at the following example:

read -p "Enter your name: " name
echo "Hello, $name"

The above code snippet produces the following result:

Reading a user input from the terminal with a Bash script, a screenshot by the author

If you need to create an array from the standard input data, you can use the -a option:

read -p "Enter numbers: " -a A
echo ${A[@]}

Conclusion

Bash is a Shell interpreter, but it gives you many features that other modern languages offer. Bash variables are indeed untyped, but we can use minimal variable typing features with the declare built-in. We usually use Bash for various automation requirements with traditional Shell scripting syntaxes by spawning other commands — but Bash is suitable for writing CLI programs (NVM is written in the POSIX compatible Shell language), utility scripts, and other general-purpose programs because of the modern syntaxes I showed before. For example, check this Bash script that generates short names based on the user-entered long person names and compare it with its identical Python version — both codes have somewhat the same readability score.