Expressões Regulares

Algumas pessoas, quando confrontadas com um problema, pensam ‘Eu sei, vou usar expressões regulares.’ Agora elas têm dois problemas.

Jamie Zawinski

Quando você corta contra as fibras da madeira, é necessária muita força. Quando você programa contra a natureza do problema, é necessário muito código.

Master Yuan-Ma, The Book of Programming
Ilustração de um sistema ferroviário representando a estrutura sintática de expressões regulares

Ferramentas e técnicas de programação sobrevivem e se espalham de forma caótica e evolutiva. Nem sempre são as melhores ou mais brilhantes que vencem, mas sim aquelas que funcionam bem o suficiente dentro do nicho certo ou que acabam sendo integradas a outra tecnologia bem-sucedida.

Neste capítulo, vou discutir uma dessas ferramentas, expressões regulares. Expressões regulares são uma forma de descrever padrões em dados de string. Elas formam uma pequena linguagem separada que faz parte do JavaScript e de muitas outras linguagens e sistemas.

Expressões regulares são ao mesmo tempo terrivelmente desajeitadas e extremamente úteis. Sua sintaxe é críptica e a interface de programação que o JavaScript fornece para elas é desajeitada. Mas ela é uma poderosa ferramenta para inspecionar e processar strings. Compreender corretamente expressões regulares fará de você um programador mais eficaz.

Criando uma expressão regular

Uma expressão regular é um tipo de objeto. Ela pode ser construída com o construtor RegExp ou escrita como um valor literal envolvendo um padrão entre barras (/).

let re1 = new RegExp("abc");
let re2 = /abc/;

Ambos esses objetos de expressão regular representam o mesmo padrão: um caractere a seguido por um b seguido por um c.

Ao usar o construtor RegExp, o padrão é escrito como uma string normal, então as regras usuais se aplicam às barras invertidas.

A segunda notação, onde o padrão aparece entre barras, trata as barras invertidas de forma um pouco diferente. Primeiro, como uma barra encerra o padrão, precisamos colocar uma barra invertida antes de qualquer barra que quisermos que faça parte do padrão. Além disso, barras invertidas que não fazem parte de códigos de caracteres especiais (como \n) serão preservadas, em vez de ignoradas como em strings, e alteram o significado do padrão. Alguns caracteres, como pontos de interrogação e sinais de mais, têm significados especiais em expressões regulares e devem ser precedidos por uma barra invertida se a intenção for representar o próprio caractere.

let aPlus = /A\+/;

Testando correspondências

Objetos de expressão regular possuem vários métodos. O mais simples é test. Se você passar uma string para ele, ele retornará um Boolean dizendo se a string contém uma correspondência do padrão na expressão.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

Uma expressão regular composta apenas por caracteres não especiais simplesmente representa essa sequência de caracteres. Se abc ocorrer em qualquer lugar na string que estamos testando (não apenas no início), test retornará true.

Conjuntos de caracteres

Descobrir se uma string contém abc poderia igualmente ser feito com uma chamada a indexOf. Expressões regulares são úteis porque nos permitem descrever padrões mais complicados.

Digamos que queremos corresponder a qualquer número. Em uma expressão regular, colocar um conjunto de caracteres entre colchetes faz com que essa parte da expressão corresponda a qualquer um dos caracteres entre os colchetes.

Ambas as expressões a seguir correspondem a todas as strings que contêm um dígito:

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

Dentro de colchetes, um hífen (-) entre dois caracteres pode ser usado para indicar um intervalo de caracteres, onde a ordenação é determinada pelo número Unicode do caractere. Os caracteres de 0 a 9 ficam lado a lado nessa ordenação (códigos 48 a 57), então [0-9] cobre todos eles e corresponde a qualquer dígito.

Vários grupos de caracteres comuns têm seus próprios atalhos embutidos. Dígitos são um deles: \d significa a mesma coisa que [0-9].

\dQualquer caractere de dígito
\wUm caractere alfanumérico (“caractere de palavra”)
\sQualquer caractere de espaço em branco (espaço, tab, quebra de linha e similares)
\DUm caractere que não é um dígito
\WUm caractere não alfanumérico
\SUm caractere que não é espaço em branco
.Qualquer caractere exceto quebra de linha

Você poderia corresponder a um formato de data e hora como 01-30-2003 15:20 com a seguinte expressão:

let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("01-30-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

Essa expressão regular parece completamente horrível, não parece? Metade dela são barras invertidas, produzindo um ruído de fundo que dificulta enxergar o padrão real expresso. Veremos uma versão um pouco melhor dessa expressão mais tarde.

Esses códigos com barra invertida também podem ser usados dentro de colchetes. Por exemplo, [\d.] significa qualquer dígito ou um caractere de ponto. O próprio ponto, entre colchetes, perde seu significado especial. O mesmo vale para outros caracteres especiais, como o sinal de mais (+).

Para inverter um conjunto de caracteres—ou seja, para expressar que você quer corresponder a qualquer caractere exceto os do conjunto—você pode escrever um acento circunflexo (^) após o colchete de abertura.

let nonBinary = /[^01]/;
console.log(nonBinary.test("1100100010100110"));
// → false
console.log(nonBinary.test("0111010112101001"));
// → true

Caracteres internacionais

Por causa da implementação inicial simplista do JavaScript e do fato de que essa abordagem simplista foi posteriormente consolidada como comportamento padrão, as expressões regulares do JavaScript são bastante limitadas em relação a caracteres que não aparecem na língua inglesa. Por exemplo, no que diz respeito às expressões regulares do JavaScript, um “caractere de palavra” é apenas um dos 26 caracteres do alfabeto latino (maiúsculo ou minúsculo), dígitos decimais e, por algum motivo, o caractere de sublinhado. Coisas como é ou β, que definitivamente são caracteres de palavra, não corresponderão a \w (e irão corresponder a \W maiúsculo, a categoria de não palavra).

Por um estranho acidente histórico, \s (espaço em branco) não tem esse problema e corresponde a todos os caracteres que o padrão Unicode considera como espaço em branco, incluindo coisas como o espaço inquebrável e o separador de vogal mongol.

É possível usar \p em uma expressão regular para corresponder a todos os caracteres aos quais o padrão Unicode atribui uma determinada propriedade. Isso nos permite corresponder a coisas como letras de uma forma mais cosmopolita. No entanto, novamente devido à compatibilidade com os padrões originais da linguagem, eles só são reconhecidos quando você coloca um caractere u (para Unicode) após a expressão regular.

\p{L}Qualquer letra
\p{N}Qualquer caractere numérico
\p{P}Qualquer caractere de pontuação
\P{L}Qualquer não letra (P maiúsculo inverte)
\p{Script=Hangul}Qualquer caractere do script fornecido (veja Capítulo 5)

Usar \w para processamento de texto que pode precisar lidar com texto não inglês (ou até mesmo texto em inglês com palavras emprestadas como “cliché”) é problemático, já que ele não tratará caracteres como “é” como letras. Embora tendam a ser um pouco mais verbosos, os grupos de propriedades \p são mais robustos.

console.log(/\p{L}/u.test("α"));
// → true
console.log(/\p{L}/u.test("!"));
// → false
console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false

Por outro lado, se você estiver correspondendo números para fazer algo com eles, frequentemente você vai querer \d para dígitos, já que converter caracteres numéricos arbitrários em um número JavaScript não é algo que uma função como Number pode fazer por você.

Repetindo partes de um padrão

Agora sabemos como corresponder um único dígito. E se quisermos corresponder um número inteiro—uma sequência de um ou mais dígitos?

Quando você coloca um sinal de mais (+) após algo em uma expressão regular, isso indica que o elemento pode ser repetido mais de uma vez. Assim, /\d+/ corresponde a um ou mais caracteres de dígito.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

O asterisco (*) tem um significado semelhante, mas também permite que o padrão corresponda zero vezes. Algo com um asterisco depois nunca impede que um padrão corresponda—ele simplesmente corresponderá a zero ocorrências se não encontrar nenhum texto adequado.

Um ponto de interrogação (?) torna uma parte de um padrão opcional, o que significa que ela pode ocorrer zero ou uma vez. No exemplo a seguir, o caractere u pode aparecer, mas o padrão também corresponde quando ele está ausente:

let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

Para indicar que um padrão deve ocorrer um número preciso de vezes, use chaves. Colocar {4} após um elemento, por exemplo, exige que ele ocorra exatamente quatro vezes. Também é possível especificar um intervalo dessa forma: {2,4} significa que o elemento deve ocorrer pelo menos duas vezes e no máximo quatro vezes.

Aqui está outra versão do padrão de data e hora que permite dias, meses e horas com um ou dois dígitos. Também é um pouco mais fácil de entender.

let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("1-30-2003 8:45"));
// → true

Você também pode especificar intervalos abertos ao usar chaves omitindo o número após a vírgula. Por exemplo, {5,} significa cinco ou mais vezes.

Agrupando subexpressões

Para usar um operador como * ou + em mais de um elemento ao mesmo tempo, você deve usar parênteses. Uma parte de uma expressão regular que está entre parênteses conta como um único elemento no que diz respeito aos operadores que vêm depois dela.

let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

O primeiro e o segundo caracteres + aplicam-se apenas ao segundo o em boo e hoo, respectivamente. O terceiro + aplica-se ao grupo inteiro (hoo+), correspondendo a uma ou mais sequências desse tipo.

O i no final da expressão no exemplo torna essa expressão regular insensível a maiúsculas e minúsculas, permitindo que ela corresponda ao B maiúsculo na string de entrada, mesmo que o padrão esteja todo em minúsculas.

Correspondências e grupos

O método test é a maneira mais simples possível de verificar uma correspondência com uma expressão regular. Ele informa apenas se houve correspondência e nada mais. Expressões regulares também possuem um método exec (execute) que retornará null se nenhuma correspondência for encontrada e retornará um objeto com informações sobre a correspondência caso contrário.

let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

Um objeto retornado por exec possui uma propriedade index que nos diz onde na string a correspondência bem-sucedida começa. Fora isso, o objeto se parece (e na verdade é) um array de strings, cujo primeiro elemento é a string que foi correspondida. No exemplo anterior, esta é a sequência de dígitos que estávamos procurando.

Valores de string possuem um método match que se comporta de forma semelhante.

console.log("one two 100".match(/\d+/));
// → ["100"]

Quando a expressão regular contém subexpressões agrupadas com parênteses, o texto que corresponde a esses grupos também aparecerá no array. A correspondência completa é sempre o primeiro elemento. O próximo elemento é a parte correspondida pelo primeiro grupo (aquele cujo parêntese de abertura aparece primeiro na expressão), depois o segundo grupo, e assim por diante.

let quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

Quando um grupo não chega a ser correspondido (por exemplo, quando é seguido por um ponto de interrogação), sua posição no array de saída conterá undefined. Quando um grupo é correspondido várias vezes (por exemplo, quando é seguido por um +), apenas a última correspondência aparece no array.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

Se você quiser usar parênteses apenas para agrupamento, sem que eles apareçam no array de correspondências, você pode colocar ?: após o parêntese de abertura.

console.log(/(?:na)+/.exec("banana"));
// → ["nana"]

Grupos podem ser úteis para extrair partes de uma string. Se não quisermos apenas verificar se uma string contém uma data, mas também extraí-la e construir um objeto que a represente, podemos colocar parênteses em torno dos padrões de dígitos e selecionar diretamente a data a partir do resultado de exec.

Mas primeiro faremos um breve desvio para discutir a forma integrada de representar valores de data e tempo em JavaScript.

A classe Date

JavaScript tem uma classe padrão Date para representar datas, ou melhor, pontos no tempo. Se você simplesmente criar um objeto de data usando new, você obtém a data e hora atuais.

console.log(new Date());
// → Fri Feb 02 2024 18:03:06 GMT+0100 (CET)

Você também pode criar um objeto para um momento específico.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

JavaScript usa uma convenção onde os números dos meses começam em zero (então dezembro é 11), enquanto os números dos dias começam em um. Isso é confuso e bobo. Tenha cuidado.

Os últimos quatro argumentos (horas, minutos, segundos e milissegundos) são opcionais e assumem zero quando não são fornecidos.

Timestamps são armazenados como o número de milissegundos desde o início de 1970, no fuso horário UTC. Isso segue uma convenção definida pelo “tempo Unix”, que foi inventado por volta dessa época. Você pode usar números negativos para tempos anteriores a 1970. O método getTime em um objeto de data retorna esse número. Ele é grande, como você pode imaginar.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

Se você fornecer um único argumento ao construtor Date, esse argumento é tratado como essa contagem de milissegundos. Você pode obter a contagem atual de milissegundos criando um novo objeto Date e chamando getTime nele ou chamando a função Date.now.

Objetos Date fornecem métodos como getFullYear, getMonth, getDate, getHours, getMinutes e getSeconds para extrair seus componentes. Além de getFullYear também existe getYear, que fornece o ano menos 1900 (como 98 ou 125) e é em grande parte inútil.

Colocando parênteses ao redor das partes da expressão que nos interessam, agora podemos criar um objeto de data a partir de uma string.

function getDate(string) {
  let [_, month, day, year] =
    /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  return new Date(year, month - 1, day);
}
console.log(getDate("1-30-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

A associação de underscore (_) é ignorada e usada apenas para pular o elemento de correspondência completa no array retornado por exec.

Limites e look-ahead

Infelizmente, getDate também extrairá alegremente uma data da string "100-1-30000". Uma correspondência pode acontecer em qualquer lugar da string, então nesse caso ela simplesmente começará no segundo caractere e terminará no penúltimo caractere.

Se quisermos impor que a correspondência cubra toda a string, podemos adicionar os marcadores ^ e $. O acento circunflexo corresponde ao início da string de entrada, enquanto o cifrão corresponde ao final. Assim, /^\d+$/ corresponde a uma string composta inteiramente por um ou mais dígitos, /^!/ corresponde a qualquer string que comece com um ponto de exclamação, e /x^/ não corresponde a nenhuma string (não pode haver um x antes do início da string).

Também existe um marcador \b que corresponde a limites de palavra, posições que têm um caractere de palavra de um lado e um caractere que não é de palavra do outro. Infelizmente, eles usam o mesmo conceito simplista de caracteres de palavra que \w e, portanto, não são muito confiáveis.

Observe que esses marcadores de limite não correspondem a nenhum caractere real. Eles apenas garantem que uma determinada condição seja satisfeita no local onde aparecem no padrão.

Testes de look-ahead (antecipação) fazem algo semelhante. Eles fornecem um padrão e fazem a correspondência falhar se a entrada não corresponder a esse padrão, mas não avançam de fato a posição da correspondência. Eles são escritos entre (?= e ).

console.log(/a(?=e)/.exec("braeburn"));
// → ["a"]
console.log(/a(?! )/.exec("a b"));
// → null

O e no primeiro exemplo é necessário para corresponder, mas não faz parte da string correspondida. A notação (?! ) expressa um look-ahead negativo. Isso corresponde apenas se o padrão entre parênteses não corresponder, fazendo com que o segundo exemplo corresponda apenas a caracteres a que não têm um espaço depois deles.

Padrões de escolha

Digamos que queremos saber se um trecho de texto contém não apenas um número, mas um número seguido por uma das palavras pig, cow ou chicken, ou qualquer uma de suas formas no plural.

Poderíamos escrever três expressões regulares e testá-las em sequência, mas há uma maneira mais elegante. O caractere pipe (|) denota uma escolha entre o padrão à sua esquerda e o padrão à sua direita. Podemos usá-lo em expressões como esta:

let animalCount = /\d+ (pig|cow|chicken)s?/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pugs"));
// → false

Parênteses podem ser usados para limitar a parte do padrão à qual o operador pipe se aplica, e você pode colocar vários desses operadores lado a lado para expressar uma escolha entre mais de duas alternativas.

A mecânica da correspondência

Conceitualmente, quando você usa exec ou test, o mecanismo de expressões regulares procura uma correspondência na sua string tentando casar a expressão primeiro a partir do início da string, depois a partir do segundo caractere, e assim por diante até encontrar uma correspondência ou alcançar o fim da string. Ele retornará a primeira correspondência que puder ser encontrada ou falhará em encontrar qualquer correspondência.

Para fazer a correspondência de fato, o mecanismo trata uma expressão regular como algo semelhante a um diagrama de fluxo. Este é o diagrama para a expressão de animais do exemplo anterior:

Diagrama de trilhos que primeiro passa por uma caixa rotulada 'digit', que tem um loop voltando de depois dela para antes dela, e depois uma caixa para um caractere de espaço. Depois disso, o trilho se divide em três, passando por caixas para 'pig', 'cow' e 'chicken'. Depois disso ele se junta novamente e passa por uma caixa rotulada 's', que, por ser opcional, também tem um trilho que a ignora. Finalmente, a linha alcança o estado de aceitação.

Se conseguirmos encontrar um caminho do lado esquerdo do diagrama até o lado direito, nossa expressão corresponde. Mantemos uma posição atual na string, e cada vez que passamos por uma caixa, verificamos se a parte da string após nossa posição atual corresponde àquela caixa.

Backtracking

A expressão regular /^([01]+b|[\da-f]+h|\d+)$/ corresponde a um número binário seguido de um b, a um número hexadecimal (ou seja, base 16, com as letras a a f representando os dígitos 10 a 15) seguido de um h, ou a um número decimal comum sem caractere de sufixo. Este é o diagrama correspondente:

Diagrama de trilhos para a expressão regular '^([01]+b|\d+|[\da-f]+h)$'

Ao corresponder essa expressão, o ramo superior (binário) frequentemente será seguido mesmo quando a entrada na verdade não contém um número binário. Ao corresponder a string "103", por exemplo, só fica claro no 3 que estamos no ramo errado. A string de fato corresponde à expressão, só não ao ramo em que estamos no momento.

Então o mecanismo faz backtracking. Ao entrar em um ramo, ele lembra sua posição atual (neste caso, no início da string, logo após a primeira caixa de limite no diagrama) para poder voltar e tentar outro ramo se o atual não funcionar. Para a string "103", depois de encontrar o caractere 3, o mecanismo começa a tentar o ramo para números hexadecimais, que falha novamente porque não há um h após o número. Em seguida, ele tenta o ramo de número decimal. Este funciona, e uma correspondência é reportada ao final.

O mecanismo para assim que encontra uma correspondência completa. Isso significa que, se vários ramos puderem potencialmente corresponder a uma string, apenas o primeiro deles (ordenado pela posição em que os ramos aparecem na expressão regular) será usado.

O backtracking também acontece para operadores de repetição como + e *. Se você corresponder /^.*x/ com "abcxe", a parte .* primeiro tentará consumir a string inteira. O mecanismo então perceberá que precisa de um x para corresponder ao padrão. Como não há um x após o fim da string, o operador estrela tenta corresponder um caractere a menos. Mas o mecanismo também não encontra um x após abcx, então ele faz backtracking novamente, fazendo o operador estrela corresponder apenas a abc. Agora ele encontra um x onde precisa e reporta uma correspondência bem-sucedida das posições 0 a 4.

É possível escrever expressões regulares que fazem muito backtracking. Esse problema ocorre quando um padrão pode corresponder a um trecho de entrada de muitas maneiras diferentes. Por exemplo, se nos confundirmos ao escrever uma expressão regular para números binários, podemos acidentalmente escrever algo como /([01]+)+b/.

Diagrama de trilhos para a expressão regular '([01]+)+b'

Se isso tentar corresponder a alguma longa sequência de zeros e uns sem um caractere b no final, o mecanismo primeiro percorre o laço interno até ficar sem dígitos. Então percebe que não há b, então retrocede uma posição, passa pelo laço externo uma vez e desiste novamente, tentando retroceder para fora do laço interno mais uma vez. Ele continuará tentando todas as rotas possíveis através desses dois laços. Isso significa que a quantidade de trabalho dobra a cada caractere adicional. Mesmo com apenas algumas dezenas de caracteres, a correspondência resultante levará praticamente uma eternidade.

O método replace

Valores do tipo String têm um método replace que pode ser usado para substituir parte da string por outra string.

console.log("papa".replace("p", "m"));
// → mapa

O primeiro argumento também pode ser uma expressão regular, caso em que a primeira correspondência da expressão regular é substituída. Quando uma opção g (de global) é adicionada após a expressão regular, todas as correspondências na string serão substituídas, não apenas a primeira.

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

O verdadeiro poder de usar expressões regulares com replace vem do fato de que podemos nos referir a grupos correspondentes na string de substituição. Por exemplo, digamos que temos uma grande string contendo os nomes de pessoas, um nome por linha, no formato Lastname, Firstname. Se quisermos inverter esses nomes e remover a vírgula para obter um formato Firstname Lastname, podemos usar o seguinte código:

console.log(
  "Liskov, Barbara\nMcCarthy, John\nMilner, Robin"
    .replace(/(\p{L}+), (\p{L}+)/gu, "$2 $1"));
// → Barbara Liskov
//   John McCarthy
//   Robin Milner

O $1 e $2 na string de substituição referem-se aos grupos entre parênteses no padrão. $1 é substituído pelo texto que correspondeu ao primeiro grupo, $2 pelo segundo, e assim por diante, até $9. A correspondência completa pode ser referida com $&.

É possível passar uma função—em vez de uma string—como o segundo argumento para replace. Para cada substituição, a função será chamada com os grupos correspondentes (bem como a correspondência completa) como argumentos, e seu valor de retorno será inserido na nova string.

Aqui está um exemplo:

let stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) { // apenas um restante, remover o 's'
    unit = unit.slice(0, unit.length - 1);
  } else if (amount == 0) {
    amount = "no";
  }
  return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\p{L}+)/gu, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

Este código pega uma string, encontra todas as ocorrências de um número seguido por uma palavra alfanumérica e retorna uma string que tem uma unidade a menos de cada quantidade desse tipo.

O grupo (\d+) acaba sendo passado como o argumento amount para a função, e o grupo (\p{L}+) é associado a unit. A função converte amount em número—o que sempre funciona, já que ele correspondeu a \d+ anteriormente—e faz alguns ajustes caso reste apenas um ou zero.

Guloso

Podemos usar replace para escrever uma função que remove todos os comentários de um trecho de código JavaScript. Aqui está uma primeira tentativa:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1  1

A parte antes do operador | corresponde a dois caracteres de barra seguidos por qualquer número de caracteres que não sejam de nova linha. A parte para comentários multilinha é mais complexa. Usamos [^] (qualquer caractere que não está no conjunto vazio de caracteres) como uma forma de corresponder a qualquer caractere. Não podemos simplesmente usar um ponto aqui porque comentários de bloco podem continuar em uma nova linha, e o caractere ponto não corresponde a caracteres de nova linha.

Mas a saída da última linha parece ter dado errado. Por quê?

A parte [^]* da expressão, como descrevi na seção sobre backtracking, primeiro vai corresponder ao máximo que puder. Se isso fizer com que a próxima parte do padrão falhe, o mecanismo de correspondência volta um caractere e tenta novamente a partir daí. No exemplo, o mecanismo primeiro tenta corresponder a todo o restante da string e então começa a voltar. Ele encontrará uma ocorrência de */ depois de voltar quatro caracteres e fará a correspondência ali. Isso não é o que queríamos—a intenção era corresponder a um único comentário, não ir até o final do código e encontrar o fim do último comentário de bloco.

Por causa desse comportamento, dizemos que os operadores de repetição (+, *, ? e {}) são gulosos, o que significa que eles correspondem ao máximo que puderem e depois fazem backtracking. Se você colocar um ponto de interrogação depois deles (+?, *?, ??, {}?), eles se tornam não gulosos e começam correspondendo ao mínimo possível, correspondendo mais apenas quando o restante do padrão não se encaixa na correspondência menor.

E é exatamente isso que queremos neste caso. Fazendo o asterisco corresponder ao menor trecho de caracteres que nos leve a um */, consumimos um comentário de bloco e nada mais.

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

Muitos bugs em programas com expressão regular podem ser rastreados até o uso não intencional de um operador guloso onde um não guloso funcionaria melhor. Ao usar um operador de repetição, prefira a variante não gulosa.

Criando objetos RegExp dinamicamente

Em alguns casos, você pode não saber o padrão exato contra o qual precisa corresponder quando está escrevendo seu código. Digamos que você queira testar o nome do usuário em um trecho de texto. Você pode montar uma string e usar o construtor RegExp nela.

let name = "harry";
let regexp = new RegExp("(^|\\s)" + name + "($|\\s)", "gi");
console.log(regexp.test("Harry is a dodgy character."));
// → true

Ao criar a parte \s da string, precisamos usar duas barras invertidas porque estamos escrevendo em uma string normal, não em uma expressão regular delimitada por barras. O segundo argumento do construtor RegExp contém as opções para a expressão regular—neste caso, "gi" para global e insensível a maiúsculas e minúsculas.

Mas e se o nome for "dea+hl[]rd" porque nosso usuário é um adolescente nerd? Isso resultaria em uma expressão regular sem sentido que não corresponderia de fato ao nome do usuário.

Para contornar isso, podemos adicionar barras invertidas antes de qualquer caractere que tenha um significado especial.

let name = "dea+hl[]rd";
let escaped = name.replace(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("(^|\\s)" + escaped + "($|\\s)",
                        "gi");
let text = "This dea+hl[]rd guy is super annoying.";
console.log(regexp.test(text));
// → true

O método search

Enquanto o método indexOf em strings não pode ser chamado com uma expressão regular, existe outro método, search, que espera uma expressão regular. Assim como indexOf, ele retorna o primeiro índice em que a expressão foi encontrada, ou -1 quando não foi encontrada.

console.log("  word".search(/\S/));
// → 2
console.log("    ".search(/\S/));
// → -1

Infelizmente, não há uma maneira de indicar que a correspondência deve começar em um deslocamento específico (como podemos com o segundo argumento de indexOf), o que frequentemente seria útil.

A propriedade lastIndex

O método exec de forma semelhante não fornece uma maneira conveniente de iniciar a busca a partir de uma posição específica na string. Mas ele fornece uma maneira inconveniente.

Objetos de expressão regular possuem propriedades. Uma dessas propriedades é source, que contém a string a partir da qual a expressão foi criada. Outra propriedade é lastIndex, que controla, em algumas circunstâncias limitadas, onde a próxima correspondência começará.

Essas circunstâncias são que a expressão regular deve ter a opção global (g) ou sticky (y) habilitada, e a correspondência deve acontecer através do método exec. Novamente, uma solução menos confusa teria sido simplesmente permitir um argumento extra para ser passado ao exec, mas a confusão é uma característica essencial da interface de expressões regulares do JavaScript.

let pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

Se a correspondência for bem-sucedida, a chamada a exec atualiza automaticamente a propriedade lastIndex para apontar para depois da correspondência. Se nenhuma correspondência for encontrada, lastIndex é redefinido para 0, que também é o valor que ela tem em um objeto de expressão regular recém-construído.

A diferença entre as opções global e sticky é que, quando sticky está habilitada, a correspondência só terá sucesso se começar diretamente em lastIndex, enquanto com global, ela irá procurar adiante por uma posição onde uma correspondência possa começar.

let global = /abc/g;
console.log(global.exec("xyz abc"));
// → ["abc"]
let sticky = /abc/y;
console.log(sticky.exec("xyz abc"));
// → null

Ao usar um valor de expressão regular compartilhado para múltiplas chamadas de exec, essas atualizações automáticas na propriedade lastIndex podem causar problemas. Sua expressão regular pode estar começando acidentalmente em um índice deixado por uma chamada anterior.

let digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

Outro efeito interessante da opção global é que ela altera a forma como o método match em strings funciona. Quando chamado com uma expressão global, em vez de retornar um array semelhante ao retornado por exec, match irá encontrar todas as correspondências do padrão na string e retornar um array contendo as strings correspondentes.

console.log("Banana".match(/an/g));
// → ["an", "an"]

Portanto, tenha cuidado com expressões regulares globais. Os casos em que elas são necessárias—chamadas a replace e situações em que você quer usar explicitamente lastIndex—geralmente são exatamente os casos em que você vai querer usá-las.

Uma coisa comum a fazer é encontrar todas as correspondências de uma expressão regular em uma string. Podemos fazer isso usando o método matchAll.

let input = "A string with 3 numbers in it... 42 and 88.";
let matches = input.matchAll(/\d+/g);
for (let match of matches) {
  console.log("Found", match[0], "at", match.index);
}
// → Found 3 at 14
//   Found 42 at 33
//   Found 88 at 40

Este método retorna um array de arrays de correspondência. A expressão regular fornecida para matchAll deve ter g habilitado.

Fazendo parsing de um arquivo INI

Para concluir o capítulo, vamos analisar um problema que exige expressões regulares. Imagine que estamos escrevendo um programa para coletar automaticamente informações sobre nossos inimigos da internet. (Na verdade não vamos escrever esse programa aqui, apenas a parte que lê o arquivo de configuração. Desculpe.) O arquivo de configuração se parece com isto:

searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7

; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451

[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn

As regras exatas para esse formato—que é um formato de arquivo amplamente utilizado, geralmente chamado de arquivo INI—são as seguintes:

Nossa tarefa é converter uma string como essa em um objeto cujas propriedades contenham strings para configurações escritas antes do primeiro cabeçalho de seção e subobjetos para seções, com esses subobjetos contendo as configurações da seção.

Como o formato precisa ser processado linha por linha, dividir o arquivo em linhas separadas é um bom começo. Vimos o método split no Capítulo 4. Alguns sistemas operacionais, no entanto, usam não apenas um caractere de nova linha para separar linhas, mas um caractere de retorno de carro seguido por uma nova linha ("\r\n"). Considerando que o método split também permite uma expressão regular como argumento, podemos usar uma expressão regular como /\r?\n/ para dividir de uma forma que aceite tanto "\n" quanto "\r\n" entre linhas.

function parseINI(string) {
  // Start with an object to hold the top-level fields
  let result = {};
  let section = result;
  for (let line of string.split(/\r?\n/)) {
    let match;
    if (match = line.match(/^(\w+)=(.*)$/)) {
      section[match[1]] = match[2];
    } else if (match = line.match(/^\[(.*)\]$/)) {
      section = result[match[1]] = {};
    } else if (!/^\s*(;|$)/.test(line)) {
      throw new Error("Line '" + line + "' is not valid.");
    }
  };
  return result;
}

console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}

O código percorre as linhas do arquivo e constrói um objeto. Propriedades no topo são armazenadas diretamente nesse objeto, enquanto propriedades encontradas em seções são armazenadas em um objeto de seção separado. A variável section aponta para o objeto da seção atual.

Existem dois tipos de linhas significativas—cabeçalhos de seção ou linhas de propriedade. Quando uma linha é uma propriedade comum, ela é armazenada na seção atual. Quando é um cabeçalho de seção, um novo objeto de seção é criado, e section passa a apontar para ele.

Observe o uso recorrente de ^ e $ para garantir que a expressão corresponda à linha inteira, não apenas a parte dela. Deixar esses elementos de fora resulta em código que na maior parte funciona, mas se comporta de forma estranha para algumas entradas, o que pode ser um bug difícil de rastrear.

O padrão if (match = string.match(...)) faz uso do fato de que o valor de uma expressão de atribuição (=) é o valor atribuído. Muitas vezes você não tem certeza de que sua chamada a match terá sucesso, então só pode acessar o objeto resultante dentro de uma instrução if que testa isso. Para não quebrar a cadeia agradável de formas else if, atribuímos o resultado da correspondência a uma variável e imediatamente usamos essa atribuição como o teste da instrução if.

Se uma linha não for um cabeçalho de seção nem uma propriedade, a função verifica se ela é um comentário ou uma linha vazia usando a expressão /^\s*(;|$)/ para corresponder linhas que contêm apenas espaços em branco, ou espaços em branco seguidos de um ponto e vírgula (tornando o restante da linha um comentário). Quando uma linha não corresponde a nenhuma das formas esperadas, a função lança uma exceção.

Unidades de código e caracteres

Outro erro de design que foi padronizado nas expressões regulares do JavaScript é que, por padrão, operadores como . ou ? funcionam sobre unidades de código (como discutido no Capítulo 5), não caracteres reais. Isso significa que caracteres compostos por duas unidades de código se comportam de forma estranha.

console.log(/🍎{3}/.test("🍎🍎🍎"));
// → false
console.log(/<.>/.test("<🌹>"));
// → false
console.log(/<.>/u.test("<🌹>"));
// → true

O problema é que o 🍎 na primeira linha é tratado como duas unidades de código, e {3} é aplicado apenas à segunda unidade. Da mesma forma, o ponto corresponde a uma única unidade de código, não às duas que formam o emoji de rosa.

Você deve adicionar a opção u (Unicode) à sua expressão regular para que ela trate esses caracteres corretamente.

console.log(/🍎{3}/u.test("🍎🍎🍎"));
// → true

Resumo

Expressões regulares são objetos que representam padrões em strings. Elas usam sua própria linguagem para expressar esses padrões.

/abc/Uma sequência de caracteres
/[abc]/Qualquer caractere de um conjunto de caracteres
/[^abc]/Qualquer caractere não presente em um conjunto de caracteres
/[0-9]/Qualquer caractere em um intervalo de caracteres
/x+/Uma ou mais ocorrências do padrão x
/x+?/Uma ou mais ocorrências, não guloso
/x*/Zero ou mais ocorrências
/x?/Zero ou uma ocorrência
/x{2,4}/De duas a quatro ocorrências
/(abc)/Um grupo
/a|b|c/Qualquer um entre vários padrões
/\d/Qualquer dígito
/\w/Um caractere alfanumérico (“caractere de palavra”)
/\s/Qualquer caractere de espaço em branco
/./Qualquer caractere exceto quebras de linha
/\p{L}/uQualquer letra
/^/Início da entrada
/$/Fim da entrada
/(?=a)/Um teste de antecipação (look-ahead)

Uma expressão regular possui um método test para verificar se uma determinada string corresponde a ela. Ela também possui um método exec que, quando uma correspondência é encontrada, retorna um array contendo todos os grupos correspondentes. Esse array possui uma propriedade index que indica onde a correspondência começou.

Strings possuem um método match para compará-las com uma expressão regular e um método search para procurar por uma, retornando apenas a posição inicial da correspondência. O método replace delas pode substituir correspondências de um padrão por uma string ou função de substituição. Expressões regulares podem ter opções, que são escritas após a barra de fechamento. A opção i torna a correspondência insensível a maiúsculas e minúsculas. A opção g torna a expressão global, o que, entre outras coisas, faz com que o método replace substitua todas as ocorrências em vez de apenas a primeira. A opção y torna a expressão aderente, o que significa que ela não irá procurar adiante e pular parte da string ao buscar uma correspondência. A opção u ativa o modo Unicode, que habilita a sintaxe \p e corrige vários problemas relacionados ao tratamento de caracteres que ocupam duas unidades de código.

Expressões regulares são uma ferramenta poderosa com um cabo estranho. Elas simplificam enormemente algumas tarefas, mas podem rapidamente se tornar incontroláveis quando aplicadas a problemas complexos. Parte de saber usá-las é resistir à tentação de forçar coisas nelas que elas não conseguem expressar de forma clara.

Exercícios

É quase inevitável que, no decorrer do trabalho nestes exercícios, você fique confuso e frustrado com algum comportamento inexplicável de uma expressão regular. Às vezes ajuda inserir sua expressão em uma ferramenta online como debuggex.com para ver se a visualização corresponde ao que você pretendia e para experimentar a forma como ela responde a várias strings de entrada.

Golfe de regexp

Code golf é um termo usado para o jogo de tentar expressar um programa específico com o menor número possível de caracteres. Da mesma forma, regexp golf é a prática de escrever a menor expressão regular possível para corresponder a um padrão dado e apenas a esse padrão.

Para cada um dos itens a seguir, escreva uma expressão regular para testar se o padrão fornecido ocorre em uma string. A expressão regular deve corresponder apenas a strings que contenham o padrão. Quando sua expressão funcionar, veja se você consegue deixá-la ainda menor.

  1. car e cat

  2. pop e prop

  3. ferret, ferry e ferrari

  4. Qualquer palavra terminando em ious

  5. Um caractere de espaço em branco seguido por um ponto, vírgula, dois-pontos ou ponto e vírgula

  6. Uma palavra com mais de seis letras

  7. Uma palavra sem a letra e (ou E)

Consulte a tabela no resumo do capítulo para ajuda. Teste cada solução com algumas strings de teste.

// Fill in the regular expressions

verify(/.../,
       ["my car", "bad cats"],
       ["camper", "high art"]);

verify(/.../,
       ["pop culture", "mad props"],
       ["plop", "prrrop"]);

verify(/.../,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);

verify(/.../,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);

verify(/.../,
       ["bad punctuation ."],
       ["escape the period"]);

verify(/.../,
       ["Siebentausenddreihundertzweiundzwanzig"],
       ["no", "three small words"]);

verify(/.../,
       ["red platypus", "wobbling nest"],
       ["earth bed", "bedrøvet abe", "BEET"]);


function verify(regexp, yes, no) {
  // Ignore unfinished exercises
  if (regexp.source == "...") return;
  for (let str of yes) if (!regexp.test(str)) {
    console.log(`Failure to match '${str}'`);
  }
  for (let str of no) if (regexp.test(str)) {
    console.log(`Unexpected match for '${str}'`);
  }
}

Estilo de citação

Imagine que você escreveu uma história e usou aspas simples ao longo de todo o texto para marcar trechos de diálogo. Agora você quer substituir todas as aspas de diálogo por aspas duplas, mantendo as aspas simples usadas em contrações como aren’t.

Pense em um padrão que diferencie esses dois tipos de uso de aspas e construa uma chamada ao método replace que faça a substituição correta.

let text = "'I'm the cook,' he said, 'it's my job.'";
// Change this call.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."
Mostrar dicas...

A solução mais óbvia é substituir apenas aspas com um caractere não alfabético em pelo menos um dos lados—algo como /\P{L}'|'\P{L}/u. Mas você também precisa levar em conta o início e o fim da linha.

Além disso, você deve garantir que a substituição também inclua os caracteres que foram correspondidos pelo padrão \P{L}, para que eles não sejam removidos. Isso pode ser feito envolvendo-os em parênteses e incluindo seus grupos na string de substituição ($1, $2). Grupos que não forem correspondidos serão substituídos por nada.

Números novamente

Escreva uma expressão que corresponda apenas a números no estilo JavaScript. Ela deve suportar um sinal de menos ou mais opcional na frente do número, o ponto decimal e a notação de expoente—5e-3 ou 1E10—novamente com um sinal opcional na frente do expoente. Observe também que não é necessário haver dígitos antes ou depois do ponto, mas o número não pode ser apenas um ponto. Ou seja, .5 e 5. são números JavaScript válidos, mas um ponto isolado não é.

// Fill in this regular expression.
let number = /^...$/;

// Tests:
for (let str of ["1", "-1", "+15", "1.55", ".5", "5.",
                 "1.3e2", "1E-4", "1e+12"]) {
  if (!number.test(str)) {
    console.log(`Failed to match '${str}'`);
  }
}
for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5",
                 ".5.", "1f5", "."]) {
  if (number.test(str)) {
    console.log(`Incorrectly accepted '${str}'`);
  }
}
Mostrar dicas...

Primeiro, não se esqueça da barra invertida na frente do ponto.

Fazer a correspondência do sinal opcional na frente do número, assim como na frente do expoente, pode ser feito com [+\-]? ou (\+|-|) (mais, menos ou nada).

A parte mais complicada do exercício é o problema de corresponder tanto "5." quanto ".5" sem também corresponder ".". Para isso, uma boa solução é usar o operador | para separar os dois casos—ou um ou mais dígitos opcionalmente seguidos por um ponto e zero ou mais dígitos ou um ponto seguido por um ou mais dígitos.

Por fim, para tornar o e insensível a maiúsculas e minúsculas, adicione uma opção i à expressão regular ou use [eE].