views:

758

answers:

6

I was trying to do this to decide whether to redirect stdin to a file or not:

[ ...some condition here... ] && input=$fileName || input="&0"
./myScript < $input

But that doesn't work because when the variable $input is "&0", bash interprets it as a filename.

However, I could just do:

if [ ...condition... ];then
    ./myScript <$fileName
else
    ./myScript

The problem is that ./myScript is actually a long command line that I don't want to duplicate, nor do I want to create a function for it because it's not that long either (it's not worth it).

Then it occurred to me to do this:

[ ...condition... ] && input=$fileName || input=  #empty
cat $input | ./myScript

But that requires to run one more command and a pipe (i.e. a subshell).
Is there another way that's simpler and more efficient?

+1  A: 

How about

function runfrom {
    local input="$1"
    shift
    case "$input" in
        -) "$@" ;;
        *) "$@" < "$input" ;;
    esac
}

I've used the minus sign to denote standard input because that's traditional for many Unix programs.

Now you write

[ ... condition ... ] && input="$fileName" || input="-"
runfrom "$input" my-complicated-command with many arguments

I find these functions/commands which take commands as arguments (like xargs(1)) can be very useful, and they compose well.

Norman Ramsey
+1  A: 

Use eval:

#! /bin/bash

[ $# -gt 0 ] && input="'"$1"'" || input="&1"

eval "./myScript <$input"

This simple stand-in for myScript

#! /usr/bin/perl -lp
$_ = reverse

produces the following output:

$ ./myDemux myScript
pl- lrep/nib/rsu/ !#
esrever = _$

$ ./myDemux
foo
oof
bar
rab
baz
zab

Note that it handles spaces in inputs too:

$ ./myDemux foo\ bar
eman eht ni ecaps a htiw elif

To pipe input down to myScript, use process substitution:

$ ./myDemux <(md5sum /etc/issue)
eussi/cte/  01672098e5a1807213d5ba16e00a7ad0

Note that if you try to pipe the output directly, as in

$ md5sum /etc/issue | ./myDemux

it will hang waiting on input from the terminal, whereas ephemient's answer does not have this shortcoming.

A slight change produces the desired behavior:

#! /bin/bash

[ $# -gt 0 ] && input="'"$1"'" || input=/dev/stdin
eval "./myScript <$input"
Greg Bacon
What's the difference between `command1 <(command2)`, `command1 <<<$(command2)` and `command2 | command1` ??
GetFree
The first is process substitution, whose documentation is linked above. The second composes command substitution (`$(...)`) with here-string (`<<<`) to send the output of `command2` to the standard input of `command1`. The last goes through the usual fork-exec sequence for running commands but creates a pipe whose write end replaces the standard output of `command2` and whose read end replaces the standard input of `command1`.
Greg Bacon
+1  A: 

If you're careful, you can use 'eval' and your first idea.

[ ...some condition here... ] && input=$fileName || input="&1"
eval ./myScript < $input

However, you say that 'myScript' is actually a complex command invocation; if it involves arguments which might contain spaces, then you must be very careful before deciding to use 'eval'.

Frankly, worrying about the cost of a 'cat' command is probably not worth the trouble; it is unlikely to be the bottleneck.

Even better is to design myScript so that it works like a regular Unix filter - it reads from standard input unless it is given one or more files to work (like, say, cat or grep as examples). That design is based on long and sound experience - and is therefore worth emulating to avoid having to deal with problems such as this.

Jonathan Leffler
+2  A: 
(
    if [ ...some condition here... ]; then
        exec <$fileName
    fi
    exec ./myscript
)

In a subshell, conditionally redirect stdin and exec the script.

ephemient
+1. Put quotes around `$fileName` in case it contains spaces.
Greg Bacon
+2  A: 

First of all stdin is file descriptor 0 (zero) rather than 1 (which is stdout).

You can duplicate file descriptors or use filenames conditionally like this:

[[ some_condition ]] && exec 3<$filename || exec 3<&0

some_long_command_line <&3
Dennis Williamson
Just what I needed. If I create the new file descriptor inside a function, will it still exist after the function ends?
GetFree
Yes, but so will the pointer into the file. In other words, if you seek to the end of the file in the function you will still be there outside the function. `test() { exec 3<inputfile; cat < }; cat <
Dennis Williamson
+1  A: 

Standard input can also be represented by the special device file /dev/stdin, so using that as a filename will work.

file="/dev/stdin"
./myscript < "$file"
Ignacio Vazquez-Abrams
I get a "permission denied" error when I'm not root.
GetFree
That sounds like possibly some sort of misconfiguration; `/dev/stdin` should (almost) always be usable. In particular, I believe Linux and Solaris both implement it as a symlink to `/proc/self/fd/0`, and the only problems I can think of would be if UID has changed.
ephemient
Well, so one "common" situation would be: terminal is owned by user A, script is running as user B, stdin is tied to terminal. Is this what you're doing?
ephemient
I logged in as root and then switched to a normal user with `su visit`. Could that be the problem?
GetFree
Yes, it could; `ls -l \`tty\`` will tell you for sure. If it's not owned by `visit`, trying to reopen `/dev/stdin` may fail.
ephemient
That's the problem then. It's owned by root. Which means I can't rely on this solution since It won't always work (and in my case it will never work as I normally use `su` to test scripts that other users will run)
GetFree