lcfirst() in Bash: Lowercase the First Character
PHP's lcfirst() lowercases just the first character of a string. It looks like the least useful of the case functions until you need to turn UserName into userName. That is the everyday job of converting PascalCase names into camelCase ones for JSON keys, JavaScript properties or API field names. In Bash 4 the expansion is ${var,}, a single comma.
This post gives you a lcfirst that also runs on the default macOS Bash 3.2. Then it deals with the case where a plain lcfirst gives the wrong answer: names that start with an acronym.
The function
# lcfirst -- print a string with its first character in lowercase
# Usage: lcfirst string
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
lcfirst() {
[ "$#" -eq 1 ] || { printf 'usage: lcfirst string\n' >&2; return 2; }
printf '%s\n' "${1,}"
}
else
# Bash 3.x (stock macOS): ASCII A-Z only, no dependencies
lcfirst() {
[ "$#" -eq 1 ] || { printf 'usage: lcfirst string\n' >&2; return 2; }
local upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ
local lower=abcdefghijklmnopqrstuvwxyz
local first=${1:0:1} before
if [ -n "$first" ]; then
before=${upper%%"$first"*}
[ "${#before}" -lt 26 ] && first=${lower:${#before}:1}
fi
printf '%s\n' "$first${1:1}"
}
fiHow it works
In Bash 4.0 and later, , and ,, are the lowercase forms of case modification: one comma for the first character, two for all of them. The rest of the string is copied through unchanged, which is exactly what you want here. "UserName" must keep its capital N.
On Bash 3 the function looks the first character up in the string of capitals. Removing everything from that character onwards, ${upper%%"$first"*}, leaves the capitals that come before it. The length of that remainder is the letter's position in the alphabet. If the character is not a capital at all, nothing is removed, the length is 26, and the character is not changed. The ucfirst() post explains the same lookup in the other direction, step by step.
Usage
$ lcfirst "Hello World"
hello World
$ lcfirst "UserName"
userName
$ lcfirst "Éclair"
éclair
$ lcfirst "_Private"
_Private
$ lcfirst "already lower"
already lower
$ lcfirst a b; echo "exit status: $?"
usage: lcfirst string
exit status: 2A leading underscore is not a letter, so _Private is not changed. If your naming convention needs the letter after the underscore lowered, strip the prefix first. The Bash 3 fallback, tested in isolation on Bash 4.4, printed the same except Éclair, which it does not know how to lower.
Generating JSON keys
A typical case: column or class property names in PascalCase that need to become camelCase keys.
$ for f in FirstName LastName APIKey; do
> printf '"%s": null\n' "$(lcfirst "$f")"
> done
"firstName": null
"lastName": null
"aPIKey": nullTwo out of three are right. The third is the subject of the next section.
Pitfalls: names that start with an acronym
lcfirst lowercases one character, so a name that starts with several capitals is only partly converted:
$ lcfirst "HTTPServer"
hTTPServer
$ lcfirst "ID"
iDPHP's lcfirst() does the same, so this matches PHP. But it is almost never what you want when building identifiers. The usual camelCase convention lowercases the whole leading acronym, except for its last capital when another word follows. That capital actually starts the next word: "HTTPServer" becomes "httpServer". Here is a small helper for that, Bash 4+ only:
# pascal_to_camel -- lowercase the leading capital or acronym (Bash 4+)
# Usage: pascal_to_camel string
pascal_to_camel() {
if [ "$#" -ne 1 ]; then
printf 'usage: pascal_to_camel string\n' >&2
return 2
fi
local s=$1 head
head=${s%%[![:upper:]]*} # leading run of capitals
if [ "${#head}" -gt 1 ] && [ "${#head}" -lt "${#s}" ]; then
head=${head%?} # last capital starts the next word
fi
printf '%s\n' "${head,,}${s#"$head"}"
}$ pascal_to_camel "HTTPServer"
httpServer
$ pascal_to_camel "XMLHttpRequest"
xmlHttpRequest
$ pascal_to_camel "UserName"
userName
$ pascal_to_camel "ID"
id
$ pascal_to_camel "lowercase"
lowercaseHow it decides: ${s%%[![:upper:]]*} cuts the string at the first character that is not a capital. This leaves the run of capitals at the front ("HTTPS" for "HTTPServer"). If that run is longer than one character and is not the whole string, the last capital belongs to the next word, so ${head%?} drops it ("HTTP"). That part is lowercased and joined back to the rest. A name made entirely of capitals, like "ID", is lowered completely. A single leading capital, as in "UserName", behaves exactly like lcfirst.
It is a heuristic, not a parser. "IOError" works ("ioError"). But two acronyms in a row, such as "HTTPAPIClient", can only become "httpapiClient", because nothing in the string says where one acronym ends. When names come from a fixed list, a lookup table of exceptions is more reliable than any rule.
Other things to watch:
[[:upper:]]follows the locale, just like${var,,}, so accented capitals count as capitals in a UTF-8 locale and not in the C locale.- Bash also accepts
${var~}and${var~~}, which toggle case. They are not in the Bash manual or man page, so I would not rely on them in a script you expect to keep working. - The function prints its result. Use
key=$(lcfirst "$key")to update a variable, orkey=${key,}inline on Bash 4.
Alternatives
On Bash 4, the expansion works on every element of an array at once, so a list of field names converts in one line without a loop or a subshell:
$ fields=(FirstName LastName EmailAddress)
$ printf '%s\n' "${fields[@],}"
firstName
lastName
emailAddressFor a file full of names, one per line, GNU sed's sed 's/./\l&/' lowers the first character of every line. \l is a GNU extension and the BSD sed on macOS does not have it. If you are converting identifiers in source code across a whole project, a proper refactoring tool in your editor will do a safer job than any shell one-liner.
History of this snippet
The 2009 lcfirst was a pair: echo ${1,} for Bash 4 and, for Bash 3, a call to strtolower() on the first character inside a command substitution. That second branch returned 1 without any message if strtolower had not been loaded first. The new fallback has no dependencies, both branches quote their output, and the acronym helper is new.
Related functions
- ucfirst(): uppercase the first character instead
- strtolower(): lowercase the whole string
- ucwords(): capitalise each word in a string
- explode() and implode(): split names on separators before converting them