Regular expressions¶
A regular expression (regex) describes a shape of text rather than exact text: “a number at the start”, “anything in brackets at the end”, “two words separated by a comma”. OpenTag uses them to find that shape in your tags and file names, and to rewrite it.
This page takes you from your first pattern to lookarounds, then documents
the main places OpenTag accepts regular expressions. The web-source parser
commands have their own reference.
Each section builds on the one before. If you already know regular expressions from another program,
skip to How OpenTag's regular expressions differ and
the $regexp reference.
Try every example as you read
Select a file, open Convert → Tag → Tag… (Alt+5; Alt is ⌥ Option
on a Mac), choose a field such as TITLE and type a $regexp(…) format
string. The preview updates as you type, and nothing changes until you
apply it. Start with the examples below on a copy of your files.
Where you can use regular expressions¶
| Where | What it does | Case |
|---|---|---|
$regexp(…) in any format string |
Replaces every match and returns the result | Ignored by default; pass 0 as the fourth argument to respect it |
| Replace with regular expression action | Rewrites the values of a field, or the file name | Case-sensitive comparison checkbox, off by default |
Filter: field MATCHES pattern |
Shows only files whose field matches | Always ignored; write (?-i) to respect it |
| Find, strategy Regular expression | Lists matching filename/field values | Case sensitive checkbox |
| Web-source parser commands, such as SayRegexp and RegexpReplace | Read or transform downloaded text and parser outputs | Case-sensitive by default |
Format strings, actions, filters and web-source parser commands use the shared Boost-compatible core. Find uses the browser’s Unicode JavaScript regular expressions. Their advanced syntax and line-break rules differ; a pattern accepted in one context is not guaranteed to work in another. The detailed replacement and core flag rules below apply to the shared core; Find’s specific behavior is described separately.
The examples use $regexp(text,pattern,replacement) and show the text,
pattern and replacement in separate columns. You'd normally use a placeholder
such as %title% as the text.
1. Plain text is a pattern too¶
The simplest regular expression is ordinary text. It finds that text wherever it occurs, and every occurrence is replaced:
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Salt & Cedar |
& |
and |
Salt and Cedar |
Low Tide, Low Light |
Low |
High |
High Tide, High Light |
Paper Boats |
boats |
Planes |
Paper Planes |
Paper Boats |
Snow |
Rain |
Paper Boats |
Two things to notice:
- In
$regexp, case is ignored unless you ask otherwise, soboatsfoundBoats. - When nothing matches, you get the text back unchanged. That's convenient, but it also means a typo in a pattern fails silently. Check the preview.
Some characters have a special meaning in patterns: . ^ $ * + ? ( ) [ ] { } | \.
To match one of them literally, put a backslash in front: \., \(, \?.
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Fog Horn (Interlude) |
\(Interlude\) |
(empty) | Fog Horn |
Who's There? |
\? |
(empty) | Who's There |
2. Character classes: one character from a set¶
A class matches one character from a set.
| Pattern | Matches one… | Example match in Track 07: Low Tide |
|---|---|---|
. |
character of any kind | T (the first character) |
\d |
digit 0–9 |
0 |
\w |
word character: letter, digit or _ |
T |
\s |
space, tab or line break | the space after Track |
\D \W \S |
character that is not a digit / word character / space | T / the space / T |
[aeiou] |
of the listed characters | a |
[0-9a-f] |
in one of the ranges | a |
[^0-9] |
not in the set (^ right after [) |
T |
[[:punct:]] |
punctuation (also [[:alpha:]], [[:upper:]], [[:digit:]]…) |
: |
Case classes, control characters and code points¶
Some escapes have different meanings in a pattern and a replacement. The following table describes their pattern meaning:
| Pattern | Matches |
|---|---|
\l |
A lowercase character |
\u |
An uppercase character |
\L |
A character that is not lowercase |
\U |
A character that is not uppercase |
\t |
A tab |
\n |
A newline/line-feed character |
\r |
A carriage-return character |
\r\n |
A Windows-style line ending, carriage return followed by line feed |
\xnn |
A character identified by a two-digit hexadecimal code point, such as \x41 for A |
\x{nnnn} |
A character identified by a braced hexadecimal code point, such as \x{732B} for 猫 |
Case-insensitive matching also affects case classes. Use (?-i) when
distinguishing uppercase from lowercase is the purpose of the pattern:
| Text | Pattern | Replacement | Result |
|---|---|---|---|
aB1 |
(?-i)\l |
# |
#B1 |
aB1 |
(?-i)\u |
# |
a#1 |
aB1 |
(?-i)\L |
# |
a## |
aB1 |
(?-i)\U |
# |
#B# |
A |
\x41 |
# |
# |
猫 |
\x{732B} |
# |
# |
In a replacement, \u and \l instead change the next character's case,
and \U / \L change a run of output until \E. See
The replacement for that separate language.
Inside […], most special characters lose their meaning: [.?] matches a
dot or a question mark. To include ], \ or ^ (at the start), escape
them: [\]]. A - is literal at the start or end of the class: [_-].
| Text | Pattern | Replacement | Result |
|---|---|---|---|
paper_boats |
_ |
|
paper boats |
06_the_keeper's_song |
[_.] |
|
06 the keeper's song |
Track 07 |
\d |
# |
Track ## |
Salt & Cedar! |
[^\w\s] |
(empty) | Salt Cedar |
3. Repetition: how many times¶
A quantifier after a character or class says how often it may repeat.
| Quantifier | Means |
|---|---|
* |
0 or more times |
+ |
1 or more times |
? |
0 or 1 time (optional) |
{3} |
exactly 3 times |
{2,} |
2 or more times |
{1,3} |
between 1 and 3 times |
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Track 07 |
\d+ |
# |
Track # |
Low··Tide··· (· is a space) |
\s{2,} |
|
Low Tide |
colour, color |
colou?r |
hue |
hue, hue |
03 - Paper Boats |
\d+ - |
(empty) | Paper Boats |
Greedy and lazy¶
Quantifiers are greedy: they take as much as they can and still let the
rest of the pattern match. Add ? to make one lazy, taking as little as
possible:
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Juniper Vale - Salt - Cedar |
^(.*) - .*$ |
$1 |
Juniper Vale - Salt |
Juniper Vale - Salt - Cedar |
^(.*?) - .*$ |
$1 |
Juniper Vale |
(a) and (b) |
\(.*\) |
X |
X |
(a) and (b) |
\(.*?\) |
X |
X and X |
The (…) and $1 in the first two rows are explained in section 5.
4. Anchors: where the match may be¶
Anchors match a position, not a character.
| Anchor | Position |
|---|---|
^ |
Start of the text (and of every line in it) |
$ |
End of the text (and of every line in it) |
\A, \z |
Start and end of the whole text only |
\b |
A word boundary: between a word character and a non-word character |
\B |
Not a word boundary |
| Text | Pattern | Replacement | Result |
|---|---|---|---|
07 Fog Horn 07 |
^\d+ |
(empty) | Fog Horn 07 |
07 Fog Horn 07 |
\d+$ |
(empty) | 07 Fog Horn |
the other theme |
\bthe\b |
a |
a other theme |
··Low Tide· |
^\s+|\s+$ |
(empty) | Low Tide |
The last row uses |, which section 5 introduces: "spaces at the start, or
spaces at the end". (The table shows it as |; you type a plain |.)
5. Groups, alternatives and capturing¶
Parentheses do three jobs:
- Grouping, so a quantifier applies to several characters:
(ab)+. - Alternatives with
|:(Live|Demo|Remix)matches any one of those words. - Capturing: the text a group matched is remembered and numbered from the left, starting at 1, so the replacement can reuse it.
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Vale, Juniper |
^(\w+), (\w+)$ |
$2 $1 |
Juniper Vale |
The Lanterns |
^The (.+)$ |
$1, The |
Lanterns, The |
North Pier (Live) |
\((Live|Demo)\)$ |
[$1] |
North Pier [Live] |
2024-05-01 |
^(\d{4}).* |
$1 |
2024 |
Use (?:…) for a group that only groups and doesn't capture. Its contents
don't get a number, which keeps your $1, $2 easier to count:
removes an optional leading track number: 03_Paper Boats and Paper Boats
both become Paper Boats.
Inside the pattern itself, \1 means "the same text group 1 matched".
That finds repeated words:
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Low Low Tide |
\b(\w+) \1\b |
$1 |
Low Tide |
6. The replacement¶
The replacement is text with a few codes in it:
| Code | Inserts |
|---|---|
$1 … $9, ${12} |
The text of that group. Use braces when a digit follows: ${1}0 |
\1 … \9 |
The same as $1 … $9 |
$& or $0 |
The whole match |
$` |
The text before the match |
$' |
The text after the match |
$$ |
A literal $ |
\U … \E |
Upper-case everything up to \E (or to the end) |
\L … \E |
Lower-case everything up to \E |
\u, \l |
Upper- or lower-case only the next character |
\t, \n |
A tab, a line break |
With the text Juniper Vale and the pattern (\w+) (\w+):
| Replacement | Result |
|---|---|
$2, $1 |
Vale, Juniper |
\2, \1 |
Vale, Juniper |
[$&] |
[Juniper Vale] |
\U$1\E $2 |
JUNIPER Vale |
\L$0 |
juniper vale |
${1}_$2 |
Juniper_Vale |
$$5 |
$5 |
Case codes can capitalise words, a job a regex alone can't do. Section 7 refines this example:
| Text | Pattern | Replacement | Result |
|---|---|---|---|
paper boats |
\b(\w) |
\u$1 |
Paper Boats |
the keeper's song |
\b(\w) |
\u$1 |
The Keeper'S Song |
7. Lookaround: conditions without consuming¶
A lookaround checks what comes before or after a position without making it part of the match. That means it isn't replaced.
| Syntax | Means |
|---|---|
X(?=Y) |
X, if followed by Y |
X(?!Y) |
X, if not followed by Y |
(?<=Y)X |
X, if preceded by Y |
(?<!Y)X |
X, if not preceded by Y |
| Text | Pattern | Replacement | Result |
|---|---|---|---|
Track 12 of 15 |
\d+(?= of) |
[$&] |
Track [12] of 15 |
Track 12 of 15 |
(?<=of )\d+ |
[$&] |
Track 12 of [15] |
the keeper's song |
(?<![\w'])(\w) |
\u$1 |
The Keeper's Song |
photo.final.jpg |
\.(?=.*\.) |
_ |
photo_final.jpg |
The third row fixes the problem from section 6. A letter only starts a word if neither a word character nor an apostrophe comes before it. The fourth row replaces every dot that has another dot somewhere after it, so only the last one (before the extension) survives.
A lookbehind must have a fixed length: (?<=of ) works, (?<=of\s+) doesn't.
8. Case and other modifiers¶
Modifiers at the start of a pattern, or around part of it, change how it matches:
| Modifier | Effect |
|---|---|
(?i) |
Ignore case from here on |
(?-i) |
Respect case from here on |
(?i:…) |
Ignore case inside the group only |
(?-m) |
^ and $ match only at the start and end of the whole text, not at line breaks |
(?-s) |
. no longer matches line breaks |
(?x) |
Ignore spaces in the pattern and allow # comments (for long patterns) |
Modifiers win over the setting of the place you use the pattern:
| Format string | Result |
|---|---|
$regexp('Live At Home','live',x) |
x At Home |
$regexp('Live At Home','live',x,0) |
Live At Home |
$regexp('Live At Home','(?-i)live',x) |
Live At Home |
$regexp('Live At Home','(?i)LIVE',x,0) |
x At Home |
$regexp('Live at home','(?-i)(?i:AT) home',x) |
Live x |
$regexp(x,expr,repl[,c])¶
Replaces every match of a regular expression in a text.
| Argument | Meaning |
|---|---|
x |
The text to search, usually a placeholder such as %title% |
expr |
The regular expression |
repl |
The replacement, with the codes from section 6 |
c |
Optional. 0 respects case; leave it out (or use any other value) to ignore case |
Returns x with every match replaced, or x unchanged when nothing matches.
| Format string | With these values | Result |
|---|---|---|
$regexp(%title%,'\s*\(feat\. [^)]+\)',) |
TITLE = Paper Boats (feat. Mira Solberg) |
Paper Boats |
$regexp(%title%,'^.*\(feat\. ([^)]+)\).*$','$1') |
same | Mira Solberg |
$regexp(%year%,'^(\d{4}).*$','$1') |
YEAR = 2024-05-01 |
2024 |
$regexp(%artist%,'^The (.+)$','$1, The') |
ARTIST = The Lanterns |
Lanterns, The |
$regexp(%title%,'abc',x) |
TITLE = ABC abc |
x x |
$regexp(%title%,'abc',x,0) |
TITLE = ABC abc |
ABC x |
Quote the pattern and the replacement¶
Inside a format string, $, %, [, ], ', , and ) have their own
meaning (see Special characters), and
regular expressions are full of them. Put the pattern and the replacement
in single quotes to keep their syntax literal:
Balanced unquoted groups can also work: argument parentheses nest, and
dollar captures such as $2 remain literal. Quotes still protect other
format syntax, including optional-section brackets and argument commas.
Inside quotes, write an apostrophe as '':
To remove matches, leave the replacement argument empty after its comma:
Do not use '' as an empty replacement: it means one literal apostrophe.
Backslashes need no doubling: format strings don't treat \ specially.
Gotcha: no match means no change
$regexp(%title%,'^.*\(feat\. ([^)]+)\).*$','$1') returns the
whole title for a song without “feat.”, because nothing matched. If you
store the result in ARTIST, songs without a guest get their title as
artist. Restrict the files first (filter
title MATCHES "\(feat\."), or guard the expression:
$if($grtr($strstr($lower(%title%),'(feat. '),0),$regexp(%title%,'^.*\(feat\. ([^)]+)\).*$','$1'),%artist%)
The search checks a lowercase copy so FEAT. follows the same
case-insensitive rule as the regex. The extraction still reads the
original title, preserving the guest's spelling.
Gotcha: only the first value of a multi-value field
%artist% gives only the first artist, so $regexp(%artist%,…) only sees
that one. To transform every value, use the
Replace with regular expression
action on the field.
The Replace with regular expression action¶
The action rewrites a field in place. Its settings:
| Setting | Meaning |
|---|---|
| Field | The field to change. _ALL changes every field, _FILENAME the file name, _DIRECTORY the folder name |
| Regular expression | The pattern |
| Replace matches with | The replacement, with the codes from section 6 |
| Case-sensitive comparison | Off: case is ignored (the default). On: case must match |
Differences from $regexp:
- No quoting needed. The pattern and the replacement are plain text, not
format strings. Type
$1as is, and%is just a percent sign. - Every value of a multi-value field is rewritten separately.
-
On
_FILENAMEthe extension is part of the text. A pattern anchored with$meets the end of.mp3, not the end of the name. To trim trailing spaces from a name, look ahead to the extension:Field Regular expression Replace matches with Low Tide .mp3becomes_FILENAME\s+$(empty) Low Tide .mp3(no change)_FILENAME\s+(?=\.\w+$)(empty) Low Tide.mp3
Like every action, it can be undone with Cmd+Z / Ctrl+Z after it has written the files.
In the filter and Find¶
In the filter, MATCHES tests a field against a regular
expression. The match always ignores case, so add (?-i) when case
matters. Put the pattern in double quotes when it contains spaces or
parentheses:
| Filter | Shows files where |
|---|---|
title MATCHES "\(live\)" |
the title contains (live) |
track MATCHES ^\d+$ |
the track has no total (7, not 7/12) |
title MATCHES "(?-i)^[^a-z]+$" |
the title has no lower-case letters (ALL CAPS) |
* MATCHES "\s{2,}" |
any field contains a double space |
artist MATCHES "^the " |
the artist starts with "The" |
Find uses JavaScript regular expressions¶
In Edit → Find… (Cmd+F / Ctrl+F), choose Regular expression to search filenames and individual tag values. Use Case sensitive to control letter case. Find compiles its pattern with JavaScript’s Unicode mode and an optional case-insensitive flag; it does not apply the shared core’s default multiline/dot-all behavior or replacement syntax.
For example, unscoped (?i) is accepted by the shared core but is invalid
in Find. Type boats and use the checkbox instead:
| Pattern | Case sensitive | Value | Result |
|---|---|---|---|
boats |
Off | Paper Boats |
Match |
boats |
On | Paper Boats |
No match |
(?i)boats |
Off | Paper Boats |
Invalid pattern |
Find reports an invalid expression instead of treating it as no matches. It does not directly rewrite tags. Selecting a result changes the main selection, where configured tag-panel defaults or save-on-move settings can still act. Review the selection before applying an action.
A pattern library¶
Copy, paste, adapt. Each row is ready to use in $regexp (with the quotes
shown) or, without the quotes, in the action. Leave empty means no text
after the last comma in $regexp, or an empty replacement box in the action.
| Goal | Pattern | Replacement | Before → After |
|---|---|---|---|
| Remove a leading track number | '^\d+[\s._-]*' |
Leave empty | 03_Paper Boats → Paper Boats |
| Underscores to spaces | '_' |
' ' |
the_keeper's_song → the keeper's song |
| Collapse repeated spaces | '\s{2,}' |
' ' |
Low··Tide → Low Tide |
| Trim spaces at both ends | '^\s+|\s+$' |
Leave empty | ·Low Tide· → Low Tide |
| Remove a "(Remastered …)" suffix | '\s*\((Remaster(ed)?|Live)[^)]*\)$' |
Leave empty | North Pier (Remastered 2021) → North Pier |
| Remove anything in square brackets | '\s*\[[^\]]*\]' |
Leave empty | Low Tide [Bonus] → Low Tide |
| Remove "feat." from a title | '\s*\(feat\. [^)]+\)' |
Leave empty | Paper Boats (feat. Mira Solberg) → Paper Boats |
| Swap "Last, First" | '^([^,]+), (.+)$' |
'$2 $1' |
Vale, Juniper → Juniper Vale |
| Move "The" to the end | '^The (.+)$' |
'$1, The' |
The Lanterns → Lanterns, The |
| Keep the year of a date | '^(\d{4}).*$' |
'$1' |
2024-05-01 → 2024 |
| "&" to "and" | '\s*&\s*' |
' and ' |
Salt & Cedar → Salt and Cedar |
| Capitalise each word | '(?<![\w''])(\w)' |
'\u$1' |
the keeper's song → The Keeper's Song |
| Upper-case a whole word | '\b(dj)\b' |
'\U$1' |
dj nimbus → DJ nimbus |
(· stands for a space, and | for a plain |.)
How OpenTag's regular expressions differ¶
The shared core used by format strings, actions, filters and web-source parser commands accepts Boost’s Perl-compatible syntax. The following core rules do not describe Find’s JavaScript engine. If you come from another tool, watch for these:
- Case is ignored by default in
$regexp, the action and the filter. JavaScript, Python and most editors respect case by default. - Replacements are global. Every match is replaced, not just the first.
\dmeans only0–9. Full-width and other non-ASCII digits don't count as digits.^and$also match at line breaks, and.matches line breaks. This only matters for multi-line values such as comments and lyrics. Use\Aand\z, or(?-m)and(?-s), for the stricter behaviour.\uand\lin a pattern are classes ("an upper-case letter", "a lower-case letter"), not Unicode escapes. Write a code point as\x{732B}. With case ignored,\umatches any letter. In a replacement,\uand\lchange the case of the next character.- An empty pattern matches between every character:
$regexp(ab,,_)gives_a_b_. The pattern argument is empty;''would search for an apostrophe instead. - Characters outside the Basic Multilingual Plane, such as emoji, count
as two characters for
.and{n}. - Case-insensitive matching of non-English letters follows your system's language settings.
- Boost's extended conditional replacement syntax is not enabled. Use
format-string conditions around
$regexpwhen the output needs a decision.