Come leggere l'input dell'utente quando si utilizza lo script in pipe


10

Problema generale

Voglio scrivere uno script che interagisca con l'utente anche se si trova nel mezzo di una catena di pipe.

Esempio concreto

Concretamente, prende un fileo stdin, visualizza le righe (con i numeri di riga), chiede all'utente di inserire una selezione o numeri di riga, quindi stampa le righe corrispondenti su stdout. Chiamiamo questo script selector. Quindi, fondamentalmente, voglio essere in grado di fare

grep abc foo | selector > myfile.tmp

Se foocontiene

blabcbla
foo abc bar
quux
xyzzy abc

poi selectormi presenta (sul terminale, non in myfile.tmp!) con opzioni

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

dopo di che scrivo

2-3

e finiamo con

foo abc bar
xyzzy abc

come contenuto di myfile.tmp.

Ho uno script selettore attivo e funzionante, e sostanzialmente funziona perfettamente se non reindirizzo input e output. Così

selector foo

si comporta come voglio. Tuttavia, quando si collegano le cose insieme come nell'esempio sopra, selectorstampa le opzioni presentate myfile.tmpe prova a leggere una selezione dall'input grepped.

Il mio approccio

Ho provato a usare la -ubandiera di read, come in

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

ma questo non fa quello che speravo potesse fare.

D: Come posso ottenere l'interazione dell'utente reale?


creare uno script e salvare l'output in variabile, quindi l'utente attuale lo desidera ??
Hackaholic l'

@Hackaholic - Non sono sicuro di cosa intendi. Voglio uno script che possa essere inserito in qualsiasi tipo di sequenza di pipeline (ovvero in modo Unix). Ho dato un esempio elaborato sopra, ma non è certamente l'unico caso d'uso che ho in mente.
jmc,

1
Usacmd | { some processing; read var </dev/tty; } | cmd
mikeserv l'

@mikeserv - Interessante! Ora ho quello 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; }'che funziona abbastanza bene. Comunque si grep b foo | selector | wc -lrompe qui. Qualche idea su come risolverlo? A proposito, quello rangeselectche ho usato può essere trovato su pastebin.com/VAxTSSHs . È un semplice script AWK che stampa le linee di un file corrispondenti a un determinato intervallo di lino. (Gli intervalli possono essere cose come "3-10, 12,14,16-20".)
jmc

1
Non farlo alias, piuttosto selector() { all of that stuff...; }in una funzione. aliases rinomina comandi semplici mentre le funzioni racchiudono un comando composto in un singolo comando semplice .
mikeserv,

Risposte:


8

L'uso /proc/$PPID/fd/0non è affidabile: il genitore del selectorprocesso potrebbe non avere il terminale come input.

C'è un percorso standard che si riferisce sempre al terminale del processo corrente: /dev/tty.

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

o

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

1
Grazie, questo risolve il mio problema. La risposta è un po 'minimalista però. Immagino che potrebbe trarre beneficio dall'incorporare alcuni dei consigli di Mikeserv nei commenti alla domanda.
jmc,

2

Ho scritto una piccola funzione: non risponderà a ciò che hai chiesto il concatenamento di pipe ma risolverà il tuo 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)"
)

La funzione capovolge immediatamente tutti gli argomenti a cui la dai grep. Se usi una shell glob per specificare i file da cui deve leggere, verranno restituite tutte le corrispondenze in tutti i file, iniziando dal primo nell'ordine glob e terminando con l'ultimo match.

greppassa il suo output a nlquali numeri ogni riga e quale passa il suo output al teequale duplica il suo output sia a stdoutche a /dev/tty. Ciò significa che l'output dalla pipeline viene contemporaneamente stampato sia sull'array di argomenti della funzione in cui viene suddiviso su \newline che sul terminale mentre funziona.

Successivamente la _in()funzione tenta di effettuare readuna selezione se vi è almeno 1 risultato dell'azione precedente per un massimo di cinque volte. La selezione può consistere solo di numeri separati da spazi, oppure intervalli di numeri separati da -. Se c'è qualcos'altro read (inclusa una riga vuota) , ci riproverà, ma solo, come prima, un massimo di cinque volte.

Infine, la _out()funzione analizza la selezione dell'utente ed espande eventuali intervalli al suo interno. Stampa i risultati nel modulo "${[num]}"per ciascuno di essi, facendo corrispondere così il valore delle linee memorizzate inf()nell'array arg. Questo output viene modificato evalcome arg su printfcui quindi stampa solo le righe selezionate dall'utente.

Viene esplicitamente readdal terminale e stampa solo il Select:menu, stderrquindi è molto adatto alla pipeline. Ad esempio, i seguenti lavori:

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

Ma puoi usare tutte le opzioni che daresti grepe qualsiasi numero di nomi di file che potresti consegnare. Cioè, puoi usare qualsiasi tipo tranne uno - come effetto collaterale del suo input di analisi con $IFSesso non funzionerà se stai cercando linee vuote. Ma chi vorrebbe selezionare da un elenco numerato di righe vuote?

Ultima nota che, poiché funziona traducendo direttamente l'input numerico dell'utente nei parametri posizionali numerici memorizzati nell'array dell'argomento della funzione, l'output sarà qualunque cosa l'utente selezioni, quante volte l'utente lo seleziona e in qualunque ordine l'utente scelga esso.

Per esempio:

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

@mikeserv era solo un'idea, non l'intero script, e una cosa, a proposito di test, il file originale è solo su disco, quindi prendi da loro. quindi penso che non sia un problema o uno sforzo extra per testarlo
Hackaholic

@mikeserv sì hai ragione, non ho convalidato tutto, come input impropri e tutto il resto. grazie per il tuo punto
Hackaholic

@mikeserv Conosco tutte le basi della programmazione della shell, puoi guidarmi su come andare avanti
Hackaholic

sì, certo che sarò felice di modificarlo
Hackaholic
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.