Bash

strtoupper() in Bash: Convert a String to Uppercase

By Nawawi JamiliPublished Updated 5 min read

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

bash
# 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"
    }
fi

How 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

terminal
$ 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: 2

Spacing 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:

terminal
$ app='my-app.prod'
$ var=$(strtoupper "${app//[^[:alnum:]]/_}")_PORT
$ printf '%s\n' "$var"
MY_APP_PROD_PORT

Normalising an answer

Uppercasing before a case means you only list each answer once instead of every capitalisation of it:

bash
read -r -p 'Continue? [y/N] ' answer
case $(strtoupper "$answer") in
    Y|YES) printf 'Continuing.\n' ;;
    *)     printf 'Aborted.\n'; exit 1 ;;
esac

Given "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:

terminal
$ s='hello world'
$ printf '%s\n' "${s^^[aeiou]}"
hEllO wOrld
$ printf '%s\n' "${s^^[lo]}"
heLLO wOrLd
$ printf '%s\n' "${s^^h}"
Hello world

I 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:

terminal
$ codes=(gb my us)
$ printf '%s\n' "${codes[@]^^}"
GB
MY
US

Pitfalls

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:

terminal
$ strtoupper "straße"
STRAßE

If 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:

terminal
$ 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-1

I 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:

terminal
$ 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ÉE

GNU 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.

  • 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