read builtin doesn't work with pipe - bash

I'd like to ask user a confirmation to read from stdin (Display output [Y/n]). It works Ok if some arguments were provided, or no arguments were provided but there was some input. However, if some data was piped to the script, there's no confirmation.
#!/bin/bash
output_file=$(mktemp)
cleanup() {
rm -f "$output_file"
}
trap cleanup 0 1 2 3 15
if [ $# -gt 0 ]; then
while [ $# -gt 0 ]; do
echo "$1" >> "$output_file"
shift
done
else
while read -r line; do
echo "$line" >> "$output_file"
done
fi
while true; do
read -p "Display output? [Y/n]" response
if [ -z "$response" ]; then
break
fi
case $response in
[Yy]*) break;;
[Nn]*) exit;;
esac
done
less "$output_file"
What prevent read -p to work? What should be done to provide consistent behavior?

The read command reads input from standard in. If you have standard in fed from a pipe then read looks for its data from the pipe, not from your terminal.
On most platforms you can work around this by redirecting the read command's input directly from the tty device, as in:
read -p "Display output? [Y/n]" response </dev/tty

If the script read everything from standard input, what is the read -p going to get? And it likely doesn't prompt if the input is not an 'interactive device' (aka terminal). Have you checked the Bash man page for read? It says:
-pprompt
Display prompt, without a trailing newline, before attempting to read any input. The prompt is displayed only if input is coming from a terminal.
When your input is from a pipe, it is not from a terminal.

Related

Pseudo-input for Bash's "read" variable

I have a script listening for a user input like that.
read -p "Run? (y/[n]) " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
[..]
fi
Is there a way (upon executing the script) to already send the value which
read is going to read and handle?
your_script.sh <<< "Y"
This also supports multiple read's
your_script.sh <<< "YNYYNNY"

Reading STDOUT from Shell Script (EASY)

I'm sure this is SUPER easy. I am trying to read a string from STDOUT. I submit a job onto another machine and then I WANT to be able to send "bjobs" which checks if the job has been finished. I want to be able to read STDOUT and detect when it has finished then move on.
This is what I have and it isn't working but I feel super close!
Waiting for stdout to read "No unfinished job found"
bjobs
IFS= read -r line
echo "$line"
while "$line" != "No unfinished job found"
do
echo "$line"
sleep 30s
bjobs
IFS= read -r line
done
any help would be appreciated! This is one of my first shell scripts
The thing that you are missing is that read will read from its stdin ... not its stdout. So you have to arrange that its stdin is corresponds to the stdout of the command that you want it to read. The straight-forward way to do that is to use a pipe (|)
For example:
$ bjobs | ( IFS= ; read -r line ; echo "$line" ; while "$line" != "No unfinished job found" ; do ; echo "$line" ; sleep 30s ; read -r line ; done )
The ( ... ) is creating a subshell ...

Bash command substitution stdout+stderr redirect

Good day. I have a series of commands that I wanted to execute via a function so that I could get the exit code and perform console output accordingly. With that being said, I have two issues here:
1) I can't seem to direct stderr to /dev/null.
2) The first echo line is not displayed until the $1 is executed. It's not really noticeable until I run commands that take a while to process, such as searching the hard drive for a file. Additionally, it's obvious that this is the case, because the output looks like:
sh-3.2# ./runScript.sh
sh-3.2# com.apple.auditd: Already loaded
sh-3.2# Attempting... Enable Security Auditing ...Success
In other words, the stderr was displayed before "Attempting... $2"
Here is the function I am trying to use:
#!/bin/bash
function saveChange {
echo -ne "Attempting... $2"
exec $1
if [ "$?" -ne 0 ]; then
echo -ne " ...Failure\n\r"
else
echo -ne " ...Success\n\r"
fi
}
saveChange "$(launchctl load -w /System/Library/LaunchDaemons/com.apple.auditd.plist)" "Enable Security Auditing"
Any help or advice is appreciated.
this is how you redirect stderr to /dev/null
command 2> /dev/null
e.g.
ls -l 2> /dev/null
Your second part (i.e. ordering of echo) -- It may be because of this you have while invoking the script. $(launchctl load -w /System/Library/LaunchDaemons/com.apple.auditd.plist)
The first echo line is displayed later because it is being execute second. $(...) will execute the code. Try the following:
#!/bin/bash
function saveChange {
echo -ne "Attempting... $2"
err=$($1 2>&1)
if [ -z "$err" ]; then
echo -ne " ...Success\n\r"
else
echo -ne " ...Failured\n\r"
exit 1
fi
}
saveChange "launchctl load -w /System/Library/LaunchDaemons/com.apple.auditd.plist" "Enable Security Auditing"
EDIT: Noticed that launchctl does not actually set $? on failure so capturing the STDERR to detect the error instead.

conditional redirection in bash

I have a bash script that I want to be quiet when run without attached tty (like from cron).
I now was looking for a way to conditionally redirect output to /dev/null in a single line.
This is an example of what I had in mind, but I will have many more commands that do output in the script
#!/bin/bash
# conditional-redirect.sh
if tty -s; then
REDIRECT=
else
REDIRECT=">& /dev/null"
fi
echo "is this visible?" $REDIRECT
Unfortunately, this does not work:
$ ./conditional-redirect.sh
is this visible?
$ echo "" | ./conditional-redirect.sh
is this visible? >& /dev/null
what I don't want to do is duplicate all commands in a with-redirection or with-no-redirection variant:
if tty -s; then
echo "is this visible?"
else
echo "is this visible?" >& /dev/null
fi
EDIT:
It would be great if the solution would provide me a way to output something in "quiet" mode, e.g. when something is really wrong, I might want to get a notice from cron.
For bash, you can use the line:
exec &>/dev/null
This will direct all stdout and stderr to /dev/null from that point on. It uses the non-argument version of exec.
Normally, something like exec xyzzy would replace the program in the current process with a new program but you can use this non-argument version to simply modify redirections while keeping the current program.
So, in your specific case, you could use something like:
tty -s
if [[ $? -eq 1 ]] ; then
exec &>/dev/null
fi
If you want the majority of output to be discarded but still want to output some stuff, you can create a new file handle to do that. Something like:
tty -s
if [[ $? -eq 1 ]] ; then
exec 3>&1 &>/dev/null
else
exec 3>&1
fi
echo Normal # won't see this.
echo Failure >&3 # will see this.
I found another solution, but I feel it is clumsy, compared to paxdiablo's answer:
if tty -s; then
REDIRECT=/dev/tty
else
REDIRECT=/dev/null
fi
echo "Normal output" &> $REDIRECT
You can use a function:
function the_code {
echo "is this visible?"
# as many code lines as you want
}
if tty -s; then # or other condition
the_code
else
the_code >& /dev/null
fi
This works well for me. If DUMP_FILE is empty things go to stdout otherwise to the file. It does the job without using explicit redirection, but just uses pipes and existing applications.
function stdout_or_file
{
local DUMP_FILE=${1:-}
if [ -z "${DUMP_FILE}" ]; then
cat
else
sed -n "w ${DUMP_FILE}"
fi
}
function foo()
{
local MSG=$1
echo "info: ${MSG}"
}
foo "bar" | stdout_or_file ${DUMP_FILE}
Of course, you can squeeze this also in one line
foo "bar" | if [ -z "${DUMP_FILE}" ]; then cat; else sed -n "w ${DUMP_FILE}"; fi
Besides sed -n "w ${DUMP_FILE}" another command that does the same is dd status=none of=${DUMP_FILE}
The simplest solution is to use eval (a shell builtin), as it will act on the redirection in the expanded variable... and also act on anything else in the command line, so add extra quoting as required (note the extra single quotes added around the echo string below due to the '?' which would otherwise cause shell filename expansion to be attempted).
#!/bin/bash
# conditional-redirect.sh
if tty -s; then
REDIRECT=
else
REDIRECT=">& /dev/null"
fi
eval echo '"is this visible?"' $REDIRECT

How to implement a timer keypress in bash?

Here's what will happen, a message is displayed with a specified time waiting for keypress, if no keypress then it will resume.
Example
"Press ESC to exit, otherwise you will die.. 3..2..1"
"Press 'x' to procrastinate and check email, read some blogs, facebook, twitter.. otherwise you will resume work for 12 hours.. 3..2..1"
This should be a really handy function. How do I create this functionality in bash?
Look on the bash man page for the "read" command and notice the "-t timeout" option. Something like this should get you started
for i in 3 2 1 ; do
read -p $i... -n 1 -t 1 a && break
done
Use the -t and -n options of the read bash builtin command, also do not forget -r and -s. (see the manual for details)
#!/bin/bash
timeout=3
echo -n "Press ESC to exit, otherwise you will die..."
while [ $timeout -gt 0 ]; do
echo -n " $timeout"
if read -n1 -t1 -r -s x; then
echo
exit 0
fi
let timeout--
done
echo

Resources