Find and execute the existing batch command - linux

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

Related

Why does this bash command's return value change?

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.

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.

Linux exit command does nothing

I'm new to Linux and am trying to create a simple program that checks if a user exists, if exists - exits the terminal, if not - creates it. I think I've done everything except exiting the terminal.
This is my code so far:
#!/bin/bash
user_name=newUser
if [ $(getent passwd $user_name) ]
then
echo "User $user_name already exists!"
exit
else
echo "The user $user_name doesn't exist and will be added"
sudo useradd -d /home/mint/newUser $user_name
fi
edit: As I said i'm new to Linux. Can someone edit my code and post it, I need to add it to a script, maybe i can close it with while function?
The exit command simply exits the current script. If you want to exit the terminal, you need to exit (or otherwise terminate) the program which is running in that terminal.
A common way to accomplish this is to make the process which wants to exit run as a function in your interactive shell.
add_user () { /path/to/your/script "$#" || exit; }
In this case, though, I would simply advise you to keep your script as is, and leave it to the user to decide whether or not they want to close their terminal.
By the way, the construct
if [ $(command) ]
will be true if the output from command is a non-empty string. The correct way to check the exit code from command is simply
if command
possibly with output redirection if you don't want the invoking user to see any output from command.
The function above also requires your scripit to exit with an explicit error; probably change it to exit 1 to explicitly communicate an error condition back to the caller.
#!/bin/bash
# First parameter is name of user to add
user_name=$1
# Quote variable; examine exit code
if getent passwd "$user_name" >/dev/null 2>&1
then
# Notice addition of script name and redirect to stderr
echo "$0: User $user_name already exists!" >&2
# Explicitly exit with nonzero exit code
exit 1
else
# script name & redirect as above
echo "$0: The user $user_name doesn't exist and will be added" >&2
# Quote argument
sudo useradd -d /home/mint/newUser "$user_name"
fi
This has been answered on askubuntu.com. I will summarise here:
In your script, as you may have noticed, exit will only exit the current script.
A user-friendly way to achieve what you're after is to use the exit command to set a suitable return code and call your script like ./<script> && exit.
If you really want the hard way, use
kill -9 $PPID
which will attempt to kill the enclosing process.

ksh su -c return value

inside of my script I need to run two scripts as another user, so I used the following line:
su otherUser -c "firstScript;secondScript;status=$?"
echo "returning $status"
return $status
The problem is that $status will always return 0.I did test with secondScript failing (wrong argument). Not sure if it's because I exited otherUser or if the $status is actually the result of the su command. Any suggestions?!
You need to capture status inside your outer shell, not in the inner shell invoked by su; otherwise, the captured value is thrown away as soon as that inner shell exits.
This is made much easier because su passes through the exit status of the command it runs -- if that command exits with a nonzero status, so will su.
su otherUser -c 'firstScript; secondScript'; status=$?
echo "returning $status"
return $status
Note that this only returns the exit status of secondScript (as would your original code have done, were it working correctly). You might think about what you want to do if firstScript fails.
Now, it's a little more interesting if you only want to return the exit code of firstScript; in that case, you need to capture the exit status in both shells:
su otherUser -c 'firstScript; status=$?; secondScript; exit $status'); status=$?
echo "returning $status"
return $status
If you want to run secondScript only if firstScript succeeds, and return a nonzero value if either of them fails, it becomes easy again:
su otherUser -c 'firstScript && secondScript'); status=$?
echo "returning $status"
return $status

ignoring command exit code in linux

I have a Postgres console command createdb appname_production_master, which return error exit code if the database with this name already exists.
Is it possible to make this command do not return any exit code?
Just ignore the exit code, for example like this.
createdb appname_production_master || true
Unix commands always return exit codes, but you need not respond to the exit code.
When you run a command $? is set to the exit code of the process. As this happens for every command, simply running a different command after the first will change $?.
For example:
createdb appname_production_master # returns 1, a failure code
# $? is 1
/bin/true # always returns 0, success
# $? is 0
Here's another example:
/bin/false # returns false, I assume usually 1
echo $? # outputs 1
echo $? # outputs 0, last echo command succeeded

Resources