I am creating a script (myscript.sh) in BASH that reads from STDOUT, typically a stream of data that comes from cat, or from a file and outputs the stream of data (amazing!), like this:
$cat myfile.txt
hello world!
$cat myfile.txt | myscript.sh
hello world!
$myscript.sh myfile.txt
hello world!
But I also would like the following behaviour: if I call the script without arguments I'd like it to output a brief help:
$myscript.sh
I am the help: I just print what you say.
== THE PROBLEM ==
The problem is that I am capturing the stream of data like this:
if [[ $# -eq 0 ]]; then
stream=$(cat <&0)
elif [[ -n "$stream" ]]; then
echo "I am the help: I just print what you say."
else
echo "Unknown error."
fi
And when I call the script with no arguments like this:
$myscript.sh
It SHOULD print the "help" part, but it just keep waiting for a stream of data in line 2 of code above...
Is there any way to tell bash that if nothing comes from STDOUT just break and continue executing?
Thanks in advance.
There's always a standard input stream; if no arguments are given and input isn't redirected, standard input is the terminal.
If you want to treat that specially, use test -t to test if standard input is connected to a terminal.
if [[ $# -eq 0 && -t 0 ]]; then
echo "I am the help: I just print what you say."
else
stream=$(cat -- "$#")
fi
There's no need to test $#. Just pass your arguments to cat; if it gets filenames it will read from them, otherwise it will read from standard input.
I agree to #Barmar's solution.
However, it might be better to entirely avoid a situation where your program behavior depends on whether the input file descriptor is a terminal (there are situations where a terminal is mimicked even though there's none -- in such a situation, your script would just produce the help string).
You could instead introduce a special - argument to explicitly request reading from stdin. This will result in simpler option handling and uniform behavior of your script, no matter what's the environment.
First answer is to help yourself - try running the script with bash -x myscript.sh. It will include lot of information to help you.
If you specific case, the condition $# -eq 0 was flipped. As per requirement, you want to print the help message is NOT ARGUMENT ARE PROVIDED:
if [[ $# -eq 0 ]] ; then
echo "I am the help: I just print what you say."
exit 0
fi
# Rest of you script, read data from file, etc.
cat -- "$#"
Assuming this approach is taken, and if you want to process standard input or a file, simple pass '-' as parameter: cat foobar.txt | myscript.sh -
Related
I'm totally new in writing shell scripts so I could use some help here.
I would like to write a script that when run with no parameters it just echo backs, and when it is given a data (.dat) file it displays the content of it.
Excuse me for my bad English,
R.
This script, when run with no parameters it just echo backs and when a filename is passed as argument, it displays the content of it:
#!/bin/sh
# Explanation - We use'$#' to count number of arguments.
if ! [ $# -gt 0 ]; then
# Explanation - Zeroth argument '$0' is scriptname itself. Print it.
cat "$0"
else
# Explanation - Print (cat) 1st argument.
cat "$1"
fi
NOTE: Since you've used 'minix' tag, I tested it on minix3. The script works well on minix as well as linux.
I apologize in advance - I don't fully understand the ideas behind what I'm asking well enough to understand why it's not working (I don't know what I need to learn). I searched stack exchange for answers first - I found some information that seemed possibly relevant, but didn't explain the concepts well enough that I understood how to build a working solution. I've been scouring google but haven't found any information that describes exactly what's going on in such a way that I understand. Any direction to background concepts that may help me understand what's going on would be greatly appreciated.
Is it possible to get user input in a bash script that was executed from a pipe?
For example:
wget -q -O - http://myscript.sh | bash
And in the script:
read -p "Do some action (y/n): " __response
if [[ "$__response" =~ ^[Yy]$ ]]; then
echo "Performing some action ..."
fi
As I understand it, this doesn't work because read attempts to read the input from stdin and the bash script is currently "executing through that pipe" (i'm sure there is a more technical accurate way to describe what is occurring, but i don't know how).
I found a solution that recommended using:
read -t 1 __response </dev/tty
However, this does not work either.
Any light shed on the concepts I need to understand to make this work, or explanations of why it is not working or solutions would be greatly appreciated.
The tty solution works. Test it with this code, for example:
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? y
Sat Apr 12 10:51:16 PDT 2014
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? n
OK
The prompt from read appears on the terminal and read waits for a response before deciding to echo the date or not.
What I wrote above differs from the line below in two key aspects:
read -t 1 __response </dev/tty
First, the option -t 1 gives read a timeout of one second. Secondly, this command does not provide a prompt. The combination of these two probably means that, even though read was briefly asking for input, you didn't know it.
The main reason why this is not working is, as the OP validly indicated,
The | <pipe> which is used, sends the standard output from the first command as standard input to the second command. In this case, the first command is
wget -q -O - http://myscript.sh
which passes a downloaded script via the pipe to its interpreter bash
The read statement in the script uses the same standard input to obtain its value.
So this is where it collapses because read is not awaiting input from you but takes it from its own script. Example:
$ cat - <<EOF | bash
> set -x
> read p
> somecommand
> echo \$p
> EOF
+ read p
+ echo somecommand
somecommand
In this example, I used a here-document which is piped to bash. The script enables debugging using set -x to show what is happening. As you see, somecommand is never executed but actually read by read and stored in the variable p which is then outputted by echo (note, the $ has been escaped to avoid the substitution in the here-document).
So how can we get this to work then?
First of, never pipe to an interpreter such as {ba,k,z,c,tc,}sh. It is ugly and should be avoided, even though it feels the natural thing to do. The better thing to do is to use any of its options:
bash -c string: If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.
$ bash -c "$(command you want to pipe)"
This also works for zsh, csh, tcsh, ksh, sh and probably a lot of others.
I am trying to write a loop, and this doesn't work:
for t in `ls $TESTS_PATH1/cmd*.in` ; do
diff $t.out <($parser_test `cat $t`)
# Print result
if [[ $? -eq 0 ]] ; then
printf "$t ** TEST PASSED **"
else
printf "$t ** TEST FAILED **"
fi
done
This also doesn't help:
$parser_test `cat $t` | $DIFF $t.out -
Diff shows that output differs (it's strange, I see output of needed error line as it was printed to stdout, and not caught by diff), but when running with temporary file, everything works fine:
for t in `ls $TESTS_PATH1/cmd*.in` ; do
# Compare output with template
$parser_test `cat $t` 1> $TMP_FILE 2> $TMP_FILE
diff $TMP_FILE $t.out
# Print result
if [[ $? -eq 0 ]] ; then
printf "$t $CGREEN** TEST PASSED **$CBLACK"
else
printf "$t $CRED** TEST FAILED **$CBLACK"
fi
done
I must avoid using temporary file. Why first loop doesn't work and how to fix it?
Thank you.
P.S. Files *.in contain erroneous command line parameters for program, and *.out contain errors messages that program must print for these parameters.
First, to your error, you need to redirect standard error:
diff $t.out <($parser_test `cat $t` 2>&1)
Second, to all the other problems you may not be aware of:
don't use ls with a for loop (it has numerous problems, such as unexpected behavior in filenames containing spaces); instead, use: for t in $TESTS_PATH1/cmd*.in; do
to support file names with spaces, quote your variable expansion: "$t" instead of $t
don't use backquotes; they are deprecated in favor of $(command)
don't use a subshell to cat one file; instead, just run: $parser_test <$t
use either [[ $? == 0 ]] (new syntax) or [ $? -eq 0 ] (old syntax)
if you use printf instead of echo, don't forget that you need to add \n at the end of the line manually
never use 1> $TMP_FILE 2> $TMP_FILE - this just overwrites stdout with stderr in a non-predictable manner. If you want to combine standard out and standard error, use: 1>$TMP_FILE 2>&1
by convention, ALL_CAPS names are used for/by environment variables. In-script variable names are recommended to be no_caps.
you don't need to use $? right after executing a command, it's redundant. Instead, you can directly run: if command; then ...
After fixing all that, your script would look like this:
for t in $tests_path1/cmd*.in; do
if diff "$t.out" <($parser_test <"$t" 2>&1); then
echo "$t ** TEST PASSED **"
else
echo "$t ** TEST FAILED **"
fi
done
If you don't care for the actual output of diff, you can add >/dev/null right after diff to silence it.
Third, if I understand correctly, your file names are of the form foo.in and foo.out, and not foo.in and foo.in.out (like the script above expects). If this is true, you need to change the diff line to this:
diff "${t/.in}.out" <($parser_test <"$t" 2>&1)
In your second test you are capturing standard error, but in the first one (and the pipe example) stderr remains uncaptured, and perhaps that's the "diff" (pun intended).
You can probably add a '2>&1' in the proper place to combine the stderr and stdout streams.
.eg.
diff $t.out <($parser_test cat $t 2>&1)
Not to mention, you don't say what "doesn't work" means, does that mean it doesn't find a difference, or it exits with an error message? Please clarify in case you need more info.
I write many simple scripts for testing or processing CSV files. Most of scripts have same logic: read one file and write the output to a console or another file. For such case I usually use the simplest for implementation approach: read from stdin, write to stdout, so invocation is like:
script < input.csv > output.csv
Can you give some points why I shall prefer this style:
script input.csv > output.csv
Or even:
script input.csv output.csv
Here's how you can have a script that handles both (not guaranteed to be 100% safe, maybe someone can give better options or pinpoint serious issues in a comment):
#!/bin/bash
if [[ -n $1 ]]; then
exec "$0" < "$1" || exit $?
fi
# your program starts here, reading stdin, e.g.,
while read; do
echo "$REPLY"
done
This is how I deal with the same task now, similar to what #gniourf_gniourf's answer does (except my scripts are in Haskell).
When the program is invoked with arguments, like CMD INPUT1 [INPUT2 [...]], treat them as input files and ignore standard input, except for special case when one or more args are -.
When no args are given, read standard input. So CMD call without args is equivalent to CMD -.
I'm writing a test suite for my app and using a bash script to check that the test suite output matches the expected output. Here is a section of the script:
for filename in test/*.bcs ;
do
./BCSC $filename > /dev/null
NUMBER=`echo "$filename" | awk -F"[./]" '{print $2}'`
gcc -g -m32 -mstackrealign runtime.c $filename.s -o test/e$NUMBER
# run the file and diff against expected output
echo "Running test file... "$filename
test/e$NUMBER > test/e$NUMBER.out
if [ $NUMBER = "4" ]
then
# it's trying to read the line
# Pass some input to the file...
fi
diff test/e$NUMBER.out test/o$NUMBER.out
done
Test #4 tests reading input from stdin. I'd like to test for script #4, and if so pass it a set of sample inputs.
I just realized you could do it like
test/e4 < test/e4.in > test/e4.out
where e4.in has the sample inputs. Is there another way to pass input to a running script?
If you want to supply the input data directly in the script, use a here-document:
test/e$NUMBER > test/e$NUMBER.out
if [ $NUMBER = "4" ]; then
then
test/e$NUMBER > test/e$NUMBER.out <<END_DATA
test input goes here
you can supply as many lines of input as you want
END_DATA
else
test/e$NUMBER > test/e$NUMBER.out
fi
There are several variants: if you quote the delimiter (i.e. <<'END_DATA'), it won't do things like replace $variable replacement in the here-document. If you use <<-DELIMITER, it'll remove leading tab characters from each line of input (so you can indent the input to match the surrounding code). See the "Here Documents" section in the bash man page for details.
The way you mentioned is the conventional method to redirect a file into stdin when issuing a command/script.
Maybe it'll help if you'll elaborate on the "other way" you're looking for, as in, why do you even need a different way to do so? Is there anything you need to do which this method does not allow?
You can do:
cat test/e4.in | test/e4 > test/e4.out