Lesen von Benutzereingaben bei Verwendung von Skripten in Pipe

10

Allgemeines Problem

Ich möchte ein Skript schreiben, das mit dem Benutzer interagiert, obwohl es sich mitten in einer Rohrkette befindet.

Konkretes Beispiel

Konkret dauert es ein fileoder stdin, zeigt Zeilen (mit Zeilennummern) an, fordert den Benutzer auf, eine Auswahl oder Zeilennummern einzugeben, und druckt dann die entsprechenden Zeilen aus stdout. Nennen wir dieses Skript selector. Dann möchte ich im Grunde in der Lage sein zu tun

grep abc foo | selector > myfile.tmp

Wenn fooenthält

blabcbla
foo abc bar
quux
xyzzy abc

dann selectorpräsentiert mir (auf dem terminal, nicht in myfile.tmp!) optionen

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

Danach tippe ich ein

2-3

und am Ende mit

foo abc bar
xyzzy abc

als Inhalt von myfile.tmp.

Ich habe ein Auswahlskript eingerichtet, und im Grunde funktioniert es perfekt, wenn ich Ein- und Ausgabe nicht umleitung. So

selector foo

benimmt sich wie ich will. Wenn Sie jedoch Dinge wie im obigen Beispiel zusammenfügen, werden selectordie dargestellten Optionen gedruckt myfile.tmpund versucht, eine Auswahl aus der Grepped-Eingabe zu lesen.

Mein Ansatz

Ich habe versucht, die -uFlagge von zu verwenden read, wie in

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

aber das macht nicht das, was ich mir erhofft hatte.

F: Wie erhalte ich die tatsächliche Benutzerinteraktion?

jmc
quelle
Machen Sie ein Skript und speichern Sie die Ausgabe in Variable, und dann präsentieren Benutzer wollen Sie wollen?
Hackaholic
@ Hackaholic - Ich bin nicht sicher, was du meinst. Ich möchte ein Skript, das in jede Art von Pipeline-Sequenz eingefügt werden kann (dh auf Unix-Weise). Ich habe oben ein ausführliches Beispiel gegeben, aber das ist sicherlich nicht der einzige Anwendungsfall, an den ich denke.
JMC
1
Verwenden Siecmd | { some processing; read var </dev/tty; } | cmd
mikeserv
@mikeserv - Interessant! Ich habe jetzt 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; }'was ziemlich gut funktioniert. Allerdings grep b foo | selector | wc -lbricht hier rüber. Irgendwelche Ideen, wie man das behebt? Das, rangeselectwas ich verwendet habe, finden Sie übrigens unter pastebin.com/VAxTSSHs . Es ist ein einfaches AWK-Skript, das die Zeilen einer Datei druckt, die einem bestimmten Bereich von Leinenzahlen entsprechen. (Bereiche können Dinge wie "3-10, 12,14,16-20" sein.)
jmc
1
Nicht aliasdas, sondern selector() { all of that stuff...; }in eine Funktion. aliasEs benennt einfache Befehle um, während Funktionen einen zusammengesetzten Befehl in einen einzigen einfachen Befehl packen .
Mikesserv

Antworten:

8

Die Verwendung /proc/$PPID/fd/0ist unzuverlässig: Das übergeordnete Element des selectorProzesses verfügt möglicherweise nicht über das Terminal als Eingabe.

Es gibt einen Standardpfad , der sich immer auf das Terminal des aktuellen Prozesses bezieht : /dev/tty.

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

oder

exec </dev/tty >/dev/tty
nl "$INPUT"
read -p"Select options: "
Gilles 'SO - hör auf böse zu sein'
quelle
1
Danke, das löst mein Problem. Die Antwort ist allerdings etwas minimalistisch. Ich denke, es könnte von Vorteil sein, einige der Ratschläge von mikeserv in die Kommentare zu der Frage aufzunehmen.
JMC
2

Ich habe eine kleine Funktion geschrieben: Sie antwortet nicht auf das, was Sie um Verkettung von Rohren gebeten haben, sondern löst Ihr Problem.

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)"
)

Die Funktion übergibt alle Argumente, denen Sie sie sofort geben grep. Wenn Sie einen Shell-Glob verwenden, um die Dateien anzugeben, aus denen er gelesen werden soll, werden alle Übereinstimmungen in allen Dateien zurückgegeben, beginnend mit der ersten in der Glob-Reihenfolge und endend mit der letzten Übereinstimmung.

grepÜbergibt seine Ausgabe an nlwelche Nummern jede Zeile und welche übergibt seine Ausgabe an teewelche dupliziert seine Ausgabe sowohl an stdoutals auch an /dev/tty. Dies bedeutet, dass die Ausgabe aus der Pipeline gleichzeitig sowohl in das Argumentarray der Funktion, wo sie in \nEwlines aufgeteilt wird, als auch in das Terminal gedruckt wird, während es funktioniert.

Als nächstes _in()versucht die Funktion readin einer Auswahl, ob mindestens ein Ergebnis der vorherigen Aktion maximal fünf Mal vorliegt. Die Auswahl kann nur aus durch Leerzeichen getrennten Zahlen oder durch durch Leerzeichen getrennten Zahlenbereichen bestehen -. Wenn etwas anderes vorhanden ist read (einschließlich einer Leerzeile) , wird es erneut versucht - jedoch nur nach wie vor maximal fünf Mal.

Zuletzt _out()analysiert die Funktion die Auswahl des Benutzers und erweitert alle darin enthaltenen Bereiche. Es druckt seine Ergebnisse in der "${[num]}"jeweiligen Form aus und stimmt damit mit dem Wert der im inf()arg-Array gespeicherten Zeilen überein. Diese Ausgabe wird evalals Argumente ausgegeben, auf printfdie daher nur die vom Benutzer ausgewählten Zeilen gedruckt werden.

Es kommt explizit readvom Terminal und druckt nur das Select:Menü aus stderrund ist daher reichlich Pipeline-freundlich. Zum Beispiel funktioniert Folgendes:

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

Sie können jedoch alle Optionen verwenden, die Sie angeben würden, grepund eine beliebige Anzahl von Dateinamen, die Sie möglicherweise auch übergeben. Das heißt, Sie können jede andere als eine Art verwenden - als Nebeneffekt der Parsing-Eingabe $IFSfunktioniert dies nicht, wenn Sie nach Leerzeilen suchen. Aber wer möchte aus einer nummerierten Liste von Leerzeilen auswählen?

Letzter Hinweis: Da dies funktioniert, indem numerische Benutzereingaben direkt in die im Argumentarray der Funktion gespeicherten numerischen Positionsparameter übersetzt werden, erfolgt die Ausgabe unabhängig von der Auswahl des Benutzers, so oft der Benutzer sie auswählt und in der vom Benutzer ausgewählten Reihenfolge es.

Zum Beispiel:

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
quelle
@mikeserv es war nur eine Idee, nicht das ganze Skript, und eine Sache, Sie sprechen über Test, die Originaldatei ist nur auf der Festplatte, also nehmen Sie von ihnen. Ich denke, es ist kein Problem oder zusätzlicher Aufwand, es zu testen
Hackaholic
@mikeserv yep du hast recht, ich habe nicht alles validiert, wie falsche Eingabe und alles.
Vielen
@mikeserv Ich kenne alle Grundlagen der Shell-Programmierung, können Sie mir zeigen, wie man fortgeschritten ist
Hackaholic
Ja sicher, ich werde es gerne bearbeiten
Hackaholic