output from ssh stored in a variable to test its value - linux

remote_file=$(ssh -t -o LogLevel=QUIET $ip_address 'if [[ -f ~/logs/output.log ]]; then echo exists; else echo nonexistant; fi')
echo $remote_file
if [[ "x$remote_file" == "xexists" ]]
then
# do some stuff here
else
# do some other stuff here
fi
The variable $remote_file seems to be storing correct value as it prints correct value in the echo statement. But when I test for its value, there seems to be something wrong as the test always fails and goes to else block. I tried to debug using set -x and noticed that the test condition left side is missing. What could be the issue here?
== \x\e\x\i\s\t\s ]]

As mentioned by Charles Duffy, the output might contain whitespace characters that are not obvious, but that you need to add to the comparison or remove somehow from the variable $remote_file before doing the comparison.
Note that the example given doesn't really need a string comparison.
It might not be obvious, but if you start ssh with a command, then when the command is finished, ssh will exit with the same exit code as the command itself. So you can simplify your code to:
if ssh -t -o LogLevel=QUIET $ip_address 'test -f ~/logs/output.log'
then
# do some stuff here
else
# do some other stuff here
fi

Related

Am I setting this script up correctly to run specific commands based on user input?

I have a small script that I am working on. This is only the second script that I have made using bash script.
Basically what I am wanting this script to do is take the users input and fire a command based on that choice.
As you can see the user first enters the host address of the instance they are going to ssh into and ultimately tail logs on. There are a couple things that I am not understanding.
If / Then / Else / Elif - The concept seems simple enough but perhaps how these should be used eludes me.
When I run my script through a bash parser, the parser comes back with the following message:
Line 2:
if [ "$mainmenuinput" = "1" ]; then
^-- SC2154: mainmenuinput is referenced but not assigned.
mainmenu() {
if [ "$mainmenuinput" = "1" ]; then
ssh "$customerurl" tail -f /data/jirastudio/jira/j2ee_*/log/main/current
elif [ "$mainmenuinput" = "2" ]; then
ssh "$customerurl" tail -f /data/jirastudio/confluence/j2ee_*/log/main/current
elif [ "$mainmenuinput" = "3" ]; then
ssh "$customerurl" tail -f /data/jirastudio/horde/service/log/main/current
elif [ "$mainmenuinput" = "4" ]; then
ssh "$customerurl" tail -f /data/jirastudio/apache/logs/access_log
fi
}
printf "\nEnter the customers host URL:\n"
read -r customerurl
printf "Press 1 for JIRA\n"
printf "Press 2 for Confluence\n"
printf "Press 3 for Horde\n"
printf "Press 4 for Apache Access\n"
printf "Press 5 for Apache Error\n"
read -p -r "Make your choice:" "$mainmenuinput"
Looking up the SC2154 entry I found that it means this:
ShellCheck has noticed that you reference a variable that is not assigned. Double check that the variable is indeed assigned, and that the name is not misspelled.
I am a little confused on what that means. If someone can explain that, I would greatly appreciate it.
As it stands, when I run the script, it pauses to wait for the user to enter the host address. The user hits ENTER and the script then presents them with the menu to have them choose which log they want to tail. The menu looks a little odd:
Press 1 for JIRA
Press 2 for Confluence
Press 3 for Horde
Press 4 for Apache Access
Press 5 for Apache Error
-r
Im not sure why the -r is showing up at the end of the menu. When a selection is made, the script ends and outputs this:
./tail_logs.sh: line 23: read:Make your choice:': not a valid identifier`
Any help with this would be appreciated or if anything a push in the right direction. I love figuring this stuff out but sometimes, its helpful to get shoved at least in the general direction of the error/resolution.
Thanks
EDIT 1
Ok, I updated my script with your suggestions. It seemed to still balk at a few things. For example:
(mainmenu "$customerurl" "$mainmenuinput")
Using ShellCheck I got back this:
Line 1:
(mainmenu "$customerurl" "$mainmenuinput") {
^-- SC2154: customerurl is referenced but not assigned.
^-- SC2154: mainmenuinput is referenced but not assigned.
^-- SC1070: Parsing stopped here. Mismatched keywords or invalid parentheses?
If I write this out like:
mainmenu() { then it does not complain. Also, if I run the script with it typed out as per the suggested way, I get an error about `syntax error near unexpected token '{'
The current code looks like this:
#!/bin/sh
mainmenu() {
echo "$1"
echo "$2"
if [ "$2" = "1" ]; then
ssh "$1" tail -f "/data/jirastudio/jira/j2ee_*/log/main/current"
elif [ "$2" = "2" ]; then
ssh "$1" tail -f "/data/jirastudio/confluence/j2ee_*/log/main/current"
elif [ "$2" = "3" ]; then
ssh "$1" tail -f "/data/jirastudio/horde/service/log/main/current"
elif [ "$2" = "4" ]; then
ssh "$1" tail -f "/data/jirastudio/apache/logs/access_log"
elif [ "$2" > 4 || < 1 ]; then
echo "Uh uh uh, you didnt say the magic word! The number you picked isnt in the list. Pick again."
fi
}
echo
echo "Enter the customers host address:"
read -r customerurl
echo "Press 1 for JIRA"
echo "Press 2 for Confluence"
echo "Press 3 for Horde"
echo "Press 4 for Apache Access"
echo "Press 5 for Apache Error"
read -r -p "Pick a number: " mainmenuinput
I get no errors when running this. But when I make a selection, the script ends and does not output the tail command at all. Also, Im not sure if I am validating user input outside of 1-4 correctly with the last elif statement although if I change this to else I get an error when I run the script.
I think my issue is in the first part of the function?
mainmenu() {
echo "$1"
echo "$2"
Without having $hostAddress and mainMenuInput does the script not know what should be assigned to $1 and $2 or does it automatically assign the first thing typed in to these variables?
The main problems are with the read command at the end. First, whatever immediately follows the -p option is used as a prompt string; in this case, the next argument is "-r", so it prints that as a prompt. You clearly want "Make your choice:" to be the prompt, so that must go immediately after -p (i.e. use either read -r -p "Make your choice:" ... or read -p "Make your choice:" -r ...). Second, when you use $mainmenuinput, it replaces that with the current value of mainmenuinput. In the shell, you use $variable to get the value of a variable, not to set it. With both of these problems corrected, the last command becomes:
read -p "Make your choice:" -r mainmenuinput
There's also another important thing: after reading the users' input, you need to actually call the mainmenu function. So just add mainmenu as the last line.
As for the if ... then ... elif ... structure, yours looks fine; I'm not sure what the question is. Although personally I'd add an else clause that printed an error that the option was not valid.
I do have some stylistic/best practice recommendations, though:
It's best to pass information to functions in the form of arguments, rather than global variables. That is, rather than using customerurl and mainmenuinput directly in the function, pass them as arguments (mainmenu "$customerurl" "$mainmenuinput"), then reference those arguments ("$1" and "$2") inside the functions. This doesn't matter much in a small script like this, but having clear distinctions between the variables used by different parts of a program makes things much easier to keep straight in larger programs.
In shell scripts, printf is the best way to do complex things like printing lines without a linefeed at the end, or translating escape characters... but if you're just doing a standard print-a-line-with-a-linefeed-at-the-end, echo is simpler. Thus, I'd replace the various printf "something\n" commands with echo "something", and printf "\nEnter the customers host URL:\n" with:
echo
echo "Enter the customers host URL:"
In the command
ssh "$customerurl" tail -f /data/jirastudio/jira/j2ee_*/log/main/current
(or ssh "$1" ... if you follow my recommendation about arguments instead of global variables), the wildcard (*) will be expanded on the local computer before being handed to ssh and passed to the remote computer to be executed. It'd be best to quote that argument to prevent that:
ssh "$customerurl" tail -f "/data/jirastudio/jira/j2ee_*/log/main/current"
Note that the quotes will be removed before it's passed to ssh and then to the remote computer, so they will not prevent the wildcard from being expanded on the remote computer.
The thing you're calling a URL isn't actually a URL; URLs are things like "https://stackoverflow.com/questions". They start with a protocol (or "scheme") like "http" or "ftp", then "://", then a server name, then "/", etc. ssh just takes a raw server name (optionally with a username, in the form user#server).
Update, based on EDIT 1: I wasn't clear on how to call the function; your definition (using mainmenu() { ...) is correct, but having defined the function you then need to actually run the function. Do to this, change the end of the script to something like this:
...
echo "Press 5 for Apache Error"
read -r -p "Pick a number: " mainmenuinput
mainmenu "$customerurl" "$mainmenuinput"
This will run the function, with the first argument ($1) set to "$customerurl", and second argument ($2) set to "$mainmenuinput".
There's also a problem with the elif clause you added in the function. The shell's syntax for test expressions is really really weird (mostly for historical reasons). Also, there are three common variants, the original [ ... ] (which is actually a command) which has the weirdest syntax, bash's [[ ... ]] variant (much cleaner syntax, but not available available in generic POSIX shells), and (( ... )) (cleaner syntax, math- rather than string-oriented, not portable). See BashFAQ #31 for details.
For what you're trying to do, any of these would work:
elif [ "$2" -gt 4 -o "$2" -lt 1 ]; then
# [ ... ] doesn't use || or &&, and uses -lt etc for numeric comparisons.
# < and > do string comparisons, which are ... different. And you'd
# need to quote them to keep them from being mistaken for redirects.
# Also, you need to specify the "$2" explicitly for each comparison.
elif [[ "$2" -gt 4 || "$2" -lt 1 ]]; then
# [[ ... ]] uses || and &&, but still uses -lt etc for numeric comparisons.
# < and > still do string comparisons, but don't need to be quoted
elif (( $2 > 4 || $2 < 1 )); then
# All numeric here, so < and > work
But there's still a problem, since the user might have entered something that isn't a number at all (just pressed return, typed "wibble", etc.), and in all of these cases numeric comparison will fail. Solution: skip the test, and use else instead of elif:
...
elif [ "$2" = "4" ]; then
ssh "$1" tail -f "/data/jirastudio/apache/logs/access_log"
else
echo "Uh uh uh, you didnt say the magic word! The number you picked isnt in the list. Pick again."
fi
}
... that way, if any of the previous conditions aren't met for any reason at all, it'll print the error message.

Does not work to execute command in double brackets in bash

In an attempt to stay consistent i have tried to use double brackets [[ ]] in all my if statements. I did however get into a problem when i was going to check the return value for a command i wanted to run. After testing several ways of creating an if statement i found that only without brackets could i execute a command.
The following does not work:
if [[ $command ]] ; then
echo "something"
fi
if [[ $(command) ]] ; then
echo "something"
fi
if [[ ${command} ]] ; then
echo "something"
fi
and the code above makes the if loop true even when the command was not run.
since the code above doesnt work with braces it doesnt work to use this either:
[[ $command ]] || echo "failed"
and it doesnt work in a subshell either.
The following works:
if $command ; then
echo "something"
fi
if $(command) ; then
echo "something"
fi
Why doesnt it work to place a command in an if loop with brackets, and why does the if loops above report true when it didnt even run the command ? I'm using bash version 4.1.9. Ive tried this many times and the if loops are just as simple as the ones i typed above, it just checks if a command was run successfully and exits if it wasnt.
The short answer is:
[ and [[ expect an expression.
if expects a command.
Saying:
[[ $(command) ]]
would essentially execute:
[[ -n <command_output> ]]
which may or may not be what you want. On the other hand, saying:
$command && echo something || echo other
would echo something or other based on the return code of the command (0 and non-zero respectively).
Double braces are a shortcut for test. In your examples, what's happening is that you're testing the shell variable $command for existence.
if [[ $PWD ]]; then
echo PWD is set to a value
fi
if [[ $NOT_A_REAL_VAR ]]; then
echo Nope, its not set
fi
In your second example, you're using command substitution to check that command output something on standard output.
if [[ $(echo hi) ]]; then
echo "echo said hi'
fi
if [[ $(true) ]]; then #true is a program that just quits with successful exit status
echo "This shouldn't execute"
fi
Your third example is the same as your first, pretty much. You use the curly braces if you want to group your variables. for example if you want to put an 's' after something.
WORD=Bike
echo "$WORDS" #won't work because "WORDS" isn't a variable
echo "${WORD}S" # will output "BikeS"
Then in your fifth example, you are running the program that is sitting inside command.
So, if you want to test some strings, use [[ ]] or [ ]. If you just want to test the exit status of a program, then don't use those, just use a bare if.
Check man test for details on the braces.
If you're just checking the return value of the command, drop the double brackets.
if $command
then
echo "Command succeeded"
else
echo "Command failed: $!"
fi
The double brackets are a test command. (Well, not really, but their a takeoff of the single square brackets that were an alias to the test command.) In early Bourne shell, you would see things like:
if test -z "$string"
then
echo "This is an empty string"
fi
The square brackets were syntactic sugar:
if [ -z "$string" ]
then
echo "This is an empty string"
fi
So, if you're not doing an actual test, you can eliminate the double or single square brackets.
If you're using square brackets, you should use the double ones and not the single ones because the double ones are a bit more forgiving and can do a bit more:
if [ -z $string ] # No quotes: This will actually fail if string is zero bytes!
if [[ -z $string ]] # This will work despite the lack of quotes

Is the directory NOT writable

Can anyone tell me why this is always saying that the directory is not writable, when it absolutely is?
$dnam="/home/bryan/renametest/C D"
# Is the directory writable
err=0
if [ ! -w $dnam ]
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
You need double-quotes around $dnam -- without them, it's interpreted as two separate shell words, "/home/bryan/renametest/C" and "D", which makes an invalid test expression and hence fails. This should work:
if [ ! -w "$dnam" ]
#tink's suggestion of [[ ]] is a cleaner way of doing tests like this, but is only available in bash (and some other shells with extended syntax). The fact that you get [[: not found means you're using a fairly basic shell, not bash.
I see multiple problems:
You are using a space inside your variable. This is not illegal, but in combination line you use the variable unescaped and generate the following command:
if [ ! -w /home/bryan/renametest/C D ]
This is not a valid syntax. The simplest way to fix this is changing the line to
if [ ! -w "$dnam" ]
The next problem is worse: On my system, help test returns the text:
-w FILE True if the file is writable by you.
Which means, the command doesn't support directories but only files. If you want to check if a directory is writable, you will have to use a different command
As everyone else said, the $dnam variable needs double quotes. Here's why:
The [ ... ] is an alias to the test command. If you look in your system, you will see a file called /bin/[ or maybe /bin/usr/[. On some systems, this is a hard link to /bin/test or /bin/usr/test. The if statement executes what comes after the if, and if that command returns a zero exit status, the if statement will execute the then clause. Otherwise, if there is an else clause, that will execute instead.
To allow for boolean testing, Unix included the test command, so you could do this:
if test -d "$directory"
then
echo "Directory $directory exists!"
fi
Later on, the /bin/[ was added as syntactic sugar. This is identical to the above:
if [ -d "$directory" ]
then
echo "Directory $directory exists!"
fi
Now, both [ and test are builtin commands, but they are *still commands. This means that the shell interpolates the command and then executes it.
Try executing the following:
$ set -xv # Turns on shell debugging
$ dnam="/home/bryan/renametest/C D"
dnam="/home/bryan/renametest/C D"
+ dnam='/home/bryan/renametest/C D'
$ test -d $dnam
test -d $dnam
+ test -d /home/bryan/renametest/C D
$ echo $?
echo $?
+ echo 1
1
$ test -d "$dnam" # Now with quotes
test -d $dnam
+ test -d "/home/bryan/renametest/C D"
$ echo $?
echo $?
+ echo 0
0
$ set +xv # Turn off the debuggin
Each command is echoed twice. The first time as written, and the second time after the line is interpolated. As part of the interpolation, the shell splits parameters on white space. As you can see, the test command is testing the presence of /home/bryan/renamtest/C which doesn't exist and thus not writable. I'm actually surprised that the test command didn't print an error message because you passed it an extra parameter.
In the second attempt, you added quotes. These quotes prevented the shell from splitting your parameters on the space and keep the directory name as a single parameter.
Since [ ... ] is a command, you have to take into account the shell's interpolation of variables and other issues. And, if you're not absolutely careful, you can end up with errors.
Even worse, sometimes the [ ... ] might work and sometimes it might not. If your directory name didn't contain spaces, it will work as expected. Imagine you're writing a program, and you test it and everything works because all directories you've tried don't have spaces. Then, someone uses your program, but has a space in the directory. A substantial number of shell script bugs are do to this type of issue in if statements.
This is why Bash introduced the [[ ... ]] tests. The [[ isn't a command but a statement. This means that the shell doesn't directly interpolate the results. Instead, the parameters are parsed, and then any interpolation is done. Thus, this would have worked:
dnam="/home/bryan/renametest/C D" # No "$" in front of the variable!
# Is the directory writable
if [[ ! -w $dnam ]] # No quotation marks needed!
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
It's almost always better to use the [[ ... ]] test rather than the [ ... ] test, so go ahead and get into the habit.
One more minor error, you had:
$dnam="/home/bryan/renametest/C D"
This gets interpolated by the shell, so the variable being set is whatever the value of $dnam just happens to be. If $dnam happened to equal "foo", you would been doing this:
foo="/home/bryan/renametest/C D"
Not what you want.
You want to leave the $ off when you set variables:
dnam="/home/bryan/renametest/C D"

I keep getting a 'while syntax' error on the output of the at job in unix and I have no idea why

#!/usr/dt/bin/dtksh
while getopts w:m: option
do
case $option in
w) wflag=1
wval="$OPTARG";;
m) mflag=1
mval="$OPTARG";;
?) printf 'BAD\n' $0
exit 2;;
esac
done
if [ ! -z "$wflag" ]; then
printf "W and -w arg is $wval\n"
fi
if [ ! -z "$mflag" ]; then
printf "M and -m arg is $mval\n"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: $* \n"
at $wval <<ENDMARKER
echo $* >> Search_List
tr " " "\n" <Search_List >Usr_List
while true; do
if [ -s Usr_List ]; then
for i in $(cat Usr_List); do
if finger -m | grep $i; then
echo '$i is online' | elm user
sed '/$i/d' <Usr_List >tmplist
mv tmplist Usr_List
fi
done
else
break
fi
done
ENDMARKER
Essentially I want to keep searching through until it is empty. Each time an element of the list is found, it is deleted. Once the list is empty quit.
There are no error messages when I first run the command, it only shows up in an email containing the output of the at job.
Thanks in advance for any advice
EDIT: The script uses getopts and takes one argument for -w and one for -m, the w value is set as the time for the at job, the m still has to be used. Any arguments after the one for m are sent to a file called Search_List, Search_List is edited and saved as Usr_List. Then in the while loop, while Usr_List is not empty, the script checks the results of finger -m against the names in Usr_List. If a name is found, it is removed from Usr_List. Once Usr_List is empty, the program should stop.
elm is a way to send an email, so elm user sends an email to user.
The error is :
while: Expression syntax
at uses /bin/sh by default.
at now <<ENDMARKER
<code here>
ENDMARKER
All of this executes under /bin/sh, which on some systems can be Bourne Shell (Solaris for example).
You need to figure out what /bin/sh is for your system, then modify things accordingly. Plus, read the gurantees about what is and what is not in your "at" environment. I think the problem lies there. You have both UNIX and linux tags. So I cannot give a lot more help than that.
You can enable logging -- the way YOU need it -- of the at code chunk:
exec 2&>1 > /tmp/somefile.log
Then write debugging messages to stdout or stderr.
Your HEREDOC is being interpolated. Try quoting the delimiter:
at $wval << 'ENDMARKER'
Although ( I haven't looked closely) it appears that you want some interpolation. But you definitely do not want it on the line in which you reference $i, so quote that $ if you do not quote the entire heredoc:
if finger -m | grep \$i; then
You need to pass the -k option to at:
...
at -k $wval <<ENDMARKER
...
at is otherwise defaulting to your login shell which is csh or one of its derivatives.
It turns out that the while command and the if command needed to be combined.
while [[ -s Usr_List ]]; do
......
done

Bash ignoring error for a particular command

I am using following options
set -o pipefail
set -e
In bash script to stop execution on error. I have ~100 lines of script executing and I don't want to check return code of every line in the script.
But for one particular command, I want to ignore the error. How can I do that?
The solution:
particular_script || true
Example:
$ cat /tmp/1.sh
particular_script()
{
false
}
set -e
echo one
particular_script || true
echo two
particular_script
echo three
$ bash /tmp/1.sh
one
two
three will be never printed.
Also, I want to add that when pipefail is on,
it is enough for shell to think that the entire pipe has non-zero exit code
when one of commands in the pipe has non-zero exit code (with pipefail off it must the last one).
$ set -o pipefail
$ false | true ; echo $?
1
$ set +o pipefail
$ false | true ; echo $?
0
Just add || true after the command where you want to ignore the error.
Don't stop and also save exit status
Just in case if you want your script not to stop if a particular command fails and you also want to save error code of failed command:
set -e
EXIT_CODE=0
command || EXIT_CODE=$?
echo $EXIT_CODE
More concisely:
! particular_script
From the POSIX specification regarding set -e (emphasis mine):
When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value >0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.
Instead of "returning true", you can also use the "noop" or null utility (as referred in the POSIX specs) : and just "do nothing". You'll save a few letters. :)
#!/usr/bin/env bash
set -e
man nonexistentghing || :
echo "It's ok.."
Thanks for the simple solution here from above:
<particular_script/command> || true
The following construction could be used for additional actions/troubleshooting of script steps and additional flow control options:
if <particular_script/command>
then
echo "<particular_script/command> is fine!"
else
echo "<particular_script/command> failed!"
#exit 1
fi
We can brake the further actions and exit 1 if required.
I found another way to solve this:
set +e
find "./csharp/Platform.$REPOSITORY_NAME/obj" -type f -iname "*.cs" -delete
find "./csharp/Platform.$REPOSITORY_NAME.Tests/obj" -type f -iname "*.cs" -delete
set -e
You can turn off failing on errors by set +e this will now ignore all errors after that line. Once you are done, and you want the script to fail again on any error, you can use set -e.
After applying set +e the find does not fail the whole script anymore, when files are not found. At the same time, error messages
from find are still printed, but the whole script continues to execute. So it is easy to debug if that causes the problem.
This is useful for CI & CD (for example in GitHub Actions).
If you want to prevent your script failing and collect the return code:
command () {
return 1 # or 0 for success
}
set -e
command && returncode=$? || returncode=$?
echo $returncode
returncode is collected no matter whether command succeeds or fails.
output=$(*command* 2>&1) && exit_status=$? || exit_status=$?
echo $output
echo $exit_status
Example of using this to create a log file
log_event(){
timestamp=$(date '+%D %T') #mm/dd/yy HH:MM:SS
echo -e "($timestamp) $event" >> "$log_file"
}
output=$(*command* 2>&1) && exit_status=$? || exit_status=$?
if [ "$exit_status" = 0 ]
then
event="$output"
log_event
else
event="ERROR $output"
log_event
fi
I have been using the snippet below when working with CLI tools and I want to know if some resource exist or not, but I don't care about the output.
if [ -z "$(cat no_exist 2>&1 >/dev/null)" ]; then
echo "none exist actually exist!"
fi
while || true is preferred one, but you can also do
var=$(echo $(exit 1)) # it shouldn't fail
I kind of like this solution :
: `particular_script`
The command/script between the back ticks is executed and its output is fed to the command ":" (which is the equivalent of "true")
$ false
$ echo $?
1
$ : `false`
$ echo $?
0
edit: Fixed ugly typo

Resources