What is the problem with my code for multiplying two numbers in a different way using Bash? - bash

This is my first time using Bash, so bear with me. I'm using Git Bash in Windows for a college project, trying to rewrite some C code that provides an alternate way of multiplying two numbers "a" and "b" to produce "c". This is what I came up with:
#!/bin/bash
declare -i a
declare -i b
declare -i c=0
declare -i i=0 # not sure if i have to initialize this as 0?
echo "Please enter a number: "
read a
echo "Please enter a number: "
read b
for i in {1..b}
do
let "c += a"
done
echo "$a x $b = $c"
I think part of the problem is in the for loop, that it only executes once. This is my first time using Bash, and if anyone could find the fault in my knowledge, that would be all I need.

There are problems with your loop:
You can't use {1..b}. Even if you had {1..$b} it wouldn't work because you would need an eval. It's easiest to use the seq command instead.
Your let syntax is incorrect.
Try this:
for i in $(seq 1 $b)
do
let c+=$a
done
Also, it's not necessary to declare or initialise i.

for i in {1..b}
won't work, because 'b' isn't interpreted as a variable but a character to iterate to.
For instance {a..c} expands to a b c.
To make the brace expansion work:
for i in $(eval echo "{1..$b}")
The let "c += a" won't work either.
let c+=$a might work, but I like ((c+=a)) better.
Another way is this:
for ((i = 1; i <= b; i++))
do
((c += a))
done
(might need to put #!/bin/bash at the top of your script, because sh does less than bash.)

Of course, bash already has multiplication, but I guess you knew that ...
If the absence of "seq" is your issue, you can replace it with something more portable, like
c=0
# Print an endless sequence of lines
yes |
# Only take the first $b lines
head -n "$b" |
# Add line number as prefix for each line
nl |
# Read the numbers into i, and the rest of the line into a dummy variable
while read i dummy; do
# Update the value of c: add line number
c=`expr "$c" + "$i"`
echo "$c"
done |
# Read the last number from the while loop
tail -n 1
This should be portable to any Bourne-compatible shell. The while ... echo ... done | tail -n 1 trick is necessary only if the value of c is not exported outside the while loop, as is the case in some, but not all, Bource shells.
You can implement a seq replacement with a Perl one-liner, but then you might as well write all of this in Perl (or awk, or Python, or what have you).

Related

continue <n> not skipping <n> iterations forward in shell script

I have created a hex to ASCII converter for strings in bash. The application I'm on changes characters (anything but [0-9],[A-Z],[a-z]) , in a string to its corresponding %hexadecimal. Eg: / changes to %2F in a string
I want to retain the ASCII characters as it is. Below is my code:
NAME=%2fhome%40%21%23
C_NAME=""
for (( i=0; i<${#NAME}; i++ )); do
CHK=$(echo "{NAME:$i:1}" | grep -v "\%" &> /dev/null;echo $?)
if [[ ${CHK} -eq 0 ]]; then
C_NAME=`echo "$C_NAME${NAME:$i:1}"`
else
HEX=`echo "${NAME:$i:3}" | sed "s/%//"`
C_NAME=`echo -n "$C_NAME";printf "\x$HEX"`
continue 2
fi
done
echo "$C_NAME"
OUTPUT:
/2fhome#40!21#23
EXPECTED:
/home#!#
So basically the conversion is happening, but not in place. Its retaining the hex values as well, which tells me the continue 2 statement is probably not working as I expect in my code. Any workarounds please.
You only have one loop so I assume you expected that continue 2 skips the current and next iteration of the current loop, however, the documentation help continue clearly states
continue [n]
[...]
If N is specified, resumes the Nth enclosing loop.
There is no built-in to skip the current and also the next iteration of the current loop, but in your case you can use (( i += 2 )) instead of continue 2.
Using the structure of your script with some simplifications and corrections:
#!/bin/bash
name=%2fhome%40%21%23
c_name=""
for (( i=0; i<${#name}; i++ )); do
c=${name:i:1}
if [[ $c != % ]]; then
c_name=$c_name$c
else
hex=${name:i+1:2}
printf -v c_name "%s\x$hex" "$c_name"
(( i += 2 )) # stolen from Dudi Boy's answer
fi
done
echo "$c_name"
Always use lower case or mixed case variables to avoid the chance of name collisions with shell or environment variables
Always use $() instead of backticks
Most of the echo commands you use aren't necessary
You can avoid using sed and grep
Variables should never be included in the format string of printf but it can't be avoided easily here (you could use echo -e "\x$hex" instead though)
You can do math inside parameter expansions
% doesn't need to be escaped in your grep command
You could eliminate the $hex variable if you used its value directly:
printf -v c_name "%s\x${name:i+1:2}" "$c_name"
I really enjoyed your exercise and decided to solve it with awk (my current study).
Hope you like it as well.
cat script.awk
BEGIN {RS = "%[[:xdigit:]]+"} { # redefine record separtor to RegEx (gawk specific)
decNum = strtonum("0x"substr(RT, 2)); # remove prefix # from record separator, convert hex num to dec
outputStr = outputStr""$0""sprintf("%c", decNum); # reconstruct output string
}
END {print outputStr}
The output
echo %2fhome%40%21%23 |awk -f script.awk
/home#!#

Change a Number inside a Variable

I have the following problem: (Its about dates)
The user will set the following variable.
variable1=33_2016
now I Somehow want to to automatically set a second variable which sets the "33" +1
that I get
variable2=34_2016
Thanks for any advice.
My first choice would be to break the first variable apart with read, then put the (updated) pieces back together.
IFS=_ read f1 f2 <<< "$variable1"
# Option 1
variable2=$((f1 + 1))_$f2
# Option 2
printf -v variable2 '%s_%s" "$((f1 + 1))" "$f2"
You can also use parameter expansion to do the parsing:
f1=${variable%_*}
f2=${variable#*_}
You can also use a regular expression, which is more readable for parsing but much longer to put the pieces back together (BASH_REMATCH could use a shorter synonym).
[[ $variable1 =~ (.*)_(.*) ]] &&
f1=$((${BASH_REMATCH[1]}+1)) f2=${BASH_REMATCH[2]}
The first and third options also allow the possibility of working with an array:
# With read -a
IFS=_ read -a f <<< "$variable1"
variable2=$(IFS=_; echo "${f[*]}")
# With regular expression
[[ $variable1 =~ (.*)_(.*) ]]
variable2=$(IFS=_; echo "${BASH_REMATCH[*]:1:2}")
You can use awk:
awk 'BEGIN{FS=OFS="_"}{$1+=1}1' <<< "${variable1}"
While this needs an external process to spawn (a bit slower) it's easier to read/write. Decide for yourself what is more important for you here.
To store the return value in a variable, use command substitution:
variable2=$(awk 'BEGIN{FS=OFS="_"}{$1+=1}1' <<< "${variable1}")
You can do somewhat the same thing with parameter expansion with substring substitution, e.g.
$ v1=33_2016
$ v2=${v1/${v1%_*}/$((${v1%_*}+1))}
$ echo $v2
34_2016
It's six to one, a half-dozen to another.

BASH - Refer to parameters using variable

I was trying to make a small script that calculates a binary number to decimal. It works like this:
-Gets '1' or '0' digits as SEPARATE parameters. e.g. "./bin2dec 1 0 0 0 1 1 1".
-For each parameter digit: if it is '1', multiplies it with the corresponding power of 2 (in the above case, the most left '1' will be 64), then adds it in a 'sum' variable.
Here's the code (it is wrong in the noted point):
#!/bin/bash
p=$((2**($#-1))) #Finds the power of two for the first parameter.
sum=0 #The sum variable, to be used for adding the powers of two in it.
for (( i=1; i<=$#; i++ )) #Counts all the way from 1, to the total number of parameters.
do
if [ $i -eq 1 ] # *THIS IS THE WRONG POINT* If parameter content equals '1'...
then
sum=$(($sum+$p)) #...add the current power of two in 'sum'.
fi
p=$(($p/2)) #Divides the power with 2, so to be used on the next parameter.
done
echo $sum #When finished show the 'sum' content, which is supposed to be the decimal equivalent.
My question is in the noted point (line #10, including blank lines). There, I’m trying to check if EACH parameter's content equals 1. How can I do this using a variable?
For example, $1 is the first parameter, $2 is the 2nd and so on. I want it to be like $i, where 'i' is the variable that is increased by one each time so that it matches the next parameter.
Among other things, I tried this: '$(echo "$"$i)' but didn't work.
I know my question is complicated and I tried hard to make it as clear as I could.
Any help?
How about this?
#!/usr/bin/bash
res=0
while [[ $# -gt 0 ]]
do
res=$(($res * 2 + $1))
shift
done
echo $res
$ ./bin2dec 1 0 0 1
9
shift is a command that moves every parameter passed to the script to the left, decreasing it's value by 1, so that the value of $2 is now at $1 after a shift. You can actually a number with shift as well, to indicate how far to shift the variables, so shift is like shift 1, and shift 2 would give $1 the value that used to be at $3.
How about this one?
#!/bin/bash
p=$((2**$#))
while(($#)); do
((sum+=$1*(p/=2)))
shift
done
echo "$sum"
or this one (that goes in the other direction):
#!/bin/bash
while(($#)); do
((sum=2*sum+$1))
shift
done
echo "$sum"
Note that there are no error checkings.
Please consider fedorqui's comment: use echo $((2# binary number )) to convert from binary to decimal.
Also note that this will overflow easily if you give too many arguments.
If you want something that doesn't overflow, consider using dc:
dc <<< "2i101010p"
(setting input radix to 2 with 2i and putting 101010 on the stack and printing it).
Or if you like bc better:
bc <<< "ibase=2;101010"
Note that these need the binary number to be entered as one argument, and not as you wanted with all digits separated. If you really need all digits to be separated, you could also use these funny methods:
Pure Bash.
#!/bin/bash
echo $((2#$(printf '%s' "$#")))
With dc.
#!/bin/bash
dc <<< "2i$(printf '%s' "$#")p"
With bc.
#!/bin/bash
bc <<< "ibase=2;$(printf '%s' "$#")"
Abusing IFS, with dc.
#!/bin/bash
( IFS=; echo "2i$*p" ) | dc
Abusing IFS, with bc.
#!/bin/bash
( IFS=; echo "ibase=2;$*" ) | bc
So we still haven't answered your question (the one in your comment of the OP): And to find out if and how can we refer to a parameter using a variable. It's done in Bash with indirect expansion. You can read about it in the Shell Parameter Expansion section of the Bash reference manual. The best thing is to show you in a terminal:
$ a="hello"
$ b=a
$ echo "$b"
a
$ echo "${!b}"
hello
neat, eh? It's similar with positional parameters:
$ set param1 param2 param3
$ echo "$1, $2, $3"
param1, param2, param3
$ i=2
$ echo "$i"
2
$ echo "${!i}"
param2
Hope this helps!
To reference variables indirectly by name, use ${!name}:
i=2
echo "${!i} is now equivalent to $2"
Try this one also:
#!/bin/bash -
( IFS=''; echo $((2#$*)) )

Variable substitution in a for-loop using {$var}

I'm very new to bash scripting and I'm trying to practice by making this little script that simply asks for a range of numbers. I would enter ex. 5..20 and it should print the range, however - it just echo's back whatever I enter ("5..20" in this example) and does not expand the variable. Can someone tell me what I'm doing wrong?
Script:
echo -n "Enter range of number to display using 0..10 format: "
read range
function func_printrage
{
for n in {$range}; do
echo $n
done
}
func_printrange
Brace expansion in bash does not expand parameters (unlike zsh)
You can get around this through the use of eval and command substitution $()
eval is evil because you need to sanitize your input otherwise people can enter ranges like rm -rf /; and eval will run that
Don't use the function keyword, it is not POSIX and has been deprecated
use read's -p flag instead of echo
However, for learning purposes, this is how you would do it:
read -p "Enter range of number to display using 0..10 format: " range
func_printrange()
{
for n in $(eval echo {$range}); do
echo $n
done
}
func_printrange
Note: In this case the use of eval is OK because you are only echo'ing the range
One way is to use eval,
crude example,
for i in $(eval echo {0..$range}); do echo $i; done
the other way is to use bash's C style for loop
for((i=1;i<=20;i++))
do
...
done
And the last one is more faster than first (for example if you have $range > 1 000 000)
One way to get around the lack of expansion, and skip the issues with eval is to use command substitution and seq.
Reworked function (also avoids globals):
function func_print_range
{
for n in $(seq $1 $2); do
echo $n
done
}
func_print_range $start $end
Use ${} for variable expansion. In your case, it would be ${range}. You left off the $ in ${}, which is used for variable expansion and substitution.

SED, using variables and in with an array

What I am trying to do is run the sed on multiple files in the directory Server_Upload, using variables:
AB${count}
Corresponds, to some variables I made that look like:
echo " AB1 = 2010-10-09Three "
echo " AB2 = 2009-3-09Foo "
echo " AB3 = Bar "
And these correspond to each line which contains a word in master.ta, that needs changing in all the text files in Server_Upload.
If you get what I mean... great, I have tried to explain it the best I can, but if you are still miffed I'll give it another go as I found it really hard to convey what I mean.
cd Server_Upload
for fl in *.UP; do
mv $fl $fl.old
done
count=1
saveIFS="$IFS"
IFS=$'\n'
array=($(<master.ta))
IFS="$saveIFS"
for i in "${array[#]}"
do
sed "s/$i/AB${count}/g" $fl.old > $fl
(( count++ ))
done
It runs, doesn't give me any errors, but it doesn't do what I want, so any ideas?
Your loop should look like this:
while read i
do
sed "s/$i/AB${count}/g" $fl.old > $fl
(( count ++ ))
done < master.ta
I don't see a reason to use an array or something similar. Does this work for you?
It's not exactly clear to me what you are trying to do, but I believe you want something like:
(untested)
do
eval repl=\$AB${count}
...
If you have a variable $AB3, and a variable $count, $AB${count} is the concatenation of $AB and $count (so if $AB is empty, it is the same as $count). You need to use eval to get the value of $AB3.
It looks like your sed command is dependent on $fl from inside the first for loop, even though the sed line is outside the for loop. If you're on a system where sed does in-place editing (the -i option), you might do:
count=1
while read i
do
sed -i'.old' -e "s/$i/AB${count}/g" Server_Upload/*.UP
(( count ++ ))
done < master.ta
(This is the entire script, which incorporates Patrick's answer, as well.) This should substitute the text ABn for every occurrence of the text of the nth line of master.ta in any *.UP file.
Does it help if you move the first done statement from where it is to after the second done?

Resources