Writing a regex in JavaScript to match $ variable prefix syntax (like found in Bash scripts)

Viewed 279

I'm trying to write a regular expression in JavaScript to search a multi-line bash script and extract all the variable names. I can neither seem to figure it out myself nor find a script that does exactly what I need.

I've gotten this far:

var re = /(?:^|\W)\$(\w+)(?!\w)/g;
var s = "test $test1 testagain$test2 testyetagain $test3 $test4$test5";
var matches = [];
while (match = re.exec(s)) {
  matches.push(match[1]);
}

This will give me "test1", "test3", and "test4". But I would also like to get "test2" and "test5".

It would also be great if there were a way I could get "\$" to NOT match if it appears in the string. In other words, is there a way to escape the "$" string in the text so that it is ignored by my regular expression? So in case "\$1.00" appeared in the text, it would not match "1" as the above regex does now.

Thanks in advance for any help or for pointing me in the right direction.

PS This is actually for Action Script 3 but anything that works in JavaScript pre-ES6 should work in AS 3.

PPS The final goal is to replace these matches with variables from a key-value hash.

4 Answers

You could repeatedly match \$ (in which case do nothing) or $\w+ (in which case, it's an unescaped dollar sign indicating the start of a variable you want to match). Use a replacer function to decide whether to leave the match alone, or replace with whatever other string you want:

const replacements = {
  test1: 'testone',
  test2: 'testtwo',
  test3: 'testthree',
  test4: 'testfour',
  test5: 'testfive'
};
var s = "test $test1 testagain$test2 testyetagain $test3 $test4$test5 \\$1.00";
const result = s.replace(
  /\\\$|\$(\w+)/g,
  (match, g1) => match === '\\$'
    ? match // replace with the matched string - in other words, leave it unchanged
    : replacements[g1]
);
console.log(result);

On newer engines, you could negative lookbehind for a backslash instead, which makes the logic a lot easier, but it's not a good cross-browser solution, since it'll only work in new environments:

const replacements = {
  test1: 'testone',
  test2: 'testtwo',
  test3: 'testthree',
  test4: 'testfour',
  test5: 'testfive'
};
var s = "test $test1 testagain$test2 testyetagain $test3 $test4$test5 \\$1.00";
const result = s.replace(
  /(?<!\\)\$(\w+)/g,
  (_, g1) => replacements[g1]
);
console.log(result);

This expression

(?:\\{2,}\$[0-9.]+)|\$([^$\s]+)

collects the undesired data using a non-capturing group

(?:\\{2,}\$[0-9.]+)

and returns the desired data using a capturing group

([^$\s]+)

Demo

const regex = /(?:\\{2,}\$[0-9.]+)|\$([^$\s]+)/g;
const str = `test \$test1 testagain\$test2 testyetagain \$test3 \$test4\$test5 \\\\\$1.00 \\\\\\\\\\\\\$1.00

`;
let m;

while ((m = regex.exec(str)) !== null) {
    // This is necessary to avoid infinite loops with zero-width matches
    if (m.index === regex.lastIndex) {
        regex.lastIndex++;
    }
    
    // The result can be accessed through the `m`-variable.
    m.forEach((match, groupIndex) => {
        console.log(`Found match, group ${groupIndex}: ${match}`);
    });
}

RegEx Circuit

jex.im visualizes regular expressions:

enter image description here

You can use this regex

(?<!\\)\$([^\W$]+)
  • (?<!\\) - Negative lookbehind to avoid \$ to match
  • \$ - Matches $
  • ([^\W$]+) - Match anything except word non word character and $

Demo

Try matching (?:\\\$\w+)|\$(\w+). Since the first group starts matching earlier, it will finish matching first; however as a non-capturing block, won't be returned.

var re = /(?:\\\$\w+)|\$(\w+)/g;
var s = "test $test1 testagain$test2 testyetagain $test3 $test4$test5\\$1.00";
var matches = [];
while (match = re.exec(s)) {
  match[1] && matches.push(match[1]);
}
console.log(matches);

Related