Linux Bash reference for bash scripting

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.