Tutorijal - Uklanjanje komentara iz programskog koda
Uvod
Prošlim člankom započeli smo mini-serijal članaka u kojima ćemo diskutovati ("na ovaj ili onaj način"), o različitim tehnikama koje se koriste pri obradi teksta u cilju prepoznavanja delova programskih kodova.
Ovoga puta - umesto da neposredno * nastavimo tamo gde smo stali - ideje o prepoznavanju konteksta pri čitanju tokena, razradićemo kroz implementaciju Python skripte za uklanjanje komentara iz programskog koda. **
Kao što smo već zaključili u uvodnom članku, glavni zadatak tiče se pravilnog prepoznavanja i razdvajanja niski i komentara ....
Osnovna razmatranja
Na kraju prethodnog članka razmotrili smo situaciju u kojoj na prvi pogled deluje da je pronalaženje komentara u programskom kodu trivijalan zadatak koji se može uspešno rešiti preko regularnih izraza, nalik na sledeći izraz (za linijske komentare) ....
.... kao i idejno sličan regularni izraz (za blok komentare) ....
.... međutim, takođe smo se osvrnuli i na druge osobine kodnih tekstova (sa kojima smo bili upoznati i ranije):
- tekst koji inače u kodu predstavlja komentar, može se naći unutar niske
- tekst koji inače u kodu predstavlja nisku, može se naći unutar komentara
.... što dovodi do zaključka da uklanjanje komentara iz programskog koda nije "baš skroz trivijalan" zadatak.
Dovoljno je pogledati sledeći primer ....
.... koji jasno prikazuje da regularni izraz lako može biti "zbunjen", što nadalje znači da bi uklanjanje komentara preko prethodno definisanih regularnih izraza, rezultiralo uklanjanjem (pravih) komentara, ali - takođe i uklanjanjem sadržaja niske s1
(koji iz perspektive regularnih izraza deluju kao komentar), a najveći problem bi bila niska s2
koja, posle uklanjanja dela koda koji "deluje" kao linijski komentar (koji se završava prelaskom u novi red) - više ne bi bila terminisana!
U opštem smislu, redosled pozivanja regularnih izraza je i te kako bitan, međutim - u implementaciji kojom se bavimo - ne postoji redosled koji dovodi do rešenja!
Ako smo ustanovili da je u prethodnom slučaju problem bio u tome što smo preko regularnih izraza prvo tražili komentare - i nismo vodili računa o niskama, jasno je da bi rešenje "moglo biti" da prvo tražimo niske (a tek posle niski komentare), i takav pristup bi u prethodnom primeru proizveo povoljan (i naizgled korektan) rezultat.
Sa druge strane, ako se prepravljenoj skripti preda drugi programski kod ....
.... niska "Niska 1"
će rastaviti prvi blok komentar - tako da više neće biti moguće da se komentar prepozna na korektan način (niske /* ---
i --- */
sada su niske koje će regularni izraz za prepoznavanje blok komentara - preskočiti), a budući da nema parsera koji će "zakrpiti" tri tokena * - delovi komentara ostaju (i pri tom nisu čak ni prepoznati kao komentar)!
Nije teško ('još jednom') doći do zaključka da problem nije u redosledu kojim se regularni izrazi pojavljuju, već - u tome što program ne vodi računa o kontekstu pri čitanju tokena (to jest, o tome da li se token koji se trenutno razmatra, nalazi unutar komentara ili niske, ili izvan navedenih elemenata).
Uklanjanje komentara (uz vođenje računa o kontekstu)
Na ovom mestu vraćamo se na ideju sa kraja uvodnog članka.
Regularne izraze nećemo koristiti za direktno prepoznavanje niski i komentara ("u jednom koraku"), već, samo za prepoznavanje tokena koji označavaju početak i kraj komentara i niski.
Kada lekser podeli ulazni tekst na tokene (videćemo uskoro kako takva podela izgleda u praksi), "štafetu" preuzima jednostavan parser koji ćemo u nastavku implementirati.
U prepoznavanju konteksta pomoći će nam (prilično jednostavna) pravila C-olikih jezika koja se tiču pojave komentara u programskom kodu:
- blok komentari počinju tokenom
/*
i završavaju se tokenom*/
- linijski komentari počinju tokenom
//
i završavaju se prelaskom u sledeći red
.... ali, postoje i pravila koja se tiču drugih tokena:
- niske počinju i završavaju se znakom
"
(mogu naravno počinjati i znakom'
(apostrof), kao i`
(backtick), ali, u ovom članku - zarad jednostavnosti - zadržaćemo se samo na navodnicima) - znak
"
(navodnik), ne može se samostalno pojaviti unutar niske (koja počinje i završava se navodnicima), već isključivo kao deo escape sekvence:\"
- unutar niske, ranije navedeni tokeni koji predstavljaju graničnike komentara - nemaju posebno značenje
Da bi programski kod bio pravilno protumačen, potrebno je uskladiti navedena pravila.
Pravila za tumačenje tokena
U praktičnom smislu, pravilno tumačenje programskog koda podrazumeva prepoznavanje konteksta u kome se određeni token pojavljuje, i davanje prioriteta određenim pravilima - u odnosu na druga pravila (koja u određenom kontekstu imaju manji značaj):
- u osnovnom kontekstu, pojava tokena
"
otvara kontekst čitanja niske, u kome pojava bilo kog tokena osim tokena"
(koji zatvara kontekst čitanja niske) - nema posebno značenje (svi tokeni između dva znaka navoda - pripajaju se niski) - u osnovnom kontekstu, pojava tokena
/*
otvara kontekst čitanja blok komentara, u kome pojava bilo kog tokena osim tokena*/
(koji zatvara kontekst čitanja blok komentara) - nema posebno značenje (svi tokeni između tokena/*
i*/
pripajaju se blok komentaru) - u osnovnom kontekstu, pojava tokena
//
otvara kontekst čitanja linijskog komentara, u kome pojava bilo kog tokena osim tokena\n
(koji zatvara kontekst čitanja linijskog komentara) - nema posebno značenje (svi tokeni između tokena//
i\n
pripajaju se linijskom komentaru)
Osnovni zadatak je: pronaći u tekstu, preko regularnih izraza, sledeće tokene: //
, \n
, /*
, */
, "
, '
(apostrof) i `
(backtick) * - i zatim pravilno protumačiti u kom kontekstu se svaki od navedenih tokena pojavljuje.
U nastavku (budući da se više "ne trudimo da niske i komentare prepoznamo odjednom"), pretvorićemo ulazni tekst u listu tokena u kojoj je svaki element:
- jedan od (gore navedenih) tokena (ili)
- tekst koji se nalazi između dva relevantna tokena iz gore navedene grupe tokena (ili, tekst između početka ili kraja niske - i nekog od relevantnih tokena)
Pogledajmo primer.
Preko novog regularnog izraza ....
.... dobija se sledeća lista (primer programskog koda koji smo prethodno koristili):
.... i preostaje pitanje - kako takvu listu treba protumačiti.
Da bismo bolje razumeli kako treba rešiti zadatak, pogledajmo prvo nekoliko jednostavnijih primera ....
Slika sadrži četiri primera sa naredbama dodele u kojima su izdvojeni i ostali tokeni (operatori, white space znakovi i sl), što smo učinili da bi primeri bili zanimljiviji, ali, princip prepoznavanja niski i komentara se ne menja.
Primer #1
U prvom primeru ....
.... prvo se pojavljuju dva tokena koji ne menjaju kontekst čitanja.
Prvi token od značaja (token /*
), uvodi čitač u režim prepoznavanja komentara ....
.... što znači da će svi tokeni do pojave tokena */
(uključujući i sam token */
), biti prepoznati kao komentar.
U konkretnom primeru, ostatak naredbe je algebarski izraz u kome se pojavljuju: identifikator promenljive, i celobrojna vrednost.
U drugom primeru (koji je idejno sličan prvom), iskoristićemo priliku da se osvrnemo na različite opcije pri obradi prepoznatih tokena (koji čine delove niski ili komentara).
Primer #2
Prvi token od značaja (treći po redu, kao i u prvom primeru) ....
.... uvodi čitač u režim prepoznavanja niski, što znači da će svi tokeni ....
.... do pojave sledećeg tokena "
....
.... biti prepoznati kao deo niske.
Međutim (kao što smo već nagovestili), postoji nekoliko zanimljivih opcija u vezi sa tim kako se mogu rasporediti tokeni koji su prepoznati kao deo niske (ili deo komentara u nekom drugom primeru; ideja je istovetna).
Budući da postoje sve neophodne informacije, moguće je, po potrebi (ili po želji), grupisati tekstualni sadržaj niske u jedan token ....
.... a druga mogućnost podrazumeva smeštanje cele niske (zajedno sa navodnicima), u jedan token:
Postoji i treća opcija (koju smo već videli u primeru #1) - ostaviti tokene u originalnom rasporedu (i upravo tako ćemo i postupiti u skripti koju pišemo).
Naravno, mnogi čitaoci mogu se zapitati - "zašto" (nećemo spajati komentare i niske)?!
Pre svega, treba se setiti čemu služi skripta koju pišemo, a skripta služi - uklanjanju komentara, što znači da će svi delovi ulaznog teksta koji predstavljaju delove komentara - jednostavno biti zanemareni (odnosno, neće biti kopirani u izlaznu datoteku).
Što se tiče niski .... opet se treba setiti toga da će lista tokena, na kraju, ionako biti spojena u jednu nisku (tj. "običan tekst"). :)
Primer #3
Treći primer predstavlja složeniju situaciji, u tom smislu da se među tokenima pojavljuju: i tokeni koji otvaraju/zatvaraju komentare, i tokeni koji otvaraju/zatvaraju niske, a prvi token od značaja, token /*
- uvodi čitač u režim prepoznavanja komentara (što znači da naredne tokene treba tretirati kao deo komentara) ....
Glavno pitanje je, šta će biti sa tokenom "
, koji je prethodno imao poseban značaj ....
.... međutim, pravila su jasna i - u trenutnom kontekstu čitanja - token "
nema poseban značaj, i biće prepoznat (samo) kao deo komentara.
U slučaju kada određeni token izvede čitač iz osnovnog režima (u konkretnom primeru, u pitanju je token /*
), svi tokeni koji inače imaju posebno značenje (u drugim kontekstima), ali - u trenutnom kontekstu nemaju posebno značenje - postaju (praktično) obični tokeni, što znači da je jedini token od značaja - token koji vraća čitač u prethodni (osnovni) * režim.
(U konkretnom primeru, token */
vraća parser u prethodni/osnovni režim.)
Primer #4
Četvrti primer (poslednji po redu), idejno je sličan kao primer #3, ali, situacija je 'obrnuta' (po pitanju pojave tokena /*
ili "
).
Token "
uvodi čitač u režim prepoznavanja niske ....
.... što (ponovo) znači da će određeni tokeni koji u osnovnom režimu imaju specijalno značenje - ali u trenutnom kontekstu nemaju posebno značenje ....
.... biti prepoznati (samo) kao deo niske ....
.... a isto važi i za preostale tokene (sve dok sledeći token "
ne zatvori nisku).
Ostaje da sve tehnikalije koje smo opisali, 'pretočimo' u funkcionalnu skriptu.
Implementacija
Koristićemo ideje koje smo razmotrili na pređašnjim primerima, s tom razlikom što smo u primerima komentare označavali, a u skripti ćemo ih praktično uklanjati (to jest: deo teksta koji je prepoznat kao komentar, jednostavno neće biti kopiran u izlaznu datoteku)
Prvo ćemo precizno definisati opšta pravila za premeštanje tokena.
Pravila za premeštanje tokena
Pravila za premeštanje tokena usklađena su sa prethodno navedenim pravilima preko kojih se definiše smisao pojedinačnih tokena od posebnog značaja i njihovi međusobni odnosi:
-
preko steka, skripta vodi računa o kontekstu:
- kontekst 0 - token koji se čita je van komentara i niski
- kontekst 1 - token je deo linijskog komentara
- kontekst 2 - token je deo blok komentara
- kontekst 3 - token je deo niske
- skripta prolazi redom kroz listu tokena
- tokeni
//
,\n
,/*
,*/
i"
, * menjaju kontekst na steku, ali, svi osim znaka navoda - uklanjaju se iz liste - ostali tokeni prate kontekst:
- ako je kontekst 0 ili 3, token će biti ostavljen
- ako je kontekst 1 ili 2, token će biti uklonjen (odnosno, neće biti kopiran)
Python skripta za uklanjanje komentara iz programskog koda
Ako skripti damo na obradu C kodove koje smo prethodno koristili, ovoga puta biće uklonjene samo niske koje predstavljaju komentar (i neće biti uklonjene niske koje samo "liče" na komentare).
Dodatne ideje za unapređenje skripte
Pošto proširite skriptu (shodno prethodno navedenim uputstvima), verujemo da ćete imati dovoljno sopstvenih ideja za dalje unapređivanje, ali, spomenućemo još jednu 'očiglednu' ideju.
Uklanjanjem linijskog komentara koji nije bio "slepljen" za početak reda (već je bio odvojen jednim ili više razmaka i/ili tabova), ostaće izvestan broj white space znakova, a uklanjanje blok komentara (koji su se prostirali u više redova), ostavlja prazne redove.
Shodno svemu što smo naveli (i kad smo već kod "ostavljanja"), ostavljamo vama - našim cenjenim čitaocima - da samostalno implementirate kodove koji se tiču ideja koje smo naveli (i drugih korisnih ideja koje vama padaju na pamet). :)