Bash passing array to expr - arrays

I'm writing a simple calculator in BASH. Its aim is to loop through the arguments given, check if they're correct, do the power function and pass the rest to the expr to do the calculation. Everything except multiplication works.
When I do something like
my_script.sh 2 \* 2
I get syntax error from expr. Checking with bash -x lets me know that
expr 2 '\*' 2
The * is in apostrophes. I don't know how to get rid of it so the expr can parse it properly.
if [ $# -le 0 ]
then
usage
exit 1
fi
ARGS=("${#}")
while [ $# -gt 0 ]
do
if [ $OP -eq 0 ]
then
if [[ $1 =~ ^[-]{0,1}[0-9]+$ ]]
then
ELEMS[$J]=$1
shift
let OP=1
let J=$J+1
else
echo $1' is not a number'
usage
exit 3
fi
else
if [[ $1 =~ ^[-+/\^\*]{1}$ ]]
then
if [[ $1 =~ ^[\^]{1}$ ]]
then
if ! [[ $2 =~ ^[0-9]+$ ]]
then
echo 'Bad power exponent'
usage
exit 3
fi
let BASE=${ELEMS[$J-1]}
let EX=$2
pow $BASE $EX
let ELEMS[$J-1]=$RES
shift 2
else
if [[ $1 =~ [\*]{1} ]]
then
ELEMS[$J]=\\*
else
ELEMS[$J]=$1
fi
let J=$J+1
shift
let OP=0
fi
else
echo $1' is not an operator'
if [[ $1 =~ ^[0-9]+$ ]]
then
let TMP=${ELEMS[$J-1]}
echo "Are you missing an operator beetwen $TMP and $1?"
fi
usage
exit 3
fi
fi
done
if [ $OP -eq 0 ]
then
echo 'Missing argument after last operator'
usage
exit 3
fi
echo "Calculation: ${ARGS[*]}"
echo -n 'Result: '
expr ${ELEMS[*]}

Change ELEMS[$J]=\\* to ELEMS[$J]="*" (or ELEMS[$J]=\*) and use:
expr "${ELEMS[#]}"
The key is to use # instead of * in the array dereference, which allows you to use double quotes. This is equivalent to expr "2" "*" "2", instead of expr "2 * 2" which you get when using expr "${ELEMS[*]}"

Related

Bash - How to check if value doesn't exist in array?

Folks,
I have an array, ex,
declare -a arr=("crm" "hr" "pos")
I need to output error if the passed value doesn't exist in this array
I'm trying this use below snippet but it prints "No match found" for any value
match=0
for i in "${arr[#]}"; do
if ! [[ $2 == "$i" ]]; then
match=1
break
fi
done
if [[ $match = 1 ]]; then
echo "No match found"
fi
Any idea how to loop in array and popup error if value doesn't exist ?
There are already answers about your question see check value is in an array, but a fix/idea for your specific approach is something like.
#!/usr/bin/env bash
declare -a arr=("crm" "hr" "pos")
match=0
for i in "${arr[#]}"; do
if ! [[ $2 == "$i" ]]; then
((match++))
fi
done
if (( match == ${#arr[*]} )); then
printf >&2 "No match found\n"
fi
The above script increments match every time the test inside the for loop is true, and in the end compare match with the length of the array ${#arr[*]}. A more verbose output is to put set -x on after the shebang and add a $ sign to the variable match (which is not required) inside the (( )).
if (( $match == ${#arr[*]} )); then
Your original approach always breaks the loop when it does not have a match, doing so will not continue the loop.
You reversed the logic. Just leave out the ! and swap the final test:
match=0
for i in "${arr[#]}"; do
if [[ $2 == "$i" ]]; then
match=1
break
fi
done
if [[ $match = 0 ]]; then
echo "No match found"
fi
This doesn't require a loop.
c.f. if a 2 strings BOTH present in 1 array in a bash script
$: x=bar
$: [[ " ${arr[*]} " =~ " $x " ]] && echo found || echo nope
nope
$: x=hr
$: [[ " ${arr[*]} " =~ " $x " ]] && echo found || echo nope
found
or
$: match=0
$: set -- foo bar
$: [[ " ${arr[*]} " =~ " $2 " ]] && match=1
$: echo match
0
$: set -- foo crm
$: [[ " ${arr[*]} " =~ " $2 " ]] && match=1
$: echo match
1
The logic seems incorrect.
If it does not match, you break the loop.
If it does match, the loop goes to the next iteration. Then it becomes unmatched and finally breaks the loop
declare -a arr=("crm" "hr" "pos")
not_match=0
for i in "${arr[#]}"; do
if [ "$2" = "$i" ]; then
not_match=1
break
fi
done
if [ $not_match -eq 0 ]; then
echo "No match found"
fi

Bash, compare string to arrayvalue

i try to match a parameter with some array content. At the if clauses should be true, but it wont be.
At the output before compare i got this:
VAL: drei_01 AND: drei
#!/bin/bash
array=( null_01 eins_01 zwei_01 drei_01 vier_01 )
lookarr() {
maxc=${#array[#]}
mbool=0
for((i=0; i<$maxc; i++))
do
val=${array[$i]}
echo "VAL: $val AND: $1"
if [[ $1 == *" $val "* ]]; then
echo "TESTENTRY1"
#do something
mbool=1
break
fi
done
if [[ $mbool -eq 0 ]]; then
echo "TESTENTRY2"
#do something else
fi
}
lookarr drei
thanks
Your if statement isn't matching because it is back-to-front and has extra spaces. For drei to match drei_01 you can replace your if statement with:
if [[ "$val" == *"$1"* ]]; then

Bash, how to iterate through an array once & append to end of line?

I am tasked with writing a script that analyzes code and attaches a comment with #Loopn or #Selection n that corresponds with the correct statements.
echo "enter full file name: "
read file
getArray(){
arr=()
while IFS= read -r line
do
arr+=("$line")
done < "$1"
}
getArray $file
echo "What file looks like before editing"
printf "%s\n" "${arr[#]}" #Test function to see if array works (it does)
#Declare variables
x=1
y=1
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ));
do
if [[ "${arr[$i]}" == "while" ]] || [[ "${arr[$i]}" == "until" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x++"
continue
elif [[ "${arr[$i]}" == "for" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x++"
continue
elif [[ "${arr[$i]}" == "break" ]] || [[ "${arr[$i]}" == "done" ]]
then sed -i 's/$/ #Loop'$x'/' $file && let "x--"
continue
elif [[ "${arr[$i]}" == "if" ]] || [[ "${arr[$i]}" == "case" ]]
then sed -i 's/$/ #Selection'$y'/' $file && let "y++"
continue
elif [[ "${arr[$i]}" == "fi" ]] || [[ "${arr[$i]}" == "esac" ]]
then sed -i 's/$/ #Selection'$y'/' $file && let "y--"
continue
else
continue
fi
done < $file
Obviously I'm a newbie in bash, and my loop logic/language usage might be a bit wonky. Can anyone help? Right now the output makes it seem like I am iterating through the array more than once and Sed appends additional text per line.
In case it wasn't clear: each array element is a line of strings; if an array element contains while || for || until then it adds a #loop n and with each of the corresponding break or done, it adds the same #loop n. And likewise for if and case and fi esac except it adds #selection n.
Sample Input:
Before
Final=$(date -d "2016-12-15 14:00" "+%j")
while true ; do
Today=$(date "+%j")
Days=$((Final - Today))
if (( Days >= 14 )) ; then
echo party
elif (( Days >= 2 )) ; then
echo study
elif (( Days == 1 )) ; then
for Count in 1 2 3
do
echo panic
done
else
break
fi
sleep 8h
done
Expected Output:
After
Final=$(date -d "2016-12-15 14:00" "+%j")
while true ; do # loop 1
Today=$(date "+%j")
Days=$((Final - Today))
if (( Days >= 14 )) ; then # selection 1
echo party
elif (( Days >= 2 )) ; then
echo study
elif (( Days == 1 )) ; then
for Count in 1 2 3 # loop 2
do
echo panic
done # loop 2
else
break
fi # selection 1
sleep 8h
done # loop 1
Right now the output makes it seem like I am iterating through the array more than once and Sed appends additional text per line.
This is because the comment to be attached to one line is appended to each line of the file, since no line number is specified for the sed substitute commands in your script. There surely are more efficient solutions, but prepending the corresponding line number is sufficient.
Though your script is quite close to working, two more problems have to be addressed. One is that the == expressions you use to test for the keywords match only if the whole line contains nothing else than the keyword (not even leading space); to allow for indentation, =~ with an appropriate regular expression is useful. The other problem is the counting of the nesting depth (including the simple, but special case of break, where the depth remains unchanged); this seems more easy if we start at depth 0. So, your main loop could be:
x=0
y=0
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ))
do let l=1+i # line numbers start at 1
if [[ "${arr[$i]}" =~ ^[$IFS]*(while|until|for) ]]
then sed -i $l"s/$/ #Loop$((++x))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*break ]]
then sed -i $l"s/$/ #Loop$x/" $file # no x-- here!
elif [[ "${arr[$i]}" =~ ^[$IFS]*done ]]
then sed -i $l"s/$/ #Loop$((x--))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*(if|case) ]]
then sed -i $l"s/$/ #Selection$((++y))/" $file
elif [[ "${arr[$i]}" =~ ^[$IFS]*(fi|esac) ]]
then sed -i $l"s/$/ #Selection$((y--))/" $file
fi
done <$file
You can test this;
echo "enter full file name: "
read file
getArray(){
arr=()
while IFS= read -r line
do
arr+=("$line")
done < "$1"
}
getArray $file
echo "What file looks like before editing"
printf "%s\n" "${arr[#]}" #Test function to see if array works (it does)
#Declare variables
x=1
y=1
#Start main loop
for (( i=0; i<${#arr[#]}; i++ ));
do
#echo $i "====" ${arr[$i]}
#echo "-------"
#echo "x"$x
if [[ "${arr[$i]}" == "while"* ]] || [[ "${arr[$i]}" == "until" ]]
then
sed -i "$((i+1))s/$/ #Loop $x/" $file
let "x++"
continue;
fi
if [[ "${arr[$i]}" == *"for"* ]]
then sed -i "$((i+1))s/$/ #Loop'$x'/" $file && let "x++"
continue
fi
if [[ "${arr[$i]}" == *"done"* ]]
then sed -i "$((i+1))s/$/ #Loop'$((x-1))'/" $file
continue
fi
if [[ "${arr[$i]}" == *"if"* ]] || [[ "${arr[$i]}" == *"case"* ]]
then
if [[ "${arr[$i]}" != *"elif"* ]]
then
sed -i "$((i+1))s/$/ #Selection'$y'/" $file && let "y++"
fi
continue
fi
if [[ "${arr[$i]}" == *"fi"* ]] || [[ "${arr[$i]}" == *"esac"* ]]
then sed -i "$((i+1))s/$/ #Selection'$((y-1))'/" $file
continue
fi
done < $file

In Bash, how to convert number list into ranges of numbers?

Currently I have a sorted output of numbers from a command:
18,19,62,161,162,163,165
I would like to condense these number lists into a list of single numbers or ranges of numbers
18-19,62,161-163,165
I thought about trying to sort through the array in bash and read the next number to see if it is +1... I have a PHP function that does essentially the same thing, but I'm having trouble transposing it to Bash:
foreach ($missing as $key => $tag) {
$next = $missing[$key+1];
if (!isset($first)) {
$first = $tag;
}
if($next != $tag + 1) {
if($first == $tag) {
echo '<tr><td>'.$tag.'</td></tr>';
} else {
echo '<tr><td>'.$first.'-'.$tag.'</td></tr>';
}
unset($first);
}
}
I'm thinking there's probably a one-liner in bash that could do this but my Googling is coming up short....
UPDATE:
Thank you #Karoly Horvath for a quick answer which I used to finish my project. I'd sure be interested in any simpler solutions out there.
Yes, shell does variable substitution, if prev is not set, that line becomes:
if [ -ne $n+1]
Here is a working version:
numbers="18,19,62,161,162,163,165"
echo $numbers, | sed "s/,/\n/g" | while read num; do
if [[ -z $first ]]; then
first=$num; last=$num; continue;
fi
if [[ num -ne $((last + 1)) ]]; then
if [[ first -eq last ]]; then echo $first; else echo $first-$last; fi
first=$num; last=$num
else
: $((last++))
fi
done | paste -sd ","
18-19,62,161-163,165
Only with a function in bash:
#!/bin/bash
list2range() {
set -- ${#//,/ } # convert string to parameters
local first a b string IFS
local -a array
local endofrange=0
while [[ $# -ge 1 ]]; do
a=$1; shift; b=$1
if [[ $a+1 -eq $b ]]; then
if [[ $endofrange -eq 0 ]]; then
first=$a
endofrange=1
fi
else
if [[ $endofrange -eq 1 ]]; then
array+=($first-$a)
else
array+=($a)
fi
endofrange=0
fi
done
IFS=","; echo "${array[*]}"
}
list2range 18,19,62,161,162,163,165
Output:
18-19,62,161-163,165

In array operator in bash

Is there a way to test whether an array contains a specified element?
e.g., something like:
array=(one two three)
if [ "one" in ${array} ]; then
...
fi
A for loop will do the trick.
array=(one two three)
for i in "${array[#]}"; do
if [[ "$i" = "one" ]]; then
...
break
fi
done
Try this:
array=(one two three)
if [[ "${array[*]}" =~ "one" ]]; then
echo "'one' is found"
fi
I got an function 'contains' in my .bashrc-file:
contains ()
{
param=$1;
shift;
for elem in "$#";
do
[[ "$param" = "$elem" ]] && return 0;
done;
return 1
}
It works well with an array:
contains on $array && echo hit || echo miss
miss
contains one $array && echo hit || echo miss
hit
contains onex $array && echo hit || echo miss
miss
But doesn't need an array:
contains one four two one zero && echo hit || echo miss
hit
I like using grep for this:
if echo ${array[#]} | grep -qw one; then
# "one" is in the array
...
fi
(Note that both -q and -w are non-standard options to grep: -w tells it to work on whole words only, and -q ("quiet") suppresses all output.)
array="one two three"
if [ $(echo "$array" | grep one | wc -l) -gt 0 ] ;
then echo yes;
fi
If that's ugly, you could hide it away in a function.
if you just want to check whether an element is in array, another approach
case "${array[#]/one/}" in
"${array[#]}" ) echo "not in there";;
*) echo "found ";;
esac
In_array() {
local NEEDLE="$1"
local ELEMENT
shift
for ELEMENT; do
if [ "$ELEMENT" == "$NEEDLE" ]; then
return 0
fi
done
return 1
}
declare -a ARRAY=( "elem1" "elem2" "elem3" )
if In_array "elem1" "${ARRAY[#]}"; then
...
A nice and elegant version of the above.
in_array() {
local needle=$1 el
shift
for el in "$#"; do
if [ "$el" = "$needle" ]; then
return 0
fi
done
return 1
}
if in_array 1 1 2 3; then
echo true
else
echo false
fi
# alternatively
a=(1 2 3)
if in_array 1 "${a[#]}"; then
...
OPTIONS=('-q','-Q','-s','-S')
find="$(grep "\-q" <<< "${OPTIONS[#]}")"
if [ "$find" = "${OPTIONS[#]}" ];
then
echo "arr contains -q"
fi

Resources