Lê chaves especiais no bash

8

Eu estou jogando com um script que, entre outras coisas, lista uma lista de seleção. Como em:

1) Item 1              # (highlighted)
2) Item 2
3) Item 3              # (selected)
4) Item 4
  • Quando o usuário pressionar down-arrow , os próximos itens serão destacados
  • Quando o usuário pressionar up-arrow itens anteriores é destacado
  • etc.
  • Quando o usuário pressiona tab item é selecionado
  • Quando o usuário pressiona shift+tab , todos os itens são selecionados / desmarcados
  • Quando o usuário pressionar ctrl+a , todos os itens serão selecionados
  • ...

Isso funciona bem a partir do uso atual, que é meu uso pessoal, onde a entrada é filtrada pela minha própria configuração.

A questão é como tornar isso confiável em vários terminais.

Eu uso uma solução um pouco hack para ler a entrada:

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

E assim por diante.

Como mencionado, a questão é como tornar isso confiável em vários terminais: ou seja, quais sequências de bytes definem uma chave específica. É mesmo possível em bash?

Um pensamento era usar tput ou infocmp e filtrar pelo resultado dado por isso. No entanto, estou com problemas, pois tput e infocmp diferem do que eu realmente leio quando estou pressionando as teclas. O mesmo vale por exemplo usando 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)';
}

Sequências de rendimento são lidas conforme definido para, por exemplo, linux , mas não xterm , que é definido por TERM .

Por exemplo seta para a esquerda:

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

O que estou perdendo?

    
por user367890 10.07.2016 / 03:38

2 respostas

5

O que você está perdendo é que a maioria das descrições de terminal ( linux é minoria aqui, devido ao uso difundido de strings codificadas em .inputrc ) usa modo de aplicação para teclas especiais . Isso faz com que as teclas de cursor mostradas por tput e infocmp sejam diferentes daquelas que seu terminal (não inicializado) envia. aplicações curses sempre inicializam o terminal, e a base de dados do terminal é usada para esse propósito.

dialog tem seus usos, mas não aborda diretamente essa questão. Por outro lado, é complicado (tecnicamente factível , raramente feito ) fornecer uma solução apenas bash. Geralmente usamos outras linguagens para fazer isso.

O problema com a leitura de chaves especiais é que elas geralmente são múltiplos bytes, incluindo caracteres estranhos como escape e ~ . Você pode fazer isso com o bash, mas então você tem que resolver o problema de determinar portavelmente qual chave especial era essa.

dialog manipula a entrada de teclas especiais e assume (temporariamente) sua exibição. Se você realmente quer um programa de linha de comando simples, isso não é dialog .

Aqui está um programa simples em C que lê uma chave especial e a imprime em imprimível (e portátil):

#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;
}

Supondo que isso seja chamado de tgetch , você o usaria em seu script da seguinte forma:

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

Leitura adicional:

por 10.07.2016 / 11:44
6

Você já tentou usar dialog ? Ele vem de fábrica com a maioria das distribuições Linux e pode criar todos os tipos de diálogos baseados em texto, incluindo listas de verificação.

Por exemplo:

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

Você terá algo assim:

E a saída será:

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

(ou qualquer item selecionado).

man dialog fornecerá informações sobre os outros tipos de diálogos que você pode criar e como personalizar a aparência.

    
por 10.07.2016 / 08:32

Tags