How to make below bash script clean and less repetitive? - bash

I'm making birthday tracker for only a few people and was wondering how to combine all if statements into one since date and name are the only different things in every if statement, rest is same format.
date=`date +%b-%d`
echo $date
if [ $date == "Sep-09" ]; then
echo "Happy Birthday abc!"
fi
if [ $date == "Oct-11" ]; then
echo "Happy Birthday xyz!"
fi
.
.
.
I think I can use positional arguments ($1 for date, $2 for name..) but can't figure out how to use it for multiple dates and names. Or if there's any better way, that would be good to know as well.

Using case:
...
case "$date" in
"Sep-09") name=abc;;
"Oct-11") name=xyz;;
...
*) exit;; # if no condition met
esac
echo "Happy Birthday $name!"

You can use map/dictionary data structure here. You can check example here: How to define hash tables in Bash?

You could store your birthdays in a text file and read them with while and read.
read will let you read into one or more variables, and if there's more words than variables it stuffs all remaining words into the last variable, so it's perfect to use here when the person's name could be multiple words:
help read
The line is split into fields as with word splitting, and the first word is assigned to the first NAME, the second word to the second NAME, and so on, with any leftover words assigned to the last NAME. Only the characters found in $IFS are recognized as word delimiters.
I also suggest using the date format +%m-%d rather than +%b-%d as it means the dates in your file will be in order (e.g. you can run sort birthdays.txt to see everyone in birthday order, which you couldn't do if the month was represented by 3 letters).
birthdays.txt:
09-08 John Smith
10-11 Alice
checkbirthdays.sh:
#!/bin/bash
nowdate="$(date +%m-%d)"
(
while read checkdate name ; do
if [ "$nowdate" = "$checkdate" ] ; then
echo "Happy Birthday $name!"
fi
done
) < birthdays.txt
output of ./checkbirthdays.sh on September 8th:
Happy Birthday John Smith!

Assuming you have bash 4.0 or newer and, names don't contain whitespace or glob characters, use of an associative array would be appropriate for this task:
#!/bin/bash
declare -A birthday_to_names=(
['Sep-09']='abc def'
['Oct-11']='xyz'
)
names=${birthday_to_names[$(date +%b-%d)]}
[[ $names ]] && printf 'Happy Birthday %s!\n' $names
Note that this version congratulates all birthday people if there are multiple names associated with the given birthday.

Related

Rename list of files based off of a list and a directory of files

So, I have a master file - countryCode.tsv, which goes like this,
01 united_states
02 canada
etc.
I have another list of country files, which go like this,
united_states.txt
Wyoming
Florida
etc.
canada.txt
some
blah
shit
etc.
and, I have a list of files that are named like this,
01_1
01_2
02_1
02_2
etc.
the first part of the filename belongs to the country code in the first list, and the second part belongs to the line number of the country file.
for example,
01_02 would contain the info related to florida (united states).
now, here comes my question,
how do i rename these numerically named files to the country_state format, i.e., for example,
01_02 becomes united_states_florida
The way I would do this is to first read all of the countries into an associative array, then I would iterate over that array looking for '.txt' files for each country. When I find one, read each line in turn and look for a file that matches the country code and the line number from that file. If found, rename it.
Here is some sample code:
#!/bin/bash
declare -A countries # countries is an associative array.
while read code country; do
if [ ${#code} -ne 0 ]; then # Ignore blank lines.
countries[${code}]=${country}
fi
done < countryCodes.txt # countryCodes.txt is STDIN for the while
# loop, which is passed on to the read command.
for code in ${!countries[#]}; do # Iterate over the array indices.
counter=0
country=${countries[${code}]}
if [ -r "${country}.txt" ]; then # In case country file does not exist.
while read state; do
((counter++))
if [ -f "${code}_${counter}" ]; then
mv "${code}_${counter}" "${country}_${state}"
fi
done < "${country}.txt"
fi
done

Process contents in array based on type in shellscript

I have an array that has three types of data in it, integer, integer/integer, and the string value.
I have shown a sample below.
myarr = (2301/2320,Team Lifeline, 2311, 7650/7670, 232)
I have the following algorithm that I want to come up with.
For index in myarr
if index contains data as number1/number2; then
create an array, "mynumbers" to hold all the numbers starting from number1 to number2
else if index is a string
add it in "mystrarr"
else
add it in "myintarr"
done
For the first case, if I have an enter in the myarr as 2301/2320,
then the mynumbers as shown in the pseudocode will have entries from {2301, 2302, ... , 2320}. I am not able to understand on how to parse the entry in myarr and identify that it has a / in the array.
For the second situation, I am also not sure on how to identify if the entry in the myarr and know it is a string. mystrarr should have {Team Lifeline}.
For the final case, the myintarr should have {2311, 232}.
Any help would be appreciated. I am very new to shell script.
Stack Overflow is not a coding service.... but I was bored so here you go...
#!/bin/bash
myarr=(2301/2320 'Team Lifeline' 2311 7650/7670 232)
for element in "${myarr[#]}"; do
if [[ $element =~ ^[0-9]+/[0-9]+$ ]]; then
range="{${element%/*}..${element##*/}}"
mynumbers=( $(eval "echo $range") )
elif [ $element -eq $element ] 2>> /dev/null; then
intarr+=( $element )
else
strarr+=( "$element" )
fi
done
echo "mynumbers = ${mynumbers[*]}"
echo "intarr = ${intarr[*]}"
echo "strarr = ${strarr[*]}"
A lot to unpack here for inexperienced. So ask questions where I didn't cover anything. Things to note:
All assignments there are no spaces around =.
Array assignments are of the format ( element1 element2 ... )
Appending to arrays with +=(...) format
Looping through array elements for element in "${myarr[#]}"
Note that the array generated by 7650/7670 will overwrite the array generated by 2301/2320. I assume you have some kind of plan for this array, so I didn't do anything to stop it from being overwritten.
More details
This line is validating the format for 111/222:
if [[ $element =~ ^[0-9]+/[0-9]+$ ]]; then
[[ x =~ x ]] performs a regex comparison and this regex essentially just means:
^ - beginning of the string
[0-9]+ - Atleast 1 number
/ - character literal
$ - end of string
These lines are expanding your beginning and ending numbers:
range="{${element%/*}..${element##*/}}"
mynumbers=( $(eval "echo $range") )
This is maybe more complicated than it needs to be as most people try to avoid eval in general for security reasons. I'm leveraging bash's brace expansion. If you run echo {5..9}, it will output 5 6 7 8 9. This does not trigger with variables, so I cheated and used eval.
This line is checking if we are dealing with an integer:
[ $element -eq $element ] 2>> /dev/null
This works by running an integer -eq (equals) comparison on the variable against itself. This will actually fail and throw an error message on anything but an integer. This is not the way it was designed to be used which is why we discard all the error messages (2>> /dev/null).
This is a nice succinct script, but is using some unconventional practices. A longer more verbose version may be better for a beginner.
You can use regular expressions to match elements that are nothing but digits, or digits/digits, and assume everything else is a string:
#!/bin/bash
myarr=(2301/2320 "Time Lifeline" 2311 7650/7670 232)
declare -a mynumbers mystrarr myintarr
for elem in "${myarr[#]}"; do
if [[ $elem =~ ^([0-9]+)/([0-9]+)$ ]]; then
mynumbers+=($(seq ${BASH_REMATCH[1]} ${BASH_REMATCH[2]}))
elif [[ $elem =~ ^[0-9]+$ ]]; then
myintarr+=($elem)
else
mystrarr+=("$elem")
fi
done
echo mynumbers is "${mynumbers[#]}"
echo myintarr is "${myintarr[#]}"
echo mystrarr is "${mystrarr[*]}"
Jason explained a lot in his (very similar; there's only so many obvious ways to do this) answer, so to expand on where ours are different:
We both use regular expressions to match the integer/integer case, but he then goes on to extract the two numbers using parameter expansion with pattern removal options, while mine captures the two integers in the regular expression, and uses the BASH_REMATCH array to access their values as well as the seq command to generate the numbers between the two.

How do I pass arguments to a defined function in a shell script?

I'm trying to create a function wherein I pass two dates as arguments and get their day of the week out. I've gotten it to work by copying and pasting the parsing process for both input dates, but I'm trying to save space by condensing the process into a defined function. The input should look like so:
datematch.sh 01/03/1984 06/12/2008
But I keep getting error messages along the lines of:
./birthday_match.sh: line 9: ${$1:0:2}: bad substitution
./birthday_match.sh: line 9: ${$1:0:2}: bad substitution
The first person was born on:
The second person was born on:
Thus, they were born on the same day.
How am I substituting wrong? The full code is below.
#!/bin/bash
var1=$1
var2=$2
if [ "$#" -ne 2 ]; then
echo "illegal number of birthdays"
else
function get_dayname ()
{
mo=${$1:0:2}
dy=${$1:3:2}
yr=${$1:6:4}
combo="${mo}${dy}0000${yr}"
fulldate="$(date $combo 2> /dev/null)"
wkdy=${fulldate:0:3}
eval $wkdy
}
first=$(get_dayname "$var1")
second=$(get_dayname "$var2")
echo "The first person was born on: $first"
echo "The second person was born on: $second"
if [ "$first" == "$second" ]; then
echo "Thus, they were born on the same day."
else
echo "Thus, they were not born on the same day."
fi
fi
The parameter expansion syntax is incorrect:
mo=${$1:0:2}
should be
mo=${1:0:2}
Consider the simple case. Substituting the first argument as-is is either $1 or ${1}. In the second version, the curly brackets serve to separate the variable name or number from what follows.
If you wrote ${$1}, the intuitive meaning would be "treat the first argument as a name, and substitute the value of the variable with that name" .... but bash parameter expansion syntax doesn't allow that.

How to extend string to certain length

Hey basically right now my program gives me this output:
BLABLABLA
TEXTEXOUAIGJIOAJGOAJFKJAFKLAJKLFJKL
TEXT
MORE TEXT OF RANDOM CHARACTER OVER LIMIT
which is a result of for loop. Now here's what i want:
if the string raches over 10 characters, cut the rest and add two dots & colon to the end "..:"
otherwise (if the string has less than 10 characters) fill the gap with spaces so they're alligned
so on the example i provided i'd want something like this as output:
BLABLABLA :
TEXTEXOUA..:
TEXT :
MORE TEXT..:
I also solved the first part of the problem (when its over 10 characters), only the second one gives me trouble.
AMOUNT=definition here, just simplyfying so not including it
for (( i=1; i<="$AMOUNT"; i++ )); do
STRING=definition here, just simplyfying so not including it
DOTS="..:"
STRING_LENGTH=`echo -n "$STRING" | wc -c`
if [ "$STRING_LENGTH" -gt 10 ]
then
#Takes
STRING=`echo -n "${STRING:0:10}"$DOTS`
else
#now i dont know what to do here, how can i take my current $STRING
#and add spaces " " until we reach 10 characters. Any ideas?
fi
Bash provides a simple way to get the length of a string stored in a variable: ${#STRING}
STRING="definition here, just simplyfying so not including it"
if [ ${#STRING} -gt 10 ]; then
STR12="${STRING:0:10}.."
else
STR12="$STRING " # 12 spaces here
STR12="${STR12:0:12}"
fi
echo "$STR12:"
The expected output you posted doesn't match the requirements in the question. I tried to follow the requirements and ignored the sample expected output and the code you posted.
Use printf:
PADDED_STRING=$(printf %-10s $STRING)

bash find keyword in an associative array

I have incoming messages from a chat server that need to be compared against a list of keywords. I was using regular arrays, but would like to switch to associative arrays to try to increase the speed of the processing.
The list of words would be in an array called aWords and the values would be a 'type' indicator, i.e. aWords[damn]="1", with 1 being swear word in a legend to inform the user.
The issue is that I need to compare every index value with the input $line looking for substrings. I'm trying to avoid a loop thru each index value if at all possible.
From http://tldp.org/LDP/abs/html/string-manipulation.html, I'm thinking of the Substring Removal section.
${string#substring}
Deletes shortest match of $substring from front of $string.
A comparison of the 'removed' string from the $line, may help, but will it match also words in the middle of other words? i.e. matching the keyword his inside of this.
Sorry for the long-winded post, but I tried to cover all of what I'm attempting to accomplish as best I could.
# create a colon-separated string of the array keys
# you can do this once, after the array is created.
keys=$(IFS=:; echo "${!aWords[*]}")
if [[ ":$keys:" == *:"$word":* ]]; then
# $word is a key in the array
case ${aWords[$word]} in
1) echo "Tsk tsk: $word is a swear word" ;;
# ...
esac
fi
This is the first time I heard of associative arrays in bash. It inspired me to also try to add something, with the chance ofcourse that I completely miss the point.
Here is a code snippet. I hope I understood how it works:
declare -A SWEAR #create associative array of swearwords (only once)
while read LINE
do
[ "$LINE"] && SWEAR["$LINE"]=X
done < "/path/to/swearword/file"
while :
do
OUTGOING="" #reset output "buffer"
read REST #read a sentence from stdin
while "$REST" #evaluate every word in the sentence
do
WORD=${REST%% *}
REST=${REST#* }
[ ${SWEAR[$WORD]} ] && WORD="XXXX"
OUTGOING="$OUTGOING $WORD"
done
echo "$OUTGOING" #output to stdout
done

Resources