Υπάρχουν περιπτώσεις που μια αφηρημένη περιγραφή είναι εξαιρετικά πολύτιμη. Αν σας κάνει εντύπωση η πρόταση που μόλις διαβάσατε, ίσως είναι επειδή πιστεύετε ότι αφαίρεση σημαίνει ασάφεια. Όμως αυτό είναι εντελώς λάθος. Για ν’ αποδείξουμε τον ισχυρισμό μας, σας προσκαλούμε σε ένα ταξίδι στον αφηρημένα θαυμαστό κόσμο των regular expressions. Μη γελιέστε, δεν πρόκειται για ένα αδιάφορο ταξίδι αναψυχής. Αν μάλιστα παίρνετε έστω και λίγο στα σοβαρά την επιστήμη της Πληροφορικής, τότε δεν πρόκειται να το αποφύγετε.

Διαβάζουμε το man page ενός προγράμματος και μαθαίνουμε ότι υποστηρίζει regular expressions. Μελετάμε τα αρχεία ρυθμίσεων μιας δικτυακής υπηρεσίας και βλέπουμε ότι μπορούμε να περιγράφουμε ολόκληρα σύνολα από domain names ή διευθύνσεις IP, με τη βοήθεια των regular expressions. Αποφασίζουμε να γράψουμε ένα πρόγραμμα σε Python, σε PHP, σε Perl ή σε κάποια άλλη γλώσσα, και συνειδητοποιούμε ότι μπορούμε να ελέγξουμε την εγκυρότητα του user input χρησιμοποιώντας regular expressions. Τι είναι επιτέλους αυτές οι κανονικές εκφράσεις ή αλλιώς regular expressions; Η μορφή τους, πάντως, μόνο σε κάτι το κανονικό δεν παραπέμπει, ενώ το συντακτικό τους μοιάζει απίστευτα περίπλοκο. Μολαταύτα, όλα τα εργαλεία που φημίζονται για την ευελιξία και την ισχύ που παρέχουν στο χρήστη, υποστηρίζουν τις κανονικές εκφράσεις και τις αξιοποιούν κατά κόρον.

Φύση και αποστολή

Μια κανονική έκφραση ή ένα regular expression, όπως συνήθως λέμε, αποτελεί μια αλληλουχία χαρακτήρων με την οποία περιγράφουμε τη γενική μορφή μιας άλλης αλληλουχίας χαρακτήρων.

Στο εξής θα αναφέρουμε τις αλληλουχίες χαρακτήρων ως character strings ή απλά strings.

Με το κατάλληλο regular expression μπορούμε να περιγράψουμε τη μορφή μιας διεύθυνσης email, τη μορφή μιας ημερομηνίας ή κάτι αρκετά πιο γενικό κι ενδεχομένως πιο περίπλοκο. Παράδειγμα: μία γραμμή που περιλαμβάνει τις λέξεις “Failed” και “user”, ενώ μετά από αυτές (κι όχι νωρίτερα) περιλαμβάνει μια διεύθυνση IP η οποία όμως διαφέρει από τη διεύθυνση του δικού μας μηχανήματος. Πώς άραγε θα αναζητούσατε μια γραμμή που ικανοποιεί αυτούς τους περιορισμούς;

Όπως αντιλαμβάνεστε, με τη βοήθεια των regular expressions μπορούσε να πραγματοποιούμε οσοδήποτε αφηρημένες αναζητήσεις. Με άλλα λόγια, έχουμε τη δυνατότητα ν’ αναζητάμε character strings για τα οποία γνωρίζουμε μόνο τη γενική τους μορφή κι όχι το ακριβές τους περιεχόμενο. Σκεφτείτε την περίπτωση που θέλουμε να εντοπίσουμε τις διευθύνσεις email μέσα σε ένα αρχείο, τους δεκαδικούς αριθμούς (όσους περιλαμβάνουν το σημείο της υποδιαστολής) ή τις διπλές λέξεις σε κάποιο κείμενο (όσες εμφανίζονται δύο φορές διαδοχικά). Ας σταθούμε λίγο στο πρώτο παράδειγμα. Αναζητώντας τα strings που έχουν τη μορφή μιας διεύθυνσης email, μπορούμε να εξάγουμε εύκολα όλες τις διευθύνσεις που περιλαμβάνει ένα αρχείο (π.χ., η βάση δεδομένων ενός mail client). Εναλλακτικά, θα μπορούσαμε να αναζητήσουμε strings που έχουν τη μορφή μιας διεύθυνσης email, μέσα στο κείμενο που εισάγει ο χρήστης στο πεδίο email μιας δικτυακής φόρμας. Από την έκβαση της αναζήτησης (από το αν δηλαδή θα επιστρέψει κάποιο αποτέλεσμα ή όχι) μπορούμε να τσεκάρουμε αν ο χρήστης πράγματι εισήγαγε μια διεύθυνση email ή όχι. Τελικά, με τη βοήθεια των regular expressions μπορούμε να πραγματοποιούμε περίπλοκες αναζητήσεις αλλά να κάνουμε κι αυτό που ονομάζεται sanity check.

Το sanity check δεν έχει καμία σχέση με το input sanitization. Το πρώτο αφορά στον έλεγχο της ορθότητας των δεδομένων που εισάγει ο χρήστης σε κάποιο πρόγραμμα. Το δεύτερο αποτελεί μια πιο δραστική διαδικασία, κατά την οποία τα δεδομένα που εισάγει ο χρήστης προσαρμόζονται με το ζόρι σε κάποιους κανόνες.

Βρίσκονται (σχεδόν) παντού

Τα regular expressions χρησιμοποιούνται εκτενώς σε πολλά εργαλεία της γραμμής εντολών του Linux. Το πιο γνωστό εξ αυτών είναι το grep, του οποίου το όνομα προκύπτει από την περιγραφή της κύριας λειτουργίας του: generalized regular expression processor. Όπως θα γνωρίζετε, με το grep μπορούμε να εκτελούμε οσοδήποτε πολύπλοκες αναζητήσεις – και μάλιστα με μεγάλη ταχύτητα. Οι κανονικές εκφράσεις χρησιμοποιούνται και σε άλλα διάσημα εργαλεία της γραμμής εντολών, όπως τα sed και awk. Τις συναντάμε επίσης και στα αρχεία ρυθμίσεων διαφόρων δικτυακών υπηρεσιών, όπου χρησιμοποιούνται σε κανόνες του τύπου “αντιμετώπισε με την τάδε ενέργεια εκείνες τις διευθύνσεις IP ή τα domains που παρουσιάζουν αυτή τη μορφή”.

Το κέλυφος BASH, όπως και πολλά άλλα shells, υποστηρίζει ορισμένους χαρακτήρες-μπαλαντέρ. Αυτοί οι χαρακτήρες (αστερίσκος, ερωτηματικό, αγκύλες κ.ά.) χρησιμοποιούνται συνήθως για την περιγραφή ονομάτων αρχείων και το συντακτικό τους μοιάζει με εκείνο των regular expressions. Ωστόσο, πέρα από την όποια επιφανειακή ομοιότητα, αυτοί οι χαρακτήρες δεν έχουν καμία σχέση με τις κανονικές εκφράσεις. Είναι λοιπόν λάθος να θεωρούμε ότι το BASH μπορεί να χειρίζεται regular expressions.

Μηχανές κανονικών εκφράσεων

Υπάρχουν προγράμματα που δέχονται ως είσοδο μια κανονική έκφραση κι ένα character string. Τα εν λόγω προγράμματα αναλύουν τη δοθείσα κανονική έκφραση και ελέγχουν αν κάτι από ό,τι περιγράφει συναντάται μέσα στο character string που δώσαμε. Αυτά τα προγράμματα ονομάζονται μηχανές κανονικών εκφράσεων (regular expression engines) κι αποτελούν υποσυστήματα άλλων προγραμμάτων. Ο χρήστης δεν έχει ποτέ άμεση πρόσβαση στην εκάστοτε μηχανή κανονικών εκφράσεων κι ο χειρισμός της πραγματοποιείται από το πρόγραμμα που την ενσωματώνει.

Δυστυχώς, οι μηχανές κανονικών εκφράσεων δεν είναι απόλυτα συμβατές μεταξύ τους. Συνεπώς, μία έκφραση που δεν περιέχει συντακτικά σφάλματα για μία συγκεκριμένη μηχανή, ενδέχεται να μη λειτουργεί καθόλου σε κάποια άλλη μηχανή. Εν ολίγοις, υπάρχουν αρκετές διάλεκτοι για τις κανονικές εκφράσεις. Τα πράγματα περιπλέκονται ακόμα περισσότερο αν λάβουμε υπόψη ότι οι διάφορες διάλεκτοι δεν είναι ισοδύναμες. Αυτό σημαίνει ότι ορισμένες διαθέτουν μεγαλύτερη περιγραφική δύναμη από κάποιες άλλες – και γι’ αυτό θα μπορούσαμε να τις χαρακτηρίσουμε ως πιο ισχυρές. Οι ισχυρές διάλεκτοι επιτρέπουν στο χρήστη να περιγράφει πιο σύνθετα strings απ’ ό,τι οι εκφράσεις μιας ασθενέστερης διαλέκτου, ή έστω strings της ίδιας πολυπλοκότητας αλλά με ευκολότερο τρόπο.

Τα εργαλεία που ανήκουν στο GNU project και τα οποία συνοδεύουν κάθε διανομή Linux υποστηρίζουν δύο διαλέκτους κανονικών εκφράσεων, οι οποίες, παρεμπιπτόντως, περιγράφονται στο πρότυπο POSIX: Την παλιά και σχετικά απλή διάλεκτο που ονομάζεται BRE (Basic Regular Expressions), όπως επίσης την πιο πρόσφατη κι εξελιγμένη που ονομάζεται ERE (Extended Regular Expressions). Όπως αντιλαμβάνεστε, η υποστήριξη αυτών των διαλέκτων από τα βασικά εργαλεία του Linux, τους προσδίδει ιδιαίτερη αξία. Μια άλλη διάλεκτος με ξεχωριστό ενδιαφέρον, η οποία μάλιστα θεωρείται κι ως η πιο ισχυρή, είναι εκείνη που ορίζεται στη γλώσσα Perl (έκδοση 5). Σε αυτή τη σειρά άρθρων θα γνωρίσουμε το συντακτικό των κανονικών εκφράσεων όπως ορίζεται στη διάλεκτο ERE. Επιπρόσθετα, θα αναφέρουμε κι ορισμένες ενδιαφέρουσες δυνατότητες που εισάγει η διάλεκτος της Perl. Σημειώστε ότι το grep υποστηρίζει εξ ορισμού τη διάλεκτο BRE, αλλά με την παράμετρο (--extended-regexp) υποστηρίζει την ERE. Επίσης, με την παράμετρο -P (--perl-regexp) υποστηρίζει εκείνη της Perl.

Κατασκευή κανονικών εκφράσεων

Έφτασε η ώρα να μάθουμε πώς κατασκευάζονται οι κανονικές εκφράσεις. Ουσιαστικά θα γνωρίσουμε τα στοιχεία που μπορούν να συμμετέχουν σε μια κανονική έκφραση, την ιδιαίτερη ερμηνεία καθενός, καθώς και πώς τα συνδυάζουμε. Σε αυτή την ενότητα θα χρησιμοποιήσουμε πολλά παραδείγματα, για τα οποία θα στηριχτούμε στο grep. Πριν ξεκινήσουμε, σημειώστε ότι το grep απαιτεί να περικλείουμε τις κανονικές εκφράσεις σε εισαγωγικά. Επιπρόσθετα, έχετε υπόψη ότι στο σύστημα που εργαστήκαμε είχαμε ορίσει το ακόλουθο alias:

alias grep='grep –E --color=auto'

Όπως έχουμε πει, με την πρώτη παράμετρο ενεργοποιείται η υποστήριξη της διαλέκτου ERE (Extended Regular Expressions). Με τη δεύτερη παράμετρο το grep θα χρωματίζει αυτόματα όσα strings εντοπίζει. Έτσι, θα αντιλαμβανόμαστε γρηγορότερα την ακριβή ερμηνεία της εκάστοτε κανονικής έκφρασης.

Η απλούστερη κανονική έκφραση περιλαμβάνει ένα ή περισσότερα γράμματα στη σειρά και περιγράφει ένα string που περιλαμβάνει ακριβώς αυτά τα γράμματα, με τη σειρά που τα έχουμε γράψει.

grep "na" fruits.txt

Banana

Pomegranate

To grep εξέτασε τα περιεχόμενα του αρχείου fruits.txt (μια απλή λίστα με φρούτα) κι εμφάνισε τις γραμμές που περιέχουν το string na. Επιπρόσθετα, χρωμάτισε όλες τις εμφανίσεις του ζητούμενου string (αντί για χρώμα, εδώ είναι bold). Δείτε κι αυτό το παράδειγμα:

grep "p" fruits.txt

Apple

Apricot

Grape

Papaya

Pineapple

Raspberry

Αυτό το παράδειγμα μπορεί να μοιάζει περιττό, αλλά δεν είναι. Μας διδάσκει κάτι κρίσιμο: Οι κανονικές εκφράσεις είναι case sensitive. Παρατηρείστε ότι το grep επισήμανε μόνο τα πεζά p κι όχι τα κεφαλαία.

Σε μια κανονική έκφραση μπορούμε να συμπεριλάβουμε οσοδήποτε μικρές ή μεγάλες λέξεις. Η κανονική έκφραση θα περιγράφει ακριβώς αυτές τις λέξεις, με τον τρόπο που τις έχουμε γράψει. Όμως, η μεγάλη δύναμη των κανονικών εκφράσεων έγκειται στην ικανότητά τους να περιγράφουν τη μορφή ενός string χωρίς να ενσωματώνουν όλες τις πιθανές παραλλαγές του. Αυτό προϋποθέτει ότι ορισμένοι χαρακτήρες δεν ερμηνεύονται κυριολεκτικά (literally). Οι χαρακτήρες αυτού του είδους ονομάζονται ειδικοί (special characters) και η παρουσία καθενός συνδέεται με κάποιον μηχανισμό των κανονικών εκφράσεων. Στη συνέχεια θα γνωρίσουμε τις κλάσεις χαρακτήρων, το χαρακτήρα μπαλαντέρ και τους χαρακτήρες επανάληψης.

Κλάσεις χαρακτήρων

Υποθέστε ότι αναζητάμε οποιοδήποτε από τα γράμματα d, P, o, A και s. Αν τα αραδιάζαμε στη σειρά, το grep θα προσπαθούσε να βρει το string dPoAs και, όπως καταλαβαίνετε, θα έψαχνε κάτι εντελώς διαφορετικό από αυτό που θέλουμε. Γι’ αυτές τις περιπτώσεις προσφέρονται οι λεγόμενες κλάσεις χαρακτήρων (character classes). Πρόκειται για σύνολα χαρακτήρων που μπορούμε να ορίζουμε αυθαίρετα, εισάγοντας τους επιθυμητούς χαρακτήρες μέσα σε αγκύλες. Παράδειγμα:

grep "e[ar]" fruits.txt

Blackberry

Cherry

Gooseberry

Tangerine

Peach

Pineapple

Raspberry

Η κανονική έκφραση του παραπάνω παραδείγματος περιγράφει ένα string των δύο χαρακτήρων. Ο πρώτος χαρακτήρας είναι το e, ενώ ο δεύτερος χαρακτήρας μπορεί να είναι ένας από τους a και r. Για λόγους ευκολίας, σε μία κλάση μπορούμε να ορίσουμε και περιοχές χαρακτήρων (character ranges). Δείτε ένα παράδειγμα και θα καταλάβετε αμέσως:

grep "[0-9]" test.txt

An addition: 1+2+3+4+5=15

Some numbers: 21, 418, 63, 271, 93, 865, 49, 17

Αυτή η κανονική έκφραση περιγράφει ένα string του ενός χαρακτήρα, ο οποίος μπορεί να είναι οποιοσδήποτε μεταξύ των 0, 1, …, 9. Με αυτόν τον τρόπο περιγράφουμε τα αριθμητικά ψηφία χωρίς να τα πληκτρολογήσουμε όλα. Δείτε κι ένα πιο σύνθετο παράδειγμα:

grep "0[xX][A-Fa-f0-9][A-Fa-f0-9]" code.asm

.equ parameters = 0xa7

.equ phase_delta = 0x63

.equ duration = 0x6d

ldi temp, 0xFF

andi counter, 0xD5

Αυτή η κανονική έκφραση περιγράφει ένα string τεσσάρων χαρακτήρων. Ο πρώτος χαρακτήρας είναι ο 0, ενώ για καθέναν από τους άλλους ορίζεται μία αντίστοιχη κλάση χαρακτήρων. Η πρώτη κλάση περιλαμβάνει τους χαρακτήρες x και X, ενώ η δεύτερη και η τρίτη κλάση περιλαμβάνουν όλα τα κεφαλαία γράμματα από το A ως και το F, όλα τα πεζά από το a ως και το f, καθώς κι όλα τα αριθμητικά ψηφία. Τελικά, η παραπάνω κανονική έκφραση περιγράφει τη μορφή με την οποία αναπαρίστανται οι (διψήφιοι) δεκαεξαδικοί αριθμοί στις περισσότερες γλώσσες προγραμματισμού.

Σε μια κλάση μπορούμε να συμπεριλάβουμε οποιονδήποτε χαρακτήρα – ακόμη και τις αγκύλες. Η δεξιά αγκύλη, όμως, πρέπει να τοποθετείται πάντα πριν από κάθε άλλο μέλος της κλάσης. Προσέξτε το ακόλουθο παράδειγμα:

grep "[]t]" test.txt

[this] [is] [a] [test] [for] bracket-matching

Εδώ ορίζουμε μια κλάση που περιλαμβάνει τη δεξιά αγκύλη και τον χαρακτήρα t. Η συγκεκριμένη κανονική έκφραση περιγράφει ένα string του ενός χαρακτήρα, ο οποίος μπορεί να είναι είτε η δεξιά αγκύλη είτε το t. Δείτε τώρα κι αυτό:

grep "[t]]" test.txt

[this] [is] [a] [test] [for] bracket-matching

Η δεξιά αγκύλη δεν έχει τοποθετηθεί σωστά μέσα στην κλάση χαρακτήρων. Έτσι, η συγκεκριμένη κανονική έκφραση περιγράφει κάτι διαφορετικό από αυτό που θέλαμε: ένα string δύο χαρακτήρων. Ο πρώτος ανήκει στην κλάση με μοναδικό στοιχείο το t κι ο δεύτερος είναι εκείνος της δεξιάς αγκύλης. Όπως βλέπετε, το αποτέλεσμα αυτής της αναζήτησης είναι εντελώς διαφορετικό.

Ας υποθέσουμε τώρα ότι αναζητάμε όλους τους χαρακτήρες εκτός από τα αριθμητικά ψηφία. Για το σκοπό αυτό χρειαζόμαστε μια κλάση η οποία θα περιλαμβάνει όλους τους χαρακτήρες, εκτός από τα αριθμητικά ψηφία. Όπως καταλαβαίνετε, ο ορισμός μιας τέτοιας κλάσης απαιτεί αρκετή πληκτρολόγηση. Έτσι δεν είναι; Ε, λοιπόν, με τη βοήθεια της άρνησης ή αλλιώς αντιστροφής (negation), μπορούμε να ορίσουμε αυτήν τη κλάση πανεύκολα:

grep "[^0-9]" document.txt

In the late 1950s and 1960s, American Airlines and I.B.M. teamed up to develop the Sabre computerized reservations system.

Παρατηρείστε το χαρακτήρα του εκθέτη (^, ονομάζεται caret) που έχουμε τοποθετήσει αμέσως μετά την αριστερή αγκύλη. Αυτός ο χαρακτήρας αντιστρέφει το περιεχόμενο της κλάσης – ή αλλιώς ορίζει τη συμπληρωματική κλάση, όπως θα έλεγε ένας μαθηματικός. Έτσι, αντί η κλάση να περιλαμβάνει όλα τα αριθμητικά ψηφία, περιλαμβάνει όλους τους χαρακτήρες εκτός από τα αριθμητικά ψηφία. Τελικά, η κανονική έκφραση του παραδείγματος ορίζει ένα string του ενός χαρακτήρα, ο οποίος μπορεί να είναι οποιοσδήποτε εκτός από κάποιο ψηφίο. Κι όπως βλέπετε από το αποτέλεσμα του παραδείγματος, το grep βρήκε όλα τα σχετικά strings (χαρακτήρες).

Απενεργοποίηση

Πριν προχωρήσουμε οφείλουμε να σταθούμε σε μια λεπτομέρεια. Είδαμε ότι οι χαρακτήρες [, ] και ^ επιδέχονται ειδικής ερμηνείας. Προκύπτει λοιπόν το ακόλουθο ερώτημα: Πώς θα μπορούσαμε να τους χρησιμοποιήσουμε με την κυριολεκτική τους έννοια; Για να καταργήσουμε την ειδική ερμηνεία ενός ειδικού χαρακτήρα κάνουμε το λεγόμενο escaping. Αυτό επιτυγχάνεται τοποθετώντας πριν από τον ειδικό χαρακτήρα τον (ακόμα πιο ειδικό) χαρακτήρα \ (backslash). Δείτε ένα παράδειγμα:

grep "\[t" test.txt

Yep, [this] is yet another [test] [for] bracket-matching

Η παραπάνω κανονική έκφραση περιγράφει ένα string δύο χαρακτήρων. Ο πρώτος είναι πάντα η αριστερή αγκύλη, ενώ ο δεύτερος είναι πάντα το γράμμα t. Για την κυριολεκτική χρήση της αριστερής αγκύλης (κι όχι για την έναρξη μιας κλάσης) τοποθετήσαμε το backslash αμέσως πριν τον αντίστοιχο χαρακτήρα. Το ίδιο θα μπορούσαμε να κάνουμε και με τον χαρακτήρα του εκθέτη, όπως και με όλους τους ειδικούς χαρακτήρες που θα δούμε στη συνέχεια.

Εδώ χρειάζεται λίγη προσοχή: Μέσα στις κλάσεις χαρακτήρων, οι περισσότεροι ειδικοί χαρακτήρες απενεργοποιούνται εξ ορισμού. Επομένως για να τους χρησιμοποιήσουμε με την κυριολεκτική έννοια αρκεί να τους βάλουμε μόνους τους, δηλαδή χωρίς το χαρακτήρα \ στ’ αριστερά. Εξαίρεση αποτελούν οι χαρακτήρες ^ και - που δύνανται να παίξουν ειδικό ρόλο στον ορισμό μιας κλάσης. Προκειμένου να τους χρησιμοποιήσουμε κυριολεκτικά, πρέπει να τους τοποθετήσουμε σε μία μη προβλεπόμενη θέση. Η προβλεπόμενη θέση για τον εκθέτη είναι αμέσως μετά την αριστερή αγκύλη, ενώ η προβλεπόμενη θέση για την παύλα είναι ανάμεσα σε οποιουσδήποτε δύο χαρακτήρες. Ας δούμε τώρα μερικά παραδείγματα.

grep "[0-9^]" test.txt

Αυτή η κλάση περιέχει όλα τα αριθμητικά ψηφία, καθώς και το χαρακτήρα ^.

grep "[5^a-z]" test.txt

Η συγκεκριμένη κλάση περιλαμβάνει όλα τα πεζά γράμματα, το 5 και το ^.

grep "[09-]" test.txt

Η κλάση στην παραπάνω κανονική έκφραση περιλαμβάνει τα ψηφία 0 κι 9, όπως επίσης και τον χαρακτήρα -. Το ίδιο ισχύει και στο ακόλουθο παράδειγμα:

grep "[-09]" test.txt

Μπαλαντέρ

Με τη βοήθεια των κλάσεων μπορούμε να περιγράψουμε έναν χαρακτήρα, δηλώνοντας ότι ανήκει σε κάποιο σύνολο. Αυτό το σύνολό, όμως, πρέπει να είναι ορισμένο με απόλυτη σαφήνεια. Αν θέλουμε να περιγράψουμε έναν οποιονδήποτε χαρακτήρα, χωρίς κανέναν περιορισμό, πρέπει να χρησιμοποιήσουμε τον ειδικό χαρακτήρα της τελείας . (dot ή period). Στις κανονικές εκφράσεις, λοιπόν, η τελεία λειτουργεί ως μπαλαντέρ κι αναφέρεται σε όλους τους εκτυπώσιμους χαρακτήρες καθώς και στους μη εκτυπώσιμους (tab, backspace, null κ.ά.). Ο μοναδικός χαρακτήρας που ξεφεύγει από την τελεία είναι ο χαρακτήρας αλλαγής γραμμής (newline character). Μάλιστα, επειδή ο συγκεκριμένος χαρακτήρας συμβολίζεται με το \n, θα μπορούσαμε να πούμε ότι η τελεία ισοδυναμεί με την εξής κλάση: [^\n]. (Τελικά, μπορούμε και με τις κλάσεις να ορίσουμε οποιονδήποτε χαρακτήρα.)

Επαναλήψεις

Με όσα έχουμε δει ως τώρα, μπορούμε να περιγράψουμε οποιονδήποτε μεμονωμένο χαρακτήρα. Τι γίνεται όμως όταν θέλουμε να περιγράψουμε ένα string οποιουδήποτε μήκους; Όπως υποψιάζεστε, υπάρχουν και γι’ αυτή τη δουλειά οι κατάλληλοι ειδικοί χαρακτήρες. Αυτοί οι τοποθετούνται μετά από μια κλάση, μετά από τον μπαλαντέρ ή μετά από ένα μεμονωμένο χαρακτήρα και υπονοούν ότι το συγκεκριμένο στοιχείο εμφανίζεται για άγνωστο πλήθος φορών. Αυτή η περιγραφή όμως είναι πολύ γενικόλογη. Ας δούμε τους σχετικούς ειδικούς χαρακτήρες.

Αστερίσκος, *: Το στοιχείο που βρίσκεται αριστερά από τον αστερίσκο μπορεί να επαναλαμβάνεται από μηδέν έως άπειρες φορές. Με άλλα λόγια, το συγκεκριμένο στοιχείο ενδέχεται να εμφανίζεται οσεσδήποτε φορές μέσα στο string που περιγράφει η κανονική έκφραση.

Σύμβολο πρόσθεσης, +: Το στοιχείο που βρίσκεται αριστερά από το σύμβολο της πρόσθεσης εμφανίζεται τουλάχιστον μία φορά.

Λατινικό ερωτηματικό, ?: Το στοιχείο που βρίσκεται αριστερά από το λατινικό ερωτηματικό ενδέχεται να εμφανίζεται το πολύ μία φορά. Με άλλα λόγια, η παρουσία του είναι προαιρετική.

Νομίζουμε ότι τώρα πρέπει να προχωρήσουμε σε μερικά παραδείγματα.

grep "0b[01]*" code.asm

ldi temp, 0b00111111

ldi temp, 0b11000000

ldi steps, 0b00010011

ldi counter, 0b11101100

Με αυτή την κανονική έκφραση περιγράφουμε ένα string που ξεκινά με το 0b και συνεχίζει με ένα άγνωστο πλήθος χαρακτήρων, καθένας από τους οποίους μπορεί να είναι 0 ή 1. Ας δούμε τώρα μία κανονική έκφραση που είδαμε και νωρίτερα:

grep "0[xX][a-fA-F0-9][a-fA-F0-9]" test.txt

A b0x and some numbers 0x0 0x2 0xae 0xf 0xffe3 0x22

Αυτή η κανονική έκφραση περιγράφει τους δεκαεξαδικούς αριθμούς, όπως εισάγονται στις περισσότερες γλώσσες προγραμματισμού. Προϋποθέτει, ωστόσο, ότι οι αριθμοί είναι διψήφιοι. Έτσι, στο πλαίσιο του συγκεκριμένου παραδείγματος το grep εντοπίζει δύο ολόκληρους αριθμούς κι έναν κατά το ήμισυ. Η συγκεκριμένη κανονική έκφραση είναι ελαττωματική, αλλά μπορούμε να τη βελτιώσουμε πολύ εύκολα:

grep "0[xX][a-fA-F0-9]+" test.txt

A b0x and some numbers 0x0 0x2 0xae 0xf 0xffe3 0x22

Με τη χρήση του συμβόλου της πρόσθεσης δηλώνουμε ότι το string καταλήγει σε τουλάχιστον έναν χαρακτήρα, ο οποίος μπορεί να είναι ένα οποιοδήποτε ψηφίο του δεκαεξαδικού συστήματος αρίθμησης. Η συγκεκριμένη κανονική έκφραση δεν θέτει άνω όριο στο μήκος του αριθμού κι έτσι το grep τους εντοπίζει όλους. Αν στη θέση του συμβόλου της πρόσθεσης είχαμε χρησιμοποιήσει τον αστερίσκο, τότε η κανονική έκφραση δεν θα έθετε κανέναν περιορισμό στο μήκος του αριθμού. Με άλλα λόγια, θα περιέγραφε και τους αριθμούς που δεν έχουν κανένα δεκαεξαδικό ψηφίο. Έτσι, θα έδινε σαν αποτέλεσμα και το 0x από το b0x:

grep "0[xX][a-fA-F0-9]*" test.txt

A b0x and some numbers 0x0 0x2 0xae 0xf 0xffe3 0x22

Ας δούμε κι ένα παράδειγμα με το λατινικό ερωτηματικό:

grep "file[s]?" unix_fs.txt

“On a UNIX system, everything is a file;

if something is not a file, it is a process.”

This statement is true because there are special files

that are more than just files (named pipes and sockets, for instance),

but to keep things simple, saying that everything is a file is …

Αυτή η κανονική έκφραση περιγράφει τα strings που ξεκινούν με το file και προαιρετικά καταλήγουν σε ένα s. Έτσι, το grep εντοπίζει όλες τις εμφανίσεις των λέξεων file και files.

Σε αυτό το σημείο είναι πολύ πιθανό να αναρωτιέστε το εξής: Τι γίνεται όταν θέλουμε να περιγράψουμε strings, στα οποία επαναλαμβάνεται ένα σύνολο χαρακτήρων; Ας υποθέσουμε ότι θέλουμε να περιγράψουμε τα strings που περιλαμβάνουν από μία έως οσεσδήποτε επαναλήψεις του 101. Κάτι τέτοιο μπορεί να γίνει με τη βοήθεια των παρενθέσεων, ως εξής:

grep "(101)+" some_file.txt

Χρησιμοποιώντας τις παρενθέσεις, υποχρεώνουμε τον ειδικό χαρακτήρα της πρόσθεσης να ισχύσει για ολόκληρο το 101. Με παρόμοιο τρόπο μπορούμε να περιγράψουμε όλους τους δεκαεξαδικούς αριθμούς που έχουν άρτιο πλήθος ψηφίων:

grep "0[xX]([a-fA-F0-9][a-fA-F0-9])+" some_file.txt

Εδώ, το σύμβολο της πρόσθεσης εφαρμόζεται στις δύο κλάσεις που περιλαμβάνει η παρένθεση. Αυτές οι δύο κλάσεις, όμως, περιγράφουν δύο ψηφία του δεκαεξαδικού αριθμητικού συστήματος. Έτσι, η ολοκληρωμένη κανονική έκφραση περιγράφει ένα string που ξεκινά με το 0, συνεχίζει με το x ή με το X κι ολοκληρώνεται με τουλάχιστον ένα ζευγάρι ψηφίων.

Αν και θα το έχετε υποψιαστεί, οι παρενθέσεις συνεργάζονται άψογα και με τους υπόλοιπους ειδικούς χαρακτήρες που χρησιμοποιούνται για τις επαναλήψεις. Αργότερα θα δούμε ότι οι παρενθέσεις λειτουργούν στις κανονικές εκφράσεις με τον ίδιο τρόπο που λειτουργούν και στις αριθμητικές: Αλλάζουν, δηλαδή, τη σειρά των “πράξεων”. Αυτό όμως μπορείτε να το ξεχάσετε για την ώρα.

Ελεγχόμενη επανάληψη

Πριν ολοκληρώσουμε την πρώτη μας επαφή με τις κανονικές εκφράσεις, οφείλουμε να εξαντλήσουμε το ζήτημα των επαναλήψεων. Όπως έχουμε ξαναπεί, οι κανονικές εκφράσεις κρύβουν μεγάλη περιγραφική δύναμη. Επομένως, θα ήταν τουλάχιστον παράξενο αν δεν επέτρεπαν να προσδιορίσουμε ένα συγκεκριμένο πλήθος επαναλήψεων για κάποιο στοιχείο. Αυτός ο προσδιορισμός επιτυγχάνεται με τη βοήθεια των αγκίστρων. Για την ακρίβεια, όταν βάζουμε έναν αριθμό μέσα σε άγκιστρα, αυτός ο αριθμός προσδιορίζει το πλήθος των επαναλήψεων του αμέσως προηγούμενου στοιχείου. Ας δούμε ένα παράδειγμα:

ls -lhR ~/my_scripts | grep "(rwx){3}"

Η κανονική έκφραση που δίνουμε στο grep, περιγράφει ένα string που περιλαμβάνει τρεις φορές διαδοχικά το rwx. Έτσι, με την εκτέλεση του παραπάνω θα εμφανιστούν τα αρχεία του καταλόγου ~/my_scripts, που στην έξοδο του ls -lhR περιλαμβάνουν το rwxrwxrwx (και άρα επιτρέπουν την ανάγνωση, την εγγραφή και την εκτέλεση για όλους τους χρήστες του συστήματος). Μέσα στα άγκιστρα έχουμε τη δυνατότητα να εισάγουμε και δύο αριθμούς, χωρισμένους με κόμμα. Ο πρώτος αριθμός προσδιορίζει το ελάχιστο πλήθος επαναλήψεων, ενώ ο δεύτερος το μέγιστο. Ένα παράδειγμα θα βοηθήσει και πάλι:

grep "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" apache.conf

Τι έχουμε εδώ; Αυτή η κανονική έκφραση μοιάζει αρκετά περίπλοκη. Το string που περιγράφεται εδώ ξεκινά με ένα έως τρία αριθμητικά ψηφία. Στη συνέχεια ακολουθεί μια τελεία, αφού ο χαρακτήρας \ απενεργοποιεί την ειδική ερμηνεία της τελείας. Αμέσως μετά έχουμε πάλι από ένα έως τρία αριθμητικά ψηφία, άλλη μία τελεία, άλλο ένα σετ αριθμητικών ψηφίων, μία ακόμη τελεία, καθώς κι ένα ακόμα σετ αριθμητικών ψηφίων. Μήπως αυτή η περιγραφή σας θυμίζει κάτι; Η παραπάνω κανονική έκφραση περιγράφει τα strings που μοιάζουν με διευθύνσεις IP. Βέβαια η συγκεκριμένη κανονική έκφραση περιγράφει και strings που δεν θα μπορούσαν να αποτελούν διευθύνσεις IP (π.χ., 000.023.135.152 και 643.121.122.174). Όπως και να ‘χει, με τη βοήθεια αυτής της έκφρασης μπορούμε να εντοπίσουμε όλες τις διευθύνσεις που περιέχει ένα αρχείο ρυθμίσεων κάποιας δικτυακής υπηρεσίας. Εκεί μέσα, εξάλλου, αποκλείεται να υπάρχουν άκυρα strings σαν το 852.412.725.123.

(Να μην ξεχάσω να κάνω εξάσκηση){100}

Οι κανονικές εκφράσεις περιλαμβάνουν αρκετούς και πολύ ισχυρούς μηχανισμούς περιγραφής. Έτσι, είναι πολύ εύκολο να δημιουργήσουμε μια έκφραση που να περιγράφει τα επιθυμητά strings. Το κακό είναι ότι, πολύ συχνά, οι εκφράσεις μας περιγράφουν και strings που δεν θέλουμε. Ο μόνος τρόπος ώστε ν’ αποφύγει κανείς αυτόν τον κίνδυνο, είναι να αποκτήσει τη μέγιστη δυνατή εξοικείωση με το συντακτικό των κανονικών εκφράσεων και τις ιδιαιτερότητές του. Προϋπόθεση φυσικά αποτελεί και η δομημένη γνώση, σε αντιδιαστολή με τη σκόρπια εμπειρία που πηγάζει από τη χρήση μερικών παραδειγμάτων που βρίσκουμε εδώ κι εκεί στο δίκτυο. Σ’ αυτό το άρθρο γνωρίσαμε τους πιο βασικούς μηχανισμούς περιγραφής και βάλαμε τα θεμέλια για να προχωρήσουμε σε άλλους, πιο εξεζητημένους. Αν θέλετε να μάθετε κι εσείς να χειρίζεστε τις κανονικές εκφράσεις, καλό θα ήταν ν’ αρχίσετε την εξάσκηση τώρα. Κι αν πράγματι το αποφασίσετε και ξεκινήσετε, μπορούμε να σας υποσχεθούμε ότι δεν θα το μετανιώσετε.

Άρθρα της σειράς

Μέρος 14: special chars, escaping, classes, ranges, negation, repetition

Μέρος 24: anchors, concatenation, alternation, eagerness, backreference

Μέρος 34: lookahead, lookbehind, greedy machines, lazy machines

Μέρος 44: backreference, anchors, character escaping, lookahead

~Spir@l Evolut10n

Σας άρεσε το post; Αν ναι μπορείτε να στηρίξετε το ðhacker, χωρίς κατ’ ανάγκη να ξοδέψετε χρήματα.