46

I have some scripts that work with parameters, they work just fine but i would like them to be able to read from stdin, from a pipe for example, an example, suppose this is called read:

#!/bin/bash
function read()
{
 echo $*
}

read $*

Now this works with read "foo" "bar", but I would like to use it as:

echo "foo" | read

How do I accomplish this?

kenorb
  • 155,785
  • 88
  • 678
  • 743
JBoy
  • 5,398
  • 13
  • 61
  • 101
  • 4
    Do you need to name the function 'read'? There is a bash built-in with the same name. – jwfearn Sep 05 '14 at 19:50
  • For command-line, check: [How to read from file or stdin in bash?](http://stackoverflow.com/q/6980090/55075) – kenorb Mar 01 '15 at 17:49

7 Answers7

45

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).

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 2
    Any tips on how to do this in bash 3? – jwfearn Sep 05 '14 at 19:58
  • 2
    Nothing simple. I think you would need to try to read from standard input in the background, then kill it after a second if it hasn't already completed, then try to determine if it read anything. I'm not sure how to do that in a fool-proof manner (there are issues of variables in subshells, dealing with partial reads of standard input, and race conditions). – chepner Sep 07 '14 at 13:12
  • @chepner: Which version of bash does this work on? It does not work on 4.1.0. – DocSalvager Dec 05 '14 at 11:38
  • How so? Works in 4.1.2 for me. – chepner Dec 05 '14 at 13:56
  • In the the first version, the 'read -t 0' is not true when data is passed by the pipe, though I don't know why. The second version, which depends instead on number of arguments, does work. It seems that your the version SHOULD work. I need functions to reliably use stdin so need to figure out the idioms required. Your answer is the most on point I've found. Any ideas? – DocSalvager Dec 05 '14 at 16:30
  • My best guess is that there is a bug in 4.1.0 that was fixed soon after. – chepner Dec 05 '14 at 16:45
  • 5
    I've managed to get this to work on my system: ' if read -t 0.00001 STDIN; then echo "$STDIN" ' The minimum timeout period that works seems to depend at least on CPU speed, and possibly system load as well. Values like 0.01 or 0.001 may be reasonably reliable. – DocSalvager Dec 05 '14 at 17:36
  • It would not work with `sleep 10 | catecho`.. Is there any way to achieve this? – hek2mgl Apr 18 '15 at 11:51
39

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
Community
  • 1
  • 1
fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • 5
    In the first example, you are really just feeding the word "echo" to the standard input of `readly`, which ignores its standard input, and setting the first argument to "hello". You could change "echo" to any other single word and get the same result. – chepner Sep 12 '13 at 14:06
  • 8
    The second example will discard everything after the first line of its standard input. – chepner Sep 12 '13 at 14:09
15

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}
    
Andy
  • 17,423
  • 9
  • 52
  • 69
  • 2
    `[ -t 0 ]` fail for me in some cases (e.g. calling the script from within a git-hook). i replaced it with `[ ! -p /dev/stdin ]` and was able to get it working on a wider case range. – Eliran Malka Mar 27 '17 at 11:53
6

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.

kenorb
  • 155,785
  • 88
  • 678
  • 743
5

Late to the party here. Building off of @andy's answer, here's how I define my to_uppercase function.

  • if stdin is not empty, use stdin
  • if stdin is empty, use args
  • if args are empty, do nothing
to_uppercase() {
  local input="$([[ -p /dev/stdin ]] && cat - || echo "$@")"
  [[ -n "$input" ]] && echo "$input" | tr '[:lower:]' '[:upper:]' 
}

Usages:

$ to_uppercase 
$ to_uppercase abc
ABC
$ echo abc | to_uppercase 
ABC
$ to_uppercase <<< echo abc 
ABC

Bash version info:

$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
zwbetz
  • 1,070
  • 7
  • 10
0

I've discovered that this can be done in one line using test and awk...

    test -p /dev/stdin  && awk '{print}' /dev/stdin

The test -p tests for input on a pipe, which accepts input via stdin. Only if input is present do we want to run the awk since otherwise it will hang indefinitely waiting for input which will never come.

I've put this into a function to make it easy to use...

inputStdin () {
  test -p /dev/stdin  && awk '{print}' /dev/stdin  && return 0
  ### accepts input if any but does not hang waiting for input
  #
  return 1
}

Usage...

_stdin="$(inputStdin)"

Another function uses awk without the test to wait for commandline input...

inputCli () {
  local _input=""
  local _prompt="$1"
  #
  [[ "$_prompt" ]]  && { printf "%s" "$_prompt" > /dev/tty; }
  ### no prompt at all if none supplied
  #
  _input="$(awk 'BEGIN {getline INPUT < "/dev/tty"; print INPUT}')"
  ### accept input (used in place of 'read')
  ###   put in a BEGIN section so will only accept 1 line and exit on ENTER
  ###   WAITS INDEFINITELY FOR INPUT
  #
  [[ "$_input" ]]  && { printf "%s" "$_input"; return 0; }
  #
  return 1
}

Usage...

_userinput="$(inputCli "Prompt string: ")"

Note that the > /dev/tty on the first printf seems to be necessary to get the prompt to print when the function is called in a Command Substituion $(...).

This use of awk allows the elimination of the quirky read command for collecting input from keyboard or stdin.

DocSalvager
  • 2,156
  • 1
  • 21
  • 28
0

Yet another version that:

  1. works by passing text through a pipe or from arguments
  2. easy to copy and paste by changing command in last line
  3. works in bash, zsh
# Prints a text in a decorated ballon
function balloon()
{
  (test -p /dev/stdin && cat - || echo $@) figlet -t | cowsay -n -f eyes | toilet -t --gay -f term
}

Usage:

# Using with a pipe
$ fortune -s | balloon
# Passing text as parameter
balloon "$(fortune -s )"
Juarez Rudsatz
  • 369
  • 3
  • 7