Comment lire les entrées utilisateur lors de l'utilisation d'un script dans un canal

10

Problème général

Je veux écrire un script qui interagit avec l'utilisateur même s'il se trouve au milieu d'une chaîne de tuyaux.

Exemple concret

Concrètement, il faut un fileou stdin, affiche les lignes (avec des numéros de ligne), demande à l'utilisateur de saisir une sélection ou des numéros de ligne, puis imprime les lignes correspondantes dans stdout. Appelons ce script selector. Alors en gros, je veux pouvoir faire

grep abc foo | selector > myfile.tmp

Si foocontient

blabcbla
foo abc bar
quux
xyzzy abc

selectorme présente ensuite (sur le terminal, pas dedans myfile.tmp!) des options

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

après quoi je tape

2-3

et finir avec

foo abc bar
xyzzy abc

comme contenu de myfile.tmp.

J'ai un script de sélection opérationnel et, fondamentalement, il fonctionne parfaitement si je ne redirige pas l'entrée et la sortie. Donc

selector foo

se comporte comme je veux. Cependant, lors de l'assemblage des éléments comme dans l'exemple ci-dessus, selectorimprime les options présentées myfile.tmpet essaie de lire une sélection à partir de l'entrée reçue.

Mon approche

J'ai essayé d'utiliser le -udrapeau de read, comme dans

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

mais cela ne fait pas ce que j'espérais.

Q: Comment obtenir une interaction réelle avec l'utilisateur?

jmc
la source
faire un script et enregistrer la sortie en variable, puis présenter l'utilisateur que vous voulez ??
Hackaholic
@Hackaholic - Je ne sais pas ce que tu veux dire. Je veux un script qui peut être placé dans n'importe quel type de séquence de pipeline (c'est-à-dire à la manière Unix). J'ai donné un exemple détaillé ci-dessus, mais ce n'est certainement pas le seul cas d'utilisation auquel je pense.
jmc
1
Utilisationcmd | { some processing; read var </dev/tty; } | cmd
mikeserv
@mikeserv - Intéressant! J'ai maintenant 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; }'qui fonctionne assez bien. Mais se grep b foo | selector | wc -lcasse ici. Des idées pour réparer celà? Soit dit en passant, celui rangeselectque j'ai utilisé se trouve sur pastebin.com/VAxTSSHs . Il s'agit d'un simple script AWK qui imprime les lignes d'un fichier correspondant à une plage donnée de linumbers. (Les plages peuvent être des choses comme "3-10, 12,14,16-20".)
jmc
1
Ne faites pas aliascela, plutôt selector() { all of that stuff...; }dans une fonction. aliases renomme les commandes simples tandis que les fonctions regroupent une commande composée en une seule commande simple .
mikeserv

Réponses:

8

L'utilisation /proc/$PPID/fd/0n'est pas fiable: le parent du selectorprocessus peut ne pas avoir le terminal comme entrée.

Il y a un chemin standard qui se réfère toujours au terminal du processus en cours: /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- arrête d'être méchant'
la source
1
Merci, cela résout mon problème. La réponse est cependant un peu minimaliste. Je suppose qu'il pourrait être avantageux d'incorporer certains des conseils de mikeserv dans les commentaires à la question.
jmc
2

J'ai écrit une petite fonction: elle ne répondra pas à ce que vous avez demandé de chaîner des tuyaux mais résoudra votre problème.

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 fonction retourne tous les arguments auxquels vous lui donnez immédiatement grep. Si vous utilisez un glob de shell pour spécifier les fichiers qu'il doit lire, il retournera toutes les correspondances dans tous les fichiers, en commençant par le premier dans l'ordre global et en terminant par la dernière correspondance.

greppasse sa sortie à nllaquelle numérote chaque ligne et qui passe sa sortie à teelaquelle duplique sa sortie à la fois vers stdoutet vers /dev/tty. Cela signifie que la sortie du pipeline est imprimée simultanément à la fois dans le tableau d'arguments de la fonction où elle est divisée sur les \nlignes électroniques et sur le terminal pendant son fonctionnement.

Ensuite, la _in()fonction tente readdans une sélection s'il y a au moins 1 résultat de l'action précédente un maximum de cinq fois. La sélection peut être composée uniquement de nombres séparés par des espaces, ou bien des plages de nombres séparées par -. Si quelque chose d'autre est read (y compris une ligne vierge), il réessayera - mais seulement, comme précédemment, au maximum cinq fois.

Enfin, la _out()fonction analyse la sélection de l'utilisateur et étend toutes les plages qui s'y trouvent. Il imprime ses résultats sous la forme "${[num]}"de chacun - correspondant ainsi à la valeur des lignes stockées dans inf()le tableau arg de. Cette sortie est evaléditée sous forme d'arguments sur printflesquels n'imprime donc que les lignes sélectionnées par l'utilisateur.

Il readprovient explicitement du terminal et n'imprime que le Select:menu vers stderret il est donc très convivial pour le pipeline. Par exemple, les travaux suivants:

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

Mais vous pouvez utiliser toutes les options que vous donneriez grepet n'importe quel nombre de noms de fichiers que vous pourriez également lui donner . Autrement dit, vous pouvez utiliser n'importe quel type, mais un effet secondaire de son entrée d'analyse avec $IFSelle ne fonctionnera pas si vous recherchez des lignes vides. Mais qui voudrait choisir parmi une liste numérotée de lignes vierges?

Enfin, comme cela fonctionne en traduisant directement l'entrée utilisateur numérique dans les paramètres positionnels numériques stockés dans le tableau d'arguments de la fonction, la sortie sera alors ce que l'utilisateur sélectionne, autant de fois que l'utilisateur le sélectionne et dans l'ordre que l'utilisateur sélectionne il.

Par exemple:

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
la source
@mikeserv c'était juste une idée, pas tout le script, et une chose, vous parlez de test, le fichier d'origine est uniquement sur le disque, donc vous en tirez d'eux. donc je pense que ce n'est pas un problème ou un effort supplémentaire pour le tester
Hackaholic
@mikeserv yep vous avez raison, je n'ai pas tout validé, comme une entrée incorrecte et tout. merci pour votre point
Hackaholic
@mikeserv je connais toutes les bases de la programmation shell, pouvez-vous me guider pour être avancé
Hackaholic
oui, bien sûr, je serai heureux de le modifier
Hackaholic