Post

Chapter 1 Shell Something Out

First Chapter in bash scripting for Linux

Chapter 1 Shell Something Out

Chapter Overview

In this chapter, I will cover

  • Printing in the terminal

  • Playing with variables and environment variables

  • Function to prepend to environment variables

  • Math with the shell

  • Playing with file descriptors and redirection

  • Arrays and associative array

  • Visiting aliases

  • Grabbing information about the terminal

  • Getting and setting dates and delays

  • Debugging the script

  • Functions and arguments

  • Reading output of a sequence of commands in a variable

  • Reading n characters without pressing the return key

  • Running a command until it succeeds

  • Field separators and iterators

  • Comparisons and tests


Printing in the Terminal

echo

1
2
3
echo "Hello World"
echo -n "No newline"          # -n suppresses the trailing newline
echo -e "Tab\there\nNewline"  # -e enables escape sequences

Double quotes expand variables. Single quotes print literally.

1
2
3
name="Omar"
echo "Hello $name"   # Hello Omar
echo 'Hello $name'   # Hello $name

printf

More control than echo — borrowed from C. Useful for aligned output.

1
2
printf "%-10s %5d\n" "Item" 42   # left-align string, right-align number
printf "Pi is %.2f\n" 3.14159    # 2 decimal places

%s = string, %d = integer, %f = float. The number before the letter sets the field width.


Variables and Environment Variables

Variables are untyped in bash — everything is a string unless told otherwise. No spaces around =.

1
2
3
4
name="Omar"
echo $name        # access with $
echo ${name}      # braces — safer, avoids ambiguity
echo "${name}!"   # braces needed: $name! would fail

Environment variables are exported to child processes:

1
2
3
export MY_VAR="value"
printenv MY_VAR       # print one var
env                   # list all

Common ones to know:

VariableMeaning
$HOMEHome directory
$PATHDirectories bash searches for commands
$USERCurrent user
$PWDCurrent working directory
$?Exit status of the last command

Prepending to Environment Variables

Add to PATH without wiping it:

1
export PATH="/new/bin:$PATH"

A safer function that avoids adding duplicates:

1
2
3
4
5
6
7
8
9
prepend_path() {
  case ":$PATH:" in
    *":$1:"*) ;;          # already present, skip
    *) PATH="$1:$PATH" ;;
  esac
  export PATH
}

prepend_path "/opt/custom/bin"

Put it in ~/.bashrc and call it for any directory you want at the front.


Math with the Shell

Bash only does integer math natively — use $(( )):

1
2
3
4
echo $((10 + 5))     # 15
echo $((2 ** 8))     # 256 — exponentiation
echo $((17 % 3))     # 2  — modulo
result=$((100 / 4))

For decimals, pipe to bc:

1
2
echo "scale=2; 10 / 3" | bc      # 3.33
echo "scale=4; sqrt(2)" | bc -l  # 1.4142 — -l loads math library

File Descriptors and Redirection

Every process starts with 3 file descriptors:

FDNameDefault
0stdinkeyboard
1stdoutterminal
2stderrterminal
1
2
3
4
5
command > file.txt        # redirect stdout (overwrite)
command >> file.txt       # redirect stdout (append)
command 2> errors.txt     # redirect stderr
command &> all.txt        # redirect both stdout and stderr
command > /dev/null 2>&1  # discard all output

Read from a file instead of keyboard:

1
wc -l < file.txt

Custom file descriptors:

1
2
3
exec 3> custom.txt   # open fd 3 for writing
echo "data" >&3      # write to it
exec 3>&-            # close fd 3

Arrays and Associative Arrays

Indexed arrays:

1
2
3
4
5
6
7
8
9
10
fruits=("apple" "banana" "cherry")

echo ${fruits[0]}     # apple
echo ${fruits[@]}     # all elements
echo ${#fruits[@]}    # length — 3
fruits+=("date")      # append

for f in "${fruits[@]}"; do
  echo "$f"
done

Associative arrays (bash 4+) — key-value pairs:

1
2
3
4
5
6
7
8
9
10
11
declare -A user
user[name]="Omar"
user[role]="admin"

echo ${user[name]}      # Omar
echo ${!user[@]}        # all keys
echo ${user[@]}         # all values

for key in "${!user[@]}"; do
  echo "$key = ${user[$key]}"
done

Aliases

Shortcuts for commands — define in ~/.bashrc:

1
2
3
4
5
6
7
alias ll='ls -lah'
alias gs='git status'
alias ..='cd ..'
alias grep='grep --color=auto'

unalias ll   # remove one
alias        # list all

Aliases don’t take arguments. Use a function instead:

1
2
3
mkcd() {
  mkdir -p "$1" && cd "$1"
}

Terminal Information

1
2
3
4
tput cols     # columns (width)
tput lines    # rows (height)
tput colors   # number of colors supported
echo $TERM    # terminal type e.g. xterm-256color

Center text dynamically based on terminal width:

1
2
3
4
width=$(tput cols)
text="Hello"
pad=$(( (width - ${#text}) / 2 ))
printf "%${pad}s%s\n" "" "$text"

Set colors:

1
tput setaf 1; echo "Red text"; tput sgr0   # sgr0 resets

Dates and Delays

1
2
3
4
5
date                      # full date and time
date +"%Y-%m-%d"          # 2026-02-25
date +"%H:%M:%S"          # 14:30:00
date +%s                  # Unix timestamp
date -d @1706140800       # convert timestamp back to readable

Delays:

1
2
3
sleep 1      # 1 second
sleep 0.5    # half second
sleep 2m     # 2 minutes

Measure how long something takes:

1
2
3
start=$(date +%s)
your_command
echo "Elapsed: $(( $(date +%s) - start ))s"

Or just:

1
time your_command

Debugging Scripts

Trace mode — prints every command before it runs:

1
bash -x script.sh

Toggle inside the script:

1
2
3
set -x   # start tracing
# commands
set +x   # stop tracing

Best practice — put this at the top of every script:

1
2
#!/bin/bash
set -euo pipefail
FlagEffect
-eExit immediately on any error
-uTreat unset variables as errors
-o pipefailPipe fails if any command in it fails, not just the last

Functions and Arguments

1
2
3
4
5
6
greet() {
  echo "Hello, $1"
  echo "Got $# arguments"
}

greet "Omar"

Special variables inside functions:

VariableMeaning
$1, $2Positional arguments
$@All arguments as separate strings
$#Number of arguments
$0Script name

Functions return exit codes (0–255), not strings. To return a value, echo it and capture:

1
2
3
4
5
get_date() {
  echo "$(date +%Y-%m-%d)"
}

today=$(get_date)

Command Substitution

Capture the output of a command into a variable:

1
2
3
files=$(ls /etc)
today=$(date +"%Y-%m-%d")
line_count=$(wc -l < file.txt)

Use $() not backticks — backticks can’t be nested cleanly:

1
2
# Nested — works fine with $()
result=$(wc -l < $(find /var/log -name "*.log" | head -1))

Reading Input

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

Read n characters without pressing Enter:

1
read -n 1 -p "Press any key..." key

Silent input (passwords):

1
2
read -s -p "Password: " pass
echo ""

With a timeout:

1
read -t 5 -p "Answer in 5s: " answer || echo "Timed out"

Running a Command Until It Succeeds

1
2
3
until command; do
  sleep 1
done

Wait for a server to be ready:

1
2
3
4
5
until curl -s http://localhost:3000/health > /dev/null; do
  echo "Waiting for server..."
  sleep 2
done
echo "Server is up"

With a max retry limit:

1
2
3
4
5
retries=0
until command || [[ $retries -eq 5 ]]; do
  retries=$((retries + 1))
  sleep 1
done

Field Separators and Iterators

IFS (Internal Field Separator) controls how bash splits strings. Default: space, tab, newline.

Split on a custom delimiter:

1
2
3
4
IFS=',' read -ra parts <<< "one,two,three"
for part in "${parts[@]}"; do
  echo "$part"
done

Always restore IFS after changing it:

1
2
3
4
old_IFS=$IFS
IFS=':'
# work with colon-separated data
IFS=$old_IFS

Parse /etc/passwd line by line:

1
2
3
while IFS=':' read -r user _ uid _ _ home shell; do
  echo "User: $user | Shell: $shell"
done < /etc/passwd

Comparisons and Tests

Use [[ ]] over [ ] — bash built-in, safer, supports regex.

String tests:

1
2
3
4
5
[[ "$a" == "$b" ]]       # equal
[[ "$a" != "$b" ]]       # not equal
[[ -z "$a" ]]            # empty string
[[ -n "$a" ]]            # non-empty
[[ "$a" =~ ^[0-9]+$ ]]   # regex match

Integer tests:

1
2
3
4
5
6
[[ $a -eq $b ]]   # equal
[[ $a -ne $b ]]   # not equal
[[ $a -lt $b ]]   # less than
[[ $a -gt $b ]]   # greater than
[[ $a -le $b ]]   # less or equal
[[ $a -ge $b ]]   # greater or equal

File tests:

1
2
3
4
5
6
7
[[ -e "$f" ]]   # exists
[[ -f "$f" ]]   # is a regular file
[[ -d "$f" ]]   # is a directory
[[ -r "$f" ]]   # readable
[[ -w "$f" ]]   # writable
[[ -x "$f" ]]   # executable
[[ -s "$f" ]]   # non-empty

Combining:

1
2
3
[[ -f "$f" && -r "$f" ]]   # AND
[[ -z "$a" || -z "$b" ]]   # OR
[[ ! -d "$dir" ]]           # NOT

📚 References


You can find me online at:

My signature image

This post is licensed under CC BY 4.0 by the author.