Lesen Sie die Sonderschlüssel in Bash

8

Ich spiele mit einem Skript, das unter anderem eine Auswahlliste auflistet. Wie in:

1) Punkt 1               # (hervorgehoben)
2) Punkt 2
3) Punkt 3 # (ausgewählt)
4) Punkt 4

  • Wenn der Benutzer die down-arrownächsten Elemente drückt , wird dies hervorgehoben
  • Wenn der Benutzer auf up-arrowvorherige Elemente drückt , wird dies hervorgehoben
  • usw.
  • Wenn Benutzer drücken tabElement ausgewählt ist
  • Wenn der Benutzer drückt, werden shift+taballe Elemente ausgewählt / abgewählt
  • Wenn der Benutzer drückt, werden ctrl+aalle Elemente ausgewählt
  • ...

Dies funktioniert ab dem aktuellen Gebrauch einwandfrei. Dies ist mein persönlicher Gebrauch, bei dem Eingaben durch mein eigenes Setup gefiltert werden.

Die Frage ist, wie dies über verschiedene Terminals hinweg zuverlässig gemacht werden kann.


Ich benutze eine etwas hackige Lösung, um Eingaben zu lesen:

while read -rsn1 k # Read one key (first byte in key press)
do
    case "$k" in
    [[:graph:]])
        # Normal input handling
        ;;
    $'\x09') # TAB
        # Routine for selecting current item
        ;;
    $'\x7f') # Back-Space
        # Routine for back-space
        ;;
    $'\x01') # Ctrl+A
        # Routine for ctrl+a
        ;;
    ...
    $'\x1b') # ESC
        read -rsn1 k
        [ "$k" == "" ] && return    # Esc-Key
        [ "$k" == "[" ] && read -rsn1 k
        [ "$k" == "O" ] && read -rsn1 k
        case "$k" in
        A) # Up
            # Routine for handling arrow-up-key
            ;;
        B) # Down
            # Routine for handling arrow-down-key
            ;;
        ...
        esac
        read -rsn4 -t .1 # Try to flush out other sequences ...
    esac
done

Und so weiter.


Wie bereits erwähnt, stellt sich die Frage, wie dies über verschiedene Terminals hinweg zuverlässig gemacht werden kann: dh welche Byte-Sequenzen einen bestimmten Schlüssel definieren. Ist es überhaupt in Bash machbar?

Ein Gedanke war, entweder tputoder infocmpund nach dem Ergebnis zu filtern. Ich bin jedoch in einem Haken als beides tputund infocmpunterscheide mich von dem, was ich tatsächlich lese, wenn ich tatsächlich Tasten drücke . Gleiches gilt zum Beispiel für C over Bash.

for t in $(find /lib/terminfo -type f -printf "%f\n"); { 
    printf "%s\n" "$t:"; 
    infocmp -L1 $t | grep -E 'key_(left|right|up|down|home|end)';
}

Ertragssequenzen werden wie zum Beispiel definiert gelesen linux, aber nicht xterm, was durch festgelegt wird TERM.

ZB Pfeil links:

  • tput/ infocmp:\x1 O D
  • read:: \x1 [ D

Was vermisse ich?

user367890
quelle
Das Rad muss nicht neu erfunden werden, iselect tut dies bereits. Verwenden Sie alternativ eine der dialogVarianten oder eine Sprache mit angemessener ncursesUnterstützung (Perl oder Python, wenn Sie sich an "Skriptsprachen" halten möchten).
Cas
1
Beachten Sie, dass zshdie integrierte Curses-Unterstützung (im zsh / curses-Modul) zusätzlich zur grundlegenden Terminfo-Abfrage mit dem echotiintegrierten und $terminfoassoziativen Array unterstützt wird.
Stéphane Chazelas

Antworten:

5

Was Sie vermissen, ist, dass die meisten Terminalbeschreibungen ( linuxdie hier aufgrund der weit verbreiteten Verwendung von fest codierten Zeichenfolgen in der Minderheit sind .inputrc) den Anwendungsmodus für Spezialschlüssel verwenden. Dadurch werden die Cursortasten wie gezeigt angezeigt tputund infocmpunterscheiden sich von dem, was Ihr (nicht initialisiertes) Terminal sendet. Fluchanwendungen initialisieren immer das Terminal, und die Terminaldatenbank wird zu diesem Zweck verwendet.

dialoghat seine Verwendung, geht aber nicht direkt auf diese Frage ein. Auf der anderen Seite ist es umständlich (technisch machbar , selten getan ) eine Bash-only Lösung. Im Allgemeinen verwenden wir dazu andere Sprachen.

Das Problem beim Lesen von Sonderschlüsseln besteht darin, dass es sich häufig um mehrere Bytes handelt, einschließlich umständlicher Zeichen wie escapeund ~. Sie können dies mit bash tun , aber dann müssen Sie das Problem lösen, tragbar zu bestimmen, um welchen speziellen Schlüssel es sich handelt.

dialogBeide übernehmen die Eingabe von Sondertasten und übernehmen (vorübergehend) Ihre Anzeige. Wenn Sie wirklich ein einfaches Befehlszeilenprogramm möchten, ist dies nicht der Fall dialog.

Hier ist ein einfaches Programm in C, das einen speziellen Schlüssel liest und ihn in druckbarer (und tragbarer) Form druckt :

#include <curses.h>

int
main(void)
{   
    int ch;
    const char *result;
    char buffer[80];

    filter();
    newterm(NULL, stderr, stdin);
    keypad(stdscr, TRUE);
    noecho();
    cbreak();
    ch = getch();
    if ((result = keyname(ch)) == 0) {
        /* ncurses does the whole thing, other implementations need this */
        if ((result = unctrl((chtype)ch)) == 0) {
            sprintf(buffer, "%#x", ch);
            result = buffer;
        }
    }
    endwin();
    printf("%s\n", result);
    return 0;
}

Angenommen, dies tgetchwürde aufgerufen , würden Sie es in Ihrem Skript folgendermaßen verwenden:

case $(tgetch 2>/dev/null) in
KEY_UP)
   echo "got cursor-up"
   ;;
KEY_BACKSPACE|"^H")
   echo "got backspace"
   ;;
esac

Weiterführende Literatur:

Thomas Dickey
quelle
Vielen Dank. Ja, inputrcwar in der Tat der Schuldige, den ich suchte. Muss es mir noch etwas ansehen. Ich habe überlegt, Python oder C zu verwenden, aber es macht Spaß, sich auch als Bash-Skript zu hacken. Ich habe auch versucht, einen Blick auf die Quelle von ncurses zu werfen, um zu sehen, ob ich die benötigten Teile extrahieren kann - aber nach einiger Zeit habe ich die Quelle auf Eis gelassen. Das "Projekt" begann als einfacher Befehl, wurde dann zu einem einfachen interaktiven Skript und wurde dann erneut erweitert. Irgendwo auf dem Weg hätte ich eine andere Sprache
wählen
Fand die Sequenzen unter anderem in /usr/share/doc/readline-common/inputrc.arrows. Da ich bereits eine generische "read_key" -Funktion habe, die ich im gesamten Skript verwende, hoffte ich, dass es eine einfachere Möglichkeit gibt, die Sequenzen (im Skript) aus dem zu definieren, was tatsächlich angezeigt wird, wenn eine Taste gedrückt wird. Dh ähnlich wie beim Extrahieren von Definitionen aus infocmp. Aber raten Sie nicht und müssen Sie es entweder so lassen wie es ist oder in eine andere Sprache wechseln. Ein Kompromiss könnte natürlich darin bestehen, Ihr nettes C-Snippet zu verwenden. Aber dann kann ich das Ganze stattdessen in C schreiben. (Entschuldigung für das Oversharing.)
user367890
Ist das der komplette C-Code? Ich bekomme ungefähr ein Dutzend Fehler, wenn ich versuche, dies mit gcc auf Debian 9 zu
kompilieren
Sie haben wahrscheinlich das -lncursesusw. weggelassen
Thomas Dickey
6

Haben Sie versucht, zu verwenden dialog? Es wird standardmäßig mit den meisten Linux-Distributionen geliefert und kann alle Arten von textbasierten Dialogen erstellen, einschließlich Checklisten.

Zum Beispiel:

exec 3>&1 # open temporary file handle and redirect it to stdout

#                           type      title        width height n-items    
items=$(dialog --no-lines --checklist "Title here" 20    70     4 \
          1 "Item 1" on \
          2 "Item 2" off \
          3 "Item 3" on \
          4 "Item 4" off \
            2>&1 1>&3) # redirect stderr to stdout to catch output, 
                       # redirect stdout to temporary file
selected_OK=$? # store result value
exec 3>&- # close new file handle 

# handle output
if [ $selected_OK = 0 ]; then
    echo "OK was selected."
    for item in $items; do
        echo "Item $item was selected."
    done
else
    echo "Cancel was selected."
fi

Sie werden so etwas bekommen:

Geben Sie hier die Bildbeschreibung ein

Und die Ausgabe wird sein:

 OK was selected.
 Item 1 was selected.
 Item 3 was selected.

(oder welche Elemente Sie ausgewählt haben).

man dialog Sie erhalten Informationen zu den anderen Arten von Dialogen, die Sie erstellen können, und zum Anpassen des Erscheinungsbilds.

Marinus
quelle
+1 für Mühe, aber Dickey war mehr auf den Punkt, was ich frage. Zum einen war das beschriebene Problem - im allgemeineren Sinne diente die Liste lediglich dazu, einen Kontext zu geben. Zweitens habe ich mir den Dialog kurz angesehen - und zugegebenermaßen habe ich ihn nicht gründlich angeschaut. Mein Fall, um ihn zu erweitern, ist eine Front für eine SQLite-Datenbank mit mehreren tausend Datensätzen, in der ich zum Beispiel Page-Up / Down to habe Auswahl durch Bildlauf. Bildlaufbereich, Bildlaufpuffer, eine Statuszeile, eine Ex-Zeile mit modaler Eingabe, Unterfunktionen zum Filtern usw. Kurz gesagt, es mag komplex klingen, ist aber ziemlich einfach ...
user367890
… Aber der Dialog schien nicht ganz den Bedürfnissen zu entsprechen oder für meinen Fall etwas umständlich zu sein.
user367890
@ user367890 Ihre Anwendung klingt wie eine perfekte Ergänzung für das Perl Curses, DBIund DBD::SQLiteModule. oder ihre Python-Äquivalente.
Cas
@ Cas: Ja. Habe früher ähnliche Anwendungen mit Python und C geschrieben - obwohl ich viel davon neu lernen muss. Dieses "Projekt" ist eher ein Abenteuer in Bash-Möglichkeiten und "zum Spaß" :) Obwohl ich kurz davor bin, es aufzugeben oder in eine andere Sprache zu portieren. Danke für die Eingabe.
user367890