Regular Expressions in Bash
Chapter 4 — Character Classes and POSIX Classes
What Is a Character Class?
A character class is a set of characters enclosed in square brackets [...]. The class matches any single character from that set. Where the dot . matches any character, a character class matches one specific character chosen from a defined set — giving you precise control over what is acceptable at that position.
A character class always matches exactly one character. To match multiple characters you combine it with a quantifier (covered in Chapter 5): [0-9]+ matches one or more digits.
Simple Character Classes
# Match any vowel echo "hello world" | grep -oE '[aeiou]' e o o # Match "grey" or "gray" echo -e "grey\ngray\ngruy" | grep 'gr[ae]y' grey gray # Match any single hex digit grep -oE '[0-9A-Fa-f]' <<< "colour #ff3a2b" f f 3 a 2 b # Match a bracket character (open or close) grep '[()]' source.c # any line with a parenthesis # Match "colour" or "color" (British vs American spelling) grep 'colo[u]r\|colour' file.txt # simpler: u is optional so [u] works but see Chapter 5 for ? quantifier
Ranges Inside Character Classes
A hyphen - between two characters inside a class defines a range of characters based on their ASCII (or locale) order. Common ranges:
| Range | Matches | ASCII positions |
|---|---|---|
[0-9] | Any digit | 48–57 |
[a-z] | Lowercase letters | 97–122 |
[A-Z] | Uppercase letters | 65–90 |
[a-zA-Z] | Any letter | 65–90, 97–122 |
[a-zA-Z0-9] | Letter or digit | Combined |
[a-f] | Hex lowercase letters a–f | 97–102 |
[!-~] | All printable ASCII (excl. space) | 33–126 |
# Match a word made of only lowercase letters grep -E '^[a-z]+$' words.txt # Match a 3-letter uppercase acronym grep -E '\b[A-Z]{3}\b' text.txt # Match a hexadecimal colour code like #3fa2c0 grep -E '#[0-9A-Fa-f]{6}' styles.css # Match a line containing only digits and hyphens (like a date 2026-06-11) grep -E '^[0-9-]+$' data.txt
[a-z] may include accented characters between a and z in the collation order, giving unexpected results. For reliable ASCII-only matching use LC_ALL=C or switch to POSIX named classes like [[:lower:]] which always behave predictably regardless of locale.
Negated Classes — [^...]
When ^ is the first character inside a class, it negates the class — matching any single character that is not in the set. This is one of the most useful patterns in regex because it lets you match "everything up to a delimiter".
# Match any character that is NOT a digit grep -oE '[^0-9]' <<< "abc123def" a b c d e f # Extract text before the first colon on each line grep -oE '^[^:]+' /etc/passwd # matches from start up to (not including) first : # The [^delimiter]* trick — match up to the first occurrence of a char # (the lazy-quantifier workaround from Chapter 10 of the SED course) echo "<b>bold</b> and <i>italic</i>" | grep -oE '<[^>]*>' <b> </b> <i> </i> # Strip all non-alphanumeric characters sed 's/[^a-zA-Z0-9]//g' <<< "hello, world! 2026" helloworld2026 # Match a quoted string (no embedded quotes) grep -oE '"[^"]+"' data.json # " then one or more non-quote chars then "
Special Characters Inside Classes
Most regex metacharacters lose their special meaning inside [...] and become literals. But a few characters need care:
| Character | Inside [...] | How to include it literally |
|---|---|---|
] | Closes the class | Put it first: []abc] |
- | Range operator between two chars | Put it first or last: [-abc] or [abc-] |
^ | Negation if it is the first char | Put it anywhere except first: [a^b] |
\ | Escape character (GNU tools) | \\ |
. | Literal dot — not any-char here | Just write . normally |
* | Literal asterisk — not quantifier here | Just write * normally |
# Include a literal ] by placing it first grep '[]abc]' file.txt # matches ], a, b, or c # Include a literal - by placing it last grep '[a-z0-9-]' file.txt # matches lowercase, digit, or hyphen # NOT [a-z0-9] followed by a range — - at the end is always literal # Include a literal . and * (they are NOT special inside []) grep '[.*]' file.txt # matches . or * — both literal inside [] # Match a URL slug: lowercase, digits, hyphens only grep -E '^[a-z0-9-]+$' slugs.txt # Match a line containing [ or ] (the bracket chars themselves) grep '[][]]' file.txt # ] first (to be literal), then [ (normal literal)
POSIX Named Character Classes
POSIX defines a set of named classes written as [:name:] inside a character class bracket: [[:name:]]. They are more portable than raw ranges like [a-z] because they correctly respect the current locale and character encoding.
# Trim leading whitespace (any whitespace, not just spaces) sed 's/^[[:space:]]*//' file.txt # Remove all punctuation from a line sed 's/[[:punct:]]//g' text.txt # Keep only alphanumeric characters and spaces sed 's/[^[:alnum:][:space:]]//g' text.txt # Match lines that start with an uppercase letter grep '^[[:upper:]]' sentences.txt # Find lines containing control characters (e.g. stray \r from Windows) grep -P '[[:cntrl:]]' file.txt # Remove Windows carriage returns (\r) at end of lines sed 's/[[:cntrl:]]//g' file.txt # Match a hex colour: # followed by exactly 6 hex digits grep -E '#[[:xdigit:]]{6}' styles.css # Combining POSIX classes: letter OR digit OR underscore (like \w) grep -E '[[:alnum:]_]+' identifiers.txt
POSIX classes can be combined and negated
# Match anything that is NOT a letter or digit grep -oE '[^[:alnum:]]' <<< "hello, world!" , ! # Combine multiple POSIX classes in one set # Match letter, digit, underscore, or hyphen (valid identifier + hyphen) grep -E '^[[:alnum:]_-]+$' slugs.txt # Match a line with at least one uppercase AND contains a digit # (two separate greps piped — one class can't express AND logic) grep '[[:upper:]]' file.txt | grep '[[:digit:]]'
GNU Shorthand Classes
GNU tools (grep, sed, awk on Linux) support shorthand escape sequences as convenient aliases for common POSIX classes. These are not available in POSIX BRE/ERE or on BSD/macOS tools.
[[:alnum:]_]
[[:digit:]]
GNU sed 4.9+
[[:space:]]
[[:blank:]]
PCRE/grep -P
# \w — word character [a-zA-Z0-9_] (GNU grep, GNU sed) grep -oE '\w+' <<< "hello_world 2026" hello_world 2026 # \W — non-word character grep -oE '\W+' <<< "hello, world!" , ! # \s — whitespace (GNU grep -E) grep -E '\s{2,}' file.txt # lines with 2+ consecutive whitespace chars # Collapse multiple spaces to one (GNU sed) sed 's/\s\+/ /g' file.txt # In [[ =~ ]] — \w works in Bash ERE [[ "hello_123" =~ ^\w+$ ]] && echo "valid identifier"
Locale and Portability
| Pattern | GNU/Linux (UTF-8) | C/POSIX locale | macOS (BSD) | Portable? |
|---|---|---|---|---|
[a-z] |
May include accented letters | a–z ASCII only | Locale-dependent | No |
[[:lower:]] |
Lowercase for locale | a–z ASCII | Correct | Yes |
\w |
GNU tools | GNU tools | Not in BSD grep/sed | No |
[[:alnum:]_] |
All tools | All tools | All tools | Yes |
[0-9] |
Always digits only | Always digits | Always digits | Yes (digits are safe) |
# Force C locale for predictable ASCII range behaviour LC_ALL=C grep '[a-z]' file.txt # Portable alternative: POSIX class (no LC_ALL needed) grep '[[:lower:]]' file.txt
Practical Recipes
Validate common input formats
# Is this a valid username? (letters, digits, underscore, 3-16 chars) [[ "$username" =~ ^[[:alnum:]_]{3,16}$ ]] && echo "valid" # Is this a valid hex colour? (#rgb or #rrggbb) [[ "$col" =~ ^#([[:xdigit:]]{3}|[[:xdigit:]]{6})$ ]] && echo "valid colour" # Is this a MAC address? (xx:xx:xx:xx:xx:xx) [[ "$mac" =~ ^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$ ]] && echo "valid MAC" # Does the password contain at least one uppercase, one digit? [[ "$pw" =~ [[:upper:]] ]] && [[ "$pw" =~ [[:digit:]] ]] && echo "strong enough"
Text cleaning pipelines
# Remove all non-printable characters from a file sed 's/[^[:print:]]//g' dirty.txt # Strip all digits from a string sed 's/[[:digit:]]//g' <<< "order123 total456" order total # Extract only the alphabetic words from a line grep -oE '[[:alpha:]]+' <<< "error: code 42 at line 7" error code at line # Convert to lowercase using tr (not regex, but pairs well) echo "HELLO WORLD" | tr '[[:upper:]]' '[[:lower:]]' hello world # Find lines containing tabs (useful to spot mixed indentation) grep -P '\t' source.py # PCRE tab grep '[[:blank:]]' source.py # space OR tab — broader
Quick Reference — Chapter 4
Character Class Syntax
Essential POSIX Classes
GNU Shorthand (Linux only)
* (zero or more), + (one or more), ? (optional), and {n,m} (exact counts). Combining quantifiers with character classes is where regex becomes truly powerful.