Come scrivere un interprete / parser di comando?


22

Problema: eseguire i comandi sotto forma di stringa.

  • esempio di comando:

    /user/files/ list all; equivalente a: /user/files/ ls -la;

  • un altro:

    post tw fb "HOW DO YOU STOP THE TICKLE MONSTER?;"

equivalente a: post -tf "HOW DO YOU STOP THE TICKLE MONSTER?;"

Soluzione attuale:

tokenize string(string, array);

switch(first item in array) {
    case "command":
        if ( argument1 > stuff) {
           // do the actual work;
        }
}

I problemi che vedo in questa soluzione sono:

  • Nessun errore nel controllo diverso da ifs nidificato nidificato all'interno di ciascun caso. La sceneggiatura diventa molto grande e difficile da mantenere.
  • I comandi e le risposte sono codificati.
  • Non c'è modo di sapere se i flag sono parametri corretti o mancanti.
  • Mancanza di intelligenza per suggerire "potresti voler eseguire $ command".

E l'ultima cosa che non posso affrontare sono i sinonimi in diverse codifiche, ad esempio:

case command:
case command_in_hebrew:
    do stuff;
break;

L'ultimo potrebbe essere banale, ma beh, quello che voglio vedere sono le solide basi di questo tipo di programma.

Attualmente sto programmando questo in PHP ma potrei farlo in PERL.


Non vedo affatto come questo si riferisca specificamente a PHP. Ci sono già molti thread su questo argomento interprete / compilatore su SO e SE.
Raffael,

3
Nessuno ha menzionato getopt?
Anton Barkovsky,

@AntonBarkovsky: l'ho fatto. Vedi i miei link. Penso che risposte come quelle di Ubermensch siano davvero complicate per ciò che l'OP sta cercando di fare.
Quentin-starin,

1
Ho anche citato un approccio semplice usando RegExp. Anche la risposta è aggiornata
Ubermensch il

Non ho menzionato alcun progresso specifico. Lang. potresti aggiungere un tag "c", un tag "ruby", un tag "php", forse c'è una libreria opensource, una libreria standard o "comunemente usata, non ancora una libreria standard". per i tuoi progressi. Lang.
Umlcat,

Risposte:


14

Consentitemi di ammettere francamente, costruire parser è un lavoro noioso e si avvicina alla tecnologia del compilatore, ma costruirne uno si rivelerebbe una bella avventura. E un parser viene fornito con un interprete. Quindi devi costruire entrambi.

Una rapida introduzione al parser e agli interpreti

Questo non è troppo tecnico. Quindi gli esperti non si arrabbiano con me.

Quando si inserisce un input in un terminale, il terminale divide l'input in più unità. L'input si chiama espressione e le unità multiple sono chiamate token. Questi token possono essere operatori o simboli. Quindi, se inserisci 4 + 5 in una calcolatrice, questa espressione viene divisa in tre token 4, +, 5. Il plus è considerato un operatore mentre 4 e 5 simboli. Questo viene passato a un programma (considerare questo come un interprete) che contiene la definizione per gli operatori. Basato sulla definizione (nel nostro caso, aggiungere), aggiunge i due simboli e restituisce il risultato al terminale. Tutti i compilatori sono basati su questa tecnologia. Il programma che divide un'espressione in più token viene chiamato lexer e il programma che converte questi token in tag per ulteriori elaborazioni ed esecuzioni si chiama parser.

Lex e Yacc sono le forme canoniche per la costruzione di lexer e parser basati sulla grammatica BNF in C ed è l'opzione consigliata. La maggior parte dei parser è un clone di Lex e Yacc.

Passaggi nella costruzione di un parser / intrepreter

  1. Classificare i token in simboli, operatori e parole chiave (le parole chiave sono operatori)
  2. Costruisci la tua grammatica usando il modulo BNF
  3. Scrivi funzioni parser per le tue operazioni
  4. Compilalo come programma

Quindi nel caso sopra il tuo token addizionale sarebbe qualsiasi cifra e un segno più con la definizione di cosa fare con il segno più nel lexer

Note e suggerimenti

  • Scegli una tecnica di analisi che valuta da sinistra a destra LALR
  • Leggi questo libro del drago sui compilatori per farne un'idea. Personalmente non ho finito il libro
  • Questo link darebbe una visione superveloce di Lex e Yacc con Python

Un approccio semplice

Se hai solo bisogno di un semplice meccanismo di analisi con funzioni limitate, trasforma il tuo requisito in un'espressione regolare e crea un intero gruppo di funzioni. Per illustrare, assumere un semplice parser per le quattro funzioni aritmetiche. Quindi dovresti prima chiamare l'operatore e poi l'elenco delle funzioni (simile a lisp) nello stile (+ 4 5)o (add [4,5])poi potresti usare un semplice RegExp per ottenere l'elenco degli operatori e i simboli su cui operare.

I casi più comuni potrebbero essere facilmente risolti con questo approccio. Il rovescio della medaglia è che non puoi avere molte espressioni nidificate con una sintassi chiara e non puoi avere facili funzioni di ordine superiore.


2
Questo è uno dei modi più difficili possibili. Separare lex e analisi delle pass, ecc. - È probabilmente utile per implementare un parser ad alte prestazioni per un linguaggio molto complesso ma arcaico. Nel mondo moderno l'analisi senza lex è un'opzione predefinita più semplice. I combinatori di analisi o eDSL sono più facili da usare rispetto ai preprocessori dedicati come Yacc.
SK-logic,

Concordato con la logica SK ma poiché è necessaria una risposta generale dettagliata, ho suggerito Lex e Yacc e alcune basi del parser. getopts suggerito da Anton è anche un'opzione più semplice.
Ubermensch,

questo è quello che ho detto: lex e yacc sono tra i modi più difficili di analisi e nemmeno abbastanza generici. L'analisi senza Lexer (ad es. Packrat o semplice Parsec) è molto più semplice per un caso generale. E il libro del Drago non è più un'introduzione molto utile all'analisi: è troppo obsoleto.
SK-logic,

@ SK-logic Puoi consigliarmi un libro meglio aggiornato. Sembra coprire tutte le basi di una persona che cerca di capire l'analisi (almeno nella mia percezione). Per quanto riguarda lex e yacc, sebbene difficile, è ampiamente usato e molti linguaggi di programmazione ne forniscono l'implementazione.
Ubermensch,

1
@ alfa64: assicurati di farcelo sapere quando in realtà codifichi una soluzione basata su questa risposta
quentin-starin

7

In primo luogo, quando si tratta di grammatica o di come specificare argomenti, non inventare i tuoi. Lo standard in stile GNU è già molto popolare e ben noto.

Secondo, poiché stai usando uno standard accettato, non reinventare la ruota. Usa una libreria esistente per farlo per te. Se usi argomenti in stile GNU, c'è quasi sicuramente una libreria matura nella tua lingua preferita. Ad esempio: c # , php , c .

Una buona libreria di analisi delle opzioni stamperà anche la guida formattata sulle opzioni disponibili per te.

MODIFICA 27/12

Sembra che tu lo stia rendendo più complicato di quello che è.

Quando si guarda una riga di comando, è davvero abbastanza semplice. Sono solo opzioni e argomenti per quelle opzioni. Ci sono pochissimi problemi complicati. L'opzione può avere alias. Gli argomenti possono essere elenchi di argomenti.

Un problema con la tua domanda è che non hai specificato alcuna regola per il tipo di riga di comando che desideri gestire. Ho suggerito lo standard GNU e i tuoi esempi si avvicinano a questo (anche se non capisco davvero il tuo primo esempio con il percorso come primo elemento?).

Se stiamo parlando di GNU, ogni singola opzione può avere solo una forma lunga e una forma breve (carattere singolo) come alias. Qualsiasi argomento contenente uno spazio deve essere racchiuso tra virgolette. È possibile concatenare più opzioni in forma breve. Le opzioni di forma breve devono essere seguite da un singolo trattino, la forma lunga da due trattini. Solo l'ultima delle opzioni in forma abbreviata concatenata può avere un argomento.

Tutto molto semplice. Tutto molto comune Inoltre è stato implementato in tutte le lingue che puoi trovare, probabilmente cinque volte.

Non scriverlo Usa ciò che è già scritto.

A meno che tu non abbia in mente qualcosa di diverso dagli argomenti della riga di comando standard, usa solo una delle MOLTE librerie già esistenti e testate che lo fanno.

Qual è la complicazione?


3
Sfrutta sempre la comunità open source.
Spencer Rathbun,

hai provato getoptionkit?
alfa64,

No, non lavoro in php da diversi anni. Potrebbero esserci anche altre librerie php. Ho usato la libreria di parser della riga di comando c # che ho collegato.
Quentin-starin,

4

Hai già provato qualcosa come http://qntm.org/loco ? Questo approccio è molto più pulito di qualsiasi scritto ad hoc scritto a mano, ma non richiede uno strumento di generazione di codice autonomo come Lemon.

EDIT: E un trucco generale per gestire le righe di comando con sintassi complessa è quello di ricomporre gli argomenti in una singola stringa separata da spazi bianchi e quindi analizzarli correttamente come se fosse un'espressione di un linguaggio specifico del dominio.


+1 bel link, mi chiedo se è disponibile su Github o qualcos'altro. E i termini di utilizzo?
Hacre,

1

Non hai dato molti dettagli sulla tua grammatica, solo alcuni esempi. Quello che posso vedere è che ci sono alcune stringhe, spazi bianchi e una (probabilmente, il tuo esempio è indifferente nella tua domanda) stringa tra virgolette doppie e poi una ";" alla fine.

Sembra che questo potrebbe essere simile alla sintassi di PHP. In tal caso, PHP viene fornito con un parser, è possibile riutilizzarlo e convalidarlo in modo più concreto. Infine, devi gestire i token, ma sembra che questo sia semplicemente da sinistra a destra, quindi in realtà solo un'iterazione su tutti i token.

Alcuni esempi per riutilizzare il parser token PHP ( token_get_all) sono riportati nelle risposte alle seguenti domande:

Entrambi gli esempi contengono anche un semplice parser, probabilmente qualcosa di simile si adatta al tuo scenario.


si, ho fatto in fretta la grammatica, la aggiungerò ora.
alfa64,

1

Se i tuoi bisogni sono semplici, ed entrambi avete il tempo e vi interessano, andrò controcorrente qui e dirò di non esitare a scrivere il vostro parser. È una buona esperienza di apprendimento, se non altro. Se hai requisiti più complessi - chiamate di funzione nidificate, array, ecc. - Tieni presente che farlo potrebbe richiedere un bel po 'di tempo. Uno dei maggiori aspetti positivi del roll-up è che non ci sarà un problema di integrazione con il sistema. Il rovescio della medaglia è, ovviamente, tutti i problemi sono colpa tua.

Lavora contro i token, tuttavia, non utilizzare comandi codificati. Quindi quel problema con comandi simili dal suono scompare.

Tutti raccomandano sempre il libro dei draghi, ma ho sempre trovato "Introduzione a compilatori e interpreti" di Ronald Mak una migliore introduzione.


0

Ho scritto programmi che funzionano così. Uno era un bot IRC che aveva una sintassi di comando simile. C'è un file enorme che è una grande dichiarazione switch. Funziona - funziona velocemente - ma è un po 'difficile da mantenere.

Un'altra opzione, che ha una rotazione più OOP, è quella di utilizzare i gestori di eventi. Si crea un array chiave-valore con comandi e le loro funzioni dedicate. Quando viene dato un comando, si controlla se l'array ha la chiave fornita. In tal caso, chiamare la funzione. Questa sarebbe la mia raccomandazione per il nuovo codice.


ho letto il tuo codice ed è esattamente lo stesso schema del mio codice, ma come ho detto, se vuoi che altre persone usino, devi aggiungere il controllo degli errori e roba del genere
alfa64,

1
@ alfa64 Aggiungi eventuali chiarimenti alla domanda, anziché commenti. Non è molto chiaro cosa stai chiedendo esattamente, anche se è in qualche modo chiaro che stai cercando qualcosa di veramente specifico. Se è così, dicci esattamente di cosa si tratta. Non credo sia molto facile passare da I think my implementation is very crude and faultya but as i stated, if you want other people to use, you need to add error checking and stuff... Dicci esattamente cosa è rozzo e difettoso, ti aiuterebbe a ottenere risposte migliori.
yannis,

certo, rielaborerò la domanda
alfa64 il

0

Suggerisco di usare uno strumento, invece di implementare un compilatore o un interprete. L'ironia usa C # per esprimere la grammatica della lingua di destinazione (la grammatica della riga di comando). La descrizione su CodePlex dice: "Irony è un kit di sviluppo per l'implementazione di linguaggi su piattaforma .NET."

Vedi l'homepage ufficiale di Irony su CodePlex: Irony - .NET Language Implementation Kit .


Come lo useresti con PHP?
SK-logic,

Non vedo alcun tag PHP o riferimento a PHP nella domanda.
Olivier Jacot-Descombes il

Vedo, originariamente riguardava PHP, ma ora riscritto.
SK-logic,

0

Il mio consiglio sarebbe google per una libreria che risolva il tuo problema.

Ultimamente ho usato NodeJS e Optimist è quello che uso per l'elaborazione da riga di comando. Ti incoraggio a cercare quello che puoi usare per la tua lingua preferita. In caso contrario ... scrivine uno e open source: D Puoi persino leggere il codice sorgente di Optimist e portarlo nella tua lingua preferita.


0

Perché non semplifichi un po 'le tue esigenze?

Non usare un parser completo, è troppo complesso e persino inutile per il tuo caso.

Crea un ciclo, scrivi un messaggio che rappresenta il tuo "prompt", può essere il percorso corrente che sei.

Attendi una stringa, "analizza" la stringa e fai qualcosa a seconda del contenuto della stringa.

La stringa potrebbe "analizzare" come aspettarsi una linea, in cui gli spazi sono i separatori ("tokenizer"), e il resto dei caratteri sono raggruppati.

Esempio.

Il programma emette (e rimane nella stessa riga): / user / files / L'utente scrive (nella stessa riga) elenca tutti;

Il tuo programma genererà un elenco, una raccolta o un array come

list

all;

o se ";" è considerato un separatore come gli spazi

/user/files/

list

all

Il tuo programma potrebbe iniziare prevedendo una singola istruzione, senza "pipe" di tipo unix, né reindirizzamento di tipo windowze.

Il tuo programma potrebbe creare un dizionario di istruzioni, ciascuna istruzione potrebbe avere un elenco di parametri.

Il modello di progettazione del comando si applica al tuo caso:

http://en.wikipedia.org/wiki/Command_pattern

Questo pseudocodice "semplice c", non è testato o finito, solo un'idea di come si potrebbe fare.

Potresti anche renderlo più orientato agli oggetti e, nel linguaggio di programmazione, ti piace.

Esempio:


// "global function" pointer type declaration
typedef
  void (*ActionProc) ();

struct Command
{
  char[512] Identifier;
  ActionProc Action; 
};

// global var declarations

list<char*> CommandList = new list<char*>();
list<char*> Tokens = new list<char*>();

void Action_ListDirectory()
{
  // code to list directory
} // Action_ListDirectory()

void Action_ChangeDirectory()
{
  // code to change directory
} // Action_ChangeDirectory()

void Action_CreateDirectory()
{
  // code to create new directory
} // Action_CreateDirectory()

void PrepareCommandList()
{
  CommandList->Add("ls", &Action_ListDirectory);
  CommandList->Add("cd", &Action_ChangeDirectory);
  CommandList->Add("mkdir", &Action_CreateDirectory);

  // register more commands
} // void PrepareCommandList()

void interpret(char* args, int *ArgIndex)
{
  char* Separator = " ";
  Tokens = YourSeparateInTokensFunction(args, Separator);

  // "LocateCommand" may be case sensitive
  int AIndex = LocateCommand(CommandList, args[ArgIndex]);
  if (AIndex >= 0)
  {
    // the command

    move to the next parameter
    *ArgIndex = (*ArgIndex + 1);

    // obtain already registered command
    Command = CommandList[AIndex];

    // execute action
    Command.Action();
  }
  else
  {
    puts("some kind of command not found error, or, error syntax");
  }  
} // void interpret()

void main(...)
{
  bool CanContinue = false;
  char* Prompt = "c\:>";

  char Buffer[512];

  // which command line parameter string is been processed
  int ArgsIndex = 0;

  PrepareCommandList();

  do
  {
    // display "prompt"
    puts(Prompt);
    // wait for user input
      fgets(Buffer, sizeof(Buffer), stdin);

    interpret(buffer, &ArgsIndex);

  } while (CanContinue);

} // void main()

Non hai menzionato il tuo linguaggio di programmazione. Puoi anche citare qualsiasi linguaggio di programmazione, ma preferibilmente "XYZ".


0

hai diversi compiti davanti a te.

guardando le tue esigenze ...

  • Devi analizzare il comando. È un compito abbastanza semplice
  • Devi avere un linguaggio di comando estensibile.
  • È necessario disporre di controllo degli errori e suggerimenti.

Il linguaggio di comando estensibile indica che è necessario un DSL. Suggerirei di non creare il tuo, ma di utilizzare JSON se le tue estensioni sono semplici. Se sono complessi, la sintassi di un'espressione s è piacevole.

Il controllo degli errori implica che il sistema sia a conoscenza anche dei possibili comandi. Sarebbe parte del sistema post-comando.

Se avessi implementato un tale sistema da zero, avrei usato Common Lisp con un lettore ridotto. Ogni token di comando verrebbe mappato su un simbolo, che verrebbe specificato in un file RC di s-expression. Dopo la tokenizzazione, verrebbe valutato / espanso in un contesto limitato, intrappolando gli errori e qualsiasi modello di errore riconoscibile restituirebbe suggerimenti. Successivamente, il comando effettivo verrebbe inviato al sistema operativo.


0

C'è una bella caratteristica nella programmazione funzionale che potresti essere interessato a esaminare.

Si chiama pattern matching .

Ecco due link per alcuni esempi di abbinamento di modelli in Scala e in F # .

Concordo con te sul fatto che usare le switchstrutture sia un po 'noioso, e mi è particolarmente piaciuto usare la corrispondenza patern durante l'implementazione di un compilatore in Scala.

In particolare, ti consiglio di dare un'occhiata all'esempio di calcolo lambda del sito web Scala.

Questo è, secondo me, il modo più intelligente di procedere, ma se devi attenersi rigorosamente a PHP, allora sei bloccato con la "vecchia scuola" switch.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.