Why this matters
Glob patterns (*.log, backup-*) answer "does this look roughly like that?". They cannot say "exactly four digits, a dash, two digits", or pull the pieces out of a line. You have used regular expressions with grep, sed and awk on whole files. bash can also apply a regex to a single string in a variable, and capture parts of the match into an array, with no extra process. That is the tool for validating input (dates, versions, IP addresses, user names) and for parsing structured lines inside a while read loop.
The concept
The =~ operator works only inside [[ ]]:
if [[ $string =~ $regex ]]; then
...
fi
- The test succeeds (status 0) if the regex matches anywhere in the string, fails (1) if it does not, and returns 2 if the regex itself is invalid.
- The regex dialect is POSIX extended regular expressions (ERE), the same as
grep -Eandawk:.any character,*+?repetition,{n,m}counts,[...]sets,(...)groups,|alternation,^and$anchors. - On a match, bash fills the array
BASH_REMATCH: element 0 is the text matched by the whole regex, element N the text matched by the Nth parenthesized group.
$ v="release-2.14.3-beta"
$ re='([0-9]+)\.([0-9]+)\.([0-9]+)'
$ [[ $v =~ $re ]] && echo "major ${BASH_REMATCH[1]}, minor ${BASH_REMATCH[2]}, patch ${BASH_REMATCH[3]}"
major 2, minor 14, patch 3
$ declare -p BASH_REMATCH
declare -a BASH_REMATCH=([0]="2.14.3" [1]="2" [2]="14" [3]="3")
Anchors: "contains" versus "is"
Because a match anywhere counts, a validation regex needs ^ (start of string) and $ (end):
$ re='[0-9]+'
$ [[ "abc123" =~ $re ]] && echo yes
yes
$ re='^[0-9]+$'
$ [[ "abc123" =~ $re ]] || echo no
no
Forgetting anchors is the most common regex bug in validation code: 12; rm -rf / "contains digits" too.
Quoting the regex
The right-hand side of =~ has unusual quoting rules: any quoted part is matched literally, as plain text. That was added so you can mix a literal string into a pattern, but it surprises everyone:
$ [[ "a+b" =~ "a+b" ]] && echo "literal match"
literal match
$ [[ "aaab" =~ a+b ]] && echo "regex match"
regex match
Writing complicated regexes unquoted inline is awkward because characters like spaces, (, ), <, | and \ are also shell syntax. The robust habit: put the regex in a single-quoted variable, and use the variable unquoted after =~.
re='^([a-z_][a-z0-9_]*)=(.*)$'
[[ $line =~ $re ]]
Inside single quotes nothing is interpreted by the shell, so the regex engine receives exactly what you wrote, backslashes included.
What happens underneath
bash does not implement regular expressions itself. When it evaluates =~, it hands the pattern to the C library's regcomp() with the "extended" flag, then calls regexec() on the string, asking for the positions of each group. From those positions it builds BASH_REMATCH. That has two consequences:
- Character classes follow the locale.
[a-z]in some locales also matches accented letters or even uppercase ones. Prefer named classes like[[:digit:]],[[:alpha:]],[[:space:]], or setLC_ALL=Cfor byte-wise behavior. The sandbox usesC.UTF-8, where[a-z]means just the 26 ASCII letters. - No Perl extensions.
\d,\w,\s, lazy.*?and lookaheads are not part of ERE. glibc happens to understand\w,\sand\bas extensions, but they are not portable to other systems. Write[0-9],[[:alnum:]_],[[:space:]].
ERE matching is "leftmost, longest": among the matches starting at the earliest position, the longest one wins. So (a|ab)c against abc matches abc, and .* is always greedy.
BASH_REMATCH is overwritten by every =~ test, successful or not. Copy the parts you need into your own variables right after the match.
Worked examples
Validating input in a script.
re='^[a-z][a-z0-9-]{1,30}$'
if [[ ! $1 =~ $re ]]; then
echo "invalid host name: $1" >&2
exit 2
fi
Parsing key = value lines with optional spaces.
re='^[[:space:]]*([A-Za-z_]+)[[:space:]]*=[[:space:]]*(.*)$'
while IFS= read -r line; do
[[ $line =~ $re ]] || continue
key=${BASH_REMATCH[1]} value=${BASH_REMATCH[2]}
echo "[$key] -> [$value]"
done < settings.conf
Leading zeros. Captured numbers are strings. Arithmetic treats a leading 0 as octal, so $(( 08 )) fails with value too great for base (error token is "08"). Force base ten with 10#:
$ m=08
$ echo $(( 10#$m + 1 ))
9
Alternation for a fixed set.
re='^(debug|info|warn|error)$'
[[ ${level,,} =~ $re ]] || { echo "unknown level: $level" >&2; exit 2; }
(For a fixed set of words, case is often clearer; the regex shines when the words have structure.)
When to leave bash. For one string at a time, =~ is fast and process-free. For filtering or transforming a whole file, grep -E, sed -E or awk are much faster than a bash while read loop, which processes one line per iteration in the interpreter. A loop over 100 000 lines with =~ can take seconds; awk takes a fraction of that.
Common mistakes
- Quoting the regex.
[[ $x =~ "^[0-9]+$" ]]matches the literal characters. Use a variable. - Single brackets.
[ $x =~ re ]gives[: =~: binary operator expected.=~lives in[[ ]]. - No anchors in a validation.
[[ $x =~ [0-9]{3} ]]acceptsabc1234xyz. - Perl syntax.
\d+does not mean digits here;[0-9]+does. - Reading BASH_REMATCH after another test. Every
=~resets it, including a failed one. - Octal surprises with captured numbers: use
10#$n. - Expecting glob behavior. In
=~,*means "repeat the previous item", not "any string";.*is the regex for "anything".
Exercises
Two quizzes on quoting and BASH_REMATCH, a date validator that also checks ranges and exit status, and a log line parser. In the sandbox shell, try your regexes interactively first: re='...'; [[ "test string" =~ $re ]]; echo $?; declare -p BASH_REMATCH.
Recap
[[ $s =~ $re ]]tests an ERE against a string: 0 match, 1 no match, 2 bad regex.BASH_REMATCH[0]is the whole match,[1],[2], ... the groups; it is reset on every test.- Matches are unanchored; add
^...$to validate whole strings. - Store the regex in a single-quoted variable and use it unquoted.
- It is POSIX ERE from the C library: no
\d, locale-dependent ranges; prefer[[:digit:]]. - Use
10#before doing arithmetic on captured numbers.
Going further: read man 7 regex (it is installed in the sandbox) for the full ERE grammar, including bracket expressions like [[:alpha:]] and the rules for {m,n}. Then write a regex for an IPv4 address and test it on 192.168.1.1, 256.1.1.1 and 1.2.3: which of these can a regex alone reject, and which need a range check like the date exercise?