forma más corta de reemplazar caracteres en una variable

18

Hay muchas formas de reemplazar caracteres en una variable.

El camino más corto que descubrí es trhasta ahora:

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

¿Hay una manera mas rápida? Y es este seguro citando a citas como ', "y `sí mismo?

rubo77
fuente
Creo que puedes seguir usando tr. La PE de BASH es buena, pero tr es mucho más rápida en este caso. por ejemplo, echo "$OUTPUT" | tr -dc '[[:alpha:]]' ya que solo desea tener alfanuméricos
Valentin Bajrami
2
Como está interesado en ser un experto en citas: ¡Siempre cite sus variables! echo "$OUTPUT". O mejor: printf "%s\n" "$OUTPUT". (¿Qué sucede cuándo OUTPUT="-n"?)
musiphil
También puede considerar navegar por codegolf , especialmente los consejos de bash .
hoosierEE

Respuestas:

22

Veamos. Lo más corto que se me ocurre es un ajuste de su trsolución:

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

Otras alternativas incluyen la sustitución de variables ya mencionada que puede ser más corta que la mostrada hasta ahora:

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

Y, sedpor supuesto, aunque esto es más largo en términos de caracteres:

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

No estoy seguro de si te refieres a la longitud más corta o en términos de tiempo empleado. En términos de longitud, estos dos son lo más cortos posible (o como puedo obtener de todos modos) cuando se trata de eliminar esos caracteres específicos. Entonces, ¿cuál es el más rápido? Probé configurando la OUTPUTvariable a lo que tenía en su ejemplo, pero repetí varias docenas de veces:

$ 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 puede ver, el tres claramente el más rápido, seguido de cerca sed. Además, parece que usar echoes en realidad un poco más rápido que usar <<<:

$ 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 la diferencia es pequeña, realicé las pruebas anteriores 10 veces para cada una de las dos y resulta que la más rápida es la que tenía que comenzar:

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

Sin embargo, esto cambia cuando tiene en cuenta la sobrecarga de asignar a una variable, aquí, el uso tres un poco más lento que el reemplazo simple:

$ 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

Entonces, en conclusión, cuando simplemente desea ver los resultados, use trpero si desea reasignar a una variable, usar las funciones de manipulación de cadenas del shell es más rápido ya que evitan la sobrecarga de ejecutar un subshell separado.

terdon
fuente
44
Dado que el OP está interesado en volver a establecer el valor modificado OUTPUT, tendrá que tener en cuenta la sobrecarga de sub-shell de sustitución de comandos involucrada try las sedsoluciones
irá el
@ 1_CR sí, pero dado que ese será el caso, sea cual sea el método que utilice, supuse que era irrelevante.
terdon
1
No del todo, OUTPUT="${OUTPUT//[`\"\']/}" no implica la sustitución de comandos
iruvar
@ 1_CR ah, ya veo, sí, tienes toda la razón y eso cambia el resultado. Gracias, respuesta editada.
terdon
2
Los métodos que implican una sustitución de comandos tienen el inconveniente de alterar un poco la cadena. (Puede evitarlo, pero a expensas de hacer que el comando sea significativamente más complejo). En particular, la sustitución de comandos elimina las nuevas líneas finales.
Gilles 'SO- deja de ser malvado'
15

Puedes usar la sustitución de variables :

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

Use esa sintaxis: ${parameter//pattern/string}para reemplazar todas las apariciones del patrón con la cadena.

$ echo "${OUTPUT//\'/x}"
axb"c`d
$ echo "${OUTPUT//\"/x}"
a'bxc`d
$ echo "${OUTPUT//\`/x}"
a'b"cxd
$ echo "${OUTPUT//[\'\"\`]/x}"
axbxcxd
caos
fuente
@ rubo77 echo ${OUTPUT//[`\"\']/x}daaxbxcxa
caos
Es incorrecto nombrar la expansión "expansión variable". Se llama "expansión de parámetros".
gena2x
@ gena2x - ¿No entiendo qué significa tu comentario aquí?
slm
12

En bash o zsh es:

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

Tenga en cuenta que ${VAR//PATTERN/} elimina todas las instancias del patrón. Para más información expansión de parámetros bash

Esta solución debería ser más rápida para cadenas cortas porque no implica ejecutar ningún programa externo. Sin embargo, para cadenas muy largas, ocurre lo contrario: es mejor usar una herramienta dedicada para operaciones de texto, por ejemplo:

$ 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
fuente
1
De hecho, tres más rápido. Regexes y globs son caros, y aunque no hay un programa externo aquí, bash siempre será más lento que algo así tr.
terdon
Eso depende en gran medida de los datos de entrada y de la implementación de expresiones regulares. En su respuesta, tomó un conjunto específico de datos grandes, pero el conjunto de datos puede ser pequeño. O diferente Además, no mide el tiempo de expresión regular sino el tiempo de eco, por lo que no puedo estar seguro de si su comparación es realmente justa.
gena2x
Buenos puntos. Sin embargo, no puede hacer afirmaciones sobre la velocidad sin realizar pruebas. De hecho, cuando se asigna a una variable, esto parece más rápido, pero cuando se imprime en la pantalla trgana (vea mi respuesta). Estoy de acuerdo en que dependerá de muchos factores, pero esa es exactamente la razón por la que no se puede saber cuál gana sin probarlo realmente.
terdon
6

Si, por casualidad, solo está tratando de manejar las cotizaciones para reutilizar el shell, puede hacerlo sin eliminarlas, y también es muy simple:

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

Ese shell de funciones cita cualquier matriz arg que le entregue e incrementa su salida por argumento iterable.

Aquí está con algunos argumentos:

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

SALIDA

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

Esa salida es a partir de la dashcual, por lo general, las comillas seguras son comillas simples '"'"'.bashharía '\''.

Reemplazar una selección de bytes únicos, no espacios en blanco, no nulos con otro byte único probablemente se puede hacer más rápido en cualquier shell POSIX con $IFSy $*.

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

SALIDA

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

Ahí lo acabo de printfver, pero por supuesto, si lo hubiera hecho:

var="$*"

... en lugar del printfcomando$var sería lo que ves en la salida allí.

Cuando le set -findico al shell que no se meta, en el caso de que la cadena contenga caracteres que podrían interpretarse como patrones globales. Hago esto porque el analizador de shells expande los patrones globales después de realizar la división de campo en las variables. globbing se puede volver a habilitar como set +f. En general, en los scripts, me resulta útil configurar mi explosión como:

#!/usr/bin/sh -f

Y luego habilitar explícitamente globbing conset +f cualquier línea que desee.

La división de campos se produce en función de los caracteres en $IFS .

Hay dos tipos de $IFSvalores: $IFSespacios en blanco y $IFSno espacios en blanco. $IFSlos campos delimitados de espacios en blanco (espacio, tabulación, nueva línea) se especifican para eluir por secuencia a un solo campo (o ninguno en absoluto si no preceden a otra cosa) , así que ...

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

Pero todos los demás están especificados para evaluar en un solo campo por ocurrencia ; no se truncan.

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

Todas las expansiones variables son, de forma predeterminada, $IFSmatrices de datos delimitadas; se dividen en campos separados según $IFS. Cuando "cita una, anula esa propiedad de matriz y la evalúa como una sola cadena.

Entonces cuando lo hago ...

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

Estoy configurando la matriz de argumentos del shell en los muchos $IFScampos delimitados generados por $varla expansión de. Cuando se expande, sus valores constitutivos para los caracteres contenidos $IFSse pierden , ahora son solo separadores de campo, lo son \0NUL.

"$*"- al igual que otras expansiones variables con comillas dobles - también anula las cualidades de división de campo de $IFS. Pero, además , sustituye el primer byte $IFS por cada campo delimitado en "$@". Así pues "fue el primer valor en $IFS todos los delimitadores posteriores se convierten "en "$*". Y "tampoco es necesario que lo separes $IFScuando lo dividas. Se podría alterar $IFS después set -- $args a otro valor en su totalidad y su nuevo primer byte A continuación, presentó a los delimitadores de campo en "$*". Además, puede eliminar todos los rastros de ellos por completo como:

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

SALIDA

some crazy string here
mikeserv
fuente
Muy bien, +1. Me pregunto si es más rápido. ¿Podría agregar algunas pruebas de tiempo comparándolo con los enfoques en mi respuesta? Espero que el tuyo sea más rápido, pero me gustaría verlo.
terdon
@terdon: eso depende del caparazón. Es casi definitivamente más rápido que tren cualquier shell, pero la diferencia es dudosa bashpara el ${var//$c/$newc/}caso. Espero que incluso en ese caso sea más rápido por algún margen, pero generalmente no me preocupo por eso porque para estas cosas siempre uso dash, que es más rápido por órdenes de magnitud en general en todos los aspectos. Y entonces es difícil de comparar.
mikeserv
@terdon - Lo intenté. Pero, incluso al bashhacer, time (IFS=\"\'`; set -- $var; printf %s "$*")y time (var=${var//\'`/\"/})ambos resultan en 0.0000sresultados para todos los campos. ¿Estoy haciendo algo mal, crees? Se supone que debe haber una barra diagonal inversa antes de la cita anterior, pero no sé cómo poner una cita inversa en un campo de código de comentario.
mikeserv