Tvorba aplikací s příkazovým řádkem v Pythonu s knihovnami GNU Readline a prompt_toolkit

10. 7. 2018
Doba čtení: 24 minut

Sdílet

Python ve své základní knihovně obsahuje funkci pro čtení dat zapsaných na standardní vstup. Pro plnohodnotné aplikace s příkazovým řádkem (GNU Octave atd.) je však nutné použít uživatelsky přívětivější řešení.

Obsah

1. Využití funkce input() bez dalších rozšíření

2. Rozhraní mezi Pythonem a knihovnou GNU Readline

3. Ukázka základního použití knihovny readline

4. Jednoduché automatické doplňování příkazů

5. Vylepšení automatického doplňování příkazů

6. Použití parametru state v okamžiku většího množství dostupných příkazů

7. Konfigurace knihovny GNU Readline – soubor .inputrc

8. Knihovna prompt_toolkit

9. Instalace knihovny prompt_toolkit

10. Využití funkce prompt_toolkit.prompt

11. Režim zápisu hesla

12. Objekt typu PromptSession

13. Automatické doplňování příkazů s využitím třídy WordCompleter

14. Režim emulující příkazy editorů Vi a Vim

15. Režim emulující příkazy editoru Emacs

16. Použití objektu PromptSession v režimu zadávání hesla

17. Standardní klávesové zkratky použité knihovnou GNU Readline v režimu Emacs

18. Standardní klávesové zkratky použité knihovnou GNU Readline v režimu Vi

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Využití funkce input() bez dalších rozšíření

Popis možností Pythonu při čtení dat z příkazové řádky začneme tou nejzákladnější funkcí, která je součástí standardního modulu Pythonu (a není ji tedy zapotřebí importovat). Tato funkce se v Pythonu 3.x jmenuje input() a slouží k načtení řetězce ze standardního vstupu, ovšem s tím, že se z řetězce odstraní znak konce řádku. Této funkci je možné předat nepovinný řetězec, který bude zobrazen jako výzva (prompt) na terminálu:

>>> help("input")
 
Help on built-in function input in module builtins:
 
input(...)
    input([prompt]) -> string
 
    Read a string from standard input.  The trailing newline is stripped.
    If the user hits EOF (Unix: Ctl-D, Windows: Ctl-Z+Return), raise EOFError.
    On Unix, GNU readline is used if enabled.  The prompt string, if given,
    is printed without a trailing newline before reading.

V Pythonu 2.x je situace nepatrně složitější, protože stejnou činnost vykonává funkce nazvaná raw_input(). Funkce input(), která taktéž v Pythonu 2.x existuje, se navíc snaží řetězec zapsaný na standardní vstup zpracovat funkcí eval() (a tedy například naparsovat celé číslo, seznam atd.), což většinou není činnost, kterou v praxi vyžadujeme:

>>> help("input")
 
input(...)
    input([prompt]) -> value
 
    Equivalent to eval(raw_input(prompt)).

V dalším textu budeme předpokládat použití Pythonu 3.x. Následující příklad je skutečně velmi jednoduchý a ukazuje, jakým způsobem lze použít funkci input() pro načtení příkazu (prozatím se tento příkaz pouze vytiskne zpět na terminál a dále s ním nebudeme pracovat):

cmd = input("Command: ")
print(cmd)
Poznámka: sami si vyzkoušejte, že základní chování funkce input() je velmi primitivní – z editovacích příkazů bude fungovat pouze klávesa Delete a dokonce ani nebude možné využít kurzorové šipky. Chování se ovšem změní při zavolání této funkce přímo ze smyčky REPL. Důvod je vysvětlen ve druhé kapitole.

Druhý příklad je nepatrně složitější, protože v něm vytváříme kostru „aplikace“, která postupně načítá příkazy ze svého příkazového řádku a nějakým způsobem na ně reaguje. Prvotní verze této aplikace pouze rozpoznává trojici příkazů „quit“, „exit“ a „eval“, popř. jejich zkrácené varianty. Zpracování příkazů je realizováno v nekonečné smyčce ukončení až zadáním příkazu „quit“, popř. jiným násilným ukončením aplikace (Ctrl+D atd.):

def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
while True:
    cmd = input("Command: ")
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

2. Rozhraní mezi Pythonem a knihovnou GNU Readline

Již v nápovědě vypsané v úvodní kapitole k funkci input() jsme si mohli přečíst zmínku o knihovně GNU Readline. Tuto knihovnu je totiž možné využít pro vylepšení vlastností funkce input(), především pak pro přidání těchto vlastností:

  1. Možnost použití kurzorových šipek pro pohyb na řádku se zapisovaným vstupním textem
  2. Příkazy do určité míry emulující editory Emacs či Vi (v Emacs režimu například Ctrl+A pro přechod na začátek řádku, Ctrl+W pro smazání slova atd.)
  3. Doplňování příkazů (vstupních dat), což je téma, které si popíšeme ve čtvrté kapitole
  4. Vyhledávání v historii již zadaných příkazů (vstupních dat). Tuto funkcionalitu je možné zajistit i externě s využitím knihovny rlwrap.
Poznámka: ve skutečnosti se můžeme setkat i s tím, že se namísto knihovny GNU Readline používá knihovna libedit (editline, viz též http://thrysoee.dk/editline/). Z pohledu uživatele spočívá největší rozdíl v tom, že tato knihovna využívá odlišný konfigurační soubor.

I o podpoře GNU Readline v Pythonu se dozvíme přímo z integrované nápovědy. Tentokrát se bude nápověda týkat modulu nazvaného readline. K nápovědě k tomuto modulu se lze dostat několika způsoby. První způsob využívá přechodu do režimu nápovědy zavoláním funkce help() z interaktivní smyčky REPL:

$ python3
 
Python 3.4.3 (default, Nov 28 2017, 16:41:13)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
 
 
>>> help()
 
Welcome to Python 3.4's help utility!
 
 
help> readline
 
Help on module readline:
 
NAME
...
...
...

Druhý způsob přímo vyvolá nápovědu k modulu, a to bez přechodu do režimu nápovědy:

>>> help("readline")

3. Ukázka základního použití knihovny readline

Aby došlo – a to prakticky zcela automaticky – k rozšíření možností funkce input() o většinu výše uvedených vlastností (plně editovatelný vstupní řádek, historie zadaných dat atd.), postačuje doplnit náš demonstrační příklad o jediný řádek:

import readline

V průběhu importu tohoto modulu dojde k inicializaci knihovny GNU Readline a po spuštění příkladu již bude možné použít kurzorové šipky a další dále popsané editační operace, historii již zadaných vstupních dat (příkazů) a vyhledávání v této historii. Žádné další úpravy v příkladu přitom není nutné provádět:

import readline
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
while True:
    cmd = input("Command: ")
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

Interně nyní bude funkce input() používat například historii dat zapsaných do vstupního řádku atd., takže bude možné využít kurzorových šipek pro průchod historií, vyhledání v historii atd.

Poznámka: knihovna GNU Readline je použita i v interaktivní smyčce REPL Pythonu, což způsobuje odlišné chování funkce input() v případě, že je z REPL zavolána.

4. Jednoduché automatické doplňování příkazů

Jedna z nejužitečnějších vlastností knihovny GNU Readline spočívá v možnosti naprogramování automatického doplňování příkazů (nebo jiných vstupních dat) po stisku klávesy Tab. Tuto funkci pravděpodobně znáte z shellu popř. přímo z REPLu Pythonu (do dokonalosti je dovedena v IPythonu). Technologie automatického doplňování je založena na volání callback funkce nazývané completer přímo z knihovny GNU Readline. Této funkci se předávají dva údaje – již zapsaný (částečný) text a celočíselná hodnota state, jejíž význam bude vysvětlen v navazujících kapitolách. Pokud completer vrátí nějaký text, bude si ho knihovna GNU Readline pamatovat jako jedno z možných slov pro automatické doplnění. Pokud naopak vrátí hodnotu None, znamená to, že další volání této callback funkce již nemá význam a bude ukončeno.

Podívejme se nyní na jednu (prozatím značně primitivní) implementaci completeru. Tato implementace bude pracovat následovně: pokud je hodnota state nastavena na hodnotu 0 (první volání completeru), bude vstupní text analyzován a pokud bude odpovídat začátku nějakého známého příkazu, vrátí se jeho plná podoba. Tj. například při vstupu „qu“ se vrátí text „quit“ atd. Při druhém volání completeru se již pouze vrátí hodnota None znamenající, že GNU Readline vždy zkompletuje celý příkaz a nikdy nenabídne více než jednu možnost (alternativní implementaci si uvedeme v dalším textu):

def completer(text, state):
    # print(text, state)
    if state >= 1:
        return None
    if text in {"q", "qu", "qui"}:
        return "quit"
    elif text in {"e", "ex", "exi"}:
        return "exit"
    elif text in {"h", "he", "hel"}:
        return "help"
    else:
        return text

Povolení doplňování příkazů a nastavení completeru se provede takto. První řádek nastaví completer, druhý řádek pro jistotu nastaví mapování klávesy Tab na operaci „complete“:

readline.set_completer(completer)
readline.parse_and_bind("tab: complete")

Úplný zdrojový text příkladu, který podporuje automatické doplňování jmen tří příkazů, bude vypadat následovně:

import readline
 
 
def completer(text, state):
    # print(text, state)
    if state >= 1:
        return None
    if text in {"q", "qu", "qui"}:
        return "quit"
    elif text in {"e", "ex", "exi"}:
        return "exit"
    elif text in {"h", "he", "hel"}:
        return "help"
    else:
        return text
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
readline.set_completer(completer)
readline.parse_and_bind("tab: complete")
 
while True:
    cmd = input("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

5. Vylepšení automatického doplňování příkazů

Předchozí implementace completeru nebyla příliš efektivní, a to minimálně ze dvou důvodů. První důvod spočíval v tom, že bylo nutné postupně zadávat všechny možné zkrácené tvary příkazů a navíc byly tyto tvary přímo součástí těla příslušné callback funkce. Ovšem completer lze relativně snadno vylepšit. Nejdříve všechny dostupné příkazy (nikoli jejich zkrácené tvary!) uložíme do proměnné mimo vlastní callback funkci, takže je bude možné snadno modifikovat nebo načíst z konfiguračního souboru:

WORDS = ("quit", "exit", "eval", "help")

Dále vlastní implementaci completeru upravíme takovým způsobem, aby se na základě částečného textu zadaného uživatelem našel první vyhovující příkaz. Je to snadné – budeme procházet n-ticí WORDS s příkazy a pokud zadaný n-znakový text bude odpovídat prvním n znakům příkazu, vrátíme přímo úplný tvar příkazu. Případné nejednoznačnosti typu „e→exit“ a „e→eval“ jsou vyřešeny pořadím příkazů (vrátí se první nalezený):

def completer(text, state):
    # print(text, state)
    if state >= 1:
        return None
    n = len(text)
    for word in WORDS:
        if text == word[:n]:
            return word
    else:
        return text

Opět si ukažme plnou verzi zdrojového kódu tohoto demonstračního příkladu:

import readline
 
 
WORDS = ("quit", "exit", "eval", "help")
 
 
def completer(text, state):
    # print(text, state)
    if state >= 1:
        return None
    n = len(text)
    for word in WORDS:
        if text == word[:n]:
            return word
    else:
        return text
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
readline.set_completer(completer)
readline.parse_and_bind("tab: complete")
 
while True:
    cmd = input("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

6. Použití parametru state v okamžiku většího množství dostupných příkazů

Předchozí implementace completeru trpěla poměrně vážným nedostatkem – pokud došlo při pokusu o doplnění příkazu k nejednoznačnosti (dva příkazy začínající na „e“), doplnil se první nalezený příkaz (v tomto případě „exit“) a uživatel se ani nedozvěděl o existenci příkazu alternativního. Toto chování je možné napravit, a to relativně snadno. Využijeme zde faktu, že parametr state se při každém volání pro stejný vstupní řetězec bude postupně zvyšovat o jedničku. Pokud jako návratovou hodnotu použijeme jiný řetězec, dojde k jeho zapamatování a následně knihovna GNU Readline zobrazí seznam všech dostupných alternativ formou „menu“. Naše úprava tedy bude spočívat v tom, že vrátíme n-tý příkaz odpovídající vstupu, samozřejmě za předpokladu, že takový příkaz vůbec existuje. Úprava není příliš efektivní, ovšem v našem příkladu se čtyřmi příkazy plně dostačující:

def completer(text, state):
    # print(text, state)
    matches = []
    n = len(text)
    for word in WORDS:
        if text == word[:n]:
            matches.append(word)
    if len(matches) >= state:
        return matches[state]
    else:
        return None

Opět si ukažme, jak může vypadat úplný zdrojový kód využívající vylepšený completer:

import readline
 
 
WORDS = ("quit", "exit", "eval", "help")
 
 
def completer(text, state):
    # print(text, state)
    matches = []
    n = len(text)
    for word in WORDS:
        if text == word[:n]:
            matches.append(word)
    if len(matches) >= state:
        return matches[state]
    else:
        return None
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
readline.set_completer(completer)
readline.parse_and_bind("tab: complete")
 
while True:
    cmd = input("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

7. Konfigurace knihovny GNU Readline – soubor .inputrc

Chování knihovny GNU Readline je možné ovlivnit parametry, které jsou zapisovány do souboru nazvaného .inputrc, jenž je umístěn v domácím adresáři uživatele a popř. ze souboru /etc/inputrc (tyto volby budou mít globální platnost). Tento soubor ve skutečnosti může obsahovat jak již zmíněné volby ovlivňující chování GNU Readline, tak i například tzv. makra, což jsou ovšem většinou pouhé expanze kódů kláves posílaných terminálem (typicky escape sekvence) na příkazy typu „přesun kurzoru na začátek zapisovaného textu“. Následuje příklad obsahu ~/.inputrc v případě, že uživatel požaduje, aby se ve všech aplikacích používajících GNU Readline používal režim emulující textové editory Vi/Vim:

set editing-mode vi

Více příkladů, včetně maker, nalezneme v souboru /usr/share/readline popř. v již zmíněném globálním konfiguračním souboru /etc/inputrc. Podívejme se jen na několik příkladů s mapováním escape sekvencí některých kláves na příkazy, které GNU Readline rozpoznává:

# Be 8 bit clean.
set input-meta on
set output-meta on
 
# allow the use of the Home/End keys
"\e[1~": beginning-of-line
"\e[4~": end-of-line
 
# allow the use of the Delete/Insert keys
"\e[3~": delete-char
"\e[2~": quoted-insert
 
# mappings for "page up" and "page down" to step to the beginning/end
# of the history
# "\e[5~": beginning-of-history
# "\e[6~": end-of-history
 
# alternate mappings for "page up" and "page down" to search the history
# "\e[5~": history-search-backward
# "\e[6~": history-search-forward
 
# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving
"\e[1;5C": forward-word
"\e[1;5D": backward-word
"\e[5C": forward-word
"\e[5D": backward-word
"\e\e[C": forward-word
"\e\e[D": backward-word
Poznámka: přesný význam jednotlivých příkazů, například forward-word, naleznete v manuálové stránce readline:
$ man readline

8. Knihovna prompt_toolkit

Druhou knihovnou, s jejímiž základními vlastnostmi se v dnešním článku alespoň ve stručnosti seznámíme, je knihovna, která se jmenuje prompt_toolkit. Název této knihovny sice naznačuje, že slouží pro implementaci vstupního (či příkazového) řádku do aplikací, to však není vše. Tato knihovna například umožňuje využít víceřádkový vstupní text, dovoluje použití myši (kromě implicitní funkce myši v terminálu pro operace copy & paste) a dokonce obsahuje sadu prvků uživatelského rozhraní (takzvaných widgetů), mezi něž patří například toolbary, menu, checkboxy, tlačítka, či dialogy. Díky tomu lze tuto knihovnu použít i pro tvorbu aplikací s plnohodnotným textovým uživatelským rozhraním (ne nepodobným starodávnému TurboVision). Dnes si sice ukážeme jen nepatrnou část funkcionality této knihovny, ovšem jejím dalším schopnostem bude později věnován samostatný článek.

9. Instalace knihovny prompt_toolkit

Instalaci knihovny prompt_toolkit provedeme pomocí nástroje pip nebo pip3 (podle zvolené verze Pythonu). Díky použití parametru –user je zajištěno, že se instalace provede do adresáře ~/.local/ (pro aktivního uživatele) a tudíž nemusí mít uživatel rootovská práva:

$ pip3 install --user prompt_toolkit
 
Downloading/unpacking prompt-toolkit
  Downloading prompt_toolkit-2.0.3-py3-none-any.whl (322kB): 322kB downloaded
Downloading/unpacking wcwidth (from prompt-toolkit)
  Downloading wcwidth-0.1.7-py2.py3-none-any.whl
Requirement already satisfied (use --upgrade to upgrade): six>=1.9.0 in ./.local/lib/python3.4/site-packages (from prompt-toolkit)
Installing collected packages: prompt-toolkit, wcwidth
Successfully installed prompt-toolkit wcwidth
Cleaning up...

10. Využití funkce prompt_toolkit.prompt

Ukažme si nyní základní způsob použití funkce prompt_toolkit.prompt, která efektivně nahrazuje výše popsanou standardní funkci input. Pokud nebudeme vyžadovat žádné další pokročilejší schopnosti (kromě plnohodnotné editace vstupního řádku!), může se funkce prompt použít následujícím způsobem:

from prompt_toolkit import prompt
 
cmd = prompt("Command: ")
print("Entered: '{cmd}'".format(cmd=cmd))

Tuto funkci můžeme velmi snadno přidat do našeho demonstračního příkladu akceptujícího trojici příkazů a nahradit tak volání input a současně i přímé použití knihovny GNU Readline:

from prompt_toolkit import prompt
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
while True:
    cmd = prompt("Command: ")
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

11. Režim zápisu hesla

V některých případech je nutné zajistit, aby se při zadávání údajů na vstupní řádku neprováděl opis (takzvané echo) zapisovaných znaků, ale aby se namísto skutečně zapisovaných znaků pouze ukazoval nějaký zástupný znak, typicky hvězdička. Tento režim se používá například při zadávání hesel a můžeme ho velmi snadno zapnout využitím nepovinného parametru is_password, který musí mít nastavenou hodnotu True. Použití tohoto režimu je snadné, což ostatně dokládá zdrojový kód další varianty našeho demonstračního příkladu:

from prompt_toolkit import prompt
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
while True:
    cmd = prompt("Command: ", is_password=True)
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

12. Objekt typu PromptSession

Funkce prompt_toolkit.prompt, kterou jsme si ukázali v předchozích dvou kapitolách, pracuje skutečně jako „pouhá“ funkce bez vnitřního stavu, což v důsledku znamená, že nedokáže zajistit práci s historií již zadaných příkazů (vstupních dat). Pokud podporu pro historii skutečně budeme potřebovat – a mnohdy se jedná o velmi užitečnou vlastnost – musíme namísto funkce prompt_toolkit.prompt využít třídu PromptSession, resp. přesněji řečeno instanci této třídy. Díky tomu, že se namísto funkce prompt_toolkit.prompt bude volat metoda PromptSession.prompt, bude možné (a to bez našeho dalšího zásahu) pracovat s historií vstupního řádku, tj. například listovat historií s využitím kurzorových šipek. Podívejme se na upravený příklad, v němž se objekt typu PromptSession konstruuje a používá:

from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
s = PromptSession()
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

13. Automatické doplňování příkazů s využitím třídy WordCompleter

Jednou z předností knihovny prompt_toolkit je existence předpřipravených tříd, které lze použít pro automatické doplňování slov nebo výrazů na vstupním řádku. Tříd existuje několik z toho důvodu, že kromě ve své podstatě primitivního doplňování slov na základě připraveného slovníku je možné naprogramovat například doplňování názvů metod v určeném kontextu apod. Dnes si představíme nejjednodušší z těchto tříd – WordCompleter. Tato třída slouží k pouhému automatickému doplňování slov na základě slovníku, tj. ke stejnému účelu, jaký jsme již použili v knihovně GNU Readline. Konstrukce instance třídy WordCompleter je snadná – konstruktoru předáme seznam slov a popř. nepovinný parametr informující o tom, zda se má nebo naopak nemá ignorovat velikost písmen:

c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)

Následně při konstrukci instance třídy PromptSession použijeme nepovinný parametr completer, kterému předáme referenci na dříve vytvořený objekt:

s = PromptSession(completer=c)

To jsou ve skutečnosti jediné dvě změny, které je nutné provést, aby automatické doplňování příkazů fungovalo, a to včetně zobrazení menu s nabídkou možností ve chvíli, kdy není doplnění jednoznačné. Ukažme si tedy úplný zdrojový kód upraveného demonstračního příkladu:

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)
s = PromptSession(completer=c)
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

14. Režim emulující příkazy editorů Vi a Vim

Další pro mnohé uživatele (zejména programátory či administrátory) užitečnou vlastností knihovny prompt_toolkit je možnost emulace příkazů textových editorů Vi a Vim. Tyto editory jsou modální, přičemž při jejich emulaci se knihovna implicitně nachází v režimu vstupu (insert mode), takže pro přechod do normálního režimu (normal mode) je nutné stisknout klávesu Esc. Jedna z variant přepnutí knihovny prompt_toolkit do režimu emulace Vi/Vim vypadá takto:

s = PromptSession(completer=c, vi_mode=True)

Celý demonstrační příklad:

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)
s = PromptSession(completer=c, vi_mode=True)
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

Druhá možnost využívá nepovinného parametru editing_mode předaného konstruktoru třídy PromptSession. Hodnota tohoto parametru se importuje z modulu enums:

s = PromptSession(completer=c, editing_mode=enums.EditingMode.VI)

Opět si ukažme celý demonstrační příklad:

from prompt_toolkit import PromptSession
from prompt_toolkit import enums
from prompt_toolkit.completion import WordCompleter
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)
s = PromptSession(completer=c, editing_mode=enums.EditingMode.VI)
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

15. Režim emulující příkazy editoru Emacs

Do režimu emulujícího některé základní příkazy editoru Emacs je možné se přepnout použitím pojmenovaného parametru editing_mode, který musí obsahovat konstantu EditingMode.EMACS naimportovanou z modulu enums. Následující příklad je až na odlišný režim editace totožný s příkladem předchozím, takže jen krátce:

from prompt_toolkit import PromptSession
from prompt_toolkit import enums
from prompt_toolkit.completion import WordCompleter
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
c = WordCompleter(["quit", "exit", "help", "eval"], ignore_case=True)
s = PromptSession(completer=c, editing_mode=enums.EditingMode.EMACS)
 
while True:
    cmd = s.prompt("Command: ")
    if cmd in {"quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

16. Použití objektu PromptSession v režimu zadávání hesla

Jen pro úplnost si na závěr ukažme, že parametry funkce prompt a metody PromptSession.prompt jsou skutečně do značné míry shodné. V dnešním posledním příkladu vytvoříme objekt typu PromptSession a při zadávání vstupních dat zvolíme režim zápisu hesla, což se podobá situaci z jedenácté kapitoly:

from prompt_toolkit import PromptSession
 
 
def show_help():
    print("""Help
--------
quit - quit this application
exit - exit from this application
eval - evaluate
""")
 
 
s = PromptSession()
 
while True:
    cmd = s.prompt("Command: ", is_password=True)
    if cmd in {"q", "quit", "Quit", "exit", "Exit"}:
        break
    elif cmd in {"help", "Help", "?"}:
        show_help()
    elif cmd == "eval":
        print("42")

17. Standardní klávesové zkratky použité knihovnou GNU Readline v režimu Emacs

V této kapitole jsou vypsány vybrané klávesové zkratky, které jsou ve výchozím nastavení použity knihovnou GNU Readline při přepnutí do režimu Emacs.

Příkazy pro přesuny kurzoru

Základní příkazy pro přesun kurzoru používají kombinaci Ctrl+znak, Alt+znak popř. alternativně Esc, znak v případě, že zkratky Alt+znak kolidují s emulátorem terminálu (například vyvolávají příkazy z menu). Pokud je terminál správně nakonfigurován, měly by fungovat i kurzorové šipky a navíc i klávesy Home a End (se zřejmou funkcí):

Klávesa Význam
Ctrl+B přesun na předchozí znak
Ctrl+F přesun na další znak
   
Alt+B přesun na předchozí slovo
Alt+F přesun na další slovo
Esc, B shodné s Alt+B
Esc, F shodné s Alt+F
   
Ctrl+A přesun na začátek řádku
Ctrl+E přesun na konec řádku

Mazání textu, práce s kill ringem

Pro přesun části textu v rámci editovaného řádku se používá takzvaný kill ring, do něhož se smazaný text uloží. Pro vložení takto smazaného textu do jiné oblasti se používá operace nazvaná yank (odpovídá paste). Některé dále uvedené příkazy dokážou s kill ringem pracovat:

Klávesa Význam
Ctrl+K smaže text od kurzoru do konce řádku a uloží ho do kill ringu
Ctrl+U smaže text od začátku řádku do pozice kurzoru a uloží ho do kill ringu
Ctrl+W smaže předchozí slovo a uloží ho do kill ringu
Alt+D smaže následující slovo a uloží ho do kill ringu
   
Ctrl+Y vloží text z kill ringu na místo, na němž se nachází kurzor (yank)
Alt+Y po operaci Ctrl+Y dokáže rotovat historií kill ringu a obnovit tak (před)předchozí smazaný text
   
Ctrl+D smaže jeden znak (pokud je ovšem na řádku nějaký obsah, jinak typicky ukončí aplikaci)

Práce s historií dříve zadaných příkazů

Klávesa Význam
Ctrl+P průchod historií – předchozí text
Ctrl+N průchod historií – následující text
Ctrl+R zpětné (interaktivní) vyhledávání v historii
Ctrl+G ukončení režimu vyhledávání

Některé další dostupné příkazy

Klávesa Význam
Tab implicitní klávesa pro zavolání completeru
Ctrl+T prohození dvou znaků (před kurzorem a na pozici kurzoru)
   
Alt+U text od pozice kurzoru do konce slova se změní NA VERZÁLKY
Alt+L text od pozice kurzoru do konce slova se změní na mínusky
Alt+C text od pozice kurzoru do konce slova se změní Tak, Že Slova Začínají Velkým Písmenem

18. Standardní klávesové zkratky použité knihovnou GNU Readline v režimu Vi

Poznámka: ve výchozím nastavení se řádka nachází v režimu vkládání znaků (insert mode). Pro přepnutí do normálního režimu použijte Esc.

V režimu emulace editorů Vi/Vim je možné mj. použít i tyto klávesové zkratky:

Příkazy pro přesuny kurzoru

Tyto příkazy jsou platné pro normální režim a lze je zkombinovat s operátory (delete, change, yank atd.):

ict ve školství 24

Klávesa Význam
h přechod na předchozí znak
l přechod na další znak
b skok (zpět) na první znak slova
e skok na poslední znak slova
w skok na první znak následujícího slova
0 skok na začátek řádku
$ skok na konec řádku
% doskok na párovou závorku
f skok na specifikovaný znak (find)

Editace textu

Klávesa Význam
x smazání jednoho znaku (odpovídá klávese Delete)
d operace smazání textu (musí být následována příkazem pro pohyb kurzoru)
D smazání textu od pozice kurzoru do konce řádku
y přenos textu do registru _ (musí být následována příkazem pro pohyb kurzoru)
c změna textu (musí být následována příkazem pro pohyb kurzoru)
r změna jediného znaku
s změna jediného znaku a přechod do vkládacího režimu
p operace put/paste, vloží smazaný text od pozice kurzoru
P operace put/paste, vloží smazaný text před pozicí kurzoru

Příkazy ve vkládacím režimu

Některé příkazy ve vkládacím režimu odpovídají režimu Emacsu:

Klávesa Význam
Ctrl+T prohození dvou znaků (před kurzorem a na pozici kurzoru)
Ctrl+H smazání znaku nalevo od kurzoru (odpovídá Backspace)
Ctrl+R zpětné (interaktivní) vyhledávání v historii
Ctrl+W smaže předchozí slovo a uloží ho do kill ringu
Ctrl+Y vloží text z registru _ na místo, na němž se nachází kurzor (yank)

Další příkazy

Klávesa Význam
Esc přepnutí do normálního režimu
1–9 prefix pro opakování další operace
u vrácení poslední operace (lze opakovat)
a append – přechod do režimu vkládání
i insert – přechod do režimu vkládání
~ změna jednoho znaku: verzálky/kapitálky a zpět
k průchod historií – předchozí text
j průchod historií – následující text

18. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:

# Příklad Popis Odkaz
1 input1.py použití input() https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/input1.py
2 input_command_line.py input() jako základ CLI aplikace https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/input_comman­d_line.py
3 readline_command_line.py povolení použití GNU Readline https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/readline_com­mand_line.py
4 readline_completer.py první verze automatického doplňování https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/readline_com­pleter.py
5 readline_completer2.py druhá verze automatického doplňování https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/readline_com­pleter2.py
6 readline_completer3.py třetí verze automatického doplňování https://github.com/tisnik/pre­sentations/blob/master/pyt­hon_readline/readline_com­pleter3.py
     
7 prompt1_basic_usage.py základní použití knihovny prompt_toolkit https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt1_ba­sic_usage.py
7 prompt2_command_line.py prompt_toolkit v CLI aplikaci https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt2_com­mand_line.py
7 prompt3_password_mode.py režim zadávání hesla https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt3_pas­sword_mode.py
7 prompt4_PromptSession.py objekt typu PromptSession https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt4_Prom­ptSession.py
7 prompt5_WordCompleter.py doplňování příkazů pomocí WordCompleteru https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt5_Wor­dCompleter.py
7 prompt6_vi_mode.py přepnutí do režimu emulace Vi (varianta A) https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt6_vi_mo­de.py
7 prompt7_vi_mode_B.py přepnutí do režimu emulace Vi (varianta B) https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt7_vi_mo­de_B.py
7 prompt8_Emacs_mode.py přepnutí do režimu emulace Emacsu https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt8_E­macs_mode.py
8 prompt9_PromptSession_password_mode.py režim zápisu hesla https://github.com/tisnik/pre­sentations/blob/master/prom­pt_toolkit/prompt/prompt9_Prom­ptSession_password_mode.py

20. Odkazy na Internetu

  1. 4 Python libraries for building great command-line user interfaces
    https://opensource.com/article/17/5/4-practical-python-libraries
  2. prompt_toolkit 2.0.3 na PyPi
    https://pypi.org/project/prom­pt_toolkit/
  3. python-prompt-toolkit na GitHubu
    https://github.com/jonathan­slenders/python-prompt-toolkit
  4. The GNU Readline Library
    https://tiswww.case.edu/php/chet/re­adline/rltop.html
  5. GNU Readline (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Readline
  6. readline — GNU readline interface (Python 3.x)
    https://docs.python.org/3/li­brary/readline.html
  7. readline — GNU readline interface (Python 2.x)
    https://docs.python.org/2/li­brary/readline.html
  8. GNU Readline Library – command line editing
    https://tiswww.cwru.edu/php/chet/re­adline/readline.html
  9. gnureadline 6.3.8 na PyPi
    https://pypi.org/project/gnureadline/
  10. Editline Library (libedit)
    http://thrysoee.dk/editline/
  11. Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click
    https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/
  12. libedit or editline
    http://www.cs.utah.edu/~bi­gler/code/libedit.html
  13. WinEditLine
    http://mingweditline.sourceforge.net/
  14. rlcompleter — Completion function for GNU readline
    https://docs.python.org/3/li­brary/rlcompleter.html
  15. rlwrap na GitHubu
    https://github.com/hanslub42/rlwrap
  16. rlwrap(1) – Linux man page
    https://linux.die.net/man/1/rlwrap
  17. readline(3) – Linux man page
    https://linux.die.net/man/3/readline
  18. history(3) – Linux man page
    https://linux.die.net/man/3/history
  19. vi(1) – Linux man page
    https://linux.die.net/man/1/vi
  20. emacs(1) – Linux man page
    https://linux.die.net/man/1/emacs

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.