2024-11-08
5 min read

How to Convert Strings to Lowercase in Bash

How to Convert Strings to Lowercase in Bash

TLDR

In Bash 4 and later, use parameter expansion with ${variable,,} to convert a string to lowercase. For older shells or POSIX compatibility, use tr '[:upper:]' '[:lower:]' or awk '{print tolower($0)}'. Each method has different performance characteristics and portability considerations.

Using Bash Parameter Expansion

Modern Bash (version 4.0+) provides built-in syntax for case conversion:

#!/bin/bash

name="PRODUCTION"
lowercase="${name,,}"
echo "$lowercase"  # Outputs: production

The ,, operator converts all characters to lowercase. For a single character:

name="PRODUCTION"
first_lower="${name,}"
echo "$first_lower"  # Outputs: pRODUCTION

This approach is fast because it doesn't spawn external processes. Here's a practical example validating environment names:

#!/bin/bash

read -p "Enter environment (dev/staging/production): " env
env_lower="${env,,}"

case "$env_lower" in
    dev|development)
        echo "Deploying to development"
        ;;
    staging|stage)
        echo "Deploying to staging"
        ;;
    production|prod)
        echo "Deploying to production"
        ;;
    *)
        echo "Unknown environment: $env"
        exit 1
        ;;
esac

Converting input to lowercase makes your scripts more user-friendly by accepting "Dev", "DEV", or "dev" interchangeably.

Using tr Command

The tr (translate) command works in all POSIX shells and is available on virtually every Unix-like system:

#!/bin/sh

name="PRODUCTION"
lowercase=$(echo "$name" | tr '[:upper:]' '[:lower:]')
echo "$lowercase"  # Outputs: production

While this spawns a subprocess, it's still fast for reasonable amounts of text. Here's how it works in a file processing script:

#!/bin/bash

while IFS= read -r line; do
    lowercase_line=$(echo "$line" | tr '[:upper:]' '[:lower:]')
    echo "$lowercase_line"
done < input.txt > output.txt

The [:upper:] and [:lower:] character classes are locale-aware and handle international characters better than A-Z and a-z.

Using awk

AWK's tolower() function provides another portable option:

#!/bin/bash

name="PRODUCTION"
lowercase=$(echo "$name" | awk '{print tolower($0)}')
echo "$lowercase"  # Outputs: production

AWK is particularly useful when you're already processing text with it:

#!/bin/bash

# Convert the second column to lowercase in a CSV
awk -F, '{$2=tolower($2); print}' OFS=, data.csv

This reads a CSV file, converts the second field to lowercase, and outputs the modified CSV.

Using sed

While less common for simple case conversion, sed can handle it:

#!/bin/bash

name="PRODUCTION"
lowercase=$(echo "$name" | sed 's/.*/\L&/')
echo "$lowercase"  # Outputs: production

The \L escape sequence converts everything after it to lowercase. For GNU sed specifically, you can use:

lowercase=$(echo "$name" | sed 's/[A-Z]/\L&/g')

However, this GNU extension isn't portable to BSD sed (used on macOS), so tr or awk are better choices for portability.

Converting File Contents

When working with entire files, you have several approaches depending on whether you need to modify the file in place or create a new one:

#!/bin/bash

# Create a new lowercase version
tr '[:upper:]' '[:lower:]' < input.txt > output.txt

# Or using awk
awk '{print tolower($0)}' input.txt > output.txt

# In-place modification with a temp file
tr '[:upper:]' '[:lower:]' < config.txt > config.txt.tmp &&
    mv config.txt.tmp config.txt

For large files, these stream-based approaches are memory-efficient since they process line by line.

Performance Comparison

When processing a single string in a loop, the choice of method matters:

#!/bin/bash

# Fastest: Parameter expansion (no external process)
for file in *.TXT; do
    newname="${file,,}"
    mv "$file" "$newname"
done

# Slower: External command (spawns process each iteration)
for file in *.TXT; do
    newname=$(echo "$file" | tr '[:upper:]' '[:lower:]')
    mv "$file" "$newname"
done

The parameter expansion approach is significantly faster when called thousands of times because it doesn't create subprocess overhead.

The processing flow looks like:

Parameter expansion (${var,,})
    |
    v
Built-in string operation
    |
    v
No subprocess needed
    |
    v
Fast execution


External commands (tr, awk, sed)
    |
    v
Fork subprocess
    |
    v
Execute external binary
    |
    v
Return result
    |
    v
Slower but more portable

Uppercase Conversion

The inverse operation uses similar syntax:

# Bash parameter expansion
uppercase="${name^^}"

# tr command
uppercase=$(echo "$name" | tr '[:lower:]' '[:upper:]')

# awk
uppercase=$(echo "$name" | awk '{print toupper($0)}')

Practical Example: Normalizing Usernames

Here's a complete script that normalizes usernames to lowercase for consistent processing:

#!/bin/bash

normalize_username() {
    local username="$1"

    # Convert to lowercase
    username="${username,,}"

    # Remove invalid characters (keep only alphanumeric and underscore)
    username=$(echo "$username" | tr -cd '[:alnum:]_')

    # Truncate to 32 characters
    username="${username:0:32}"

    echo "$username"
}

# Read usernames from file and normalize them
while IFS= read -r raw_username; do
    normalized=$(normalize_username "$raw_username")
    echo "Original: $raw_username -> Normalized: $normalized"
done < users.txt

This function combines lowercase conversion with other validation steps to create clean usernames.

Shell Compatibility

Different shells support different methods:

Bash 4.0+: Parameter expansion (${var,,})
All POSIX shells: tr, awk, sed
Zsh: Parameter expansion with ${(L)var}
Dash/ash: Only external commands like tr

Check your Bash version:

bash --version

If you're writing portable scripts that run on various systems, stick with tr:

#!/bin/sh
# Works on bash, dash, zsh, and other POSIX shells

username="JohnDoe"
lowercase=$(printf '%s' "$username" | tr '[:upper:]' '[:lower:]')
echo "$lowercase"

Using printf instead of echo avoids potential issues with strings that start with - or contain escape sequences.

Handling Special Characters

Be aware of locale settings when converting case:

#!/bin/bash

# Turkish has special dotted and dotless i
export LC_ALL=en_US.UTF-8

name="İSTANBUL"
lowercase=$(echo "$name" | tr '[:upper:]' '[:lower:]')
echo "$lowercase"

The character class [:upper:] and [:lower:] respect the current locale, which determines which characters are considered uppercase or lowercase.

For consistent behavior across different systems, you might want to explicitly set LC_ALL=C for simple ASCII-only processing:

LC_ALL=C tr '[:upper:]' '[:lower:]'

This forces basic ASCII behavior regardless of the system's locale settings.

When choosing a method, consider your needs: use parameter expansion for performance in Bash-specific scripts, tr for portability across shells, and awk when you're already processing text. The right tool depends on whether you prioritize speed, portability, or integration with existing text processing pipelines.

Published: 2024-11-08|Last updated: 2024-11-08T09:00:00Z

Found an issue?