Unit conversion with Bash - bash

I'm writing a bash script that converts units from a very specific input.
I started out doing simple read and echo statements and was able to get it to read a very specific input derived from declared integers and numbers but I'm having trouble getting it to work with if statements.
Here is my simple code so far:
#!/bin/bash
declare -i n
in=inches
ft=feet
read number in "as" feet
if [ ]; then
echo "$n $in = $[n/12] $ft"
fi
What I want to do now is to create if/else statements to flow according to a number of conditionals dependent on the user input. So I want the user to type something like "24 inches as feet" or "50 yards as inches" and execute its respective output. Right now, I don't really know what to put into the if statements without getting an error like "command not found".
Any help would be appreciated. Thank you.

First, don't use quotes on the variables in your read command.
read number in as feet
Another improvement is to rename the variables to represent what they store. I'm going to replace as with the underscore, a valid but "unreadable" name that emphasizes that it's just a placeholder, and we don't really care about its value.
read value src_unit _ dest_unit
# If the user enters "24 inches as feet", we have:
# value=24
# src_unit=inches
# _=as
# dest_unit=feet
Now, your if statements need to check two things: what is the units of the value, and what unit do we want to convert it to. Here's a template:
if [ "$src_unit" = X ] && [ "$dest_unit" = Y ]; then
# Convert X into Y
fi
You would replace X and Y with the units you are converting from and to, and
the code in the middle would be something like new_value=$(( $value / 12 )), if
converting from inches to feet. Note that bash cannot handle floating-point arithmetic,
which is a topic for another question.
A case statement, as suggested by cwgem:
case "$src_unit-$dest_unit" in
inches-feet)
new_value=$(( $value / 12 ))
;;
gallons-quarts)
new_value=$(( $value * 4 ))
;;
*) echo "I don't know how to convert $src_unit to $dest_unit"
;;
esac

!/bin/bash
echo "enter a number to be converted"
read number
feet=$(($number*12))
inches=$(($number/12))
echo "feet conversion to inches="$feet
echo "inches conversion to feet="$inches
fi

Related

How can i make my arguments my starting and ending number in my loop

my script
how can i make the first argument become the first number in the loop and make my second argument become the second number in the loop?
We can use a new variable $counter to avoid changing the value of your variables as follows. Obviously you can do other things with the values than echo them to STDOUT.
#!/bin/bash
bhuser1=2
bhuser2=5
counter=$bhuser1
while [ $counter -le $bhuser2 ]
do
echo $counter
((counter++))
done
output
2
3
4
5
[Execution complete with exit code 0]
I have found the following tutorial helpful: https://ryanstutorials.net/bash-scripting-tutorial/bash-loops.php
The range expression { .. } unfortunately does not accept variables, because range expansion happens before parameter expansion. You have to implement a countint loop explicitly, something like.
for ((i=bhuser1; i<=bhuser2; i+=1))
do
....
done

Bash how to minus the number of guesses each time the user inputs?

while true
do
if [ $userinput = 1 ];
then
guesses=10
(( answer = RANDOM % 20 ))
read -p "Guess the number between 1-20 if you can $answer : " input
if [ $input != $answer ];
then
(( guesses=guesses-1 ))
echo "Wrong answer! You got ${guesses} left!"
else
echo "Correct answer! You had ${guesses} left. Lucky you!"
read -p "${name}, would you like to continue playing or not [Yes/No]? " decide
if [ $decide = "Yes" ];
then
continue
else
echo -e "${Red}Bye bye!"
Example:
guess=10
User inputs 2 guesses then program has to minus those 2 guesses from total of 10 guesses, in that case, 10-2=8 guesses left. How to do this?
The only necessary change is moving guesses=10 out of your loop, such that it's run only once (when your script is starting).
As for best-practice decrement forms, a terser bash-only approach (albeit no more or less valid than your existing implementation) would look like:
(( guesses-- ))
...whereas a more portable approach (compatible with all POSIX-family shells) is:
guesses=$(( guesses - 1 ))
If you are specifically using the Bash shell, then the let builtin is what you want.
The let builtin is a clean way to perform integer arithmetic in Bash. You should read the output of help let to get a better picture of how it works.
The two lines you circled in your linked picture could be written as follows:
let guesses=10 # Set "guesses" to 10
let guesses-=1 # Decrement "guesses" by 1
Most proficient Bash hackers would write these operations in this manner. You could also use the post-decrement operator.
let guesses-- # Also decrement "guesses" by 1
This may look a little cleaner to you.

How to iterate over two strings simultaneously ksh

I'm using data that is returned by another person's ksh93 script in the format of a print to the standard output. Depending on the flag I give it, their script gives me the information I need for my code. It comes out like a list separated by spaces, such that a run of the program has the format of:
"1 3 4 7 8"
"First Third Fourth Seventh Eighth"
For what I'm working on, I need to be able to match the entries of each output, so that I could make the information print in the following format:
1:First
3:Third
4:Fourth
7:Seventh
8:Eighth
I need to do more than just printing with the data, I just need to be able to access the pairs of information in each of the strings. Even though the actual contents of the strings can be any number of values, the two strings I get from running the other script will always be the same length.
I'm wondering if there exists a way to iterate over both at the same time, something along the lines of:
str_1=$(other_script -f)
str_2=$(other_script -i)
for a,b in ${str_1},${str_2} ; do
print "${a}:${b}"
done
This obviously isn't the right syntax, but I have been unable to find a way to make it work. Is there a way to iterate over both at the same time?
I know I could convert them to arrays first then iterate by numerical element, but I would like to save the time of converting them if there's a way to iterate over both simultaneously.
Why do you think it is not quick to convert the strings to arrays?
For example:
`#!/bin/ksh93
set -u
set -A line1
string1="1 3 4 7 8"
line1+=( ${string1} )
set -A line2
string2="First Third Fourth Seventh Eighth"
line2+=( ${string2})
typeset -i num_elem_line1=${#line1[#]}
typeset -i num_elem_line2=${#line2[#]}
typeset -i loop_counter=0
if (( num_elem_line1 == num_elem_line2 ))
then
while (( loop_counter < num_elem_line1 ))
do
print "${line1[${loop_counter}]}:${line2[${loop_counter}]}"
(( loop_counter += 1 ))
done
fi
`
As with the other comments, not sure why an array would be out of the question, especially if you plan on referencing the individual elements more than once later in your code.
A sample script that assumes you want to maintain your str_1/str_2 variables as strings; we'll load into arrays for referencing individual elements:
$ cat testme
#!/bin/ksh
str_1="1 3 4 7 8"
str_2="First Third Fourth Seventh Eighth"
str1=( ${str_1} )
str2=( ${str_2} )
# at this point matching array elements have the same index (0..4) ...
echo "++++++++++ str1[index]=element"
for i in "${!str1[#]}"
do
echo "str1[${i}]=${str1[${i}]}"
done
echo "++++++++++ str2[index]=element"
for i in "${!str1[#]}"
do
echo "str2[${i}]=${str2[${i}]}"
done
# since matching array elements have the same index, we just need
# to loop through one set of indexes to allow us to access matching
# array elements at the same time ...
echo "++++++++++ str1:str2"
for i in "${!str1[#]}"
do
echo ${str1[${i}]}:${str2[${i}]}
done
echo "++++++++++"
And a run of the script:
$ testme
++++++++++ str1[index]=element
str1[0]=1
str1[1]=3
str1[2]=4
str1[3]=7
str1[4]=8
++++++++++ str2[index]=element
str2[0]=First
str2[1]=Third
str2[2]=Fourth
str2[3]=Seventh
str2[4]=Eighth
++++++++++ str1:str2
1:First
3:Third
4:Fourth
7:Seventh
8:Eighth
++++++++++

Is there a way to implement a counter in bash but for letters instead of numbers?

I'm working with an existing script which was written a bit messily. Setting up a loop with all of the spaghetti code could make a bigger headache than I want to deal with in the near term. Maybe when I have more time I can clean it up but for now, I'm just looking for a simple fix.
The script deals with virtual disks on a xen server. It reads multipath output and asks if particular LUNs should be formatted in any way based on specific criteria. However, rather than taking that disk path and inserting it, already formatted, into a configuration file, it simply presents every line in the format
'phy:/dev/mapper/UUID,xvd?,w',
UUID, of course, is an actual UUID.
The script actually presents each of the found LUNs in this format expecting the user to copy and paste them into the config file replacing each ? with a letter in sequence. This is tedious at best.
There are several ways to increment a number in bash. Among others:
var=$((var+1))
((var+=1))
((var++))
Is there a way to do the same with characters which doesn't involve looping over the entire alphabet such that I could easily "increment" the disk assignment from xvda to xvdb, etc?
To do an "increment" on a letter, define the function:
incr() { LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'$1")+1))")"; }
Now, observe:
$ echo $(incr a)
b
$ echo $(incr b)
c
$ echo $(incr c)
d
Because, this increments up through ASCII, incr z becomes {.
How it works
The first step is to convert a letter to its ASCII numeric value. For example, a is 97:
$ printf '%d' "'a"
97
The next step is to increment that:
$ echo "$((97+1))"
98
Or:
$ echo "$(($(printf '%d' "'a")+1))"
98
The last step is convert the new incremented number back to a letter:
$ LC_CTYPE=C printf "\\$(printf '%03o' "98")"
b
Or:
$ LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'a")+1))")"
b
Alternative
With bash, we can define an associative array to hold the next character:
$ declare -A Incr; last=a; for next in {b..z}; do Incr[$last]=$next; last=$next; done; Incr[z]=a
Or, if you prefer code spread out over multiple lines:
declare -A Incr
last=a
for next in {b..z}
do
Incr[$last]=$next
last=$next
done
Incr[z]=a
With this array, characters can be incremented via:
$ echo "${Incr[a]}"
b
$ echo "${Incr[b]}"
c
$ echo "${Incr[c]}"
d
In this version, the increment of z loops back to a:
$ echo "${Incr[z]}"
a
How about an array with entries A-Z assigned to indexes 1-26?
IFS=':' read -r -a alpharray <<< ":A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
This has 1=A, 2=B, etc. If you want 0=A, 1=B, and so on, remove the first colon.
IFS=':' read -r -a alpharray <<< "A:B:C:D:E:F:G:H:I:J:K:L:M:N:O:P:Q:R:S:T:U:V:W:X:Y:Z"
Then later, where you actually need the letter;
var=$((var+1))
'phy:/dev/mapper/UUID,xvd${alpharray[$var]},w',
The only problem is that if you end up running past 26 letters, you'll start getting blanks returned from the array.
Use a Bash 4 Range
You can use a Bash 4 feature that lets you specify a range within a sequence expression. For example:
for letter in {a..z}; do
echo "phy:/dev/mapper/UUID,xvd${letter},w"
done
See also Ranges in the Bash Wiki.
Here's a function that will return the next letter in the range a-z. An input of 'z' returns 'a'.
nextl(){
((num=(36#$(printf '%c' $1)-9) % 26+97));
printf '%b\n' '\x'$(printf "%x" $num);
}
It treats the first letter of the input as a base 36 integer, subtracts 9, and returns the character whose ordinal number is 'a' plus that value mod 26.
Use Jot
While the Bash range option uses built-ins, you can also use a utility like the BSD jot utility. This is available on macOS by default, but your mileage may vary on Linux systems. For example, you'll need to install athena-jot on Debian.
More Loops
One trick here is to pre-populate a Bash array and then use an index variable to grab your desired output from the array. For example:
letters=( "" $(jot -w %c 26 a) )
for idx in 1 26; do
echo ${letters[$idx]}
done
A Loop-Free Alternative
Note that you don't have to increment the counter in a loop. You can do it other ways, too. Consider the following, which will increment any letter passed to the function without having to prepopulate an array:
increment_var () {
local new_var=$(jot -nw %c 2 "$1" | tail -1)
if [[ "$new_var" == "{" ]]; then
echo "Error: You can't increment past 'z'" >&2
exit 1
fi
echo -n "$new_var"
}
var="c"
var=$(increment_var "$var")
echo "$var"
This is probably closer to what the OP wants, but it certainly seems more complex and less elegant than the original loop recommended elsewhere. However, your mileage may vary, and it's good to have options!

How to optimize the following script

This script runs a query to get a list of dates and runs two other queries for these dates.
Then compares which one is the smaller of the numbers and multiplies it by 2.
Then writes to file and sums them.
Please suggest improvements. Also checks for 0 numbers.
#!/bin/bash
1>output.txt
today=$(date +"%Y%m%d")
FirstOfTheMonth=$(date -d "--$(($(date +%-d)-1)) day" `enter code here`+"%Y%m%d")
echo "XXXX activity report on daily and cumulative monthly `enter code here`basis "
#query that outputs dates to a file
SQL query > list
#for each date I run 2 queries
for i in `cat list`;do
a1=SQL query;
b1=SQL query;
# I compare to find out which one is the smaller number and `enter code here`multiply it by 2
buy=${a1#-}
sell=${b1#-}
echo "XXX report for $yesterday month = $i "
echo "Buy $buy"
echo "Sell $sell"
if [ "$buy" -lt "$sell" ];
then DayNumber=$[buy * 2];
else DayNumber=$[sell * 2];
fi;
#I write all the numbers to a file since I have to sum them
MonthNumber=`awk '{ sum += $1 } END { print sum }' `enter `enter code here`code here`DayNumber$i`
echo "Day Number $DayNumber"
echo "$DayNumber$i $MonthNumber$1 $yesterday" >> DayNumber$i
echo "Day Number since $FirstOfTheMonth $MonthNumber$1"
echo ---------------------------------------------------------------------------------------
done
/usr/bin/mail -s "XXXX report $today" xxx#xxxx.com < `enter code here`output.txt
You are most likely getting down votes, do to the size of the code, lack of clarity on what you want, not showing effort to get that result on your own, and very rudimentary mistakes in code readability that could be fixed with a web search for a tutorial. Google has a shell style guide that you should read.
If you are redirecting stdout to a file, you are probably running autonomously. Which means you should redirect stderr too.
exec &>output.txt
First of the month-- not sure why you don't just do this
FirstOfTheMonth=$(date +%Y%m1)
For Pete's sake, indent! This makes me want to beat scripters. Also, don't use i unless it's a very small loop. Use a variable that means something.
while read -rd' ' month; do
some commands
if [[ $buy -lt $sell ]]; then
do this thing here
fi
done
Bash isn't semi-colon terminated, you don't need one at the end of a line. Use the interan [[ ]] over the externam [ ]. Don't quote a variable (e.g. "$buy") to do a numeric comparison (e.g. -lt). Keep your then on the same line as if-- it servers no other purpose than to make it more readable.

Resources