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:

  1. 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".
  2. Ha a regkif "|" jelet tartalmaz, akkor a legelső megtalált főrész az első "találat".
  3. A *, +, és ? szerkezetekre illő lehető leghosszabb string-rész a találat.
  4. 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.

Ld. még az ext_reg_expr.txt kiegészítést.