The following Perl script (my.pl
) can read from either the file on the command line args or from STDIN:
while (<>) {
print($_);
}
perl my.pl
will read from STDIN, while perl my.pl a.txt
will read from a.txt
. This is very handy.
Wondering is there an equivalent in Bash?
The following solution reads from a file if the script is called
with a file name as the first parameter $1
otherwise from standard input.
while read line
do
echo "$line"
done < "${1:-/dev/stdin}"
The substitution ${1:-...}
takes $1
if defined otherwise
the file name of the standard input of the own process is used.
I combined all of the above answers and created a shell function that would suit my needs. This is from a cygwin terminal of my 2 Windows10 machines where I had a shared folder between them. I need to be able to handle the following:
cat file.cpp | tx
tx < file.cpp
tx file.cpp
Where a specific filename is specified, I need to used the same filename during copy. Where input data stream has been piped thru, then i need to generate a temporary filename having the hour minute and seconds. The shared mainfolder has subfolders of the days of the week. This is for organizational purposes.
Behold, the ultimate script for my needs:
tx ()
{
if [ $# -eq 0 ]; then
local TMP=/tmp/tx.$(date +'%H%M%S')
while IFS= read -r line; do
echo "$line"
done < /dev/stdin > $TMP
cp $TMP //$OTHER/stargate/$(date +'%a')/
rm -f $TMP
else
[ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
fi
}
If there is any way that you can see to further optimize this, I would like to know.
Please try the following code:
while IFS= read -r line; do
echo "$line"
done < file
How about
for line in `cat`; do
something($line);
done
Perl's behavior, with the code given in the OP can take none or several arguments, and if an argument is a single hyphen -
this is understood as stdin. Moreover, it's always possible to have the filename with $ARGV
.
None of the answers given so far really mimic Perl's behavior in these respects. Here's a pure Bash possibility. The trick is to use exec
appropriately.
#!/bin/bash
(($#)) || set -- -
while (($#)); do
{ [[ $1 = - ]] || exec < "$1"; } &&
while read -r; do
printf '%s\n' "$REPLY"
done
shift
done
Filename's available in $1
.
If no arguments are given, we artificially set -
as the first positional parameter. We then loop on the parameters. If a parameter is not -
, we redirect standard input from filename with exec
. If this redirection succeeds we loop with a while
loop. I'm using the standard REPLY
variable, and in this case you don't need to reset IFS
. If you want another name, you must reset IFS
like so (unless, of course, you don't want that and know what you're doing):
while IFS= read -r line; do
printf '%s\n' "$line"
done
I think this is the straight-forward way:
$ cat reader.sh
#!/bin/bash
while read line; do
echo "reading: ${line}"
done < /dev/stdin
--
$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
echo "line ${i}"
done
--
$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5
Code ${1:-/dev/stdin}
will just understand first argument, so, how about this.
ARGS='$*'
if [ -z "$*" ]; then
ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
echo "$line"
done
More accurately...
while IFS= read -r line ; do
printf "%s\n" "$line"
done < file
Here is the simplest way:
#!/bin/sh
cat -
Usage:
$ echo test | sh my_script.sh
test
To assign stdin to the variable, you may use: STDIN=$(cat -)
or just simply STDIN=$(cat)
as operator is not necessary (as per @mklement0 comment).
To parse each line from the standard input, try the following script:
#!/bin/bash
while IFS= read -r line; do
printf '%s\n' "$line"
done
To read from the file or stdin (if argument is not present), you can extend it to:
#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")
Notes:
-
read -r
- Do not treat a backslash character in any special way. Consider each backslash to be part of the input line.- Without setting
IFS
, by default the sequences of Space and Tab at the beginning and end of the lines are ignored (trimmed).- Use
printf
instead ofecho
to avoid printing empty lines when the line consists of a single-e
,-n
or-E
. However there is a workaround by usingenv POSIXLY_CORRECT=1 echo "$line"
which executes your external GNUecho
which supports it. See: How do I echo "-e"?
See: How to read stdin when no arguments are passed? at stackoverflow SE
The following works with standard sh
(Tested with dash
on Debian) and is quite readable, but that's a matter of taste:
if [ -n "$1" ]; then
cat "$1"
else
cat
fi | commands_and_transformations
Details: If the first parameter is non-empty then cat
that file, else cat
standard input. Then the output of the whole if
statement is processed by the commands_and_transformations
.
as a workaround you can use the stdin device in /dev directory
....| for item in `cat /dev/stdin` ; do echo $item ;done
The Perl loop in the question reads from all the file name arguments on the command line, or from standard input if no files are specified. The answers I see all seem to process a single file or standard input if there is no file specified.
Although often derided accurately as UUOC (Useless Use of cat
), there are times when cat
is the best tool for the job, and it is arguable that this is one of them:
cat "$@" |
while read -r line
do
echo "$line"
done
The only downside to this is that it creates a pipeline running in a sub-shell, so things like variable assignments in the while
loop are not accessible outside the pipeline. The bash
way around that is Process Substitution:
while read -r line
do
echo "$line"
done < <(cat "$@")
This leaves the while
loop running in the main shell, so variables set in the loop are accessible outside the loop.
I don't find any of these answers acceptable. In particular, the accepted answer only handles the first command line parameter and ignores the rest. The Perl program that it is trying to emulate handles all the command line parameters. So the accepted answer doesn't even answer the question. Other answers use bash extensions, add unnecessary 'cat' commands, only work for the simple case of echoing input to output, or are just unnecessarily complicated.
However, I have to give them some credit because they gave me some ideas. Here is the complete answer:
#!/bin/sh
if [ $# = 0 ]
then
DEFAULT_INPUT_FILE=/dev/stdin
else
DEFAULT_INPUT_FILE=
fi
# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
while IFS= read -r LINE
do
# Do whatever you want with LINE here.
echo $LINE
done < "$FILE"
done
This one is easy to use on the terminal:
$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3
The echo
solution adds new lines whenever IFS
breaks the input stream. @fgm's answer can be modified a bit:
cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
Perhaps the simplest solution is to redirect stdin with a merging redirect operator:
#!/bin/bash
less <&0
Stdin is file descriptor zero. The above sends the input piped to your bash script into less's stdin.
#!/usr/bin/bash
if [ -p /dev/stdin ]; then
#for FILE in "$@" /dev/stdin
for FILE in /dev/stdin
do
while IFS= read -r LINE
do
echo "$@" "$LINE" #print line argument and stdin
done < "$FILE"
done
else
printf "[ -p /dev/stdin ] is false\n"
#dosomething
fi
running:
echo var var2 | bash std.sh
result:
var var2
running:
bash std.sh < <(cat /etc/passwd)
result:
root:x:0:0::/root:/usr/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin
Source: Stackoverflow.com