{"id":486811,"date":"2026-07-09T18:32:26","date_gmt":"2026-07-09T18:32:26","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=486811"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=486811","title":{"rendered":"Atomic Regular Expressions"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p> In this series of articles, we explore the capabilities of the <a href=\"https:\/\/www.autohotkey.com\/docs\/v2\/Language.htm\" rel=\"noopener noreferrer nofollow\">AutoHotkey<\/a> language by demonstrating how to output colored text to the terminal for the <a href=\"https:\/\/github.com\/JoyHak\/Launcher\" rel=\"noopener noreferrer nofollow\">Launcher<\/a> console utility. <\/p>\n<p><a href=\"https:\/\/habr.com\/ru\/articles\/1053052\/\" rel=\"noopener noreferrer nofollow\">Part 1: Naive algorithm and its optimization<\/a> <br \/><a href=\"https:\/\/habr.com\/ru\/articles\/1054890\/\" rel=\"noopener noreferrer nofollow\">Part 2: Control sequences<\/a> <br \/>Part 3: Atomic Regular Expressions (you are here) <br \/>Part 4: Declarative Programming <br \/>Part 5: 256 Colors and Text Styling<\/p>\n<p>In the previous part, we learned about control sequences and how to apply them in our algorithm to change the color of text. In this article, we will explore some of the capabilities of regular expressions that will allow us to change the color of an entire text region.<\/p>\n<p>If you are not familiar with regular expressions at a basic level, I recommend reading <a href=\"https:\/\/habr.com\/ru\/articles\/990282\/\" rel=\"noopener noreferrer nofollow\">this article<\/a>. If you are not familiar with the AutoHotkey syntax, I recommend reading <a href=\"https:\/\/habr.com\/ru\/articles\/1053052\/\" rel=\"noopener noreferrer nofollow\">the first part<\/a>. However, this is not necessary to understand this article \ud83d\ude09<\/p>\n<h3>Constructing Expressions<\/h3>\n<h4>Paired Characters<\/h4>\n<p>In the previous part, we wrote an algorithm for processing character pairs: <code>\" \"<\/code> or <code>* *<\/code>. We pass the intermediate array <code>aRegexColor<\/code> consisting of \u201ccharacter-color\u201d pairs, convert it into a HashMap <code>chrColors<\/code> containing \u201ccharacter-code\u201d pairs, and then search for each pair and add the corresponding character from <code>chrColors<\/code> before the opening character:<\/p>\n<pre><code class=\"coffeescript\">Color(msg, aRegexColor) {    static colors := Map(        'black',    30,        'red',      31,        'orange',   33,        'magenta',  35,        'gray',     90,        'crimson',  91,        'green',    92,        'yellow',   93,        'blue',     94,        'purple',   95,        'cyan',     96,    )        static esc := Chr(27)    static end := esc '[0m'        regex     := '' ; final regular expression    chars     := '' ; character string    chrColors := Map()        index := 1    loop (aRegexColor.length \/ 2) {        ; `aRegexColor` - input array of \"char-color\" pairs        str   := aRegexColor[index++]        color := aRegexColor[index++]            chars .= str        chrColors[str] := colors[color]  ; `colors` - HashMap of \"color-code\" pairs    }        if chars        regex .= '([' chars '])'    else        regex := regex.RTrim('|')            stack := []    while (pos &lt;= len) {        if !RegExMatch(msg, regex, &amp;match, pos) {            ; Remaining text            clrMsg .= msg.Slice(pos)            break        }                ; Normal text before the found character        clrMsg .= msg.Slice(pos, match.pos - pos)        ; Move forward        pos := match.pos + match.len                if (stack.Has(-1) &amp;&amp; stack[-1] = match[1]) {            clrMsg .= match[1] . end            stack.Pop()        } else {            begin  := esc '[0;' chrColors[match[1]] 'm'            clrMsg .= begin . match[1]            stack.Push(match[1])        }    }        return clrMsg}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><a href=\"https:\/\/www.autohotkey.com\/docs\/v2\/lib\/RegExMatch.htm\" rel=\"noopener noreferrer nofollow\">RegExMatch<\/a> searches for characters from the set (e.g., <code>[\"*]<\/code>) and stores one of the found characters in <code>match[1]<\/code>. As a result, the algorithm processes one character per iteration. This allows us to correctly highlight text inside paired characters: <code>\"text and text\"<\/code>. It also highlights text inside that text: <code>\"text *and* text\"<\/code>.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/302\/cc2\/d47\/302cc2d473a067c285e3aef17aad71a5.gif\" width=\"1344\" height=\"474\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/302\/cc2\/d47\/302cc2d473a067c285e3aef17aad71a5.gif 780w,&#10;       https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/302\/cc2\/d47\/302cc2d473a067c285e3aef17aad71a5.gif 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>However, we have a more complex help message in which we need to highlight specific constructs: <code>--switches<\/code>, <code>@file<\/code>, and variables like <code>%AhkDir%<\/code>. We need to improve the algorithm and add the ability to work with entire expressions, rather than just individual characters.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fb0\/3a4\/b80\/fb03a4b809e3a8ca73ce92cef1143677.png\" width=\"1372\" height=\"325\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/fb0\/3a4\/b80\/fb03a4b809e3a8ca73ce92cef1143677.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fb0\/3a4\/b80\/fb03a4b809e3a8ca73ce92cef1143677.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>Key-Value<\/h4>\n<p>In the algorithm above, we passed a \u201ccharacter-color\u201d array, where the <em>key<\/em> is a character and the <em>value<\/em> is a color code:<\/p>\n<pre><code class=\"coffeescript\">msg.Color([   '\"',  'cyan',  '*',  'green'])<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>This array was converted to <code>chrColors<\/code>:<\/p>\n<pre><code class=\"coffeescript\">\" 96* 92<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>As a result, we could call <code>chrColors['\"']<\/code> or <code>chrColors['*']<\/code>, where each character served simultaneously as a search pattern (the regular expression, the <code>regex<\/code> variable), the matched character (the <code>match[1]<\/code>), and a key in <code>chrColors<\/code>.<\/p>\n<p>Now we\u2019re going to pass \u201cexpression-color\u201d pairs (for simplicity, let\u2019s temporarily forget about characters):<\/p>\n<pre><code class=\"coffeescript\">msg.Color([  '(@(file|list\\.ini))',    'yellow',   ; list  '(mainDir|AhkDir)(?=\\=)', 'purple',   ; variables names  '(%[^%]+%)',              'blue',     ; variables values  '(\\-+[\\-\\w]+)(?=[ =])',   'cyan',     ; switches])<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>With this approach, each expression can no longer serve as both a search pattern and a matched expression (text) at the same time, and therefore cannot be a key in <code>chrColors<\/code>.<\/p>\n<p>Every regular expression we pass to <a href=\"https:\/\/www.autohotkey.com\/docs\/v2\/lib\/RegExMatch.htm\" rel=\"noopener noreferrer nofollow\">RegExMatch<\/a> is plain text. On its own, it doesn\u2019t search for anything; instead, it is first compiled into a set of instructions for the processor. In other words, Regular Expressions is something that we can call a low-level language that can be compiled, where every character, such as <code>\\w<\/code>, is converted into a specific instruction.<\/p>\n<p>In our case, this means that an expression like <code>(%[^%]+%)<\/code> is converted into code that searches for a text like <code>%AhkDir%<\/code>, <code>%mainDir%<\/code>, and so on. In other words, the input (<code>(%[^%]+%)<\/code>) is not equal to the output (<code>%mainDir%<\/code>), and as a result, information about the original regular expression itself &#8212; which allowed us to apply the desired color to the found text &#8212; is lost. Consequently, the expression itself cannot serve as a key in <code>chrColors<\/code> to retrieve the color.<\/p>\n<h4>Mark<\/h4>\n<p>As you may have noticed earlier, the <code>match<\/code> variable is somewhat similar to an array: <code>match[0]<\/code> stores everything that was found, and <code>match[1]<\/code> stores what capturing group 1 found. However, it is actually a <strong>match\u00a0object<\/strong>, and it stores not only <em>indices<\/em> but also <em>fields<\/em>: <code>match.pos<\/code>, <code>match.len<\/code>, <code>match.count<\/code>. It also has a <code>mark<\/code> field. When <code>RegExMatch()<\/code> encounters the <a href=\"https:\/\/pcre.org\/original\/doc\/html\/pcrepattern.html#SEC27\" rel=\"noopener noreferrer nofollow\"><code>(*MARK:..)<\/code> construct<\/a> in the pattern, the <code>mark<\/code> field takes on the value following the colon <code>:<\/code> For example, after <code>(*MARK:chr)<\/code>, the value of <code>match.mark<\/code> will be <code>\"chr\"<\/code>.<\/p>\n<p>Let\u2019s look at another expression:<\/p>\n<pre><code class=\"cs\">(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>If <em>group 1<\/em> is captured, <code>match.mark<\/code> will be equal to \u201c1\u201d. If <em>group 2<\/em> is captured, <code>match.mark<\/code> will be equal to \u201c2\u201d.<\/p>\n<p>We can use this feature to <em>mark<\/em> (index) the patterns: 1, 2, 3, \u2026 Each pattern will have a unique <em>mark<\/em>, a unique key that we\u2019ll use to retrieve the color from <code>chrColors<\/code>:<\/p>\n<pre><code class=\"coffeescript\">begin  := esc '[0;' chrColors[match.mark] 'm'clrMsg .= begin . match[1]<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>To do this, we need to add <em>mark<\/em> with the corresponding index to the end of each pattern in the <code>aRegexColor<\/code> array, and then combine all the patterns into a single regular expression using the OR operator <code>|<\/code><\/p>\n<pre><code class=\"coffeescript\">regex     := '' ; final regular expression    chars     := ''     ; separate expression with chars    chrColors := Map()        index     := 1; array index    idxColor  := 1  ; mark index        loop (aRegexColor.length \/ 2) {        str   := aRegexColor[index++]        color := aRegexColor[index++]        if (str.length = 1) {            ; Characters are grouped into set like [\"*\"]            chars .= str            chrColors[str] := colors[color]; char-code        } else {            ; Group patterns into one            regex .= str '(*MARK:' idxColor ')|'             chrColors[String(idxColor++)] := colors[color]  ; index-code        }    }        if chars        regex .= '([' chars '])(*MARK:chr)'  ; Add the characters separately at the end    else        regex := regex.RTrim('|') ; Remove the extra OR |<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The result will be a single large expression like <code>(..)(*MARK:1)|(..)(*MARK:2)|...|([..])(*MARK:chr)<\/code>. For convenience, we\u2019ll use the same HashMap that contains \u201cindex-code\u201d pairs and the \u201ccharacter-code\u201d pairs we\u2019re already familiar with:<\/p>\n<ol>\n<li>\n<p>Expressions: the key is <code>match.mark<\/code> (index).<\/p>\n<\/li>\n<li>\n<p>Characters: the key is the character itself, <code>match[1]<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>Since each expression (whether a <em>character<\/em> or an <em>indexed expression<\/em>) has a different key, we need to distinguish between <em>indexed expressions<\/em> and <em>character expressions<\/em> using <code>(*MARK:chr)<\/code>:<\/p>\n<pre><code class=\"coffeescript\">        if (match.mark != 'chr') {            ; Indexed expression: wrap in color            begin  := esc '[0;' chrColors[match.mark] 'm'            clrMsg .= begin . match[1] . end            continue        }        ; Character expression        if (stack.Has(-1) &amp;&amp; stack[-1] = match[1]) {            clrMsg .= end            stack.Pop()        } else {            begin  := esc '[0;' chrColors[match[1]] 'm'            clrMsg .= begin            stack.Push(match[1])        }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>Branch Reset<\/h4>\n<p>The constructed expression has a critical flaw: it has many capturing groups. As a result, each captured text fragment will be associated with its own capturing group: 1, 2, \u2026<\/p>\n<p>Let\u2019s look at this code snippet:<\/p>\n<pre><code class=\"coffeescript\">\"%variable% is ~gray~\".Color([    '(%[^%]+%)', 'blue',     '(~[^~]+~)', 'gray'])<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>Color<\/code> will construct the expression:<\/p>\n<pre><code class=\"cs\">(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>which contains 2 capturing groups:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th data-colwidth=\"71\" width=\"71\">\n<p align=\"left\">Index<\/p>\n<\/th>\n<th data-colwidth=\"109\" width=\"109\">\n<p align=\"left\">Group<\/p>\n<\/th>\n<th data-colwidth=\"498\" width=\"498\">\n<p align=\"left\">Text<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td data-colwidth=\"71\" width=\"71\">\n<p align=\"left\">1<\/p>\n<\/td>\n<td data-colwidth=\"109\" width=\"109\">\n<p align=\"left\">(%[^%]+%)<\/p>\n<\/td>\n<td data-colwidth=\"498\" width=\"498\">\n<p align=\"left\">%variable%<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"71\" width=\"71\">\n<p align=\"left\">2<\/p>\n<\/td>\n<td data-colwidth=\"109\" width=\"109\">\n<p align=\"left\">(~[^~]+~)<\/p>\n<\/td>\n<td data-colwidth=\"498\" width=\"498\">\n<p align=\"left\">~gray~<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>Accordingly, the text <code>~gray~<\/code> will be stored in <code>match[2]<\/code>, while <code>match[1]<\/code> (which we use in the algorithm) will be empty. In other words, we need to know the group\u2019s index to retrieve the text. But if the user has included multiple groups in each pattern, such as <code>=(%([^@](\\w+))%)<\/code>, it will be difficult to determine the index. We want the text or character to always be stored in <code>match[1]<\/code>.<\/p>\n<p>To solve this problem, we can use <a href=\"https:\/\/pcre.org\/original\/doc\/html\/pcrepattern.html#TOC1\" rel=\"noopener noreferrer nofollow\">Branch Reset<\/a>. Branch Reset is a <code>(?|..)<\/code> construct, which makes the index of each group relative. If we represent the expression <code>()|()|...<\/code> as a list of items separated by <code>|<\/code>, then the index of each capturing group within each item will be relative to that item:<\/p>\n<pre><code class=\"coffeescript\">( () )|( () () )1  2   1  2  3 <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>In other words, we don\u2019t look at the entire expression, but rather to its individual items, and then at groups within those items. Without Branch Reset, indexing is absolute:<\/p>\n<pre><code class=\"coffeescript\">( () )|( () () )1  2   3  4  5 <\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Let\u2019s wrap the constructed expression in <code>(?|..)<\/code>:<\/p>\n<pre><code class=\"cs\">(?|(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2))<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>It is important to understand that <code>(?|..)<\/code> is not a capturing group, but a \u201cBranch Reset\u201d construct. And <code>(*MARK:..)<\/code> is a \u201cverb\u201d (<strong>verb<\/strong>). The parentheses are merely part of the syntax.<\/p>\n<p>As a result, we end up with the following indexing:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th data-colwidth=\"87\" width=\"87\">\n<p align=\"left\">Index<\/p>\n<\/th>\n<th data-colwidth=\"75\" width=\"75\">\n<p align=\"left\">Mark<\/p>\n<\/th>\n<th data-colwidth=\"116\" width=\"116\">\n<p align=\"left\">Group<\/p>\n<\/th>\n<th>\n<p align=\"left\">Text<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td data-colwidth=\"87\" width=\"87\">\n<p align=\"left\">1<\/p>\n<\/td>\n<td data-colwidth=\"75\" width=\"75\">\n<p align=\"left\">1<\/p>\n<\/td>\n<td data-colwidth=\"116\" width=\"116\">\n<p align=\"left\">(%[^%]+%)<\/p>\n<\/td>\n<td>\n<p align=\"left\">%variable%<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"87\" width=\"87\">\n<p align=\"left\">1<\/p>\n<\/td>\n<td data-colwidth=\"75\" width=\"75\">\n<p align=\"left\">2<\/p>\n<\/td>\n<td data-colwidth=\"116\" width=\"116\">\n<p align=\"left\">(~[^~]+~)<\/p>\n<\/td>\n<td>\n<p align=\"left\">~gray~<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p><code>match[1]<\/code> will always contain the captured text from the 1st, 2nd, or 3rd group. At the same time, <code>match.mark<\/code> will still store the correct index:<\/p>\n<pre><code class=\"coffeescript\">    if chars        regex .= '([' chars '])(*MARK:chr)'    else        regex := regex.RTrim('|')    regex := '(?|' regex ')'<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>Color Switching<\/h4>\n<p>Currently, our algorithm simply wraps the found text fragment in the opening and closing colors:<\/p>\n<pre><code class=\"coffeescript\">     static esc := Chr(27)     static end := esc '[0m'         if (match.mark != 'chr') {            ; Indexed expression: wrap in color            begin  := esc '[0;' chrColors[match.mark] 'm'            clrMsg .= begin . match[1] . end            continue        }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>However, as you may remember from the <a href=\"https:\/\/habr.com\/ru\/articles\/1054890\/\" rel=\"noopener noreferrer nofollow\">previous part<\/a>, control sequences \u201ctoggle\u201d colors:<\/p>\n<pre><code class=\"powershell\">\\e[96m\"  &lt;- own color# level 1text     \\e[92m*   &lt;- own color   # level 2    and    *\\e[96m    &lt;- previous color   text\"\\e[0m   &lt;- no previous color<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>The closing color of each pair of characters depends on the nesting level of that pair. That is why we use a <em>stack<\/em> to keep track of which color to apply: the opening color (the <code>begin<\/code> variable) or the closing color <code>\\e[0m<\/code> (the <code>end<\/code> variable).<\/p>\n<p>The closing color does not depend on anything in the current algorithm. The text region is simply wrapped in color, and then the color is reset to white using <code>\\e[0m<\/code>.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/914\/74b\/572\/91474b5727d20d2b7eb9855a9e2a0d9b.gif\" width=\"805\" height=\"434\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/914\/74b\/572\/91474b5727d20d2b7eb9855a9e2a0d9b.gif 780w,&#10;       https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/914\/74b\/572\/91474b5727d20d2b7eb9855a9e2a0d9b.gif 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>However, it would be more correct to check whether we are inside a pair of characters (e.g., <code>\" \"<\/code>) and switch the color to the previous one, opened by the paired character:<\/p>\n<pre><code class=\"coffeescript\">        if (match.mark != 'chr') {            ; We take the `begin` color from chrColors            begin  := esc '[0;' chrColors[match.mark] 'm'            if (stack.Has(-1)) {                ; Within a pair, we look for the closing color                 ; using the \"key\" from the stack                _end := esc '[0;' chrColors[stack[-1]] 'm'                clrMsg .= begin . match[1] . _end            } else {                ; Independent - close the color                clrMsg .= begin . match[1] . end            }            continue        }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/942\/61f\/8cb\/94261f8cb5129434077f2206e593b6e9.gif\" width=\"800\" height=\"319\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/942\/61f\/8cb\/94261f8cb5129434077f2206e593b6e9.gif 780w,&#10;       https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/942\/61f\/8cb\/94261f8cb5129434077f2206e593b6e9.gif 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>As a result, the algorithm, whose <a href=\"https:\/\/github.com\/JoyHak\/Launcher\/blob\/e666e853253b6627bd6767a743ad3f47a557bcf6\/Lib\/output.ahk#L1\" rel=\"noopener noreferrer nofollow\">source code is available on GitHub<\/a>, looks like this:<\/p>\n<pre><code class=\"coffeescript\">; Define add. method for readability({}.DefineProp)(String.prototype, 'Match', {call: RegExMatch})Color(msg, aRegexColor) {; The color name corresponds to the color code    static colors := Map(        'black',    30,        'red',      31,        'orange',   33,        'magenta',  35,        'gray',     90,        'crimson',  91,        'green',    92,        'yellow',   93,        'blue',     94,        'purple',   95,        'cyan',     96,    )        ; Constants    static esc := Chr(27)    static end := esc '[0m'; Transform the input array    regex     := ''    chars     := ''    chrColors := Map()    chrColors.capacity := aRegexColor.capacity        index     := 1    ; array index    idxColor  := 1    ; mark index        loop (aRegexColor.length \/ 2) {        str   := aRegexColor[index++]        color := aRegexColor[index++]        if (str.length = 1) {            ; Group chars into a set [..]            chars .= str            chrColors[str] := colors[color]    ; char-code        } else {            ; Group atomic patterns using OR |            regex .= str '(*MARK:' idxColor ')|'             chrColors[String(idxColor++)] := colors[color]    ; index-code        }    }        if chars        regex .= '([' chars '])(*MARK:chr)'    else        regex := regex.RTrim('|')            ; Apply Branch Reset    regex := options '(?|' regex ')'            ; Color the message    pos := 1    len := msg.length    clrMsg := ''        stack := []    stack.capacity := aRegexColor.capacity * 2    while (pos &lt;= len) {        if !msg.Match(regex, &amp;match, pos) {            ; Remaining text            clrMsg .= msg.Slice(pos)            break        }                ; Regular text before the match        clrMsg .= msg.Slice(pos, match.pos - pos)        ; Move forward        pos    := match.pos + match.len                if (match.mark != 'chr') {            ; Atomic pattern.            ; Take the `begin` color from `chrColors`            begin  := esc '[0;' chrColors[match.mark] 'm'            if (stack.Has(-1)) {                ; Within a pair, we look for the closing color                 ; using the \"key\" from the stack                _end := esc '[0;' chrColors[stack[-1]] 'm'                clrMsg .= begin . match[1] . _end            } else {                ; Independent - close the color                clrMsg .= begin . match[1] . end            }            continue        }        ; Paired char        if (stack.Has(-1) &amp;&amp; stack[-1] = match[1]) {            ; Found the char that completes the pair.             ; Close the color            clrMsg .= end            stack.Pop()        } else {            ; Open matching color            begin  := esc '[0;' chrColors[match[1]] 'm'            clrMsg .= begin            stack.Push(match[1])        }    }        return clrMsg}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>In the <a href=\"https:\/\/habr.com\/ru\/articles\/1053052\/\" rel=\"noopener noreferrer nofollow\">first part<\/a>, we began with the help message. <a href=\"https:\/\/github.com\/JoyHak\/Launcher\/blob\/4c3f2ac4729f54c35f60068ebbcab8d534178b4d\/launcher.ahk#L9\" rel=\"noopener noreferrer nofollow\">Now we can color it completely<\/a>:<\/p>\n<pre><code class=\"coffeescript\">PrintHelp(*) {; ...    msg :=       msg.Color([        '(@(file|list\\.ini))',    'yellow',   ; list        '(mainDir|AhkDir)(?=\\=)', 'purple',   ; variables names        '(%[^%]+%)',              'blue',     ; variables values        '(\\-+[\\-\\w]+)(?=[ =])',   'cyan',     ; switches        '\\*\\*([^\\*]+)\\*\\*',       'crimson',          '__([^_]+)__',            'magenta',          '~',                      'gray',        '``',                     'green',         '#',                      'orange'      ]).Print()}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cef\/2e5\/ce1\/cef2e5ce1514940055fbc0f737117d1e.png\" width=\"1015\" height=\"985\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/cef\/2e5\/ce1\/cef2e5ce1514940055fbc0f737117d1e.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cef\/2e5\/ce1\/cef2e5ce1514940055fbc0f737117d1e.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>Conclusion<\/h4>\n<p><a href=\"https:\/\/habr.com\/ru\/articles\/990282\/\" rel=\"noopener noreferrer nofollow\">Regular Expressions<\/a> are very convenient tool to parse text. They are not so readable, but they allow us to find text regions using low-level code and process them using the high-level <a href=\"https:\/\/www.autohotkey.com\/docs\/v2\/Language.htm\" rel=\"noopener noreferrer nofollow\">AutoHotkey<\/a> language.<\/p>\n<p>In this article, we saw that regular expressions have the ability to track the path and they have some reflection. Thanks to these capabilities, we were able to use them more effectively and write a concise and readable algorithm.<\/p>\n<p>In the next article, we\u2019ll explore how to describe text parsing in human-readable language using AutoHotkey and examine the language from the perspective of declarative programming. <a href=\"https:\/\/github.com\/JoyHak?tab=repositories\" rel=\"noopener noreferrer nofollow\">Check out my GitHub<\/a> if you want to see the AutoHotkey capabilities in action!<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1057662\/\">https:\/\/habr.com\/ru\/articles\/1057662\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p> In this series of articles, we explore the capabilities of the AutoHotkey language by demonstrating how to output colored text to the terminal for the Launcher console utility. Part 1: Naive algorithm and its optimization Part 2: Control sequences Part 3: Atomic Regular Expressions (you are here) Part 4: Declarative Programming Part 5: 256 Colors and Text StylingIn the previous part, we learned about control sequences and how to apply them in our algorithm to change the color of text. In this article, we will explore some of the capabilities of regular expressions that will allow us to change the color of an entire text region.If you are not familiar with regular expressions at a basic level, I recommend reading this article. If you are not familiar with the AutoHotkey syntax, I recommend reading the first part. However, this is not necessary to understand this article ;)Constructing ExpressionsPaired CharactersIn the previous part, we wrote an algorithm for processing character pairs: &#187; &#187; or * *. We pass the intermediate array aRegexColor consisting of \u201ccharacter-color\u201d pairs, convert it into a HashMap chrColors containing \u201ccharacter-code\u201d pairs, and then search for each pair and add the corresponding character from chrColors before the opening character:Color(msg, aRegexColor) {    static colors := Map(        &#8216;black&#8217;,    30,        &#8216;red&#8217;,      31,        &#8216;orange&#8217;,   33,        &#8216;magenta&#8217;,  35,        &#8216;gray&#8217;,     90,        &#8216;crimson&#8217;,  91,        &#8216;green&#8217;,    92,        &#8216;yellow&#8217;,   93,        &#8216;blue&#8217;,     94,        &#8216;purple&#8217;,   95,        &#8216;cyan&#8217;,     96,    )        static esc := Chr(27)    static end := esc &#8216;[0m&#8217;        regex     := &#187; ; final regular expression    chars     := &#187; ; character string    chrColors := Map()        index := 1    loop (aRegexColor.length \/ 2) {        ; `aRegexColor` &#8212; input array of &#171;char-color&#187; pairs        str   := aRegexColor[index++]        color := aRegexColor[index++]            chars .= str        chrColors[str] := colors[color]  ; `colors` &#8212; HashMap of &#171;color-code&#187; pairs    }        if chars        regex .= &#8216;([&#8216; chars &#8216;])&#8217;    else        regex := regex.RTrim(&#8216;|&#8217;)            stack := []    while (pos &lt;= len) {        if !RegExMatch(msg, regex, &amp;match, pos) {            ; Remaining text            clrMsg .= msg.Slice(pos)            break        }                ; Normal text before the found character        clrMsg .= msg.Slice(pos, match.pos &#8212; pos)        ; Move forward        pos := match.pos + match.len                if (stack.Has(-1) &amp;&amp; stack[-1] = match[1]) {            clrMsg .= match[1] . end            stack.Pop()        } else {            begin  := esc &#8216;[0;&#8217; chrColors[match[1]] &#8216;m&#8217;            clrMsg .= begin . match[1]            stack.Push(match[1])        }    }        return clrMsg}RegExMatch searches for characters from the set (e.g., [&#171;*]) and stores one of the found characters in match[1]. As a result, the algorithm processes one character per iteration. This allows us to correctly highlight text inside paired characters: &#171;text and text&#187;. It also highlights text inside that text: &#171;text *and* text&#187;.However, we have a more complex help message in which we need to highlight specific constructs: &#8212;switches, @file, and variables like %AhkDir%. We need to improve the algorithm and add the ability to work with entire expressions, rather than just individual characters.Key-ValueIn the algorithm above, we passed a \u201ccharacter-color\u201d array, where the key is a character and the value is a color code:msg.Color([   &#8216;&#187;&#8216;,  &#8216;cyan&#8217;,  &#8216;*&#8217;,  &#8216;green&#8217;])This array was converted to chrColors:&#187; 96* 92As a result, we could call chrColors[&#8216;&#187;&#8216;] or chrColors[&#8216;*&#8217;], where each character served simultaneously as a search pattern (the regular expression, the regex variable), the matched character (the match[1]), and a key in chrColors.Now we\u2019re going to pass \u201cexpression-color\u201d pairs (for simplicity, let\u2019s temporarily forget about characters):msg.Color([  &#8216;(@(file|list\\.ini))&#8217;,    &#8216;yellow&#8217;,   ; list  &#8216;(mainDir|AhkDir)(?=\\=)&#8217;, &#8216;purple&#8217;,   ; variables names  &#8216;(%[^%]+%)&#8217;,              &#8216;blue&#8217;,     ; variables values  &#8216;(\\-+[\\-\\w]+)(?=[ =])&#8217;,   &#8216;cyan&#8217;,     ; switches])With this approach, each expression can no longer serve as both a search pattern and a matched expression (text) at the same time, and therefore cannot be a key in chrColors.Every regular expression we pass to RegExMatch is plain text. On its own, it doesn\u2019t search for anything; instead, it is first compiled into a set of instructions for the processor. In other words, Regular Expressions is something that we can call a low-level language that can be compiled, where every character, such as \\w, is converted into a specific instruction.In our case, this means that an expression like (%[^%]+%) is converted into code that searches for a text like %AhkDir%, %mainDir%, and so on. In other words, the input ((%[^%]+%)) is not equal to the output (%mainDir%), and as a result, information about the original regular expression itself &#8212; which allowed us to apply the desired color to the found text &#8212; is lost. Consequently, the expression itself cannot serve as a key in chrColors to retrieve the color.MarkAs you may have noticed earlier, the match variable is somewhat similar to an array: match[0] stores everything that was found, and match[1] stores what capturing group 1 found. However, it is actually a match\u00a0object, and it stores not only indices but also fields: match.pos, match.len, match.count. It also has a mark field. When RegExMatch() encounters the (*MARK:..) construct in the pattern, the mark field takes on the value following the colon : For example, after (*MARK:chr), the value of match.mark will be &#171;chr&#187;.Let\u2019s look at another expression:(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2)If group 1 is captured, match.mark will be equal to \u201c1\u201d. If group 2 is captured, match.mark will be equal to \u201c2\u201d.We can use this feature to mark (index) the patterns: 1, 2, 3, \u2026 Each pattern will have a unique mark, a unique key that we\u2019ll use to retrieve the color from chrColors:begin  := esc &#8216;[0;&#8217; chrColors[match.mark] &#8216;m&#8217;clrMsg .= begin . match[1]To do this, we need to add mark with the corresponding index to the end of each pattern in the aRegexColor array, and then combine all the patterns into a single regular expression using the OR operator |regex     := &#187; ; final regular expression    chars     := &#187;     ; separate expression with chars    chrColors := Map()        index     := 1; array index    idxColor  := 1  ; mark index        loop (aRegexColor.length \/ 2) {        str   := aRegexColor[index++]        color := aRegexColor[index++]        if (str.length = 1) {            ; Characters are grouped into set like [&#171;*&#187;]            chars .= str            chrColors[str] := colors[color]; char-code        } else {            ; Group patterns into one            regex .= str &#8216;(*MARK:&#8217; idxColor &#8216;)|&#8217;             chrColors[String(idxColor++)] := colors[color]  ; index-code        }    }        if chars        regex .= &#8216;([&#8216; chars &#8216;])(*MARK:chr)&#8217;  ; Add the characters separately at the end    else        regex := regex.RTrim(&#8216;|&#8217;) ; Remove the extra OR |The result will be a single large expression like (..)(*MARK:1)|(..)(*MARK:2)|&#8230;|([..])(*MARK:chr). For convenience, we\u2019ll use the same HashMap that contains \u201cindex-code\u201d pairs and the \u201ccharacter-code\u201d pairs we\u2019re already familiar with:Expressions: the key is match.mark (index).Characters: the key is the character itself, match[1].Since each expression (whether a character or an indexed expression) has a different key, we need to distinguish between indexed expressions and character expressions using (*MARK:chr):        if (match.mark != &#8216;chr&#8217;) {            ; Indexed expression: wrap in color            begin  := esc &#8216;[0;&#8217; chrColors[match.mark] &#8216;m&#8217;            clrMsg .= begin . match[1] . end            continue        }        ; Character expression        if (stack.Has(-1) &amp;&amp; stack[-1] = match[1]) {            clrMsg .= end            stack.Pop()        } else {            begin  := esc &#8216;[0;&#8217; chrColors[match[1]] &#8216;m&#8217;            clrMsg .= begin            stack.Push(match[1])        }Branch ResetThe constructed expression has a critical flaw: it has many capturing groups. As a result, each captured text fragment will be associated with its own capturing group: 1, 2, \u2026Let\u2019s look at this code snippet:&#187;%variable% is ~gray~&#187;.Color([    &#8216;(%[^%]+%)&#8217;, &#8216;blue&#8217;,     &#8216;(~[^~]+~)&#8217;, &#8216;gray&#8217;])Color will construct the expression:(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2)which contains 2 capturing groups:IndexGroupText1(%[^%]+%)%variable%2(~[^~]+~)~gray~Accordingly, the text ~gray~ will be stored in match[2], while match[1] (which we use in the algorithm) will be empty. In other words, we need to know the group\u2019s index to retrieve the text. But if the user has included multiple groups in each pattern, such as =(%([^@](\\w+))%), it will be difficult to determine the index. We want the text or character to always be stored in match[1].To solve this problem, we can use Branch Reset. Branch Reset is a (?|..) construct, which makes the index of each group relative. If we represent the expression ()|()|&#8230; as a list of items separated by |, then the index of each capturing group within each item will be relative to that item:( () )|( () () )1  2   1  2  3 In other words, we don\u2019t look at the entire expression, but rather to its individual items, and then at groups within those items. Without Branch Reset, indexing is absolute:( () )|( () () )1  2   3  4  5 Let\u2019s wrap the constructed expression in (?|..):(?|(%[^%]+%)(*MARK:1)|(~[^~]+~)(*MARK:2))It is important to understand that (?|..) is not a capturing group, but a \u201cBranch Reset\u201d construct. And (*MARK:..) is a \u201cverb\u201d (verb). The parentheses are merely part of the syntax.As a result, we end up with the following indexing:IndexMarkGroupText11(%[^%]+%)%variable%12(~[^~]+~)~gray~match[1] will always contain the captured text from the 1st, 2nd, or 3rd group. At the same&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-486811","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/486811","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=486811"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/486811\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=486811"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=486811"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=486811"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}