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

10

Problema geral

Quero escrever um script que interaja com o usuário, mesmo que esteja no meio de uma cadeia de tubos.

Exemplo concreto

Concretamente, ele pega a fileou stdinexibe linhas (com números de linha), solicita ao usuário que insira 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 poder fazer

grep abc foo | selector > myfile.tmp

Se foocontém

blabcbla
foo abc bar
quux
xyzzy abc

então selectorme apresenta (no terminal, não no myfile.tmp!) opções

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

após o 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 funciona perfeitamente se eu não redirecionar a entrada e a saída. assim

selector foo

se comporta como eu quero. No entanto, ao canalizar as coisas juntas, como no exemplo acima, selectorimprime as opções apresentadas myfile.tmpe tenta ler uma seleção da entrada grepped.

Minha abordagem

Eu tentei usar a -ubandeira 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 obtenho uma interação real do usuário?

jmc
fonte
faça um script e salve a saída na variável, e então o usuário presente quer que você queira ??
Hackaholic
@ Hackaholic - Eu não tenho certeza do que você quer dizer. Eu quero um script que possa ser colocado em qualquer tipo de sequência de pipeline (ou seja, da maneira Unix). Dei um exemplo elaborado acima, mas esse certamente não é o único caso de uso que tenho em mente.
jmc
1
Usecmd | { some processing; read var </dev/tty; } | cmd
mikeserv
@mikeserv - Interessante! Agora eu tenho o alias selector='{ TMPFILE=$(mktemp); cat > $TMPFILE; nl -s") " $TMPFILE | column -c $(tput cols); read -e -p"Select options: " < /dev/tty; rangeselect -v range="$REPLY" $TMPFILE; rm $TMPFILE; }'que funciona muito bem. No entanto grep b foo | selector | wc -lquebra aqui. Alguma idéia de como consertar isso? A propósito, o rangeselectque eu usei pode ser encontrado em pastebin.com/VAxTSSHs . É um script AWK simples que imprime as linhas de um arquivo correspondente a um determinado intervalo de números de roupa. (Os intervalos podem ser coisas como "3-10, 12,14,16-20".)
jmc
1
Não faça aliasisso, sim, selector() { all of that stuff...; }em uma função. aliasrenomeia comandos simples, enquanto as funções agrupam um comando composto em um único comando simples .
mikeserv

Respostas:

8

O uso /proc/$PPID/fd/0não é confiável: o pai do selectorprocesso pode não ter o terminal como entrada.

Há 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: "
Gilles 'SO- parar de ser mau'
fonte
1
Obrigado, isso resolve o meu problema. A resposta é um pouco minimalista. Eu acho que pode se beneficiar da incorporação de alguns dos conselhos de mikeserv nos comentários da pergunta.
jmc
2

Eu escrevi uma pequena função: ela não responde sobre o que você pediu para encadear tubos, mas resolve 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ê fornece imediatamente grep. Se você usar um shell glob para especificar os arquivos que ele deve ler, retornará todas as correspondências em todos os arquivos, começando com o primeiro na ordem glob e terminando com a última correspondência.

greppassa sua saída para nlquais números cada linha e que passa sua saída para a teequal duplica sua saída para stdoute para /dev/tty. Isso significa que a saída do pipeline é impressa simultaneamente na matriz de argumentos da função, onde é dividida nas \nlinhas ew e no terminal, conforme funciona.

Em seguida, a _in()função tenta readem uma seleção se houver pelo menos 1 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 houver mais alguma coisa read (incluindo uma linha em branco), ela tentará novamente - mas apenas, como antes, no máximo cinco vezes.

Por último, a _out()função analisa a seleção do usuário e expande os intervalos nela. Ele imprime seus resultados no formulário "${[num]}"para cada um - correspondendo, assim, ao valor das linhas armazenadas na inf()matriz arg. Essa saída é evaleditada como args, para a printfqual, portanto, imprime apenas as linhas que o usuário selecionou.

Ele é explicitamente readdo terminal e apenas imprime o Select:menu para, stderrportanto, é bastante amigável ao pipeline. Por exemplo, o seguinte funciona:

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 todas as opções que você daria grepe qualquer número de nomes de arquivos que também possa ser entregue. Ou seja, você pode usar qualquer tipo, exceto um - porque o efeito colateral de sua entrada de análise $IFSnão funcionará se você estiver procurando linhas em branco. Mas quem gostaria de selecionar em uma lista numerada de linhas em branco?

Última observação: como isso funciona convertendo 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 selecionar e na ordem em que o usuário selecionar. isto.

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
Hackaholic
fonte
@mikeserv, foi apenas uma idéia, não o script inteiro, e uma coisa, você está falando sobre teste, o arquivo original está apenas em disco, então você está tirando deles. Então eu acho que não é um problema ou um esforço extra para testá-lo
Hackaholic
@ MikeServ sim, você está certo, eu não validei tudo, como entrada imprópria e tudo. obrigado por seu ponto
Hackaholic
@mikeserv eu sei tudo básico de programação shell, pode guiá-u-me como ser ir para avançados
Hackaholic
sim certeza eu vou estar editar feliz que
Hackaholic