Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash script read pipe or argument

I want my script to read a string either from stdin , if it's piped, or from an argument. So first i want to check if some text is piped and if not it should use an argument as input. My code looks something like this:

value=$(cat) # read from stdin
if [ "$pipe" != "" ]; then #check if pipe is not empty
 #Do something with pipe string
else
 #Do something with argument string
fi

The problem is when it's not piped, then the script will halt and wait for "ctrl d" and i dont want that. Any suggestions on how to solve this?

Thanks in advance. /Tomas

like image 645
Tom Avatar asked Dec 03 '13 12:12

Tom


Video Answer


2 Answers

If you only need to know if it's a pipe or a redirection, it should be sufficient to determine if stdin is a terminal or not:

if [ -t 0 ]; then
    # stdin is a tty: process command line
else 
    # stdin is not a tty: process standard input
fi

[ (aka test) with -t is equivalent to the libc isatty() function. The above will work with both something | myscript and myscript < infile. This is the simplest solution, assuming your script is for interactive use.

The [ command is a builtin in bash and some other shells, and since [/test with -tis in POSIX, it's portable too (not relying on Linux, bash, or GNU utility features).

There's one edge case, test -t also returns false if the file descriptor is invalid, but it would take some slight adversity to arrange that. test -e will detect this, though assuming you have a filename such as /dev/stdin to use.

The POSIX tty command can also be used, and handles the adversity above. It will print the tty device name and return 0 if stdin is a terminal, and will print "not a tty" and return 1 in any other case:

if tty >/dev/null ; then
    # stdin is a tty: process command line
else
    # stdin is not a tty: process standard input
fi

(with GNU tty, you can use tty -s for silent operation)

A less portable way, though certainly acceptable on a typical Linux, is to use GNU stat with its %F format specifier, this returns the text "character special file", "fifo" and "regular file" in the cases of terminal, pipe and redirection respectively. stat requires a filename, so you must provide a specially-named file of the form /dev/stdin, /dev/fd/0, or /proc/self/fd/0, and use -L to chase symlinks:

stat -L -c "%F" /dev/stdin

This is probably the best way to handle non-interactive use (since you can't make assumptions about terminals then), or to detect an actual pipe (FIFO) distinct from redirection.

There is a slight gotcha with %F in that you cannot use it to tell the difference between a terminal and certain other device files, for example /dev/zero or /dev/null which are also "character special files" and might reasonably appear. An unpretty solution is to use %t to report the underlying device type (major, in hex), assuming you know what the underlying tty device number ranges are... and that depends on whether you're using BSD style ptys or Unix98 ptys, or whether you're on the actual console, among other things. In the simple case %t will be 0 though for a pipe or a redirection of a normal (non-special) file.

More general solutions to this kind of problem are to use bash's read with a timeout (read -t 0 ...) or non-blocking I/O with GNU dd (dd iflag=nonblock).

The latter will allow you to detect lack of input on stdin, dd will return an exit code of 1 if there is nothing ready to read. However, these are more suitable for non-blocking polling loops, rather than a once-off check: there is a race condition when you start two or more processes in a pipeline as one may be ready to read before another has written.

like image 165
mr.spuratic Avatar answered Oct 12 '22 22:10

mr.spuratic


What about checking the argument first?

if (($#)) ; then
     process "$1"
else
     cat | process
fi

Or, just take advantage from the same behaviour of cat:

cat "$@" | process
like image 30
choroba Avatar answered Oct 12 '22 23:10

choroba