Conditional variables in bash script? - bash

I'm not used to writing code in bash but I'm self teaching myself. I'm trying to create a script that will query info from the process list. I've done that but I want to take it further and make it so:
The script runs with one set of commands if A OS is present.
The script runs with a different set of commands if B OS is present.
Here's what I have so far. It works on my Centos distro but won't work on my Ubuntu. Any help is greatly appreciated.
#!/bin/bash
pid=$(ps -eo pmem,pid | sort -nr -k 1 | cut -d " " -f 2 | head -1)
howmany=$(lsof -l -n -p $pid | wc -l)
nameofprocess=$(ps -eo pmem,fname | sort -nr -k 1 | cut -d " " -f 2 | head -1)
percent=$(ps -eo pmem,pid,fname | sort -k 1 -nr | head -1 | cut -d " " -f 1)
lsof -l -n -p $pid > ~/`date "+%Y-%m-%d-%H%M"`.process.log 2>&1
echo " "
echo "$nameofprocess has $howmany files open, and is using $percent"%" of memory."
echo "-----------------------------------"
echo "A log has been created in your home directory"
echo "-----------------------------------"
echo " "
echo ""$USER", do you want to terminate? (y/n)"
read yn
case $yn in
[yY] | [yY][Ee][Ss] )
kill -15 $pid
;;
[nN] | [n|N][O|o] )
echo "Not killing. Powering down."
echo "......."
sleep 2
;;
*) echo "Does not compute"
;;
esac

Here's my version of your script. It works with Ubuntu and Debian. It's probably safer than yours in some regards (I clearly had a bug in yours when a process takes more than 10% of memory, due to your awkward cut). Moreover, your ps are not "atomic", so things can change between different calls of ps.
#!/bin/bash
read percent pid nameofprocess < <(ps -eo pmem,pid,fname --sort=-pmem h)
mapfile -t openfiles < <(lsof -l -n -p $pid)
howmany=${#openfiles[#]}
printf '%s\n' "${openfiles[#]}" > ~/$(date "+%Y-%m-%d-%H%M.process.log")
cat <<EOF
$nameofprocess has $howmany files open, and is using $percent% of memory.
-----------------------------------
A log has been created in your home directory
-----------------------------------
EOF
read -p "$USER, do you want to terminate? (y/n) "
case $REPLY in
[yY] | [yY][Ee][Ss] )
kill -15 $pid
;;
[nN] | [n|N][O|o] )
echo "Not killing. Powering down."
echo "......."
sleep 2
;;
*) echo "Does not compute"
;;
esac
First, check that your version of ps has the --sort flag and the h option:
--sort=-pmem tells ps to sort wrt decreasing pmem
h tells ps to not show any header
All this is given to the read bash builtin, which reads space-separated fields, here the fields pmem, pid, fname and puts these values in the corresponding variables percent, pid and nameofprocess.
The mapfile command reads standard input (here the output of the lsof command) and puts each line in an array field. The size of this array is computed by the line howmany=${#openfiles[#]}. The output of lsof, as stored in the array openfiles is output to the corresponing file.
Then, instead of the many echos, we use a cat <<EOF, and then the read is use with the -p (prompt) option.
I don't know if this really answers your question, but at least, you have a well-written bash script, with less multiple useless command calls (until your case statement, you called 16 processes, I only called 4). Moreover, after the first ps call, things can change in your script (even though it's very unlikely to happen), not in mine.
You might also like the following which doesn't put the output of lsof in an array, but uses an extra wc command:
#!/bin/bash
read percent pid nameofprocess < <(ps -eo pmem,pid,fname --sort=-pmem h)
logfilename="~/$(date "+%Y-%m-%d-%H%M.process.log")
lsof -l -n -p $pid > "$logfilename"
howmany=$(wc -l < "$logfilename")
cat <<EOF
$nameofprocess has $howmany files open, and is using $percent% of memory.
-----------------------------------
A log has been created in your home directory ($logfilename)
-----------------------------------
EOF
read -p "$USER, do you want to terminate? (y/n) "
case $REPLY in
[yY] | [yY][Ee][Ss] )
kill -15 $pid
;;
[nN] | [n|N][O|o] )
echo "Not killing. Powering down."
echo "......."
sleep 2
;;
*) echo "Does not compute"
;;
esac

You could achieve this for example by (update)
#!/bin/bash
# place distribution independent code here
# dist=$(lsb_release -is)
if [[ -f /etc/redheat-release ]];
then # this is a RedHead based distribution like centos, fedora, ...
dist="redhead"
elif [[ -f /etc/issue.net ]];
then
# dist=$(cat /etc/issue.net | cut -d' ' -f1) # debian, ubuntu, ...
dist="ubuntu"
else
dist="unknown"
fi
if [[ $dist == "ubuntu" ]];
then
# use your ubuntu command set
elif [[ $dist == "redhead" ]];
then
# use your centos command set
else
# do some magic here
fi
# place distribution independent code here

Related

trap ERR doesn't work with pipes

I am try to make a system backup script with trap "" ERR. I realized the trap doesn't get called when commands are part of pipes |.
Heres are some parts of my code that don't work with trap "" ERR ...
OpenFiles=$(lsof "$Source" | wc -l)
PackagesList=$(dpkg --get-selections | awk '!/deinstall|purge|hold/ {print $1}' | tee "$FilePackagesList")
How can I get this to work without using if [ "$?" -eq 0 ]; then, or similar coding ? Because this is the reason I declared a trap this way.
Here is the script ...
root#Lian-Li:~# cat /usr/local/bin/create_incremental_backup_of_system.sh
#!/bin/bash
# Create an incremental GNU-standard backup of important system-files.
# This script works with Debian Jessie and newer systems.
# Created for my lian-li NAS 2016-11-27.
MailTo="admin#example.com" # Mail Address of an admin
Source="boot etc root usr/local usr/lib/cgi-bin var/www"
BackupDirectory=/media/hdd1/backups/lian-li
SubDir="system.d"
FileTimeStamp=$(date "+%Y%m%d%H%M%S")
FileName=$(uname -n)
File="${BackupDirectory}/${SubDir}/${FileName}-${FileTimeStamp}.tgz"
FileIncremental="${BackupDirectory}/${SubDir}/${FileName}.gtar"
FilePackagesList="${BackupDirectory}/${SubDir}/installed_packages_on_${FileName}.txt"
# have2do ...
# Backup rotate
MailContent="None"
TimeStamp=$(date "+%F %T") # This format "2011-12-31 23:59:59" is needed to read the journal
exec 1> >(logger -i -s -t "$0" -p 3) 2>&1 # all error messages are redirected to syslog journal and after that to stdout
trap "BriefExit" ERR # Provide information for an admin (via sendmail) when an error occurred and exit the script
function BriefExit(){
rm -f "$File"
if [ "$MailContent" = "None" ]
then
case "$LANG" in
de_DE.UTF-8)
echo "Beende Skript, aufgrund vorherige Fehler." 1>&2
;;
*)
echo "Stopping script because of previous error(s)." 1>&2
;;
esac
MailContent=$(journalctl -p 3 -o "short" --since="$TimeStamp" --no-pager)
ScriptName="${0##*/}"
SystemName=$(uname -n)
MailSubject="${SystemName}: ${ScriptName}"
echo -e "Subject: ${MailSubject}\n\n${MailContent}\n" | sendmail "$MailTo"
fi
exit 1
}
if [ ! -d "${BackupDirectory}/${SubDir}" ]
then
mkdir -p "${BackupDirectory}/${SubDir}"
fi
LoopCount=0
OpenFiles=1
cd /
while [ "$OpenFiles" -ne 0 ]
do
if [ "$LoopCount" -le 180 ]
then
sleep 1
OpenFiles=$(lsof $Source | wc -l)
LoopCount=$(($LoopCount + 1))
else
echo "Closing Script. Reason: Can't create incremental backup, because some files are open." 1>&2
BriefExit
fi
done
tar -cpzf "$File" -g "$FileIncremental" $Source
chmod 0700 "$File"
PackagesList=$(dpkg --get-selections | awk '!/deinstall|purge|hold/ {print $1}' | tee "$FilePackagesList")
while read -r PackageName
do
case "$PackageName" in
minidlna)
# Code ...
;;
slapd)
# Code ...
;;
esac
done <<< "$PackagesList"
exit 0
This isn't a problem with ERR traps at all, or with command substitutions, but with pipelines.
false | true
returns true, unless the pipefail option is set.
Thus in OpenFiles=$(lsof "$Source" | wc -l), only a failure in wc will cause the pipeline to be considered a failure, or in PackagesList=$(dpkg --get-selections | awk '!/deinstall|purge|hold/ {print $1}' | tee "$FilePackagesList"), only a failure in tee will cause the command as a whole to be considered failed.
Put the command set -o pipefail at the top of your script if you want a failure from any pipeline component (as opposed to the last component alone) to cause the command as a whole to be considered failed -- and note the other caveats for ERR traps given in BashFAQ #105.
Another alternative is to look at the status for each stage in the pipeline:
# cat test_bash_return.bash
true | true | false | true
echo "${PIPESTATUS[#]}"
# ./test_bash_return.bash
0 0 1 0

Variable scope in Bash [duplicate]

Please explain to me why the very last echo statement is blank? I expect that XCODE is incremented in the while loop to a value of 1:
#!/bin/bash
OUTPUT="name1 ip ip status" # normally output of another command with multi line output
if [ -z "$OUTPUT" ]
then
echo "Status WARN: No messages from SMcli"
exit $STATE_WARNING
else
echo "$OUTPUT"|while read NAME IP1 IP2 STATUS
do
if [ "$STATUS" != "Optimal" ]
then
echo "CRIT: $NAME - $STATUS"
echo $((++XCODE))
else
echo "OK: $NAME - $STATUS"
fi
done
fi
echo $XCODE
I've tried using the following statement instead of the ++XCODE method
XCODE=`expr $XCODE + 1`
and it too won't print outside of the while statement. I think I'm missing something about variable scope here, but the ol' man page isn't showing it to me.
Because you're piping into the while loop, a sub-shell is created to run the while loop.
Now this child process has its own copy of the environment and can't pass any
variables back to its parent (as in any unix process).
Therefore you'll need to restructure so that you're not piping into the loop.
Alternatively you could run in a function, for example, and echo the value you
want returned from the sub-process.
http://tldp.org/LDP/abs/html/subshells.html#SUBSHELL
The problem is that processes put together with a pipe are executed in subshells (and therefore have their own environment). Whatever happens within the while does not affect anything outside of the pipe.
Your specific example can be solved by rewriting the pipe to
while ... do ... done <<< "$OUTPUT"
or perhaps
while ... do ... done < <(echo "$OUTPUT")
This should work as well (because echo and while are in same subshell):
#!/bin/bash
cat /tmp/randomFile | (while read line
do
LINE="$LINE $line"
done && echo $LINE )
One more option:
#!/bin/bash
cat /some/file | while read line
do
var="abc"
echo $var | xsel -i -p # redirect stdin to the X primary selection
done
var=$(xsel -o -p) # redirect back to stdout
echo $var
EDIT:
Here, xsel is a requirement (install it).
Alternatively, you can use xclip:
xclip -i -selection clipboard
instead of
xsel -i -p
I got around this when I was making my own little du:
ls -l | sed '/total/d ; s/ */\t/g' | cut -f 5 |
( SUM=0; while read SIZE; do SUM=$(($SUM+$SIZE)); done; echo "$(($SUM/1024/1024/1024))GB" )
The point is that I make a subshell with ( ) containing my SUM variable and the while, but I pipe into the whole ( ) instead of into the while itself, which avoids the gotcha.
#!/bin/bash
OUTPUT="name1 ip ip status"
+export XCODE=0;
if [ -z "$OUTPUT" ]
----
echo "CRIT: $NAME - $STATUS"
- echo $((++XCODE))
+ export XCODE=$(( $XCODE + 1 ))
else
echo $XCODE
see if those changes help
Another option is to output the results into a file from the subshell and then read it in the parent shell. something like
#!/bin/bash
EXPORTFILE=/tmp/exportfile${RANDOM}
cat /tmp/randomFile | while read line
do
LINE="$LINE $line"
echo $LINE > $EXPORTFILE
done
LINE=$(cat $EXPORTFILE)

Loop shell script until successful log message

I am trying to get a shell script to recognize when an app instance has come up. That way it can continue issuing commands.
I've been thinking it would be something like this:
#/bin/bash
startApp.sh
while [ `tail -f server.log` -ne 'regex line indicating success' ]
do
sleep 5
done
echo "App up"
But, even if this worked, it wouldn't address some concerns:
What if the app doesn't come up, how long will it wait
What if there is an error when bringing the app up
How can I capture the log line and echo it
Am I close, or is there a better way? I imagine this is something that other admins have had to overcome.
EDIT:
I found this on super user
https://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found
tail -f logfile.log | while read LOGLINE
do
[[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
My only problem with this is that it might never exit. Is there a way to add in a maximum time?
Ok the first answer was close, but didn't account for everything I thought could happen.
I adapted the code from this link:
Ending tail -f started in a shell script
Here's what I came up with:
#!/bin/bash
instanceDir="/usr/username/server.name"
serverLogFile="$instanceDir/server/app/log/server.log"
function stopServer() {
touch ${serverLogFile}
# 3 minute timeout.
sleep 180 &
local timerPid=$!
tail -n0 -F --pid=${timerPid} ${serverLogFile} | while read line
do
if echo ${line} | grep -q "Shutdown complete"; then
echo 'Server Stopped'
# stop the timer..
kill ${timerPid} > /dev/null 2>&1
fi
done &
echo "Stoping Server."
$instanceDir/bin/stopserver.sh > /dev/null 2>&1
# wait for the timer to expire (or be killed)
wait %sleep
}
function startServer() {
touch ${serverLogFile}
# 3 minute timeout.
sleep 180 &
local timerPid=$!
tail -n0 -F --pid=${timerPid} ${serverLogFile} | while read line
do
if echo ${line} | grep -q "server start complete"; then
echo 'Server Started'
# stop the timer..
kill ${timerPid} > /dev/null 2>&1
fi
done &
echo "Starting Server."
$instanceDir/bin/startserver.sh > /dev/null 2>&1 &
# wait for the timer to expire (or be killed)
wait %sleep
}
stopServer
startServer
Well, tail -f won't ever exit, so that's not what you want.
numLines=10
timeToSleep=5
until tail -n $numLines server.log | grep -q "$serverStartedPattern"; do
sleep $timeToSleep
done
Be sure that $numLines is greater than the number of lines that might show up during $timeToSleep when the server has come up.
This will continue forever; if you want to only allow so much time, you could put a cap on the number of loop iterations with something like this:
let maxLoops=60 numLines=10 timeToSleep=5 success=0
for (( try=0; try < maxLoops; ++try )); do
if tail -n $numLines server.log | grep -q "$serverStartedPattern"; then
echo "Server started!"
success=1
break
fi
sleep $timeToSleep
done
if (( success )); then
echo "Server started!"
else
echo "Server never started!"
fi
exit $(( 1-success ))

How to make "if not true condition"?

I would like to have the echo command executed when cat /etc/passwd | grep "sysa" is not true.
What am I doing wrong?
if ! [ $(cat /etc/passwd | grep "sysa") ]; then
echo "ERROR - The user sysa could not be looked up"
exit 2
fi
try
if ! grep -q sysa /etc/passwd ; then
grep returns true if it finds the search target, and false if it doesn't.
So NOT false (! false) == true.
if evaluation in shells are designed to be very flexible, and many times doesn't require chains of commands (as you have written).
Also, looking at your code as is, your use of the $( ... ) form of cmd-substitution is to be commended, but think about what is coming out of the process. Try echo $(cat /etc/passwd | grep "sysa") to see what I mean. You can take that further by using the -c (count) option to grep and then do if ! [ $(grep -c "sysa" /etc/passwd) -eq 0 ] ; then which works but is rather old school.
BUT, you could use the newest shell features (arithmetic evaluation) like
if ! (( $(grep -c "sysa" /etc/passwd) == 0 )) ; then ...`
which also gives you the benefit of using the c-lang based comparison operators, ==,<,>,>=,<=,% and maybe a few others.
In this case, per a comment by Orwellophile, the arithmetic evaluation can be pared down even further, like
if ! (( $(grep -c "sysa" /etc/passwd) )) ; then ....
OR
if (( ! $(grep -c "sysa" /etc/passwd) )) ; then ....
Finally, there is an award called the Useless Use of Cat (UUOC). :-) Some people will jump up and down and cry gothca! I'll just say that grep can take a file name on its cmd-line, so why invoke extra processes and pipe constructions when you don't have to? ;-)
I hope this helps.
I think it can be simplified into:
grep sysa /etc/passwd || {
echo "ERROR - The user sysa could not be looked up"
exit 2
}
or in a single command line
$ grep sysa /etc/passwd || { echo "ERROR - The user sysa could not be looked up"; exit 2; }
This one
if [[ ! $(cat /etc/passwd | grep "sysa") ]]; then
echo " something"
exit 2
fi
What am I doing wrong?
$(...) holds the value, not the exit status, that is why this approach is wrong. However, in this specific case, it does indeed work because sysa will be printed which makes the test statement come true. However, if ! [ $(true) ]; then echo false; fi would always print false because the true command does not write anything to stdout (even though the exit code is 0). That is why it needs to be rephrased to if ! grep ...; then.
An alternative would be cat /etc/passwd | grep "sysa" || echo error. Edit: As Alex pointed out, cat is useless here: grep "sysa" /etc/passwd || echo error.
Found the other answers rather confusing, hope this helps someone.
Here is an answer by way of example:
In order to make sure data loggers are online a cron script runs every 15 minutes that looks like this:
#!/bin/bash
#
if ! ping -c 1 SOLAR &>/dev/null
then
echo "SUBJECT: SOLAR is not responding to ping" | ssmtp abc#def.com
echo "SOLAR is not responding to ping" | ssmtp 4151112222#txt.att.com
else
echo "SOLAR is up"
fi
#
if ! ping -c 1 OUTSIDE &>/dev/null
then
echo "SUBJECT: OUTSIDE is not responding to ping" | ssmtp abc#def.com
echo "OUTSIDE is not responding to ping" | ssmtp 4151112222#txt.att.com
else
echo "OUTSIDE is up"
fi
#
...and so on for each data logger that you can see in the montage
at http://www.SDsolarBlog.com/montage
FYI, using &>/dev/null redirects all output from the command, including errors, to /dev/null
(The conditional only requires the exit status of the ping command)
Also FYI, note that since cron jobs run as root there is no need to use sudo ping in a cron script.
simply:
if ! examplecommand arg1 arg2 ...; then
#code block
fi
without any brackets.
On Unix systems that supports it (not macOS it seems):
if getent passwd "$username" >/dev/null; then
printf 'User %s exists\n' "$username"
else
printf 'User %s does not exist\n' "$username"
fi
This has the advantage that it will query any directory service that may be in use (YP/NIS or LDAP etc.) and the local password database file.
The issue with grep -q "$username" /etc/passwd is that it will give a false positive when there is no such user, but something else matches the pattern. This could happen if there is a partial or exact match somewhere else in the file.
For example, in my passwd file, there is a line saying
build:*:21:21:base and xenocara build:/var/empty:/bin/ksh
This would provoke a valid match on things like cara and enoc etc., even though there are no such users on my system.
For a grep solution to be correct, you will need to properly parse the /etc/passwd file:
if cut -d ':' -f 1 /etc/passwd | grep -qxF "$username"; then
# found
else
# not found
fi
... or any other similar test against the first of the :-delimited fields.
I'd expect to see in the answers the direct use of grep with -q option (as we don't care the result but need only the return code.)
if ! grep -qs "sysa" /etc/passwd; then
echo "ERROR - The user sysa could not be looked up" >&2
exit 2
fi
or (to use chained execution on fail)
grep -qs "sysa" /etc/passwd || {
echo "ERROR - The user sysa could not be looked up" >&2
exit 2
}
Better, because the opposite is wanted, there's option -v for that
if grep -qsv "sysa" /etc/passwd; then
echo "ERROR - The user sysa could not be looked up" >&2
exit 2
fi
or (to use chained execution on success)
grep -qsv "sysa" /etc/passwd && {
echo "ERROR - The user sysa could not be looked up" >&2
exit 2
}
Notes
I like redirecting error messages to stderr, but echo output to stdout, hence >&2
Taylor the search pattern, e.g something like '^sysa:' if it's full login.

Bash variable scope

Please explain to me why the very last echo statement is blank? I expect that XCODE is incremented in the while loop to a value of 1:
#!/bin/bash
OUTPUT="name1 ip ip status" # normally output of another command with multi line output
if [ -z "$OUTPUT" ]
then
echo "Status WARN: No messages from SMcli"
exit $STATE_WARNING
else
echo "$OUTPUT"|while read NAME IP1 IP2 STATUS
do
if [ "$STATUS" != "Optimal" ]
then
echo "CRIT: $NAME - $STATUS"
echo $((++XCODE))
else
echo "OK: $NAME - $STATUS"
fi
done
fi
echo $XCODE
I've tried using the following statement instead of the ++XCODE method
XCODE=`expr $XCODE + 1`
and it too won't print outside of the while statement. I think I'm missing something about variable scope here, but the ol' man page isn't showing it to me.
Because you're piping into the while loop, a sub-shell is created to run the while loop.
Now this child process has its own copy of the environment and can't pass any
variables back to its parent (as in any unix process).
Therefore you'll need to restructure so that you're not piping into the loop.
Alternatively you could run in a function, for example, and echo the value you
want returned from the sub-process.
http://tldp.org/LDP/abs/html/subshells.html#SUBSHELL
The problem is that processes put together with a pipe are executed in subshells (and therefore have their own environment). Whatever happens within the while does not affect anything outside of the pipe.
Your specific example can be solved by rewriting the pipe to
while ... do ... done <<< "$OUTPUT"
or perhaps
while ... do ... done < <(echo "$OUTPUT")
This should work as well (because echo and while are in same subshell):
#!/bin/bash
cat /tmp/randomFile | (while read line
do
LINE="$LINE $line"
done && echo $LINE )
One more option:
#!/bin/bash
cat /some/file | while read line
do
var="abc"
echo $var | xsel -i -p # redirect stdin to the X primary selection
done
var=$(xsel -o -p) # redirect back to stdout
echo $var
EDIT:
Here, xsel is a requirement (install it).
Alternatively, you can use xclip:
xclip -i -selection clipboard
instead of
xsel -i -p
I got around this when I was making my own little du:
ls -l | sed '/total/d ; s/ */\t/g' | cut -f 5 |
( SUM=0; while read SIZE; do SUM=$(($SUM+$SIZE)); done; echo "$(($SUM/1024/1024/1024))GB" )
The point is that I make a subshell with ( ) containing my SUM variable and the while, but I pipe into the whole ( ) instead of into the while itself, which avoids the gotcha.
#!/bin/bash
OUTPUT="name1 ip ip status"
+export XCODE=0;
if [ -z "$OUTPUT" ]
----
echo "CRIT: $NAME - $STATUS"
- echo $((++XCODE))
+ export XCODE=$(( $XCODE + 1 ))
else
echo $XCODE
see if those changes help
Another option is to output the results into a file from the subshell and then read it in the parent shell. something like
#!/bin/bash
EXPORTFILE=/tmp/exportfile${RANDOM}
cat /tmp/randomFile | while read line
do
LINE="$LINE $line"
echo $LINE > $EXPORTFILE
done
LINE=$(cat $EXPORTFILE)

Resources