Como ler a entrada do usuário ao usar o script no pipe

9

Problema geral

Eu quero escrever um script que interaja com o usuário, mesmo que esteja no meio de uma cadeia de canais.

Exemplo concreto

Concretamente, é preciso um file ou stdin , exibe linhas (com números de linha), pede ao usuário para inserir uma seleção ou números de linha e imprime as linhas correspondentes em stdout . Vamos chamar esse script selector . Então, basicamente, eu quero ser capaz de fazer

grep abc foo | selector > myfile.tmp

Se foo contiver

blabcbla
foo abc bar
quux
xyzzy abc

então selector me apresenta (no terminal, não em myfile.tmp !) com opções

1) blabcbla
2) foo abc bar
3) xyzzy abc
Select options:

depois do que eu digito

2-3

e acabar com

foo abc bar
xyzzy abc

como conteúdo de myfile.tmp .

Eu tenho um script seletor instalado e funcionando, e basicamente ele está funcionando perfeitamente se eu não redirecionar entrada e saída. Então

selector foo

se comporta como eu quero. No entanto, ao juntar as coisas como no exemplo acima, selector imprime as opções apresentadas para myfile.tmp e tenta ler uma seleção da entrada grepped.

Minha abordagem

Eu tentei usar o sinal -u de read , como em

exec 4< /proc/$PPID/fd/0
exec 4> /proc/$PPID/fd/1
nl $INPUT >4
read -u4 -p"Select options: "

mas isso não faz o que eu esperava.

P: Como faço para obter interação real do usuário?

    
por jmc 08.11.2014 / 22:01

2 respostas

7

Usar /proc/$PPID/fd/0 não é confiável: o pai do processo selector pode não ter o terminal como entrada.

Existe um caminho padrão que sempre se refere ao terminal do processo atual: /dev/tty .

nl "$INPUT" >/dev/tty
read -p"Select options: " </dev/tty

ou

exec </dev/tty >/dev/tty
nl "$INPUT"
read -p"Select options: "
    
por 09.11.2014 / 01:23
2

Eu escrevi uma pequena função: ela não responderá sobre o que você pediu para encadear o encanamento, mas resolverá o seu problema.

inf() ( [ -n "$ZSH_VERSION" ] && emulate sh
        unset n i c; set -f; tab='      ' IFS='
';      _in()   until [ "$((i+=1))" -gt 5 ] && exit 1
                printf '\nSelect: '
                read -r c && [ -n "${c##*[!- 0-9]*}" ]
                do echo "Invalid selection."
                done
        _out()  for n do i=; [ "$n" = . ]  &&
                printf '"${%d#*$tab}" ' $c ||
                until c="${c#*.} ${i:=${n%%-*}}"
                [ "$((i+=1))" -gt "${n#*-}" ]
                do :; done; done
set -- $(grep "$@"|nl -w1 -s "$tab"|tee /dev/tty)
i=$((($#<1)*5)); _in </dev/tty >/dev/tty
eval "printf '%s\n' $(c=$c\ . IFS=\ ;_out $c)"
)

A função vira todos os argumentos que você deu imediatamente para grep . Se você usar um shell glob para especificar os arquivos que ele deve ler, ele retornará todas as correspondências em todos os arquivos, começando com o primeiro na ordem glob e terminando com a última correspondência.

grep passa sua saída para nl , que numera cada linha e que passa sua saída para tee , o que duplica sua saída para stdout e para /dev/tty . Isso significa que a saída do pipeline é impressa simultaneamente para a matriz de argumentos da função, onde é dividida em \n ewlines e para o terminal como funciona.

Em seguida, a função _in() tentará read em uma seleção se houver pelo menos um resultado da ação anterior no máximo cinco vezes. A seleção pode consistir em apenas números separados por espaços, ou então intervalos de números separados por - . Se qualquer outra coisa for read (incluindo uma linha em branco) , ele tentará novamente - mas apenas, como antes, no máximo cinco vezes.

Por último, a função _out() analisa a seleção do usuário e expande os intervalos nele. Ele imprime seus resultados na forma "${[num]}" para cada um - correspondendo, assim, ao valor das linhas armazenadas na matriz arg de inf() . Essa saída é eval ed como args para printf , portanto, imprime apenas as linhas selecionadas pelo usuário.

É explicitamente read do terminal e imprime apenas o menu Select: para stderr e, portanto, é muito fácil de usar. Por exemplo, os seguintes trabalhos:

seq 100 |inf 3|grep 8
1       3
2       13
3       23
4       30
5       31
6       32
7       33
8       34
9       35
10      36
11      37
12      38
13      39
14      43
15      53
16      63
17      73
18      83
19      93

Select: 6 9 12-18
38
83

Mas você pode usar qualquer opção que você daria a grep e qualquer número de nomes de arquivos que você possa entregar também. Ou seja, você pode usar qualquer um, exceto um tipo - como um efeito colateral de sua entrada de análise com $IFS , isso não funcionará se você estiver procurando por linhas em branco. Mas quem gostaria de selecionar uma lista numerada de linhas em branco?

Por último, note que, como isso funciona traduzindo diretamente a entrada numérica do usuário nos parâmetros posicionais numéricos armazenados na matriz de argumentos da função, a saída será o que o usuário selecionar, quantas vezes o usuário a selecionar, e em qualquer ordem que o usuário selecione.

Por exemplo:

seq 1000 | inf 00\$

1       100
2       200
3       300
4       400
5       500
6       600
7       700
8       800
9       900
10      1000

Select: 4-8 1 1 3-6
400
500
600
700
800
100
100
300
400
500
600
    
por 08.11.2014 / 23:21