How to make a bash function which can read from standard input?

It's a little tricky to write a function which can read standard input, but works properly when no standard input is given. If you simply try to read from standard input, it will block until it receives any, much like if you simply type cat at the prompt.

In bash 4, you can work around this by using the -t option to read with an argument of 0. It succeeds if there is any input available, but does not consume any of it; otherwise, it fails.

Here's a simple function that works like cat if it has anything from standard input, and echo otherwise.

catecho () {
    if read -t 0; then
        cat
    else
        echo "$*"
    fi
}

$ catecho command line arguments
command line arguments
$ echo "foo bar" | catecho
foo bar

This makes standard input take precedence over command-line arguments, i.e., echo foo | catecho bar would output foo. To make arguments take precedence over standard input (echo foo | catecho bar outputs bar), you can use the simpler function

catecho () {
    if [ $# -eq 0 ]; then
        cat
    else
        echo "$*"
    fi
}

(which also has the advantage of working with any POSIX-compatible shell, not just certain versions of bash).


Here is example implementation of sprintf function in bash which uses printf and standard input:

sprintf() { local stdin; read -d '' -u 0 stdin; printf "$@" "$stdin"; }

Example usage:

$ echo bar | sprintf "foo %s"
foo bar

This would give you an idea how function can read from standard input.


You can use <<< to get this behaviour. read <<< echo "text" should make it.

Test with readly (I prefer not using reserved words):

function readly()
{
 echo $*
 echo "this was a test"
}

$ readly <<< echo "hello"
hello
this was a test

With pipes, based on this answer to "Bash script, read values from stdin pipe":

$ echo "hello bye" | { read a; echo $a;  echo "this was a test"; }
hello bye
this was a test

To combine a number of other answers into what worked for me (this contrived example turns lowercase input to uppercase):

  uppercase() {
    local COMMAND='tr [:lower:] [:upper:]'
    if [ -t 0 ]; then
      if [ $# -gt 0 ]; then
        echo "$*" | ${COMMAND}
      fi
    else
      cat - | ${COMMAND} 
    fi
  }

Some examples (the first has no input, and therefore no output):

:; uppercase
:; uppercase test
TEST
:; echo test | uppercase 
TEST
:; uppercase <<< test
TEST
:; uppercase < <(echo test)
TEST

Step by step:

  • test if file descriptor 0 (/dev/stdin) was opened by a terminal

    if [ -t 0 ]; then
    
  • tests for CLI invocation arguments

    if [ $# -gt 0 ]; then
    
  • echo all CLI arguments to command

    echo "$*" | ${COMMAND}
    
  • else if stdin is piped (i.e. not terminal input), output stdin to command (cat - and cat are shorthand for cat /dev/stdin)

    else
      cat - | ${COMMAND}
    

Tags:

Bash

Stdin

Pipe