Bash

ucfirst() in Bash: Uppercase the First Character

By Nawawi JamiliPublished Updated 5 min read

Sometimes you only want the first letter in capitals. Examples are a username at the start of a greeting, a month name read from a config file, or the first word of a message built from pieces. PHP has ucfirst() for that. Bash 4 has ${var^}, a single caret, and on the stock macOS Bash 3.2 you need a small fallback. The interesting part is less the conversion itself than what "first character" actually means for your input.

The function

bash
# ucfirst -- print a string with its first character in uppercase
# Usage: ucfirst string
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
    ucfirst() {
        [ "$#" -eq 1 ] || { printf 'usage: ucfirst string\n' >&2; return 2; }
        printf '%s\n' "${1^}"
    }
else
    # Bash 3.x (stock macOS): ASCII a-z only, no dependencies
    ucfirst() {
        [ "$#" -eq 1 ] || { printf 'usage: ucfirst string\n' >&2; return 2; }
        local lower=abcdefghijklmnopqrstuvwxyz
        local upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ
        local first=${1:0:1} before
        if [ -n "$first" ]; then
            before=${lower%%"$first"*}
            [ "${#before}" -lt 26 ] && first=${upper:${#before}:1}
        fi
        printf '%s\n' "$first${1:1}"
    }
fi

How it works

Bash 4 and newer. ${1^} converts the first character of $1 to uppercase and leaves the rest exactly as it was. Two carets would convert everything. That is strtoupper(). The conversion follows the current locale, so in a UTF-8 locale an accented first letter is handled too.

Bash 3. The fallback splits the string into ${1:0:1} (the first character) and ${1:1} (everything after it). Then it needs to know whether that first character is a lowercase ASCII letter, and if so, which one. The method is a lookup by position:

  • ${lower%%"$first"*} removes everything from the first occurrence of the character onwards, leaving only the letters before it. For "h" that leaves "abcdefg", which is 7 characters long.
  • If the character is not in the alphabet at all, nothing is removed and the result is all 26 letters. That is the "not a lowercase letter" signal.
  • Otherwise, ${upper:7:1} is the capital at the same position: "H".

The quotes around "$first" matter. They make the character match literally, so a string starting with * or [ is not treated as a pattern. The -n test skips the lookup for an empty string, where there is no first character to convert.

The fallback costs no subshell and no external command, and it no longer depends on another function being loaded. More on that in the history section below.

Usage

terminal
$ ucfirst "hello world"
Hello world
$ ucfirst "émile zola"
Émile zola
$ ucfirst "42 apples"
42 apples
$ ucfirst ""

$ user=alice
$ printf 'Welcome back, %s.\n' "$(ucfirst "$user")"
Welcome back, Alice.
$ ucfirst; echo "exit status: $?"
usage: ucfirst string
exit status: 2

A string that starts with a digit is unchanged, as in PHP. An empty string prints an empty line and succeeds: there was nothing to capitalise, which is not an error. I tested the Bash 3 fallback by loading it on its own in Bash 4.4. It gave identical output except for the "émile zola" line. It left that line as it was, because "é" is not one of its 26 letters.

What "first character" means

Both versions, like PHP's, look at the literal first character and nothing else. That has two results people sometimes do not expect.

The rest of the string is not lowered. ucfirst is not "make this look like a proper word". Input that arrives in capitals stays in capitals:

terminal
$ ucfirst "hELLO"
HELLO

If you want "Hello" whatever the input looked like, lowercase first and capitalise second. On Bash 4 that is two expansions and no subshell:

terminal
$ s='hELLO wORLD'
$ lower=${s,,}
$ printf '%s\n' "${lower^}"
Hello world

Bash will not let you nest the two operators in one expansion, so the intermediate variable is needed.

Leading whitespace counts. If the first character is a space, the space is "capitalised", which means nothing happens:

terminal
$ ucfirst "  indented"
  indented

This affects input read from files or pasted at a prompt. Strip the leading whitespace first if you want the first visible letter changed:

terminal
$ s='   hello'
$ trimmed=${s#"${s%%[![:space:]]*}"}
$ printf '[%s]\n' "${trimmed^}"
[Hello]

The inner expansion, ${s%%[![:space:]]*}, is the run of leading whitespace. The outer one removes exactly that from the front. The trim() post covers this properly, including trailing whitespace.

Pitfalls

Multibyte first letters on Bash 3. The fallback only knows a to z. A name such as "émile" or "øystein" keeps its lowercase first letter on macOS's /bin/bash. If that matters, install a newer Bash (Homebrew and MacPorts both package 5.x) or call an external tool for that one case.

Titles need more than this. ucfirst changes one character in the whole string. For "first letter of every word", use ucwords().

Capture the result, do not expect in-place change. The function prints its result. name=$(ucfirst "$name") updates the variable. On Bash 4, name=${name^} does the same without a subshell.

Alternatives

On Bash 4 and newer I usually write the expansion inline. It also works element by element across an array, which is neat for lists of names or labels:

terminal
$ months=(january february march)
$ printf '%s\n' "${months[@]^}"
January
February
March

Outside Bash, GNU sed can do it with sed 's/./\u&/'. But \u is a GNU extension and is not available in the BSD sed that macOS ships. That is exactly the system where you would want a fallback. On Bash 3 the pure-Bash function above is the more portable answer.

History of this snippet

The 2009 version had a Bash 4 branch (echo ${1^}, unquoted) and a Bash 3 branch that called strtoupper on the first character in a subshell. If strtoupper had not been sourced, it quietly returned 1 with no output and no message, which is a confusing way for a function to fail:

terminal
$ ucfirst "hello"; echo "exit status: $?"
exit status: 1

The rewrite makes the fallback self-contained, quotes the output, and keeps failure for actual misuse: wrong argument count, with a message on stderr and status 2.

  • lcfirst(): the reverse: lowercase the first character
  • ucwords(): capitalise the first letter of every word
  • strtoupper(): uppercase the whole string
  • strtolower(): lowercase the whole string, useful before ucfirst
  • trim(): remove leading and trailing whitespace