expr: O comando esquecido

Introdução

Nesse pequeno post irei mostrar alguns usos do comando expr. Esse comando era muito utilizado, pois oferecia de maneira fácil operações aritméticas e expressões regulares. Com o passar do tempo, várias de suas funções foram embutidas nos shells e isso ajudou para tornar esse programa um pouco obsoleto. Assim, seu uso foi se tornando muito restrito e raro, sendo visto praticamente em códigos antigos (ou códigos escritos por pessoas antigas ;D)

Nesse post mostrarei detalhadamente as opções do expr e no final alguns exercícios de possívels hacking com esse comando.

$ man expr

Antes de tudo, aconselho que você dê uma lida na man page desse comando. A man page está de fácil leitura e muito objetiva. Recomendo.

Sintaxe

A sintaxe básica desse comando é:

$ expr ARG1 OPERAÇÃO ARG2

Veja que temos 3 parâmetros para montar uma operação, isso é, todas as operações no expr devem ser passadas como parâmetro isolados e não como um só parâmetro. Veja:

$ expr '2 + 7' # ERRADO
...
$ expr 2 + 7 # CORRETO: Operação e operandos ocupando um parâmetro

Se a operação for de multiplicação use o símbolo ‘*’ ou \* para evitar possíveis interpretações errôneas de seu shell. ARG1 e ARG2 devem ser números inteiros, pois esse programa não trabalha com números em ponto flutuante. Que pena!

Mais exemplos:

$ expr 10 + 5
15
$ expr 10 '*' 333
3330
$ expr 66 '*' -99
-9653
$ expr 10 / 5
2
$ expr 11 / 5
2

Veja que no último comando o valor correto de 11/5 seria 2.2, mas como o expr só realiza divisão inteira, o resultado mostrado foi 2. Sabendo disso, como regra geral, se ARG1 < ARG2 e a operação for divisão, então o resultado será sempre zero.

Operações Lógicas

O expr também implementa operações lógicas. Essas operações retornam somente verdadeiro (1) ou falso (0) no terminal. Quando o expr avalia expressões lógicas ele também retorna para o shell um valor indicando sucesso ou não. Esse valor é acessível através da variável $? e seu valor é o inverso do que é retornado no terminal. Veja:

$ expr 10 '>' 6
1
$ echo $?
0

O valor de sucesso ‘1’ no primeiro comando é para fornecer compatibilidade lógica com linguagens de programação (ex: C) e o valor ‘0’ é o mais utilizado em scripts, já que comandos como if analisam o valor da variável $?.

Exemplos:

$ expr 5 '>=' 5
1
$ expr 1 '<=' -9
0
$ expr '' = ''
1

Lembrando aqui também o uso das aspas onde aparece ‘<‘ e ‘>’ para evitar as interpretações do shell.

Para as operações de comparação ARG1 e ARG2 podem ser strings. Se esse for o caso, então a comparação é feita no código ASCII de cada caractere.

$ expr abcd '>' aXcd
1

Retornou verdadeiro pois b > X, ou melhor 98 > 88. Veja que a comparação ocorre no primeiro caractere que diferencia ARG1 e ARG2.

Uma coisa que achei interessante no expr foi as operações de AND e OR. Elas ao invés de retornarem 0 ou 1 no terminal retornam o valor de ARG1 ou ARG2 (mas em $? continua retornando 0 ou 1):

$ expr 5 '&' 6
5
$ expr 0 '&' 6
0
$ expr 6 '|' 10
6
$ expr 0 '|' "sofoda"
sofoda
$ expr '' '|' -9
-9

Escape sempre essas duas operações, pois & e | são caracteres reservados em praticamente todos shells existentes.

Os resultados acima são obtidos assim:

  1. Para o AND o retorno é ARG1 se os argumentos são diferentes de zero. Se um deles for zero, então zero e’ retornado.
  2. Para o OR o retorno é ARG1 se ele NÃO for nulo ou ARG2 caso contrario.

Esses resultados são assim por respeitarem a tabela verdade de tais operações e também pela ordem de avaliação da expressão. Por exemplo, veja o AND, se ARG1 é zero, então nem preciso saber quem é ARG2 para retornar zero. Já no caso do OR, se ARG1 é zero então retorno ARG2, pois sendo ele 0 ou não, vai ser ele quem decidirá o resultado final.

Tamanho de uma string

Tá em dúvida sobre o tamanho de uma string? Use o operador length:

$ expr length "123456"
6

Expressões Regulares

Em algumas ocasiões é vantajoso utilizar expressões regulares no expr ao invés de comandos do sistema, como grep e sed. O expr pode ser útil em situações como essas:

if $(echo $VAR | grep -q 'EXPR') ...

Acima executamos um comando echo e sua saída vai para um outro comando, o grep. Em suma, podemos utilizar um só comando expr para substituir os dois comandos anteriores:

if expr $VAR : EXPR 2> /dev/null

Mais rápido ainda seria: (no bash)

if [[ $VAR =~ EXPR ]]

A sintaxe para usar expressões regulares no expr é:

$ expr STRING : EXPR
ou
$ expr match STRING EXPR

em que EXPR tem que ser dada no padrão antigo de regexp. Isso é devemos escapar tudo que é parênteses, +, ? e {, }. Para quem já usa o sed a tempos está acostumado com essa sintaxe.

O valor retornado no terminal na avaliação de uma expressão regular é o número de caracteres casados pela regex ou a backref \1, se ela estiver definida.

$ expr 123X123 : '123'
3

Como três caracteres foram casados em 123 então o valor 3 foi retornado. Note que a expressão regular sempre tenta casar o primeiro caractere da string e retorna a quantidade de caracteres casados até onde ela pode ir. Se o casamento da expressão foi total, então o tamanho da string é retornado. Se a regex não casar no primeiro caractere da string, zero já é retornado de cara. Sem mais.

Como dito, se você utilizar parênteses no sentido de backref (ou retrovisores como alguns preferem dizer), somente o conteúdo do primeiro retrovisor (o \1) será retornado.

$ expr 'aaab' : '\(aa\)'
aa
$ expr 'aaab' : '\(a*\)'
aaa
$ expr 'aaab' : '\(c*\)'
''

As aspas acima são apenas para representar a string nula. Note que a regex casou com zeros c, por isso que \1 é igual a nulo. Continuando..

$ expr 'aaab' : '\(\(aaa\)b\)'
aaab

Aqui \1=aaab e \2=aaa, mas como expr só retorna o valor da backref \1 então aaab é retornado.

$ expr 'daemonio@darkstar' : '\(.*\)@'
daemonio
$ expr match 'root:x:0:0::/root:/bin/zsh' '\([^:]*:\?\)\{7\}'
/bin/zsh

O último exemplo nos mostra que ? e {} devem ser escapados, assim como o + e os parênteses. Um modo de contornar a limitação do expr em mostrar apenas o conteúdo da backref \1 é deslocar essa backref para onde você quer. Esse deslocamento foi usado no último exemplo e foi feito assim: Case tudo antes do : e o próprio :, se ele existir. Faça o passo anterior 7 vezes para garantir que a backref \1 estará no sétimo casamento da regexp, isso é, na string /bin/zsh.

Substrings

Para obter uma substring usando o expr basta você dizer qual a string de entrada, o início de onde você quer pegar e o tamanho do pedaço. Tanto o início quanto o tamanho devem ser valores positivos.

$ expr substr '12345' 3 2
34

O expr começa a contar do 1 e pára no terceiro caractere que é o 3. Como você indicou o tamanho de saída como 2, então expr irá ler mais um caractere, pois o 3 já foi lido. Logo, o 2 é lido e 34 é retornado.

Outro exemplo:

$ expr substr substr 1 3
expr: syntax error

Assim não há como obter uma substring da string “substr”, pois o expr entende nossa string como comando interno. Resolve-se isso adicionando um ‘+’ como parâmetro antes da string:

$ expr substr + substr 1 2
su

Indexando caracteres

Essa opção é meio confusa. Ela teoricamente mostra a posição de determinados caracteres dentro de uma string:

$ expr index daemonio o
5

Até ai tudo bem. Mas se você colocar mais caracteres como parâmetro, então a menor posição dos caracteres do parâmetro é retornada. Isso é: Descobre-se quais caracteres do terceiro parâmetro estão na string e o menor valor de posição é retornado.

$ expr index daemonio onimoeadxxxxxxxxxxx
1

O caractere d contém a menor posição (1) por isso que 1 é retornado.

Expressão

Tudo que vimos até agora é uma expressão. Várias expressões podem ser agrupadas por meio de parênteses para formar expressões maiores que serão avaliadas pelo expr. Se há várias expressões dentro de um mesmo parênteses, então uma ordem de execução deve ser estabelecida. Essa ordem é sempre do comando mais interno para o mais externo. As operações matemáticas possuem menor precedência, por isso tendem a ser executadas por último.

$ expr length match 123AAA '[^A]*\(A*\)' '>' 3
3

Primeiro se executa o match, depois o length e depois o > 3.

Você pode usar parênteses para mudar as precedência das operações. Use parênteses sempre que possível, pois assim você garante quem será executado e quando. Falando em parênteses, eles também devem ser um parâmetro isolado.

$ expr substr 123xyzAAA \( length match 123xyzAAA '[^A]*\(.*\)' + 1 \) 3
xyz

O que será retornado do parênteses será a quantidade de A’s mais 1, cujo resultado é 4. Esse 4 será usado como índice para substr. Contando 3 caracteres à partir do quarto, obtém-se xyz.

Mais exemplos

1) Verificar o tamanho do username em: daemonio@slackware1337

$ expr length match daemonio@slackware1337 '\(.*\)@'
8
ou
$ expr match daemonio@slackware1337 '[^@]*'

O match extrai o username e manda para length retornar o tamanho.

2) Verificar se o username tem tamanho maior que 10

$ expr length match daemonio@slackware1337 '\(.*\)@' '>' 10
0

3) Somar +55 a parte numérica de um string

$ expr match daemonio@slackware1337 '[^0-9]*\([0-9]*\)' + 55
1392

Através desses exemplos vemos uma grande característica do expr que é a de misturar com facilidade a manipulação de strings com operações aritméticas.

Bem, acho que é isso. Como estou meio professor hoje deixei alguns exercícios para serem feitos pelo leitor. As respostas estão logo mais abaixo.

Questões

1) Questão 1

Essa questão apresenta como você deve pensar para resolver as questões seguintes. O segredo do expr está nos operadores \& e \|. A ordem de como o expr analisa cada uma desses operadores é o que permite realizar um certo de if. O operador \& retorna ARG1 se e somente se ARG1 e ARG2 são diferentes de zero. Com isso vemos que ARG1 depende de ARG2. Se você garantir que ARG1 é verdadeiro então o resultado irá depender somente de ARG2. Tipo assim:

$ expr "Tamanho maior que 3" \& length "string" > 3

O valor de ARG1 é “Tamanho maior que 3” que é verdadeiro. Essa string só será retornada se o que tiver depois do \& for verdadeiro. Se o tamanho da string for realmente maior que três então ARG1 é retornado na tela. É usando esse tipo de lógica que você deve construir seus scripts.

Veja um exemplo abaixo que mostra o primeiro campo da string delimitada por ‘:’ se e somente se o número no segundo campo é maior que 18:

$ VAR=daemonio:22
$ expr \( match $VAR '\(.*\):' \) \& \( match $VAR '[^0-9]*\([0-9]*\)' '>' 18 \)
daemonio

Esse esquema serve para mostrar os nomes de pessoas que tem idade maior que 18. Note que se a idade da pessoa for menor que 18 o \& retorna 0 e esse resultado vai para a tela. Nos exercícios seguintes isso não é importante, mas em scripts é necessário filtrar a saída com outro comando (ex: sed, grep) ou testar o valor de retorno em $?.

2) Questão 2

Sendo o padrão nome:idade visto acima, mostre o nome das pessoas que tem idade maior que 18 de um determinado arquivo.

$ cat nomes1.txt
maria:19
joana:15
kevin:36
tupac:20
bebe:2
ninfa:17

Dica: Use o comando xargs -i para montar os parâmetros para expr.

$ cat nomes.txt | xargs -i expr \( match \{\} ......... \)

O \{\} guarda o valor da linha.

3) Questão 3

Você é um professor e precisa calcular a média das notas de seus alunos. A única ferramenta que você tem a sua frente para realizar os cálculos é o expr.

A partir da lista abaixo, exiba o nome dos alunos que obtiveram uma media maior que 20. Veja que cada coluna após a primeira, corresponde a uma nota. A média deve ser calculada como a soma dessas notas dividida por 3.

$ cat nomes2.txt
maria:10:25:32
demetrius:15:20:4
john:25:21:22
ana:15:18:31
karina:16:17:24
daemonio:33:33:33
romulo:14:5:6

4) Questão 4

Veja o arquivo de convidados para uma festa:

$ cat nomes3.txt
joao:m:35
rene:m:15
troll:m:19*
gatinha:f:25:*
budchen:f:29:*
maria:f:30
joana:f:36
milf:f:40
sylviasaint:f:20:***
feinha:f:24
irmadafeia:f:27
primadafeia:f:26
patrick:m:17

A regra para entrar na festa é ser maior que 30 anos. Mas como a festa iria ficar meio sem graça, decidiram convidar as mulheres mais novas, mas somente aquelas que tem estrelinha no fim da linha (* = hooottt ;) ).

Sua tarefa é mostrar o nome das pessoas que irão entrar na festa. A linha das pessoas que não irão entrar pode ficar como 0 ou 1 (aí depende de sua lógica).

Dica: O comando expr ficará grande e poderá tomar varias linhas de seu terminal. Como dica, crie a linha de comando em um arquivo e o execute em separado.

5) Questão 5

Um cara muito pegador julga as mulheres pelo seu nível de beleza. Um nível de beleza é caracterizado pela quantidade de estrelinhas. Seu trabalho é mostrar na tela se determinada mulher é “PT”, “PF”, “PR”, “PV”.

O tipo de mulher foi dado pelo mesmo autor do arquivo. Ele descreveu o seguinte:

+-------------+-----------------------------------+
|   Nível     |           Denominação             |
+-------------+-----------------------------------+
|Entre 1 e 2  |          PT (Pego Tonto)          |
+-------------+-----------------------------------+
|Entre 3 e 5  | PF (How easy, como Pego Fácil)    |
+-------------+-----------------------------------+
|Entre 6 e 8  |          PR (Pego Rindo)          |
+-------------+-----------------------------------+
|     9       |  PV (Pego Virando saltos mortais) |
+-------------+-----------------------------------+

O arquivo de “níveis” é listado a seguir:

$ cat nomes4.txt
kristaallen:f:*********
troll1:m:*****
joaquina:f:**
marieta:f:*
fogosa:f:****
patricia:f:*******
meganfox:f:*********
troll2:m:*********
julia:f:*****
roselfa:f:**

Para te ajudar, a saída deve ser algo assim:

PV
1
PT
PT
PF
PR
PV
1
PF
PT

Opcional: Para relacionar a saída acima com os nomes, utilize o cut+paste, pois com o expr não é possível “salvar” um pedaço de linha para depois utilizar.

$ paste <(cut -f1 -d: lista_nomes) <(expr .........)
kristaallen PV
troll1 1
joaquina PT
marieta PT
fogosa PF
patricia PR
meganfox PV
troll2 1
julia PF
roselfa PT

Conclusão

O comando expr tem lá seus atrativos, mas infelizmente todas suas operações já são built-in em muitos shells. Porém, ele tem a vantagem de juntar manipulação de texto com expressões matemáticas. Usando o expr é fácil dizer “some tanto naquilo que case com essa expressão regular”. Sua simplicidade em termos de operações permite que problemas simples sejam transformados em desafios para entreter aqueles que não conseguem dormir de noite. ;D

Galera, fui!

t+

Respostas

Questão 2

$ cat nomes1.txt
maria:19
joana:15
kevin:36
tupac:20
bebe:2
ninfa:17
$ cat nomes1.txt | xargs -i \
expr \( match \{\} '\(.*\):' \) '&' \( match \{\} '[^0-9]*\([0-9]*\)' '>' 18 \)

Questão 3

$ cat nomes2.txt
maria:10:25:32
demetrius:15:20:4
john:25:21:22
ana:15:18:31
karina:16:17:24
daemonio:33:33:33
romulo:14:5:6
$ cat exprf2
expr \( match $A '\([^:]*\)' \) \& \
\( \
\( \( match $A '[^:]*:\([0-9]*\)' \) + \
\( match $A '[^:]*:[^:]*:\([^:]*\)' \) + \
\( match $A '.*:\(.*\)' \) \
\) / 3 '>' 20 \
\)
$ cat nomes2.txt | while read A; do export A; source exprf2 ; done

Questão 4

$ cat nomes3.txt
joao:m:35
rene:m:15
troll:m:19*
gatinha:f:25:*
budchen:f:29:*
maria:f:30
joana:f:36
milf:f:40
sylviasaint:f:20:***
feinha:f:24
irmadafeia:f:27
primadafeia:f:26
patrick:m:17
$ cat exprf3
expr \( match $A '\([^:]*\)' \) \& \
\(\
\( match $A '[^:]*:.:\([^:]*\)' '>' 30 \) \| \
\( \
\( match $A '[^:]*:\(.\)' = f \) \& \
\( length match $A '\([^:]*:\?\)\{4\}' '>' 0 \) \
\)\
\)

Questão 5

$ cat nomes4.txt
kristaallen:f:*********
troll1:m:*****
joaquina:f:**
marieta:f:*
fogosa:f:****
patricia:f:*******
meganfox:f:*********
troll2:m:*********
julia:f:*****
roselfa:f:**
$ cat exprf4
expr \( match $A '[^:]*:\(.\)' != f \) \| \( \
\( "PV" \& \( length match $A '.*:\(.*\)' = 9 \) \) \| \
\( "PR" \& \( length match $A '.*:\(.*\)' = 8 \) \) \| \
\( "PR" \& \( length match $A '.*:\(.*\)' = 7 \) \) \| \
\( "PR" \& \( length match $A '.*:\(.*\)' = 6 \) \) \| \
\( "PF" \& \( length match $A '.*:\(.*\)' = 5 \) \) \| \
\( "PF" \& \( length match $A '.*:\(.*\)' = 4 \) \) \| \
\( "PF" \& \( length match $A '.*:\(.*\)' = 3 \) \) \| \
\( "PT" \& \( length match $A '.*:\(.*\)' = 2 \) \) \| \
\( "PT" \& \( length match $A '.*:\(.*\)' = 1 \) \) \
\)

Um pensamento sobre “expr: O comando esquecido

Deixe um comentário