0

I have the following code that takes in a file via an arg and if none exists, take it from stdin:

file=
column=
pattern=
path=
cmd="echo"

while getopts "f:c:e:" opt; do
        case $opt in
                f) file="$OPTARG";;
                c) column="$OPTARG";;
                e) pattern="$OPTARG";;
                *) echo "Usage: $0 -f [file] -c [column] -e [pattern]"
        esac
done

if [ -z "$file" ]; then
        file=$(cat)
fi

the problem with this approach is, cat won't work on non-files so if it were taken in from stdout for example:

cat data.csv | ./read.sh -c 2 -e " Max"

then I won't be able to run:

cat "$file"

inside the bash script, I'd have to use echo instead.

OTOH, if it is a file, I can use cat and everything is fine. My question is how to do i make the script able to recognize whether or not the var $file is an actual filename or stdout.

Edited: I've taken into account the comments and answers, here's my entire script of how I read to the user their specified column and regex and account for them both passing in a file from stdin or as an arg:

#!/bin/bash

unset -v column pattern filepath
cmd=echo

DELIMITER=","

while getopts "f:c:e:" opt; do
        case $opt in
                f) file="$OPTARG";;
                c) column="$OPTARG";;
                e) pattern="$OPTARG";;
                *) printf>&2 '%s\n' "Usag: $0 [-f file] [-c column] [-e pattern]"; exit 1
        esac
done

if [ -z "$file" ]; then
        file=$(cat)
elif [ -z "$column" ]; then
        echo "column number neeeded!"
elif [ -z "$pattern" ]; then
        echo "pattern needed!"
fi

if [ -f "$file" ]; then
        cmd="cat"
fi

if [ "$column" -eq 5 ]; then
        DELIMITER=":"
fi

"$cmd" "$file" | awk -v col="$column" -v pattern="$pattern" -v del="$DELIMITER" -F "$DELIMITER" '{
        for (i=1;i<=NF;i++) {
                if ( i == col && $i == pattern ) {
                        print $0
                } else if ( del == ":" && $i == pattern ) {
                        print $0
                }
        }       
}'

exit 0

it works, but I'm sure its not the right approach. once again, I'm trying to let my file do both:

cat data.csv | ./read.sh -c 2 -e "Max"

and:

./read.sh -f data.csv -c 2 -e "Max"

the script above works, but optimization is ofc the name of the game!

1
  • 1
    I'm not sure I understand your question; cat will read from stdin (or, indeed, other non-files, such as block/character devices); it reads stdin by default if you don't give it a filename to work on, or if you specify - as the filename. Oct 21, 2023 at 2:14

2 Answers 2

2

For cat, - means stdin. On many systems /dev/stdin would work with any command, not just cat, while on Linux or Cygwin, /dev/stdin would point to the same file as open on stdin, not behaving exactly the same as stdin in several cases.

So you could do:

file=-
unset -v column pattern filepath
cmd=echo

while getopts f:c:e: opt; do
  case $opt in
    (f) file="$OPTARG";;
    (c) column="$OPTARG";;
    (e) pattern="$OPTARG";;
    (*) printf>&2 '%s\n' "Usage: $0 [-f file] [-c column] [-e pattern]"; exit 1
  esac
done
shift "$(( OPTIND - 1 ))"

cat -- "$file"

Beware you'll be able to read stdin (-) only once.

Also note:

  • errors should go to stderr
  • echo can't be used for arbitrary data (such as $0 upon which you have no control).
  • [...] in usage messages by convention means optional, so with -f [file], you're implying -f is required but its argument is optional which is not true here.
  • avoid naming a variable path if that script might end up being interpreted in zsh (when not in sh emulation), where $path is the array variable tied to $PATH like in csh/tcsh.
  • you should avoid testing for empty values to check whether an argument was provided. For instance, with -f '', you'd get an empty $file even though the user explicitly passed an argument to -f (admittedly a bogus one for a file path here). Instead, use unset -v var in the initialisation to make sure the variable is unset, and check for [ -z "${var+set}" ] to check whether it's still unset afterwards. Or use extra boolean flag variables to record whether the option was passed or not.
  • if you want to allow the user to pass a file called - and mean the actual file called - in the current working directory, not stdin, you'd transform it to ./- in the part that handles (f).
  • file=$(cat) reads what can be read from stdin and stores it in the file variable (in memory). So as others have already said the name of the variable is a bit misleading as that's the input file's contents you get. But also it's the mangle contents you get as $(...) removes all trailing newline characters and except in zsh removes or chokes on NUL characters. If you use echo (which again cannot be used for arbitrary data) on it, echo will do its own mangling.

Here you could also take the reverse approach and open the file passed as argument to -f on stdin:

unset -v column pattern filepath
cmd=echo

while getopts f:c:e: opt; do
  case $opt in
    (f) exec < "$OPTARG";;
    (c) column="$OPTARG";;
    (e) pattern="$OPTARG";;
    (*) printf>&2 '%s\n' "Usage: $0 [-f file] [-c column] [-e pattern]"; exit 1
  esac
done
shift "$(( OPTIND - 1 ))"

cat

Note that POSIX shells would exit automatically when exec fails to open the file. If passed more than one -f option, like when assigning $file, only the last one will be taken into account.

1
  • Thank you so much for this! I truly appreciate all the extra info about the mistakes I'm making that aren't related to the question. I added to the original question with my current method to print echo if its file contents from stdin or a file as an arg, can you take a look?
    – Mathew
    Oct 21, 2023 at 22:31
2

To answer your actual question, you can use test to see if it is a file. so

if [ -z "$file" ] ; then
    echo no file specified
elif [ -f "$file" ] ; then
    echo given a valid file "$file"
else
    echo Given "$file" but it is not a file
fi

However I think this is missing the point, you have file=$(cat) and I think you are being confused by the reuse of the file variable name. If instead you said file_contents=$(cat) then I think you will find things clearer.

3
  • how would i read the contents of a file that was provided as an arg? is this going to work: file="$OPTARG";;? or will that just store the name of the file? and if so, how would i get the files contents?
    – Mathew
    Oct 21, 2023 at 15:43
  • When the question was originally posted it didn't show the processing that was being done on the contents of the file or stdin. As such it was hard to make a reasonable guess about what was needed. Now we have the details. As the other answer has shown you, you can avoid reading the file into a shell variable and always use cat to copy the file, using a filename of - if you want to read from stdin. Yes file=$OPTIND stores the value given on the command line, the filename, into the variable file.
    – icarus
    Oct 22, 2023 at 13:17
  • I've written the following to test it works: ``` 1 #!/bin/bash 2 3 file=- 4 5 while getopts f: opt; do 6 case $opt in 7 f) file="$OPTARG";; 8 esac 9 done 10 11 shift "$(( OPTIND - 1 ))" 12 13 cat - - "$file" | grep Max``` This code works only with cat data.csv | ./read.sh and not with ./read.sh -f data.csv
    – Mathew
    Oct 23, 2023 at 13:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .