I've been banging my head on this one for a bit. Wanted to see what I'm doing wrong, and it's probably all me on this one. I'm pinging an IP, then when it goes down or up, it will send a notification. My issue is with getops. I'm using it to attempt to parse the email address and the ip. Neither of these are being parsed. How can I go about parsing my two variables using getopts I need to allow my script to run properly. Thank you in advance :)
#!/bin/bash
# Variable(s)
# --------
EMAIL_DOWN_SENT="0";
EMAIL_UP_SENT="0";
PING_FAILED="0";
PING_UP_AGAIN="0";
# FUNctions
# ---------
#
# Echo a string value that is passed
echo_text() {
echo -e >&2 "$#";
}
echo_help() {
echo_text "Usage: $(basename "$0") [-e] [EMAIL][-h] [-i] [IP]\n\nScript to ping ip address to see if it's up and send an alert on status changes.\n
-e Enter email: user#domain.tld
-h This help screen
-i IP to ping";
exit;
}
# Main body
# ---------
#
# Get command line options
# ------------------------
while getopts ":e:hi:" OPTIONS
do
case $OPTIONS in
e) EMAIL=$OPTARGS ;;
h|\?) echo_help ;;
i) IP= $OPTARGS | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'; if [ "$?" != "0" ]; then echo_text "Please enter a valid IP"; exit 1; fi; ;;
*) echo_text "$OPTARGS is invalid, try again";
exit 1 ;;
esac
shift $((OPTIND-1))
done
echo_text "Your email is: $EMAIL";
echo_text "Your ip to ping is: $IP";
# Ping me
# -------
trap "echo You hit ctrl-c, now exiting..; exit" SIGINT
while :
do
ping -i 2 -c 2 $IP > /dev/null
if [ $? -ne 0 ]; then
let PING_DOWN+=1
echo_text "Down! Abandon all hope who enter here...";
PING_FAILED="1";
# If greater than 3 cycles send an email...
if [[ $PING_DOWN -ge 3 && $EMAIL_DOWN_SENT -eq 0 ]]; then
echo "$IP Down!"|mutt -s "Ping Failed!" $EMAIL;
EMAIL_DOWN_SENT="1";
fi
else
let PING_UP+=1
echo_text "Up! Ahoy! Host alive!";
# If greater than 3 cycles send an email...
if [[ $PING_UP -ge 3 && $EMAIL_UP_SENT -eq 0 && $PING_FAILED -eq 1 ]]; then
echo "$IP up!"|mutt -s "Ping succeeded!" $EMAIL;
EMAIL_UP_SENT="1";
PING_UP_AGAIN="1";
fi
# Reset checks
if [ $PING_UP_AGAIN -eq 1 ]; then
PING_FAILED="0";
PING_UP="0";
PING_DOWN="0";
EMAIL_UP_SENT="0";
EMAIL_DOWN_SENT="0";
fi
fi
done
if [ ! `echo "$OPTARG" | grep -qE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'` ]; then
echo_text "Please enter a valid IP"
exit 1
fi
IP="$OPTARG"
There are two main things wrong in your script:
First, it's $OPTARG, not $OPTARGS.
Second,
IP= $OPTARGS | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}';
You cannot do that. Change the whole paragraph to something along the lines of
if ! echo "$OPTARG" | grep -qE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'; then
echo_text "Please enter a valid IP"
exit 1
fi
IP="$OPTARG"
It's also worth noting that, unless you are using an ancient bash, your shell should have a =~ operator (see here) that you can replace the external grep with:
When it is used, the string to the right of the operator is considered an extended regular expression and matched accordingly (as in regex3)). The return value is 0 if the string matches the pattern, and 1 otherwise.
In addition to the problems Adrian Frühwirth pointed out, you need to move the shift command to after the loop. The way you've written it, it's removing the script's arguments while getopts is still parsing them, which will confuse things completely.
while getopts ":e:hi:" OPTIONS
do
...
done
shift $((OPTIND-1)) # This must be after the "done"
Try to enclose $OPTARGS with "" :
e) EMAIL="$OPTARGS" ;;
Related
Latest Version
#!/bin/bash
set -e
shopt -s nocasematch
#vars
redbgbold='\e[1;97;41m'
resetcolor='\e[0m'
RegExFQDN='(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)'
#functions
ask() {
local input
until
read -rp "$1 > " input >&2 || return 1
grep -q -P "$2" <<< "$input"
do
printf "ERROR - "${redbgbold}"\"$input\""${resetcolor}" is not a valid " >&2; sed "s/.*the //" <<< "$1" >&2
done
printf '%s\n' "$input"
}
#code
while [ -z $fqdn ]; do
fqdn=$(ask "Enter the FQDN" $RegExFQDN)
echo "FQDN is $fqdn"
done
The Question
I have a read line, and I want to take what the user entered and see if it matches my regex, if it matches we leave the loop, if it fails it prints an error and we do the loop again until we get a match. It looks redundant to me, and I assume there should be a better way but not sure what that should be.
Original Code
#!/bin/bash
set -e
shopt -s nocasematch
function RegexValidation() {
if [ "$2" = "fqdn" ]; then
if [ `echo $1 | grep -c -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'` == "0" ]; then
echo "ERROR - $1 is not a valid FQDN"
unset $!{1}
fi
fi
}
while [ -z $fqdn ]; do
read -e -r -p "Enter the Fully Qualified Domain Name > " fqdn
RegexValidation $fqdn fqdn
done
shopt -u nocasematch
any help is appreciated.
Update #1 - fixed formatting issues.
Update #2 - using that other guy's suggestions with a few additional tweaks
I would do basically the same thing, but split it differently to make it easier to reuse:
#!/bin/bash
set -e
ask() {
local input
until
read -rp "$1 > " input >&2 || return 1
grep -q -P "$2" <<< "$input"
do
echo "Invalid answer. Try again" >&2
done
printf '%s\n' "$input"
}
ask_fqdn() {
ask "$1" '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'
}
fqdn=$(ask_fqdn "Enter first FQDN")
echo "You wrote $fqdn"
fqdn=$(ask_fqdn "Enter second FQDN")
echo "This time it was $fqdn"
number=$(ask "And now a number because why not" '^\d+$')
echo "So $number"
Now you don't have to write a new loop every time you want new information, and you can easily ask for new things without modifying the existing functions.
Have the function return a status, which you can test with if in the loop.
And rather than use test to check the result of grep, just test it directly with if. grep returns a non-zero status if the input doesn't match.
function RegexValidation() {
if [ "$2" = "fqdn" ]; then
if ! echo "$1" | grep -q -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'; then
echo "ERROR - $1 is not a valid FQDN"
return 1
fi
return 0
fi
}
while :; do
read -e -r -p "Enter the Fully Qualified Domain Name > " fqdn
if RegexValidation "$fqdn" fqdn
then break
fi
done
Also, remember to quote your variables.
I am new to bash scripting have patience.
I have this code where it scans ports of domains and shows them to the user and it works perfectly by it self but when i added functions to it gives me errors. What am I doing wrong here?
check_parms () {
case $# in
1) ports='1-1023'
host=$1 ;;
2) ports=$1
host=$2 ;;
*) echo 'Usage: portscan [port|range] host'
exit 1 ;;
esac
}
# check port range
check_ports () {
if [ "$(echo $ports | grep '^[1-9][0-9]*-[1-9][0-9]*$')" != "" ]; then
firstport=$(echo $ports | cut -d- -f1)
lastport=$(echo $ports | cut -d- -f2)
elif [ "$(echo $ports | grep '^[1-9][0-9]*$')" != "" ]; then
firstport=$ports
lastport=$ports
else
echo "$ports is an invalid port(s) value"
exit 2
fi
}
# check firstport > lastport
check_order () {
if [ $firstport -gt $lastport ]; then
echo $firstport is larger than $lastport
exit 3
fi
}
# check host value
check_host () {
regex='^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$'
if [ "$(echo $host | grep '[A-Za-z]')" != "" ]; then
response=$(host $host)
if [[ "$response" =~ "timed out" || "$response" =~ "not found" ]]; then
echo host $host not found
exit 4
fi
elif [[ ! $host =~ $regex ]]; then
echo $host is an invalid host address
exit 5
fi
}
# check if host is reachable using ping
check_ping () {
if [[ "$(ping -c 1 -W 2 -n $host)" =~ "0 received" ]]; then
echo $host is unreachable
exit 6
fi
}
# start the scan
do_scan () {
echo -n "Scanning "
for p in $(seq $firstport $lastport)
do
echo -n .
x=$((echo >/dev/tcp/$host/$p) >/dev/null 2>&1 && echo "$p open")
if [ "$x" != "" ]; then
y="${y}
$x"
fi
done
}
# show results of scan
show_results () {
echo -e "\n$y\n"
exit 0
}
check_parms $#
check_ports $ports
check_order $firstport $lastport
check_host $host
check_ping $host
do_scan $host $firstport $lastport
show_results
The way you're passing and using function arguments doesn't make sense. The one I see that's blatantly wrong is the first:
check_parms () {
case $# in
1) ports='1-1023'
host=$1 ;;
2) ports=$1
host=$2 ;;
*) echo 'Usage: portscan [port|range] host'
exit 1 ;;
esac
}
#... then in the main program:
check_parms $#
To understand the problem, suppose the main script is run with two arguments: "5-50" and "10.10.10.10". The line check_parms $# runs check_params and passes the number of arguments (2) as an argument to it. Fine so far. Then in check_params, it runs case $# in, but at this point we're inside check_params, so $# refers to the number of arguments passed to check_params, not the number passed to the main script. check_params was passed only one parameter (the number 2), so $# is 1, and it executes the 1) case, which sets ports to "1-1023" and host to "2". This is not what you wanted at all.
If you want check_params to be able to see the same parameters that were passed to the main script, you have to pass it those same parameters with check_params "$#".
The arguments passed to the other functions simply don't appear to be used; this won't necessarily cause trouble, but it makes for confusing code (and confusing code breeds bugs). Consider, for example the check_order function. You pass it $firstport and $lastport as arguments (meaning that within check_order, $1 is set to the value of $firstport and $2 is set to the value of $lastport), but then you don't use those arguments inside check_order; instead you use $firstport and $lastport directly, ignoring the parameters. Since they're the same thing, it doesn't really matter, but if someone later tried to use it for something else (check_order $currentport 1023) it would check $firstport and $lastport rather than what it looks like it should be checking.
Solution: change the functions to use their arguments rather than accessing global variables directly. (Or use the global variables explicitly and stop pretending to pass them as arguments; but globals are generally bad, so this is the worse option.) Something like this:
check_order () {
if [ $1 -gt $2 ]; then
echo $1 is larger than $2
exit 3
fi
}
I was fiddling around with bash last month and am trying to create a script.
I want the script to search through the folders for files with some kind of extension defined by the argument -e. The folders are defined without -option. The output is 2 columns where in the first it prints the found files, and in the second the respective folders.
Is this the most efficient and/or flexible way to go?
I also can't manage to let the -l command work. Any idea what's wrong? When I enter -name \${CHAR}*, it simply doesn't work. Also, how can I make it recognize a range being used? With an if-function looking for the "-" character or something?
I think I managed to mount a block device, but how can I add the path as a parameter so it can be used as a folder? Setting a number as a var doesn't work, it tells me it doesn't recognize the command.
For some reason the 'no recursion' tag works, but the 'no numbers' doesn't. I have no idea why this would be different.
When using the 'no recursion' (nn) and 'no numbers' (nr) tags I use a long tag --tag for the arguments. Is it possible to use only 1 -tag? This is possible with get opts, but then I can't manage to use the other tags after the get opts has been used. Someone a solution?
Finally, is it possible, when finding 2 files with the same file name, instead of printing the file twice, can it just show the file once. But for every file with the same name keep a white space, so it can still show all the folders in the second column?
#!/bin/bash
#FUNCTIONS
#Error
#Also written to stderr
err() {
echo 1>&2;
echo "Error, not enough arguments" 1>&2;
echo "Usage: $0 [-e <file extension>] [<folder>]";
echo "Please enter the argument -e and at least 1 folder.";
echo "More: Please chek Help by using -h or --help.";
echo 1>&2;
exit
}
#Help
help() {
echo
echo "--- Help ---"
echo
echo "This script will look for file extentions in 1 or more directories. The output shows the found files with the according folder where it's located."
echo
echo "Argument -e <ext> is required."
echo "Other arguments the to-look-trough folders."
echo
echo "These are also usable options:"
echo "-h or --help shows this."
echo "-l <character> looks for files starting with the character."
echo "-l <character1>-<character2> does the same, but looks trough a range of characters."
echo "-b <block-device> mounts a partition to /mnt and let it search through."
echo "--nn (no numbers) makes sure there are no numbers in the file name."
echo "--nr (no recursion) doesn't look trough subdirectories."
echo "-r of –-err <file> writes the errors (f.e. corrupted directory) to <file>."
echo "-s <word> searches the word through the files and only shows the files having that word."
echo
exit
}
#VARS
#execute getopt
OPTS=$(getopt -o e:hl:b:r:s: -l "help,nn,nr,err" -n "FileExtensionScript" -- "$#");
#Bad arguments
if [ $? -ne 0 ];
then
err;
exit
fi
#Rearrange arguments
eval set -- "$OPTS";
#echo "AFTER SET -- \$OPTS: $#";
while true; do
case "$1" in
-e)
shift;
if [ -n "$1" ]; then
EXT=$1;
shift;
fi
;;
-h|--help)
shift;
help;
;;
-l)
shift;
if [ -n "$1" ]; then
CHAR=$1;
shift;
fi
;;
-b)
shift;
if [ -n "$1" ]; then
sudo mkdir /mnt/$1;
sudo echo -e "/dev/$1 /mnt/$1 vfat defaults 0 0 " >> /etc/fstab;
sudo mount -a;
999=/mnt/$1;
shift;
fi
;;
--nn)
shift;
NONUM=" ! -name '*[0-9]*'";
;;
--nr)
shift;
NOREC="-maxdepth 1";
;;
-f|--err)
shift;
if [ -n "$1" ]; then
ERROR="| 2>filename | tee -a $1";
shift;
fi
;;
-s)
shift;
if [ -n "$1" ]; then
SEARCH="-name '*$1*'";
shift;
fi
;;
--)
shift;
break;
;;
esac
done
#No folder or arguments given
if [ $# -lt 1 ];
then
err;
exit
fi
#Debug
echo "Folder argumenten: $#" >2;
echo \# $# >2;
#Create arrays with found files and according folders
FILES=( $(find $# $NOREC $SEARCH $NONUM -name \*.${EXT} $ERROR | rev | cut -d/ -f 1 | rev) )
FOLDERS=( $(find $# $NOREC $SEARCH $NONUM -name \*.${EXT} $ERROR | rev | cut -d/ -f 1 --complement | rev) )
#Show arrays in 2 columns
for ((i = 0; i <= ${#FILES[#]}; i++));
do
printf '%s %s\n' "${FILES[i]}" "${FOLDERS[i]}"
done | column -t | sort -k1 #Make columns cleaner + sort on filename
I am not native English speaker and am hoping to get some tips to finish my script :) Thanks in advance!
I am trying to write a bash script that takes in an option.
Lets call these options A and B.
In the script A and B may or may not be defined as variables.
I want to be able to check if the variable is defined or not.
I have tried the following but it doesn't work.
if [ ! -n $1 ]; then
echo "Error"
fi
Thanks
The "correct" way to test whether a variable is set is to use the + expansion option. You'll see this a lot in configure scripts:
if test -s "${foo+set}"
where ${foo+set} expands to "set" if it is set or "" if it's not. This allows for the variable to be set but empty, if you need it. ${foo:+set} additionally requires $foo to not be empty.
(That $(eval echo $a) thing has problems: it's slow, and it's vulnerable to code injection (!).)
Oh, and if you just want to throw an error if something required isn't set, you can just refer to the variable as ${foo:?} (leave off the : if set but empty is permissible), or for a custom error message ${foo:?Please specify a foo.}.
You did not define how these options should be passed in, but I think:
if [ -z "$1" ]; then
echo "Error"
exit 1
fi
is what you are looking for.
However, if some of these options are, err, optional, then you might want something like:
#!/bin/bash
USAGE="$0: [-a] [--alpha] [-b type] [--beta file] [-g|--gamma] args..."
ARGS=`POSIXLY_CORRECT=1 getopt -n "$0" -s bash -o ab:g -l alpha,beta:,gamma -- "$#"`
if [ $? -ne 0 ]
then
echo "$USAGE" >&2
exit 1
fi
eval set -- "$ARGS"
unset ARGS
while true
do
case "$1" in
-a) echo "Option a"; shift;;
--alpha) echo "Option alpha"; shift;;
-b) echo "Option b, arg '$2'"; shift 2;;
--beta) echo "Option beta, arg '$2'"; shift 2;;
-g|--gamma) echo "Option g or gamma"; shift;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo Remaining args
for arg in "$#"
do
echo '--> '"\`$arg'"
done
exit 0
Don't do it that way, try this:
if [[ -z $1 ]]; then
echo "Error"
fi
The error in your version is actually the lack of quoting.
Should be:
if [ ! -n "$1" ]; then
echo "Error"
fi
But you don't need the negation, use -z instead.
If you work on Bash, then use double brackets [[ ]] too.
from the man bash page:
-z string
True if the length of string is zero.
-n string
True if the length of string is non-zero.
Also, if you use bash v4 or greater (bash --version) there's -v
-v varname
True if the shell variable varname is set (has been assigned a value).
The trick is "$1", i.e.
root#root:~# cat auto.sh
Usage () {
echo "error"
}
if [ ! -n $1 ];then
Usage
exit 1
fi
root#root:~# bash auto.sh
root#root:~# cat auto2.sh
Usage () {
echo "error"
}
if [ ! -n "$1" ];then
Usage
exit 1
fi
root#root:~# bash auto2.sh
error
I'm thinking that this needs to be changed to a while clause, at the moment it'll wait till all 10000 pings are done, I need it to return when the ping is successful. The program "say" is on OSX it makes the computer speak.
#!/bin/bash
echo begin ping
if ping -c 100000 8.8.8.8 | grep timeout;
then echo `say timeout`;
else echo `say the internet is back up`;
fi
OK I don't have rights to answer my own question so here's my answer for it after playing around:
Thanks, yeah I didn't know about $? until now. Anyway now I've gone and made this. I like that yours doesn't go forever but in my situation I didn't need it to stop until it's finished.
#!/bin/bash
intertube=0
echo "begin ping"
while [ $intertube -ne 1 ]; do
ping -c 3 google.com
if [ $? -eq 0 ]; then
echo "ping success";
say success
intertube=1;
else
echo "fail ping"
fi
done
echo "fin script"
You probably shouldn't rely on textual output of a command to decide this, especially when the ping command gives you a perfectly good return value:
The ping utility returns an exit status of zero if at least one response was heard from the specified host; a status of two if the transmission was successful but no responses were received; or another value from <sysexits.h> if an error occurred.
In other words, use something like:
((count = 60)) # Maximum number to try.
while [[ $count -ne 0 ]] ; do
ping -c 1 8.8.8.8 # Try once.
rc=$?
if [[ $rc -eq 0 ]] ; then
((count = 1)) # If okay, flag loop exit.
else
sleep 1 # Minimise network storm.
fi
((count = count - 1)) # So we don't go forever.
done
if [[ $rc -eq 0 ]] ; then # Make final determination.
echo `say The internet is back up.`
else
echo `say Timeout.`
fi
You don't need to use echo or grep. You could do this:
ping -oc 100000 8.8.8.8 > /dev/null && say "up" || say "down"
This can also be done with a timeout:
# Ping until timeout or 1 successful packet
ping -w (timeout) -c 1
I use this Bash script to test the internet status every minute on OSX
#address=192.168.1.99 # forced bad address for testing/debugging
address=23.208.224.170 # www.cisco.com
internet=1 # default to internet is up
while true;
do
# %a Day of Week, textual
# %b Month, textual, abbreviated
# %d Day, numeric
# %r Timestamp AM/PM
echo -n $(date +"%a, %b %d, %r") "-- "
ping -c 1 ${address} > /tmp/ping.$
if [[ $? -ne 0 ]]; then
if [[ ${internet} -eq 1 ]]; then # edge trigger -- was up now down
echo -n $(say "Internet down") # OSX Text-to-Speech
echo -n "Internet DOWN"
else
echo -n "... still down"
fi
internet=0
else
if [[ ${internet} -eq 0 ]]; then # edge trigger -- was down now up
echo -n $(say "Internet back up") # OSX Text-To-Speech
fi
internet=1
fi
cat /tmp/ping.$ | head -2 | tail -1
sleep 60 ; # sleep 60 seconds =1 min
done
If you use the -o option, BSD ping (which is also on macOS) will exit after receiving one reply packet.
Further reading: https://www.freebsd.org/cgi/man.cgi?query=ping
EDIT: paxdiablo makes a very good point about using ping’s exit status to your advantage. I would do something like:
#!/usr/bin/env bash
echo 'Begin ping'
if ping -oc 100000 8.8.8.8 > /dev/null; then
echo $(say 'timeout')
else
echo $(say 'the Internet is back up')
fi
ping will send up to 100,000 packets and then exit with a failure status—unless it receives one reply packet, in which case it exits with a success status. The if will then execute the appropriate statement.
Here's my one-liner solution:
screen -S internet-check -d -m -- bash -c 'while ! ping -c 1 google.com; do echo -; done; echo Google responding to ping | mail -s internet-back my-email#example.com'
This runs an infinite ping in a new screen session until there is a response, at which point it sends an e-mail to my-email#example.com. Useful in the age of e-mail sent to phones.
(You might want to check that mail is configured correctly by just running echo test | mail -s test my-email#example.com first. Of course you can do whatever you want from done; onwards, sound a bell, start a web browser, use your imagination.)
I liked paxdiablo's script, but wanted a version that ran indefinitely. This version runs ping until a connection is established and then prints a message saying so.
echo "Testing..."
PING_CMD="ping -t 3 -c 1 google.com > /dev/null 2>&1"
eval $PING_CMD
if [[ $? -eq 0 ]]; then
echo "Already connected."
else
echo -n "Waiting for connection..."
while true; do
eval $PING_CMD
if [[ $? -eq 0 ]]; then
echo
echo Connected.
break
else
sleep 0.5
echo -n .
fi
done
fi
I also have a Gist of this script which I'll update with fixes and improvements as needed.