Cerca e sostituisci in bash usando le espressioni regolari


161

Ho visto questo esempio:

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

Che segue questa sintassi: ${variable//pattern/replacement}

Sfortunatamente il patterncampo non sembra supportare la sintassi della regex completa (se uso .o \s, ad esempio, cerca di abbinare i caratteri letterali).

Come posso cercare / sostituire una stringa usando la sintassi regex completa?


Trovato una domanda relativa qui: stackoverflow.com/questions/5658085/...
jheddings

2
Cordiali saluti, \snon fa parte della sintassi standard delle espressioni regolari definite da POSIX (né BRE né ERE); è un'estensione PCRE e per lo più non è disponibile dalla shell. [[:space:]]è l'equivalente più universale.
Charles Duffy,

1
\spuò essere sostituito [[:space:]], tra l'altro, .da ?, ed extglob le estensioni al linguaggio del modello di shell di base possono essere usate per cose come sottogruppi opzionali, gruppi ripetuti e simili.
Charles Duffy,


Lo uso nella versione 4.1.11 di Bash su Solaris ... echo $ {hello // [0-9]} Nota la mancanza della barra finale.
Daniel Liston,

Risposte:


175

Usa sed :

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

Si noti che i successivi -evengono elaborati in ordine. Anche ilg flag per l'espressione corrisponderà a tutte le occorrenze nell'input.

Puoi anche scegliere il tuo strumento preferito usando questo metodo, ad esempio perl, awk, ad esempio:

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

Ciò può consentire di eseguire corrispondenze più creative ... Ad esempio, nella sezione precedente, la sostituzione numerica non verrebbe utilizzata a meno che non vi fosse una corrispondenza sulla prima espressione (a causa di una andvalutazione pigra ). E, naturalmente, hai il supporto linguistico completo di Perl per fare le tue offerte ...


Questo è solo un singolo sostituto, per quanto posso dire. C'è un modo per farlo sostituire tutte le occorrenze del modello come quello che fa il codice che ho pubblicato?
Lanaru,

Ho aggiornato la mia risposta per dimostrare più sostituzioni e la corrispondenza globale dei modelli. Fammi sapere se questo aiuta.
jheddings,

Grazie mille! Per curiosità, perché sei passato da una versione a una riga (nella risposta originale) a una doppia?
Lanaru,

9
L'uso sedo altri strumenti esterni è costoso a causa del tempo di inizializzazione del processo. In particolare ho cercato una soluzione all-bash, perché ho scoperto che usare le sostituzioni bash è più di 3 volte più veloce rispetto alla chiamata sedper ogni elemento nel mio ciclo.
r-

6
@CiroSantilli granted 事件 法轮功 纳米比亚 威 视, garantito, questa è la saggezza comune, ma non lo rende saggio. Sì, bash è lento a prescindere da cosa - ma bash ben scritto che evita le conchiglie è letteralmente ordini di grandezza più veloci di bash che chiama strumenti esterni per ogni piccola piccola attività. Inoltre, script di shell ben scritti trarranno beneficio da interpreti più veloci (come ksh93, che ha prestazioni alla pari di awk), mentre quelli scritti male non hanno nulla da fare.
Charles Duffy,

133

Questo in realtà può essere fatto in puro bash:

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

... i rendimenti ...

howareyoudoingtodday

2
Qualcosa mi dice che amerete questi: stackoverflow.com/questions/5624969/... =)
nickl-

=~è la chiave. Ma un po 'goffo, data la riassegnazione nel ciclo. La soluzione @jheddings 2 anni prima è un'altra buona opzione: chiamare sed o perl).
Brent Faust,

3
Chiamare sedo perlè sensato, se si utilizza ogni invocazione per elaborare più di una singola riga di input. Invocare un simile strumento all'interno di un loop, invece di utilizzare un loop per elaborare il suo flusso di output, è insensato.
Charles Duffy,

2
Cordiali saluti, in zsh, è solo $matchinvece di $BASH_REMATCH. (Puoi farlo comportare come se ti bastasse setopt bash_rematch.)
Marian,

È strano - dal momento che zsh non sta cercando di essere una shell POSIX, sta probabilmente seguendo la lettera della guida POSIX sulle variabili all-cap utilizzate per scopi specificati da POSIX (shell o rilevanti per il sistema) e le variabili minuscole riservate per uso dell'applicazione. Ma dal momento che zsh è qualcosa che esegue le applicazioni, piuttosto che un'applicazione stessa, questa decisione di usare lo spazio dei nomi delle variabili delle applicazioni piuttosto che lo spazio dei nomi del sistema sembra terribilmente perversa.
Charles Duffy,

95

Questi esempi funzionano anche in bash senza bisogno di usare sed:

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

puoi anche usare le espressioni di parentesi della classe di caratteri

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

produzione

XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Ciò che @Lanaru voleva sapere, tuttavia, se capisco correttamente la domanda, è perché le estensioni "complete" o PCRE \s\S\w\W\d\D ecc. Non funzionano come supportate in php ruby ​​python ecc. Queste estensioni provengono da espressioni regolari compatibili con Perl (PCRE) e potrebbe non essere compatibile con altre forme di espressioni regolari basate su shell.

Questi non funzionano:

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


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

output con tutti i caratteri "d" letterali rimossi

ho02123ware38384you44334o3434ingto38384ay

ma quanto segue funziona come previsto

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

produzione

howareyoudoingtodday

Spero che chiarisca un po 'di più le cose, ma se non sei ancora confuso perché non provi questo su Mac OS X che ha il flag REG_ENHANCED abilitato:

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

Sulla maggior parte dei gusti di * nix vedrai solo il seguente output:

d
d
d

nJoy!


6
Scusi? non${foo//$bar/$baz} è sintassi POSIX.2 BRE o ERE - è fnmatch () - corrispondenza del modello di stile.
Charles Duffy,

8
... quindi, mentre ${hello//[[:digit:]]/}funziona, se volessimo filtrare solo le cifre precedute dalla lettera o, ${hello//o[[:digit:]]*}avrebbe un comportamento completamente diverso da quello atteso (poiché in modelli fnmatch, *corrisponde a tutti i caratteri, piuttosto che modificare l'elemento immediatamente precedente per essere 0-o-più).
Charles Duffy,

1
Vedi pubs.opengroup.org/onlinepubs/9699919799/utilities/… (e tutto ciò che include per riferimento) per le specifiche complete su fnmatch.
Charles Duffy,

1
man bash: è disponibile un ulteriore operatore binario, = ~, con la stessa precedenza di == e! =. Quando viene utilizzata, la stringa a destra dell'operatore viene considerata un'espressione regolare estesa e adattata di conseguenza (come in regex (3)).
Nickl

1
@aderchox hai ragione, per le cifre puoi usare [0-9]o[[:digit:]]
nickl-

13

Se stai effettuando chiamate ripetute e sei preoccupato per le prestazioni, questo test rivela che il metodo BASH è ~ 15 volte più veloce di un fork di sed e probabilmente di qualsiasi altro processo esterno.

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]

1
Se sei interessato a ridurre le forcelle, cerca la parola newConnector in questa risposta a Come impostare una variabile sull'output di un comando in Bash?
F. Hauri,

8

Utilizzare [[:digit:]](notare le doppie parentesi) come modello:

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

Volevo solo riassumere le risposte (in particolare @ https://stackoverflow.com/a/22261334/2916086 di @ nickl- ).


1

So che questo è un thread antico, ma è stato il mio primo successo su Google e volevo condividere quanto segue resubche ho messo insieme, il che aggiunge il supporto per più riferimenti secondari da $ 1, $ 2, ecc ...

#!/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

Da H / T a @Charles Duffy in riferimento a :(.*)$match(.*)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.