Creating loop within Unix Script - bash

I'm curious of how I might go about creating a more detailed output within the same scripting file. I would like to modify the script so that after it finds the matching line for the currency code, it outputs the following:
Currency code: {currency code value}
Currency name: {currency name value}
Units per USD: {units value}
USD per unit : {USD value}
What I have so far:
#!/bin/bash
# Set local variable $cc to first command line argument, if present
cc=$1
# execute the do loop while $curr is blank
while [ -z "$curr" ]
do
# test if $cc is blank. If so, prompt for a value
if [ -z "$cc" ]
then
echo -n "Enter a currency code: "
read cc
fi
# Search for $cc as a code in the curr.tab file
curr=`grep "^$cc" curr.tab`
# clear cc in case grep found no match and the loop should be repeated
cc=
done
# echo the result
echo $curr
The curr.tab I am using would look something like this:
USD US Dollar 1.0000000000 1.0000000000
EUR Euro 0.7255238463 1.3783144484
GBP British Pound 0.6182980743 1.6173428992
INR Indian Rupee 61.5600229886 0.0162443084
AUD Australian Dollar 1.0381120551 0.9632871472
CAD Canadian Dollar 1.0378792155 0.9635032527
AED Emirati Dirham 3.6730001428 0.2722570000
MYR Malaysian Ringgit 3.1596464286 0.3164911083

At the end, instead of echo $curr do:
[[ $curr =~ ^(...)[[:space:]]+(.+)([[:space:]]+([[:digit:]\.]+)){2}$ ]]
printf "Currency code: %s\nCurrency name: %s\nUnits per USD: %s\nUSD per unit : %s\n" "${BASH_REMATCH[#]:1}"

Related

BASH - read lines from section of file

I have a file formatted like this:
[SITE1]
north
west
[MOTOR]
west
south
north
[AREA]
west
east
north
[CLEAR]
What I need to be able to do is read all values from a specific section.
Eg: read AREA and be returned:
west
east
north
The examples I've found online are for ini files, which have key value pairs.
Can anyone help advise how I can do this ?
Thanks
Using sed :
category=MOTOR; sed -nE "/^\[$category\]$/{:l n;/^(\[.*\])?$/q;p;bl}" /path/to/your/file
It doesn't do anything until it matches a line that constists of your target category, at which point it enters a loop. In this loop, it consumes a line, exits if it's an empty line or another category (or the end of the file) and otherwise prints the line.
The sed commands used are the following :
/pattern/ executes the next command or group of commands when the current line matches the pattern
{commands} regroups commands, for instance to execute them conditionnaly.
:l defines a label named "l", to which you'll be able to jump to.
n asks sed to start working on the next line.
q exits
p prints the current line
bl jumps to the "l" label
You can try it here.
Two options in mind - use a filter (e.g., awk, sed) to extract the relevant section, or use bash to filter to the specific section.
With bash, using a function:
#! /bin/bash
function read_section {
local id=$1
local match
input=()
while read p ; do
if [ "$p" = "[$id]" ] ; then
# Read data here
while read p ; do
# Check for end of section - empty line
if [ "$p" = "" ] ; then
break
fi
# Do something with '$p'
input+=("$p")
echo "Item $p"
done
# Indicate section was found
return 0
fi
done
# Indicate section not found
return 1
}
if read_section "AREA" < p.txt ; then
echo "Found Area" "${#input[$#]}"
else
echo "Missing AREA"
fi
if read_section "FOO" < p.txt ; then
echo "Found FOO"
else
echo "Missing FOO"
fi
Output: (placing sample input into property file p.txt)
Item west
Item east
Item north
Found Area 4
Missing FOO
Notes
that it's not clear if each section ends with empty line. Code assumes that this is the case. Otherwise, the section change can be modified to if [[ "$p" = \[* ]], or similar, with extra check to ignore empty line.
The function return true/false to indicate if the section was found. The script can act on this information.
The loaded items are placed into the input array, for further processing
The alternative is to use an external program to filter the input. This MAY provide performance advantage if the input file is VERY large, or if additional logic is needed.
function filter_section {
local id=$1
awk -v ID="$id" '/^\[/ { p= ($0 == "[" ID "]" ); next } p && $0 { print }' < p.txt
}
function read_section {
local id=$1
local match
input=()
while read p ; do
# Do something with '$p'
input+=("$p")
echo "Item $p"
done <<< $(filter_section "$id")
# Indicate section not found
[ "${#input[*]}" -gt 0 ] && return 0
return 1
}
if read_section "AREA" < p.txt ; then
echo "Found Area" "${#input[$#]}"
else
echo "Missing AREA"
fi
if read_section "FOO" < p.txt ; then
echo "Found FOO"
else
echo "Missing FOO"
fi

Count IP addresses from list

My goal is to take a list of IPs, sometimes the list is just a single IP or it could be a range of IPs and count how many IPs there are in a list. We have another product that generates a list of IPs and we have been manually counting it using an Excel spreadsheet.
Using an existing post here on Stack, I have been attempting to incorporate it into a script that will accept a list. https://codegolf.stackexchange.com/questions/28760/how-many-ip-addresses-are-in-a-given-range (See Pure Bash, 66 bytes).
Instead of grabbing two arguments on the command line $1 and $2, I pull a list into an array then enumerate through the array. If it is a single IP, it just adds 1 to the counter variable, if it is a range it uses the logic to convert the IPs to hex and counts them. I'm running into an issue where I am receiving errors.
I can't seem to figure out why this says "invalid number: printf: 229". I've read up on the expansion principle and I cannot seem to get my head around why it keeps throwing this error and calculating it improperly.
I've used this site for years, this is my first post. Any help would be greatly appreciated!
Thank you!
This is what I have so far:
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Please supply a list of IP addresses"
echo "Example: " $0 "list.txt"
exit
fi
#Set some variables
LIST=($(cat ./$1))
COUNT=0
# Function for removing dots and converting to hex
p()(printf %02x ${1//./ })
# Enumerate the array of IPs
for RANGE in ${LIST[#]};do
IFS=- read IP1 IP2 <<< $RANGE
if [ -z $IP2 ]; then
COUNT=$[COUNT + 1]
else
r=$[0x`p $IP1`-0x`p $IP2`]
COUNT=$[COUNT + $[1+${r/-}]]
fi
done
echo "The count is" $COUNT
sample_range.txt:
192.168.47.11
192.168.48.10
192.168.65.228-192.168.65.229
192.168.65.234
192.168.65.239
192.168.65.241
192.168.65.244
192.168.80.220
192.168.93.231-192.168.93.235
192.168.93.237-192.168.93.239
192.168.103.222
This should result in 18, instead it gives me this output:
# ./rc.sh sample_range.txt
: invalid number: printf: 229
: invalid number: printf: 235
: invalid number: printf: 239
The count is 707
IPs are numbers base 256.
#!/bin/bash
ipdiff() {
declare -i dec1 dec2 diff # set integer attribute
dec1=$1*256*256*256+$2*256*256+$3*256+$4
dec2=$5*256*256*256+$6*256*256+$7*256+$8
diff=$dec2-$dec1+1
counter=counter+$diff
}
declare -i counter
# read IP(s) from file, use . and - as separator
while IFS=".-" read -r a1 a2 a3 a4 b1 b2 b3 b4; do
if [[ -z $b1 ]]; then # $b1 is empty (line with one IP)
counter=counter+1
else # $b1 is not empty (line with 2 IPs)
ipdiff $a1 $a2 $a3 $a4 $b1 $b2 $b3 $b4
fi
done < file
echo $counter
Output:
18

How to cut a variable to sub variable

I have a variable which store consist of
TITLE:AUTHOR:PRICE:QUANTITY:SOLDQUANTITY
in this case its
wrinkle in time:myauthor:50.00:20:50
I store it in
Result=`awk -F : -v "Title=$1" -v "Author=$2" 'tolower($1) == tolower(Title) && tolower($2) == tolower(Author)' BookDB.txt`
however I would like to separate it into 4 variables like
TITLE= "wrinkle in time"
AUTHOR= "myauthor"
PRICE= "50.00"
Quantity= "20"
UNIT= "50"
then I would like to do a calculation for unit sold
enter unit sold: 3
wrinkle in time, myauthor, 50.00, 20, 50
after update
wrinkle in time, myauthor, 50.00, 17, 53
thanks in advance your help is much appreciated!!!
You can separate $result into the different variables you describe by using read:
IFS=: read TITLE AUTHOR PRICE QUANTITY UNIT <<< "$result"
Example:
$ result="wrinkle in time:myauthor:50.00:20:50"
$ IFS=: read TITLE AUTHOR PRICE QUANTITY UNIT <<< "$result"
$ echo "$TITLE - by - $AUTHOR"
wrinkle in time - by - myauthor
You can also use read to prompt a user for input:
read -p "Enter units sold: " SOLDUNITS
This will store the value entered in the variable $SOLDUNITS. You can then use this to alter $QUANTITY and $UNIT as desired. To do (integer) arithmetic in bash, you can use the $((expression)) construct:
QUANTITY=$((QUANTITY-SOLDUNITS))
Or:
((QUANTITY-=SOLDUNITS))

Currency converter

I'm stuck on this USD to SEK currency converter and the empty row, row 12. The current conversion is 1 SEK = 0.158193 USD 1 USD = 6.32138 SEK.
Row 12 will have something like SEK="(?(USD) )?"
I don't know what to enter in the question marks.
#!/bin/bash
shopt -s -o nounset
declare -i USD # USD
declare -i SEK # SEK
# Title
printf "%s\n" "USD-SEK Currency Convertor"
printf "\n"
# Get the value to convert
read -p "Enter a USD: " USD
# Do the conversion
printf "You will get SEK %d\n" "$SEK"
exit 0
You can do floating point arithmetic using bc like this:
SEK=$( echo " 6.32138 * $USD " | bc -l )
Explanation:
Bash does not have built it support for floating point arithmetic. Thus, we usually handle these operation using the bc program. bc reads an arithmetic expression as a string from the standard input, and prints the result the standard output. Note that the -l option is necessary for keeping the decimal part of the expression.
In order to get the result from bc, and store it in a variable, we use command redirection i.e. the $( ). Note that there are no spaces before and after the = in the previous expression.
Complete Example
#!/bin/bash
printf "%s\n" "USD-SEK Currency Convertor"
# Get the value to convert
read -p "Enter a USD: " USD
SEK=$(echo " 6.32138 * $USD " | bc -l )
printf "You will get SEK %s\n" "$SEK" ;# NOTE THAT I CHANGED THIS TO %s FROM %f DUE TO THE LOCALE SETTINGS
Output
$ ./converter.sh
USD-SEK Currency Convertor
Enter a USD: 10
You will get SEK 63.213800
Note that i removed the declare -i SEK from the script, since the SEK variable is NOT Integer
The harm of declare -i. This code produces:
#!/bin/bash
declare -i SEK ;# WOOOPS I FORGOT THE declare -i
printf "%s\n" "USD-SEK Currency Convertor"
# Get the value to convert
read -p "Enter a USD: " USD
SEK=$(echo " 6.32138 * $USD " | bc -l )
printf "You will get SEK %s\n" "$SEK"
This output:
$ ./converter.sh
USD-SEK Currency Convertor
Enter a USD: 10
./converter.sh: line 6: 63.21380: syntax error: invalid arithmetic operator (error token is ".21380")
You will get SEK 0.000000

Convert floating point variable to integer?

The shell script shown below will show a warning if the page takes more than 6 seconds to load. The problem is that the myduration variable is not an integer. How do I convert it to integer?
myduration=$(curl http://192.168.50.1/mantisbt/view.php?id=1 -w %{time_total}) > /dev/null ; \
[[ $myduration -gt 1 ]] && echo "`date +'%y%m%d%H%M%S'
It took more than 6 seconds to load the page http://192.168.50.1/mantisbt/view.php?id=1
Assuming $myduration is a decimal or integer
$ myduration=6.5
$ myduration=$( printf "%.0f" $myduration )
$ echo $myduration
6
You can do this:
float=1.23
int=${float%.*}
I am using this on bash.
It's not entirely clear, but I think you're asking how to convert a floating-point value (myduration) to an integer in bash. Something like this may help you, depending on which way you want to round your number.
#!/bin/bash
floor_val=
ceil_val=
function floor() {
float_in=$1
floor_val=${float_in/.*}
}
function ceiling() {
float_in=$1
ceil_val=${float_in/.*}
ceil_val=$((ceil_val+1))
}
float_val=$1
echo Passed in: $float_val
floor $float_val
ceiling $float_val
echo Result of floor: $floor_val
echo Result of ceiling: $ceil_val
Example usage:
$ ./int.sh 12.345
Passed in: 12.345
Result of floor: 12
Result of ceiling: 13
Eliminate page contents from the variable:
When I tried your command, myduration contained the HTML contents of the page at the URL I used in my test plus the time value. By adding -s to suppress the progress bar and adding -o /dev/null to the options for curl, I was able to remove the redirect to /dev/null and have only the time saved in myduration.
Since the value of myduration is likely to be short, you can use the technique ire_and_curses shows which will often yield zero as its result which would be less than the 1 you are testing for (note that your log message says "6 seconds", though).
Finer resolution:
If you'd like to have a finer resolution test, you can multiply myduration by 1000 using a technique like this:
mult1000 () {
local floor=${1%.*}
[[ $floor = "0" ]] && floor=''
local frac='0000'
[[ $floor != $1 ]] && frac=${1#*.}$frac
echo ${floor}${frac:0:3}
}
Edit: This version of mult1000 properly handles values such as "0.234", "1", "2.", "3.5"
and "6.789". For values with more than three decimal places, the extra digits are truncated without rounding regardless of the value ("1.1119" becomes "1.111").
Your script with the changes I mentioned above and using mult1000 (with my own example time):
myduration=$(curl -s -o /dev/null http://192.168.50.1/mantisbt/view.php?id=1 -w %{time_total}); [[ $(mult1000 $myduration) -gt 3500 ]] && echo "`date +'%y%m%d%H%M%S'` took more than 3.5 seconds to load the page http://192.168.50.1/mantisbt/view.php?id=1 " >> /home/shantanu/speed_report.txt
Here it is broken into multiple lines (and simplified) to make it more readable here in this answer:
myduration=$(curl -s -o /dev/null http://example.com -w %{time_total})
[[ $(mult1000 $myduration) -gt 3500 ]] &&
echo "It took more than 3.5 seconds to load thttp://example.com" >> report.txt

Resources