Regex Error Unterminated Character Class and How to Fix It
Fix the regex unterminated character class error: unclosed brackets, a literal closing bracket placed first, POSIX class syntax and quoting.

Square brackets define a set of characters, and every set that opens must close. When the tool cannot find the closing bracket it stops with a message such as "Unmatched [", "unterminated character class", "missing terminating ] for character class" or "unterminated [". The commands below use GNU grep 3.11, sed 4.9, Perl 5.38 and Python 3.12. A related parse error is described in nothing to repeat.
The messages
echo "abc" | grep '[abc'
echo "abc" | sed 's/[a-c/X/'
perl -e '"abc" =~ /[a-c/'
python3 -c 'import re; re.compile("[a-c")'grep: Unmatched [, [^, [:, [., or [=
sed: -e expression #1, char 9: unterminated `s' command
Unmatched [ in regex; marked by <-- HERE in m/[ <-- HERE a-c/ at -e line 1.
re.error: unterminated character set at position 0
All four point at the same defect: the set opened at the [ was never closed. The sed message is indirect, since the unclosed bracket swallows the delimiters and the substitution looks unfinished. Count brackets, and note that a nested [ inside a set does not open a second set in most engines.
A literal closing bracket
echo 'a]b' | grep '[]]' # matches the literal ]
echo 'a]b' | grep '[]ab]' # set of ], a and b
echo 'a-b' | grep '[a-]' # hyphen last is literal
echo 'a^b' | grep '[x^]' # caret not first is literalIn POSIX bracket expressions, a ] directly after the opening [ or [^ is a member of the set, and that is the only way to include it. Writing [a]b] is a set a followed by the literal text b], not a set of three characters. A - is literal when first or last, and a ^ only negates in first position. In PCRE, Python and JavaScript, a backslash works as well, [\]], though not in POSIX tools.
POSIX classes need doubled brackets
echo "abc123" | grep -o '[[:digit:]]\+'
echo "abc123" | grep -o '[:digit:]\+'123
The class name is [:digit:] and sits inside a bracket expression, so you write [[:digit:]]. The second form is a set containing the characters :, d, i, g, t and matches the wrong characters; GNU grep warns "character class syntax is [[:space:]], not [:space:]" and newer versions treat it as an error. See POSIX character classes.
The shell removes characters
grep "[\]]" file # inside double quotes, the shell sees \]
grep '[]]' file
sed "s/[$chars]/X/g" file # if $chars contains ] or ^, the set changesSingle quotes pass every character unchanged. Inside double quotes the shell handles backslashes and expands variables, so a variable containing ], ^ or - changes the set's meaning, and a variable that is empty produces an empty set, an error. Escape or order the characters, or avoid building sets from variables; see sed with variables and special characters.
Locate the problem
Reduce the pattern until it compiles, then add parts back. Perl's message prints a <-- HERE marker at the failure point, which is the clearest of the four. Editors with regex highlighting show unbalanced brackets, and regex101.com with the right flavour explains the position. See testing and debugging a regex.
What a bracket expression allows
Between the brackets, a pattern lists characters, ranges and classes, and a few characters have special meaning only in certain positions. A hyphen between two characters forms a range, such as a-z, and is a literal at the start or end of the set. A caret right after the opening bracket negates the set and is a literal anywhere else. A closing bracket is a literal only as the first member. Backslash is an escape in Perl, Python, JavaScript and most modern flavours, and is a plain character in POSIX bracket expressions, so [\]] means a set containing a backslash, followed by a literal bracket in POSIX, and a set containing a closing bracket in Perl. The same text therefore ends differently on different engines, which is why a pattern that compiles in one place reports an unterminated set in another.
Equivalence classes and collating elements, written [=a=] and [.hyphen.], are rarely used and explain the odd wording of the GNU message that lists several bracket forms. An unmatched opening bracket anywhere in the pattern can start one of them, and the error then names all the possibilities.
The error moves when you add quoting
When the pattern comes through a shell, the character that closes the set can be consumed before the tool sees it. In double quotes, a backslash before a closing bracket is kept as two characters, which the tool reads as an escape or a literal depending on its flavour. A dollar sign starts an expansion, and an expanded variable that holds a bracket changes the structure of the set. In unquoted patterns the shell treats the brackets as a glob and may expand them to file names, or leave them alone if nothing matches, so the same command behaves differently depending on which files exist in the directory. Single-quote the pattern, and when a variable must be included, close the single quotes, add the variable in double quotes, and reopen them.
Building sets from user input
A set assembled from data, such as a list of allowed characters, is fragile. If the data includes a closing bracket, a caret at the start, a hyphen between letters or a backslash, the set changes meaning or fails to compile. Either escape each character for the target engine, which differs between flavours, or avoid a set altogether: test membership in code, strip with a loop, or use a tool built for the job. In Python, re.escape handles it, and in a shell script, tr -cd 'a-z0-9' keeps listed characters without a regex. For complex cleaning of text, a small program in a language with real string handling is more reliable than a clever pattern.
Debugging a long pattern
Bisecting is the most dependable method. Remove the second half of the pattern, check that it compiles, and add pieces back until the error returns. Tools such as regex101 in the browser, with the right flavour selected, highlight an unclosed set, explain each token and show test matches, though pasting private data into a website is not advisable. Editors with regex search show the same feedback locally. In Python, the re.DEBUG flag prints the parsed structure, and in Perl, use re 'debug' does the same, which shows exactly where the parser thought the set ended.
Prevention
Keep patterns short and readable. Use named constants for repeated sets, in verbose mode with comments where supported, and add unit tests for any pattern that is not obvious, with at least one input that should match and one that should not. Run the tests on every platform the code supports, because a bracket rule that one engine accepts is exactly the kind of difference that surfaces after deployment.
Where this does not apply
A different message, "Invalid range end" or "invalid character class", means a set such as [z-a] with reversed ends or an unknown class name. Unicode and locale-dependent ranges, such as [a-z] in a UTF-8 locale, can include characters you did not expect, so use [[:lower:]] or set LC_ALL=C. The syntax is in the POSIX bracket expression section and the GNU grep character class documentation.


