Passing Scripts as Arguments Bash - bash

This script is incomplete since I will want to do error testing later, but the idea is that ARG is a script and ARG2 is a directory, and ARG should mark all of the files in ARG2. How would I do this in such a way that bash knows the first argument has to be a script and argument 2 is a directory?
ARG=$1
ARG2=$2
CHECK=0
aCount=0
bCount=0
cCount=0
dCount=0
fCount=0
if [ $CHECK -e 0 ]; then
for files in $ARG2; do
if [ sh $1 $2 -eq A]; then
aCount=$((aCount+1))
elif [ sh $1 $2 -eq B];
bCount=$((bCount+1))
elif [ sh $1 $2 -eq C];
cCount=$((cCount+1))
elif [ sh $1 $2 -eq D ];
dCount=$((dCount+1))
else;
fCount=$((fCount+1))
fi
done
fi
echo A: $aCount
echo B: $bCount
echo C: $cCount
echo D: $dCount
echo F: $fCount

There are a variety of errors you could catch by running your script through shellcheck.net.
Corrections:
To loop over the files in a directory, write for file in dir/* not for file in dir. The latter just loops once with $file set to the string "dir", rather than iterating over the contents of the directory dir/.
[ sh $1 $2 -eq A] is a jumble of shell constructs. You want to capture the output of the script, so you need $(...). You're doing a string check, so you should use == not -eq. Correcting both yields:
[ $(sh $1 $2) == A ]
I'm guessing $2 should be $files, though. The loop variable, yes?
[ $(sh $1 $files) == A ]
There are other miscellaneous mistakes, such as missing thens and not always having a space before ].
Improvements:
You should quote everything properly to prevent inadvertent word splitting and glob expansion.
[ "$(sh "$1" "$files")" == A ]
Let's replace $1 with $script and $files with singular $file.
[ "$(sh "$script" "$file")" == A ]
If the script has a proper shebang line like #!/bin/bash at the top then there's no need to explicitly invoke sh.
[ "$("$script" "$file")" == A ]
That's all great. Now you have something like this:
if [ "$("$script" "$file")" == A ]; then
aCount=$((aCount+1))
elif [ "$("$script" "$file")" == B ]; then
bCount=$((bCount+1))
elif [ "$("$script" "$file")" == C ]; then
cCount=$((cCount+1))
elif [ "$("$script" "$file")" == D ]; then
dCount=$((dCount+1))
else
fCount=$((fCount+1))
fi
Awfully repetitious, no? Let's try a case statement instead.
case "$("$script" "$file")" in
A) aCount=$((aCount+1));;
B) bCount=$((bCount+1));;
C) cCount=$((cCount+1));;
D) dCount=$((dCount+1));;
*) fCount=$((fCount+1));;
esac
That case statement is still quite complex. Let's break it up to make it easier to parse.
grade=$("$script" "$file")
case $grade in
...
esac
Variable names ought to be lowercase. UPPERCASE names are reserved for the shell, so best not to use those. Change COUNT to count.
Let's rename ARG and ARG2 to script and dir, respectively. Meaningful names make everything easier to read.
var=$((var+1)) can be simplified to ((var += 1)) or ((var++)).
End result:
script=$1
dir=$2
check=0
aCount=0
bCount=0
cCount=0
dCount=0
fCount=0
if ((check == 0)); then
for file in "$dir"/*; do
grade=$("$script" "$file")
case $grade in
A) ((aCount++));;
B) ((bCount++));;
C) ((cCount++));;
D) ((dCount++));;
*) ((fCount++));;
esac
done
fi
echo "A: $aCount"
echo "B: $bCount"
echo "C: $cCount"
echo "D: $dCount"
echo "F: $fCount"

#John Kugelman did a great job above. For an alternate take -
declare -A count # count is an array
for file in "$dir"/* # skipping assignments, and $check
do grade=$("$script" "$file") # grab the output as $grade
case $grade in # look up its value
[A-D]) (( count[$grade]++ ));; # use as-is for a-d
*) (( count['F']++ ));; # throw everything else in f
esac
done
for g in A B C D F # then for a-f (known values)
do echo "$g: "${count[$g]} # pull the counts
done

Related

How to combine AND and OR condition in bash script for if condition?

I was trying to combine logical AND & OR in a bash script within if condition. Somehow I am not getting the desired output and it is hard to troubleshoot.
I am trying to validate the input parameters passed to a shell script for no parameter and the first parameter passed is valid or not.
if [ "$#" -ne 1 ] && ([ "$1" == "ABC" ] || [ "$1" == "DEF" ] || [ "$1" == "GHI" ] || [ "$1" == "JKL" ])
then
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
exit 1
fi
Can anyone point out what is going wrong here?
The immediate problem with your statement is one of logic: you probably meant to write:
if [ "$#" -ne 1 ] || ! ([ "$1" = "ABC" ] || [ "$1" = "DEF" ] || [ "$1" = "GHI" ] || [ "$1" = "JKL" ])
then
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]" >&2
exit 1
fi
That is: abort, if either more than 1 argument is given OR if the single argument given does NOT equal one of the acceptable values.
Note the ! to negate the expression in parentheses and the use of the POSIX-compliant form of the string equality operator, = (rather than ==).
However, given that you're using Bash, you can make do with a single [[ ... ]] conditional and Bash's regular-expression matching operator, =~:
if [[ $# -ne 1 || ! $1 =~ ^(ABC|DEF|GHI|JKL)$ ]]
then
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]" >&2
exit 1
fi
If POSIX compliance is not required, [[ ... ]] is preferable to [ ... ] for a variety of reasons.
In the case at hand, $# and $1 didn't need quoting, and || could be used inside the conditional.
Note that =~ as used above works in Bash 3.2+, whereas the implicit extglob syntax used in anubhava's helpful answer requires Bash 4.1+;
in earlier versions you can, however, explicitly enable (and restore to its original value after) the extglob shell option: shopt -s extglob.
BASH actually allows use of extended glob inside [[ ... ]] and have && inside as well.
So you can do:
if [[ $# -ne 1 && $1 == #(ABC|DEF|GHI|JKL) ]]; then
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
exit 1
fi
A few things:
[...] in bash is equivalent to the same test command (check the man page), so those && and || are not logical operators, but rather the shell equivalents
Parentheses in POSIX shell are not a grouping operator. They will work here, but they open a subshell, you are better off using standard test options of -a and -o (making your if statement if [ "$#" -ne 1 -a \( "$1" == "ABC" -o "$1" == "DEF" -o "$1" == "GHI" -o "$1" == "JKL" \) ], though based on your logic, it sounds like you actually want something like if [ "$#" -ne 1 -o \( "$1" != "ABC" -a "$1" != "DEF" -a "$1" != "GHI" -a "$1" != "JKL" \) ]. You probably can get better results with a case statement like follows:
usage() {
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
}
if [ "$#" -ne 1 ]
then
usage
exit 1
fi
case "$1" in
ABC)
echo "Found ABC"
;;
DEF)
echo "Found DEF"
;;
GHI)
echo "Found GHI"
;;
JKL)
echo "Found JKL"
;;
*)
usage
exit 1
;;
esac
If you want to pass a set of possible static arguments in, you might want to look at the getopts special shell command.

if condition inside function is not working as desired when function called with command line arguments inside find statement

#!/bin/bash
# Code to generate script usage
if [[ "$#" -ne 1 ]] && [[ "$#" -ne 2 ]]; then
flag=1;
elif ! [[ "$1" == "abcd" || "$1" == "dcba" ]]; then
echo "Invalid"
flag=1;
fi
while [ $# -gt 1 ]
do
case $2 in
'streams')
;;
*)
echo "unrecognised optional arg $2"; flag=1;
;;
esac
shift
done
if [ "$flag" == "1" ]; then
echo "Usage:"
exit
fi
function main {
arg1=$1
streams=$2
if [ "${streams}" == "streams" ]; then
echo entering here
else
echo entering there
fi
}
parent_dir=`pwd`
find $parent_dir -name "*" -type d | while read d; do
cd $denter code here
main $1 $2
done
Why the code does not enter "entering here" when script run with arguments "abcd" and "streams" ?
I feel that function having two arguments is causing the problem, code was working fine with one argument
Several things you might want to fix in your code, before attempts are made to find the specific problem. It is possible that it will disappear after modifying your script accordingly. If the problem is still alive, I'll edit my answer with a solution. If you decide to apply the following changes, please update your code in the question.
Consistent usage of either [[ or [. [[ is a Bash keyword similar to (but more powerful than) the [ command.
See
Bash FAQ 31
Tests And Conditionals
Unless you're writing for POSIX sh, I recommend [[.
Use (( for arithmetic expressions. ((...)) is an arithmetic command, which returns an exit status of 0 if the expression is nonzero, or 1 if the expression is zero. Also used as a synonym for let, if assignments are needed. See Arithmetic Expression.
Use the variable PWD instead of pwd. PWD is a builtin variable in all POSIX shells that contains the current working directory. pwd(1) is a POSIX utility that prints the name of the current working directory to stdout. Unless you're writing for some non-POSIX system, there is no reason to waste time executing pwd(1) rather than just using PWD.
The function keyword is not portable. I suggest you to avoid using it and simply write function_name() { your code here; } # Usage
$parent_dir is not double-quoted. "Double quote" every literal that contains spaces/metacharacters and every expansion: "$var", "$(command "$var")", "${array[#]}", "a & b". See
Quotes
Arguments
ShellCheck your code before uploading.
Replace the while condition logic with an if condition, so that shift is no longer required. Shift was the devil I was facing I found.
#!/bin/bash
# Code to generate script usage
if [[ "$#" -ne 1 ]] && [[ "$#" -ne 2 ]]; then
flag=1;
elif ! [[ "$1" == "abcd" || "$1" == "dcba" ]]; then
echo "Invalid"
flag=1;
fi
#while [[ $# -gt 1 ]]
#do
# case $2 in
# 'streams')
# ;;
# *)
# echo "unrecognised optional arg $2"; flag=1;
# ;;
# esac
# shift
#done
if [[ $2 == "streams" ]]; then
:
elif [[ (-z $2) ]]; then
:
else
echo "unrecognised optional arg $2"; flag=1;
fi
if [[ "$flag" == "1" ]]; then
echo "Usage:"
exit
fi
function main {
streams=$2
if [[ "${streams}" == "streams" ]]; then
echo entering here
else
echo entering there
fi
}
parent_dir=`pwd`
find $parent_dir -name "*" -type d | while read d; do
cd $d
main $1 $2
done

How do I rewrite a bash 'while' loop as a 'for' loop?

This is my bash scripting code so I want to know How to Rewrite the below Bash script using a “for” loop instead of the “while” loop.
#!/bin/bash
if [ $# -gt 0 ]; then
a=0;
if [ -f RandNos ]; then
rm RandNos;
fi
while [ $a -lt $1 ]
do
a='expr $a + 1';
myrand=$RANDOM;
if [ "$2" "1"]; then
echo "No. $a ==> $myrand";
fi
echo $myrand>>RandNos
done
else
echo "please use with an argument..."
fi
Thanks.
The short of it: for counter-based loops, use the C-like form of the for loop:
for (( a = 0; a < $1; a++ )); do
# ... use $a
done
(This replaces while [ $a -lt $1 ]; do a='expr $a + 1' ...; done.)
See below for more on the rules that apply inside (( ... )).
As for the rest of your code:
Conditional [ "$2" "1"] is broken: it's missing the mandatory space before ]
With that fixed, it'll only work if $2 expands to a unary test operator such as -n.
Perhaps you meant if [[ -z $myrand ]]; then, to check if $RANDOM resulted in a nonempty string?
a='expr $a + 1' - which you don't need anymore with the for loop - doesn't actually invoke expr, because you're using single quotes - you'd need backticks (`) instead, or, preferably, the modern equivalent: $(expr $a + 1). However, with arithmetic evaluation, this could be simplified to (( ++a )).
[ ... ] conditionals work in bash, but they're provided for POSIX compatibility - use [[ ... ]] as the bash-specific alternative, which is more robust, has more features, and is faster.
bash statements only need terminating with ; if you place multiple on a single line
Note that bash considers do ... and then ... separate statements, hence you often see if ...; then and for ...; do.
In general, I encourage you to syntax-check your shell code at http://shellcheck.net - it's a great tool for detecting syntax problems.
Note how different rules apply inside (( ... )) compared to elsewhere in bash:
spaces around the = in the variable assignment are allowed.
referencing a variable without the $ prefix (a++) is allowed.
< performs numerical comparison (whereas inside [[ ... ]] it's lexical) -i.e., it's the more natural equivalent to -lt inside [ ... ] or [[ ... ]].
several other mathematical and even bit-wise operators are supported
...
All these different rules apply when bash operates in an arithmetic context, which applies to (( ... )), $(( ... )), array subscripts, and other cases.
For all the rules, run man bash and read the ARITHMETIC EVALUATION section.
Simply rewriting it with a for loop results in:
#!/bin/bash
if [ $# -gt 0 ]; then
if [ -f RandNos ]; then
rm RandNos;
fi
lim=$(expr $1 - 1)
as=$(seq 0 $lim)
for a in $as
do
a='expr $a + 1';
myrand=$RANDOM;
if [ "$2" "1"]; then # <- Caveat: conditional is BROKEN
echo "No. $a ==> $myrand";
fi
echo $myrand>>RandNos
done
else
echo "please use with an argument..."
fi
But there are several things wrong with the script anyhow. Like the last if statement.
if [ $# -lt 1 ];then
echo "First argument must be number".
exit 1;
fi
for a in `seq $1`
do
...
done
Several things can be improved:
#!/bin/bash
if (( $# )); then # anything but 0 is true
rm -f RandNos # remove if existing, otherwise fail silently
for ((a=0; a<$1; a++)); do
myrand=$RANDOM
# what is the intention here?
(( $2 > 1 )) && echo "No. $a ==> $myrand"
echo "$myrand" >> RandNos
done
else
echo "please use with an argument..."
fi
not sure what your intention was with the [ "$2" "1" ] expression. it is probably not what I made from it.
for ((a=1; a<=$1; a++)); do
may reflect your intended logic better, as you use $a for output only after incrementing it. as pointed out and corrected by #mklement0
!/bin/bash
if [ $# -gt 0 ]; then
a=0;
if [ -f RandNos ]; then
rm RandNos;
fi
for (( i=$a; i<$1; i++ ))
do
myrand=$RANDOM;
if [ "$2" = "1" ]; then
echo "No. $a ==> $myrand";
fi
echo $myrand >> RandNos
done
else
echo "please use with an argument..."
fi

How can I loop If Statements in Bash

Here is my code:
#!/bin/bash
echo "Letter:"
read a
if [ $a = "a" ]
then
echo "LOL"
fi
if [ $a = "b" ]
then
echo "ROFL"
fi
Is there a way for me to loop this so that, after displaying either LOL or ROFL, I would be asked for a letter again?
Yes.
Oh, you want to know how?
while true; do
echo "Letter:"
read a
if [ $a = "a" ]
then
echo "LOL"
elif [ $a = "b" ]
then
echo "ROFL"
fi
done
Of course, you probably want some way to get out of that infinite loop. The command to run in that case is break. I would write the whole thing like this:
while read -p Letter: a; do
case "$a" in
a) echo LOL;;
b) echo ROFL;;
q) break;;
esac
done
which lets you exit the loop either by entering 'q' or generating end-of-file (control-D).
Don't forget that you always want -r flag with read.
Also there is a quoting error on that line:
if [ $a = "a" ] # this will fail if a='*'
So here is a bit better version(I've also limited the user to input only 1 character):
#!/bin/bash
while true; do
read -rn1 -p 'Letter: ' a
echo
if [[ $a = 'a' ]]; then
echo "LOL"
elif [[ $a = 'b' ]]; then
echo "ROFL"
else
break
fi
done
Or with switch statement:
#!/bin/bash
while read -rn1 -p 'Letter: ' a; do
echo
case $a in
a) echo LOL;;
b) echo ROFL;;
*) break;;
esac
done

can't seem to get this function with bash to work

# define some variables for later
date=`date`
usr=`whoami`
# define usage function to echo syntax if no args given
usage(){
echo "error: filename not specified"
echo "Usage: $0 filename directory/ directory/ directory/"
exit 1
}
# define copyall function
copyall() {
# local variable to take the first argument as file
local file="$1" dir
# shift to the next argument(s)
shift
# loop through the next argument(s) and copy $file to them
for dir in "$#"; do
cp -R "$file" "$dir"
done
}
# function to check if filename exists
# $f -> store argument passed to the script
file_exists(){
local f="$1"
[[ -f "$f" ]] && return 0 || return 1
}
# call usage() function to print out syntax
[[ $# -eq 0 ]] && usage
here's what I can't figure out
# call file_exists() and copyall() to do the dirty work
if ( file_exists "$1" ) then
copyall
I would also love to figure out how to take this next echo section and condense it to one line. Instead of $1 then shift then move on. Maybe split it into an array?
echo "copyall: File $1 was copied to"
shift
echo "$# on $date by $usr"
else
echo "Filename not found"
fi
exit 0
It seems to me that the file_exists macro is superfluous:
if [ -f "$1" ]
then copy_all "$#"
else echo "$0: no such file as $1" >&2; exit 1
fi
or even just:
[ -f "$1" ] && copy_all "$#"
You probably just need to remove the parentheses around file_exists "$1", and add a semicolon:
if file_exists "$1"; then
copyall

Resources