Buscar y reemplazar en bash usando expresiones regulares

161

He visto este ejemplo:

hello=ho02123ware38384you443d34o3434ingtod38384day
echo ${hello//[0-9]/}

Que sigue a esta sintaxis: ${variable//pattern/replacement}

Desafortunadamente, el patterncampo no parece admitir la sintaxis de expresiones regulares completas (si uso .o \s, por ejemplo, intenta hacer coincidir los caracteres literales).

¿Cómo puedo buscar / reemplazar una cadena usando la sintaxis completa de expresiones regulares?

Lanaru
fuente
Encontré una pregunta relacionada aquí: stackoverflow.com/questions/5658085/…
jheddings
2
Para su información, \sno es parte de la sintaxis de expresión regular estándar definida por POSIX (ni BRE ni ERE); es una extensión PCRE, y en su mayoría no está disponible desde shell. [[:space:]]Es el equivalente más universal.
Charles Duffy
1
\spuede ser reemplazado por [[:space:]], por cierto, .por ?, y las extensiones extglob al lenguaje de patrón de shell de línea de base pueden usarse para cosas como subgrupos opcionales, grupos repetidos y similares.
Charles Duffy
Lo uso en bash versión 4.1.11 en Solaris ... echo $ {hello // [0-9]} Observe la falta de la barra final.
Daniel Liston el

Respuestas:

175

Use sed :

MYVAR=ho02123ware38384you443d34o3434ingtod38384day
echo "$MYVAR" | sed -e 's/[a-zA-Z]/X/g' -e 's/[0-9]/N/g'
# prints XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Tenga en cuenta que los subsiguientes -ese procesan en orden. Además, el gindicador de la expresión coincidirá con todas las apariciones en la entrada.

También puede elegir su herramienta favorita utilizando este método, es decir, perl, awk, por ejemplo:

echo "$MYVAR" | perl -pe 's/[a-zA-Z]/X/g and s/[0-9]/N/g'

Esto puede permitirle hacer más coincidencias creativas ... Por ejemplo, en el recorte anterior, el reemplazo numérico no se usaría a menos que hubiera una coincidencia en la primera expresión (debido a una andevaluación diferida ). Y, por supuesto, tiene el soporte de idiomas completo de Perl para hacer su oferta ...

jheddings
fuente
Esto solo reemplaza solo por lo que puedo decir. ¿Hay alguna manera de que reemplace todas las ocurrencias del patrón como lo hace el código que publiqué?
Lanaru
He actualizado mi respuesta para demostrar múltiples reemplazos, así como la coincidencia global de patrones. Avísame si eso ayuda.
jheddings
¡Muchas gracias! Por curiosidad, ¿por qué cambiaste de una versión de una línea (en tu respuesta original) a una de dos líneas?
Lanaru
9
Usar sedu otras herramientas externas es costoso debido al tiempo de inicialización del proceso. Especialmente busqué la solución all-bash, porque descubrí que usar sustituciones bash es más de 3 veces más rápido que llamar seda cada elemento de mi ciclo.
rr-
66
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 视, concedido, esa es la sabiduría común, pero eso no lo hace sabio. Sí, bash es lento, pase lo que pase, pero bash bien escrito que evita subcapas es literalmente órdenes de magnitud más rápido que bash que llama a herramientas externas para cada pequeña tarea. Además, los scripts de shell bien escritos se beneficiarán de intérpretes más rápidos (como ksh93, que tiene un rendimiento a la par con awk), mientras que los mal escritos no tienen nada que hacer.
Charles Duffy
133

Esto realmente se puede hacer en puro bash:

hello=ho02123ware38384you443d34o3434ingtod38384day
re='(.*)[0-9]+(.*)'
while [[ $hello =~ $re ]]; do
  hello=${BASH_REMATCH[1]}${BASH_REMATCH[2]}
done
echo "$hello"

... rinde ...

howareyoudoingtodday
Charles Duffy
fuente
2
Algo me dice que te encantarán estos: stackoverflow.com/questions/5624969/… =)
nickl-
=~es la llave. Pero un poco torpe, dada la reasignación en el bucle. La solución @jheddings 2 años antes es otra buena opción: llamar a sed o perl).
Brent Faust
3
Llamar sedo perles sensato, si usa cada invocación para procesar más de una sola línea de entrada. Invocar dicha herramienta en el interior de un bucle, en lugar de usar un bucle para procesar su flujo de salida, es insensato.
Charles Duffy
2
FYI, en zsh, es solo en $matchlugar de $BASH_REMATCH. (Puedes hacer que se comporte como una fiesta setopt bash_rematch).
Marian
Es extraño, ya que zsh no está tratando de ser un shell POSIX, podría decirse que sigue la carta de orientación de POSIX sobre las variables en mayúsculas que se utilizan para fines específicos de POSIX (relevantes para el shell o el sistema) y las variables en minúsculas que se reservan para uso de la aplicación Pero dado que zsh es algo que ejecuta aplicaciones, en lugar de una aplicación en sí, esta decisión de usar el espacio de nombres de la variable de la aplicación en lugar del espacio de nombres del sistema parece terriblemente perverso.
Charles Duffy
95

Estos ejemplos también funcionan en bash sin necesidad de usar sed:

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day
MYVAR=${MYVAR//[a-zA-Z]/X} 
echo ${MYVAR//[0-9]/N}

también puedes usar las expresiones de paréntesis de clase de caracteres

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day
MYVAR=${MYVAR//[[:alpha:]]/X} 
echo ${MYVAR//[[:digit:]]/N}

salida

XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Sin embargo, lo que @Lanaru quería saber, si entiendo la pregunta correctamente, es por qué las extensiones "completa" o PCRE, \s\S\w\W\d\Detc., no funcionan como se admite en php ruby ​​python, etc. Estas extensiones son de expresiones regulares compatibles con Perl (PCRE) y puede no ser compatible con otras formas de expresiones regulares basadas en shell.

Estos no funcionan:

#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo ${hello//\d/}


#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo $hello | sed 's/\d//g'

salida con todos los caracteres literales "d" eliminados

ho02123ware38384you44334o3434ingto38384ay

pero lo siguiente funciona como se esperaba

#!/bin/bash
hello=ho02123ware38384you443d34o3434ingtod38384day
echo $hello | perl -pe 's/\d//g'

salida

howareyoudoingtodday

Espero que eso aclare un poco más las cosas, pero si aún no está confundido, ¿por qué no prueba esto en Mac OS X que tiene habilitado el indicador REG_ENHANCED?

#!/bin/bash
MYVAR=ho02123ware38384you443d34o3434ingtod38384day;
echo $MYVAR | grep -o -E '\d'

En la mayoría de los sabores de * nix solo verá el siguiente resultado:

d
d
d

nJoy!

nickl-
fuente
66
¿Perdón? no${foo//$bar/$baz} es la sintaxis POSIX.2 BRE o ERE, es una coincidencia de patrones de estilo fnmatch ().
Charles Duffy
8
... entonces, mientras ${hello//[[:digit:]]/}funciona, si quisiéramos filtrar solo los dígitos precedidos por la letra o, ${hello//o[[:digit:]]*}tendría un comportamiento completamente diferente al esperado (ya que en patrones fnmatch, *coincide con todos los caracteres, en lugar de modificar el elemento inmediatamente anterior para que sea 0 o más).
Charles Duffy
1
Ver pubs.opengroup.org/onlinepubs/9699919799/utilities/… (y todo lo que incorpora por referencia) para la especificación completa de fnmatch.
Charles Duffy
1
man bash: hay disponible un operador binario adicional, = ~, con la misma precedencia que == y! =. Cuando se usa, la cadena a la derecha del operador se considera una expresión regular extendida y se corresponde en consecuencia (como en regex (3)).
nickl-
1
@aderchox tienes razón, para los dígitos que puedes usar [0-9]o[[:digit:]]
nickl-
13

Si realiza llamadas repetidas y le preocupa el rendimiento, esta prueba revela que el método BASH es ~ 15 veces más rápido que la bifurcación y probablemente cualquier otro proceso externo.

hello=123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X123456789X

P1=$(date +%s)

for i in {1..10000}
do
   echo $hello | sed s/X//g > /dev/null
done

P2=$(date +%s)
echo $[$P2-$P1]

for i in {1..10000}
do
   echo ${hello//X/} > /dev/null
done

P3=$(date +%s)
echo $[$P3-$P2]
Josiah DeWitt
fuente
1
Si está interesado en la forma de reducir las horquillas, busque la palabra newConnector en esta respuesta a ¿Cómo establecer una variable en la salida de un comando en Bash?
F. Hauri
8

Use [[:digit:]](observe los corchetes dobles) como patrón:

$ hello=ho02123ware38384you443d34o3434ingtod38384day
$ echo ${hello//[[:digit:]]/}
howareyoudoingtodday

Solo quería resumir las respuestas (especialmente @ nickl- 's https://stackoverflow.com/a/22261334/2916086 ).

yegeniy
fuente
1

Sé que este es un hilo antiguo, pero fue mi primer éxito en Google, y quería compartir lo siguiente resubque armé, lo que agrega soporte para múltiples referencias de $ 1, $ 2, etc.

#!/usr/bin/env bash

############################################
###  resub - regex substitution in bash  ###
############################################

resub() {
    local match="$1" subst="$2" tmp

    if [[ -z $match ]]; then
        echo "Usage: echo \"some text\" | resub '(.*) (.*)' '\$2 me \${1}time'" >&2
        return 1
    fi

    ### First, convert "$1" to "$BASH_REMATCH[1]" and 'single-quote' for later eval-ing...

    ### Utility function to 'single-quote' a list of strings
    squot() { local a=(); for i in "$@"; do a+=( $(echo \'${i//\'/\'\"\'\"\'}\' )); done; echo "${a[@]}"; }

    tmp=""
    while [[ $subst =~ (.*)\${([0-9]+)}(.*) ]] || [[ $subst =~ (.*)\$([0-9]+)(.*) ]]; do
        tmp="\${BASH_REMATCH[${BASH_REMATCH[2]}]}$(squot "${BASH_REMATCH[3]}")${tmp}"
        subst="${BASH_REMATCH[1]}"
    done
    subst="$(squot "${subst}")${tmp}"

    ### Now start (globally) substituting

    tmp=""
    while read line; do
        counter=0
        while [[ $line =~ $match(.*) ]]; do
            eval tmp='"${tmp}${line%${BASH_REMATCH[0]}}"'"${subst}"
            line="${BASH_REMATCH[$(( ${#BASH_REMATCH[@]} - 1 ))]}"
        done
        echo "${tmp}${line}"
    done
}

resub "$@"

##################
###  EXAMPLES  ###
##################

###  % echo "The quick brown fox jumps quickly over the lazy dog" | resub quick slow
###    The slow brown fox jumps slowly over the lazy dog

###  % echo "The quick brown fox jumps quickly over the lazy dog" | resub 'quick ([^ ]+) fox' 'slow $1 sheep'
###    The slow brown sheep jumps quickly over the lazy dog

###  % animal="sheep"
###  % echo "The quick brown fox 'jumps' quickly over the \"lazy\" \$dog" | resub 'quick ([^ ]+) fox' "\"\$low\" \${1} '$animal'"
###    The "$low" brown 'sheep' 'jumps' quickly over the "lazy" $dog

###  % echo "one two three four five" | resub "one ([^ ]+) three ([^ ]+) five" 'one $2 three $1 five'
###    one four three two five

###  % echo "one two one four five" | resub "one ([^ ]+) " 'XXX $1 '
###    XXX two XXX four five

###  % echo "one two three four five one six three seven eight" | resub "one ([^ ]+) three ([^ ]+) " 'XXX $1 YYY $2 '
###    XXX two YYY four five XXX six YYY seven eight

H / T a @Charles Duffy re:(.*)$match(.*)

Dabe Murphy
fuente