I am running bash 4.4.19 on MacOS. I found the signal function doesn't work when if "set -e" is set. is it expected behavior? Here is my sample code:
#!/usr/local/bin/bash
set -e
declare -A Array1
Array1=([index1]="abc" [index2]="def" [index3]="dfkdjkfjdkdjfdk")
trap ReactSignal USR1
fun() {
PPid=$1
NUM=0
Array1[index4]="insidefunction"
while [ $NUM -le 5 ]
do
((NUM++))
echo "inside number is $NUM"
sleep 1
done
kill -USR1 $PPid
}
ReactSignal() {
IFS= read -r -d '' -u 3 checkOutput
echo "function output is ${checkOutput}"
}
Ppid="$$"
echo "start...."
coproc funfd { fun $Ppid; }
exec 3>&${funfd[0]}
echo "end...."
sleep 7
echo array value is ${Array1[#]}
It's because ((expression)) actually has a return code, as per the bash documentation:
((expression))
The expression is evaluated according to the rules described below under ARITHMETIC EVALUATION. If the value of the expression is non-zero, the return status is 0; otherwise the return status is 1.
So, of course, if NUM is zero going in to the expression, the return code will be one and set -e will pick this up as an error that needs termination.
There are any number of ways to solve this, from using the (rather ugly, in my opinion):
set +e ; ((NUM++)); set -e
to not using ((expression)) at all:
for i in {0..4} ; do
doSomethingWith $i
done
Related
I would like to run a bash script with a watchdog function launched in sub thread that will stop my program when a given variable reach a value. This variable is incremented in the main thread.
var=0
function watchdog()
{
if [[ $var -ge 3 ]]; then
echo "Error"
fi
}
{ watchdog;} &
# main program loop
((var++))
The problem in this code is that $var stays at 0. I also tried without {} around the watchdog call, same result.
Is my code style good ?
You cannot share variables between processes in bash, and it does not support multi-threading. So you need a form of Inter-Process Communication. One of the simplest is to use a named pipe, also known as a FIFO.
Here is and example:
pipe='/tmp/mypipe'
mkfifo "$pipe"
var=0
# Your definition is not strictly correct (although it will work)
watchdog()
{
# Note the loop
while read var
do
if (( var >= 3 )) # a better way to do numeric comparisons
then
echo "Error $var"
else
echo "$var"
fi
sleep 2 # to prevent CPU hogging
done
}
watchdog < "$pipe" & # No need for a group
# main program loop - ??? I see no loop
((var++))
echo "$var" > "$pipe"
((var++))
echo "$var" > "$pipe"
((var++))
echo "$var" > "$pipe"
echo "waiting"
wait
rm "$pipe"
Example run:
$ bash gash.sh
1
waiting
2
Error 3
However I really don't see the point in using a separate process. Why not just call a function to test the value after each change?
if you run your bashscript with a . before, it will be use the same environment and can change existing variable. Look at this:
$ cat test.sh
#!/usr/bin/env bash
a=12
echo $a
$ a=1
$ echo $a
1
$ ./test.sh
12
$ echo $a
1
$ . ./test.sh
12
$ echo $a
12
After i run . ./test.sh the variable $a has been changed through the script.
I'm trying to pass the second argument to get an array and loop trough but im getting this error: ${$2[#]}: bad substitution
my code is:
/etc/init.d/displaycameras start c1
#!/bin/bash
dis1cam1="screen -dmS dis1cam1 sh -c 'omxplayer --avdict rtsp_transport:tcp --win \"0 0 640 428\" rtsp://myvideo --live -n -1'";
camera_feeds=('c1=(dis1cam1 dis1cam2 dis1cam3 dis1cam4 dis1cam5 dis1cam6 dis1cam8 dis1cam9)' 'c2=(dis2cam1 dis2cam2 dis2cam3 dis2cam4)')
for elt in "${camera_feeds[#]}";do eval $elt;done
# Start displaying camera feeds
case "$1" in
start)
for i in "${$2[#]}"
do
eval eval '$'$i
done
echo "Camera Display 1 Started"
;;
Is there a way to pass the 2nd argument to call the c2 set ?
in this way is working perfect:
#!/bin/bash
dis1cam1="screen -dmS dis1cam1 sh -c 'omxplayer --avdict rtsp_transport:tcp --win \"0 0 640 428\" rtsp://myvideo --live -n -1'";
camera_feeds=('c1=(dis1cam1 dis1cam2 dis1cam3 dis1cam4 dis1cam5 dis1cam6 dis1cam8 dis1cam9)' 'c2=(dis2cam1 dis2cam2 dis2cam3 dis2cam4)')
for elt in "${camera_feeds[#]}";do eval $elt;done
# Start displaying camera feeds
case "$1" in
start)
for i in "${c1[#]}"
do
eval eval '$'$i
done
echo "Camera Display 1 Started"
;;
I would strongly advise implementing this differently.
#!/usr/bin/env bash
die() { echo "$*" >&2; exit 1; }
[[ $BASH_VERSION = [0-3]* ]] && die "Bash 4.3 or newer needed"
[[ $BASH_VERSION = 4.[0-2].* ]] && die "Bash 4.3 or newer needed"
dis1cam1() { : "code to start camera dis1cam1 here"; )
dis1cam2() { : "code to start camera dis1cam2 here"; )
# ...etc...
camera_feeds__c1=(dis1cam1 dis1cam2 dis1cam3 dis1cam4 dis1cam5 dis1cam6 dis1cam8 dis1cam9)
camera_feeds__c2=(dis2cam1 dis2cam2 dis2cam3 dis2cam4)
# here, we're showing the iterate-over-all-feeds case
# you can just set var=camera_feeds__c1 yourself if you prefer
for var in "${!camera_feeds__#}"; do # var will be camera_feeds__c1 or camera_feeds__c2
feed_name=${var#camera_feeds__} # feed_name will be "c1" or "c2"
declare -n camera_feeds=$var
for i in "${camera_feeds[#]}"; do
echo "Starting $i in feed $feed_name" >&2
"$i" # look up and run code in variable named in $i
done
unset -n camera_feeds
done
"${camera_feeds__#}" expands to the list of shell variables whose names start with camera_feeds__; this is thus the name of our two arrays.
declare -n camera_feeds=$var then makes camera_feeds an alias for the array presently being iterated over, such that for i in "${camera_feeds[#]}" iterates over that array.
unset -n camera_feeds clears this association.
In specifically Bash version 4.0.0, is there any way to work around the use of an empty $# raising an unbound variable error when set -u is enabled?
Consider the following:
#!/usr/bin/env bash-4.0.0-1
set -xvu
echo "$BASH_VERSION"
echo "${BASH_VERSINFO[#]}"
main () {
printf '%q\n' "${#:-}"
}
main "${#:-}"
Gives me the following output when I provide an empty set of arguments:
neech#nicolaw.uk:~ $ ./test.sh
echo "$BASH_VERSION"
+ echo '4.0.0(1)-release'
4.0.0(1)-release
echo "${BASH_VERSINFO[#]}"
+ echo 4 0 0 1 release x86_64-unknown-linux-gnu
4 0 0 1 release x86_64-unknown-linux-gnu
main () {
printf '%q\n' "${#:-}"
}
main "${#:-}"
./test.sh: line 12: $#: unbound variable
I only see this behaviour in Bash version 4.0.0.
I was hoping that using variable substitution ${#:-} would allow me to work around this, but it seems not.
Is there a way to work around this?
$#, $* are special variables so should always be defined it's a bug
https://unix.stackexchange.com/questions/16560/bash-su-unbound-variable-with-set-u
a workaround, maybe:
set +u
args=("$#")
set -u
main "${args[#]}"
or maybe also
main "${#:+$#}"
Why not do error handling on your own? This way you can control exactly what happens when an exception is encountered, for instance return a custom exit code and message for that error, rather than be confined to some predefined behavior.
function log_error
{
[[ $# -ne 1 ]] && return 1
typeset msg="$1"
typeset timestamp=$(date "+%F %T")
echo "[${timestamp}] [ERROR] - $msg " >&2
}
if [[ -z "$BASH_VERSION" ]]
then
log_error "BASH VERSION is not set"
exit 1
fi
so i have a simple lab exercise in class. Write and Interrupt/signal trapping program. this program will prompt user to guess the age of a grandmother. The user may guess as many times as possible. nothing will be able to terminate this program execpt when the user enters the right answer.
so my question is this. I have trapped ctrl_c but is there some "trick" or command i can use to trap ALL the interrupts or do i need to just make a statement for each signal i want to trap.
age=88
trap ctrl_c INT
function ctrl_c()
{
echo "**Trapped CTRL-C"
}
while [ 1 ]
do
echo "Please enter Grandmothers age. "
read ageGuess
echo $ageGuess
if [ $ageGuess == $age ]
then
echo "Exiting!"
exit
fi
done
Afaik trap xxx INT should be enough you can read more here and here, I will mention a few things about your script though:
The test command ([) uses = to compare two strings, not ==.
You should double-quote all your variables unless you are sure what will happen if they are not quoted, consider this:
a=a
b=a
# This will work since `$a` and `$b` contains a value
if [ $a = $b ]; then
echo hello
fi
This will fail since $c is empty and the statement will evaluate to: if [ = ]; then
if [ $c = $d ]; then
echo fail
fi
You should use true or : in your while loop:
while :; do
You should usually always use -r in read, and remember the shebang:
#!/bin/bash
age=88
trap ctrl_c INT
ctrl_c() {
echo "**Trapped CTRL-C"
}
while :; do
echo "Please enter Grandmothers age. "
read -r ageGuess
echo "$ageGuess"
if [ "$ageGuess" = "$age" ]; then
echo "Exiting!"
exit
fi
done
Actually the code could now use #!/bin/sh since it POSIX compatible.
Just note that you might want to use printf "%s\n" "$user_input" instead of echo "$user_input"
I generally have -e set in my Bash scripts, but occasionally I would like to run a command and get the return value.
Without doing the set +e; some-command; res=$?; set -e dance, how can I do that?
From the bash manual:
The shell does not exit if the command that fails is [...] part of any command executed in a && or || list [...].
So, just do:
#!/bin/bash
set -eu
foo() {
# exit code will be 0, 1, or 2
return $(( RANDOM % 3 ))
}
ret=0
foo || ret=$?
echo "foo() exited with: $ret"
Example runs:
$ ./foo.sh
foo() exited with: 1
$ ./foo.sh
foo() exited with: 0
$ ./foo.sh
foo() exited with: 2
This is the canonical way of doing it.
as an alternative
ans=0
some-command || ans=$?
Maybe try running the commands in question in a subshell, like this?
res=$(some-command > /dev/null; echo $?)
Given behavior of shell described at this question it's possible to use following construct:
#!/bin/sh
set -e
{ custom_command; rc=$?; } || :
echo $rc
Another option is to use simple if. It is a bit longer, but fully supported by bash, i.e. that the command can return non-zero value, but the script doesn't exit even with set -e. See it in this simple script:
#! /bin/bash -eu
f () {
return 2
}
if f;then
echo Command succeeded
else
echo Command failed, returned: $?
fi
echo Script still continues.
When we run it, we can see that script still continues after non-zero return code:
$ ./test.sh
Command failed, returned: 2
Script still continues.
Use a wrapper function to execute your commands:
function __e {
set +e
"$#"
__r=$?
set -e
}
__e yourcommand arg1 arg2
And use $__r instead of $?:
if [[ __r -eq 0 ]]; then
echo "success"
else
echo "failed"
fi
Another method to call commands in a pipe, only that you have to quote the pipe. This does a safe eval.
function __p {
set +e
local __A=() __I
for (( __I = 1; __I <= $#; ++__I )); do
if [[ "${!__I}" == '|' ]]; then
__A+=('|')
else
__A+=("\"\$$__I\"")
fi
done
eval "${__A[#]}"
__r=$?
set -e
}
Example:
__p echo abc '|' grep abc
And I actually prefer this syntax:
__p echo abc :: grep abc
Which I could do with
...
if [[ ${!__I} == '::' ]]; then
...