Bash

strlen() in Bash: Get the Length of a String

By Nawawi JamiliPublished Updated 6 min read

PHP has strlen(). Bash has no function with that name, but parameter expansion gives you the same answer without starting a process. There is one difference you should know before you rely on it: PHP's strlen() counts bytes, while Bash's ${#var} counts characters in the current locale. For plain ASCII the two agree. For "café" they do not.

This post gives you a tidy strlen function and a companion strbytes for when you really need the byte count. It also covers the common mistakes I see people make when measuring strings in shell scripts.

The function

bash
# strlen -- print the length of a string in characters
# Usage: strlen string
strlen() {
    [ "$#" -eq 1 ] || { printf 'usage: strlen string\n' >&2; return 2; }
    printf '%s\n' "${#1}"
}
# strbytes -- print the length of a string in bytes
# Usage: strbytes string
strbytes() {
    [ "$#" -eq 1 ] || { printf 'usage: strbytes string\n' >&2; return 2; }
    local LC_ALL=C
    printf '%s\n' "${#1}"
}

Both work in every Bash from 3.2 (the stock /bin/bash on macOS) up to the current 5.x releases. Nothing here needs Bash 4.

How it works

${#name} is the length form of parameter expansion. Inside a function, $1 is the first argument, so ${#1} is the length of whatever the caller passed. The shell computes it internally, which is why it costs almost nothing compared with piping into wc.

The usage check refuses anything other than exactly one argument and returns 2, the conventional status for "you called me wrong". That matters because a caller who forgets the quotes, as in strlen $name, would otherwise get the length of the first word only and never notice. With the check, word splitting turns into an error message instead of a silently wrong number.

printf '%s\n' replaces the old echo. For a number it makes no practical difference. But I use the same output line in every function on this site, so that none of them fail on data that looks like an echo option.

strbytes is the same expansion with one extra line. local LC_ALL=C switches the locale to C for the duration of the function only. In the C locale every byte is one character, so ${#1} reports bytes. When the function returns, the caller's locale is restored automatically because the variable was local.

Usage

terminal
$ strlen "hello"
5
$ strlen "hello world"
11
$ strlen ""
0
$ name="The quick brown fox"
$ len=$(strlen "$name")
$ printf 'Length: %s\n' "$len"
Length: 19
$ strlen; echo "exit status: $?"
usage: strlen string
exit status: 2
$ strlen one two; echo "exit status: $?"
usage: strlen string
exit status: 2

Spaces count, an empty string is 0, and a wrong call fails with an error.

Characters versus bytes

This is the part that causes problems. My terminal runs with LANG=en_GB.UTF-8. In UTF-8 the letter "é" is stored as two bytes, c3 a9, so the word "café" is four characters but five bytes:

terminal
$ echo "$LANG"
en_GB.UTF-8
$ word='café'
$ strlen "$word"
4
$ strbytes "$word"
5
$ printf '%s' "$word" | od -An -tx1
 63 61 66 c3 a9

The gap grows with the script you are dealing with. Accented Latin letters take two bytes, the euro sign and most CJK characters take three:

terminal
$ s='naïve 日本'
$ strlen "$s"; strbytes "$s"
8
13
$ price='€20'
$ strlen "$price"; strbytes "$price"
3
5

The same ${#var} gives a different answer depending on the locale in force when it runs. Change LC_ALL in the shell and the result changes with it:

terminal
$ LC_ALL=C
$ strlen "café"
5
$ LC_ALL=en_GB.UTF-8
$ strlen "café"
4

So which one do you want? Use characters (strlen) when the number is about what a person sees: padding a column, truncating a title, checking that a username is at most 16 letters. Use bytes (strbytes) when the number is about storage or the wire: a VARCHAR limit in a byte-based column, a protocol field with a byte length, a Content-Length header. If you are porting PHP code, remember that PHP's strlen() is the byte version and mb_strlen() is the character version.

Note: scripts started from cron or systemd often run with no LANG set, which means the C locale. A length check that passes in your interactive shell can count differently in a cron job. If the answer matters, set the locale explicitly at the top of the script or use strbytes so the result does not depend on the environment.

Pitfalls

Unquoted arguments. strlen $name splits on whitespace and expands globs before the function ever sees the value. The usage check catches the multi-word case. The fix is always to quote: strlen "$name".

Arrays. On an array, ${#arr} is the length of the first element, not the number of elements. You want ${#arr[@]} for the count:

terminal
$ files=(*.txt)
$ echo "${#files[@]}"
3
$ echo "${#files}"
9

Here the directory held alpha.txt, b.txt and c.txt. The 9 is the length of "alpha.txt".

Trailing newlines. Command substitution strips trailing newlines, so x=$(printf 'abc\n\n') leaves x at three characters. Sometimes you measure file content or command output and the newlines matter. Then read it another way, for example IFS= read -r -d '' x < file, which keeps them (without IFS= the newlines are trimmed as whitespace).

Invalid UTF-8. Bash does not fail on a byte sequence that is not valid UTF-8. It counts each stray byte as one character. So bad input gives you a number that looks correct but is not.

set -u. Under set -u, ${#var} on an unset variable is an "unbound variable" error, just like $var. The function guards against a missing argument with the $# check, so it is safe in strict-mode scripts.

The one-liner without a function

Honestly, in most scripts I do not call strlen at all. I write the expansion inline, because it needs no subshell:

bash
read -r -p 'Password: ' pw
if [ "${#pw}" -lt 8 ]; then
    printf 'Password must be at least 8 characters.\n' >&2
    exit 1
fi

The function is useful when you are porting PHP habits, building a small library, or want the usage check. To give you a sense of scale, I timed this on one machine (Bash 4.4). 1000 iterations of n=${#s} took 0.002 s, 1000 calls of n=$(strlen "$s") took 0.25 s, and 1000 runs of printf | wc -m took 0.95 s. The cost is the subshell and the external process, not the counting.

Alternatives

External tools work too, but each has a trap:

terminal
$ s='café'
$ echo "$s" | wc -c
6
$ printf '%s' "$s" | wc -c
5
$ printf '%s' "$s" | wc -m
4
$ expr length "$s"
4
$ awk -v s="$s" 'BEGIN { print length(s) }'
4
$ LC_ALL=C awk -v s="$s" 'BEGIN { print length(s) }'
5
  • echo | wc -c counts the newline that echo adds, which is the classic off-by-one. Use printf '%s'.
  • wc -c is bytes, wc -m is characters in the current locale.
  • expr length is a GNU extension and is not in POSIX, so do not use it in portable scripts.
  • awk's length() is locale-aware in GNU awk (shown above). Older or minimal awks may count bytes.

Use wc when the data is already in a file or a pipe, since reading a large file into a variable just to measure it is wasteful. For a value already in a variable, ${#var} is the best choice.

History of this snippet

The 2009 version was a single line, echo ${#1}, with an example string in Malay. It worked for normal input but accepted any number of arguments and said nothing about locale. This rewrite adds the usage check, switches to printf, and adds strbytes for the byte count that PHP's own strlen() returns.