Linux, improved cal, shell programming - linux

I am working with cal for a homework assignment and i am stuck on one point.
It is unlikely that anyone is really interested in getting a gregorian
calendar for a year in the first century, a time when the gregorian
calendar didn't even exist. Use the "windowing" strategy to allow your
new cal to handle years that are not the full 4 digit year. If the
year is in the range of 0 <= year <= 50, assume the year is really
2000-2050. If the year is in the range 51 <= year <= 99, assume the
year is really 1951-1999.
file named improvedcal.sh
call the shell with sh improvedcal.sh 1 2011 for example
code
case $# in
# rando stuff
*) m=$1; y=$2 ; # 2 ags: month and year
if ((y >= 0 && y <= 50)); then
y+=2000
elif ((y >= 51 && y <= 99)); then
y+=1900
fi;;
esac
case $m in
jan*|Jan*) m=1 ;;
feb*|Feb*) m=2 ;;
mar*|Mar*) m=3 ;;
apr*|Apr*) m=4 ;;
may*|May*) m=5 ;;
jun*|Jun*) m=6 ;;
jul*|Jul*) m=7 ;;
aug*|Aug*) m=8 ;;
sep*|Sep*) m=9 ;;
oct*|Oct*) m=10 ;;
nov*|Nov*) m=11 ;;
dec*|Dec*) m=12 ;;
[1-9]|10|11|12) ;; # numeric month
0[1-9]|010|011|012) ;; # numeric month
# *) y=$m; m="" ;; # plain year
esac
/usr/bin/cal $m $y # run cal with new inputs
But this is not working for some reason does anyone have any pointers for me?
It just skips right over this part for some reason.

If you don't declare a variable and directly assign to it, then it's either a string (var=stuff) or an array (var=(element0 element1 element2)). Since y is a string, y+=2000 appends the string 2000 to the value.
You can declare y as an integer variable, then the += operator will perform an addition.
declare -i y=$2
if ((y >= 0 && y <= 50)); then
y+=2000
elif ((y >= 51 && y <= 99)); then
y+=1900
fi
Another way is to use the += operator inside an arithmetic expression:
y=$2
if ((y >= 0 && y <= 50)); then
((y+=2000))
elif ((y >= 51 && y <= 99)); then
((y+=1900))
fi
Or you can perform the arithmetic operation and assign the result:
y=$2
if ((y >= 0 && y <= 50)); then
y=$((y+2000))
elif ((y >= 51 && y <= 99)); then
y=$((y+1900))
fi
You can write all of this in a single arithmetic expression using the ? … : conditional operator:
y=$2
if ((y >= 0)); then ((y <= 50 ? y += 2000 : y <= 99 ? y+=1900 : 0)); fi

Try running your script with bash -xv, it will help you understand what is happening.
Read also Bash programming intro

Related

Time difference in shell (hour)

I'm trying to calculate time difference stored inside of two variables inside of a shell script, I'm observing the following pattern:
hhmm -> 0950
so:
time1=1333
time2=0950
Now I need to calculate the difference in time between time1 and time2, as for now I have tried:
deltaTime=$(($time1-$time2))
but I'm facing the following error message
1333-0950: value too great for base (error token is "0950")
I'm expecting as a result: $deltaTime=0343
Unfortunately, I am strictly bound to use this time pattern. I have already researched for a solution online, some of them propose to use date -d... but I couldn't get it to work :(
Your approach has two issues.
First issue: bash recognizes numbers with leading zeroes as octal. You can force base10 by adding 10# prefix.
Second issue: it is incorrect to consider strings in hhmm format as numbers and substract them. e.g. 1333-950=383 but difference between 09:50 and 13:33 is 3 hours and 43 minutes. You should convert string values to common units, e.g. to minutes, substract them and convert back to hhmm format.
time1=1333
time2=0950
str2min()
{
printf "%u" $((10#${1%??} * 60 + 10#${1#??}))
}
min2str()
{
printf "%02u%02u" $(($1 / 60)) $(($1 % 60))
}
time1m=$(str2min $time1)
time2m=$(str2min $time2)
timediff=$(($time1m - $time2m))
deltaTime=$(min2str $timediff)
You could use this implementation maybe?
#!/usr/bin/env bash
diff_hhmm() {
local -r from=$1
local -i from_hh=10#${from:0:2} # skip 0 chars, read 2 chars (`${from:0:2}`) using base 10 (`10#`)
local -ri from_mm=10#${from:2:2} # skip 2 chars, read 2 chars (`${from:0:2}`) using base 10 (`10#`)
local -r upto=$2
local -ri upto_hh=10#${upto:0:2}
local -ri upto_mm=10#${upto:2:2}
local -i diff_hh
local -i diff_mm
# Compute difference in minutes
(( diff_mm = from_mm - upto_mm ))
# If it's negative, we've "breached" into the previous hour, so adjust
# the `diff_mm` value to be modulo 60 and compensate the `from_hh` var
# to reflect that we've already subtracted some of the minutes there.
if (( diff_mm < 0 )); then
(( diff_mm += 60 ))
(( from_hh -= 1 ))
fi
# Compute difference in hours
(( diff_hh = from_hh - upto_hh ))
# Ensure the result is modulo 24, the number of hours in a day.
if (( diff_hh < 0 )); then
(( diff_hh += 24 ))
fi
# Print the values with 0-padding if necessary.
printf '%02d%02d\n' "$diff_hh" "$diff_mm"
}
$ diff_hhmm 1333 0950
0343
$ diff_hhmm 0733 0950
2143
$ diff_hhmm 0733 0930
2203
Or an even shorter implementation using a big arithmetic compound command ((( ... )) ) and inlining some variables:
diff_hhmm_terse() {
local -i diff_hh diff_mm
((
diff_mm = 10#${1:2:2} - 10#${2:2:2},
diff_hh = 10#${1:0:2} - 10#${2:0:2},
diff_hh -= diff_mm < 0 ? 1 : 0,
diff_mm += diff_mm < 0 ? 60 : 0,
diff_hh += diff_hh < 0 ? 24 : 0
))
printf '%02d%02d\n' "$diff_hh" "$diff_mm"
}
Do you have the possibility to drop the leading zero?
As you can see from my prompt:
Prompt> echo $((1333-0950))
-bash: 1333-0950: value too great for base (error token is "0950")
Prompt> echo $((1333-950))
383
Other proposal:
date '+%s'
Let me give you some examples:
date '+%s'
1662357975
... (after some time)
date '+%s'
1662458180
=>
echo $((1662458180-1662357975))
100205 (amount of seconds)
=>
echo $(((1662458180-1662357975)/3600))
27 (amount of hours)
This bash one-liner may be used if time difference is not negative (that is, time1 >= time2):
printf '%04d\n' $(( 10#$time1 - 10#$time2 - (10#${time1: -2} < 10#${time2: -2} ? 40 : 0) ))

"Attempted assignment to a non-variable" in bash

I'm new to Bash and I've been having issues with creating a script. What this script does is take numbers and add them to a total. However, I can't get total to work.It constantly claims that total is a non-variable despite it being assigned earlier in the program.
error message (8 is an example number being entered)
./adder: line 16: 0 = 0 + 8: attempted assignment to non-variable (error token is "= 0 + 8")
#!/bin/bash
clear
total=0
count=0
while [[ $choice != 0 ]]; do
echo Please enter a number or 0 to quit
read choice
if [[ $choice != 0 ]];
then
$(($total = $total + $choice))
$(($count = $count + 1))
echo Total is $total
echo
echo Total is derived from $count numbers
fi
done
exit 0
Get rid of some of the dollar signs in front of the variable names. They're optional inside of an arithmetic context, which is what ((...)) is. On the left-hand side of an assignment they're not just optional, they're forbidden, because = needs the variable name on the left rather than its value.
Also $((...)) should be plain ((...)) without the leading dollar sign. The dollar sign will capture the result of the expression and try to run it as a command. It'll try to run a command named 0 or 5 or whatever the computed value is.
You can write:
((total = $total + $choice))
((count = $count + 1))
or:
((total = total + choice))
((count = count + 1))
or even:
((total += choice))
((count += 1))

Shell for loop, stopping at declaration

I'm trying to write a for loop that goes from 1 to 10, then calculates ( 1 through 10 mod 5) + 2. After that I want to display it like this (1 to 10 mod 5) + 2 = answer. However i'm getting an error at the beginning of the loop which is a syntax error.
for (( i = 0; i <= 10; i++)); do
calculate=(i % 5) + 2
echo ("("i "% 5) + 2" calculate)
done
Try these changes:
calculate=$(( i % 5 + 2 ))
# $(( ... )) is the shell's way to do arithmetic
echo "($i % 5) + 2 = " $calculate
# $x is a way to refer to the value of variable x
# (also inside a double-quoted string)
The for loop header is actually OK.

Modulus function in bash shell script

for ((i=0; i<lenPT; i++)) do
if [[ $(($lenPT % 2)) == 0]] then
P[i] = "$((K [0] * arrT[i] + K[2] * arrT[i+1]))"
else
P[i] = "$((K[1]*arrT[i-1]+K[3]*arrT[i]))"
fi
done
I got errors saying that "syntax error in conditional expression" and "syntax error near 'then'". What is the error in my conditional statement?
Space matters, see Barmar's answer. You also need a semicolon after the [[ ]] conditional if you want to put then on the same line.
Instead of the cumbersome [[ $(( )) ... ]] combination, you can use the (Bash-only) (( )) conditional, the contents of which are evaluated in an arithmetic context:
if ((lenPT % 2 == 0)); then
You don't even need $lenPT in this construct, lenPT is enough (see Conditional Constructs in the manual for details).
Since the exit status of ((...)) is 1 (not successful) if the expression evaluates to 0, and 0 (successful) otherwise, you could swap the branches and shorten the condition a little:
if ((lenPT % 2)); then
P[i]=$((K[1] * arrT[i-1] + K[3] * arrT[i]))
else
P[i]=$((K[0] * arrT[i] + K[2] * arrT[i+1]))
fi
You need a space before ]].
if [[ $(($lenPT % 2)) == 0 ]]; then
The if ... else that depends on the value of $lenPT is needless, since $lenPT never changes within the loop. The assignments are so similar the if logic can be replaced with arithmetic. Example:
n=$((lenPT % 2))
for ((i=0; i<lenPT; i++))
do
P[i]="$((K[n] * arrT[i-n] + K[2+n] * arrT[i+1-n]))"
done

compare value to multiple ranges in bash script and set other variables based on that match

I am setting up a script in bash that in one part takes in an ip and based on that ip determines other network settings.
Here is what I have
if[ 1 <= $net <= 5]
then
network=10.1.0.0
netmask=255.255.248.0
gateway=10.1.0.1
elif[16 <= $net <= 23]
then
network=10.1.16.0
netmask=255.255.248.0
gateway=10.1.16.1
elif[24 <= $net <= 31]
then
network=10.1.24.0
netmask=255.255.248.0
gateway=10.1.24.1
elif[32 <= $net <= 39]
then
network=10.1.32.0
netmask=255.255.248.0
gateway=10.1.32.1
.............. (it continues like that for a while.
Is there a better way to do this using case or something else?
I'm also not sure I am doing the IF statements correctly.
Thanks in advance.
EDIT: I guess I forgot to mention that the IP address is sourced from a "read" input that is then cut to get the value.
IE user enters 10.1.40.207 wish is then piped to cut to get the $net=40
Set the values directly:
network=10.1.$(($net/8*8)).0
netmask=255.255.248.0
gateway=10.1.$(($net/8*8)).1
I've posted an explanation in steps on how I simplified your code into the above in 4 steps here: http://paste.ubuntu.com/5585785/.
The "magic" is in the maths. A little maths prevents the need for any conditions. No case, no ifs, no buts! :)
If the values can be calculated directly (as shown in other answer), that eliminates a lot of scripting; however, you may still need to check for valid/invalid input; but that also could be done w/o using an extended if/elif/else/fi.
But to answer the question, here's an if/else that can check for ranges of numbers (I don't think a 'case' would simplify matters):
#!/bin/bash
arg=$1
if (( 1 <= arg && arg <= 5 )) ; then
echo "from 1-5: $arg"
elif (( 16 <= arg && arg <= 23 )) ; then
echo "from 16-23: : $arg"
elif (( 24 <= arg && arg <= 31 )) ; then
echo "from 24-31: : $arg"
else
echo "invalid : $arg"
fi
The main point is that ((...)) is used for arithmetic evaluation, not [..]; it's equivalent to let and returns true/false.
Sample output:
$ ./scr 1
from 1-5: 1
$ ./scr 7
invalid : 7
$ ./scr 23
from 16-24: : 23

Resources