2

I am trying to set my Linux shell script to read from a file (which I have working) but if there isn't any file then I need to read from stdin.

The command for reading a file looks like this:

./stats -row test_file

How would I be able to read what the user enters with something like this:

./stats -row 4 2 3 5 3 4 5 3 6 5 6 3 4

When I enter a command like this I get 'no such file or directory'

I broke my script down to the problem I need help with.

#!/bin/sh

INPUT_FILE=$2         #Argument 2 from command line is the input file
exec 5< $INPUT_FILE   #assign input file to file descriptor #5

while read -u 5 line  #read from file descriptor 5 (input file)
do
    echo "$line"
done

exec 5<&-   #close file descriptor #5

This also won't work for the input I need.

while read line  
do
    echo "$line"
done <$2
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Casey Balza
  • 152
  • 1
  • 12
  • If a file exists named '4', and the user enters `./stats -row 4`, what behavior do you want? – William Pursell Apr 01 '15 at 22:19
  • Thanks for pointing that out. I know I can use $@ to get all of the arguments, how do you get the arguments from 2 and on? – Casey Balza Apr 01 '15 at 22:24
  • 1
    Use `shift` to remove the first argument, then use $@. – William Pursell Apr 01 '15 at 22:25
  • a plus one for "I broke my script down to the problem I need help with" Good luck. – shellter Apr 01 '15 at 22:34
  • You can read from `/dev/stdin` or `/dev/fd/0` if there is no input file at all. How do you determine between having a file and having command line arguments? If it is 'number of arguments > 2', then there are various tricks you can use to arrange for the arguments to be fed -- process substitution might be one, for example. – Jonathan Leffler Apr 01 '15 at 22:44
  • `4 2 3 5 3 4 5 3 6 5 6 3 4` can be converted to a file-like object using process substitution. In this way, you could write the command as: `./stats -row < <(echo 4 2 3 5 3 4 5 3 6 5 6 3 4)`. Is that acceptable? – John1024 Apr 01 '15 at 22:48
  • Or `./stats -row <<< '4 2 3 5 3 4 5 3 6 5 6 3 4'` in bash. – John Kugelman Apr 01 '15 at 22:49
  • You ought to use a `-f filename` and/or `--file filename` to indicate it _should_ read from a file, rather than assuming a file and using args if the file doesn't exist. As William Pursell asks, what if there happens to be a file with the name as your first arg (which you meant as a value, not a filename). You may also want to see [how to use getopts](http://stackoverflow.com/q/16483119/17300) – Stephen P Apr 01 '15 at 23:03
  • Also, do you want to **"read from stdin if no file"** as in the question title, or do you want to _use command-line args if no file_ as your "How would I be able" example suggests? – Stephen P Apr 01 '15 at 23:07
  • @CaseyBalza, you read from the second argument on with `"${@:2}"`. – David C. Rankin Apr 01 '15 at 23:17
  • possible duplicate of [How to read from file or stdin in bash?](http://stackoverflow.com/questions/6980090/how-to-read-from-file-or-stdin-in-bash) – David C. Rankin Apr 01 '15 at 23:30
  • Thanks all for the input, I used a little combination of what is here, the big key of getting this to work was placing the arguments from 2 and up into a temp file. – Casey Balza Apr 02 '15 at 03:26

2 Answers2

2

InArtful Solution

A very in-artful if statement will do the trick:

INPUT_FILE=$2         #Argument 2 from command line is the input file

if [ -f "$INPUT_FILE" ]; then

    while read -r line
    do
        echo "$line"
    done <"$INPUT_FILE"

else

    while read -r line
    do
        echo "$line"
    done

fi

Note: this presumes you are still looking for the filename as the 2nd argument.


Artful Solution

I cannot take credit, but the artful solution was already answered here: How to read from file or stdin in bash?

INPUT_FILE=${2:-/dev/stdin}         #Argument 2 from command line is the input file

while read -r line
do
    echo "$line"
done <"$INPUT_FILE"

exit 0

I was picking around with a solution like this but missed the stdin device /dev/stdin as the default for INPUT_FILES. note this solution is limited to OS's with a proc-filesystem.

Community
  • 1
  • 1
David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • The script changes behavior depending on the existence of its argument? Ick, I don't care for that too much. It should be designed so the caller can clearly delineate whether they're passing a file name or not. Having to do this is a sign of a poor API. – John Kugelman Apr 01 '15 at 22:59
  • 1
    I don't like it either.. That was the purpose of the `very in-artful` comment at the beginning. – David C. Rankin Apr 01 '15 at 23:15
  • 1
    @JohnKugelman, `INPUT_FILE=${2:-/dev/stdin}` was the key to an `artful` solution. – David C. Rankin Apr 01 '15 at 23:29
1

In bash scripts, I usually put code that reads from a file (or a pipe) in a function, where the redirection can be separated from the logic.

Also, when reading from a file or from STDIN, it's a good idea for the logic to not care which is which. So, it's best to capture STDIN into a temp file and then the rest of the file reading code is the same.

Here's an example script that reads from ARG 1 or from STDIN, and just counts the lines in the file. It also invokes wc -l on the same input and shows the results from both methods.

#!/bin/bash

# default input is this script
input=$0

# If arg given, read from it
if (( $# > 0 )); then
  input=$1
  echo 1>&2 "Reading from $input"
else
  # otherwise, read from STDIN
  # since we're reading twice, need to capture it into
  # a temp file
  input=/tmp/$$.tmp
  cat >$input
  trap "rm -f $input" EXIT ERR HUP INT QUIT
  echo 1>&2 "Reading from STDIN (saved to $input)"
fi

count_lines() {
  local count=0
  while read line ; do
    let count+=1
  done
  echo $count
}

lines1=`count_lines <$input`
lines2=`wc -l <$input`

fmt="%15s: %d\n"
printf "$fmt" 'count_lines' $lines1
printf "$fmt" 'wc -l'       $lines2

exit

Here are two invocations: one with a file on arg 1, and one with no argument, reading from STDIN:

$ ./t2.sh t2.sh
Reading from t2.sh
    count_lines: 35
          wc -l: 35

$ ./t2.sh <t2.sh
Reading from STDIN (saved to /tmp/8757.tmp)
    count_lines: 35
          wc -l: 35
aks
  • 2,328
  • 1
  • 16
  • 15
  • Thanks, creating a temp file and placing the values the user entered worked out after making a few adjustments. – Casey Balza Apr 02 '15 at 02:48