strtoupper() in Bash: Convert a String to Uppercase
Shell scripts need to uppercase text more often than you would think. Examples are turning an app name into an environment variable name or normalising a "y" or "yes" answer. Another is printing a region code or a log level the way everyone expects to see it. PHP calls it strtoupper(). In Bash 4 and later it is the ${var^^} expansion, which can also do something PHP's function cannot: uppercase only the characters that match a pattern.
The function
# strtoupper -- print a string in uppercase
# Usage: strtoupper string
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
strtoupper() {
[ "$#" -eq 1 ] || { printf 'usage: strtoupper string\n' >&2; return 2; }
printf '%s\n' "${1^^}"
}
else
# Bash 3.x (stock macOS): ASCII a-z only, no extra process
strtoupper() {
[ "$#" -eq 1 ] || { printf 'usage: strtoupper string\n' >&2; return 2; }
local str=$1 lower=abcdefghijklmnopqrstuvwxyz
local upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ i
for ((i = 0; i < 26; i++)); do
str=${str//${lower:i:1}/${upper:i:1}}
done
printf '%s\n' "$str"
}
fiHow it works
Which branch you get is decided once, when the file is sourced. BASH_VERSINFO[0] is the major version number as an integer, which is safer to compare than parsing $BASH_VERSION. The :-0 default turns an empty value into a number, so [ never sees an empty operand. The file itself is still Bash-only: dash, for one, rejects the array syntax with "Bad substitution".
On Bash 4.0 and newer, ${1^^} uppercases every character of the first argument. A single caret, ${1^}, does only the first character, which is all ucfirst() needs.
On Bash 3.x, mainly the /bin/bash 3.2 that macOS still ships, the fallback replaces each of the 26 lowercase ASCII letters in turn with ${str//x/X}. It is the same technique as the strtolower() fallback with the two alphabets swapped, and it has the same limit: letters outside a to z are left alone.
Usage
$ strtoupper "hello world"
HELLO WORLD
$ strtoupper "release-1.2.3"
RELEASE-1.2.3
$ strtoupper "crème brûlée"
CRÈME BRÛLÉE
$ strtoupper " two spaces "
TWO SPACES
$ strtoupper; echo "exit status: $?"
usage: strtoupper string
exit status: 2Spacing is kept exactly, including the leading and trailing spaces, because the argument is quoted all the way through. The accented line was converted because this shell runs in en_GB.UTF-8. The strtolower() post explains how the locale changes that. With the Bash 3 fallback loaded instead, the same line came out as CRèME BRûLéE, and everything else was identical.
Building an environment variable name
This is my most common use. Replace anything that is not a letter or digit with an underscore, then uppercase:
$ app='my-app.prod'
$ var=$(strtoupper "${app//[^[:alnum:]]/_}")_PORT
$ printf '%s\n' "$var"
MY_APP_PROD_PORTNormalising an answer
Uppercasing before a case means you only list each answer once instead of every capitalisation of it:
read -r -p 'Continue? [y/N] ' answer
case $(strtoupper "$answer") in
Y|YES) printf 'Continuing.\n' ;;
*) printf 'Aborted.\n'; exit 1 ;;
esacGiven "yes" this printed Continuing.. Given "n" it printed Aborted. and exited with status 1. For a complete prompt that loops until it gets a valid reply, see yesno().
Uppercasing only some characters
The case-modification operators accept an optional pattern after the carets. Only characters matching the pattern are converted, and the pattern is matched one character at a time:
$ s='hello world'
$ printf '%s\n' "${s^^[aeiou]}"
hEllO wOrld
$ printf '%s\n' "${s^^[lo]}"
heLLO wOrLd
$ printf '%s\n' "${s^^h}"
Hello worldI do not need this every day, but it is the tidy answer to "uppercase only these letters" requests, and PHP has no direct equivalent. The pattern form only exists on Bash 4+. The fallback has no way to express it.
The expansion also works across a whole array in one step. Each element is converted separately:
$ codes=(gb my us)
$ printf '%s\n' "${codes[@]^^}"
GB
MY
USPitfalls
No one-to-many mappings. The German ß has no single-character uppercase in traditional spelling. The correct result is "SS". PHP's mb_strtoupper("straße") gives "STRASSE" (I checked with PHP 8.0). Bash converts one character to one character, so ß stays as it is:
$ strtoupper "straße"
STRAßEIf you are generating text for people to read in German, check this. For identifiers it rarely matters, and those should be ASCII anyway.
The function returns a copy. strtoupper "$x" prints the result. It does not change $x. Capture it with x=$(strtoupper "$x"), or on Bash 4 just write x=${x^^}, which changes it in place without a subshell.
Unquoted output in old snippets. The 2009 version ended with echo ${1^^}. Without quotes, the shell word-splits and glob-expands the result, so a string containing * could print a directory listing, and repeated spaces collapsed to one. The quoted printf in the function above prints exactly what it was given, converted.
Alternatives
declare -u. On Bash 4+, a variable can carry the uppercase attribute. Anything assigned to it from then on is converted, including values from read:
$ declare -u region
$ region='eu-west-1'
$ printf '%s\n' "$region"
EU-WEST-1
$ read -r region <<< 'ap-southeast-1'
$ printf '%s\n' "$region"
AP-SOUTHEAST-1I like this for values that must always be uppercase, like a country code or a log level, because no later assignment can forget to convert.
tr and awk. For a stream or a file, use an external tool:
$ printf '%s\n' 'crème brûlée' | tr '[:lower:]' '[:upper:]'
CRèME BRûLéE
$ printf '%s\n' 'crème brûlée' | awk '{ print toupper($0) }'
CRÈME BRÛLÉEGNU tr (coreutils 8.30 on this machine) is byte-oriented and missed the accented letters even in a UTF-8 locale. GNU awk converted them. On a Bash 3 system that needs accented letters handled, awk is the practical choice.
Speed. For one value it does not matter. In a loop it can. On one machine (Bash 4.4), 1000 iterations took 0.003 s with the inline u=${s^^}, 0.47 s calling the pure-Bash fallback through $(...), and 1.2 s with printf | tr. Most of the cost of the middle figure is the subshell from command substitution, not the 26 replacements. The lesson is the usual one: inline the expansion in loops that run many times, call the function where readability matters more.
History of this snippet
The 2009 original had an array-based loop for Bash 3 and, from August 2009, a ${1^^} branch for the then-new Bash 4. Both printed with an unquoted echo and returned 1 on bad usage. The rewrite quotes the output, uses printf, returns 2 with a usage message, and replaces the two 26-element arrays with two plain strings.
Related functions
- strtolower(): lowercase a string, and how the locale affects the result
- ucfirst(): uppercase only the first character
- ucwords(): uppercase the first character of every word
- yesno(): a full yes/no prompt