No bash: captura de espaço à direita na substituição de variável

No bash: captura de espaço à direita na substituição de variável

Vejo isso no BASH 4.3.48 (SLES12 SP4) e no BASH 4.4.23 (OpenSUSE Leap 15.1) ao tentar remover vários espaços à direita do valor de uma variável:

~> xxx="-O -Wall  "
~> echo "X${xxx%% }X"    # (1)
X-O -Wall X
~> echo "X${xxx%% *}X"
X-OX
~> echo "X${xxx% }X"
X-O -Wall X
~> echo "X${xxx% *}X"    # (2)
X-O -Wall X
~> echo "X${xxx%% \*}X"
X-O -Wall  X

Eu sinto que (1)ou (2)deveria fazer o trabalho.

O manual indica para ${parameter%%word}:

Remova o padrão de sufixo correspondente. A palavra é expandida para produzir um padrão, assim como na expansão do nome do caminho. Se o padrão corresponder a uma parte final do valor expandido do parâmetro, então o resultado da expansão será o valor expandido do parâmetro com o padrão de correspondência mais curto (o caso ``%'') ou o padrão de correspondência mais longo (o ``% %'' caso) excluído.

Como não funciona conforme documentado (ou pelo que entendi a documentação), suspeito que seja um bug (o sufixo não correspondente (" -Wall") está sendo removido no caso de " %% *") no BASH. Estou certo?

Responder1

Em echo "X${xxx%% }X", o padrão é um espaço único :. A maior parte correspondente para isso é apenas isso: um único espaço. A menor parte correspondente também é apenas isso: um único espaço.

Para mais alguma coisa, você precisa do operador globbing *. Mas isso corresponderá a qualquer coisa, removendo o -Wall. O globbing do Bash não suporta diretamente um equivalente da expressão regular a*. Você precisariaglobulação estendida:

$ shopt -s extglob
$ echo "X${xxx%%+( )}X"
X-O -WallX

Responder2

Use a remoção de um prefixo dentro de uma remoção de sufixo:

$ xxx="-O -Wall  "
$ echo "X${xxx%"${xxx##*[! ]}"}X"
X-O -WallX
  • Remova tudo até o último caractere que não seja espaço - deixando apenas espaços à direita
  • Use esses espaços como padrão para remoção de sufixos
  • A expansão do parâmetro interno deve ser citada para evitar que seja interpretada como um padrão (não necessário acima, mas pode ser útil em outros casos):
$ bash -c 'xxx="-O -Wall*   "; echo "X${xxx%%"${xxx##*[! *]}"}X"'
X-O -WallX
$ bash -c 'xxx="-O -Wall*   "; echo "X${xxx%%${xxx##*[! *]}}X"'
XX

Um exemplo inventado, mas se a expansão interna não for citada, o asterisco que ela inclui será tratado como um padrão de casca pela expansão externa. Citado, torna-se um asterisco literal.


O comportamento que você observou não é um bug, é apenas como funcionam os padrões simples do shell:

${xxx%% }
  • um único espaço é um único espaço
  • a ocorrência mais longa de um único espaço é um único espaço
${xxx%% *}
  • ocorrência mais longa de um único espaço seguido por qualquer coisa/nada
  • qualquer coisa/nada incluirá-Wall
${xxx% }
  • a menor ocorrência de um único espaço é um único espaço
${xxx% *}
  • a ocorrência mais curta de um único espaço seguido por qualquer coisa/nada é um espaço único
${xxx%% \*}
  • \*é um asterisco com escape de barra invertida e será interpretado como um asterisco literal
  • não há espaço seguido de asterisco na variável, nenhum sufixo é removido

Responder3

readtambém pode funcionar (assumindo que IFScontém "espaço"):

xxx="-O -Wall  "
read -r xxx <<EOF
$xxx
EOF
echo "X${xxx}X"

Saída:

X-O -WallX

  • readdivide a entrada em campos de acordo comIFS
  • IFSpor padrão é espaço/tab/nova linha, então isso removerá quaisquer espaços iniciais e finais
  • Funciona na primeira linha da variável (pode não ser adequado para vars multilinhas, bashpoderia usar read -d '')

Responder4

Uma simples expansão de parâmetro é bastante limitada nos padrões que pode corresponder e remover. Para remover vários caracteres (repetidos) do final de uma string, a solução usual é primeiro remover tudo o que estánãoo caractere em questão ${xxx##*[! ]}(todos os espaços à direita). Então, como segunda etapa, remover tudo o que resulta dessa expansão (todos os espaços à direita) do final lhe dará o que você deseja (remover espaços à direita).

$ xxx="-O -Wall  "
$ echo "<${xxx%"${xxx##*[! ]}"}>"
<-O -Wall>

Como alternativa, no bash, você poderia usar globbing estendido:

$ shopt -s extglob
$ echo "<${xxx%%+( )}>"
<-O -Wall>

Ou, também, como alternativa de nível superior, você pode combinar o que quiser com uma regex:

$ regex='(.*[^ ]) +$';
$ [[ $xxx =~ $regex ]] && echo "<${BASH_REMATCH[1]}>" || echo "<$xxx>"
<-O -Wall>

Ou, como um script:

#!/bin/bash

xxx=${1:-"-O -Wall  "}

regex='(.*[^ ]) +$'

if    [[ $xxx =~ $regex ]]          # if there are trailing spaces
then 
      echo "<${BASH_REMATCH[1]}>"   # Print the string without spaces
else
      echo "<$xxx>"                 # if there are no trailing spaces.
fi

informação relacionada