Arithmetic and String Operations

🔢 Topic 4 — Arithmetic and String Operations

Bash stores everything as a string, but it can perform integer arithmetic natively and provides a rich set of parameter expansion operators for slicing, replacing, and transforming strings — all without calling an external command. This chapter covers the arithmetic context $(( )), floating-point math with bc, and the full string manipulation toolkit built into bash's parameter expansion syntax.

1 — Integer Arithmetic with $(( ))

The arithmetic expansion $(( expression )) evaluates an integer expression and substitutes the result. It is the standard, preferred way to do arithmetic in bash — fast, built-in, and readable.

🐧 Basic arithmetic operations
#!/bin/bash a=10 b=3 echo "Addition : $((a + b))" → 13 echo "Subtraction : $((a - b))" → 7 echo "Multiplication : $((a * b))" → 30 echo "Division : $((a / b))" → 3 (integer — truncates) echo "Modulo : $((a % b))" → 1 echo "Exponentiation : $((a ** b))" → 1000 # Store result in a variable result=$(( a * b + 5 )) echo "Result: $result" Result: 35 # Variables inside (( )) do NOT need the $ prefix total=$(( a + b )) # both work total=$(( $a + $b )) # also fine, but redundant

Increment, Decrement, and Compound Assignment

🐧 Updating variables in place
count=0 # Increment by 1 count=$(( count + 1 )) # explicit form $(( count++ )) # post-increment (returns old value, then adds 1) $(( ++count )) # pre-increment (adds 1 first, then returns) (( count++ )) # (( )) alone — no $ needed when not capturing value # Compound assignment operators n=10 (( n += 5 )) # n = n + 5 → 15 (( n -= 3 )) # n = n - 3 → 12 (( n *= 2 )) # n = n * 2 → 24 (( n /= 4 )) # n = n / 4 → 6 (( n %= 4 )) # n = n % 4 → 2 echo "$n" 2
(( expr )) without the leading $ evaluates the expression for its side effects (like updating a counter) and sets the exit status to 0 (true) if the result is non-zero, 1 (false) if zero. This is useful in loop conditions (Topic 6).

Arithmetic with let

The let built-in is an older alternative that evaluates arithmetic expressions without needing $(( )) syntax. It is less commonly used in modern scripts but still appears in legacy code.

🐧 let vs $(( ))
# let evaluates the expression directly let x=5+3 echo "$x" 8 let x++ echo "$x" 9 # Equivalent using $(( )) — preferred in modern scripts x=$(( 5 + 3 )) (( x++ ))

2 — Floating-Point Arithmetic with bc

Bash's built-in arithmetic only handles integers. For decimal calculations you need an external tool — bc (basic calculator) is the standard choice. You pipe an expression to it as a string and capture the result.

🐧 Using bc for decimal maths
# Basic: pipe an expression string to bc echo "3.14 * 2" | bc 6.28 # scale= controls decimal places echo "scale=2; 10 / 3" | bc 3.33 echo "scale=4; sqrt(2)" | bc -l # -l loads the math library (sqrt, sin, cos...) 1.4142 # Store result in a variable using command substitution price=49.99 tax_rate=0.20 total=$(echo "scale=2; $price * (1 + $tax_rate)" | bc) echo "Total with tax: £$total" Total with tax: £59.98 # Comparison — bc returns 1 (true) or 0 (false) result=$(echo "3.14 > 3" | bc) if [ "$result" -eq 1 ]; then echo "3.14 is greater than 3" fi
bc is an external programme — it adds a small overhead per call. For scripts doing thousands of float calculations, consider Python or awk instead.
bc -l math functions: the -l flag loads bc's standard maths library, which provides sqrt(x), s(x) (sine), c(x) (cosine), a(x) (arctangent), e(x) (e^x), and l(x) (natural log). It also sets the default scale to 20 decimal places.

3 — String Length and Slicing

Bash provides parameter expansion operators for extracting information from strings. These work without any external commands — they are built directly into the shell.

🐧 String length and substrings
str="Hello, World!" # ${#var} — length of string echo "Length: ${#str}" Length: 13 # ${var:offset} — substring from offset to end echo "${str:7}" World! # ${var:offset:length} — substring of given length echo "${str:0:5}" Hello echo "${str:7:5}" World # Negative offset — count from the END of the string # Note: space before negative number avoids confusion with ${var:-default} echo "${str: -6}" World! echo "${str: -6:5}" World

4 — Prefix and Suffix Removal

These operators strip matching patterns from the beginning or end of a string. They are extremely useful for manipulating file paths, extensions, and structured strings — all without calling sed or cut.

🐧 # ## % %% operators
filepath="/home/philip/documents/report.final.txt" # ${var#pattern} — remove SHORTEST match from the FRONT echo "${filepath#*/}" home/philip/documents/report.final.txt # ${var##pattern} — remove LONGEST match from the FRONT echo "${filepath##*/}" # strips everything up to last / report.final.txt # ${var%pattern} — remove SHORTEST match from the END echo "${filepath%.*}" # strips last extension /home/philip/documents/report.final # ${var%%pattern} — remove LONGEST match from the END echo "${filepath%%.*}" # strips everything from first dot /home/philip/documents/report
Practical: extract components from a file path
These four operators handle the most common path manipulation tasks without needing basename or dirname.
file="/var/log/nginx/access.log" # Filename only (equivalent to basename) filename="${file##*/}" echo "Filename : $filename" Filename : access.log # Directory only (equivalent to dirname) dir="${file%/*}" echo "Directory: $dir" Directory: /var/log/nginx # Extension only ext="${filename##*.}" echo "Extension: $ext" Extension: log # Filename without extension base="${filename%.*}" echo "Base name: $base" Base name: access
Pattern syntax: The patterns in #, ##, %, %% use glob wildcards, not regular expressions. * matches any sequence of characters, ? matches a single character, and [abc] matches a character class. Regular expression matching is covered in Topic 10.

5 — Search and Replace

Bash can search for a pattern in a string and replace it — again using parameter expansion, with no external tools.

🐧 / and // replacement operators
sentence="the cat sat on the mat" # ${var/pattern/replacement} — replace FIRST occurrence echo "${sentence/the/a}" a cat sat on the mat # ${var//pattern/replacement} — replace ALL occurrences echo "${sentence//the/a}" a cat sat on a mat # ${var/pattern/} — delete pattern (replace with nothing) echo "${sentence// /}" # remove all spaces thecatsatonthemat # Replace only at the start (# anchor) echo "${sentence/#the/a}" a cat sat on the mat # Replace only at the end (% anchor) echo "${sentence/%mat/rug}" the cat sat on the rug # Practical: replace spaces with underscores in a filename name="my document file.txt" safe_name="${name// /_}" echo "$safe_name" my_document_file.txt

6 — Case Conversion

Bash 4.0 introduced built-in case conversion operators. These are available on almost all modern Linux systems (check with bash --version — you need 4.0 or higher).

🐧 Uppercase and lowercase operators (bash 4+)
str="Hello, World!" # ${var^^} — convert ALL characters to UPPERCASE echo "${str^^}" HELLO, WORLD! # ${var,,} — convert ALL characters to lowercase echo "${str,,}" hello, world! # ${var^} — capitalise FIRST character only word="hello" echo "${word^}" Hello # ${var,} — lowercase FIRST character only word="HELLO" echo "${word,}" hELLO # With a pattern — only matching characters are changed echo "${str^^[aeiou]}" # uppercase vowels only HEllO, WOrld!
macOS ships with bash 3.2 by default — these operators will not work there. Use tr as a fallback: echo "$str" | tr '[:upper:]' '[:lower:]'
Case-insensitive input comparison
Converting input to a known case before comparing is more reliable than trying to match every capitalisation variant.
#!/bin/bash read -r -p "Continue? (yes/no): " answer if [ "${answer,,}" = "yes" ]; then echo "Proceeding..." else echo "Aborted." fi # "YES", "Yes", "yes", "yEs" all match cleanly

7 — Testing Strings

Before operating on a string it is often useful to check its length, or whether it contains a particular substring. Here are the standard approaches.

🐧 Empty, non-empty, and contains checks
str="Hello, World!" # Check if empty (zero length) if [ -z "$str" ]; then echo "empty"; else echo "not empty"; fi not empty # Check if non-empty (non-zero length) if [ -n "$str" ]; then echo "has content"; fi has content # Check if a string contains a substring — use glob matching in [[ ]] if [[ "$str" == *"World"* ]]; then echo "Contains 'World'" fi Contains 'World' # Check if a string starts with a prefix if [[ "$str" == "Hello"* ]]; then echo "Starts with Hello" fi Starts with Hello # Check if a string ends with a suffix if [[ "$str" == *"!" ]]; then echo "Ends with !" fi Ends with ! # String equality and inequality if [ "$str" = "Hello, World!" ]; then echo "equal"; fi if [ "$str" != "Goodbye" ]; then echo "not equal"; fi
[[ ]] (double brackets) is needed for glob matching with *. Conditionals are covered fully in Topic 5.

8 — String Concatenation

Bash has no explicit concatenation operator — you simply place strings and variables next to each other inside double quotes. You can also use += to append to a string variable.

🐧 Joining strings
# Adjacent values concatenate automatically first="Hello" second="World" combined="$first, $second!" echo "$combined" Hello, World! # += appends to an existing string msg="Hello" msg+=", World" msg+="!" echo "$msg" Hello, World! # Building a string in a loop csv="" for item in apple banana cherry; do csv+= "$item," done # Strip trailing comma echo "${csv%,}" apple, banana, cherry

9 — Quick Reference

Arithmetic

SyntaxWhat it does
$(( a + b ))Integer addition (also - * / % **)
(( x++ ))Post-increment x (side-effect only, no substitution)
(( x += 5 ))Compound assignment (also -= *= /= %=)
let x=a+bAlternative arithmetic (legacy — prefer $(( )))
echo "scale=2; expr" | bcFloating-point arithmetic
echo "expr" | bc -lFloat with maths library (sqrt, sin, cos…)

String Operations

SyntaxWhat it doesExample result
${#var}String length${#"hello"}5
${var:n}Substring from index n${"hello":2}llo
${var:n:len}Substring of length len from index n${"hello":1:3}ell
${var#pat}Remove shortest prefix matching pat${"file.tar.gz"#*.}tar.gz
${var##pat}Remove longest prefix matching pat${"file.tar.gz"##*.}gz
${var%pat}Remove shortest suffix matching pat${"file.tar.gz"%.*}file.tar
${var%%pat}Remove longest suffix matching pat${"file.tar.gz"%%.*}file
${var/pat/rep}Replace first occurrence of pat 
${var//pat/rep}Replace all occurrences of pat 
${var/#pat/rep}Replace prefix pat 
${var/%pat/rep}Replace suffix pat 
${var^^}Convert to UPPERCASE (bash 4+) 
${var,,}Convert to lowercase (bash 4+) 
${var^}Capitalise first character (bash 4+) 

✏️ Exercises

Apply what you have learned in this chapter. Try each exercise yourself before looking at the sample solution.

Exercise 1
Write a script called calc.sh that accepts two numbers and an operator (+, -, *, /) as command-line arguments and prints the result. Division should show two decimal places. If the operator is not one of the four supported ones, print an error to stderr and exit with code 1.
Hint: use a case statement on $2 (the operator). For division use bc with scale=2; for the others use $(( )). Quote the operator argument carefully to avoid shell interpretation of *.
Sample Solution
#!/bin/bash # calc.sh — usage: ./calc.sh 10 + 3 a="$1" op="$2" b="$3" case "$op" in +) echo "$(( a + b ))" ;; -) echo "$(( a - b ))" ;; x) echo "$(( a * b ))" ;; # use 'x' to avoid shell expanding * /) echo "scale=2; $a / $b" | bc ;; *) echo "ERROR: unsupported operator '$op'. Use + - x /" >&2 exit 1 ;; esac

We use x for multiply to avoid the shell expanding * into a filename glob. Run it as: ./calc.sh 10 + 3 or ./calc.sh 7 / 2

Exercise 2
Write a script called pathinfo.sh that accepts a full file path as a command-line argument and prints: the directory, the filename, the file extension, and the filename without its extension — all using parameter expansion only (no basename, dirname, or cut).
Hint: use ${path%/*} for directory, ${path##*/} for filename, ${filename##*.} for extension, and ${filename%.*} for base name.
Sample Solution
#!/bin/bash # pathinfo.sh — usage: ./pathinfo.sh /var/log/nginx/access.log path="$1" filename="${path##*/}" echo "Full path : $path" echo "Directory : ${path%/*}" echo "Filename : $filename" echo "Extension : ${filename##*.}" echo "Base name : ${filename%.*}"
Exercise 3
Write a script called invoice.sh that stores a list of at least four item prices as variables, adds them together using arithmetic expansion, calculates 20% VAT, and prints a formatted invoice using printf showing each item, the subtotal, the VAT amount, and the grand total — all to 2 decimal places.
Hint: use bc with scale=2 for the float additions and VAT calculation. Use printf "%-20s £%8.2f\n" to align the rows.
Sample Solution
#!/bin/bash # invoice.sh item1_name="Raspberry Pi 5"; item1_price=74.99 item2_name="USB-C Cable"; item2_price=8.50 item3_name="MicroSD 64GB"; item3_price=11.99 item4_name="HDMI Adapter"; item4_price=12.00 subtotal=$(echo "scale=2; $item1_price + $item2_price + $item3_price + $item4_price" | bc) vat=$(echo "scale=2; $subtotal * 0.20" | bc) total=$(echo "scale=2; $subtotal + $vat" | bc) printf "\n %-22s %s\n" "INVOICE" "$(date +%Y-%m-%d)" printf ' %.0s─' {1..34}; echo printf " %-22s £%7.2f\n" "$item1_name" $item1_price printf " %-22s £%7.2f\n" "$item2_name" $item2_price printf " %-22s £%7.2f\n" "$item3_name" $item3_price printf " %-22s £%7.2f\n" "$item4_name" $item4_price printf ' %.0s─' {1..34}; echo printf " %-22s £%7.2f\n" "Subtotal" $subtotal printf " %-22s £%7.2f\n" "VAT (20%%)" $vat printf ' %.0s─' {1..34}; echo printf " %-22s £%7.2f\n" "TOTAL" $total echo
Exercise 4
Write a script called slugify.sh that accepts a string argument (a blog post title, for example) and converts it into a URL-friendly slug: lowercase, spaces replaced with hyphens, and any characters that are not letters, numbers, or hyphens removed. For example, "Hello World! This is Bash 4" should become hello-world-this-is-bash-4.
Hint: chain three operations — use ${title,,} for lowercase, ${result// /-} to replace spaces, then pipe through tr -cd 'a-z0-9-' to strip non-slug characters.
Sample Solution
#!/bin/bash # slugify.sh — usage: ./slugify.sh "Hello World! This is Bash 4" title="$*" # $* joins all arguments into one string # Step 1: lowercase slug="${title,,}" # Step 2: replace spaces with hyphens slug="${slug// /-}" # Step 3: remove any character that isn't a-z, 0-9, or hyphen slug=$(echo "$slug" | tr -cd 'a-z0-9-') # Step 4: collapse multiple consecutive hyphens into one while [[ "$slug" == *"--"* ]]; do slug="${slug//--/-}" done # Step 5: strip leading/trailing hyphens slug="${slug#-}" slug="${slug%-}" echo "$slug"

Uses $* so you can call it as ./slugify.sh Hello World! This is Bash 4 without quotes. The while loop handling double hyphens is a preview of Topic 6.