74

How can one write a script that accept input from either a filename argument or from stdin?

for instance, you could use less this way. one can execute less filename and equivalently cat filename | less.

is there an easy "out of the box" way to do so? or do i need to re-invent the wheel and write a bit of logic in the script?

Oliver Salzburg
  • 86,445
  • 63
  • 260
  • 306
gilad hoch
  • 843
  • 1
  • 7
  • 8
  • @PlasmaPower As long as the question is on-topic on SU, there is no *requirement* to ask on a different SE site. A lot of SE sites have overlap; generally we do not need to suggest an overlapping site unless the question is either off-topic (in which case, vote to migrate) or on-topic but not getting much of a response (in which case, the asker should flag for moderator-attention/migration, not cross-post). – Bob May 04 '14 at 09:54
  • Related: [How to read from file or stdin in bash?](http://stackoverflow.com/q/6980090/55075) at SO – kenorb May 29 '15 at 17:12

8 Answers8

74

If the file argument is the first argument to your script, test that there is an argument ($1) and that it is a file. Else read input from stdin -

So your script could contain something like this:

#!/bin/bash
[ $# -ge 1 -a -f "$1" ] && input="$1" || input="-"
cat $input

e.g. then you can call the script like

./myscript.sh filename

or

who | ./myscript.sh

Edit Some explanation of the script:

[ $# -ge 1 -a -f "$1" ] - If at least one command line argument ($# -ge 1) AND (-a operator) the first argument is a file (-f tests if "$1" is a file) then the test result is true.

&& is the shell logical AND operator. If test is true, then assign input="$1" and cat $input will output the file.

|| is the shell logical OR operator. If the test is false, then commands following || are parsed. input is assigned to "-". The command cat - reads from the keyboard.

Summarising, if the script argument is provided and it is a file, then variable input is assigned to the file name. If there is no valid argument then cat reads from the keyboard.

suspectus
  • 4,735
  • 14
  • 25
  • 34
15

read reads from standard input. Redirecting it from file ( ./script <someinput ) or through pipe (dosomething | ./script) will not make it work differently.

All you have to do is to loop through all the lines in input (and it doesn't differ from iterating over the lines in file).

(sample code, processes only one line)

#!/bin/bash

read var
echo $var

Will echo first line of your standard input (either through < or |).

Lukasz Daniluk
  • 261
  • 1
  • 4
  • thanks! i choose the other answer because it suited me better. i was wrapping another script, and i didn't wanted to loop until all input recieved (could be a lot of input... would be wasteful). – gilad hoch Apr 30 '14 at 12:00
5

You don't mention what shell you plan on using, so I'll assume bash, though these are pretty standard things across shells.

File Arguments

Arguments can be accessed via the variables $1-$n ($0 returns the command used to run the program). Say I have a script that just cats out n number of files with a delimiter between them:

#!/usr/bin/env bash
#
# Parameters:
#    1:   string delimiter between arguments 2-n
#    2-n: file(s) to cat out
for arg in ${@:2} # $@ is the array of arguments, ${@:2} slices it starting at 2.
do
   cat $arg
   echo $1
done

In this case, we are passing a file name to cat. However, if you wanted to transform the data in the file (without explicitly writing and rewriting it), you could also store the file contents in a variable:

file_contents=$(cat $filename)
[...do some stuff...]
echo $file_contents >> $new_filename

Read from stdin

As far as reading from stdin, most shells have a pretty standard read builtin, though there are differences in how prompts are specified (at the very least).

The Bash builtins man page has a pretty concise explanation of read, but I prefer the Bash Hackers page.

Simply:

read var_name

Multiple Variables

To set multiple variables, just provide multiple parameter names to read:

read var1 var2 var3

read will then place one word from stdin into each variable, dumping all remaining words into the last variable.

λ read var1 var2 var3
thing1 thing2 thing3 thing4 thing5
λ echo $var1; echo $var2; echo $var3
thing1
thing2
thing3 thing4 thing5

If fewer words are entered than variables, the leftover variables will be empty (even if previously set):

λ read var1 var2 var3
thing1 thing2
λ echo $var1; echo $var2; echo $var3
thing1
thing2
# Empty line

Prompts

I use -p flag often for a prompt:

read -p "Enter filename: " filename

Note: ZSH and KSH (and perhaps others) use a different syntax for prompts:

read "filename?Enter filename: " # Everything following the '?' is the prompt

Default Values

This isn't really a read trick, but I use it a lot in conjunction with read. For example:

read -p "Y/[N]: " reply
reply=${reply:-N}

Basically, if the variable (reply) exists, return itself, but if is's empty, return the following parameter ("N").

4

The simplest way is to redirect stdin yourself:

if [ "$1" ] ; then exec < "$1" ; fi

Or if you prefer the more terse form:

test "$1" && exec < "$1"

Now the rest of your script can just read from stdin. Of course you can do similarly with more advanced option parsing rather than hard-coding the position of the filename as "$1".

  • `exec` will try to execute the argument as a command which is not what we want here. – Suzana May 27 '15 at 01:20
  • @Suzana_K: Not when it has no arguments, like here. In that case it just replaces file descriptors for the shell itself rather than a child process. – R.. GitHub STOP HELPING ICE May 27 '15 at 02:08
  • I copied `if [ "$1" ] ; then exec < "$1" ; fi` in a test script and it gives an error message because the command is unkown. Same with the terse form. – Suzana May 27 '15 at 03:05
  • 1
    @Suzana_K: What shell are you using? If that's true it's not a working implementation of the POSIX sh command/Bourne shell. – R.. GitHub STOP HELPING ICE May 27 '15 at 15:43
  • GNU bash 4.3.11 on Linux Mint Qiana – Suzana May 27 '15 at 15:58
  • @Suzana_K: I just tested with bash 4.3.33 and it works fine. I suspect you have a typo somewhere in the script or else some weird configuration getting processed. Are you entering the command interactively or in a script you're executing? Perhaps you could pastebin the script if you'd like me to look at it (or better yet, post a question here about why it's not working despite that it should). – R.. GitHub STOP HELPING ICE May 27 '15 at 19:52
  • Yes, that's weird. I'd also like to know the reason. I posted the output and the script to [this gist](https://gist.github.com/SuzanaK/04a1a66b2fe77f107013). – Suzana May 27 '15 at 20:24
  • @Suzana_K This error occurs because `exec` is trying to open the file `hallo`, which does not exist. If you want to *write* to the file, flip the `<` to `>`. – yyny Mar 26 '18 at 20:20
  • @R.. Thanks. But `exec < somefile` doesn't make the shell read from the file by reading from stdin. Instead, it treats `somefile` as a shell script and executes it like `source` the script file. See https://superuser.com/questions/747884/how-to-write-a-script-that-accepts-input-from-a-file-or-from-stdin?answertab=votes#tab-top – Tim Dec 28 '18 at 16:27
  • @Tim: That's false. I don't know where you got that misconception but it's easily testable as well as clear from the specification. – R.. GitHub STOP HELPING ICE Dec 28 '18 at 17:13
  • My typo. See https://unix.stackexchange.com/q/447317/674 – Tim Dec 28 '18 at 17:22
  • @Tim: That's in an interactive shell that's reading commands from stdin, as opposed to a shell executing a script. – R.. GitHub STOP HELPING ICE Dec 28 '18 at 18:27
  • Bash manual doesn't say `exec < somefile` behaves differently in a noninteractive shell from an interactive shell. From what reference did you get it? – Tim Dec 28 '18 at 18:32
  • It's not that `exec < somefile` behaves any differently. It doesn't. It's that the *shell's basic command processing* behaves differently in interactive mode (reading commands from stdin) vs executing a script (reading commands from the script). – R.. GitHub STOP HELPING ICE Dec 28 '18 at 18:49
4

You can also do:

#!/usr/bin/env bash

# Set variable input_file to either $1 or /dev/stdin, in case $1 is empty
# Note that this assumes that you are expecting the file name to operate on on $1
input_file="${1:-/dev/stdin}"

# You can now use "$input_file" as your file to operate on
cat "$input_file"

For more neat parameter substitution tricks in Bash, see this.

Daniel
  • 141
  • 4
3

use (or chain off of) something else that already behaves this way, and use "$@"

let's say i want to write a tool that will replace runs of spaces in text with tabs

tr is the most obvious way to do this, but it only accepts stdin, so we have to chain off of cat:

$ cat entab1.sh
#!/bin/sh

cat "$@"|tr -s ' ' '\t'
$ cat entab1.sh|./entab1.sh
#!/bin/sh

cat     "$@"|tr -s      '       '       '\t'
$ ./entab1.sh entab1.sh
#!/bin/sh

cat     "$@"|tr -s      '       '       '\t'
$ 

for an example where the tool being used already behaves this way, we could reimplement this with sed instead:

$ cat entab2.sh
#!/bin/sh

sed -r 's/ +/\t/g' "$@"
$ 
Aaron Davies
  • 131
  • 1
1

You can also keep it simple and use this code


When you create a script file pass_it_on.sh with this code,

#!/bin/bash

cat

You can run

cat SOMEFILE.txt | ./pass_it_on.sh

and all the contents of stdin will be simply spewed out to the screen.


Alternatively use this code to both keep a copy of stdin in a file and then spew it out to the screen.

#!/bin/bash

tmpFile=`mktemp`
cat > $tmpFile
cat $tmpFile    

and here is another example, maybe more readable, explained here:

http://mockingeye.com/blog/2013/01/22/reading-everything-stdin-in-a-bash-script/

#!/bin/bash

VALUE=$(cat)

echo "$VALUE"

Have fun.

RaamEE

RaamEE
  • 472
  • 1
  • 4
  • 19
1

The simplest way and POSIX compliant is:

file=${1--}

which is equivalent to ${1:--}.

Then read the file as usual:

while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")
kenorb
  • 24,736
  • 27
  • 129
  • 199