I want my calendar to correctly handle months with leading zeros
for example: "cal 01" or "cal 01 2012"
How do I write the code to make my calendar to correctly handle months with leading zeros?
This is my code so far:
$ cat cal
#cal: nicer interface to /usr/bin/cal
case $# in
0) set 'data';m=$2; y=$6 ;; # no argu: use today
1) m=$1; set 'data'; y=$6 ;; # 1 rg: use this year
*) m=$1; y=$2 ;; # 2 ags: month and year
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
*) y=$m; m="" ;; # plain year
esac
/usr/bin/cal $m $y # run the real one
$
You can do multiple regex matching in your case statement, i.e.
case $m in
01|1|jan*|Jan*) m=1 ;;
02|2|feb*|Feb*) m=2 ;;
....
Else, you could use shell parameter substitution to remove any leading 0's, i.e.
# as a way to demonstrate param sub on $1 etc, load values to $1 and $2
set -- 01 02
echo ${1#0}
echo ${2#0}
# output
1
2
Edit
For your follow-up question
Example, the current month is November, 2005, if you run "cal 01", you should print out the calendar of Jan. 2006
Try this:
# if the month input is less than the current month, assume the next year
if (( ${y:-0} == 0 && m < $(/bin/date +%m) )) ; then
y=$(/bin/date +%Y)
((y++))
fi
${y:-0} is one of several parameter checking syntaxs provided by most shells that allows a default value to be substituted if the var value is completely unset (not set at all) or = "". So in this case, if y wasn't set by the command line, it will appear as 0 in this evaluation, allowing the && section to be be executed to test the month, etc.
You'll need to extend your case $# processing to allow for 1 argument, that is assumed to be a month value.
I hope this helps.
Related
This question already has answers here:
How do I parse command line arguments in Bash?
(40 answers)
Closed 3 years ago.
I am very new to shell scripting and using CLI all together. However, I want to create a simple program that can collect arguments from a user.
Optimally, I want my shell script to get a network name and instance name from the user, and assign the values to variables.
I know that $# can be used to gather arguments, but are there any other ways? I often notice when I make a command, there is, for example, something like this: $create --instance_name NAME . Can I gather an argument by using -- to specify the parameter? If so, here is an example of the command:
$collect_variable.sh --network NETWORK_NAME --instance_name INSTANCE_ID
Once again, thank you for any help. I am very new to stack overflow and unix all together, and any help is appreciated.
You can using a variable to assign to for example:
while getopts "x:y:z:" res
do
case "$res" in
x ) paramX="$OPTARG" ;;
y ) paramY="$OPTARG" ;;
z ) paramX="$OPTARG" ;;
? ) echo "not a valid value"
esac
done
Check the rest of the links that other StackO users sent, they provide great examples!
Hope that can help you solve your issue.
Peace!
There are lots of ways of gathering arguments. You can name their position, e.g. $1 is the first argument and $14 is the fourteenth.
You can refer to the argument list like $# (this preserves spacing) or like $* (this collapses spacing):
test_args() {
echo '# as "$#"'
for argument in "$#"; do
echo "$argument"
done
echo '# as "$*"'
for argument in "$*"; do
echo "$argument"
done
echo '# as $*'
for argument in $*; do
echo "$argument"
done
}
$ test_args "1 2" three four
# as "$#"
1 2
three
four
# as "$*"
1 2 three four
# as $*
1
2
three
four
Since you're exclusively using long options separated from their arguments by spaces,
while [ "$#" -gt 0 ]; do
case "$1" in
( --network ) NETWORK="$2"; shift ;;
( --network=?* ) NETWORK="${1#*=}" ;;
( --instance_name ) INSTANCE_NAME="$2"; shift ;;
( --instance_name=?* ) INSTANCE_NAME="${1#*=}" ;;
( -* ) echo "Illegal option -- $1" >&2; exit 2 ;;
( * ) break ;; # perhaps non-options are used later
esac
shift
done
This loops on each option and parses it. There are two conditions for each option, which lets us handle when the arguments are spaced from the options (--network bobnet) or when they're assigned to the options (--network=bobnet). Spaced means we need to refer to the next argument (thus the extra shift) while assigned means we need to use parameter subsitution to remove the characters at the front of the string up until (and including) the first =.
The final shift pulls the first item off of the argument list so we can operate on the next one. Shifting separately for the two-argument clauses (rather than ending with shift 2) also allows for binary options like --verbose, which doesn't contain an argument.
You can also use a while getopts loop to support a mix of short and long arguments; see my more detailed post on getopts.
I am trying to convert a numerical date to an alphabetical date in BASH without using the date function (legacy BASH environment).
What I am trying to do is have a case statement of each month, set it to a variable (ConvDate) based on the input TestDate and then return it in the OutputDate variable. However, when I test this, it prints the date rearranged without adding the string to it:
Sample in:
10/25/18
Sample out as of now:
25//18
Some thoughts about your code:
in the branches of the case command, you are attempting to execute the Jan command, when you need to assign that string to a variable.
you need to parse the datestring before the case command, since you clearly need to know the month number first.
braces are not quotes: be careful with how you quote your variables (i.e. always quote them) -- this will not hurt you here, unless you actually get the input data from the user.
you are handling the month number as a string here. If, as your script progresses, you start to handle it as a number, be careful that 08 and 09 are invalid octal numbers and bash will be happy to let you know:
$ m=08; echo $(($m+1))
bash: 08: value too great for base (error token is "08")
revised
TestDate=09/25/18
IFS=/ read m d y <<INPUT
$TestDate
INPUT
case $m in
01) Mon=Jan ;;
02) Mon=Feb ;;
03) Mon=Mar ;;
04) Mon=Apr ;;
05) Mon=May ;;
06) Mon=Jun ;;
07) Mon=Jul ;;
08) Mon=Aug ;;
09) Mon=Sep ;;
10) Mon=Oct ;;
11) Mon=Nov ;;
12) Mon=Dec ;;
esac
OutputDate="$d/$Mon/$y"
echo "$OutputDate"
You're nearly there. When you say case ${ConvDate} in this means "When ${ConvDate} is...". You should replace ${ConvDate} with what you're actually testing (so characters 0 to 1 of the date string, or ${TestDate:0:2}), and then inside the case statements do the assignment:
ConvDate=Jan
I am trying to parse arguments in ksh. Can't do getopt for the same as in short options I have two/three characters. Currently I am using for loop. Its stupid but am unable to find something better.
Question: How do I set option+value as one unit in order to parse?
Also if eval set -- $option will help me then how do I use it? echo on option does not show the expected "--" at the end. Am I assuming something wrong?
I am thinking of using a variable to keep track of when an option is found but this method seems too confusing and unnecessary.
Thanks for your time and help.
Update 1:
Adding code as pointed out. Thanks to markp, Andre Gelinas and random down-voter in making this question better. Trying to execute the script as given in line 2 and 3 of code - or any other combination of short and long options passed together.
#!/bin/ksh
# bash script1.sh --one 123 --two 234 --three "some string"
# bash script1.sh -o 123 -t 234 -th "some string"
# the following creates problems for short options.
#options=$(getopt -o o:t:th: -l one:two:three: "--" "$#")
#Since the below `eval set -- "$options"` did not append "--" at the end
#eval set -- "$options"
for i in $#; do
options="$options $i"
done
options="$options --"
# TODO capture args into variables
Attempted code below TODO until now:
for i in $options; do
echo $i
done
Will be capturing the args using:
while true; do
case $1 in
--one|-o) shift; ONE=$1
;;
--two|-t) shift; TWO=$1
;;
--three|-th) shift; THREE=$1
;;
--) shift; break
;;
esac
done
Try something like this :
#!/bin/ksh
#Default value
ONE=123
TWO=456
# getopts configuration
USAGE="[-author?Andre Gelinas <andre.gelinas#foo.bar>]"
USAGE+="[-copyright?2018]"
USAGE+="[+NAME?TestGetOpts.sh]"
USAGE+="[+DESCRIPTION?Try out for GetOps]"
USAGE+="[o:one]#[one:=$ONE?First.]"
USAGE+="[s:second]#[second:=$TWO?Second.]"
USAGE+="[t:three]:[three?Third.]"
USAGE+=$'[+SEE ALSO?\aman\a(1), \aGetOpts\a(1)]'
while getopts "$USAGE" optchar ; do
case $optchar in
o) ONE=$OPTARG ;;
s) TWO=$OPTARG ;;
t) THREE=$OPTARG ;;
esac
done
print "ONE = "$ONE
print "TWO = "$TWO
print "THREE = "$THREE
You can use either --one or -o. Using --man or --help are also working. Also -o and -s are numeric only, but -t will take anything. Hope this help.
I'm trying to trim only the left half of a string that is given to ltrim() as an argument. This is my current code.
ltrim()
{
string=${1}
divider=$((${#string} / 2))
trimrule=${2}
string_left=${string:0:$divider}
string_right=${string:$divider}
echo ${string:$divider} ## My own quick debug lines
echo ${string:0:$divider} ## My own quick debug lines
if [ $# -ne 2 ]
then
printf "%d argument(s) entered. 2 required.\n" "$#"
else
while :
do
case $string_left in
${2}*) string_left=${string_left#?} ;;
*${2}) string_left=${string_left%?} ;;
*) break ;;
esac
done
printf "Left side string is %s\n" "${string_left}"
fi
}
However, when I enter ltrim abcdefghijklmnopq abc the shell returns the following:
ijklmnopq
abcdefgh
Left side string is bcdefgh
So I only lost 'a' out of the word while I'm looking to get 'defgh' as a result. What am I doing wrong?
function substr_remove() {
echo "${1//$2/}"
}
substr_remove carfoobar123foo456 foo
Output:
carbar123456
Are you searching for something like this?
function ltrim() {
echo ${1##$2}
}
ltrim abcdefghijklmnopq abc # Prints: defghijklmnopq
I want a variable whose value should be in already defined range.
for e.g.
variable DAY=$1
should only get values Mon,Tue,Wed,Thus,Fri,Sat,Sun.
if user enter something else, it will not accept it.
I know i can do this by defining array or normally a variable storing all the days and then check variable DAY in a "for or while" loop.
But i want the simplest way without loop.
Thanks in advance.
Use the extended glob support to match exactly one of a list of choices.
range="Mon|Tue|Wed|Thu|Fri"
read day
if [[ $day = #($range) ]]; then
echo "$day is valid"
else
echo "$day is not valid"
fi
Without using a loop you can do something like this:
EOL=$'\n'
arr=(Mon Tue Wed Thus Fri Sat Sun)
read -p 'Enter day value: ' day
Tue
[[ $(printf "$EOL%s$EOL" "${arr[#]}") == *"$EOL$day$EOL"* ]] &&
echo "in range" || echo "not in range"
in range
read -p 'Enter day value: ' day
ue We
[[ $(printf "$EOL%s$EOL" "${arr[#]}") == *"$EOL$day$EOL"* ]] &&
echo "in range" || echo "not in range"
not in range
There's really no need for an array.
read -r day
case ' Mon Tue Wed Thu Fri Sat Sun ' in
*" $day "*)
echo "in range";;
*) echo "not in range";;
esac
Notice the spaces around the string expressions.
If you need to be strict, you should reject "Tue Wed" which this doesn't do. It would require a second case wrapper, which is clunky but not completely unwieldy.
To expand slightly on anubhava's answer:
arr=(Mon Tue Wed Thus Fri Sat Sun)
read -p 'Enter day value: ' day
case "${arr[#]}" in *"$day"*) echo "in range";; esac
Basically, this is building an array, turning it into a string, and doing a pattern match on it.
In the context of a case expression, there's no difference between "${arr[#]}" and "${arr[*]}"; I personally would stick with *, which makes it clearer that you're building one big string instead of a list of separate ones.
With this specific set of values, you don't have to worry about substring overlap, but the above check is still overly forgiving. For instance, if you enter u, it will be considered "in range". That may not be what you want.
A more exact test would be something like this:
case "${arr[*]}" in
"$day "*|*" $day "*|*" $day") echo "in range";;
*) echo "not in range";;
esac
This still permits the user to enter multiple days as "Mon Tue" or similar; an easy way to fix that in this particular case is to change the read line from read day to read day _.
The "$day "* pattern will match at the beginning of the array (so if they enter Mon in this case). The *" $day" pattern will match at the end of the array (Fri), and the *" $day "* pattern will match the rest. Nothing else but an exact day string will match.
Also, here you can see how to handle the else case - a pattern of * matches anything that hasn't already matched something else, so that's the equivalent of else in a case.