Counting the number of inputs provided by user - linux

Description: I have a script that begins with a warning/question posed to user. I need the user to answe with yes/y or no/n.
Issue: Though I have a conditional to make sure the user provides one of the following answers presented above I also need to make that only ONE input is provided by the user. I have attempted using
[ "$#" -eq 1 ] or [ $# -eq 1 ] neither of these coniditionals seems to work to solve the problem
here is what I have thus far:...
#!/bin/bash
#
# Descritpion: some Windows OS script
#
#
printf "(!)WARNING(!): 1. The this program is ONLY compatible with Windows operating systems. "
printf "2. You will NEED to be logged in as an ADMIN in order to fully make use of the '*****' script.\n"
printf "Would you like to continue? yes or no (y/n): "
#would it be cleaner to use "case" rather than a "while" w/ multiple conditionals? (01.19.2020)
read opt
while (true)
do
if [ $opt == yes ] || [ $opt == y ]
then
printf "continue. \n"
break
elif [ $opt == no ] || [ $opt == n ]
then
printf "OK, exiting the script! \n"
exit 0
#elif [ "$#" -ne 1 ]
#then
# "Too many arguments have been provided, please try again. \n"
# read opt
else
printf "The opition you provided is not recognized, please try again. \n"
read opt
fi
done

Not the ideal solution, but it includes an explanation of why $# is not working as you expect:
$# returns the number of arguments passed to a function. You have read 'opt' in as a user input - not the arguments to the function.
One solution could be to wrap your infinite while loop inside a function, and then pass $opt into it. It would then be the argument(s) to your function and you could use $# to count how many there are.
For details of the workings of many built-in functions in bash, you could try:
man bash
but I accept that there is a lot of information in there. You can search for the relevant words, normally using "/{searchstring}" (but you will probably need to "escape" the special characters, in this case: "/\$\#")

Take a look at read options
read: read [-ers] [-u fd] [-t timeout] [-p prompt] [-a array] [-n nchars] [-d delim] [name ...]
One line is read from the standard input, or from file descriptor FD if the
-u option is supplied, and the first word is assigned to the first NAME,
the second word to the second NAME, and so on, with leftover words assigned
to the last NAME. Only the characters found in $IFS are recognized as word
delimiters. If no NAMEs are supplied, the line read is stored in the REPLY
variable. If the -r option is given, this signifies `raw' input, and
backslash escaping is disabled. The -d option causes read to continue
until the first character of DELIM is read, rather than newline. If the -p
option is supplied, the string PROMPT is output without a trailing newline
before attempting to read. If -a is supplied, the words read are assigned
to sequential indices of ARRAY, starting at zero. If -e is supplied and
the shell is interactive, readline is used to obtain the line. If -n is
supplied with a non-zero NCHARS argument, read returns after NCHARS
characters have been read. The -s option causes input coming from a
terminal to not be echoed.
you would be able to simplify your script using some of them.

Read the input and parse it exactly. Decide, if you want to ignore spaces or not. You could even use a regex to make the input formatted as exactly as you want.
while (true) is an unnecessary use of subshell. Just while true.
if [ $opt == yes ] will not work if opt contains whitespaces, for example user inputs y e s. It will exit with a nonzero exit status and print an error message on standard error stream. As a rule, always quote your variable expansions. Always "$opt", never $opt. This is most probably the error you are asking for fixing.
Also the == is a bash extension. Use = for string comparison.
Cosmetics: [ $opt == yes ] || [ $opt == y ] unnecessary runs two processes in case the first one fails. Just run one process [ "$opt" = yes -o "$opt" = y ]. But the first one may be just more clear.
read opt ignores leading and trailing whitespaces and removes the \ slashes. Use IFS= read -r opt to read the whole line exactly as it is.
Use shellcheck.net to check your scripts.
The $# is the number of arguments passed to the script. It is unrelated to what read does. read saves the input to the variable.
So:
while true; do
if ! IFS= read -r opt; then
echo "ERROR: END OF INPUT!" >&2
exit 2
fi
case "$opt" in
y|yes) printf 'continue. \n'; break; ;;
n|no) printf 'OK, exiting the script! \n'; exit 1; ;;
*) printf 'The opition you provided is not recognized, please try again. \n'; ;;
esac
done

Related

How to validate the number of arguments (mandatory and optional) in a shell script?

I am having trouble validating my script arguments. I am trying to achieve 5 mandatory arguments and 2 optional arguments to my script. Here is what I have tried so far.
#!/bin/sh
if [ $# -lt 5 ] || [ $# -gt 7 ]
then
echo "Please supply 5 or 6 or 7 parameters. Aborting."
exit 1
fi
echo MP1 = "$1"
echo MP2 = "$2"
echo MP3 = "$3"
echo MP4 = "$4"
echo MP5 = "$5"
while getopts c:a: optionalargs
do
case $optionalargs in
c)copt=$OPTARG;;
a)aopt=$OPTARG;;
*)echo "Invalid arg";;
esac
done
if [ ! -z "$copt" ]
then
export CHAR_SET=$copt
fi
if [ ! -z "$aopt" ]
then
export ADDITIONAL_FLAGS=$aopt
fi
shift $((OPTIND -1))
echo OP_C = "${CHAR_SET}"
echo OP_A = "${ADDITIONAL_FLAGS}"
The problem is validating the total number of arguments to this script as I can have a total of 5 or 7 arguments. Providing -a additional -c character is treated as 9 arguments.
./a.sh 1 2 3 4 5 -a additional -c character
Please supply 5 or 6 or 7 parameters. Aborting.
I am open to designs with no - as long as I am able to have both mandatory and optional parameters.
How to get this properly validated?
First: the shell doesn't know anything about optional vs. mandatory arguments, doesn't treat arguments that start with "-" specially, anything like that. It just has a list of "words", and it's up to your script to figure out what they mean. The getopts command can help with parsing the arguments, but it handles a fairly limited syntax:
Options start with a single dash, and are a single letter (maybe with an argument after that). For options that don't take arguments, you can stack multiple options on a single dash (e.g. ls -la).
After all the options, there can be a number of positional parameters (the meaning of these is defined -- as the name implies -- by their position in the list, e.g. first, second, etc).
There are a number of syntax extensions (mostly GNU conventions) that getopts does not support:
Putting options after positional parameters (e.g. ls filename -l). Options must always come first.
Long options (multi-letter and/or double-dash, e.g. ls --all).
Using -- to separate the options from the positional parameters.
So if you want to use getopts-style optional arguments, you need to put them first in the argument list. And when parsing them, you need to parse and remove* them from the argument list (with shift) before you check the number of positional parameters, and use $1 etc to access the positional parameters. Your current script is running into trouble because it's trying to handle the positional parameters first, and that won't work with getopts (at least without some heavy-duty kluging).
If you need a more general argument syntax, you might be able to use getopt (note the lack of "s" in the name). Some versions of getopt support the GNU conventions, some don't. Some have other problems. IMO this is a can of worms that's best left unopened.
Another possibility is to abandon the - option syntax, and give the script 7 positional parameters where the last two can be omitted. The problem with this is that you can't omit the sixth but pass the seventh (unless you're willing to consider the sixth being blank as equivalent to omitting it). The code for this would look something like this:
if [ $# -lt 5 ] || [ $# -gt 7 ]
then
echo "Please supply 5 or 6 or 7 parameters. Aborting."
exit 1
fi
echo "MP1 = $1"
echo "MP2 = $2"
echo "MP3 = $3"
echo "MP4 = $4"
echo "MP5 = $5"
if [ -n "$6" ]; then
# This will run if a sixth argument was specified AND it wasn't blank.
export CHAR_SET=$6
echo "OP_C = ${CHAR_SET}"
fi
if [ $# -ge 7 ]; then
# This will run if a seventh argument was specified EVEN IF it was blank.
# If you want to omit this for a blank arg, use the `-n` test instead.
export ADDITIONAL_FLAGS=$7
echo "OP_A = ${ADDITIONAL_FLAGS}"
fi
...and then run the script with e.g.
./a.sh 1 2 3 4 5 character additional # Both optional args supplied
./a.sh 1 2 3 4 5 character # Only first optional arg supplied
./a.sh 1 2 3 4 5 "" additional # Only second optional arg supplied
Or, if you really want the more extended syntax and don't want to risk the vagaries of the getopt command, you can spend a lot of time and effort writing your own parsing system. IMO this is way more work than it's worth.
Every space-separated string will be interpreted as a new argument by the shell. Are you allowed to have the -a flag or -c arguments without parameters? Assuming those need to have something following them, you actually need to allow 5 or 7 or 9 arguments. Getopts is separate from the initial shell argument parsing.
Other than that, the script looks good. For quality check, I'd recommend Shellcheck

comparison [ "$var" == "value" ] always false; how can this be debugged?

I'm having a ton of trouble with an if statement in bash. Here's what I have:
if [ "$returncode" == "HTTP/1.1 200 OK" ]; then
echo "Works!"
fi
This will not work successfully. I've verified over and over again that the variable $returncode is equal to the text specified, I'll print the two values right before this and they are definitely identical.
I've tried a couple different variations of this statement, all with no luck. I feel like a moron because this should be extremely simple! What do you think is wrong here?
If the values truly are identical (no hidden characters), the most likely problem is that your shell isn't actually bash.
/bin/sh only promises compliance with the POSIX sh standard, and == isn't a valid comparison operator in POSIX test; the standard-compliant string comparison operator is =.
Also try eliminating a CR from the end of the line:
#!/bin/bash
# ^^^^ NOT /bin/sh ($'...' is a non-POSIX extension)
# ...and if starting with an explicit interpreter, "bash yourscript", not "sh yourscript"
set -x # this will log each command run to stderr, allowing easy debugging
if [ "${returncode%$'\r'}" = "HTTP/1.1 200 OK" ]; then
echo "Works!"
fi
${foo%val} expands to the contents of $foo with any suffix consisting of val removed; $'\r' is, in bash, a constant referring to the ASCII CR character, which would be present if reading a CRLF-terminated string using standard UNIX tools (which expect LF-only newlines).

Delete words from given files with sed

I have this assignment to solve:
"Write a shell script that continuously reads words from the keyboard and
deletes them from all the files given in the command line."
I've tried to solve it, here's my attempt:
#!/bin/bash
echo "Enter words"
while (true)
do
read wrd
if [ "$wrd" != "exit" ]
then
for i in $#
do
sed -i -e 's/$wrd//g' $i
done
else
break
fi
done
This is the error that I receive after introducing the command: ./h84a.sh fisier1.txt
Enter words
suc
sed: can't read 1: No such file or directory
Sorry if I'm not very specific, it's my first time posting in here. I'm working in a terminal on Linux Mint which is installed on another partition of my PC. Please help me with my problem. Thanks!
I think you can simplify your script quite a lot:
#!/bin/bash
echo "Enter words"
while read -r wrd
do
[ "$wrd" = exit ] && break
sed -i "s/$wrd//g" "$#"
done
Some key changes:
The double quotes around the sed command are essential, as shell variables are not expanded within single quotes
Instead of using a loop, it is possible to pass all of the file names to sed at once, using "$#"
read -r is almost always what you want to use
I would suggest that you take care with in-place editing using the -i switch. In some versions of sed, you can specify the suffix of a backup file like -i.bak, so the original file is not lost.
In case you're not familiar with the syntax [ "$wrd" = exit ] && break, it is functionally equivalent to:
if [ "$wrd" = exit ]
then break
fi
$# expands to the number of arguments (so 1 in this case)
You probably meant to use $* or "$#"

Scripts in sed - linux

I got the concept of bash, now, I found a site full of riddles for practising bash. I solve a couple of scripts (you should mention what do they do, what they are missing or so, depends on the question) and I bumped this script:
random_var="$(echo $1 | sed -e 's/[^[:alnum:]]//g')"
Correct me if I'm wrong about my basic assumptions on the following code:
$1 is the second argument that the script got (when the first is the script name)
There is a pipeline between the second argument and the sed script that removes all alpha numerics and... according to what I understand, this script can be "broken" by using a delimiter such as [/\]^$ and so ?
Now, there comes the difficulty (well, for me), the program gets an input from the user and, when the following script I just mention is found at a function returning true if the input is different than the result. I have no idea what is happening here, can someone enlighten me?
#!/bin/sh
func()
{
somevar="$(echo $1 | sed -e 's/[^[:alnum:]]//g')"
if [ "$somevar" != "$input" ] ; then
return 1
else
return 0
fi
}
# Sample usage of this function in a script
echo -n "Enter input: "
read input
if ! func "$input" ; then
echo "HELL NO"
exit 1
else
echo "YES!"
fi
exit 0
The script tests a string to see whether it contains any non-alphanumeric characters.
As Avinash has mentioned in the comments, the sed command removes all non-alphanumeric characters. Within the function, $input has the same value as it does in the calling scope, which is also the same as the first argument, $1. This is perhaps a little bit confusing...
If $somevar is different to $input (=$1), then this means that sed has changed the string in some way. Therefore, the string must contain at least one non-alphanumeric character.
If the function returns 1 (there were some non-alphanumeric characters in the input), then ! func is false, so the else branch will be executed and the script will return with an exit code of 0 (success). Otherwise, the script will return a non-zero exit code, indicating a failure.

BASH scripting: taking in multiple flags

I have this issue where I am doing a menu sort of program where users are supposed to type in different numbers depending on what operation(s) the want to do.
Right now I need to take in multiple flags to be placed in a command, but I can't seem to get it to work.
Catch: I cannot type in the flags directly into one variable and I will probably have to have this case list to be able to quit at will)
function fileCpy {
printf "Choices:\n1. Interactive copy, answer yes/no before doing the copy: \n2. Make backups of existing destination files\n3. Preserve file attributes\n4. Do a recursive copy\n0. Return to main menu\n"
#read choices
read $a
read $b
read $c
for choices in $choice ; do
case "$choices" in
1)
a=-i ;;
2)
b=--backup ;;
3)
c=-p ;;
#4)
#4= -R -v;;
0)
main;;
esac
echo "$a"
echo "$b"
echo "$c"
printf "\nType the name of the file you wish to copy/backup: "
read $fl1
printf "\nType the destination file: "
read $des1
cp $a $b $c $fl1 $des1
done
}
One problem is that read $a etc is wrong: you should write read a if you need the read at all. As it stands, the value read is stored in the variable with the name stored in $a.
Another problem is that it is far from clear to the innocent user that they're supposed to enter 3 lines of information before the script will continue, but the three read lines force that.
Another problem is that you don't read into $choice (via read choice) so the for loop has nothing to do.
Another problem is that your script will inherit the values of any environment variables that happen to be the same as the names of the variables you're using.
Another problem is that you don't quote the file names. It mostly won't matter unless you have a name that contains spaces or other similarly awkward characters.
A cosmetic issue is that the printf statement is ridiculously long. Use one printf per line. Or use echo. Stuff that scrolls off the RHS of the page is bad (though I don't regard 80 characters as a fixed length for lines, there's a quadratic penalty for lines that are longer than 80 — as (length-80)2 increases, the pain of the longer line goes up.
At another level altogether, the interface is modestly grotesque. As an exercise in shell scripting, it makes sense. As an exercise in how to design good shell scripts, it is a very bad design.
A design that might make sense is:
Set variables to empty: a=""; b=""; c=""; etc.
Offer a range of choices similar to those given now, but add an option to execute the command, and another to abandon ship.
Have a loop that reads choices, and sets flags.
When the user chooses execute, exit the loop and prompt for the file names.
If all's well, execute the command.
Note that you should check that the read commands work; if they don't, fail safe (don't damage anything).
Putting all those together (with some slight differences, but the same overall effect — witness the use of local for the variables):
fileCpy()
{
local a b c file dest
echo "Choices:"
echo "0. Return to main menu"
echo "1. Interactive copy, answer yes/no before doing the copy"
echo "2. Make backups of existing destination files"
echo "3. Preserve file attributes"
echo "4. Do a recursive copy"
echo "5. Execute the copy"
while printf "Your choice: " && read choice
do
[ -z "$choice" ] && return 1 # Empty input - failure
case "$choice" in
(0) return 0;;
(1) a="-i";;
(2) b="--backup";;
(3) c="-p";;
(4) d="-R";;
(5) break;;
(*) echo "Unrecognized response ($choice); please enter 0..5";;
esac
done
[ "$choice" != 5 ] && return 1 # EOF - failure
printf "Type the name of the file you wish to copy/backup: "
read file
[ -z "$file" ] && return 1 # Empty file name - failure
printf "Type the name of the destination file/directory: "
read dest
[ -z "$dest" ] && return 1 # Empty file name - failure
cp $a $b $c "$file" "$dest"
}
Test code:
echo "a=$a b=$b c=$c file=$file dest=$dest"
if fileCpy
then : OK
else echo "Failed"
fi
echo "a=$a b=$b c=$c file=$file dest=$dest"
The last block is a simple test harness. It reports on the values of the variables used inside the function, runs the function, reports if the function failed, and re-echoes the variables to demonstrate that they've not been set.
I would not use that interface unless you paid me to do so, but it more or less meets the goal of a training exercise.
if you want to do menu, you can use select construct (or case/esac). See here for info

Resources