Cari dan ganti bash menggunakan ekspresi reguler

161

Saya telah melihat contoh ini:

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

Yang mengikuti sintaks ini: ${variable//pattern/replacement}

Sayangnya, patternbidang tersebut tampaknya tidak mendukung sintaksis regex penuh (jika saya menggunakan .atau \s, misalnya, mencoba mencocokkan karakter literal).

Bagaimana saya bisa mencari / mengganti string menggunakan sintaks regex penuh?

Lanaru
sumber
Menemukan pertanyaan terkait di sini: stackoverflow.com/questions/5658085/…
jheddings
2
FYI, \sbukan bagian dari sintaks ekspresi reguler standar yang didefinisikan POSIX (baik BRE atau ERE); itu adalah ekstensi PCRE, dan sebagian besar tidak tersedia dari shell. [[:space:]]adalah padanan yang lebih universal.
Charles Duffy
1
\sdapat diganti oleh [[:space:]], dengan cara, .oleh ?, dan ekstensi extglob ke bahasa pola dasar shell dapat digunakan untuk hal-hal seperti subkelompok opsional, kelompok berulang, dan sejenisnya.
Charles Duffy
3
Deskripsi pola Bash .
ceving
Saya menggunakan ini di bash versi 4.1.11 pada Solaris ... echo $ {hello // [0-9]} Perhatikan kurangnya slash terakhir.
Daniel Liston

Jawaban:

175

Gunakan sed :

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

Perhatikan bahwa proses selanjutnya -ediproses secara berurutan. Juga, gtanda untuk ekspresi akan cocok dengan semua kemunculan dalam input.

Anda juga dapat memilih alat favorit Anda menggunakan metode ini, yaitu perl, awk, misalnya:

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

Ini memungkinkan Anda melakukan lebih banyak pertandingan kreatif ... Misalnya, dalam snip di atas, penggantian numerik tidak akan digunakan kecuali ada kecocokan pada ekspresi pertama (karena andevaluasi malas ). Dan tentu saja, Anda memiliki dukungan bahasa penuh dari Perl untuk melakukan penawaran Anda ...

jheddings
sumber
Ini hanya satu penggantian sejauh yang saya tahu. Apakah ada cara untuk mengganti semua kejadian pola seperti apa yang saya posting kode lakukan?
Lanaru
Saya telah memperbarui jawaban saya untuk menunjukkan beberapa penggantian serta pencocokan pola global. Beri tahu saya jika itu membantu.
jheddings
Terima kasih banyak! Karena penasaran, mengapa Anda beralih dari versi satu baris (dalam jawaban awal Anda) ke dua baris?
Lanaru
9
Menggunakan sedatau alat eksternal lainnya mahal karena waktu proses inisialisasi. Saya terutama mencari solusi all-bash, karena saya menemukan menggunakan pengganti bash menjadi lebih dari 3x lebih cepat daripada memanggil sedsetiap item di loop saya.
rr-
6
@CiroSantilli 六四 事件 法轮功 纳米比亚 威 granted, memang, itu adalah kebijaksanaan umum, tapi itu tidak membuatnya bijaksana. Ya, bash lambat apa pun yang terjadi - tetapi bash yang ditulis dengan baik yang menghindari subkulit secara harfiah lebih cepat daripada bash yang memanggil alat eksternal untuk setiap tugas kecil. Juga, skrip shell yang ditulis dengan baik akan mendapat manfaat dari penerjemah yang lebih cepat (seperti ksh93, yang memiliki kinerja setara dengan awk), sedangkan skrip yang ditulis dengan buruk tidak ada yang bisa dilakukan.
Charles Duffy
133

Ini sebenarnya bisa dilakukan dalam bash murni:

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

... menghasilkan ...

howareyoudoingtodday
Charles Duffy
sumber
2
Sesuatu memberi tahu saya bahwa Anda akan menyukai ini: stackoverflow.com/questions/5624969/… =)
nickl-
=~adalah kuncinya. Tapi agak kikuk, mengingat penugasan kembali dalam lingkaran. Solusi @jheddings 2 tahun sebelumnya adalah pilihan lain yang bagus - memanggil sed atau perl).
Brent Faust
3
Memanggil sedatau perlmasuk akal, jika menggunakan setiap doa untuk memproses lebih dari satu baris input. Meminta alat semacam itu di bagian dalam loop, sebagai lawan menggunakan loop untuk memproses aliran outputnya, adalah hal yang bodoh.
Charles Duffy
2
FYI, di zsh, hanya saja $matchbukan $BASH_REMATCH. (Anda dapat membuatnya berperilaku seperti bash setopt bash_rematch.)
Marian
Ini aneh - karena zsh tidak mencoba menjadi shell POSIX, ini bisa dibilang mengikuti surat panduan POSIX tentang semua variabel kapitalisasi yang digunakan untuk tujuan yang ditentukan POSIX (terkait sistem atau shell) dan variabel huruf kecil dicadangkan untuk penggunaan aplikasi. Tetapi karena zsh adalah sesuatu yang menjalankan aplikasi, daripada aplikasi itu sendiri, keputusan untuk menggunakan variabel namespace aplikasi daripada sistem namespace tampaknya sangat menyimpang.
Charles Duffy
95

Contoh-contoh ini juga bekerja di bash tidak perlu menggunakan sed:

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

Anda juga dapat menggunakan ekspresi braket kelas karakter

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

keluaran

XXNNNNNXXXXNNNNNXXXNNNXNNXNNNNXXXXXXNNNNNXXX

Apa yang ingin diketahui @Lanaru, jika saya memahami pertanyaan dengan benar, adalah mengapa ekstensi "penuh" atau PCRE \s\S\w\W\d\Ddll tidak berfungsi sebagaimana didukung dalam php ruby ​​python dll. Ekstensi ini berasal dari ekspresi reguler yang kompatibel dengan Perl (kompatibel dengan PCRE) dan mungkin tidak kompatibel dengan bentuk reguler ekspresi berbasis shell lainnya.

Ini tidak berfungsi:

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


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

output dengan semua karakter "d" literal dihapus

ho02123ware38384you44334o3434ingto38384ay

tetapi yang berikut ini berfungsi seperti yang diharapkan

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

keluaran

howareyoudoingtodday

Berharap itu menjelaskan hal-hal sedikit lebih banyak tetapi jika Anda belum bingung mengapa tidak Anda coba ini pada Mac OS X yang memiliki flag REG_ENHANCED diaktifkan:

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

Pada sebagian besar rasa * nix Anda hanya akan melihat output berikut:

d
d
d

nJoy!

nickl-
sumber
6
Maaf? ${foo//$bar/$baz}adalah tidak POSIX.2 BRE atau sintaks ERE - itu fnmatch () - gaya pola yang cocok.
Charles Duffy
8
... jadi, sedangkan ${hello//[[:digit:]]/}karya, jika kita ingin memfilter hanya digit yang didahului oleh surat o, ${hello//o[[:digit:]]*}akan memiliki perilaku yang sama sekali berbeda dari yang diharapkan (karena dalam pola fnmatch, *cocok dengan semua karakter, daripada memodifikasi item segera sebelum menjadi 0 atau lebih).
Charles Duffy
1
Lihat pubs.opengroup.org/onlinepubs/9699919799/utilities/… (dan semua yang digabungkannya dengan referensi) untuk spesifikasi lengkap tentang fnmatch.
Charles Duffy
1
man bash: Operator biner tambahan, = ~, tersedia, dengan prioritas yang sama dengan == dan! =. Ketika digunakan, string di sebelah kanan operator dianggap sebagai ekspresi reguler yang diperluas dan dicocokkan sesuai (seperti dalam regex (3)).
nickl-
1
@aderchox Anda benar, untuk digit Anda dapat menggunakan [0-9]atau[[:digit:]]
nickl-
13

Jika Anda melakukan panggilan berulang dan mementingkan kinerja, Tes ini mengungkapkan metode BASH ~ 15x lebih cepat daripada melakukan forking dan kemungkinan proses eksternal lainnya.

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]
Yosia DeWitt
sumber
1
Jika Anda tertarik dengan cara mengurangi fork, cari kata newConnector di jawaban ini untuk Cara mengatur variabel ke output dari perintah di Bash?
F. Hauri
8

Gunakan [[:digit:]](perhatikan tanda kurung ganda) sebagai polanya:

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

Hanya ingin meringkas jawaban (terutama @ nickl-'s https://stackoverflow.com/a/22261334/2916086 ).

yegeniy
sumber
1

Saya tahu ini adalah utas kuno, tetapi ini adalah hit pertama saya di Google, dan saya ingin membagikan yang berikut resubyang saya kumpulkan, yang menambahkan dukungan untuk beberapa backreferences $ 1, $ 2, dll. Referensi ...

#!/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 ke @Charles Duffy re:(.*)$match(.*)

Dabe Murphy
sumber