Published on: Apr 22, 2025
Linux Bash Scripting
Bash scripting is like general programming language and is basically writing Linux commands in a file and executing them in one go. This is very helpful in executing a series of commands in a single file where each step depends on the previous operation. Bash supports almost all features that normal programming language supports. Bash scripting is widely used in system admin tasks, system configuration, server automation, etc.
The following sections describe the bash features and they are more like a reference than an actual tutorial or something.
Running bash script
Save the file with .sh extension and give necessary permissions to execute like chmod +x script.sh. And execute the shell script as bash script.sh or ./script.sh.
The bash script runs in a new subshell. So, the custom system configuration won't be applied in the script execution.
Use set -ueo pipefail at the start of the script to exit when an error occurs in the pipeline, and also if there are any unset variables.
1#!/bin/bash 2 3set -ueo pipeline 4 5... 6
Subshell
A subshell is another shell/process spawned within the current shell that inherits all environment, variables, and current config from its parent process. The subshell runs in an isolated environment compared to the parent. A subshell is created when the following syntax is used
- Parenthesis: ()
- Command substitution: $()
I/O Redirection
Redirection controls where to get input and where to send output.
Standard File Descriptors (FD)
- 0: Standard Input (stdin)
- 1: Standard Output (stdout)
- 2: Standard Error (stderr)
Input Redirection
- Send stdin to a command from a file as: command < input.txt
- | (Pipe): Pipe connects the stdout of the command on the left to the stdin of the command on the right.
- Process substitution: Process substitution passes outputs of multiple passes to stdin for another command. Ex: cat <(ls -l) <(ls -la)
- herestring: Here-string (<<<) passes string directly to standard input for a command. Instead of echo "abc def" | wc -w, use wc -w <<< "abc def
- heredocs: Here documents pass multi-line input/strings to a command. The command should be anything that accepts the standard input.
1# Syntax 2<command> <<'DELIMETER' 3multi-line input/text 4DELIMETER 5 6# example, send the heredoc input to sys.conf 7cat <<'CONF' > sys.conf 8PORT=$port 9ENV=$env 10CONF 11
Output Redirection
- For saving output to a file from a command, using the > operator will create a new file and write to it. If the file doesn't exist it will create a new file.
- >> will append to the existing file.
- Redirect stderr to a file: command 2> error.log
- Redirect stdout and stderr: commmand > res.txt 2> err.txt
- Redirect both stdout and stderr to the same file: command &> log.txt or command > log.txt 2>&1
Special Files Special files expose hardware devices as files in the system. Character special files (listed below) are helpful in redirecting the I/O.
- /dev/stdin: Symlink to file descriptor 0 (standard input)
- /dev/stdout: Symlink to file descriptor 1 (standard output)
- /dev/stderr: Symlink to file descriptor 2 (standard error)
- /dev/tty: Represents the terminal connected to the current process
- /dev/null: Discards all data written to it
Command-line arguments & Pre-pending variables
1#!/bin/bash 2 3: <<'MCOMMENT' 4Assume for this script, command line arguments are passed as: 5> ./script.sh "a" "b" "c" 6 7Command line arguments follow the same rules as bash variables. So, passing 8arguments as "a b" or '$var' will be expanded just like as bash variables. 9MCOMMENT 10 11# Access command line argument as 12echo "$1", "$2" # a b 13# For argument greater than '9', enclose the number with {} 14echo "${13}" 15 16# '$#' is for no.of arguments, '$@' is for all arguments 17echo "$#" # 3 18echo "$@" # a b c 19 20# Read all arguments to array 21arr=() 22for arg in "$@"; do 23 arr[i]=$arg 24 i=$((i + 1)) 25done 26 27echo "$arr" 28 29: <<'MCOMMENT' 30Pre-pending variables as 31> ENV_MODE="DEV" DEPLOY_MODE="STAGING" ./script.sh "arg1" "arg2" 32 33These variables are only scoped to the current script context. 34MCOMMENT 35 36echo "$ENV_MODE" # DEV 37echo "$DEPLOY_MODE" # STAGING 38
Variables inside strings
1#!/bin/bash 2 3name_msg="Hi Nomina" 4echo $name_msg. Welcome. # Hi Nomina. Welcome. 5 6# Single quotes 7echo '$name_msg' # $name_msg 8 9# Backquotes or Command Substitution 10usr_name="uname" 11echo `$usr_name` # Linux 12echo $($usr_name) # Linux 13 14# Without curly brackets 15dir_name="/usr/bin" 16echo "$dir_name_prod/env" # /env 17 18# With curly brackets 19echo ${dir_name}_prod/env # /usr/bin_prod/env 20
Expressions
For expressions:
- Use $((x + y))
- Or $(($x + $y))
- Or (( x += 1))
Note: This will concat the numbers: z=$x+$y
Command substitution: capture output of a command to another command
- x=`echo "Hi "$name`
- x=$(echo "Hi "$name)
For arithmetic operations natural results use bc -l <<< "$x/$y"
Conditional statements
- The test command takes an expression and evaluates it. Successful test returns with status code 0.
- [] is an alternative for the test command. It requires at least a single space after opening and before closing.
- Use [[]] instead of [] in conditions for better and advanced behaviour.
Equality and Pattern Matching
- Use = or == to compare match globs like if [[ "$file" = *.txt ]]
- =~ is a condition to match the pattern like if [[ "$x" =~ "\<pattern>" ]]
- For substring check, if [[ "$x" == *substring* ]]
- Capture regex group matches with ${BASH_REMATCH[n]}
Word based operators with [] and Symbols with [[]]:
1-a && 2-o || 3-eq = or == 4-ne != 5-gt > 6-ge >= 7-lt < 8-le <= 9-z (true if string is empty or null) 10-n (true if string is not null) 11=~ (to use regex like [[ ... =~ ... ]]) 12
Some file operators
1-e (true if file exists) 2-f (true if the variable is file) 3-d (true if the variable is directory) 4-t (true if file associates with terminal device) 5-r (true if file has read permissions for effective user id (EUID)) 6-w (true if file has write permissions) 7-x (true if file has execute permissions) 8
Control flow
Conditionals
1#!/bin/bash 2 3# if 4if [[ : ]]; then 5 : 6fi 7 8# if-else 9if [[ : ]]; then 10 : 11else 12 : 13fi 14 15# if-elif-else 16if [[ : ]]; then 17 : 18elif [[ : ]]; then 19 : 20else 21 : 22fi 23 24# nested-if 25if [[ : ]]; then 26 : 27 if [[ : ]]; then 28 : 29 fi 30fi 31
Case Pattern Matching
1#!/bin/bash 2 3# case 4x="1" 5case $x in 60) echo "0" ;; 71) echo "1" ;; 8*) echo "none" ;; # default case 9esac 10 11 12# case, match multiple patterns 13x="3" 14case $x in 150|1|2) echo "<= 2" ;; 163|4|5) echo "<=5" ;; 17*) echo "none" ;; # default case 18esac 19 20# case with ";&" to go to the next statement even if it doesn't match 21x="3" 22case $x in 233) echo "matched" ;& 245) echo "this will also print" ;; 25esac 26 27# case with ";;&" to go to next only if matches 28x="3" 29case $x in 303) echo "matched" ;;& 315) echo "this will not print" ;;& 32[0-9]) echo "but this will" ;; 33esac 34
Loops
1#!/bin/bash 2 3# c-style for loop 4for (( i=0; i<"$#"; i++ )); do 5 : 6done 7 8# loop over a range 9# where 100 is in the inclusive range 10for i in {1..100}; do 11 : 12done 13 14# loop for multiple elements 15# this will loop over all files with a pattern 16for file in *.txt; do 17 : 18done 19 20# example 21IFS=',' read -ra arr <<< "1,2,3" 22for i in "${arr[@]}"; do 23 echo "$i" 24done 25 26# while loop 27i=0 28while [[ $i <= 3 ]]; do 29 (( i++ )) 30done 31 32# while until loop 33while read -r line; do 34 echo "read: $line" 35done < file.txt 36 37# unitl: loop over condition becomes true (differ to while) 38i=0 39until [[ $i >= 3 ]]; do 40 (( i++ )) 41done 42
Arrays
1#!/bin/bash 2# IFS is mostly " \t\n" i.e., space, tab, or new line 3arr=() 4x="abc def" 5# If variables are not quoted, the value split into multiple based on IFS 6arr+=($x) 7echo ${#arr[@]} # 2 8 9# If quoted 10arr=() 11arr+=("$x") 12echo ${#arr[@]} # 1 13 14# Without "", array values split for each element based on IFS 15arr=("abc" "abc def" "def") 16arr2=() 17for i in ${arr[@]}; do 18 arr2+=("$i") 19done 20echo ${#arr2[@]} # 4 21pIFS=$IFS 22IFS=',' 23echo "${arr2[*]}" # abc,abc,def,def 24IFS=$pIFS 25 26# With "" 27arr2=() 28for i in "${arr[@]}"; do 29 arr2+=("$i") 30done 31echo ${#arr2[@]} # 3 32pIFS=$IFS 33IFS=',' 34echo "${arr2[*]}" # abc,abc def,def 35IFS=$pIFS 36
Use Associative Arrays for more advanced array behavior.
Strings
1#!/usr/bin/env bash 2 3# Concatenation 4x="" 5x+="1" 6x+="2 3" 7echo "$x" # 12 3 8 9x="1" 10y="2 3" 11z="$x $y" # 1 2 3 12echo "$z" 13 14# Length 15echo ${#z} # 5 16 17# Slicing 18x="1 2 3 4 5" 19# ${str:pos} from starting position to end 20echo ${x:2} # 2 3 4 5 21# ${str:pos:len} sub-string from that position with length 22echo ${x:2:5} # 2 3 4 23# ${str:(-n)} from last 'n' character 24echo "${x:(-3)}" # 4 5 25# ${str:pos:${#str}-n} sub-string from position to last 'n'th position 26echo "${x:2:${#x}-4}" # 2 3 4 27 28
Functions
1#!/bin/bash 2 3# A function can be defined as 4function f1 { 5 : 6} 7 8# or 9 10f2() { 11 : 12} 13 14# there is no explicit declaration for function parameters, as the 15# arguments are passed to function just like script command line arguments 16# function is invoked with its name and any arguments 17f1 "a" "b" 18 19# function can access the arguments the same as the script with $1, $2, ... or $@ 20f3() { 21 echo "received arguments for func ${FUNCNAME[0]}:" "$@" 22} 23 24f3 "a" "b" # received arguments for func f3: a b 25 26# function can return values or exit status (only numerics) 27f4() { 28 if [[ $# == 2 ]]; then 29 return 0 30 else 31 return 1 32 fi 33} 34 35f4 1 2 36echo $? # 2 37 38# if the functions called in the same shell, 39# outside function scope, those variables can be assessed 40f5() { 41 x=10 42} 43 44f5 45echo $x # 10 46
To return/access other types, store the standard output of the function with command substitution/sub-shell
1#!/bin/bash 2 3# capture stdout by calling a function inside sub-shell 4f() { 5 echo $(($1 + $2)) 6} 7 8echo "Sum is $(f 1 2)" # Sum is 3 9 10# as the function is called inside the sub-shell, 11# the variables declared inside the function can't be accessed outside 12f() { 13 x=10 14 echo "Value is $x" 15} 16 17echo $(f) # Value is 10 18echo "function value is $x" # function value is 19
If functions are called inside command substitution like $(func1 par1 par2)
and the stdout is captured, then every function statements stdout will be returned like
1#!/bin/bash 2 3f() { 4 printf "received %s, %s" $1 $2 5 echo $(($1 + $2)) 6} 7 8res=$(f 1 2) 9echo "Sum is $res" # Output: Sum is received 1, 23 10
To avoid this, redirect the stdout and stderr of each statement (if any) to the terminal output
1#!/bin/bash 2 3f() { 4 printf "received %s, %s\n" $1 $2 >/dev/tty 5 echo $(($1 + $2)) 6} 7 8res=$(f 1 2) 9echo "Sum is $res" # Output: Sum is 3 10
: null command
- : command does nothing and returns status 0
- Use the : (null or no-op) command to avoid syntax errors when the block is empty like
1if [[ <condition> ]]; then 2 : 3else 4 echo "Condition failed" 5fi 6 7# or 8 9for :; do 10 echo "Continuous loop" 11done 12 13# or, for parameter expansion if unset like below instead of x=${x:="default"} 14: "${x:="default"}" 15
These are all building blocks for writing bash scripts, combine these with other linux commands to build advanced scripts.