Why does this bash command's return value change? - linux

What I actually want to do
Save a command's output and check its return status.
The solution?
After some googling I found basically the same answer here on StackOverflow as well as on AskUbuntu and Unix/Linux StackExchange:
if output=$(command); then
echo "success: $output"
fi
Problem
When trying out this solution with command info put the if clause is executed even if the actual command fails, but I can't explain myself why?
I tried to check the return value $? manually and it seems like the var= changes the return value:
$ info put
info: No menu item 'put' in node '(dir)Top'
$ echo $?
1
$ command info put
info: No menu item 'put' in node '(dir)Top'
$ echo $?
1
$ var=$(command info put)
info: No menu item 'put' in node '(dir)Top'
$ echo $?
0
$ var=$(command info put); echo $?
info: No menu item 'put' in node '(dir)Top'
0
It's also the same behavior when `
So why does that general solution not work in this case?
And how to change/adapt the solution to make it work properly?
My environment/system
I'm working on Windows 10 with WSL2 Ubuntu 20.04.2 LTS:
$ tmux -V
tmux 3.0a
$ echo $SHELL
/bin/bash
$ /bin/bash --version
GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)
$ info --version
info (GNU texinfo) 6.7

When trying out this solution with command info put the if clause is executed even if the actual command fails, but I can't explain myself why?
Indeed, info exits with 0, when output is not a terminal and there's an error.
// texinfo/info.c
if ((!isatty (fileno (stdout))) && (user_output_filename == NULL))
{
user_output_filename = xstrdup ("-");
...
}
...
// in called get_initial_file()
asprintf (error, _("No menu item '%s' in node '%s'"),
(*argv)[0], "(dir)Top");
...
if (user_output_filename)
{
if (error)
info_error ("%s", error);
...
exit (0); // exits with 0!
}
References: https://github.com/debian-tex/texinfo/blob/master/info/info.c#L848 , https://github.com/debian-tex/texinfo/blob/master/info/info.c#L277 , https://github.com/debian-tex/texinfo/blob/master/info/info.c#L1066 .
why does that general solution not work in this case?
Because the behavior of the command changes when its output is redirected not to a terminal.
how to change/adapt the solution to make it work properly?
You could simulate a tty - https://unix.stackexchange.com/questions/157458/make-program-in-a-pipe-think-it-has-tty , https://unix.stackexchange.com/questions/249723/how-to-trick-a-command-into-thinking-its-output-is-going-to-a-terminal .
You could grab stderr of the command and check if it's not-empty or match with some regex.
I think you could also contact texinfo developers and let them know that it's I think a bug and make a patch, so it would be like exit(error ? EXIT_FAILURE : EXIT_SUCCESS);.

Instead of checking the exit status of the command, I ended up with saving the output and simply checking if there is any output that could be used for further processing (in my case piping into less):
my_less () {
output=$("$#")
if [[ ! -z "$output" ]]; then
printf '%s' "$output" | less
fi
}
Even with the bug in info, my function now works as the bug only affects the command's exit status. It's error messages are written to stderr as expected, so I'm using that behavior.

Related

How to detect when a bash script is triggered from keybinding

Background
I have a Bash script that requires user input. It can be run by calling it in a terminal or by pressing a keyboard shortcut registered in the i3 (or Sway) config file as follows:
bindsym --release $mod+Shift+t exec /usr/local/bin/myscript
The Problem
I know I can use read -p to prompt in a terminal, but that obviously won't work when the script is triggered through the keybinding. In that case I can use something like Yad to create a GUI, but I'm unable to detect when it's not "in a terminal". Essentially I want to be able to do something like this:
if [ $isInTerminal ]; then
read -rp "Enter your username: " username
else
username=$(yad --entry --text "Enter your username:")
fi
How can I (automatically) check if my script was called from the keybinding, or is running in a terminal? Ideally this should be without user-specified command-line arguments. Others may use the script and as such, I'd like to avoid introducing any possibility of user error via "forgotten flags".
Attempted "Solution"
This question suggests checking if stdout is going to a terminal, so I created this test script:
#!/usr/bin/env bash
rm -f /tmp/detect.log
logpass() {
echo "$1 IS opened on a terminal" >> /tmp/detect.log
}
logfail() {
echo "$1 IS NOT opened on a terminal" >> /tmp/detect.log
}
if [ -t 0 ]; then logpass stdin; else logfail stdin; fi
if [ -t 1 ]; then logpass stdout; else logfail stdout; fi
if [ -t 2 ]; then logpass stderr; else logfail stderr; fi
However, this doesn't solve my problem. Regardless of whether I run ./detect.sh in a terminal or trigger it via keybind, output is always the same:
$ cat /tmp/detect.log
stdin IS opened on a terminal
stdout IS opened on a terminal
stderr IS opened on a terminal
It seems like the easiest way to solve your actual problem would be to change the i3 bind to
bindsym --release $mod+Shift+t exec /usr/local/bin/myscript fromI3
and do
if [[ -n "$1" ]]; then
echo "this was from a keybind"
else
echo "this wasn't from a keybind"
fi
in your script.
False Positive
As most results on Google would suggest, you could use tty which would normally return "not a tty" when it's not running in a terminal. However, this seems to differ with scripts called via bindsym exec in i3/Sway:
/dev/tty1 # From a keybind
/dev/pts/6 # In a terminal
/dev/tty2 # In a console
While tty | grep pts would go part way to answering the question, it is unable to distinguish between running in a console vs from a keybinding which you won't want if you're trying to show a GUI.
"Sort of" Solution
Triggering via keybinding seems to always have systemd as the parent process. With that in mind, something like this could work:
{
[ "$PPID" = "1" ] && echo "keybind" || echo "terminal"
} > /tmp/detect.log
It is likely a safe assumption that the systemd process will always have 1 as its PID, but there's no guarantee that every system using i3 will also be using systemd so this is probably best avoided.
Better Solution
A more robust way would be using ps. According to PROCESS STATE CODES in the man page:
For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group
The key here is + on the last line. To check that in a terminal you can call ps -Sp <PID> which will have a STAT value of Ss when running "interactively", or S+ if it's triggered via keybinding.
Since you only need the STATE column, you can further clean that up with -o stat= which will also remove the headers, then pipe through grep to give you the following:
is_interactive() {
ps -o stat= -p $$ | grep -q '+'
}
if is_interactive; then
read -rp "Enter your username: " username
else
username=$(yad --entry --text "Enter your username:")
fi
This will work not only in a terminal emulator and via an i3/Sway keybinding, but even in a raw console window, making it a much more reliable option than tty above.

Function in shell script not executed correctly

I write a script to start/stop/restart a custom server application.
When starting the daemon server it should make the following:
#!/bin/sh -e
### BEGIN INIT INFO
...
...
### END INIT INFO
# Start service
pga_server_start()
{
/opt/pga/server/server -d
}
# Stop service
pga_server_stop()
{
PID=`cat /var/lock/pga_server.lock`
/bin/kill --signal SIGTERM $PID
}
pga_load_excalibur()
{
is_loaded=`lsmod | grep excalbr`
echo "Done"
if [ -z "$is_loaded" ]; then
/usr/local/bin/excload
echo "Driver excalibur loaded."
else
echo "Driver excalibur already loaded."
fi
}
case "$1" in
start)
pga_load_excalibur
pga_server_start
;;
...
...
Initialy it worked fine. Then I've added the pga_load_excalibur function.
Afterward, it does not work anymore.
It never returns from the function pga_load_excalibur.
It seems that the call to is_loaded=lsmod | grep excalbrnever returns as the subsequentecho` is never printed.
However, if I copy/paste this function in a separate shell script...it works.
But if I launch the starter script manually this way:
/etc/init.d/server start
or
service server start
it does not work.
I'm using a Debian Wheezy 7.9 x64.
Although I'm not a schell script, it looks correct. I don't understand why it does not work when it's embedded into this service starter script.
Please note that I've also tried to replace the grep line with:
is_loaded=$(lsmod | grep excalbr)
But it does not work either.
I'm running out of ideas :(
Z.
What do you get if you run the script in debug mode? try to run it with:
#!/bin/sh -xv
That may give some idea of why it's failing, post the output if you can't figure it out

Linux LVM lvs command fails from cron perl script but works from cron directly

I am trying to run "lvs" in a perl script to parse its output.
my $output = `lvs --noheadings --separator : --units m --nosuffix 2>&1`;
my $result = $?;
if ($result != 0 || length($output) == 0) {
printf STDERR "Get list of LVs failed (exit result: %d): %s\n",
$result, $output;
exit(1);
}
printf "SUCCESS:\n%s\n", $output;
When I run the above script from a terminal window it runs fine. If I run via cron it fails:
Get list of LVs failed (exit result: -1):
Note the lack of any output (stdout + stderr)
If I run the same "lvs --noheadings --separator : --units m --nosuffix" command directly in cron, it runs and outputs just fine.
If I modify the perl script to use open3() I also get the same failure with no output.
If I add "-d -d -d -d -d -v -v -v" to the lvs command to enable verbose/debug output I see that when I run the perl script from terminal, but there is no output when run via cron/perl.
I'm running this on RHEL 7.2 with /usr/bin/perl (5.16.3)
Any suggestions???
According to perldoc system, "Return value of -1 indicates a failure to start the program or an error of the wait(2) system call (inspect $! for the reason)." So the reason there's no output is because lvs isn't being started successfully.
Given the usual nature of cron-related problems, I'd say the most likely reason it's failing to run would be that it's not on the $PATH used by cron. Try specifying the full path and, if that doesn't work, check $! for the operating system's error message.
Try using absolute path to lvs:
my $output = `/sbin/lvs --noheadings --separator : --units m --nosuffix 2>&1`;

Find and execute the existing batch command

I am trying to restart the gui with the following bash script (under Mint + Cinnamon):
if ! type gnome-shell > /dev/null; then
gnome-shell --replace
elif ! type cinnamon > /dev/null; then
cinnamon --replace
fi
I got an error message, that the gnome-shell does not exist. Is there a way to write this script multi-platform?
What you actually want is
type gnome-shell &> /dev/null
The &> redirects both stdout and stderr (bash only). You just redirected stdout, therefore you still get error messages.
You're only interested in the return value of type, not the output.
Also, what is the negation doing there? You call gnome-shell if it does NOT exist? In case you checked the return value $?, remember 0 is true, 1 is false in shells:
type gnome-shell
echo $? # prints '0', indicating success / true, or '1' if gnome-shell does not exist
The return value, or rather exit code / exit status, ($?) is what is evaluated by the if statement.
A little bit nicer:
function cmdExists()
{
type "$1" &> /dev/null
}
function echoErr()
{
echo "$1" 1>&2
}
if cmdExists gnome-shell; then
gnome-shell --replace
elif cmdExists cinnamon; then
cinnamon --replace
else
echoErr 'No shell found'
exit
fi
Some more useful thoughts on related topics:
Check if a program exists from a Bash script
Exit codes
EDIT: exit codes
Actually, every value except 0 is false in the shell. That is because programs use these values to indicate different errors.
There are also some exceptions. Inside (( )) you can use "normal" arithmetic... Shell arithmetic

Notify via email if something wrong got happened in the shell script

fileexist=0
mv /data/Finished-HADOOP_EXPORT_&Date#.done /data/clv/daily/archieve-wip/
fileexist=1
--some other script below
Above is the shell script I have in which in the for loop, I am moving some files. I want to notify myself via email if something wrong got happened in the moving process, as I am running this script on the Hadoop Cluster, so it might be possible that cluster went down while this was running etc etc. So how can I have better error handling mechanism in this shell script? Any thoughts?
Well, atleast you need to know "What are you expecting to go wrong". based on that you can do this
mv ..... 2> err.log
if [ $? -ne 0 ]
then
cat ./err.log | mailx -s "Error report" admin#abc.com
rm ./err.log
fi
Or as William Pursell suggested, use-
trap 'rm -f err.log' 0; mv ... 2> err.log || < err.log mailx ...
mv may return a non-zero return code upon error, and $? returns that error code. If the entire server goes down then unfortunately this script doesn't run either so that's better left to more advanced monitoring tools such as Foglight running on a different monitoring server. For more basic checks, you can use method above.

Resources