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.
Found an issue?