Output a text file's values in a specific manner using bash? - linux

I am trying to look a for some specific values (such as 1 or 4) from a text file using bash. If this value is found in the file, then I want to call a function and pass it the found value as an argument. If the values (1 or 4) are found under a certain column (such as Col3), then I would like to call another function.
The problem I am having is with the code not being able to recognize that the value found is from Col3 and calling a separate function. Its because I am skipping the first two lines so I have no way of tracking which value is under which column.
file.txt :
Name Col1 Col2 Col3
-----------------------
row1 1 4 1
row2 2 5 2
row3 3 6 3
Note that I am skipping the first two lines of the text file when searching the file. Also note that this code is a dummy version of what I have because I just need the general idea on how to approach this.
function retrieve {
if [[ "$1" == "1" ]]; then
var="one beer on the wall"
elif [[ "$1" == "4" ]]; then
var="four beers on the wall"
fi
}
function retrieve2 {
if [[ "$1" == "1" ]]; then
var="22 beers on the wall"
elif [[ "$1" == "4" ]]; then
var="44 beers on the wall"
fi
}
tail -n +3 $PWD/file.txt | while read -r ignored c1: do
echo "$c1"
done | while read -r value; do
if [[ //need to check if the value is under Col3 here// ]]; then
retrieve2 $value
else
retrieve1 $value
fi
echo $var
done

If I understand the problem correctly, we can read each line into an array, and the interate over it. If the field matches $val, then if we are in column $col we call retrieve2, if we are not in column $col we call retrieve1. Here is sketch of it.
#!/bin/bash
val=1
col=3
while read -ra cols
do
for ((i=1; i<${#cols[#]}; i++))
do
if (( cols[i] == val ))
then
if (( i == col ))
then
retrieve2
else
retrieve1
fi
fi
done
done < <(tail -n +3 file)
Note that this assumes that the values are numeric, if not, change the condition (( cols[i] == val )) to [[ ${cols[$i]} == "$val" ]]
However, since it is a bit cumbersome, it would probably be better to restructure your functions from bash to awk, and then do the whole processing in awk, if that makes sense.

Related

Bash script outputs wrong answer [duplicate]

I'm unable to get numeric comparisons working:
echo "enter two numbers";
read a b;
echo "a=$a";
echo "b=$b";
if [ $a \> $b ];
then
echo "a is greater than b";
else
echo "b is greater than a";
fi;
The problem is that it compares the number from the first digit on, i.e., 9 is bigger than 10, but 1 is greater than 09.
How can I convert the numbers into a type to do a true comparison?
In Bash, you should do your check in an arithmetic context:
if (( a > b )); then
...
fi
For POSIX shells that don't support (()), you can use -lt and -gt.
if [ "$a" -gt "$b" ]; then
...
fi
You can get a full list of comparison operators with help test or man test.
Like this:
#!/bin/bash
a=2462620
b=2462620
if [ "$a" -eq "$b" ]; then
echo "They're equal";
fi
Integers can be compared with these operators:
-eq # Equal
-ne # Not equal
-lt # Less than
-le # Less than or equal
-gt # Greater than
-ge # Greater than or equal
See this cheatsheet.
There is also one nice thing some people might not know about:
echo $(( a < b ? a : b ))
This code will print the smallest number out of a and b
In Bash I prefer doing this as it addresses itself more as a conditional operation unlike using (( )) which is more of arithmetic.
[[ n -gt m ]]
Unless I do complex stuff like
(( (n + 1) > m ))
But everyone just has their own preferences. Sad thing is that some people impose their unofficial standards.
You can also do this:
[[ 'n + 1' -gt m ]]
Which allows you to add something else which you could do with [[ ]] besides arithmetic stuff.
The bracket stuff (e.g., [[ $a -gt $b ]] or (( $a > $b )) ) isn't enough if you want to use float numbers as well; it would report a syntax error. If you want to compare float numbers or float number to integer, you can use (( $(bc <<< "...") )).
For example,
a=2.00
b=1
if (( $(bc <<<"$a > $b") )); then
echo "a is greater than b"
else
echo "a is not greater than b"
fi
You can include more than one comparison in the if statement. For example,
a=2.
b=1
c=1.0000
if (( $(bc <<<"$b == $c && $b < $a") )); then
echo "b is equal to c but less than a"
else
echo "b is either not equal to c and/or not less than a"
fi
That's helpful if you want to check if a numeric variable (integer or not) is within a numeric range.
One-line solution.
a=2
b=1
[[ ${a} -gt ${b} ]] && echo "true" || echo "false"
gt reference: https://www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html
&& reference: https://www.gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
[[...]] construct reference: https://www.gnu.org/software/bash/manual/bash.html#index-_005b_005b
${} reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 (2.6.2)
The format for parameter expansion is as follows:
${expression}
where expression consists of all characters until the matching '}'.
Any '}' escaped by a or within a quoted string, and
characters in embedded arithmetic expansions, command substitutions,
and variable expansions, shall not be examined in determining the
matching '}'.
The simplest form for parameter expansion is:
${parameter}
This code can also compare floats. It is using AWK (it is not pure Bash). However, this shouldn't be a problem, as AWK is a standard POSIX command that is most likely shipped by default with your operating system.
$ awk 'BEGIN {return_code=(-1.2345 == -1.2345) ? 0 : 1; exit} END {exit return_code}'
$ echo $?
0
$ awk 'BEGIN {return_code=(-1.2345 >= -1.2345) ? 0 : 1; exit} END {exit return_code}'
$ echo $?
0
$ awk 'BEGIN {return_code=(-1.2345 < -1.2345) ? 0 : 1; exit} END {exit return_code}'
$ echo $?
1
$ awk 'BEGIN {return_code=(-1.2345 < 2) ? 0 : 1; exit} END {exit return_code}'
$ echo $?
0
$ awk 'BEGIN {return_code=(-1.2345 > 2) ? 0 : 1; exit} END {exit return_code}'
$ echo $?
To make it shorter for use, use this function:
compare_nums()
{
# Function to compare two numbers (float or integers) by using AWK.
# The function will not print anything, but it will return 0 (if the comparison is true) or 1
# (if the comparison is false) exit codes, so it can be used directly in shell one liners.
#############
### Usage ###
### Note that you have to enclose the comparison operator in quotes.
#############
# compare_nums 1 ">" 2 # returns false
# compare_nums 1.23 "<=" 2 # returns true
# compare_nums -1.238 "<=" -2 # returns false
#############################################
num1=$1
op=$2
num2=$3
E_BADARGS=65
# Make sure that the provided numbers are actually numbers.
if ! [[ $num1 =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then >&2 echo "$num1 is not a number"; return $E_BADARGS; fi
if ! [[ $num2 =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then >&2 echo "$num2 is not a number"; return $E_BADARGS; fi
# If you want to print the exit code as well (instead of only returning it), uncomment
# the awk line below and comment the uncommented one which is two lines below.
#awk 'BEGIN {print return_code=('$num1' '$op' '$num2') ? 0 : 1; exit} END {exit return_code}'
awk 'BEGIN {return_code=('$num1' '$op' '$num2') ? 0 : 1; exit} END {exit return_code}'
return_code=$?
return $return_code
}
$ compare_nums -1.2345 ">=" -1.2345 && echo true || echo false
true
$ compare_nums -1.2345 ">=" 23 && echo true || echo false
false
If you have floats, you can write a function and then use that. For example,
#!/bin/bash
function float_gt() {
perl -e "{if($1>$2){print 1} else {print 0}}"
}
x=3.14
y=5.20
if [ $(float_gt $x $y) == 1 ] ; then
echo "do stuff with x"
else
echo "do stuff with y"
fi
I solved this by using a small function to convert version strings to plain integer values that can be compared:
function versionToInt() {
local IFS=.
parts=($1)
let val=1000000*parts[0]+1000*parts[1]+parts[2]
echo $val
}
This makes two important assumptions:
The input is a "normal SemVer string"
Each part is between 0-999
For example
versionToInt 12.34.56 # --> 12034056
versionToInt 1.2.3 # --> 1002003
Example testing whether npm command meets the minimum requirement...
NPM_ACTUAL=$(versionToInt $(npm --version)) # Capture npm version
NPM_REQUIRED=$(versionToInt 4.3.0) # Desired version
if [ $NPM_ACTUAL \< $NPM_REQUIRED ]; then
echo "Please update to npm#latest"
exit 1
fi
Just adding to all the above answers:
If you have more than one expression in single if statement, you can do something like this:
if (( $a % 2 == 0 )) && (( $b % 2 != 0));
then
echo "What you want to do"
fi
Hope this helps!

How to validate a number against a list in Bash? [duplicate]

This question already has answers here:
Check if a Bash array contains a value
(41 answers)
Closed 2 years ago.
I was wondering if there is an efficient way to check if an element is present within an array in Bash? I am looking for something similar to what I can do in Python, like:
arr = ['a','b','c','d']
if 'd' in arr:
do your thing
else:
do something
I've seen solutions using associative array for bash for Bash 4+, but I am wondering if there is another solution out there.
Please understand that I know the trivial solution is to iterate in the array, but I don't want that.
You could do:
if [[ " ${arr[*]} " == *" d "* ]]; then
echo "arr contains d"
fi
This will give false positives for example if you look for "a b" -- that substring is in the joined string but not as an array element. This dilemma will occur for whatever delimiter you choose.
The safest way is to loop over the array until you find the element:
array_contains () {
local seeking=$1; shift
local in=1
for element; do
if [[ $element == "$seeking" ]]; then
in=0
break
fi
done
return $in
}
arr=(a b c "d e" f g)
array_contains "a b" "${arr[#]}" && echo yes || echo no # no
array_contains "d e" "${arr[#]}" && echo yes || echo no # yes
Here's a "cleaner" version where you just pass the array name, not all its elements
array_contains2 () {
local array="$1[#]"
local seeking=$2
local in=1
for element in "${!array}"; do
if [[ $element == "$seeking" ]]; then
in=0
break
fi
done
return $in
}
array_contains2 arr "a b" && echo yes || echo no # no
array_contains2 arr "d e" && echo yes || echo no # yes
For associative arrays, there's a very tidy way to test if the array contains a given key: The -v operator
$ declare -A arr=( [foo]=bar [baz]=qux )
$ [[ -v arr[foo] ]] && echo yes || echo no
yes
$ [[ -v arr[bar] ]] && echo yes || echo no
no
See 6.4 Bash Conditional Expressions in the manual.
Obvious caveats aside, if your array was actually like the one above, you could do
if [[ ${arr[*]} =~ d ]]
then
do your thing
else
do something
fi
Initialize array arr and add elements
set variable to search for SEARCH_STRING
check if your array contains element
arr=()
arr+=('a')
arr+=('b')
arr+=('c')
SEARCH_STRING='b'
if [[ " ${arr[*]} " == *"$SEARCH_STRING"* ]];
then
echo "YES, your arr contains $SEARCH_STRING"
else
echo "NO, your arr does not contain $SEARCH_STRING"
fi
If array elements don't contain spaces, another (perhaps more readable) solution would be:
if echo ${arr[#]} | grep -q -w "d"; then
echo "is in array"
else
echo "is not in array"
fi
array=("word" "two words") # let's look for "two words"
using grep and printf:
(printf '%s\n' "${array[#]}" | grep -x -q "two words") && <run_your_if_found_command_here>
using for:
(for e in "${array[#]}"; do [[ "$e" == "two words" ]] && exit 0; done; exit 1) && <run_your_if_found_command_here>
For not_found results add || <run_your_if_notfound_command_here>
As bash does not have a built-in value in array operator and the =~ operator or the [[ "${array[#]" == *"${item}"* ]] notation keep confusing me, I usually combine grep with a here-string:
colors=('black' 'blue' 'light green')
if grep -q 'black' <<< "${colors[#]}"
then
echo 'match'
fi
Beware however that this suffers from the same false positives issue as many of the other answers that occurs when the item to search for is fully contained, but is not equal to another item:
if grep -q 'green' <<< "${colors[#]}"
then
echo 'should not match, but does'
fi
If that is an issue for your use case, you probably won't get around looping over the array:
for color in "${colors[#]}"
do
if [ "${color}" = 'green' ]
then
echo "should not match and won't"
break
fi
done
for color in "${colors[#]}"
do
if [ "${color}" = 'light green' ]
then
echo 'match'
break
fi
done
Here's another way that might be faster, in terms of compute time, than iterating. Not sure. The idea is to convert the array to a string, truncate it, and get the size of the new array.
For example, to find the index of 'd':
arr=(a b c d)
temp=`echo ${arr[#]}`
temp=( ${temp%%d*} )
index=${#temp[#]}
You could turn this into a function like:
get-index() {
Item=$1
Array="$2[#]"
ArgArray=( ${!Array} )
NewArray=( ${!Array%%${Item}*} )
Index=${#NewArray[#]}
[[ ${#ArgArray[#]} == ${#NewArray[#]} ]] && echo -1 || echo $Index
}
You could then call:
get-index d arr
and it would echo back 3, which would be assignable with:
index=`get-index d arr`
FWIW, here's what I used:
expr "${arr[*]}" : ".*\<$item\>"
This works where there are no delimiters in any of the array items or in the search target. I didn't need to solve the general case for my applicaiton.

Check if parameter is value X or value Y [duplicate]

This question already has answers here:
Bash If-statement to check If string is equal to one of several string literals [duplicate]
(2 answers)
Closed 6 years ago.
I want to check in my bash script, if a variable is equal to value 1 OR equal to value 2.
I don't want to use something like this, because the 'if true statements' are the same (some big echo texts), when the variable is equal to 1 or 2. I want to avoid data redundancy.
if [ $1 == 1 ] ; then echo number 1 ; else
if [ $1 == 2 ] ; then echo number 2 ; fi
More something like
if [ $1 == 1 OR 2 ] ; then echo number 1 or 2 ; fi
Since you are comparing integer values, use the bash arithmetic operator (()), as
(( $1 == 1 || $1 == 2 )) && echo "number 1 or 2"
For handling-strings using the regex operator in bash
test="dude"
if [[ "$test" =~ ^(dude|coolDude)$ ]]; then echo "Dude Anyway"; fi
# literally means match test against either of words separated by | as a whole
# and not allow for sub-string matches.
Probably the most easy to extend option is a case statement:
case $1 in
[12])
echo "number $1"
esac
The pattern [12] matches 1 or 2. For larger ranges you could use [1-5], or more complicated patterns like [1-9]|[1-9][0-9] to match any number from 1 to 99.
When you have multiple cases, you should separate each one with a ;;.

Use result of bash comparison in another comparison

I'd like to use the result of a comparison in a subsequent comparison. I'm trying to do something like:
# $1 - expected result
# $2 - actual result
function print_result() {
if [[ [[ $1 -eq 0 ]] -eq [[ $2 -eq 0 ]] ]]; then # invalid
echo "Pass"
else
echo "Fail"
fi
}
I can get the desired behaviour with the more verbose form:
function print_result() {
if [[ (($1 -eq 0) && ($2 -eq 0)) || (($1 -ne 0) && ($2 -ne 0)) ]]; then
echo "Pass"
else
echo "Fail"
fi
}
but it seems like there should be a simpler way?
You need something which produces the result of the comparison operator as text rather than as a status return. That would be arithmetic expansion: $((expression)).
Note also that bash includes a numeric conditional compound statement -- (( expr )) -- which is often easier to use for numerical comparisons than the non-numerical [[ ... ]] conditional compound statement.
Putting that together, what you are looking for is:
if (( $(($1==0)) == $(($2==0)) )); then
Or just:
if (( ($1==0) == ($2==0) )); then
If you only want to know if $1 and $2 are both zero or both non-zero, then you can use the fact that boolean not (!) always evaluates to either 0 or 1. Consequently, the following even simpler expression is equivalent (but see warning below):
if ((!$1 == !$2)); then
Important: If you use that in a script, it will work fine, but at the command prompt you need to put a space after the ! to avoid it being interpreted as a history expansion character (unless you've turned history expansion off: if you don't actually use history expansion, turning it off is not a bad idea.)
A possibly slightly more readable (but not particularly simpler) way would be to store the result of each comparison in a variable and then compare the variables:
function print_result() {
[[ $1 -eq 0 ]]; arg_one_is_zero=$?
[[ $2 -eq 0 ]]; arg_two_is_zero=$?
if [[ $arg_one_is_zero -eq $arg_two_is_zero ]]; then
echo "Pass"
else
echo "Fail"
fi
}
There might be a better way to store the logical result (exit code) of a comparison in a variable but I couldn't find one after a quick look.

Unix shell script how to test all the array values is equal to particular value?

I Have three array variables like
var[1]=1
var[2]=1
var[3]=1
Now i want to check all these three array variables is equal to 1 or not using if command.
I tried something like
if [[ ${var[#] == 1 ]]; then
echo "Yes"
else
echo "No"
fi
The result of the above code should be Yes but i'm getting No as an answer.
Can anyone please help me on this?
${var[#]} will expand to 1 1 1 which is most definitely not equal to 1.
You can use something like:
rc=Yes
for val in ${var[#]} ; do
if [[ ${val} != 1 ]]; then
rc=No
fi
done
echo $rc
or the more succinct:
rc=Yes
for v in ${var[#]}; do [[ $v == 1 ]] || rc=No; done
echo $rc

Resources