cmd - processori di riga di comando

Scopo: Crea processori di riga di comando.

Il modulo cmd contiene una classe pubblica: Cmd, progettata per l'uso come classe base per shell interattive ed altri interpreti di comando. Nella modalità predefinita usa readline per la gestione del prompt interattivo, per la modifica della riga di comando e per il completamento del comando.

Elaborazione dei Comandi

Un interprete di comando creato con cmd usa un ciclo per leggere tutte le righe dal suo input, analizzarle, quindi inviare il comando all'appropriato gestore di comando. Le righe in input sono elaborate in due parti. Il comando, quindi qualsiasi altro testo nella riga. Se si digita il comando foo bar, e l'interprete della propria classe comprende un metodo chiamato do_foo(), esso viene chiamato con "bar" come unico argomento.

Il marcatore di fine file viene inviato a do_EOF(). Se un gestore di comando restituisce un valore True, il programma uscirà in modo pulito. Quindi per fare sì che il proprio interprete esca in modo pulito, ci si deve assicurare di implementare do_EOF() in modo che restituisca True.

Questo semplice programma di esempio supporta il comando "greet" (saluta):

# cmd_simple.py

import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, line):
        print("Salve")

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Eseguendolo interattivamente, si può dimostrare come i comandi vengano inviati, e mostrare qualcuna delle caratteristiche di cui Cmd è già dotato.

$ python3 cmd_simple.py

(Cmd)

La prima cosa da notare è il prompt di comando, (Cmd). Il prompt può essere configurato attraverso l'attributo prompt. Questo valore è dinamico, e se un gestore di comando cambia l'attributo, il nuovo valore è utilizzato per richiedere il comando successivo.

(Cmd) help

Documented commands (type help <topic>):
========================================
help

Undocumented commands:
======================
EOF  greet

Il comando help è costruito dentro Cmd. Senza argomenti, mostra l'elenco dei comandi disponibili. Se nell'input si include il nome di un comando, il risultato è più particolareggiato e confinato ai dettagli di quel comando, se disponibili.

Se si usa il comando greet, viene chiamato do_greet() per gestirlo:

(Cmd) greet
Salve

Se la propria classe non include uno specifico gestore per un comando, viene chiamato il metodo default() con l'intero input della riga come argomento. L'implementazione built-in di default() segnala un errore.

(Cmd) foo
*** Unknown syntax: foo

Visto che do_EOF() restituisce True, se si digita Ctrl-D si esce dall'interprete.

(Cmd) ^D$

Non viene stampata una nuova riga in uscita, quindi il risultato è un poco confuso.

Argomenti di cmd

Questo esempio comprende qualche miglioria per eliminare qualche fastidio ed aggiungere un aiuto per il comando greet.

# cmd_arguments.py

import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, person):
        """greet [person]
        Saluta la persona"""
        if person:
            print("Salve,", person)
        else:
            print('Salve')

    def do_EOF(self, line):
        return True

    def postloop(self):
        print()


if __name__ == '__main__':
    HelloWorld().cmdloop()

La docstring aggiunta a do_greet() diventa il testo di aiuto per il comando:

$ python3  cmd_arguments.py

(Cmd) help

Documented commands (type help <topic>):
========================================
greet  help

Undocumented commands:
======================
EOF

(Cmd) help greet
greet [person]
        Saluta la persona
(Cmd)

Il risultato mostra un argomento opzionale per il comando greet: person. Sebbene l 'argomento sia opzionale per il comando c'è distinzione tra il comando ed il metodo di callback. Il metodo riceve sempre l' argomento, ma talvolta il valore è una stringa vuota. E' compito del gestore di comando determinare se un argomento vuoto sia valido, oppure se occorre eseguire una ulteriore analisi ed elaborazione del comando. In questo esempio, se il nome di una persona viene fornito, allora il saluto viene personalizzato.

(Cmd) greet Alice
Salve, Alice
(Cmd) greet
Salve

A prescindere dal fatto che un argomento sia fornito dall'utente o meno, il valore passato al gestore di comando non comprende il comando stesso. Questo semplifica l'analisi al gestore di comando, specialmente nel caso siano necessari parametri multipli.

Aiuto in Diretta

Nell'esempio precedente, la formattazione del testo di aiuto lascia un poco a desiderare. Visto che viene ricavato dalla docstring mantiene l'indentazione della sorgente. Si potrebbe modificare la sorgente togliendo qualche spazio extra, tuttavia il codice dell'applicazione risulterebbe scarsamente formattato. Una soluzione migliore è implementare un gestore di aiuto per il comando greet, chiamato help_greet(). Il gestore di aiuto viene chiamato per produrre un testo di aiuto per il comando specificato.

# cmd_do_help.py

# Imposta gnureadline se readline è installato
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, person):
        if person:
            print("salve,", person)
        else:
            print('salve')

    def help_greet(self):
        print('\n'.join([
            'greet [person]',
            'Saluta la persona con il nome ricevuto',
        ]))

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

In questo esempio, il testo è statico ma la formattazione è migliore. Sarebbe stato anche possibilie usare lo stato del comando precedente per modellare il contenuto del testo di aiuto sul contesto corrente.

$ python3 cmd_do_help.py

(Cmd) help greet
greet [person]
Saluta la persona con il nome ricevuto

Tocca al gestore di aiuto la stampa del messaggio di aiuto, che non deve semplicemente restituire del testo per essere gestito altrove.

Auto-Completamento

Cmd include il supporto per il completamento del comando in base ai nomi dei comandi tramite metodi di gestione. L'utente attiva li completamento tramite la pressione del tasto tab al prompt. Se ci sono opzioni multiple di completamento, premendo tab due volte viene stampato un elenco di opzioni.

Le librerie GNU richieste per readline non sono disponibili in tutte le piattaforme nella modalità predefinita. In questi casi il completamento via tab potrebbe non funzionare. Si faccia riferimento a readline per suggerimenti sull'installazione delle librerie necessarie se la propria installazione Python non ne dispone.
$ python3 cmd_do_help.py
(Cmd) <tab><tab>
EOF    greet  help
(Cmd) h<tab>
(Cmd) help

Una volta che il comando viene riconosciuto, il completamento del argomento viene gestita dai metodi il cui nome inizia per complete_. Questo consente ai gestori di completamento di assemblare un elenco di possibili completamenti usando criteri arbitrari (es. interrogare un database, cercare in un file o directory nel filesystem). In questo caso, il programma ha scritto al suo interno un gruppo di "amici" che ricevono un saluto meno formale rispetto a nomi di sconosciuti od anonimi. Un vero programma probabilmente salverebbero l'elenco da qualche parte, per leggerlo interamente la prima volta, quindi conservare quanto letto per poterlo scorrere se necessario.

# cmd_arg_completion.py

# Imposta gnureadline come readline se installato.
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class HelloWorld(cmd.Cmd):

    FRIENDS = ['Alice', 'Adam', 'Barbara', 'Bob']

    def do_greet(self, person):
        "Saluta la persona"
        if person and person in self.FRIENDS:
            greeting = 'Ciao, {}!'.format(person)
        elif person:
            greeting = 'Salve, {}'.format(person)
        else:
            greeting = 'Salve'
        print(greeting)

    def complete_greet(self, text, line, begidx, endidx):
        if not text:
            completions = self.FRIENDS[:]
        else:
            completions = [
                f
                for f in self.FRIENDS
                if f.startswith(text)
            ]
        return completions

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Se c'è un testo in input, complete_greet() restituisce un elenco di amici che corrispondono. Altrimenti viene restituita l'intera lista di amici.

$ python3 cmd_arg_completion.py
(Cmd) greet <tab><tab>
Adam     Alice    Barbara  Bob
(Cmd) greet A<tab><tab>
Adam   Alice
(Cmd) greet Ad<tab>
Ciao, Adam!

Se il testo passato non è nell'elenco di amici viene fornita la formula di saluto formale.

(Cmd)greet Joe
Salve, Joe

Riscrittura dei Metodi Base della Classe

Cmd comprende parecchi metodi che possono essere riscritti come agganci per compiere azioni o per alterare il comportamento della classe base. Questo esempio non è esaustivo, ma contiene molti dei metodi che sono utili nell'uso comune.

# cmd_illustrate_methods.py

# Imposta gnureadline come readline se installato.
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class Illustrate(cmd.Cmd):
    "Illustra l'uso del metodo della classe base."

    def cmdloop(self, intro=None):
        print('cmdloop({})'.format(intro))
        return cmd.Cmd.cmdloop(self, intro)

    def preloop(self):
        print('preloop()')

    def postloop(self):
        print('postloop()')

    def parseline(self, line):
        print('parseline({!r}) =>'.format(line), end='')
        ret = cmd.Cmd.parseline(self, line)
        print(ret)
        return ret

    def onecmd(self, s):
        print('onecmd({})'.format(s))
        return cmd.Cmd.onecmd(self, s)

    def emptyline(self):
        print('emptyline()')
        return cmd.Cmd.emptyline(self)

    def default(self, line):
        print('default({})'.format(line))
        return cmd.Cmd.default(self, line)

    def precmd(self, line):
        print('precmd({})'.format(line))
        return cmd.Cmd.precmd(self, line)

    def postcmd(self, stop, line):
        print('postcmd({}, {})'.format(stop, line))
        return cmd.Cmd.postcmd(self, stop, line)

    def do_greet(self, line):
        print('Salve,', line)

    def do_EOF(self, line):
        "Exit"
        return True


if __name__ == '__main__':
    Illustrate().cmdloop('Illustrazione dei metodi di cmd.Cmd')

cmdloop() è il ciclo principale di elaborazione dell'interprete. Si può sovrascrivere, ma in genere non è necessario, visto che sono disponibili gli agganci con preloop() e postloop().

Ogni iterazione attraverso cmdloop() chiama onecmd() per inviare il comando al suo gestore. L'effettiva riga di input viene elaborata da parseline() per creare una tupla contenente il comando e la parte rimanente della riga.

Se la riga è vuota, viene chiamato emptyline(). L'implementazione predefinita esegue nuovamente il comando precedente. Se la riga contiene un comando, prima viene chiamato precmd(), quindi viene cercato il gestore e chiamato. Se non viene trovato, viene chiamato default(). Infine viene invocato postcmd().

Ecco una sessione di esempio alla quale sono state aggiunte delle istruzioni print:

$ python3 cmd_illustrate_methods.py

cmdloop(Illustra l'uso dei metodi base della classe.)
preloop()
Illustra l'uso dei metodi base della classe.
(Cmd) greet Bob
precmd(greet Bob)
onecmd(greet Bob)
parseline(greet Bob) => ('greet', 'Bob', 'greet Bob')
Salve, Bob
postcmd(None, greet Bob)
(Cmd) ^Dprecmd(EOF)
onecmd(EOF)
parseline(EOF) => ('EOF', '', 'EOF')
postcmd(True, EOF)
postloop()

Configurare cmd Tramite Attributi

Oltre ai metodi sopra descritti, ci sono parecchi attributi per controllare gli interpreti di comando. prompt può essere impostato come stringa da stamparsi ogni volta che l'utente richiede un nuovo comando. intro è il messaggio di benvenuto stampato all'inizio del programma. cmdloop() ottiene un argomento per questo valore, oppure si può impostarlo direttamente nelle classe. Quando si stampa l'aiuto, gli attributi doc_header, misc_header, undoc_header e ruler vengono usati per formattare il risultato.

# cmd_attributes.py

import cmd


class HelloWorld(cmd.Cmd):

    prompt = 'prompt: '
    intro = "Esempio di semplice processore comando."

    doc_header = 'doc_header'
    misc_header = 'misc_header'
    undoc_header = 'undoc_header'

    ruler = '-'

    def do_prompt(self, line):
        "Cambia il prompt interattivo"
        self.prompt = line + ': '

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Questa classe di esempio mostra un gestore di comando che consente all'utente di controllare il prompt per la sessione interattiva.

$ python3 cmd_attributes.py

Esempio di semplice processore comando.
prompt: prompt salve
salve: help

doc_header
----------
help  prompt

undoc_header
------------
EOF

salve:

Eseguire Comandi di Shell

Per integrare l'elaborazione standard del comando, Cmd comprende due prefissi speciali di comando. Un punto interrogativo (?) equivale al comando built-in help, e può essere usato allo stesso modo. Un punto esclamativo (!) è mappato a do_shell(), ed è concepito per eseguire altri comandi al di fuori della shell, come in questo esempio.

# cmd_do_shell.py

import cmd
import subprocess


class ShellEnabled(cmd.Cmd):

    last_output = ''

    def do_shell(self, line):
        "Esegue un comando shell"
        print("esecuzione di comando shell:", line)
        sub_cmd = subprocess.Popen(line,
                                   shell=True,
                                   stdout=subprocess.PIPE)
        output = sub_cmd.communicate()[0].decode('utf-8')
        print(output)
        self.last_output = output

    def do_echo(self, line):
        """Stampa l'input, sostituendo '$out' con
        il risultato dell'ultimo comando shell
        """
        # Ovviamente non robusto
        print(line.replace('$out', self.last_output))

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    ShellEnabled().cmdloop()

Questa implementazione del comando echo sostituisce la stringa $out nei suoi argomenti con il risultato dal comando shell precedente.

$ python3 cmd_do_shell.py

(Cmd) ?

Documented commands (type help <topic>):
========================================
echo  help  shell

Undocumented commands:
======================
EOF

(Cmd) ? shell
Esegue un comando shell
(Cmd) ? echo
Stampa l'input, sostituendo '' con
        il risultato dell'ultimo comando shell

(Cmd) shell pwd
esecuzione di comando shell: pwd
.../dumpscripts

(Cmd) echo $out
.../dumpscripts

(Cmd)

Input Alternativi

Sebbene la modalità predefinita per Cmd() sia l'interazione con l'utente attraverso la libreria readline è tuttavia possibile passare una serie di comandi verso lo standard input usando la redirezione standard della shell Unix.

$ echo help | python3 cmd_do_help.py

(Cmd)
Documented commands (type help <topic>):
========================================
greet  help

Undocumented commands:
======================
EOF

(Cmd)

Se si preferisce che sia un programma a leggere il file script direttamente, potrebbe essere necessario qualche altro cambiamento. Visto che readline interagisce con il dispositivo termnal/tty piuttosto che con il flusso standard di input, si dovrebbe disabilitare quando lo script andrà a leggere da un file. Inoltre, per evitare la stampa di prompt superflui, si può impostare il prompt ad una stringa vuota. Questo esempio mostra come aprire un file e passarlo come input ad una versione modificata dell'esempio HelloWorld:

# cmd_file.py

import cmd


class HelloWorld(cmd.Cmd):

    # Disabilita l'uso del modulo rawinput
    use_rawinput = False

    # Non mostra il prompt dopo ogni lettura di comando
    prompt = ''

    def do_greet(self, line):
        print("Salve,", line)

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    import sys
    with open(sys.argv[1], 'rt') as input:
        HelloWorld(stdin=input).cmdloop()

Con use_rawinput impostato a False e prompt impostato ad una stringa vuota, si può chiamare lo script su questo file di input che contiene un comando su ogni riga.

# cmd_file.txt

greet
greet Alice e Bob

L'esecuzione dello script con il file sopra citato produce questo risultato:

$ python3 cmd_file.py cmd_file.txt

Salve,
Salve, Alice e Bob

Comandi da sys.argv

Anche gli argomenti da riga di comando per il programma possono essere elaborati come comandi per la classe dell'interprete, invece che leggere comandi dalla console o da un file. Per usare gli argomenti da riga di comando, occorre chiamare onecmd() direttamente, come in questo esempio:

# cmd_argv.py

import cmd


class InteractiveOrCommandLine(cmd.Cmd):
    """Accetta comandi tramite il normale prompt
    interattivo o sulla riga di comando
    """

    def do_greet(self, line):
        print('Salve,', line)

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    import sys
    if len(sys.argv) > 1:
        InteractiveOrCommandLine().onecmd(' '.join(sys.argv[1:]))
    else:
        InteractiveOrCommandLine().cmdloop()

Visto che onecmd() riceve una singola stringa come input, gli argomenti per il programma devono essere uniti prima di essere passati.

$ python3 cmd_argv.py greet Utente da Riga di Comando

Salve, Utente di Riga di Comando
$ python3 cmd_argv.py

(Cmd) greet Utente Interattivo
Salve, Utente Interattivo
(Cmd)

Vedere anche:

cmd
La documentazione della libreria standard per questo modulo.
cmd2
Rimpiazzo drop-in per cmd con funzionalità aggiuntive.
GNU readline
La libreria GNU Readline fornisce funzioni che consentono all'utente di modificare le righe di input mentre vengono digitate.
readline
L'interfaccia della libreria standard di Python per readline
subprocess
Gestire altri processi ed i loro risultati