Reguláris kifejezések
Elég elolvasni a normál szöveget. Példák segítik a megértését, így néznek ki.
Mire jó?
Stringek (szövegek) rugalmas megadására, string kereső, helyettesítő programokban.
A reguláris kifejezés egy minta, amire sokszor egynél több konkrét string is "ráillik".
Olyan esetekre ad megoldást, amikor nem tudjuk megnevezni a string összes jelét,
hanem csak a stringet leíró "szabályokat" tudunk mondani.
Pl. a következők valamelyike jellemzi a stringet (zárójelben a megfelelő reguláris kifejezés):
- egy számjegy ([0-9])
- egy szokásos természetes szám ([1-9][0-9]*)
- egy előjeles vagy anélküli egész szám ([+-]?[0-9]+)
- egy <> jelpár, közte akármi, ezt a két jelet kivéve (<[^<>]*>)
- "alma"-val kezdődő, "dio"-ra végződő sor (^alma.*dio$)
- legalább két "-" jel egymás után (---* vagy --+ vagy -{2,})
Hol használjuk Unixban?
Szövegkereső programokban (grep, egrep), szövegszerkesztőkben
(sed, vi, emacs), awk-ban, újabban a bash "[[]]"-es kifejezéseiben is.
Hol használják még?
Egyre több szoftver eszközben. Pl. programozási nyelvekben (c, perl, Java, Javascript, Delphi ...),
adatbázis kezelő rendszerekben (MySQL, Oracle,, ...).
Hol ne használjuk? (sokan megpróbálják)
- A Unix szövegkereső programok fgrep nevű változatában.
- A shell-ben (kivéve a "[[]]" belsejét).
Az alábbi helyeken szokták hibásan használni:
- Fájlnevek megadásánál, mert formailag nagyon hasonlít a "joker" helyettesítés megadására.
- case ágak feltételében, mert még Unix könyv is állítja néha, hogy lehet. (Ott "joker" szabályok érvényesek.)
- A test (vagy [ ] ) parancs feltételében (zh-ban igen gyakori), mert olyan jó lenne, ha működne.
- A find-ban fájlnevek keresésére. (A "joker" szabályok érvényesek.)
Részletes leírás
Egy reguláris kifejezés (regular expression, a továbbiakban sokszor röviden csak regkif) legtágabb értelemben valahány (nulla vagy több)
főrészből áll, amiket egymástól "|" jel választ el. Az a string felel meg neki,
ami a főrészek valamelyikének (legalább egynek) megfelel.
Az "alma|dio" regkif-nek az alma vagy dio szöveg valamelyike felel meg.
Megkülönböztetünk alap (basic) és kiterjesztett (extended) reguláris kifejezéseket.
A kiterjesztettet jelenleg az általunk használt programok közül az egrep és az awk "tudja", de (-r opcióval) az újabb sed-ek is (a kör bővülhet idővel).
Így írom azt, ami csak a kiterjesztett reguláris kifejezésekben szerepelhet.
Az alap reguláris kifejezés nulla vagy egy főrészből áll, tehát a "|" jel csak a kiterjesztett regkif-ekben használható (részek elválasztására).
Egy főrész részek konkatenációja (egymás után írása). Az a string felel meg
neki, aminek az egymást követő részei rendre megfelelnek a regkif részeinek.
A rész egy atom, amit esetleg a "*", "+" vagy "?" jelek egyike vagy egyéb "ismétlési tényező" követ.
Ezeknek rendre a következő stringek felelnek meg:
|
atom* |
Az atom egymásutáni 0, 1, 2, ... előfordulása
A "0*" regkif-nek az üres string vagy egy akárhány nullából álló jelsorozat felel meg.
A " *" regkif-nek (a * előtt 2 helyköz van) egy vagy több egymásutáni helyköz felel meg.
A "*.*" nem értelmezhető regkif-ként.
|
|
atom+ |
Az atom egymásutáni 1, 2, ... előfordulása
A " +" regkif-nek egy vagy több egymásutáni helyköz felel meg.
|
|
atom? |
Az atom egymásutáni 0 vagy 1 előfordulása
A "-?28" regkif-nek a "28" és a "-28" stringek felelnek meg.
A "string-?helyettesítő" regkif-nek a "stringhelyettesítő" és a "string-helyettesítő" sztringek felelnek meg.
| |
atom{m,n} |
Az atom egymásutáni, legalább m-szeres, legfeljebb n-szeres előfordulása.
Ha m=n, akkor elég "{m}"-et írni. Ha "n" nincs megadva (de a vessző igen), akkor "végtelen".
(Sajnos) a dolog nem egységes, előfordulnak olyan regkif megvalósítások, amikben a "{" és "}" helyett a "\{" és "\}" párokat kell használni.
Az "[12]-[0-9]{6}-[0-9]{4}" regkif-nek minden "személyi szám" megfelel.
|
Az atom a következők valamelyike lehet:
- Egy zárójelbe tett reguláris kifejezés. Az a string felel meg neki,
ami a reguláris kifejezésnek megfelel.
Az "(alma|dió)fa" reguláris kifejezésnek az "almafa" és a "diófa" string felel meg.
- Egy tartomány (lásd alább).
- Egy "." karakter. Egyetlen akármilyen jel felel meg neki.
A "..." regkif-nek bármilyen 3 egymásutáni jel megfelel.
- Egy "^" karakter a regkif elején. Egy sor elején levő üres string felel meg neki.
A "^." regkif-nek minden olyan sor (eleje) megfelel, amiben legalább egy jel van.
- Egy "$" karakter a regkif végén. Egy sor végén levő üres string felel meg neki.
A " $" regkif-nek minden helyközre végződő sor (vége) megfelel.
- Egy "\" jelet követő egyetlen karakter. Az "egyetlen karakter" felel meg neki.
A ".*\..*" regkif-nek minden olyan string megfelel, amiben van pont.
A "^$.*^.*\$$" regkif-nek minden olyan sor megfelel, ami $ jellel kezdődik, $ jelre végződik, és van benne ^ jel.
A "\-1\.0" regkif-nek a "-1.0" string felel meg. Az első \ jel pl. a grep keresőprogramban kell,
azért, hogy a parancs a regkif-et ne parancs opciónak nézze.
- Egyetlen egyéb karakter. Saját maga (mint string) felel meg neki.
Tehát bármilyen közönséges string (ami nem tartalmaz regkif szempontból speciális jelet) szintén reguláris kifejezés.
A tartomány ("range") egy szögletes zárójelek közt álló jelsorozat.
- Alapesetben a jelsorozat valamelyik (egyetlen) jele felel meg neki.
A "[aáeéiíoóöőuúüű]" regkif-nek egy kisbetűs magánhangzó felel meg.
- Ha a jelsorozat első jele a "^" jel, akkor minden, a jelsorozat
folytatásában nem szereplő (egyetlen) jel megfelel neki (vagyis ez egy negálás).
A "[^0123456789]" regkif-nek minden egyes jel megfelel, a számjegyeket kivéve.
- Két jel közé tett "-" jellel a két jel által képviselt ASCII
intervallumot írhatjuk rövidebben.
A "[^0-9]" ugyanaz, mint az előző.
- Ha a "]" jel szerepel a jelsorozatban, akkor az első helyen (ill. negálás esetén
a "^" jel mögött a 2. helyen) kell állnia. (Ekkor nem számít a tartomány
végét jelző zárójelnek.)
A "[])}]" reguláris kifejezésnek a háromféle bezárójel bármelyike megfelel.
- Ha a "-" jel maga szerepel a jelsorozatban, akkor az első helyen vagy az utolsó
helyen kell állnia. (Ekkor nem számít ASCII intervallum középső jelének.)
A "[-+]" és "[+-]" regkif-eknek egy előjel felel meg.
Az "[A-Za-z_-]" regkif-nek egy betű (az angol ABC-ből), vagy egy mínuszjel, vagy egy aláhúzásjel felel meg.
- "[]" belsejében elvesztik speciális jelentésüket az egyéb, a regkif-ben másutt speciális jelek.
A "[.*]" reguláris kifejezésnek (csak) egy pont vagy egy csillag felel meg.
Kijelölés:
Egy reguláris kifejezésnek egy "regkif1" részét "\(regkif1\)" formában írhatjuk abból a célból, hogy aztán hivatkozzunk rá.
A hivatkozás "\1", "\2", ... formában történhet az 1., 2., ... ilyen formában kijelölt regkif-re. A hivatkozás nem rövidített írásmód, hanem azt jelzi, hogy a két reguláris kifejezésnek
(a hivatkozásra kijelöltnek és a hivatkozásnak) azonos string felel meg.
(Sajnos) ez sem egységes, előfordulnak olyan regkif megvalósítások, amikben a "\(" és "\)" helyett a "(" és ")" irandó.
A "^\([0-9]\).*\1$" regkif-nek a számjeggyel kezdődő, és ugyanarra a számjegyre végződő sorok felelnek meg.
A "^\(.*\)\1$" regkif-nek azok a sorok felelnek meg, amik két egyező (esetleg üres) stringre bonthatók.
Gyakori az, hogy sed és vi string-helyettesítő parancsaiban a helyettesítendő stringben kijelölt kifejezésre a helyettesítő stringben hivatkozunk.
Az input minden sorának az első jelét a sor végére teszi át a sed "s/^\(.\)\(.*\)$/\2\1/" parancs. (A "^" és "$" jelek - az alább leírtak alapján - feleslegesek a parancsban.)
Ha egy regkif-nek több, ugyanott kezdődő string is megfelel, akkor helyettesítésnél nem mindegy, hogy melyik szövegrész lesz helyettesítve.
Pl. az "abba" stringre minden jeltől kezdve ráillik az "a*b*" reguláris kifejezés, ezen belül az elején levő 1-2-3 hosszú részek mindegyikére.
Az "első, azon belül a leghosszabb" szabály érvényes ilyenkor.
Ami azt jelenti, hogy a példában a string elején levő "abb" a megtalált rész (röviden: "a találat").
Részletesebben mondva: az alábbi szabályokat kell alkalmazni, a felírt sorrendben:
- Ha egy regkif a string két különböző helyen kezdődő részének is megfelel, akkor
az előbb (a másiktól balra) kezdődő rész az első "találat".
- Ha a regkif "|" jelet tartalmaz, akkor a legelső megtalált
főrész az első "találat".
- A *, +, és ? szerkezetekre illő lehető leghosszabb string-rész a találat.
- Nincsenek "átfedő", vagy menet közben keletkező találatok.
Pl. echo ababa | sed "s/aba/ABa/g" eredménye "ABaba".
Megjegyzések:
- Esetenként még ennél is több lehetőséget lehet használni a reguláris kifejezésekben. Ld. pl. "man egrep".
- A reguláris kifejezés igen hatékony programozói eszköz, de elég csunyán néz ki.
Sokszor el is bonyolítjuk. A pontot tartalmazó sorok keresésére elég a grep-nek a "\."-ot,
mint reguláris kifejezést megadni, az ugyancsak jó, de feleslegesen hosszú "^.*\..*$" helyett. Még jobb az fgrep-el "."-ot keresni.
- Shell scriptben a reguláris kifejezéseket mindig tegyük macskakörmök közé. Ezzel megakadályozzuk, hogy a shell joker (wildcard) helyettesítéseket végezve elrontsa őket,
mielőtt eljutnak ahhoz a programhoz, ami használja őket. Az esetleg szükséges környezetváltozó helyettesítéseket ez nem akadályozza.
Bonyolultabb példák:
- A következő reguláris kifejezésnek csak az "óó:pp:mm" formájú érvényes időpontok felelnek meg, ahol "óó" az óra (00-23), "pp" ill. "mm" a perc
ill. másodperc (00-59). A reguláris kifejezéseket mindig tegyük idézőjelbe, hogy a shell ne tegyen kárt bennük.
"^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$"
- A következő reguláris kifejezésnek csak a "hhnn" formájú érvényes dátumok felelnek meg, ahol "hh" a hónap, "nn" a nap.
"^((0[13578]|1[02])(0[1-9]|[12][0-9]|3[01]))
|((0[469]|11)(0[1-9]|[12][0-9]|30))
|02(0[1-9]|1[0-9]|2[0-8])$"
A kifejezést három sorra bontottam, a valóságban ezeket egyetlen sorba kell tenni, szorosan egymás mellé. Az első sor írja le a 31-es hónapok napjait, a második a 30-asokéit, a 3. a februárt (szökőnap nélkül).
A kifejezés (grep-el nem, csak egrep-pel) működik, és elég ijesztő. Az ilyen regkif-eket talán könnyebb felírni, mint utólag megérteni.
Áttekinthetőbb, könnyebben megérthető egy olyan - kisebb regkif-eket tartalmazó - megoldás, mint az alábbi:
grep "^[01][0-9][0-3][0-9]$" |\
grep -v "^00" | grep -v "00$" | grep -v "^1[3-9]" |\
grep -v "3[2-9]$" | grep -v "[469]31$" | grep -v "1131$" |\
grep -v "023.$" | grep -v "0229$"
Az első grep átenged minden "0000" és "1939" közötti, négy számjegyből álló számot.
A 2. sor kihagyja a "00" hónapot és napot, valamint a 13-19. hónapokat.
A 3. sor kihagyja a 32-39. napokat, valamint a 30-as hónapokból a 31-edikét.
A 4. sor kihagyja a februárnak a 28-a utáni napjait. (Szökőnapot ez a megoldás sem ismer!)
- A következő szűrő csak számokat enged át. Amik egy opcionális mínusz előjelből, egészrészből és opcionális törtrészből állnak,
ahol a tizedesvessző helyett pont is állhat, és ha van tizedesrész, az egy vagy kétjegyű.
A script az esetleges értéktelen vezető nullákat elhagyja, a tizedes pontot vesszővel helyettesíti, a szám egészrészét pedig hármasával ponttal tagolja.
sed "s/\(-*\)0*\([0-9]\)/\1\2/" |\
egrep "^-?[0-9]+([.,][0-9]+)*$" | sed "s/\./,/" |\
rev | sed "s/\([0-9][0-9][0-9]\)/\1./g" |\
sed "s/\.$//" | sed "s/\.-$/-/" | rev
Az első sor hagyja el az értéktelen vezető nullákat az egészrészből.
A másodikban az egrep csak azt engedi át, amit kell, utána a sed a tizedespontot vesszőre cseréli (ha van).
Mivel az egészrészt a végétől kezdve kell hármasával tagolni, a sed viszont balról jobbra tud tagolni, a 3. sorban a sed a rev-el megfordított számot egészíti ki három jegyenként egy-egy ponttal.
Ez a sor rev | sed "s/[0-9][0-9][0-9]/&./g" |\
formában is írható, mert a sed az "&" jel helyére a megtalált stringet teszi (ez egy sed szabály).
Az utolsó sor az egészrész végére esetleg feleslegesen tett pontot veszi le, majd visszafordítja a számot.
Rövidebben így is írható: sed "s/\.\(-*\)$/\1/" | rev
- Az alábbi sed program az inputban (pl. html szövegben) a < > "kacsacsőrök" belsejében levő jeleket pontokkal helyettesíti.
sed "/<\.*[^>.<][^><]*>/ { :cimke
s/\(<\.*\)[^>.<]\([^><]*>\)/\1.\2/
/<\.*[^>.<][^><]*>/b cimke
}"
Ha a feldolgozott sor megfelel az első sorban (a "//" jelpár között levő) levő regkif-nek (vagyis a kacsacsőrben van ponttól különböző jel),
akkor végrehajtódik a kapcsos-zárójelben levő sed-program. A 2. sorban az első ilyen jelet ponttal helyettesíti a sed,
aztán (a 3. sorban) ismét keresi ugyanazt, amit az elején, és ha van, akkor visszaugrik (b) a "cimke"-re, vagyis ciklusban addig végzi a
helyettesítést, amíg a feldolgozott sorban levő egyetlen kacsacsőrben sem marad ponttól különböző jel.
Azért (is) ilyen bonyolult, mert a lezáró ">" jel nélküli "<" jelek mögötti részt nem szabad kipontozni.
Alább ennek az az eredeti változata látható, amire a gyakorlatban szükségem volt. Ebben "[]" zárójelpár belsejét kell kipontozni.
A script a megfelelő helyettesítésekkel az előzőből kapható, annyi kiegészítéssel, hogy az önmaga helyett álló "[" jelet le kell védeni ("\" jellel).
Itt már nem mindegy, hogy a(z önmaga helyett álló) "]" jel hol áll a szögletes zárójeleken belül. Ránézésre ez már egészen ijesztő (de működik).
sed "/\[\.*[^].[][^][]*]/ { :cimke
s/\(\[\.*\)[^].[]\([^][]*]\)/\1.\2/
/\[\.*[^].[][^][]*]/b cimke
}"
Ha az utóbbi scriptet saját magára (mint adatra) alkalmazzuk, ezt kapjuk:
sed "/\[\.*[.].[][.][]*]/ { :cimke
s/\(\[\.*\)[.].[]\([.][]*]\)/\1.\2/
/\[\.*[.].[][.][]*]/b cimke
}"
Ha az eredményből kell kitalálni, hogy milyen script csinálta, a következőre is lehetne gondolni:
sed "s/\^/./g" $1
Ágyuval sikerült verebet lőni.
|
|