How to read stdin when no arguments are passed?

2019-03-25 13:43发布

问题:

Script doesn't work when I want to use standard input when there are no arguments (files) passed. Is there any way how to use stdin instead of a file in this code?

I tried this:

if [ ! -n $1 ] # check if argument exists
   then
   $1=$(</dev/stdin)  # if not use stdin as an argument
   fi

var="$1"
while read line
   do
   ...                # find the longest line
   done <"$var"

回答1:

Just substitute bash's specially interpreted /dev/stdin as the filename:

VAR=$1
while read blah; do
  ...
done < "${VAR:-/dev/stdin}"

(Note that bash will actually use that special file /dev/stdin if built for an OS that offers it, but since bash 2.04 will work around that file's absence on systems that do not support it.)



回答2:

For a general case of wanting to read a value from stdin when a parameter is missing, this will work.

$ echo param | script.sh
$ script.sh param

script.sh

#!/bin/bash

set -- "${1:-$(</dev/stdin)}" "${@:2}"

echo $1


回答3:

Variables are assigned a value by Var=Value and that variable is used by e.g. echo $Var. In your case, that would amount to

1=$(</dev/stdin)

when assigning the standard input. However, I do not think that variable names are allowed to start with a digit character. See the question bash read from file or stdin for ways to solve this.



回答4:

Here is my version of script:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line"
done < <(cat -- "$file")

If file is not present in the argument, read the from standard input.

See more examples: How to read from file or stdin in bash? at stackoverflow SE



回答5:

pilcrow's answer provides an elegant solution; this is an explanation of why the OP's approach didn't work.

The main problem with the OP's approach was the attempt to assign to positional parameter $1 with $1=..., which won't work.

The LHS is expanded by the shell to the value of $1, and the result is interpreted as the name of the variable to assign to - clearly, not the intent.

The only way to assign to $1 in bash is via the set builtin. The caveat is that set invariably sets all positional parameters, so you have to include the other ones as well, if any.

set -- "${1:-/dev/stdin}" "${@:2}"     # "${@:2}" expands to all remaining parameters

(If you expect only at most 1 argument, set -- "${1:-/dev/stdin}" will do.)

The above also corrects a secondary problem with the OP's approach: the attempt to store the contents rather than the filename of stdin in $1, since < is used.

${1:-/dev/stdin} is an application of bash parameter expansion that says: return the value of $1, unless $1 is undefined (no argument was passed) or its value is the empty string (""or '' was passed). The variation ${1-/dev/stdin} (no :) would only return /dev/stdin if $1 is undefined (if it contains any value, even the empty string, it would be returned).

If we put it all together:

# Default to filename '/dev/stdin' (stdin), if none was specified.
set -- "${1:-/dev/stdin}" "${@:2}"

while read -r line; do
   ...                # find the longest line
done < "$1"

But, of course, the much simpler approach would be to use ${1:-/dev/stdin} as the filename directly:

while read -r line; do
   ...                # find the longest line
done < "${1:-/dev/stdin}"

or, via an intermediate variable:

filename=${1:-/dev/stdin}
while read -r line; do
   ...                # find the longest line
done < "$filename"


标签: bash stdin