maneira mais curta de substituir caracteres em uma variável

18

Existem várias maneiras de substituir caracteres em uma variável.

A maneira mais curta que descobri é traté agora:

OUTPUT=a\'b\"c\`d_123and_a_lot_more
OUTPUT=$(echo "$OUTPUT"|tr -d "'\`\"")
echo $OUTPUT

Existe uma maneira mais rápida? E é este citando-seguro para citações como ', "e `si?

rubo77
fonte
Eu acho que você pode continuar usando tr. O PE do BASH é bom, mas tr é muito mais rápido nesse caso. por exemplo, echo "$OUTPUT" | tr -dc '[[:alpha:]]' desde que você quer ter alphanumerics única
Valentin Bajrami
2
Como você está interessado em ser citado: sempre cite suas variáveis! echo "$OUTPUT". Ou melhor: printf "%s\n" "$OUTPUT". (O que acontece quando OUTPUT="-n"?)
musiphil
Você também pode considerar navegar no codegolf , especialmente as dicas do bash .
hoosierEE

Respostas:

22

Vamos ver. O mais curto que posso apresentar é uma emenda da sua trsolução:

OUTPUT="$(tr -d "\"\`'" <<<$OUTPUT)"

Outras alternativas incluem a substituição de variável já mencionada, que pode ser mais curta do que a mostrada até agora:

OUTPUT="${OUTPUT//[\'\"\`]}"

E é sedclaro que isso é mais longo em termos de caracteres:

OUTPUT="$(sed s/[\'\"\`]//g <<<$OUTPUT)"

Não tenho certeza se você quer dizer o menor tempo ou o tempo gasto. Em termos de comprimento, esses dois são os mais curtos possíveis (ou, de qualquer maneira,) quando se trata de remover esses caracteres específicos. Então, qual é o mais rápido? Testei definindo a OUTPUTvariável para o que você tinha no seu exemplo, mas repeti várias dezenas de vezes:

$ echo ${#OUTPUT} 
4900

$ time tr -d "\"\`'" <<<$OUTPUT
real    0m0.002s
user    0m0.004s
sys     0m0.000s
$ time sed s/[\'\"\`]//g <<<$OUTPUT
real    0m0.005s
user    0m0.000s
sys     0m0.000s
$ time echo ${OUTPUT//[\'\"\`]}
real    0m0.027s
user    0m0.028s
sys     0m0.000s

Como você pode ver, o tré claramente o mais rápido, seguido de perto sed. Além disso, parece que o uso echoé realmente um pouco mais rápido que o uso <<<:

$ for i in {1..10}; do 
    ( time echo $OUTPUT | tr -d "\"\`'" > /dev/null ) 2>&1
done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0025
$ for i in {1..10}; do 
    ( time tr -d "\"\`'" <<<$OUTPUT > /dev/null ) 2>&1 
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0029

Como a diferença é pequena, executei os testes acima 10 vezes para cada um dos dois, e o mais rápido é realmente o que você tinha para começar:

echo $OUTPUT | tr -d "\"\`'" 

No entanto, isso muda quando você leva em conta a sobrecarga de atribuir a uma variável, aqui, usar tré um pouco mais lenta que a simples substituição:

$ for i in {1..10}; do
    ( time OUTPUT=${OUTPUT//[\'\"\`]} ) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0032

$ for i in {1..10}; do
    ( time OUTPUT=$(echo $OUTPUT | tr -d "\"\`'")) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0044

Portanto, em conclusão, quando você simplesmente deseja visualizar os resultados, use, trmas se quiser reatribuir a uma variável, o uso dos recursos de manipulação de string do shell é mais rápido, pois evita a sobrecarga de executar um subshell separado.

terdon
fonte
4
Uma vez que o OP está interessado em definir a volta valor modificado em OUTPUT, você terá que conta para substituição de comando sub-shell sobrecarga envolvida na tre sedsoluções
Iruvar
@ 1_CR sim, mas como esse será o caso, qualquer que seja o método que ele use, achei que era irrelevante.
terdon
11
Não é bem assim, OUTPUT="${OUTPUT//[`\"\']/}" não envolve a substituição de comando
Iruvar
@ 1_CR ah, entendo, sim, você está certo e isso muda o resultado. Obrigado, resposta editada.
terdon
2
Os métodos que envolvem uma substituição de comando têm a desvantagem de manipular um pouco a cadeia. (Você pode evitá-lo, mas à custa de tornar o comando significativamente mais complexo.) Em particular, a substituição de comando remove as novas linhas finais.
Gilles 'SO- stop be evil'
15

Você poderia usar substituição de variável :

$ OUTPUT=a\'b\"c\`d
$ echo "$OUTPUT"
a'b"c`d

Use essa sintaxe: ${parameter//pattern/string}para substituir todas as ocorrências do padrão pela sequência.

$ echo "${OUTPUT//\'/x}"
axb"c`d
$ echo "${OUTPUT//\"/x}"
a'bxc`d
$ echo "${OUTPUT//\`/x}"
a'b"cxd
$ echo "${OUTPUT//[\'\"\`]/x}"
axbxcxd
caos
fonte
@ rubo77 echo ${OUTPUT//[`\"\']/x}givesaxbxcxa
caos
É incorreto nomear a expansão "expansão variável". É chamado de "expansão de parâmetros".
gena2x
@ gena2x - Não entendo o que seu comentário significa aqui?
slm
12

No bash ou zsh é:

OUTPUT="${OUTPUT//[\`\"\']/}"

Observe que ${VAR//PATTERN/}remove todas as instâncias do padrão. Para obter mais informações, expansão do parâmetro bash

Essa solução deve ser mais rápida para cadeias curtas, porque não envolve a execução de nenhum programa externo. No entanto, para strings muito longas, o oposto é verdadeiro - é melhor usar uma ferramenta dedicada para operações de texto, por exemplo:

$ OUTPUT="$(cat /usr/src/linux/.config)"

$ time (echo $OUTPUT | OUTPUT="${OUTPUT//set/abc}")
real    0m1.766s
user    0m1.681s
sys     0m0.002s

$ time (echo $OUTPUT | sed s/set/abc/g >/dev/null)
real    0m0.094s
user    0m0.078s
sys     0m0.006s
gena2x
fonte
11
De fato, tré mais rápido. Regexes e globs são caros e, embora não haja nenhum programa externo aqui, o bash sempre será mais lento do que algo parecido tr.
terdon
Isso depende muito dos dados de entrada e da implementação do regexp. Na sua resposta, você pegou um grande conjunto de dados específico - mas o conjunto de dados pode ser pequeno. Ou diferente. Além disso, você mede não o tempo de regexp, mas o tempo de eco, então não posso ter certeza se sua comparação é realmente justa.
gena2x
Bons pontos. No entanto, você não pode fazer reivindicações sobre velocidade sem testar. De fato, ao atribuir a uma variável, isso parece mais rápido, mas ao imprimir na tela trganha (veja minha resposta). Concordo que dependerá de muitos fatores, mas é exatamente por isso que você não pode dizer qual deles vence sem realmente testá-lo.
terdon
6

Se, por acaso, você estiver apenas tentando manipular cotações para reutilizar o shell, poderá fazer isso sem removê-las, e também é simples:

aq() { sh -c 'for a do
       alias "$((i=$i+1))=$a"
       done; alias' -- "$@"
}

Essa função shell cita qualquer matriz arg que você a entrega e incrementa sua saída por argumento iterável.

Aqui está com alguns argumentos:

aq \
"here's an
ugly one" \
"this one is \$PATHpretty bad, too" \
'this one```****```; totally sucks'

RESULTADO

1='here'"'"'s an
ugly one'
2='this one is $PATHpretty bad, too'
3='this one```****```; totally sucks'

Essa saída é da dashqual tipicamente aspas seguras saem entre aspas '"'"'. bashfaria '\''.

Substituir uma seleção de bytes únicos, sem espaço em branco e não nulos por outro byte único provavelmente pode ser feito mais rapidamente em qualquer shell POSIX com $IFSe $*.

set -f; IFS=\"\'\`; set -- $var; printf %s "$*"

RESULTADO

"some ""crazy """"""""string ""here

Aí eu apenas printfpara que você possa ver, mas é claro, se eu tivesse feito:

var="$*"

... ao invés do valor do printfcomando $var, seria o que você vê na saída.

Quando set -finstruo o shell a não glob - caso a string contenha caracteres que possam ser interpretados como padrões glob. Eu faço isso porque o analisador de shells expande os padrões glob depois de executar a divisão de campo nas variáveis. globbing pode ser reativado como set +f. Em geral - em scripts - acho útil definir meu estilo como:

#!/usr/bin/sh -f

E, em seguida, para ativar explicitamente englobamento com set +fem qualquer linha que eu poderia desejar.

A divisão do campo ocorre com base nos caracteres em $IFS.

Existem dois tipos de $IFSvalores - $IFSespaço em branco e $IFSnão-espaço em branco. $IFSos campos delimitados por espaço em branco (espaço, tabulação, nova linha) são especificados para eleger, por sequência, um único campo (ou nenhum, se não precederem outra coisa) - então ...

IFS=\ ; var='      '; printf '<%s>' $var
<>

Mas todos os outros são especificados para avaliar um único campo por ocorrência - eles não são truncados.

IFS=/; var='/////'; printf '<%s>' $var
<><><><><>

Todas as expansões variáveis ​​são, por padrão, $IFSmatrizes de dados delimitadas - elas são divididas em campos separados de acordo com $IFS. Ao "citar uma, você substitui a propriedade da matriz e a avalia como uma única sequência.

Então, quando eu faço ...

IFS=\"\'\`; set -- $var

Estou definindo a matriz de argumentos do shell para os muitos $IFScampos delimitados gerados pela $varexpansão do. Quando é expandido, seus valores constituintes para os caracteres contidos $IFSsão perdidos - eles são apenas separadores de campo agora - eles são \0NUL.

"$*"- como outras expansões variáveis ​​de aspas duplas - também substitui as qualidades de divisão de campos de $IFS. Mas, além disso , ele substitui o primeiro byte no $IFS para cada campo delimitado em "$@". Então, porque "foi o primeiro valor em $IFS todos os delimitadores subseqüentes "em que se tornou "$*". E a "necessidade também não está presente $IFSquando você a divide. Você poderia alterar $IFS depois set -- $args para outro valor completamente e seu novo primeiro byte apareceria para os delimitadores de campo em "$*". Além disso, você pode remover todos os traços deles como:

set -- $var; IFS=; printf %s "$*"

RESULTADO

some crazy string here
mikeserv
fonte
Muito bom, +1. Gostaria de saber se é realmente mais rápido. Você poderia adicionar alguns testes de tempo comparando-os com as abordagens na minha resposta? Espero que o seu seja mais rápido, mas gostaria de ver.
terdon
@terdon - isso depende da casca. É quase definitivamente mais rápido do que trem qualquer shell, mas a diferença é duvidosa bashpara o ${var//$c/$newc/}caso. Espero que, mesmo nesse caso, seja mais rápido em alguma margem, mas geralmente não me preocupo com isso porque, para essas coisas, eu sempre uso dash- o que é mais rápido em ordens de grandeza em todos os aspectos. E, portanto, é difícil comparar.
mikeserv
@terdon - eu tentei. Mas - mesmo em execução bash- time (IFS=\"\'`; set -- $var; printf %s "$*")e time (var=${var//\'`/\"/})ambos resultam em 0.0000sresultados para todos os campos. Estou fazendo algo errado, você acha? Deveria haver uma barra invertida antes da citação lá em cima, mas não sei como colocar uma citação em um campo de código de comentário.
mikeserv