3

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
12
  • If a file exists named '4', and the user enters ./stats -row 4, what behavior do you want? Commented Apr 1, 2015 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? Commented Apr 1, 2015 at 22:24
  • 1
    Use shift to remove the first argument, then use $@. Commented Apr 1, 2015 at 22:25
  • a plus one for "I broke my script down to the problem I need help with" Good luck. Commented Apr 1, 2015 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. Commented Apr 1, 2015 at 22:44

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

3 Comments

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.
I don't like it either.. That was the purpose of the very in-artful comment at the beginning.
@JohnKugelman, INPUT_FILE=${2:-/dev/stdin} was the key to an artful solution.
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

1 Comment

Thanks, creating a temp file and placing the values the user entered worked out after making a few adjustments.

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.