Why use parens instead of square brackets in ''if !(ssh ...) then''? - shell

I'm trying to understand the use of brackets in this line:
if !(ssh -q $user#$server "[ -d /some/directory ]")then
Usually the condition comes between [], what exactly is the use of () here?

There is a small "debatable" (See comments here) typo in your line. A space in front of your first bracket. In a shell script, the space is not needed. However, depending on some options from your shell, it might be needed to add a space when typing this line directly on the command-line. A rewrite with space would be :
if ! (ssh -q $user#$server "[ -d /some/directory ]"); then ... ; fi
There are several parts in this line
[ -d /some/directory ] : this is a call to the so called test command which can also be written as [ ... ]. test -d /some/directory and [ -d /some/directory ] are identical commands. It will check if the directory /some/directory exists or is accessible. (see man test for more information)
ssh -q $user#$server cmd: this will execute the command cmd as $user on $server. However it will do this quietly (-q) by not throwing any error output. So essentially, with cmd replaced by the above test, we check if that directory exists or is accessible for $user on $server.
man ssh :: -q : Quiet mode. Causes most warning and diagnostic messages to be suppressed
The full command is now enclosed between brackets. The brackets is a grouping which allows to execute several commands in unity. I.e. ( cmd1; cmd2 ) will be executed as one unit. Nonetheless, these brackets will do this in a separate instance. It will fork a sub-shell to do the execution. In this case only a single command is being executed in the sub-shell. It's unnecessary overhead. As you see, even though the syntax looks cleaner, it has an effect.
man bash :: (list) list is executed in a subshell environment. Variable assignments and builtin commands that affect the shell's environment do not remain in effect after the command completes. The return status is the exit status of list.
! cmd : this negates the exit status of cmd. If the command successfully executed (exit status=0) it will assume to be false. Or vice versa.
The if statement is written as if list; then thenlist; fi. The if statement is executed if the exit-status of list is zero. In this case list is ! (ssh -q $user#$server "[ -d /some/directory ]"), while a more common example of list is [ -e foo ] which is the test command.
Similar ways of writing the if-condition are now :
if ! ssh -q $user#$server "[ -d /some/directory ]"; then ...; fi
or you can just move the ! in the test, leading to :
if ssh -q $user#$server "[ ! -d /some/directory ]"; then ...; fi
Finally, if you want to keep the grouping for looks without the sub-shell, you can use { ... }, i.e.
if ! { ssh -q $user#$server "[ -d /some/directory ]"; }; then ...; fi
man bash :: { list; }
list is simply executed in the current shell environment. list must be terminated with a newline or semicolon.
This is known as a group command. The return status is the exit
status of list.

You need a space between ! and (...). One correct way to write this using bash shell:
if ! (ssh -q $user#$server "[ -d \"/some/directory\" ]"); then
echo "do something";
else
echo "do something else";
fi
This checks whether a directory /some/directory exists on a remote server using ssh.
man test to know about options, such as -d.
-d FILE
FILE exists and is a directory
! represents a logical (boolean) NOT or negation to the output of ().
-q option in ssh is for quiet mode.
For information on the single parentheses () in bash see https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html
Note, single parentheses are used to group command lines in a subshell. As OP suggested, () is not strictly needed/recommended here and the same if condition can be written without it,
if ! ssh -q $user#$server "[ -d \"/some/directory\" ]"; then
.....

Related

Bash - break conditional clause if one of the statements return error code != 0

I have the following bash script which runs on my CI and intends to run my code on a physical MacOS and on several docker Linux images:
if [[ "$OS_NAME" == "mac_os" ]]; then
make all;
run_test1;
run_test2;
make install;
else
docker exec -i my_docker_image bash -c "make all";
docker exec -i my_docker_image bash -c "run_test1";
docker exec -i my_docker_image bash -c "run_test2";
docker exec -i my_docker_image bash -c "make install";
fi
If the tests fail (run_test1 or run_test2) they return error code 1. If they pass they return error code 0.
The whole script runs with set -e so whenever it sees exit code other than 0 it stops and fails the entire build.
The problem is that currently, when run_test1 and run_test2 are inside the conditional clause - when they fail and return error code 1 the conditional clause doesn't break and the build succeeds although tests didn't pass.
So I have 2 questions:
How to break the conditional clause if one of the commands return error code other than 0?
How to break the conditional clause in such a way that the entire clause will return an error code (so the whole build will fail)?
Your code should work as expected, let's demonstrate this:
#!/usr/bin/env bash
set -e
var="test"
if [[ $var = test ]]; then
echo "Hello"
cat non_existing_file &> /dev/null
echo "World"
else
echo "Else hello"
cat non_existing file &> /dev/null
fi
echo I am done
This will output only "Hello", as expected. If it works differently for you, it means you didn't provide enough code. Let's try to change the code above and show some examples when set -e is ignored, just like in your case:
Quoting Bash Reference manual:
If a compound command or shell function executes in a context where -e
is being ignored, none of the commands executed within the compound
command or function body will be affected by the -e setting, even if
-e is set and a command returns a failure status.
Now let's quote the same manual and see some cases where -e is ignored:
The shell does not exit if the command that fails is part of the
command list immediately following a while or until keyword, part of
the test in an if statement, part of any command executed in a && or
|| list except the command following the final && or ||, any command
in a pipeline but the last, or if the command’s return status is being
inverted with !.
From this we can see that, for example, if you had the code above in a function and tested that function with if, set -e would be ignored:
#!/usr/bin/env bash
set -e
f() {
var="test"
if [[ $var = test ]]; then
echo "Hello"
cat non_existing_file &> /dev/null
echo "World"
else
echo "Else hello"
cat non_existing file &> /dev/null
fi
}
if f; then echo "Function OK!"; fi
echo I am done
Function f is executed in a context where set -e is being ignored (if statement), meaning that set -e doesn't affect any of the commands executed within this function. This code outputs:
Hello
World
Function OK!
I am done
The same rules apply when you execute the function in a && or || list. If you change the line if f; then echo "Function OK!"; fi to f && echo "Function OK", you will get the same output. I believe the latter could be your case.
Even so, your second question can be solved easily by adding || exit:
run_test1 || exit 1;
run_test2 || exit 1;
Your first question is trivial if you are inside a function for example. Then you can simply return. Or inside a loop, then you can break. If you are not, breaking out of the conditional clause is not that easy. Take a look at this answer.
set -e can be a surprising thing as it is being ignored in many cases. Use it with care and be aware of these cases.

Bash Echo passing to another script, not working as expected

I created a bash file to write some content into a file, which should be written into another users home directory, with the users account.
It should work the follwing:
sudo ./USER.sh run 49b087ef9cb6753f "echo test > test.txt"
Basically USER.sh contains this:
if [ "$1" = "run" ]; then
cd /home/${2}/;
sudo -u ${2} ${3};
fi
But it does not write any stuff into test.txt, it just direct executes the Bash command, instead of writing it into the file.
Did anyone got an Idea how I can fix it, that it does actually write the Content into a file instead of direct executing it?
Thanks.
You want:
sudo -u "$2" sh -c "$3"
The curlies are useless. They don't prevent splitting and file-globbing.
The double quotes do.
With the double quotes "$3" expands to "echo test > test.txt" (without them, it's "echo" "test" ">" and "test.txt"). This needs to be executed by a shell, hence the sh -c (a POSIX shell is sufficient in this case and if it's dash, it'll start a few ms faster than bash does).
You could also do:
if [ "$1" = "run" ]; then
sudo -u "$2" --set-home sh -c "$(printf '%s\n' 'cd "$HOME"' "$3")"
fi
which would be more robust in the general case where user home directories aren't necessarily /home/$username, but whatever the appropriate field in /etc/passwd is.

How do I check if a program is running in bash?

I have made a chat-script in bash and I want to check whether or not netcat is running.
I've tried pgrep but and it's working but it prints out an error in the terminal but it still keeps going like normal.
This is a part of that script:
function session()
{
echo -n "Port (default is 3333): "
read port
if [ -n "${port}" ]
then
clear
echo "Only 2 users can talk to each other simultaneously."
echo "To send a message, simply write and hit enter. Press Ctrl+C to quit."
nc -l -p ${port}
if [ pgrep "nc -l -p ${port}" ]
then
echo "${l_name} logged in to chat session"
else
clear
new
fi
else
echo "Invalid port!"
new
fi
}
Don't put prep inside [ ]. That doesn't run the prep command, it just treats the word pgrep as an argument to the test command.
You also need to use the -f option to make pgrep match the entire command line, not just the program name.
It should be
if pgrep -f "nc -l -p ${port}"
then
...
else
...
fi
Try to run script with a "-x" parameter. This shows each line that runs. Here is a description from man page:
-x : After expanding each simple command, for command, case
command, select command, or arithmetic for command, display the
expanded value of PS4, followed by the command and its expanded
arguments or associated word list.
Here is an example script:
#!/bin/bash
for i in $( ls ); do
echo item: $i
done
If you run with -x you can see each command running with a head of + sign:
$ bash -x list.sh
++ ls
+ for i in '$( ls )'
+ echo item: list.sh
item: list.sh

Trouble escaping quotes in a variable held string during a Sub-shell execution call [duplicate]

This question already has answers here:
Why does shell ignore quoting characters in arguments passed to it through variables? [duplicate]
(3 answers)
Closed 6 years ago.
I'm trying to write a database call from within a bash script and I'm having problems with a sub-shell stripping my quotes away.
This is the bones of what I am doing.
#---------------------------------------------
#! /bin/bash
export COMMAND='psql ${DB_NAME} -F , -t --no-align -c "${SQL}" -o ${EXPORT_FILE} 2>&1'
PSQL_RETURN=`${COMMAND}`
#---------------------------------------------
If I use an 'echo' to print out the ${COMMAND} variable the output looks fine:
echo ${COMMAND}
screen output:-
#---------------
psql drupal7 -F , -t --no-align -c "SELECT DISTINCT hostname FROM accesslog;" -o /DRUPAL/INTERFACES/EXPORTS/ip_list.dat 2>&1
#---------------
Also if I cut and paste this screen output it executes just fine.
However, when I try to execute the command as a variable within a sub-shell call, it gives an error message.
The error is from the psql client to the effect that the quotes have been removed from around the ${SQL} string.
The error suggests psql is trying to interpret the terms in the sql string as parameters.
So it seems the string and quotes are composed correctly but the quotes around the ${SQL} variable/string are being interpreted by the sub-shell during the execution call from the main script.
I've tried to escape them using various methods: \", \\", \\\", "", \"" '"', \'"\', ... ...
As you can see from my 'try it all' approach I am no expert and it's driving me mad.
Any help would be greatly appreciated.
Charlie101
Instead of storing command in a string var better to use BASH array here:
cmd=(psql ${DB_NAME} -F , -t --no-align -c "${SQL}" -o "${EXPORT_FILE}")
PSQL_RETURN=$( "${cmd[#]}" 2>&1 )
Rather than evaluating the contents of a string, why not use a function?
call_psql() {
# optional, if variables are already defined in global scope
DB_NAME="$1"
SQL="$2"
EXPORT_FILE="$3"
psql "$DB_NAME" -F , -t --no-align -c "$SQL" -o "$EXPORT_FILE" 2>&1
}
then you can just call your function like:
PSQL_RETURN=$(call_psql "$DB_NAME" "$SQL" "$EXPORT_FILE")
It's entirely up to you how elaborate you make the function. You might like to check for the correct number of arguments (using something like (( $# == 3 ))) before calling the psql command.
Alternatively, perhaps you'd prefer just to make it as short as possible:
call_psql() { psql "$1" -F , -t --no-align -c "$2" -o "$3" 2>&1; }
In order to capture the command that is being executed for debugging purposes, you can use set -x in your script. This will the contents of the function including the expanded variables when the function (or any other command) is called. You can switch this behaviour off using set +x, or if you want it on for the whole duration of the script you can change the shebang to #!/bin/bash -x. This saves you explicitly echoing throughout your script to find out what commands are being run; you can just turn on set -x for a section.
A very simple example script using the shebang method:
#!/bin/bash -x
ec() {
echo "$1"
}
var=$(ec 2)
Running this script, either directly after making it executable or calling it with bash -x, gives:
++ ec 2
++ echo 2
+ var=2
Removing the -x from the shebang or the invocation results in the script running silently.

check isatty in bash

I want my shell to detect if human behavior, then show the prompt.
So, assume the file name is test.bash
#!/bin/bash
if [ "x" != "${PS1:-x}" ] ;then
read -p "remove test.log Yes/No" x
[ "$x" = "n" ] && exit 1
fi
rm -f test.log
But, I found it can not work if I haven't set PS1. Is there better method?
my test methods:
./test.bash # human interactive
./test.bash > /tmp/test.log # stdout in batch mode
ls | ./test.bash # stdin in batch mode
to elaborate, I would try
if [ -t 0 ] ; then
# this shell has a std-input, so we're not in batch mode
.....
else
# we're in batch mode
....
fi
I hope this helps.
From help test:
-t FD True if FD is opened on a terminal.
You could make use of the /usr/bin/tty program:
if tty -s
then
# ...
fi
I admit that I'm not sure how portable it is, but it's at least part of GNU coreutils.
Note that in bash scripts (see the test expr entry in man bash), it is not necessary to use the beefy && and || shell operators to combine two separate runs of the [ command, because the [ command has its own built-in and -a and or -o operators that let you compose several simpler tests into a single outcome.
So, here is how you can implement the test that you asked for — where you flip into batch mode if either the input or the output has been redirected away from the TTY — using a single invocation of [:
if [ -t 0 -a -t 1 ]
then
echo Interactive mode
else
echo Batch mode
fi

Resources