9

General problem

I want to write a script that interacts with the user even though it is in the middle of a chain of pipes.

Concrete example

Concretely, it takes a file or stdin, displays lines (with line numbers), asks the user to input a selection or line numbers, and then prints the corresponding lines to stdout. Let's call this script selector. Then basically, I want to be able to do

grep abc foo | selector > myfile.tmp

If foo contains

blabcbla
foo abc bar
quux
xyzzy abc

then selector presents me (on the terminal, not in myfile.tmp!) with options

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

after which I type in

2-3

and end up with

foo abc bar
xyzzy abc

as contents of myfile.tmp.

I've got a selector script up and running, and basically it is working perfectly if I don't redirect input and output. So

selector foo

behaves like I want. However, when piping things together as in the above example, selector prints the presented options to myfile.tmp and tries to read a selection from the grepped input.

My approach

I've tried to use the -u flag of read, as in

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

but this doesn't do what I hoped it would.

Q: How do I get actual user interaction?

  • make a script and save the output in variable, and then present user want you want?? – Hackaholic Nov 8 '14 at 21:07
  • @Hackaholic — I'm not sure what you mean. I want a script that can be placed in any kind of pipeline sequence (i.e. the Unix way). I gave an elaborate example above, but that is certainly not the only use case I have in mind. – jmc Nov 8 '14 at 21:09
  • 1
    Use cmd | { some processing; read var </dev/tty; } | cmd – mikeserv Nov 8 '14 at 21:15
  • @mikeserv — Interesting! I now have 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; }' which works pretty good. However grep b foo | selector | wc -l breaks over here. Any ideas how to fix that? By the way, the rangeselect that I used can be found at pastebin.com/VAxTSSHs. It is a simple AWK script that prints the lines of a file corresponding to a given range of linenumbers. (Ranges can be things like "3-10, 12,14,16-20".) – jmc Nov 8 '14 at 21:38
  • 1
    Don't alias that, rather selector() { all of that stuff...; } into a function. aliases rename simple commands whereas functions pack a compound command into a single simple command. – mikeserv Nov 8 '14 at 21:42
7

Using /proc/$PPID/fd/0 is unreliable: the parent of the selector process may not have the terminal as its input.

There is a standard path that always refers to the current process's terminal: /dev/tty.

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

or

exec </dev/tty >/dev/tty
nl "$INPUT"
read -p"Select options: "
  • 1
    Thanks, that solves my problem. The answer is a bit minimalistic though. I guess it might benefit from incorporating some of mikeserv's advice in the comments to the question. – jmc Nov 10 '14 at 22:29
2

I have written a small function: it will not answer about what you have asked chaining of pipe but will solve your 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)"
)

The function turns over all arguments you give it immediately to grep. If you use a shell glob to specify the files it should read from it will return all of the matches in all files, starting with the first in the glob order and ending with the last match.

grep passes its output to nl which numbers each line and which passes its output to tee which duplicates its output both to stdout and to /dev/tty . This means that the output from the pipeline is simultaneously printed both to the function's argument array where it is split on \newlines and to the terminal as it works.

Next the _in() function attempts to read in a selection if there is at least 1 result from the previous action a maximum of five times. The selection can consist of only numbers separated by spaces, or else number ranges separated by -. If anything else is read (including a blank line) it will try again - but only, as before, a maximum of five times.

Last the _out() function parses the user's selection and expands any ranges therein. It prints its results in the form "${[num]}" for each - thereby matching the value of the lines stored in inf()'s arg array. This output is evaled as args to printf which therefore prints only the lines the user has selected.

It explicitly read's from the terminal and only prints the Select: menu to stderr and so it is plenty pipeline friendly. For example, the following works:

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

But you can use any options you would give grep and any number of filenames you might hand it as well. That is, you can use any but one kind - as a side-effect of its parsing input with $IFS it will not work if you are searching for blank lines. But who would want to select from a numbered list of blank lines?

Last note that because this works by directly translating numeric user input into the numeric positional parameters stored in the function's argument array, then the output will be whatever the user selects, as many times as the user selects it, and in whatever order the user selects it.

For example:

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 it was just a idea, not the whole script, and one thing , you talking about test, the original file is in disk only, so you taking from them . so i think it not an issue or extra effort to test it – Hackaholic Nov 8 '14 at 22:33
  • @mikeserv yep you are right, i haven't validated all the thing,like improper input and all. thanks for your point – Hackaholic Nov 8 '14 at 22:44
  • @mikeserv i know all basic of shell programming, can u guide me how to be go for advanced – Hackaholic Nov 8 '14 at 22:53
  • yes sure i will be glad edit it – Hackaholic Nov 8 '14 at 23:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.