PHP String Functions in Bash: A Cheat Sheet
I came to shell scripting from PHP, and for years I kept looking for strtolower() or str_replace() in Bash and finding nothing. Most of them do exist, just not as functions: they are hidden in parameter expansion, printf and [[ ]]. That is where this site's function library started in 2009. This page is the index to it: the PHP name, the core Bash expansion, and a link to the full article with the function, edge cases and test output.
All snippets below were run on Bash 4.4. The case conversion expansions (${s,,}, ${s^^}, ${s^}, ${s,}) need Bash 4 or later, so they do not work in the Bash 3.2 that macOS ships as /bin/bash. The full articles have fallbacks for it.
The cheat sheet
| PHP | Bash equivalent | Full article |
|---|---|---|
strlen($s) | ${#s} | strlen() |
strtolower($s) | ${s,,} | strtolower() |
strtoupper($s) | ${s^^} | strtoupper() |
ucfirst($s) | ${s^} | ucfirst() |
lcfirst($s) | ${s,} | lcfirst() |
ucwords($s) | ${word^} for each word | ucwords() |
strstr($h, $n) | [[ $h == *"$n"* ]] | strstr() |
stristr($h, $n) | [[ ${h,,} == *"${n,,}"* ]] | stristr() |
str_replace($a, $b, $s) | ${s//"$a"/"$b"} | str_replace() |
str_replace($n, '', $s) | ${s//"$n"/} | rm_str() |
str_repeat($s, $n) | printf -v r '%*s' "$n" ''; r=${r// /"$s"} | str_repeat() |
str_pad($s, 10) | printf '%-10s' "$s" | str_pad() |
trim($s) | s=${s#"${s%%[![:space:]]*}"} then s=${s%"${s##*[![:space:]]}"} | trim() |
explode(',', $s) | IFS=, read -ra arr <<< "$s" | explode() and implode() |
implode(',', $arr) | (IFS=,; printf '%s' "${arr[*]}") | explode() and implode() |
in_array($n, $arr) | loop: [[ $x == "$n" ]] for each element | in_array() |
is_numeric($s) (digits only) | [[ $s =~ ^[[:digit:]]+$ ]] | is_num() |
basename($p) | ${p##*/} | basename() |
dirname($p) | ${p%/*} (see caveats) | dirname() |
Two of these are not exact matches. In this library strstr and stristr are tests that succeed or fail, which is how you use them in an if. The full articles also have php_strstr and php_stristr variants that print the rest of the string as PHP does. And is_num accepts only unsigned digits, while PHP's is_numeric() also accepts signs, decimals and exponents.
Everything in the table is a parameter expansion or a builtin, so none of it starts a new process. That matters inside loops, as the last section shows.
Case conversion
Two commas lowercase the whole string, two carets uppercase it, and a single one changes only the first character.
s="hello World"
printf '%s\n' "${#s}" "${s,,}" "${s^^}" "${s^}" "${s,}"11
hello world
HELLO WORLD
Hello World
hello WorldFor ucwords, the case expansions also work on every element of an array at once. Splitting into words with read -ra collapses runs of spaces, which is fine for titles. The ucwords() article has a version that keeps the original spacing.
s="the quick brown fox"
read -ra words <<< "$s"
printf '%s\n' "${words[*]^}"The Quick Brown FoxSearching
Inside [[ ]], the right-hand side of == is a glob pattern, so *"$n"* means "anything, the needle, anything". Quote the needle so that characters like * in it are matched literally. To ignore case, lowercase both sides.
h="user@example.com" n="@" n2="EXAMPLE"
[[ $h == *"$n"* ]] && echo "contains @"
[[ ${h,,} == *"${n2,,}"* ]] && echo "found, ignoring case"
printf '%s\n' "${h#*@}" "${h%%@*}"contains @
found, ignoring case
example.com
userThe last line shows the related trick for cutting a string at a delimiter: ${h#*@} removes everything up to the first @, and ${h%%@*} removes everything from it.
Replacing and padding
${s//search/replace} replaces every match. A single slash replaces only the first. Quote the search string when it comes from a variable. Unquoted, it is a glob pattern, and the result can be quite different:
p="a*b" t="xa*byab"
printf '%s\n' "${t//"$p"/-}" "${t//$p/-}"x-yab
x-The quoted version replaced the literal a*b. The unquoted one treated * as "anything" and matched to the last b. printf covers repeating and padding: %*s takes the width from an argument, %-8s pads on the right and %8s on the left.
printf -v line '%*s' 20 ''
printf '%s\n' "${line// /-}"
printf '[%-8s]\n' "id"
printf '[%8s]\n' "42"--------------------
[id ]
[ 42]Trimming, splitting and joining
Trimming is two nested expansions: the inner one finds the run of leading (or trailing) whitespace, the outer one removes exactly that.
s=$' \t padded text '
s=${s#"${s%%[![:space:]]*}"}
s=${s%"${s##*[![:space:]]}"}
printf '[%s]\n' "$s"[padded text]Splitting uses read with IFS set only for that one command, and joining uses "${arr[*]}", which puts the first character of IFS between elements. The subshell keeps the changed IFS from leaking into the rest of the script.
IFS=, read -ra parts <<< "red,green,blue"
printf '%s\n' "${#parts[@]}" "${parts[1]}"
joined=$(IFS=:; printf '%s' "${parts[*]}")
printf '%s\n' "$joined"3
green
red:green:bluePaths
${p##*/} strips everything up to the last slash and ${p%/*} strips from the last slash on. Strip a known extension with ${f%.log}, or get the extension with ${f##*.}.
p="/var/log/nginx/access.log"
f=${p##*/}
printf '%s\n' "$f" "${p%/*}" "${f%.log}" "${f##*.}"access.log
/var/log/nginx
access
logThese two expansions are not identical to the basename and dirname commands for edge cases. A bare file.txt gives file.txt as its "directory", /file.txt gives an empty one, and a trailing slash (/usr/lib/) gives an empty base name. The basename() and dirname() articles handle those cases.
When to use awk or sed instead
Parameter expansion is fast because it avoids starting a process, and that makes a big difference for one string. On this machine, a thousand calls of x=$(echo "Hello" | tr '[:upper:]' '[:lower:]') took about 1.0 seconds, while a thousand x=${s,,} took 0.002 seconds.
The result is the opposite when you process a whole file. Lowercasing a 100,000-line file with a while read loop and ${l,,} took 2.0 seconds. tr '[:upper:]' '[:lower:]' < big.txt did the same job in a few milliseconds, with identical output. Both were measured once on one machine, but the difference is too large to be random variation. A Bash loop pays for every iteration. tr, sed and awk pay for one process start and then stream.
My simple rule: use the functions on this page for variables, arguments and a few lines of input. When the data is a file or a stream, or you need fields, counters or regular expression replacement across many lines, use awk or sed. The Shell Parameter Expansion section of the Bash manual lists every expansion used here.