πŸ”€ Bash String Manipulation

Comprehensive Reference Guide β€” Parameter expansion, substring operations, pattern matching, case conversion, and validation

πŸ“‹ Table of Contents

Overview

Bash provides powerful string manipulation through parameter expansionβ€”a set of operators that transform variable values without external commands. These built-in operations are faster than calling sed, awk, or cut, and are essential for efficient shell scripting.

Key Capabilities

Extraction: Get substrings by position or pattern

Replacement: Substitute patterns within strings

Trimming: Remove prefixes and suffixes

Case: Convert upper/lower case

Defaults: Handle empty/unset variables

Basic String Operations

String Length

# Get string length
str="Hello World"
echo ${#str}      # 11

# Length of array element
arr=(one two three)
echo ${#arr[2]}   # 5 (length of "three")

Concatenation

# Simple concatenation
first="Hello"
second="World"
combined="$first $second"     # Hello World
combined="${first}${second}"  # HelloWorld

# Append to variable
str="Hello"
str+=" World"    # Hello World

Substring Extraction

# ${var:offset:length}
str="Hello World"
echo ${str:0:5}    # Hello (first 5 chars)
echo ${str:6}      # World (from position 6)
echo ${str:6:3}    # Wor (3 chars from position 6)
echo ${str: -5}    # World (last 5 chars, note space)
echo ${str: -5:3}  # Wor (3 chars from 5th-to-last)
⚠️ Negative Offset Requires Space
${str:-5} means "default value" but ${str: -5} (with space) means "last 5 characters"

Parameter Expansion Reference

Substring Removal (Trimming)

SyntaxDescriptionExampleResult
${var#pattern} Remove shortest match from front ${file#*/} path/to/file.txt β†’ path/to/file.txt
${var##pattern} Remove longest match from front ${file##*/} /path/to/file.txt β†’ file.txt
${var%pattern} Remove shortest match from end ${file%.*} file.tar.gz β†’ file.tar
${var%%pattern} Remove longest match from end ${file%%.*} file.tar.gz β†’ file
πŸ’‘ Memory Aid
# is on the left of $ on the keyboard β†’ removes from front
% is on the right of $ on the keyboard β†’ removes from end

Search and Replace

SyntaxDescriptionExampleResult
${var/pattern/replace} Replace first match ${str/old/new} old old β†’ new old
${var//pattern/replace} Replace all matches ${str//old/new} old old β†’ new new
${var/#pattern/replace} Replace if at beginning ${str/#old/new} old text β†’ new text
${var/%pattern/replace} Replace if at end ${str/%old/new} text old β†’ text new
${var/pattern} Delete first match ${str/old} old old β†’ old
${var//pattern} Delete all matches ${str//old} old old β†’ (empty)

Case Conversion (Bash 4.0+)

SyntaxDescriptionExampleResult
${var^} Uppercase first character ${str^} hello β†’ Hello
${var^^} Uppercase all ${str^^} hello β†’ HELLO
${var,} Lowercase first character ${str,} HELLO β†’ hELLO
${var,,} Lowercase all ${str,,} HELLO β†’ hello
${var~} Toggle first character ${str~} Hello β†’ hello
${var~~} Toggle all ${str~~} Hello β†’ hELLO

Default Values

SyntaxDescriptionWhen Used
${var:-default} Use default if unset or empty Provide fallback value
${var:=default} Assign default if unset or empty Set and use default
${var:+alternate} Use alternate if var is set Conditional value
${var:?error} Error if unset or empty Required variables

Indirect Reference and More

SyntaxDescriptionExample
${!var} Indirect reference name="USER"; echo ${!name}
${!prefix*} Variable names with prefix ${!BASH*}
${var@Q} Quoted for reuse ${str@Q}
${var@E} Expand escapes ${str@E}
${var@L} Lowercase (Bash 5.1+) ${str@L}
${var@U} Uppercase (Bash 5.1+) ${str@U}

Detailed Examples

1
Path and Filename Manipulation

Extracting directory, filename, basename, and extension from pathsβ€”essential for file processing scripts.

#!/bin/bash
# Path and filename manipulation

filepath="/home/craig/documents/report.tar.gz"

echo "=== Full Path Analysis ==="
echo "Full path: $filepath"
echo ""

# Extract components using parameter expansion
echo "=== Using Parameter Expansion ==="
echo "Directory:     ${filepath%/*}"        # Remove from last /
echo "Filename:      ${filepath##*/}"       # Remove up to last /
echo "Extension:     ${filepath##*.}"       # Remove up to last .
echo "Without ext:   ${filepath%.*}"        # Remove from last .
echo "Base name:     ${filepath##*/}"       # Filename with ext
echo "Base no ext:   ${${filepath##*/}%.*}" # Won't work - nested

# For nested operations, use intermediate variable
filename="${filepath##*/}"
echo ""
echo "=== With Intermediate Variable ==="
echo "Filename:      $filename"
echo "Base name:     ${filename%.*}"        # report.tar
echo "First ext:     ${filename##*.}"       # gz
echo "All removed:   ${filename%%.*}"       # report

echo ""
echo "=== Multiple Extensions ==="
echo "Remove .gz:    ${filepath%.gz}"
echo "Remove .tar.gz: ${filepath%.tar.gz}"
echo "Just report:   ${filename%%.*}"

echo ""
echo "=== Comparison with basename/dirname ==="
echo "dirname:       $(dirname "$filepath")"
echo "basename:      $(basename "$filepath")"
echo "basename -s:   $(basename -s .tar.gz "$filepath")"

echo ""
echo "=== Practical: Batch File Rename ==="
files=("photo.jpg" "document.pdf" "script.sh" "data.csv")

for f in "${files[@]}"; do
    name="${f%.*}"
    ext="${f##*.}"
    echo "$f β†’ ${name}_backup.${ext}"
done

echo ""
echo "=== Build Output Filename ==="
input="/var/log/apache/access.log"
output_dir="/backup/logs"
date_stamp=$(date +%Y%m%d)

# Extract just the filename
logname="${input##*/}"
# Build new path
output="${output_dir}/${logname%.log}_${date_stamp}.log.gz"
echo "Input:  $input"
echo "Output: $output"
Output: === Full Path Analysis === Full path: /home/craig/documents/report.tar.gz === Using Parameter Expansion === Directory: /home/craig/documents Filename: report.tar.gz Extension: gz Without ext: /home/craig/documents/report.tar Base name: report.tar.gz === With Intermediate Variable === Filename: report.tar.gz Base name: report.tar First ext: gz All removed: report === Practical: Batch File Rename === photo.jpg β†’ photo_backup.jpg document.pdf β†’ document_backup.pdf script.sh β†’ script_backup.sh data.csv β†’ data_backup.csv === Build Output Filename === Input: /var/log/apache/access.log Output: /backup/logs/access_20251119.log.gz
2
Search and Replace Operations

Substituting patterns within stringsβ€”single occurrence, all occurrences, and positional replacements.

#!/bin/bash
# Search and replace operations

echo "=== Basic Replacement ==="
str="The quick brown fox jumps over the lazy dog"
echo "Original: $str"
echo ""

# Replace first occurrence
echo "First 'the' β†’ 'a':  ${str/the/a}"

# Replace all occurrences (case sensitive)
echo "All 'the' β†’ 'a':    ${str//the/a}"

# Case-insensitive workaround
lower="${str,,}"
echo "All 'THE' (lower):  ${lower//the/a}"

echo ""
echo "=== Positional Replacement ==="
str="old_prefix_data_old_suffix"
echo "Original: $str"

# Replace only at beginning
echo "At start:  ${str/#old/new}"

# Replace only at end
echo "At end:    ${str/%old_suffix/new_suffix}"

echo ""
echo "=== Delete (Replace with Nothing) ==="
str="Hello   World   with   spaces"
echo "Original:     '$str'"
echo "Remove spaces: '${str// /}'"

# Remove specific characters
phone="(555) 123-4567"
echo "Phone:        $phone"
echo "Digits only:  ${phone//[^0-9]/}"

echo ""
echo "=== Replace with Patterns ==="
# Using wildcards in patterns
path="/usr/local/bin/script"
echo "Path: $path"
echo "Replace /usr with /opt: ${path/\/usr/\/opt}"

# Multiple character replacement
str="a-b-c-d-e"
echo "Dashes: $str"
echo "To underscores: ${str//-/_}"

echo ""
echo "=== Practical: Configuration Processing ==="
config_line="DATABASE_HOST=localhost"
echo "Original: $config_line"

# Extract key and value
key="${config_line%%=*}"
value="${config_line#*=}"
echo "Key:   $key"
echo "Value: $value"

# Modify value
new_config="${config_line/localhost/192.168.1.100}"
echo "Updated: $new_config"

echo ""
echo "=== Practical: URL Processing ==="
url="https://example.com/path/to/resource?query=value"
echo "URL: $url"

# Extract protocol
protocol="${url%%://*}"
echo "Protocol: $protocol"

# Extract domain
temp="${url#*://}"
domain="${temp%%/*}"
echo "Domain: $domain"

# Extract path
path="/${temp#*/}"
path="${path%%\?*}"
echo "Path: $path"

echo ""
echo "=== Sanitize User Input ==="
user_input="  Hello <script>alert('xss')</script> World  "
echo "Raw input: '$user_input'"

# Remove leading/trailing spaces (Bash 4.4+)
# Or use: trimmed=$(echo "$user_input" | xargs)
sanitized="${user_input#"${user_input%%[![:space:]]*}"}"
sanitized="${sanitized%"${sanitized##*[![:space:]]}"}"

# Remove potential HTML tags
sanitized="${sanitized//</<}"
sanitized="${sanitized//>/>}"
echo "Sanitized: '$sanitized'"
Output: === Basic Replacement === Original: The quick brown fox jumps over the lazy dog First 'the' β†’ 'a': The quick brown fox jumps over a lazy dog All 'the' β†’ 'a': The quick brown fox jumps over a lazy dog === Positional Replacement === Original: old_prefix_data_old_suffix At start: new_prefix_data_old_suffix At end: old_prefix_data_new_suffix === Delete (Replace with Nothing) === Original: 'Hello World with spaces' Remove spaces: 'HelloWorldwithspaces' Phone: (555) 123-4567 Digits only: 5551234567 === Practical: Configuration Processing === Original: DATABASE_HOST=localhost Key: DATABASE_HOST Value: localhost Updated: DATABASE_HOST=192.168.1.100 === Practical: URL Processing === URL: https://example.com/path/to/resource?query=value Protocol: https Domain: example.com Path: /path/to/resource
3
Case Conversion

Converting between upper and lower case for normalization, comparison, and display.

#!/bin/bash
# Case conversion operations

echo "=== Basic Case Conversion ==="
str="Hello World"
echo "Original:   $str"
echo "UPPERCASE:  ${str^^}"
echo "lowercase:  ${str,,}"
echo "First cap:  ${str^}"
echo "First low:  ${str,}"
echo "Toggle all: ${str~~}"

echo ""
echo "=== Selective Conversion ==="
str="hello world"
# Capitalize specific characters
echo "Cap vowels: ${str^^[aeiou]}"

str="HELLO WORLD"
# Lowercase specific characters
echo "Low vowels: ${str,,[AEIOU]}"

echo ""
echo "=== Case-Insensitive Comparison ==="
input="YES"
# Convert to lowercase for comparison
if [[ "${input,,}" == "yes" ]]; then
    echo "'$input' matches 'yes' (case-insensitive)"
fi

# Alternative using shopt
shopt -s nocasematch
if [[ "$input" == "yes" ]]; then
    echo "nocasematch: '$input' matches 'yes'"
fi
shopt -u nocasematch

echo ""
echo "=== Practical: Username Normalization ==="
usernames=("JohnDoe" "JANE_SMITH" "bob.jones" "Alice")

echo "Normalizing usernames to lowercase:"
for user in "${usernames[@]}"; do
    normalized="${user,,}"
    # Also replace dots with underscores
    normalized="${normalized//./_}"
    echo "  $user β†’ $normalized"
done

echo ""
echo "=== Practical: Title Case ==="
# Bash doesn't have native title case, build it
title_case() {
    local str="$1"
    local result=""
    local words
    read -ra words <<< "$str"
    
    for word in "${words[@]}"; do
        # Lowercase the word, then capitalize first letter
        word="${word,,}"
        word="${word^}"
        result+="$word "
    done
    
    # Remove trailing space
    echo "${result% }"
}

echo "Title case examples:"
echo "  'hello world' β†’ $(title_case 'hello world')"
echo "  'THE QUICK FOX' β†’ $(title_case 'THE QUICK FOX')"
echo "  'mixED CaSe' β†’ $(title_case 'mixED CaSe')"

echo ""
echo "=== Practical: Environment Variable Handling ==="
# Environment variables are typically uppercase
var_name="database_host"
env_name="${var_name^^}"
echo "Variable: $var_name β†’ Env: $env_name"

# Check if env var exists
if [[ -n "${!env_name:-}" ]]; then
    echo "$env_name = ${!env_name}"
else
    echo "$env_name is not set"
fi

echo ""
echo "=== Filename Case Standardization ==="
files=("Document.PDF" "IMAGE.JPG" "Script.SH" "Data.CSV")

echo "Standardizing extensions to lowercase:"
for f in "${files[@]}"; do
    name="${f%.*}"
    ext="${f##*.}"
    ext_lower="${ext,,}"
    echo "  $f β†’ ${name}.${ext_lower}"
done
Output: === Basic Case Conversion === Original: Hello World UPPERCASE: HELLO WORLD lowercase: hello world First cap: Hello World First low: hello World Toggle all: hELLO wORLD === Selective Conversion === Cap vowels: hEllO wOrld Low vowels: HeLLo WoRLD === Case-Insensitive Comparison === 'YES' matches 'yes' (case-insensitive) nocasematch: 'YES' matches 'yes' === Practical: Username Normalization === Normalizing usernames to lowercase: JohnDoe β†’ johndoe JANE_SMITH β†’ jane_smith bob.jones β†’ bob_jones Alice β†’ alice === Practical: Title Case === Title case examples: 'hello world' β†’ Hello World 'THE QUICK FOX' β†’ The Quick Fox 'mixED CaSe' β†’ Mixed Case === Filename Case Standardization === Standardizing extensions to lowercase: Document.PDF β†’ Document.pdf IMAGE.JPG β†’ IMAGE.jpg Script.SH β†’ Script.sh Data.CSV β†’ Data.csv
4
Substring Extraction

Extracting portions of strings by positionβ€”first N characters, last N characters, middle sections.

#!/bin/bash
# Substring extraction by position

str="Hello World Example"

echo "=== Basic Extraction ==="
echo "String: '$str' (length: ${#str})"
echo ""
echo "First 5:    '${str:0:5}'"     # Hello
echo "From 6:     '${str:6}'"       # World Example
echo "6 to 10:    '${str:6:5}'"     # World
echo "Last 7:     '${str: -7}'"     # Example
echo "Last 7, 4:  '${str: -7:4}'"   # Exam

echo ""
echo "=== Negative Length (Bash 4.2+) ==="
# Negative length means "stop before end"
echo "Skip first 6, stop before last 8: '${str:6:-8}'"  # World

echo ""
echo "=== Fixed-Width Field Extraction ==="
# Simulating fixed-width record
record="JohnDoe   25New York  "
#       0123456789012345678901
echo "Record: '$record'"
echo "Name (0-10):  '${record:0:10}'"
echo "Age (10-12):  '${record:10:2}'"
echo "City (12-):   '${record:12}'"

echo ""
echo "=== Practical: Log Parsing ==="
log_line="2025-11-18 14:30:45 ERROR Connection timeout"
echo "Log: $log_line"
echo ""
echo "Date:    ${log_line:0:10}"
echo "Time:    ${log_line:11:8}"
echo "Level:   ${log_line:20:5}"
echo "Message: ${log_line:26}"

echo ""
echo "=== Practical: Format Phone Numbers ==="
format_phone() {
    local digits="${1//[^0-9]/}"  # Remove non-digits
    
    if [[ ${#digits} -eq 10 ]]; then
        echo "(${digits:0:3}) ${digits:3:3}-${digits:6:4}"
    elif [[ ${#digits} -eq 11 && ${digits:0:1} == "1" ]]; then
        echo "+1 (${digits:1:3}) ${digits:4:3}-${digits:7:4}"
    else
        echo "$1 (invalid)"
    fi
}

echo "Phone formatting:"
echo "  5551234567 β†’ $(format_phone '5551234567')"
echo "  15551234567 β†’ $(format_phone '15551234567')"
echo "  555-123-4567 β†’ $(format_phone '555-123-4567')"

echo ""
echo "=== Practical: Truncate with Ellipsis ==="
truncate() {
    local str="$1"
    local max="$2"
    
    if [[ ${#str} -gt $max ]]; then
        echo "${str:0:$((max-3))}..."
    else
        echo "$str"
    fi
}

long_text="This is a very long string that needs truncation"
echo "Original: $long_text"
echo "Max 20:   $(truncate "$long_text" 20)"
echo "Max 30:   $(truncate "$long_text" 30)"
echo "Max 50:   $(truncate "$long_text" 50)"

echo ""
echo "=== Practical: Extract UUID Components ==="
uuid="550e8400-e29b-41d4-a716-446655440000"
echo "UUID: $uuid"
echo ""
echo "Time-low:  ${uuid:0:8}"
echo "Time-mid:  ${uuid:9:4}"
echo "Time-high: ${uuid:14:4}"
echo "Clock:     ${uuid:19:4}"
echo "Node:      ${uuid:24:12}"
Output: === Basic Extraction === String: 'Hello World Example' (length: 19) First 5: 'Hello' From 6: 'World Example' 6 to 10: 'World' Last 7: 'Example' Last 7, 4: 'Exam' === Fixed-Width Field Extraction === Record: 'JohnDoe 25New York ' Name (0-10): 'JohnDoe ' Age (10-12): '25' City (12-): 'New York ' === Practical: Log Parsing === Log: 2025-11-18 14:30:45 ERROR Connection timeout Date: 2025-11-18 Time: 14:30:45 Level: ERROR Message: Connection timeout === Practical: Format Phone Numbers === Phone formatting: 5551234567 β†’ (555) 123-4567 15551234567 β†’ +1 (555) 123-4567 555-123-4567 β†’ (555) 123-4567 === Practical: Truncate with Ellipsis === Original: This is a very long string that needs truncation Max 20: This is a very l... Max 30: This is a very long str... Max 50: This is a very long string that needs truncation
5
Default Values and Required Variables

Handling unset or empty variables gracefully with defaults, assignments, and error checking.

#!/bin/bash
# Default values and required variables

echo "=== Use Default If Unset/Empty ==="
unset myvar
echo "Unset: '${myvar:-default}'"   # Uses default

myvar=""
echo "Empty: '${myvar:-default}'"   # Uses default

myvar="value"
echo "Set:   '${myvar:-default}'"   # Uses value

echo ""
echo "=== Assign Default If Unset/Empty ==="
unset myvar
echo "Before: myvar='$myvar'"
: ${myvar:=assigned_default}        # Assigns if unset/empty
echo "After:  myvar='$myvar'"

echo ""
echo "=== Use Alternate If Set ==="
unset myvar
echo "Unset: '${myvar:+alternate}'" # Empty (var not set)

myvar="value"
echo "Set:   '${myvar:+alternate}'" # Uses alternate

# Practical: conditional flag
verbose=""
echo "Verbose flag: ${verbose:+--verbose}"

verbose="yes"
echo "Verbose flag: ${verbose:+--verbose}"

echo ""
echo "=== Error If Unset/Empty ==="
# This would exit the script:
# : ${REQUIRED_VAR:?Error: REQUIRED_VAR must be set}

# Safe way to demonstrate
check_required() {
    local var_name=$1
    local var_value="${!var_name}"
    
    if [[ -z "$var_value" ]]; then
        echo "ERROR: $var_name is required but not set"
        return 1
    else
        echo "OK: $var_name = '$var_value'"
        return 0
    fi
}

DATABASE_HOST="localhost"
unset DATABASE_PORT

check_required DATABASE_HOST
check_required DATABASE_PORT

echo ""
echo "=== Practical: Script Configuration ==="
# Common pattern for script defaults
CONFIG_FILE="${CONFIG_FILE:-/etc/myapp/config.conf}"
LOG_LEVEL="${LOG_LEVEL:-INFO}"
MAX_RETRIES="${MAX_RETRIES:-3}"
TIMEOUT="${TIMEOUT:-30}"

echo "Configuration:"
echo "  CONFIG_FILE: $CONFIG_FILE"
echo "  LOG_LEVEL:   $LOG_LEVEL"
echo "  MAX_RETRIES: $MAX_RETRIES"
echo "  TIMEOUT:     $TIMEOUT"

echo ""
echo "=== Practical: Command Line Defaults ==="
# Simulate processing arguments with defaults
process_args() {
    local input="${1:-/dev/stdin}"
    local output="${2:-/dev/stdout}"
    local format="${3:-json}"
    
    echo "Processing:"
    echo "  Input:  $input"
    echo "  Output: $output"
    echo "  Format: $format"
}

echo "No args:"
process_args

echo ""
echo "With args:"
process_args "data.txt" "output.csv" "csv"

echo ""
echo "=== Practical: Safe Variable Expansion ==="
# Avoid errors with unset variables in strict mode
# set -u  # Would cause errors on unset variables

# Safe patterns
name="${USER:-unknown}"
home="${HOME:-/tmp}"
shell="${SHELL:-/bin/sh}"

echo "Safe defaults:"
echo "  name=$name, home=$home, shell=$shell"

# Check before use pattern
if [[ -n "${OPTIONAL_VAR:-}" ]]; then
    echo "OPTIONAL_VAR is set: $OPTIONAL_VAR"
else
    echo "OPTIONAL_VAR is not set"
fi
Output: === Use Default If Unset/Empty === Unset: 'default' Empty: 'default' Set: 'value' === Assign Default If Unset/Empty === Before: myvar='' After: myvar='assigned_default' === Use Alternate If Set === Unset: '' Set: 'alternate' Verbose flag: Verbose flag: --verbose === Error If Unset/Empty === OK: DATABASE_HOST = 'localhost' ERROR: DATABASE_PORT is required but not set === Practical: Script Configuration === Configuration: CONFIG_FILE: /etc/myapp/config.conf LOG_LEVEL: INFO MAX_RETRIES: 3 TIMEOUT: 30 === Practical: Command Line Defaults === No args: Processing: Input: /dev/stdin Output: /dev/stdout Format: json With args: Processing: Input: data.txt Output: output.csv Format: csv
⚠️ With vs Without Colon
${var-default} only checks if unset
${var:-default} checks if unset OR empty
The colon makes it also check for empty string.
6
Pattern Matching with Regex

Using regex for validation, extraction, and complex pattern matching with [[ =~ ]].

#!/bin/bash
# Regex pattern matching

echo "=== Basic Regex Matching ==="
str="Error: Connection failed at 192.168.1.100:3306"

if [[ $str =~ Error ]]; then
    echo "String contains 'Error'"
fi

if [[ $str =~ ^Error ]]; then
    echo "String starts with 'Error'"
fi

echo ""
echo "=== Capture Groups with BASH_REMATCH ==="
str="User john logged in from 192.168.1.50"

if [[ $str =~ User\ ([a-z]+)\ logged\ in\ from\ ([0-9.]+) ]]; then
    echo "Full match: ${BASH_REMATCH[0]}"
    echo "Username:   ${BASH_REMATCH[1]}"
    echo "IP:         ${BASH_REMATCH[2]}"
fi

echo ""
echo "=== Validation Patterns ==="

# Email validation (simplified)
validate_email() {
    local email=$1
    if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
        echo "$email: VALID"
    else
        echo "$email: INVALID"
    fi
}

validate_email "= user@example.com"
validate_email "invalid@"
validate_email "= user@example.com"

echo ""
# IP address validation
validate_ip() {
    local ip=$1
    local octet="([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"
    local regex="^${octet}\.${octet}\.${octet}\.${octet}$"
    
    if [[ $ip =~ $regex ]]; then
        echo "$ip: VALID"
    else
        echo "$ip: INVALID"
    fi
}

validate_ip "192.168.1.1"
validate_ip "256.1.1.1"
validate_ip "10.0.0.255"

echo ""
echo "=== Extract Multiple Matches ==="
log="Error at 10:30:45, Warning at 11:15:22, Error at 14:00:00"

# Find all times
echo "Times found:"
remaining="$log"
while [[ $remaining =~ ([0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; do
    echo "  ${BASH_REMATCH[1]}"
    # Remove the match to find next
    remaining="${remaining#*${BASH_REMATCH[1]}}"
done

echo ""
echo "=== Practical: Parse Key=Value Pairs ==="
parse_kvp() {
    local str="$1"
    
    if [[ $str =~ ^([^=]+)=(.*)$ ]]; then
        echo "Key:   '${BASH_REMATCH[1]}'"
        echo "Value: '${BASH_REMATCH[2]}'"
    else
        echo "Invalid format: $str"
    fi
}

parse_kvp "name=John Doe"
echo ""
parse_kvp "DATABASE_URL=mysql://localhost:3306/mydb"

echo ""
echo "=== Practical: Version Number Extraction ==="
version_string="MyApp version 2.4.1-beta (build 1234)"

if [[ $version_string =~ ([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-z]+)? ]]; then
    echo "Version: ${BASH_REMATCH[0]}"
    echo "  Major: ${BASH_REMATCH[1]}"
    echo "  Minor: ${BASH_REMATCH[2]}"
    echo "  Patch: ${BASH_REMATCH[3]}"
    echo "  Tag:   ${BASH_REMATCH[4]:-none}"
fi

echo ""
echo "=== Practical: Log Level Detection ==="
detect_level() {
    local line="$1"
    
    if [[ $line =~ (ERROR|FATAL|CRITICAL) ]]; then
        echo "CRITICAL: $line"
    elif [[ $line =~ (WARN|WARNING) ]]; then
        echo "WARNING: $line"
    elif [[ $line =~ (INFO) ]]; then
        echo "INFO: $line"
    else
        echo "OTHER: $line"
    fi
}

detect_level "2025-11-18 ERROR Database connection failed"
detect_level "2025-11-18 WARNING Disk space low"
detect_level "2025-11-18 INFO Service started"
Output: === Basic Regex Matching === String contains 'Error' String starts with 'Error' === Capture Groups with BASH_REMATCH === Full match: User john logged in from 192.168.1.50 Username: john IP: 192.168.1.50 === Validation Patterns === = user@example.com: VALID invalid@: INVALID = user@example.com: VALID 192.168.1.1: VALID 256.1.1.1: INVALID 10.0.0.255: VALID === Extract Multiple Matches === Times found: 10:30:45 11:15:22 14:00:00 === Practical: Parse Key=Value Pairs === Key: 'name' Value: 'John Doe' Key: 'DATABASE_URL' Value: 'mysql://localhost:3306/mydb' === Practical: Version Number Extraction === Version: 2.4.1-beta Major: 2 Minor: 4 Patch: 1 Tag: -beta
7
String Padding and Alignment

Padding strings to fixed widths for formatted output, reports, and data alignment.

#!/bin/bash
# String padding and alignment

echo "=== printf for Padding ==="
str="Hello"

printf "Right-align 15: '%15s'\n" "$str"
printf "Left-align 15:  '%-15s'\n" "$str"
printf "Zero-pad num:   '%08d'\n" 42

echo ""
echo "=== Custom Padding Functions ==="

# Pad right (left-align)
pad_right() {
    local str="$1"
    local width="$2"
    local char="${3:- }"
    printf "%-${width}s" "$str" | tr ' ' "$char"
}

# Pad left (right-align)
pad_left() {
    local str="$1"
    local width="$2"
    local char="${3:- }"
    printf "%${width}s" "$str" | tr ' ' "$char"
}

# Center
center() {
    local str="$1"
    local width="$2"
    local len=${#str}
    local padding=$(( (width - len) / 2 ))
    printf "%*s%s%*s" $padding "" "$str" $((width - len - padding)) ""
}

echo "Pad right: '$(pad_right "Test" 10)'"
echo "Pad left:  '$(pad_left "Test" 10)'"
echo "Pad zeros: '$(pad_left "42" 8 "0")'"
echo "Center:    '$(center "Title" 20)'"

echo ""
echo "=== Practical: Table Formatting ==="
# Data
declare -a data=(
    "web-01|192.168.1.10|Running|45%"
    "web-02|192.168.1.11|Running|62%"
    "db-01|192.168.1.20|Stopped|0%"
    "cache-01|192.168.1.30|Running|28%"
)

# Header
printf "%-12s %-15s %-10s %6s\n" "Server" "IP Address" "Status" "CPU"
printf "%-12s %-15s %-10s %6s\n" "------" "----------" "------" "---"

# Rows
for row in "${data[@]}"; do
    IFS='|' read -r name ip status cpu <<< "$row"
    printf "%-12s %-15s %-10s %6s\n" "$name" "$ip" "$status" "$cpu"
done

echo ""
echo "=== Practical: Progress Bar ==="
progress_bar() {
    local current=$1
    local total=$2
    local width=${3:-40}
    
    local percent=$((current * 100 / total))
    local filled=$((current * width / total))
    local empty=$((width - filled))
    
    # Build bar
    local bar=""
    for ((i=0; i<filled; i++)); do bar+="β–ˆ"; done
    for ((i=0; i<empty; i++)); do bar+="β–‘"; done
    
    printf "\r[%s] %3d%%" "$bar" "$percent"
}

echo "Progress bar example:"
for i in {0..100..10}; do
    progress_bar $i 100 30
    sleep 0.1
done
echo ""

echo ""
echo "=== Practical: Fixed-Width Record ==="
# Create fixed-width record
create_record() {
    local name="$1"
    local age="$2"
    local city="$3"
    
    # Fixed widths: name=20, age=3, city=15
    printf "%-20s%3s%-15s\n" \
        "${name:0:20}" \
        "${age:0:3}" \
        "${city:0:15}"
}

echo "Fixed-width records:"
create_record "John Doe" "25" "New York"
create_record "Jane Smith" "32" "Los Angeles"
create_record "Bob Johnson Jr." "45" "San Francisco"

echo ""
echo "=== Repeat Character ==="
repeat_char() {
    local char="$1"
    local count="$2"
    printf "%${count}s" | tr ' ' "$char"
}

echo "Divider: $(repeat_char '=' 40)"
echo "Dots:    $(repeat_char '.' 20)"
Output: === printf for Padding === Right-align 15: ' Hello' Left-align 15: 'Hello ' Zero-pad num: '00000042' === Custom Padding Functions === Pad right: 'Test ' Pad left: ' Test' Pad zeros: '00000042' Center: ' Title ' === Practical: Table Formatting === Server IP Address Status CPU ------ ---------- ------ --- web-01 192.168.1.10 Running 45% web-02 192.168.1.11 Running 62% db-01 192.168.1.20 Stopped 0% cache-01 192.168.1.30 Running 28% === Practical: Fixed-Width Record === Fixed-width records: John Doe 25New York Jane Smith 32Los Angeles Bob Johnson Jr. 45San Francisco === Repeat Character === Divider: ======================================== Dots: ....................
8
String Splitting and Joining

Splitting strings into arrays and joining array elements back into strings.

#!/bin/bash
# String splitting and joining

echo "=== Split String to Array ==="

# Using read with IFS
str="one:two:three:four"
IFS=':' read -ra arr <<< "$str"
echo "String: $str"
echo "Array:  ${arr[*]}"
echo "Count:  ${#arr[@]}"

echo ""
echo "=== Split on Different Delimiters ==="

# Comma-separated
csv="apple,banana,cherry,date"
IFS=',' read -ra fruits <<< "$csv"
echo "CSV: ${fruits[*]}"

# Multi-character delimiter (workaround)
str="item1||item2||item3"
# Replace delimiter with single char first
temp="${str//||/$'\n'}"
readarray -t items <<< "$temp"
echo "Double-pipe: ${items[*]}"

echo ""
echo "=== Join Array to String ==="

# Using printf and parameter expansion
arr=("one" "two" "three" "four")

# Join with comma
joined=$(IFS=','; echo "${arr[*]}")
echo "Comma-joined: $joined"

# Join with custom delimiter
join_array() {
    local delim="$1"
    shift
    local first="$1"
    shift
    printf "%s" "$first" "${@/#/$delim}"
}

echo "Pipe-joined:  $(join_array '|' "${arr[@]}")"
echo "Dash-joined:  $(join_array ' - ' "${arr[@]}")"

echo ""
echo "=== Practical: PATH Manipulation ==="
echo "Current PATH:"
IFS=':' read -ra paths <<< "$PATH"
for i in "${!paths[@]}"; do
    echo "  [$i] ${paths[$i]}"
done | head -5
echo "  ... (truncated)"

# Add to PATH
new_path="/opt/myapp/bin"
if [[ ":$PATH:" != *":$new_path:"* ]]; then
    PATH="$new_path:$PATH"
    echo "Added: $new_path"
fi

echo ""
echo "=== Practical: CSV Processing ==="
csv_data="name,age,city
John,25,New York
Jane,30,Boston
Bob,35,Chicago"

echo "CSV Data:"
while IFS=',' read -r name age city; do
    printf "  Name: %-8s Age: %s  City: %s\n" "$name" "$age" "$city"
done <<< "$csv_data"

echo ""
echo "=== Practical: Parse Command Output ==="
# Simulate parsing 'ls -l' output
ls_output="drwxr-xr-x 2 user group 4096 Nov 18 14:30 documents"

read -r perms links owner group size month day time name <<< "$ls_output"
echo "Parsed ls output:"
echo "  Name:  $name"
echo "  Owner: $owner"
echo "  Size:  $size"
echo "  Date:  $month $day $time"

echo ""
echo "=== Split Preserving Empty Fields ==="
# Default IFS skips empty fields
str="one::three"
IFS=':' read -ra arr <<< "$str"
echo "Default: ${#arr[@]} elements"  # Only 2!

# Use -d '' to preserve
readarray -d ':' -t arr <<< "$str:"
# Remove trailing newline from last element
arr[-1]="${arr[-1]%$'\n'}"
arr[-1]="${arr[-1]%:}"
echo "With -d: ${#arr[@]} elements"
echo "Elements: '${arr[0]}' '${arr[1]}' '${arr[2]}'"
Output: === Split String to Array === String: one:two:three:four Array: one two three four Count: 4 === Split on Different Delimiters === CSV: apple banana cherry date Double-pipe: item1 item2 item3 === Join Array to String === Comma-joined: one,two,three,four Pipe-joined: one|two|three|four Dash-joined: one - two - three - four === Practical: PATH Manipulation === Current PATH: [0] /usr/local/sbin [1] /usr/local/bin [2] /usr/sbin [3] /usr/bin [4] /sbin ... (truncated) Added: /opt/myapp/bin === Practical: CSV Processing === CSV Data: Name: name Age: age City: city Name: John Age: 25 City: New York Name: Jane Age: 30 City: Boston Name: Bob Age: 35 City: Chicago === Practical: Parse Command Output === Parsed ls output: Name: documents Owner: user Size: 4096 Date: Nov 18 14:30
9
Trimming Whitespace

Removing leading, trailing, and excess whitespace from strings.

#!/bin/bash
# Trimming whitespace

echo "=== Basic Trim Functions ==="

# Trim leading whitespace
trim_left() {
    local var="$1"
    var="${var#"${var%%[![:space:]]*}"}"
    echo "$var"
}

# Trim trailing whitespace
trim_right() {
    local var="$1"
    var="${var%"${var##*[![:space:]]}"}"
    echo "$var"
}

# Trim both ends
trim() {
    local var="$1"
    var="${var#"${var%%[![:space:]]*}"}"
    var="${var%"${var##*[![:space:]]}"}"
    echo "$var"
}

str="   Hello World   "
echo "Original:    '$str'"
echo "Trim left:   '$(trim_left "$str")'"
echo "Trim right:  '$(trim_right "$str")'"
echo "Trim both:   '$(trim "$str")'"

echo ""
echo "=== Alternative Methods ==="

# Using xargs (also collapses internal spaces)
str="   Hello    World   "
echo "Original: '$str'"
echo "xargs:    '$(echo "$str" | xargs)'"

# Using sed
echo "sed trim: '$(echo "$str" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')'"

# Using awk
echo "awk trim: '$(echo "$str" | awk '{$1=$1};1')'"

echo ""
echo "=== Collapse Multiple Spaces ==="
str="Hello    World    with    spaces"
echo "Original:  '$str'"
echo "Collapsed: '${str//+([[:space:]])/ }'"

# Need extglob for +() pattern
shopt -s extglob
collapsed="${str//+([[:space:]])/ }"
shopt -u extglob
echo "Collapsed: '$collapsed'"

echo ""
echo "=== Remove All Whitespace ==="
str="  H e l l o   W o r l d  "
echo "Original:  '$str'"
echo "No spaces: '${str//[[:space:]]/}'"

echo ""
echo "=== Practical: Clean User Input ==="
clean_input() {
    local input="$1"
    
    # Trim leading/trailing
    input="${input#"${input%%[![:space:]]*}"}"
    input="${input%"${input##*[![:space:]]}"}"
    
    # Collapse multiple spaces
    shopt -s extglob
    input="${input//+([[:space:]])/ }"
    shopt -u extglob
    
    echo "$input"
}

inputs=(
    "   John   Doe   "
    "	Tab	Separated	"
    "  Multiple   Spaces   Here  "
)

echo "Cleaning user input:"
for input in "${inputs[@]}"; do
    cleaned=$(clean_input "$input")
    echo "  '$input' β†’ '$cleaned'"
done

echo ""
echo "=== Practical: Clean Config Values ==="
parse_config() {
    local line="$1"
    
    # Split on =
    local key="${line%%=*}"
    local value="${line#*=}"
    
    # Trim whitespace from both
    key="${key#"${key%%[![:space:]]*}"}"
    key="${key%"${key##*[![:space:]]}"}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    
    # Remove quotes if present
    value="${value#\"}"
    value="${value%\"}"
    value="${value#\'}"
    value="${value%\'}"
    
    echo "Key: '$key', Value: '$value'"
}

config_lines=(
    "  DATABASE_HOST = localhost  "
    "PORT=3306"
    "  NAME  =  \"My Database\"  "
)

echo "Parsing config:"
for line in "${config_lines[@]}"; do
    parse_config "$line"
done

echo ""
echo "=== Trim Specific Characters ==="
str="###Hello###"
echo "Original:  '$str'"
echo "No hashes: '${str//\#/}'"
echo "Trim left: '${str##\#\#\#}'"
echo "Trim both: '${str//\#/}'"

# Trim specific leading chars
str="000042"
echo "Leading zeros: '${str##+(0)}'"  # Needs extglob
Output: === Basic Trim Functions === Original: ' Hello World ' Trim left: 'Hello World ' Trim right: ' Hello World' Trim both: 'Hello World' === Alternative Methods === Original: ' Hello World ' xargs: 'Hello World' sed trim: 'Hello World' awk trim: 'Hello World' === Collapse Multiple Spaces === Original: 'Hello World with spaces' Collapsed: 'Hello World with spaces' === Remove All Whitespace === Original: ' H e l l o W o r l d ' No spaces: 'HelloWorld' === Practical: Clean User Input === Cleaning user input: ' John Doe ' β†’ 'John Doe' ' Tab Separated ' β†’ 'Tab Separated' ' Multiple Spaces Here ' β†’ 'Multiple Spaces Here' === Practical: Clean Config Values === Parsing config: Key: 'DATABASE_HOST', Value: 'localhost' Key: 'PORT', Value: '3306' Key: 'NAME', Value: 'My Database'
10
Complete String Library

A reusable library of string manipulation functions for your scripts.

#!/bin/bash
# Complete string manipulation library

#######################################
# Basic Operations
#######################################

# String length
str_length() {
    echo "${#1}"
}

# Check if empty
str_empty() {
    [[ -z "$1" ]]
}

# Check if contains substring
str_contains() {
    [[ "$1" == *"$2"* ]]
}

# Check if starts with
str_starts_with() {
    [[ "$1" == "$2"* ]]
}

# Check if ends with
str_ends_with() {
    [[ "$1" == *"$2" ]]
}

#######################################
# Case Conversion
#######################################

str_upper() {
    echo "${1^^}"
}

str_lower() {
    echo "${1,,}"
}

str_capitalize() {
    echo "${1^}"
}

#######################################
# Trimming
#######################################

str_trim() {
    local var="$1"
    var="${var#"${var%%[![:space:]]*}"}"
    var="${var%"${var##*[![:space:]]}"}"
    echo "$var"
}

str_trim_left() {
    echo "${1#"${1%%[![:space:]]*}"}"
}

str_trim_right() {
    echo "${1%"${1##*[![:space:]]}"}"
}

#######################################
# Extraction
#######################################

str_substring() {
    local str="$1"
    local start="$2"
    local length="${3:-${#str}}"
    echo "${str:start:length}"
}

str_left() {
    echo "${1:0:$2}"
}

str_right() {
    echo "${1: -$2}"
}

#######################################
# Search and Replace
#######################################

str_replace() {
    echo "${1//$2/$3}"
}

str_replace_first() {
    echo "${1/$2/$3}"
}

str_remove() {
    echo "${1//$2/}"
}

#######################################
# Path Operations
#######################################

str_dirname() {
    echo "${1%/*}"
}

str_basename() {
    echo "${1##*/}"
}

str_extension() {
    local name="${1##*/}"
    echo "${name##*.}"
}

str_strip_extension() {
    echo "${1%.*}"
}

#######################################
# Splitting and Joining
#######################################

str_split() {
    local str="$1"
    local delim="$2"
    local -n arr="$3"
    IFS="$delim" read -ra arr <<< "$str"
}

str_join() {
    local delim="$1"
    shift
    local first="$1"
    shift
    printf "%s" "$first" "${@/#/$delim}"
}

#######################################
# Validation
#######################################

str_is_number() {
    [[ "$1" =~ ^-?[0-9]+\.?[0-9]*$ ]]
}

str_is_integer() {
    [[ "$1" =~ ^-?[0-9]+$ ]]
}

str_is_alpha() {
    [[ "$1" =~ ^[a-zA-Z]+$ ]]
}

str_is_alnum() {
    [[ "$1" =~ ^[a-zA-Z0-9]+$ ]]
}

#######################################
# Utility
#######################################

str_repeat() {
    local str="$1"
    local count="$2"
    local result=""
    for ((i=0; i<count; i++)); do
        result+="$str"
    done
    echo "$result"
}

str_reverse() {
    echo "$1" | rev
}

str_pad_left() {
    printf "%$2s" "$1"
}

str_pad_right() {
    printf "%-$2s" "$1"
}

#######################################
# Demo
#######################################

echo "========================================"
echo "    String Library Demo"
echo "========================================"

echo ""
echo "=== Basic Operations ==="
str="Hello World"
echo "str_length '$str': $(str_length "$str")"
str_contains "$str" "World" && echo "Contains 'World': yes"
str_starts_with "$str" "Hello" && echo "Starts with 'Hello': yes"
str_ends_with "$str" "World" && echo "Ends with 'World': yes"

echo ""
echo "=== Case Conversion ==="
echo "str_upper 'hello': $(str_upper 'hello')"
echo "str_lower 'HELLO': $(str_lower 'HELLO')"
echo "str_capitalize 'hello': $(str_capitalize 'hello')"

echo ""
echo "=== Trimming ==="
echo "str_trim '  hello  ': '$(str_trim '  hello  ')'"

echo ""
echo "=== Extraction ==="
echo "str_substring 'Hello World' 0 5: '$(str_substring 'Hello World' 0 5)'"
echo "str_left 'Hello World' 5: '$(str_left 'Hello World' 5)'"
echo "str_right 'Hello World' 5: '$(str_right 'Hello World' 5)'"

echo ""
echo "=== Search and Replace ==="
echo "str_replace 'foo bar foo' 'foo' 'baz': '$(str_replace 'foo bar foo' 'foo' 'baz')'"
echo "str_remove 'Hello World' 'l': '$(str_remove 'Hello World' 'l')'"

echo ""
echo "=== Path Operations ==="
path="/home/user/document.txt"
echo "str_dirname '$path': $(str_dirname "$path")"
echo "str_basename '$path': $(str_basename "$path")"
echo "str_extension '$path': $(str_extension "$path")"

echo ""
echo "=== Validation ==="
str_is_number "42.5" && echo "'42.5' is number: yes"
str_is_integer "42" && echo "'42' is integer: yes"
str_is_alpha "Hello" && echo "'Hello' is alpha: yes"

echo ""
echo "=== Utility ==="
echo "str_repeat '*' 10: '$(str_repeat '*' 10)'"
echo "str_reverse 'Hello': '$(str_reverse 'Hello')'"

echo ""
echo "========================================"
Output: ======================================== String Library Demo ======================================== === Basic Operations === str_length 'Hello World': 11 Contains 'World': yes Starts with 'Hello': yes Ends with 'World': yes === Case Conversion === str_upper 'hello': HELLO str_lower 'HELLO': hello str_capitalize 'hello': Hello === Trimming === str_trim ' hello ': 'hello' === Extraction === str_substring 'Hello World' 0 5: 'Hello' str_left 'Hello World' 5: 'Hello' str_right 'Hello World' 5: 'World' === Search and Replace === str_replace 'foo bar foo' 'foo' 'baz': 'baz bar baz' str_remove 'Hello World' 'l': 'Heo Word' === Path Operations === str_dirname '/home/user/document.txt': /home/user str_basename '/home/user/document.txt': document.txt str_extension '/home/user/document.txt': txt === Validation === '42.5' is number: yes '42' is integer: yes 'Hello' is alpha: yes === Utility === str_repeat '*' 10: '**********' str_reverse 'Hello': 'olleH' ========================================

Quick Reference

Essential Parameter Expansion

# Length
${#var}                    # String length

# Substring
${var:offset:length}       # Extract substring
${var: -n}                 # Last n characters

# Trim patterns
${var#pattern}             # Remove shortest from front
${var##pattern}            # Remove longest from front
${var%pattern}             # Remove shortest from end
${var%%pattern}            # Remove longest from end

# Replace
${var/find/replace}        # First occurrence
${var//find/replace}       # All occurrences
${var/#find/replace}       # If at beginning
${var/%find/replace}       # If at end

# Case (Bash 4+)
${var^^}                   # UPPERCASE
${var,,}                   # lowercase
${var^}                    # First char upper

# Defaults
${var:-default}            # Use default if unset/empty
${var:=default}            # Assign default
${var:+alternate}          # Use alternate if set
${var:?error}              # Error if unset/empty

Common Patterns

TaskCode
Get filename${path##*/}
Get directory${path%/*}
Get extension${file##*.}
Remove extension${file%.*}
Check contains[[ $str == *"sub"* ]]
Check starts[[ $str == "pre"* ]]
Check ends[[ $str == *"suf" ]]
Split to arrayIFS=':' read -ra arr <<< "$str"
βœ“ Best Practices
βœ— Common Mistakes
← Back to BashSpecial Index ↑ Back to EXPANDED