-+  Documenti
 |-  Bibliografia
 |-  Articoli
 |-  Perlfunc
 |-  F.A.Q.
 |-  F.A.Q. iclp
-+  Eventi
-+  Contatti
-+  Blog
-+  Link
R: Tags



 

Versione stampabile.


Marco Marongiu
Marco Marongiu è nato nel 1971. Nel 1997 si laurea in Matematica. Nel 1996 contribuisce a fondare il GULCh, il primo Linux Users Group in Sardegna e uno fra i primi in Italia. Oggi è Amministratore di Sistema presso una nota software house ad Oslo (Norvegia). Si diletta a programmare in Perl, scrivere articoli tecnici e tenere seminari.
Grazie a Dio, i file di configurazione sono dappertutto, e non è più necessario inserire configurazioni direttamente nel codice. E se per caso continui a farlo... beh, almeno sai che non è una buona prassi, no? In ogni caso, i file di configurazione devono essere interpretati per estrarre le informazioni che contengono. Se il formato del file è uno di quelli ben noti, troverai sicuramente una libreria già pronta. Se il formato non è poi così conosciuto, ma è orientato alla linea (cioè: ogni direttiva di configurazione si trova su una singola riga di testo) e semplice, allora scrivere un parser sarà facile. Se è un file XML, le cose si complicano un po' ma decodificare il file è sempre fattibile: basta usare una delle tante librerie per il parsing di XML, ispezionare gli elementi che ci interessano, e il gioco è fatto. Se poi siamo noi a dover scegliere il formato del file, possiamo usarne uno che sia fatto apposta per la nostra libreria preferita --nel mio caso il modulo AppConfig. Ma... cosa fare nel caso in cui ci si trovi in un caso diverso? Io, per esempio, dovevo decodificare le informazioni di un file pseudo-INI di 350 chilobyte. Con "pseudo-ini" intendo che ha dei costrutti tipici dei file INI, come le dichiarazioni di sezione:
[nome_sezione]
e associazioni chiave valore, come:
    parametro=valore
    valori="possonno essere anche fra doppi apici"
linee di commento:
    ; come questa
e sono permesse linee vuote. Una differenza è che sono permesse assegnazioni multiple allo stesso parametro, che risultano nell'assegnazione di un array di valori al parametro stesso:
    parametro=valore1
    parametro=valore2
    parametro=valore3
    ; il valore del parametro è (valore1, valore2, valore3)
E non è finita! La parte più difficile era che ad un parametro era possibile assegnare una struttura multivalore del linguaggio Pike, cioè un array:
    parametro=({ valore1, valore2, valore3 })
o un mapping (associazioni chiave/valore):
    parametro=([ "key1" : "value1", "key2" : "value2" ])
C'è di più: le strutture Pike possono essere multilinea e annidate! Ad esempio:
	parameter=({
	            ([
	                "key1" : "value1",
	                "key2" : ([ "array", "value" ]),
	            ]),
	            "second element of this array",
	            ({
	                "and here is another array",
	                ({
	                    "with another one nested",
	                }),
	                ([
	                    "that" : "contains",
	                    "one"  : "more",
	                    "hash" : "value",
	                ]),
	            }),
	            "Hooray!",
	          })
Un incubo, vero? Dovevo interpretare questo file. Il formato del file può essere descritto (è quello che ho appena fatto, giusto?), quindi ciò che mi serviva era una descrizione formale (ossia: una grammatica) da dare in pasto ad un generatore di parser. Mi sono ricordato di quel corso sul modulo Parse::RecDescent che seguii nel lontano 1999 alla Open Source Software Convention di Monterey, USA... bei ricordi!!! Dopo 11 anni, era finalmente arrivato il momento di applicare ciò che avevo imparato durante il corso. Ma, come ho poi scoperto, Parse::RecDescent è un modulo molto potente, e usarlo correttamente può diventare altrettanto complicato. Ecco perché ho deciso di scrivere questo articolo come una guida passo-passo. Naturalmente è richiesta la conoscenza di Perl, dato che parlerò diffusamente di strutture dati in Perl, reference ed altri costrutti senza spiegarli. La prima parte è scrivere la grammatica. Il nostro file conterrà zero o più linee, fino alla fine del file. Possiamo esprimerlo in questo modo:
AsIni: Line(s?) /\Z/
che significa che AsIni è composto da zero o più "Line", fino alla fine del file. So che il nome AsIni fa ridere in Italiano, ma il file si chiama proprio as.ini, quindi non avevo molta scelta... Come è fatta una "Line"? Può essere un commento, una linea vuota, una dichiarazione di sezione, o una assegnazione a un parametro; nessun altro tipo di linea è consentito. Lo esprimeremo così:
Line:   CommentLine
                | BlankLine
                | SectionDeclaration
                | AssignmentLine
                | <error>
Abbiamo fatto un passo avanti, proseguiamo: com'è fatta una linea di commento? Abbiamo detto che inizia con un ";", eventualmente preceduto da spazi, e il commento è tutto il testo che segue quel carattere, fino alla fine della linea. Lo possiamo esprimere in questo modo:
CommentLine:    <skip: q{}> /^\s*/m ';' /.*$/m
Cosa significa il costrutto "skip"? Normalmente, Parse::RecDescent isola i "token" che compongono il file, scartando gli spazi eccedenti. Se vogliamo esprimere che la linea può avere spazi in testa, dobbiamo cambiare questo comportamento quando cerchiamo questo tipo di linea: è ciò che il costrutto skip fa. Per maggiori informazioni vi rimando alla documentazione del modulo. Possiamo usare lo stesso trucco per esprimere BlankLine:
BlankLine:      <skip: q{}> /^\s+$/m
Cosa ci fa il modificatore "m" alla fine del pattern matching? È necessario usarlo perché il testo da elaborare viene passato al parser come un'unica stringa, che quindi conterrà anche le sequenze di ritorno a capo. La "m" è un modificatore che, in un certo senso, indica a Perl di match-are l'inizio e la fine della linea all'interno della stringa. Continuando ad esaminare le possibili linee, arriviamo alla dichiarazione di sezione:
SectionDeclaration:     '[' /[^\]]+/ ']'
Che significa: una "[", una stringa non nulla che non contiene "]", e infine il carattaere "]. Era facile. Siamo arrivati al dunque: le assegnazioni ai parametri. Com'è fatta una linea di assegnazione?
AssignmentLine: Parameter '=' Value(?)
Questo significa: il nome del parametro, un segno di "=", ed un valore, o nessun valore (il che significa che il parametro è undef). "(?)" è una "repetition", che significa "0 o 1 ricorrenze di questo elemento". Abbiamo espresso la AssignmentLine, ma com'è fatto un parametro?
Parameter:      /\w[\w\s-]*/
Questo significa: un "word character" (potete pensarlo come un carattere alfanumerico, più il "_"), che può essere seguito da una seuqenza di altri word character, o "spazi" (cioè lo spazio vero e proprio, la tabulazione...), o "-". Il valore, come detto, può essere una struttura Pike o una stringa:
Value:          PikeStructure   | ValueString
Esaminiamo prima le stringhe, che sono più semplici. Abbiamo detto che possono essere stringhe normali o racchiuse fra doppi apici; questo concetto può essere espresso in questo modo:
ValueString:    QuotedString    | UnquotedString

QuotedString:   '"' /[^"]+/ '"'

UnquotedString: /.+/
Ciò significa: una QuotedString è una stringa che inizia e finisce con i doppi apici, e contiene caratteri diversi dai doppi apici. Una UnquotedString è una stringa qualsiasi. Si noti che verifichiamo prima se si tratti di una QuotedString, e poi una UnquotedString, altrmenti la UnquotedString sarebbe verificata anche quando non vorremmo che lo fosse (gli apici verrebbero "catturati" come facenti parte della stringa). Questo mi dà anche l'opportunità di far notare che i parser generati da Parse::RecDescent sono di tipo "first match": fra tutte quelle possibili, viene utilizzata la prima regola che viene verificata. Quindi, è importante che le regole più specifiche vengano verificate per prime. E siamo arrivati alla PikeStructure. Se non lo avete già fatto da poco, procuratevi un caffè perché questa spiegazione sarà lunga e, a tratti, anche complessa. Andate a prenderlo, io vi aspetto qui. Preso il caffè? Bene. Il primo livello di definizione della PikeStructure è abbastanza semplice, così come il secondo:
PikeStructure:  PikeArray | PikeMapping

PikeArray:      '({' PikeArrayContent '})'

PikeMapping:    '([' PikeMappingContent '])'
Cominciamo a lavorare sugli array. Il contenuto di un array può essere:
  1. nessun contenuto (un array vuoto)
  2. un elemento
  3. due o più elementi, separati da virgole
Inoltre, è ammissibile che ci sia una virgola in coda all'ultimo valore. Chiameremo la virgola "PikeStructureSeparator", e la sequenza di valori contenuta nell'array la chiameremo "PikeArraySequence". Scriviamo le definizioni:
PikeStructureSeparator: ','

PikeArrayContent:       PikeArraySequence(?) PikeStructureSeparator(?)
Quindi possiamo avere zero o una PikeArraySequence (perché zero o una??? Continua a leggere!!!), e zero o uno PikeStructureSeparator in coda. Incidentalmente, questo permette anche di avere una dichiarazione di array vuoto che contiene solo una virgola, ma faremo finta che va bene anche questo. Ora, definiamo formalmente la sequenza a partire dalla definizione che ne abbiamo dato a parole poche righe più su:
PikeArraySequence:      PikeValue PikeArrayFurtherValue(s?)
Questo significa: almeno un PikeValue, pià zero o uno ulteriori valori. Probabilmente cominci a vedere qual'era la strategia: la ripetizione zero/uno "PikeArraySequence(?)" si occupa del caso in cui l'array è vuoto, il singolo PikeValue tiene conto del caso in cui ci sia un singolo valore, e "PikeArrayFurtherValue(s?)" tiene conto del resto. Ma non abbiamo ancora finito. Che cos'è un PikeValue?
PikeValue:              PikeStructure | QuotedString | Number
Un PikeValue può essere: un'altra PikeStructure (che tiene conto del caso in cui vi siano più strutture annidate), o una QuotedString, o un numero... Number non lo avevamo ancora visto! Cos'è un numero?
Number: /[+-]?\d*(\.\d+)?/ <reject: $item[1] eq ''>
Questo significa che possiamo avere un segno, quindi potrebbero esserci cifre, dopo le quali potrebbero esserci anche un punto e ulteriori cifre. Putroppo però questo pattern è verificato anche dalla stringa nulla, che non vogliamo che sia verificata perché... non è un numero! Esprimiamo questa condizione con la direttiva reject, che significa: se la stringa che verifica il pattern è la stinga nulla, allora la regola non è verificata. Questo completa la discussione sul PikeValue, ci manca PikeArrayFurtherValue. Che cos'è? Se ci pensi un attimo, si tratta di prevedere ulteriori valori dopo il primo, quindi:
PikeArrayFurtherValue:  PikeStructureSeparator PikeValue
Abbiamo finito: se torni indietro alla PikeArraySequence vedrete che è un PikeValue seguito da zero o più ", PikeValue"! Questo conclude la discussione sui PikeArray, e possiamo dedicarci ai mapping. Abbiamo già imparato parecchio dalla discussione precedente, quindi per PikeMapping avremo meno bisogno di spiegare:
PikeMappingContent:     PikeMappingSequence(?) PikeStructureSeparator(?)

PikeMappingSequence:    PikeMappingPair PikeMappingFurtherPair(s?)

PikeMappingPair:        QuotedString ':' PikeValue

PikeMappingFurtherPair: PikeStructureSeparator PikeMappingPair
Essenzialmente, un PikeMapping è una sequenza di zero o più "QuotedString : PikeValue", che può avere una virgola in coda. Montando tutti i pezzi assieme otteniamo la nostra grammatica:
AsIni: Line(s?) /\Z/

Line:   CommentLine
                | BlankLine
                | SectionDeclaration
                | AssignmentLine
                | 
                
CommentLine:    <skip: q{}> /^\s*/m ';' /.*$/m

BlankLine:      <skip: q{}> /^\s+$/m

SectionDeclaration:     '[' /[^\]]+/ ']'

AssignmentLine: Parameter '=' Value(?)

Parameter:      /\w[\w\s-]*/

Value:          PikeStructure   | ValueString

ValueString:    QuotedString    | UnquotedString

QuotedString:   '"' /[^"]+/ '"'

UnquotedString: /.+/

PikeStructure:  PikeArray | PikeMapping

PikeArray:      '({' PikeArrayContent '})'

PikeMapping:    '([' PikeMappingContent '])'

PikeStructureSeparator: ','

PikeArrayContent:       PikeArraySequence(?) PikeStructureSeparator(?)

PikeArraySequence:      PikeValue PikeArrayFurtherValue(s?)

PikeValue:              PikeStructure | QuotedString | Number

Number: /[+-]?\d*(\.\d+)?/ <reject: $item[1] eq ''>

PikeArrayFurtherValue:  PikeStructureSeparator PikeValue

PikeMappingContent:     PikeMappingSequence(?) PikeStructureSeparator(?)

PikeMappingSequence:    PikeMappingPair PikeMappingFurtherPair(s?)

PikeMappingPair:        QuotedString ':' PikeValue

PikeMappingFurtherPair: PikeStructureSeparator PikeMappingPair
I più attenti potrebbero aver intuito una struttura ad albero o un grafo sotto questa grammatica, ed è proprio così che Parse::RecDescent lavora: parte da una radice (nel nostro caso AsIni) e va sempre più in fondo nella struttura delle regole che compongono la grammatica, cercando di verificare ogni regola con le sue sottoregole, andando sempre più in profondità. Se un ramo non viene verificato, il parser torna alla biforcazione precedente e tenta di percorrerla verificando le sue regole, e così via. Abbiamo quindi una grammatica, che se data in pasto a Parse::RecDescent produce un parser che elaborerà correttamente il nostro file pseudo-ini. Il problema è che non farà niente a parte questo. Perché? Dovreste sapere che i computer sono macchine stupide, e se non gli chiedete di fare qualcosa, loro non fanno assolutamente niente! Dobbiamo istruire esplicitamente Parse::RecDescent su cosa fare quando una regola viene verificata, allegando delle azioni alle regole della nostra grammatica: le azioni sono pezzi di codice Perl. Per quanto nelle azioni si possa fare qualunque cosa, ho scoperto che nel mio caso specifico la cosa migliore era lasciare che le sezioni di testo "match-ate" dalle regole della grammatica salissero su per l'albero come delle bolle, fino ad arrivare alla regola AssignmentLine. Arrivate lì, le informazioni verranno disposte in un hash annidato, con i nomi delle sezioni al primo livello, i parametri al secondo, e associati a questa coppia avremo un array di valori per quel parametro in quella sezione. Per indirizzare un certo valore useremo quindi un'espressione del tipo:
  $AsIni::node{$section_name}{$parameter_name}[$value_index]
Per poterlo fare, è necessario conoscere alcune altre cose. La prima: gli elementi che verificano una regola vengono disposti su un array che si chiama @item, e su un hash che si chiama %item. Per ciò che riguarda questo articolo, faremo riferimento solo ad @item, rimandando alla documentazione per la descrizione del hash. Accade quindi che se la stringa
"ParseMe"
verifica la regola
QuotedString:   '"' /[^"]+/ '"'
allora:
  • $item[0] conterrà il nome della regola, cioè: QuotedString
  • $item[1] conterrà il valore della stringa '"', cioè... "
  • $item[2] conterrà la stringa che verifica il pattern, cioè: ParseMe
  • $item[3] conterrà ancora il valore della stringa '"'
Se invece la regola contiene anche ripetizioni --cioè le stringhe "(?)", "(s?)"... che seguono una sottoregola--, il funzionamento è leggermente diverso. In questo caso, gli elementi di @item conterranno una reference all'array dei valori che la sottoregola ha verificato. Cioè, quando la linea:
	cheese=pecorino
verifica AssignmentLine, allora:
  • $item[0] conterrà il nome della regola, cioè: 'AssignmentLine'
  • $item[1] conterrà il valore di Parameter, cioè: "cheese"
  • $item[2] conterrà il valore di '=', cioè... =
  • $item[3] conterrà una reference ad array dei Value che sono stati verificati. In questo caso potremmo esprimere Value con l'espressione Perl [ "pecorino" ]
Infine, $return è una variabile speciale che contiene il valore che l'azione deve restuire se l'intera regola è verificata. Se una sottoregola viene verificata, ma l'intera regola non lo è, il valore di $return viene scartato. È un comportamento decisamente conveniente, perché se invece conservassimo i risultati man mano che vengono verificati dalle sottoregole, ma la regola successivamente fallisse, dovremmo anche occuparci di eliminare i valori precedentemente salvati. E potrebbe non essere semplice! Ora sappiamo abbastanza per poter far "salire" i valori, partendo dalle foglie dell'albero (cioè QuotedString, UnquotedString e Number) fino alla cima. È molto semplice:
QuotedString:   '"' /[^"]+/ '"'
  {
    $return = $item[2] ;
  }

UnquotedString: /.+/
  {
    $return = $item[1] ;
  }

# This rule matches a number, but rejects null-length results
Number: /[+-]?\d*(\.\d+)?/ <reject: $item[1] eq ''>
  {
    $return = $item[1] ;
  }
Nelle regole per la ripetizione "(?)", bisogna ricordarsi di "spacchettare" il valore contenuto nella reference:
PikeMappingContent:     PikeMappingSequence(?) PikeStructureSeparator(?)
  {
    # Since we have a repetition here, $item[1] is a reference to an
    # array which may contain 0 or 1 PikeMappingSequence's.
    # In turn, PikeMappingSequence returns an hash reference.
    # So, if we want the hash reference to bubble up, we have to
    # unwrap it and return it as is.

    ( $return ) = @{ $item[1] } ;
  }
...e così via per gli altri casi simili. Quando incontriamo una sezione, dobbiamo conservarne il nome in una package variable per poterlo usare successivamente:
SectionDeclaration:     '[' /[^\]]+/ ']'
  {
    print STDERR qq{In section "$item[2]"\n} ;

    my $sectionname = $item[2] ;

    $AsIni::section = $sectionname ;
  }
E infine, quando arriviamo a verificare una AssignmentLine, dobbiamo salvare i valori ottenuti nel hash %AsIni::node:
AssignmentLine: Parameter '=' Value(?)
  {
    my $distvalue = $item[3] ;
    my $parmname  = $item[1] ;
    my $paramvalue ;

    ( $paramvalue ) = @$distvalue ;

    if ( not exists $AsIni::node{$AsIni::section}{$parmname} ) {
      $AsIni::node{$AsIni::section}{$parmname} = [] ;
    }

    # Get a reference to the current array of values for this parameter
    # in $current
    my $current = $AsIni::node{$AsIni::section}{$parmname} ;

    # We can update this safely, since we are using the reference
    push @$current,$paramvalue ;

  }
La versione finale della grammatica è la seguente:
# $Id: g3.txt,v 1.14 2010/07/23 12:41:24 bronto Exp bronto $

AsIni: Line(s?) /\Z/

Line:	CommentLine
		| BlankLine
		| SectionDeclaration
		| AssignmentLine
		| 


CommentLine:	<skip: q{}> /^\s*/m ';' /.*$/m
  {
    print STDERR qq{\tSkipping comment: $item[4]\n} ;
  }

BlankLine:	<skip: q{}> /^\s+$/m
  {
    print STDERR qq{\tSkipping blank line\n}
  }

SectionDeclaration:	'[' /[^\]]+/ ']'
  {
    print STDERR qq{In section "$item[2]"\n} ;

    my $sectionname = $item[2] ;

    $AsIni::section = $sectionname ;
  }

AssignmentLine:	Parameter '=' Value(?)
  {
    my $distvalue = $item[3] ;
    my $parmname  = $item[1] ;
    my $paramvalue ;

    ( $paramvalue ) = @$distvalue ;

    if ( not exists $AsIni::node{$AsIni::section}{$parmname} ) {
      $AsIni::node{$AsIni::section}{$parmname} = [] ;
    }

    # Get a reference to the current array of values for this parameter
    # in $current
    my $current = $AsIni::node{$AsIni::section}{$parmname} ;

    # We can update this safely, since we are using the reference
    push @$current,$paramvalue ;

  }

Parameter:	/\w[\w\s-]*/
  {
    $return = $item[1] ;
  }

Value:		PikeStructure	| ValueString
  {
    $return = $item[1] ;
  }

ValueString:	QuotedString	| UnquotedString
  {
    $return = $item[1] ;
  }

QuotedString:	'"' /[^"]+/ '"'
  {
    $return = $item[2] ;
  }

UnquotedString:	/.+/
  {
    $return = $item[1] ;
  }

# This rule matches a number, but rejects null-length results
Number:	/[+-]?\d*(\.\d+)?/ <reject: $item[1] eq ''>
  {
    $return = $item[1] ;
  }

PikeStructure:	PikeArray	| PikeMapping
  {
    # $item[1] is a reference to an array (PikeArray) or hash
    # (PikeMapping). We bubble it up as is
    $return = $item[1] ;
  }

PikeArray:	'({' PikeArrayContent '})'
  {
    # $item[2] is a PikeArrayContent, and since PikeArrayContent
    # bubbles up a reference to an array of PikeValues, this should be
    # an array reference that we can safely bubble up as is.
    $return = $item[2] ;
  }

PikeMapping:	'([' PikeMappingContent '])'
  {
    # $item[2] is a PikeMappingContent, and since PikeMappingContent
    # bubbles up an hash reference, this should be an hash reference that we
    # can safely bubble up as is.
    $return = $item[2] ;
  }

PikeStructureSeparator:	','

PikeArrayContent:	PikeArraySequence(?) PikeStructureSeparator(?)
  {
    # $item[1] comes from a repetition of PikeArraySequence,
    # so it is a reference to an array of 0 or 1 PikeArraySequence.
    # In turn, PikeArraySequence is a reference to an array of
    # PikeValue's. We don't want to change the PikeValue's but we need
    # to unroll $item[1] before bubbling it up.
    ( $return ) = @{ $item[1] } ;
  }

PikeArraySequence:	PikeValue PikeArrayFurtherValue(s?)
  {
    # $item[1] is a PikeValue, hence:
    # - a reference to an array or hash (if PikeStructure)
    # - a scalar (if QuotedString or Number)
    #
    # $item[2] comes from a repetition of PikeArrayFurtherValue,
    # so it is a reference to an array of 0 or 1 PikeArrayFurtherValue.
    # In turn, PikeArrayFurtherValue just returns a PikeValue. So,
    # we actually don't want to change $item[1], but we need to
    # unroll $item[2] before returning it. Actually, we return an
    # array reference with the whole thing.
    $return = [ $item[1], @{ $item[2] } ] ;
  }

PikeArrayFurtherValue:	PikeStructureSeparator PikeValue
  {
    # $item[2] is a PikeValue, hence:
    # - a reference to an array or hash (if PikeStructure)
    # - a scalar (if QuotedString or Number)
    # We bubble it up as is.
    $return = $item[2] ;
  }

PikeMappingContent:	PikeMappingSequence(?) PikeStructureSeparator(?)
  {
    # Since we have a repetition here, $item[1] is a reference to an
    # array which may contain 0 or 1 PikeMappingSequence's.
    # In turn, PikeMappingSequence returns an hash reference.
    # So, if we want the hash reference to bubble up, we have to
    # unwrap it and return it as is.
    
    ( $return ) = @{ $item[1] } ;
  }

PikeMappingSequence:	PikeMappingPair PikeMappingFurtherPair(s?)
  {
    # $item[1] is a PikeMappingPair, hence a reference to an array
    # of two elements: a string and a PikeValue, that is:
    # - a reference to an array or hash (if Pikevalue ~ PikeStructure)
    # - a scalar (if PikeValue ~ QuotedString or Number)
    #
    # $item[2] has a repetition, so it is a reference to an array of
    # PikeMappingFurtherPair's. Since PikeMappingFurtherPair just
    # returns a PikeMappingPair (see $item[1]), then $item[2] is
    # a reference to an array where each element is, in turn, a
    # reference to an array of two elements.
    #
    # Since we are going to return an hash here, we create a reference
    # to an hash; to correctly unroll the values of $item[1] and
    # $item[2] we:
    # - simply dereference $item[1], hence unrolling the only hash
    #   pair the array contained
    # - we dereference $item[2], getting an array of arrays, and
    #   then we use map to further unroll the key/value pairs
    #
    # We then bubble up the outcome
    $return = { @{ $item[1] } , map( @$_ , @{ $item[2] } ) } ;
  }

PikeMappingPair:	QuotedString ':' PikeValue
  {
    # $item[1] is a scalar (QuotedString)
    # $item[3] is a PikeValue, hence:
    # - a reference to an array or hash (if Pikevalue ~ PikeStructure)
    # - a scalar (if PikeValue ~ QuotedString or Number)
    # We throw them up together as a single entity: a reference to an array
    $return = [ $item[1], $item[3] ] ;
  }

PikeMappingFurtherPair:	PikeStructureSeparator PikeMappingPair
  {
    # $item[2] is a PikeMappingPair, hence a reference to an array
    # containing a QuotedString (the first) and a PikeValue, hence:
    # - a reference to an array or hash (if Pikevalue ~ PikeStructure)
    # - a scalar (if PikeValue ~ QuotedString or Number)
    $return = $item[2] ;
  }

PikeValue:		PikeStructure | QuotedString | Number
  {
    # $item[1] is:
    # - a reference to an array or hash (if PikeStructure)
    # - a scalar (if QuotedString or Number)
    $return = $item[1] ;
  }
Potete provarla su uno dei vostri file, magari uno di grosse dimensioni... ma se non ne avete uno a portata di mano potete anche accontentarvi di questo piccolo esempio:
[section1]
  onehash=([ "onekey" : "onevalue" ])
  hashtest=([ "key1" : "val1", "key2" : "val2" ])
  aohtest=({
    ([ "hash1key1": "hash1val1" ]),
    ([
      "hash2key1": "hash2val1",
      "hash2key2": "hash2val2",
    ]),
    })
  string=sample string
  onearray=({ "onevalue" })
E per testarlo avrete bisogno di uno script di test, giusto? Ve ne lascio uno molto sbrigativo, che legge la grammatica da un file che si chiama g3.txt e un file INI da mockup.ini, entrambi nella directory corrente --scrivere qualcosa di più serio è lasciato al lettore per esercizio.
#!/usr/bin/perl

# $Id: test.pl,v 1.8 2010/07/23 13:00:09 bronto Exp bronto $

use strict ;
use warnings ;

use Parse::RecDescent ;
use Data::Dumper ;

########################################################################
package AsIni ;

our %node ;
our $section ;
our $parmname ;
our $content ;

########################################################################
package AsIni::Parser ;

push @ARGV,0 unless @ARGV > 0 ;

eval { use constant DEBUG => $ARGV[0] } ;
die $@ if $@ ;

if (DEBUG >= 1) {
  $::RD_HINT  = 'true' ;
  $::RD_WARN  = 'true' ;

  $Data::Dumper::Indent = 1 ;

  if (DEBUG >= 2) {
    $::RD_TRACE  = 'true' ;
    $::RD_ERRORS = 'true' ;
  }
}

my $grammar = q{} ;
my $asini   = q{} ;
{
  local $/ ;
  undef $/ ;

  open my $grammarfh, '<', 'g3.txt' or die ;
  $grammar = <$grammarfh> ;


  open my $asinifh, '<', 'mockup.ini' or die ;
  $asini   = <$asinifh> ;
}

my $parser = Parse::RecDescent->new($grammar) ;
die if not defined $parser ;

my $parser_outcome = $parser->AsIni($asini) ;

print Dumper ( $parser_outcome ) if DEBUG >= 3 ;

print Dumper ( \%AsIni::node  )  if DEBUG >= 0 ;

Potete passare allo script un parametro, il debug level, da zero a tre per fargli emettere delle informazioni di debug sia dallo script stesso, sia da Parse::RecDescent. Il significato delle variabili RD_* è spiegato nel manuale.
Buon divertimento!!!

Bibliografia:
Ti è piaciuto questo articolo? Iscriviti al feed!


Inviato da oha il September 7, 2010 11:30 AM

ottimo articolo, e delizioso esempio d'uso!

la direttiva per parsare stringhe bilanciate (quoted) e' perl_quotelike oppure:

balanced: '"' /(\\.|[^"\\])*/ '"'










Devo ricordare i dati personali?






D:
Annunci Google