13

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

2
  • 1
    you could always read only from stdin. If you want to pass input via command line then you could use your_script <<< 'some string' -- your script will see it as an ordinary input from stdin. Commented Dec 3, 2013 at 21:24
  • duplicate of stackoverflow.com/questions/29501860/… Commented Jul 25, 2022 at 4:46

3 Answers 3

13

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
Sign up to request clarification or add additional context in comments.

1 Comment

Couldn't get what I wanted working. In fish using test with or without if or other commands to get argument list count would steal the potential piped input, and so does echo $argv | cat | read ..... If I really wanted the functionality, maybe ruby's ARGF or even just any larger programming language at that point maybe, but I don't need it. Guess I'll just force everything through as a prompt or argument for now.
8

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.

Comments

6

It's easier to check for command line arguments first and fallback to stdin if no arguments. Shell Parameter Expansion is a nice shorthand instead of the if-else:

value=${*:-`cat`}
# do something with $value

2 Comments

Would you care to provide a more complete example? This looks like an interesting idea, but hard to understand how this might be used to solve the question.
The question didn't do anything with the input, so there's not much more to add. It creates a variable that has all arguments as it's value. If no arguments are provided it will fallback to stdin. So just use $value as a regular variable.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.