Cómo leer la entrada del usuario cuando se usa el script en la tubería


10

Problema general

Quiero escribir un script que interactúe con el usuario a pesar de que está en medio de una cadena de tuberías.

Ejemplo concreto

Concretamente, toma una fileo stdin, muestra líneas (con números de línea), le pide al usuario que ingrese una selección o números de línea, y luego imprime las líneas correspondientes en stdout. Llamemos a este script selector. Entonces, básicamente, quiero poder hacer

grep abc foo | selector > myfile.tmp

Si foocontiene

blabcbla
foo abc bar
quux
xyzzy abc

luego selectorme presenta (¡en la terminal, no dentro myfile.tmp!) con opciones

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

después de lo cual escribo

2-3

y terminar con

foo abc bar
xyzzy abc

como contenido de myfile.tmp.

Tengo un script selector en funcionamiento, y básicamente funciona perfectamente si no redirijo la entrada y la salida. Entonces

selector foo

se comporta como yo quiero. Sin embargo, al unir elementos como en el ejemplo anterior, selectorimprime las opciones presentadas myfile.tmpe intenta leer una selección de la entrada grepped.

Mi acercamiento

He tratado de usar la -ubandera de read, como en

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

Pero esto no hace lo que esperaba.

P: ¿Cómo obtengo la interacción real del usuario?


hacer un script y guardar la salida en variable, y luego el usuario actual quiere que quieras ??
Hackaholic

@Hackaholic: no estoy seguro de lo que quieres decir. Quiero un script que se pueda colocar en cualquier tipo de secuencia de canalización (es decir, la forma Unix). Di un ejemplo elaborado anteriormente, pero ese no es el único caso de uso que tengo en mente.
jmc

1
Usocmd | { some processing; read var </dev/tty; } | cmd
mikeserv el

@mikeserv - ¡Interesante! Ahora tengo 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 bastante bien. Sin embargo se grep b foo | selector | wc -lrompe por aquí. ¿Hay alguna idea de cómo arreglar eso? Por cierto, el rangeselectque usé se puede encontrar en pastebin.com/VAxTSSHs . Es un script AWK simple que imprime las líneas de un archivo correspondiente a un rango dado de números de lino. (Los rangos pueden ser cosas como "3-10, 12,14,16-20".)
jmc

1
No hagas aliaseso, más bien selector() { all of that stuff...; }en una función. aliases renombrar comandos simples mientras que las funciones empaquetan un comando compuesto en un solo comando simple .
mikeserv

Respuestas:


8

El uso /proc/$PPID/fd/0no es confiable: el padre del selectorproceso puede no tener el terminal como entrada.

Hay una ruta estándar que siempre se refiere a la terminal del proceso actual: /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
Gracias, eso resuelve mi problema. Sin embargo, la respuesta es un poco minimalista. Supongo que podría beneficiarse incorporar algunos de los consejos de mikeserv en los comentarios a la pregunta.
jmc

2

He escrito una pequeña función: no responderá sobre lo que ha pedido encadenamiento de tubería, pero resolverá su 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 función entrega todos los argumentos a los que se los da de inmediato grep. Si usa un glob de shell para especificar los archivos que debería leer, devolverá todas las coincidencias en todos los archivos, comenzando con el primero en el orden glob y terminando con la última coincidencia.

greppasa su salida a nlqué números cada línea y cuál pasa su salida a la teecual duplica su salida tanto a stdoutcomo a /dev/tty. Esto significa que la salida de la tubería se imprime simultáneamente tanto en la matriz de argumentos de la función donde se divide en \nlíneas electrónicas como en el terminal a medida que funciona.

Luego, la _in()función intenta readen una selección si hay al menos 1 resultado de la acción anterior un máximo de cinco veces. La selección puede consistir solo en números separados por espacios, o bien rangos de números separados por -. Si hay algo más read (incluida una línea en blanco) volverá a intentarlo, pero solo, como antes, un máximo de cinco veces.

Por último, la _out()función analiza la selección del usuario y expande cualquier rango en ella. Imprime sus resultados en el formulario "${[num]}"para cada uno, igualando así el valor de las líneas almacenadas en inf()la matriz arg de. Esta salida se evaledita como argumentos en los printfque, por lo tanto, imprime solo las líneas que el usuario ha seleccionado.

Es explícitamente readdesde la terminal y solo imprime el Select:menú stderry, por lo tanto, es mucho más fácil de usar. Por ejemplo, lo siguiente 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

Pero puede usar cualquier opción que le dé grepy cualquier cantidad de nombres de archivo que pueda darle también. Es decir, puede usar cualquier tipo, ya que como efecto secundario de su entrada de análisis $IFSno funcionará si está buscando líneas en blanco. Pero, ¿quién querría seleccionar de una lista numerada de líneas en blanco?

Última nota que debido a que esto funciona traduciendo directamente la entrada numérica del usuario en los parámetros posicionales numéricos almacenados en la matriz de argumentos de la función, la salida será lo que el usuario seleccione, tantas veces como el usuario lo seleccione, y en el orden que el usuario seleccione eso.

Por ejemplo:

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 fue solo una idea, no todo el script, y una cosa, hablando de la prueba, el archivo original está solo en el disco, por lo que tomas de ellos. así que creo que no es un problema o un esfuerzo extra probarlo
Hackaholic

@mikeserv sí, tienes razón, no he validado todo, como entradas incorrectas y todo. gracias por tu punto
Hackaholic

@mikeserv Sé todo lo básico de la programación de shell, ¿puedes guiarme sobre cómo avanzar
Hackaholic

sí, seguro que me alegrará editarlo
Hackaholic
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.