Как прочитать пользовательский ввод при использовании скрипта в трубе


10

Общая проблема

Я хочу написать сценарий, который взаимодействует с пользователем, даже если он находится в середине цепочки каналов.

Конкретный пример

Конкретно, требуется fileили stdin, отображает строки (с номерами строк), просит пользователя ввести выбор или номера строк, а затем печатает соответствующие строки stdout. Давайте назовем этот скрипт selector. Тогда в принципе, я хочу быть в состоянии сделать

grep abc foo | selector > myfile.tmp

Если fooсодержит

blabcbla
foo abc bar
quux
xyzzy abc

тогда selectorдарит мне (на терминале, а не в myfile.tmp!) варианты

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

после чего я набираю

2-3

и в конечном итоге

foo abc bar
xyzzy abc

как содержание myfile.tmp.

У меня есть скрипт выбора и работает, и в основном он работает отлично, если я не перенаправить ввод и вывод. Так

selector foo

ведет себя как я хочу. Однако при объединении вещей, как в приведенном выше примере, selectorпечатает представленные параметры myfile.tmpи пытается прочитать выборку из сброшенного ввода.

Мой подход

Я пытался использовать -uфлаг read, как в

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

но это не то, что я надеялся.

Q: Как я могу получить реальное взаимодействие с пользователем?


сделать скрипт и сохранить вывод в переменную, а затем представить пользователю, что вы хотите ??
Hackaholic

@Hackaholic - я не уверен, что ты имеешь в виду. Я хочу сценарий, который может быть размещен в любой последовательности конвейера (например, в Unix). Я привел сложный пример выше, но это, конечно, не единственный вариант использования, который я имею в виду.
JMC

1
Использованиеcmd | { some processing; read var </dev/tty; } | cmd
mikeserv

@mikeserv - Интересно! У меня сейчас есть, 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; }'который работает довольно хорошо. Однако grep b foo | selector | wc -lперерывы здесь. Есть идеи как это исправить? Кстати, то, rangeselectчто я использовал, можно найти на pastebin.com/VAxTSSHs . Это простой скрипт AWK, который печатает строки файла, соответствующие заданному диапазону номеров белья. (Диапазоны могут быть такими, как "3-10, 12,14,16-20".)
JMC

1
Не так alias, скорее selector() { all of that stuff...; }в функцию. aliases переименовывают простые команды, тогда как функции упаковывают составную команду в одну простую команду .
mikeserv

Ответы:


8

Использование /proc/$PPID/fd/0ненадежно: родительский selectorпроцесс может не иметь терминала в качестве входных данных.

Существует стандартный путь , который всегда относится к терминалу текущего процесса в: /dev/tty.

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

или

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

1
Спасибо, это решает мою проблему. Ответ немного минималистичный, хотя. Я полагаю, что это может быть полезно, если бы некоторые комментарии mikeserv были включены в комментарии к вопросу.
JMC

2

Я написал небольшую функцию: она не будет отвечать о том, о чем вы просили связать трубы, но решит вашу проблему.

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

Функция переворачивает все аргументы, которые вы ей передаете grep. Если вы используете глобус оболочки для указания файлов, которые он должен прочитать, он вернет все совпадения во всех файлах, начиная с первого в порядке глобинга и заканчивая последним совпадением.

grepпередает свои выходные данные, на nlкоторые нумерует каждую строку, и которые передают свои выходные данные, на teeкоторые дублирует свои выходные данные и в stdoutи в /dev/tty. Это означает, что выходные данные из конвейера одновременно выводятся как в массив аргументов функции, где он разделяется на электронные \nлинии, так и в терминал, как он работает.

Затем _in()функция пытается выполнить readвыборку, если хотя бы один результат предыдущего действия был максимум пять раз. Выбор может состоять только из чисел, разделенных пробелами, или диапазонов номеров, разделенных -. Если что-то еще read (включая пустую строку), он попытается снова - но только, как и прежде, максимум пять раз.

Последняя _out()функция анализирует выбор пользователя и расширяет любые диапазоны в нем. Он печатает свои результаты в форме "${[num]}"для каждого - тем самым сопоставляя значение строк, хранящихся в inf()массиве arg. Этот вывод evalобрабатывается как аргументы, printfпоэтому печатает только те строки, которые выбрал пользователь.

Он явно readиз терминала и печатает только Select:меню stderrи поэтому достаточно дружественен к конвейеру. Например, следующие работы:

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

Но вы можете использовать любые опции, которые вы дадите, grepи любое количество имен файлов, которые вы можете передать. То есть вы можете использовать любой, кроме одного вида - поскольку побочный эффект его синтаксического ввода с $IFSним не будет работать, если вы ищете пустые строки. Но кто хотел бы выбрать из нумерованного списка пустых строк?

В заключение отметим, что поскольку это работает путем прямого преобразования числового пользовательского ввода в числовые позиционные параметры, хранящиеся в массиве аргументов функции, то результатом будет то, что выберет пользователь, столько раз, сколько выберет пользователь, и в любом порядке, выбранном пользователем. Это.

Например:

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 это была просто идея, а не весь сценарий, и одна вещь, вы говорите о тесте, исходный файл находится только на диске, так что вы берете их. так что я думаю, что это не проблема или дополнительные усилия, чтобы проверить это
Hackaholic

@mikeserv Да, вы правы, я не все проверял, например, неправильный ввод и все такое. спасибо за вашу точку зрения
Hackaholic

@mikeserv Я знаю все основы программирования оболочки, можете ли вы
научить

да, конечно, я буду рад отредактировать его
Hackaholic
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.