在管道中使用脚本时如何读取用户输入


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并尝试从grepped输入中读取选择。

我的方法

我尝试使用的-u标志read,如

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

但这并没有达到我的期望。

问:如何获得实际的用户交互?


制作脚本并将输出保存在变量中,然后呈现用户所需的内容??
Hackaholic

@Hackaholic —我不确定您的意思。我想要一个可以放置在任何形式的管道序列中的脚本(即Unix方式)。我在上面给出了一个详尽的示例,但这当然不是我想到的唯一用例。
jmc 2014年

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 2014年

1
不用alias那样,而是selector() { all of that stuff...; }变成一个函数。aliases重命名简单命令,而函数将复合命令打包为单个简单命令
mikeserv

Answers:


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 2014年

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。如果您使用Shell Glob来指定应从其中读取的文件,则它将返回所有文件中的所有匹配项,从Glob顺序中的第一个开始到最后一个匹配项结束。

grep将其输出传递给nl每行编号,将其输出传递给输出,tee该输出将其输出复制到stdout/dev/tty。这意味着管道的输出同时打印到函数的参数数组(\n在行上拆分)和工作时的端子。

接下来,如果前一操作至少有1个结果(最多五次),则该_in()功能将尝试进行read选择。该选择只能包含以空格分隔的数字,也可以包含以分隔的数字范围-。如果还有其他任何内容read (包括空行),它将再次尝试-但只能像以前一样最多进行五次。

最后,该_out()函数解析用户的选择并扩展其中的任何范围。它"${[num]}"以每种形式打印结果-从而匹配存储在inf()arg数组中的行的值。此输出eval以args 形式编辑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 2014年

@mikeserv是的,您是对的,我还没有验证所有内容,例如输入不正确等。感谢您的意见
Hackaholic 2014年

@mikeserv我知道Shell编程的所有基本知识,您能指导我如何进阶

是的,我肯定会很高兴对其进行编辑
Hackaholic
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.