Simplify Dynamic Menu in Bash - bash

I'm working on a custom bash script (with which I have very little experience) and I'm looking for some help with a menu function. My script had several different menus accomplishing different functions so I decided to create one menu function that could be called and customized with set variables. It's unclean and ugly, but I've learned a lot making it. Can anyone shed some light on where I could have simplified or done things differently or more cleanly? Thanks in advance!
#!/bin/bash
# set colors
red='tput setaf 1'
blue='tput setaf 4'
green='tput setaf 2'
bold='tput bold'
normal='tput sgr0'
# define function to reset screen to title
function reset {
clear
$bold
$blue
echo -e "lineage build script\n\n"
$normal
}
function title {
reset
$bold
echo -e "$1\n"
$normal
}
# function to create menu
# menu "<title(required)>" "<question(required)>" <all(required)> "<option1>"...
# <title> - Page title
# <question> - Question to be posed before the menu
# <all> - Whether or not to present a menu choice for "All"
# <option#> - List of options to present to the user
function menu {
# set input at 255 (max error level for return function later)
input=255
# check input to see if it's acceptable (within presented range and a number)
while [[ $input -ge $(($counter+1)) ]] || [[ $((input)) != $input ]]; do
# a call to a previously defind function establishing consistent page feel.
title "$1"
echo -e "$2"
# present warning if input was invalid
if ! [[ $input =~ "255" ]]; then
$red
echo -e "Invalid entry. Please try again.\n"
$normal
else
echo -e "\n"
fi
counter=0
# present list items for each variable passed to function starting at position 4
# in order to skip the first 3 (title, question, all)
for i in "${#:4}"; do
counter=$(($counter+1))
echo -e "$counter) $i"
done
# present "All" option if variable 3 is any form of yes
if [[ $3 =~ (y|yes|Y|YES) ]]; then $green; counter=$(($counter+1)); echo -e "$counter) All"; $normal; fi
# present "Exit" option
$red; counter=$(($counter+1)); echo -e "$counter) Exit\n"; $normal
# gather input
read -N 1 -p "[1-$counter]: " input
done
# bail if exit was chosen
if [[ $input == $counter ]]; then $red; echo -e "\nExit"; exit; fi
# pass choice back to script as return code
return $input
}
# call the menu function for testing
menu "Main Menu" "What would you like to do?" y "option 1" "option 2" "option 3" "option 4" "option 5"
#echo return code just to verify function
echo -e "\nYou chose $?"

Let's see if this helps in anyway (in retro). Not only shell scripting related.
Naming
The reset function is a verb, where as title is not. But it could be, indicating what it does and that it's not a variable.
Printing
You consistently used echo -e for printing. As Charles Duffy pointed out in a comment below, you are sacrificing POSIX compatibility by using -e.
It is not possible to use echo portably across all POSIX systems unless both -n (as the first argument) and escape sequences are omitted.
The printf utility can be used portably to emulate any of the traditional behaviors of the echo utility as follows (assuming that IFS has its standard value or is unset)
http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html
Whatever actual way you will use for printing: how about making that a function? If you have to change, it's one place to go. Hopefully this won't lead you to a big list of overloaded functions (for the various printing options).
Local
One reason I liked your program is that you kept variables where they belong, locally to the functions. Maybe you could step that up by actually denoting (where it makes sense) them local.
local [option] name[=value]
https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
Shift
To continue the previous point I usually prefer to store / move the ordered parameters to local variables. For example title_text=$1. In the menu function it would allow you to use shift. You would process "title", "menu name" and "all" options then you end up with only the options list in the $# variable. A list of option names. Very handy if you later want to refactor some parts of the function (without breaking order) or you want to pass said list to another function to deal with the options.
shift [n]
Shift the positional parameters to the left by n. The positional
parameters from n+1 … $# are renamed to $1 … $#-n. Parameters
represented by the numbers $# to $#-n+1 are unset. n must be a
non-negative number less than or equal to $#. If n is zero or greater
than $#, the positional parameters are not changed. If n is not
supplied, it is assumed to be 1. The return status is zero unless n is
greater than $# or less than zero, non-zero otherwise.
https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
Exit codes
I have found your way of use for the return values (exit codes) nice. It's somewhat a pity that the only situation where it's not used is the actual case when you call exit. What I mean is that without going into the menu function, I was confused for a little while why I never see "You chose" if I "exit".

Related

Is there any better way to repeatedly call function for each case statement

I wrote a bash script to automate lamp installing in my server, they work great but there's a problem regarding its readability.
# Asking if want to install PHP or build from source
read -p "This script will install PHP, do you want to install it (y/n)?" phpchoice
case "$phpchoice" in
[Yy]* ) isinstallphp=yes;;
[Nn]* ) isinstallphp=no;;
* ) echo "invalid";;
esac
if [ isinstallphp == "yes" ]; then
# Prompt for php version
options=("php5.6" "php7.0" "php7.1" "php7.2" "php7.3" "php7.4" "php8.0" "php8.1")
function do_something () {
echo -e "${Ye}You picked ${Wh}$php_opt${Ye}, will be installing that instead${Nc}"
}
select php_opt in "${options[#]}"; do
case "$php_opt,$REPLY" in
php5.6,*|*,php5.6) do_something; break ;;
php7.0,*|*,php7.0) do_something; break ;;
php7.1,*|*,php7.1) do_something; break ;;
php7.2,*|*,php7.2) do_something; break ;;
php7.3,*|*,php7.3) do_something; break ;;
php7.4,*|*,php7.4) do_something; break ;;
php8.0,*|*,php8.0) do_something; break ;;
php8.1,*|*,php8.1) do_something; break ;;
esac
done
fi
The case statement part where I put do_something function looks messy, all the function do is echo which php option user choose in color. Is there any way to shorten the code ?
The case statement part where I put do_something function looks messy, all the function do is echo which php option user choose in color. Is there any way to shorten the code ?
It appears that you are accommodating the dual possibilities that the user chooses an option by typing its number or by typing the item text. One simplification would be to require the user to make their selection by item number only, in which case you would only need to confirm that $php_opt was not set to NULL. The case statement would not be required at all.
If you want to retain the full input functionality of the current script, then you can still do better than the current code. Instead of a long case statement covering all the options, check whether $php_opt is NULL, and if it is, check whether $REPLY is equal to one of the options. There is a variety of ways you could implement that, but I like this one:
validate_option() {
local choice=$1
while [[ $# -gt 1 ]]; do
shift
[[ "$choice" = "$1" ]] && return 0
done
return 1
}
# ...
select php_opt in "${options[#]}"; do
if [[ -n "$php_opt" ]] || validate_option "$REPLY" "${options[#]}"; then
do_something
fi
done
I find that a lot clearer. Note also that the validate_option function is reusable, and that this approach is completely driven by the option list, so you don't need to modify this code if the option list changes.
Addendum
You additionally raised a question about your given do_something function not printing the selected option when the user enters the option value instead of its number. This will have been a behavior of your original code, too. It arises from the fact that the select command sets the specified variable (php_opt in your case) to NULL in the event that the user enters a non-empty response that is not one of the menu item numbers.
If you want to avoid that, and perhaps also to have the selected value in the form of the option string for other, later processing, then you probably want to address that issue in the body of the select statement. Something like this variation might do, for example:
select php_opt in "${options[#]}"; do
if [[ -n "$php_opt" ]] ||
{ php_opt=$REPLY; validate_option "$php_opt" "${options[#]}"; }; then
do_something
fi
done
That copies the user-entered text into $php_opt in the event that the user entered something other than an option number. Do note that that behavior change might have other effects later in the script, too.
What you're checking is if $php_opt or $REPLY is in the $options array:
array_contains() {
local -n ary=$1
local elem=$2
local IFS=$'\034'
[[ "${IFS}${ary[*]}$IFS" == *"${IFS}${elem}$IFS"* ]]
}
#...
select php_opt in "${options[#]}"; do
if array_contains options "$php_opt" || array_contains options "$REPLY"; then
do_something
break
fi
done
local -n requires bash v4.3+
Octal 034 is the ASCII "FS" character, it's unlikely to be in your data
select sets the given variable name to the selected string, only if a valid selection is made. If an invalid selection is made (ie. not one of the available numbers), $php_opt will be empty. Even if a valid selection was made in a previous iteration, it will be reset to empty.
Hence you just need to test if $php_opt is empty or not:
select php_opt in "${options[#]}"; do
# check for literal reply
reply=${REPLY,,}
for i in "${options[#]}"; do
if [[ "$i" == "$reply" ]]; then
php_opt=$reply
break
fi
done
if [[ "$php_opt" ]]; then
do_something
break
else
echo "$REPLY: invalid selection"
fi
done
Or with case:
select php_opt in "${options[#]}"; do
# check for literal reply
reply=${REPLY,,}
for i in "${options[#]}"; do
if [[ "$i" == "$reply" ]]; then
php_opt=$reply
break
fi
done
case $php_opt in
'') echo "$REPLY: invalid selection";;
*) do_something; break
esac
done
From help select:
If the line consists of the number corresponding to one of the displayed words, then NAME is set to that word. If the line is empty, WORDS and the prompt are redisplayed. If EOF is read, the command completes. Any other value read causes NAME to be set to null. The line read is saved in the variable REPLY.
Also note that you are missing $ in your if statement.
edit: as per the comment, I added a check for a literal name, as well as a number. It's case insensitive, PHP7.0 is valid input. Using the original array is easier to maintain, than a second list of patterns in a case statement.
To make things tidier, you can also put do_something after select (so you only need break). Maybe something like [[ "$reply" == [Qq] ]] && exit too. Similarly in your very first case statement, just do exit if the reply is no.

How to write a bash function that saves user input to new variable names

Does anyone know how to convert this mac bash statement into a reusable function?
test $var || echo -e "\n$(tput bold)Question for User$(tput sgr0)" && read var
I've got about 30 similar statements in a row and am trying to make it a bit more efficient. I'm thinking this function would be fired with something like this:
userInput "Question for User" var
If your script runs under bash (not zsh or dash or...), you can do this:
newline=$'\n'
tput_bold=$(tput bold)
tput_sgr0=$(tput sgr0)
userInput() {
if [[ -z "${!2}" ]]; then
read -p "${newline}${tput_bold}$1${tput_sgr0}" $2
fi
}
userInput "Question for User" var
Notes: The critical trick here is to use ${! } to do an indirect variable reference -- the ! makes it essentially dereference twice, so 2 -> var -> the value of $var. Also, echo -e is unreliable; depending on a variety of factors, it might interpret escape characters in the string, or it might print "-e " as part of the output. bash's read -p (prompt) option is much better. Finally, as #l0b0 suggested, I ran the tput command just twice and put the results in variables (along with newline, needed because I'm not using echo -e).
Some hints:
Use More Quotes™. test on its own (which is the same command as test $var if var is empty) is falsy and test some_string in general is truthy (as long as the string doesn't contain special characters used in test expressions).
The tput stuff is simply string formatting and has no other side effects, so they can be put into variables to avoid two forks per run.
You cannot pass the variable name to a function and have read populate it. You're best off printfing the user response in the function and capturing the string in the caller.
Use read's -p option if available.
I finally managed to piece together this function, which does exactly what I'm trying to do:
userInput () {
if [[ ${!2} == "" ]]; then
echo -e "\n$(tput bold)$1$(tput sgr0)" && read newinput
printf -v $2 "$newinput"
fi
}
This function tests to see if the variable has a value. If it doesn't, then it captures that information from the user and sets the variable value. Here's a few examples.
If $name isn't set, then this command captures and sets it:
userInput "Enter Your Name" name`
Enter Your Name
John Doe
echo $name
John Doe
If $name is already set, then running this function will do nothing.
name="Frank Smith";
userInput "Your Name" name
// Function does nothing, since $name is set
echo $name
Frank Smith
I've read it depends on some sort of parameter expansion / substitution, but can't really explain what that is or why this works.

Print out a list of all cases of a switch

Curious question. Is it somehow possible to print out all cases of a certain switch-case automatically in bash? In a way, such that it stays as maintainable as possible, meaning that one does not have to add any more code if a new case is added to print out that same case.
For instance, that would be useful if the cases represented commands. A help function could then print out all available commands.
There is no direct way to achieve this, but you can use an array to maintain your choices:
# Define your choices - be sure not to change their order later; only
# *append* new ones.
choices=( foo bar baz )
# Make the array elements the case branches *in order*.
case "$1" in
"${choices[0]}")
echo 'do foo'
;;
"${choices[1]}")
echo 'do bar'
;;
"${choices[2]}")
echo 'do baz'
;;
*)
echo "All choices: ${choices[#]}"
esac
This makes the branches less readable, but it's a manageable solution, if you maintain your array carefully.
Note how the branch conditions are enclosed in "..." so as to prevent the shell from interpreting the values as (glob-like) patterns.
That said, as chepner points out, perhaps you want to define your choices as patterns to match variations of a string:
In that event:
Define the pattern with quotes in the choices array; e.g., choices=( foo bar baz 'h*' )
Reference it unquoted in the case branch; e.g., ${choices[3]})
bash does not give you access to the tokens it parses and does not save case strings (which can be glob expressions as well).
Unfortunately, that means you will not be able to DRY your code in the way you were hoping.
After a while of hacking together several Bash 4 features I've got this one.
#!/bin/bash
set -euo pipefail
# create coprocess with 2 descriptors so we can read and write to them
coproc CAT { cat ; }
# creme de la creme of this solution - use function to both collect and select elements
function element {
echo "$1" >&${CAT[1]}
echo "$1"
}
case "$1" in
$(element A))
echo "Your choice is: A"
;;
$(element B))
echo "Your choice is: B"
;;
*)
echo "Your choice is not present in available options: $1"
# close writing descriptor
exec {CAT[1]}>&-
#read colected options into an array
mapfile -t OPTIONS <&${CAT[0]}
echo "Available options are: [ ${OPTIONS[#]} ]"
;;
esac
Output:
Your choice is not present in available options: C
Available options are: [ A B ]
There are 2 parts for this solution:
coproc - which creates subprocess for reading and writing from subshell
function element which both writes into descriptors of coproc subrocess and returns it's argument so we can use it inside case ... esac
If handling all options should be done outside of case then you can use ;;& feature of Bash 4 case statement which forces checking every statement inside case (usually - i.e. ;; - it stops after first match). This checking is needed so we can collect all options into an array later
There is probably a lot of reasons not to use this (limits of data which can be safely stored in descriptor without reading them being one of those) and I welcome all comments which can make this solution better.
You could have your script inspect itself:
#!/bin/bash
case_start=$(("$LINENO" + 2)) #store line number of start
case "$1" in
simple_case) echo "this is easy";;
--tricky)
echo "This is a tricky"
(echo "Multiline")
echo "Statement"
;;
-h|--help) echo "heeeelp me";;
-q|--quiet) ;;
*) echo "unknown option";;
esac
case_end=$(("$LINENO" - 2)) #store line number of end
# - take lines between $case_start and $case_end
# - replace newlines with spaces
# - replace ";;" with newlines
# -=> now every case statement should be on its own line
# - then filter out cases: delete everything after the first ")" including the ")" and trim blanks
cases_available=`sed -n "${case_start},${case_end}p" $0 | sed 's/#.*//' | tr '\n' ' ' | sed 's/;;/\n/g' | sed 's/).*//;s/[[:blank:]]*//'`
echo -e "cases_available:\n\n$cases_available"
this would print:
cases_available:
simple_case
--tricky
-h|--help
-q|--quiet
*
There are some pitfalls with this:
Comments or strings inside the case statement with a ";;" in it will break stuff
Can't handle nested switch case statements.
Dunno if I understood correctly your question, but you can do something like this page says to print out all the available cases as default choice:
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status anacron
;;
restart)
stop
start
;;
condrestart)
if test "x`pidof anacron`" != x; then
stop
start
fi
;;
*)
echo $"Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
I think the best approach and also in OP regards is to actually print the switch case structure to a new file and then source it in the original file if needed.
echo "case "'"$1"'" in" > case.sh
echo " test)" >> case.sh
echo " echo "This case!"" >> case.sh
echo " ;;" >> case.sh
echo "esac" >> case.sh
chmod+x case.sh
./case.sh test
#This case!
This way you can easily use variables to build your switch / case condition.

Understanding parameters in a function

I found this function:
findsit()
{
OPTIND=1
local case=""
local usage="findsit: find string in files.
Usage: fstr [-i] \"pattern\" [\"filename pattern\"] "
while getopts :it opt
do
case "$opt" in
i) case="-i " ;;
*) echo "$usage"; return;;
esac
done
shift $(( $OPTIND - 1 ))
if [ "$#" -lt 1 ]; then
echo "$usage"
return;
fi
find . -type f -name "${2:-*}" -print0 | \
xargs -0 egrep --color=always -sn ${case} "$1" 2>&- | more
}
I understand the output and what it does, but there are some terms I still don't understand and find it hard to find a reference, but believe they would be useful to learn in my programming. Can anyone quickly explain them? Some don't have man pages.
local
getopts
case
shift
$#
${2:-*}
2>&-
Thank you.
local: Local variable. Let's say you had a variable called foo in your program. You call a function that also has a variable foo. Let's say the function changes the value of foo.
Try this program:
testme()
{
foo="barfoo"
echo "In function: $foo"
}
foo="bar"
echo "In program: $foo"
testme
echo "After function in program: $foo"
Notice that the value of $foo has been changed by the function even after the function has completed. By declaring local foo="barfoo" instead of just foo="barfoo", we could have prevented this from happening.
case: A case statement is a way of specifying a list of options and what you want to do with each of those options. It is sort of like an if/then/else statement.
These two are more or less equivelent:
if [[ "$foo" == "bar" ]]
then
echo "He said 'bar'!"
elif [[ "$foo" == "foo" ]]
then
echo "Don't repeat yourself!"
elif [[ "$foo" == "foobar" ]]
then
echo "Shouldn't it be 'fubar'?"
else
echo "You didn't put anything I understand"
fi
and
case $foo in
bar)
echo "He said 'bar'!"
;;
foo)
echo "Don't repeat yourself!"
;;
foobar)
echo "Shouldn't it be 'fubar'?"
;;
*)
echo "You didn't put anything I understand"
;;
esac
The ;; ends the case option. Otherwise, it'll drop down to the next one and execute those lines too. I have each option in three lines, but they're normally combined like
foobar) echo "Shouldn't it be 'fubar'?";;
shift: The command line arguments are put in the variable called $*. When you say shift, it takes the first value in that $* variable, and deletes it.
getopts: Getopts is a rather complex command. It's used to parse the value of single letter options in the $# variable (which contains the parameters and arguments from the command line). Normally, you employ getopts in a while loop and use case statement to parse the output. The format is getopts <options> var. The var is the variable that will contain each option one at a time. The specify the single letter parameters and which ones require an argument. The best way to explain it is to show you a simple example.
$#: The number of parameters/arguments on the command line.
${var:-alternative}: This says to use the value of the environment variable $var. However, if this environment variable is not set or is null, use the value alternative instead. In this program ${2:-*} is used instead. The $2 represents the second parameter of what's left in the command line parameters/arguments after everything has been shifted out due to the shift command.
2>&-: This moves Standard Error to Standard Output. Standard Error is where error messages are put. Normally, they're placed on your terminal screen just like Standard Output. However, if you redirect your output into a file, error messages are still printed to the terminal window. In this case, redirecting the output to a file will also redirect any error messages too.
Those are bash built-ins. You should read the bash man page or, for getopts, try help getopts
One at a time (it's really annoying to type on ipad hence switched to laptop):
local lets you define local variables (within the scope of a function)
getopts is a bash builtin which implements getopt-style argument processing (the -a, -b... type arguments)
case is the bash form for a switch statement. The syntax is
case: case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
shift shifts all of the arguments by 1 (so that the second argument becomes the first, third becomes second, ...) similar to perl shift. If you specify an argument, it will shift by that many indices (so shift 2 will assign $3 -> $1, $4 -> $2, ...)
$# is the number of arguments passed to the function
${2:-*} is a default argument form. Basically, it looks at the second argument ($2 is the second arg) and if it is not assigned, it will replace it with *.
2>&- is output redirection (in this case, for standard error)

Correct way to check for a command line flag in bash

In the middle of a script, I want to check if a given flag was passed on the command line. The following does what I want but seems ugly:
if echo $* | grep -e "--flag" -q
then
echo ">>>> Running with flag"
else
echo ">>>> Running without flag"
fi
Is there a better way?
Note: I explicitly don't want to list all the flags in a switch/getopt. (In this case any such things would become half or more of the full script. Also the bodies of the if just set a set of vars)
An alternative to what you're doing:
if [[ $* == *--flag* ]]
See also BashFAQ/035.
Note: This will also match --flags-off since it's a simple substring check.
I typically see this done with a case statement. Here's an excerpt from the git-repack script:
while test $# != 0
do
case "$1" in
-n) no_update_info=t ;;
-a) all_into_one=t ;;
-A) all_into_one=t
unpack_unreachable=--unpack-unreachable ;;
-d) remove_redundant=t ;;
-q) GIT_QUIET=t ;;
-f) no_reuse=--no-reuse-object ;;
-l) local=--local ;;
--max-pack-size|--window|--window-memory|--depth)
extra="$extra $1=$2"; shift ;;
--) shift; break;;
*) usage ;;
esac
shift
done
Note that this allows you to check for both short and long flags. Other options are built up using the extra variable in this case.
you can take the straight-forward approach, and iterate over the arguments to test each of them for equality with a given parameter (e.g. -t, --therizinosaurus).
put it into a function:
has_param() {
local term="$1"
shift
for arg; do
if [[ $arg == "$term" ]]; then
return 0
fi
done
return 1
}
… and use it as a predicate in test expressions:
if has_param '-t' "$#"; then
echo "yay!"
fi
if ! has_param '-t' "$1" "$2" "$wat"; then
echo "nay..."
fi
if you want to reject empty arguments, add an exit point at the top of the loop body:
for arg; do
if [[ -z "$arg" ]]; then
return 2
fi
# ...
this is very readable, and will not give you false positives, like pattern matching or regex matching will.
it will also allow placing flags at arbitrary positions, for example, you can put -h at the end of the command line (not going into whether it's good or bad).
but, the more i thought about it, the more something bothered me.
with a function, you can take any implementation (e.g. getopts), and reuse it. encapsulation rulez!
but even with commands, this strength can become a flaw. if you'll be using it again and again, you'll be parsing all the arguments each time.
my tendency is to favor reuse, but i have to be aware of the implications. the opposed approach would be to parse these arguments once at the script top, as you dreaded, and avoid the repeated parsing.
you can still encapsulate that switch case, which can be as big as you decide (you don't have to list all the options).
You can use the getopt keyword in bash.
From http://aplawrence.com/Unix/getopts.html:
getopt
This is a standalone executable that has been around a long time.
Older versions lack the ability to handle quoted arguments (foo a "this
won't work" c) and the versions that can, do so clumsily. If you are
running a recent Linux version, your "getopt" can do that; SCO OSR5,
Mac OS X 10.2.6 and FreeBSD 4.4 has an older version that does not.
The simple use of "getopt" is shown in this mini-script:
#!/bin/bash
echo "Before getopt"
for i
do
echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
echo "-->$i"
done
I've made small changes to the answer of Eliran Malka:
This function can evaluate different parameter synonyms, like "-q" and "--quick". Also, it does not use return 0/1 but an echo to return a non-null value when the parameter is found:
function has_param() {
local terms="$1"
shift
for term in $terms; do
for arg; do
if [[ $arg == "$term" ]]; then
echo "yes"
fi
done
done
}
# Same usage:
# Assign result to a variable.
FLAG_QUICK=$(has_param "-q --quick" "$#") # "yes" or ""
# Test in a condition using the nonzero-length-test to detect "yes" response.
if [[ -n $(has_param "-h --help" "$#") ]]; then;
echo "Need help?"
fi
# Check, is a flag is NOT set by using the zero-length test.
if [[ -z $(has_param "-f --flag" "$#") ]]; then
echo "FLAG NOT SET"
fi
The modification of Dennis Williamson's answer with additional example for a argument in the short form.
if [[ \ $*\ == *\ --flag\ * ]] || [[ \ $*\ == *\ -f\ * ]]
It solves the problem of false positive matching --flags-off and even --another--flag (more popular such case for an one-dashed arguments: --one-more-flag for *-f*).
\ (backslash + space) means space for expressions inside [[ ]]. Putting spaces around $* allows to be sure that the arguments contacts neither line's start nor line's end, they contacts only spaces. And now the target flag surrounded by spaces can be searched in the line with arguments.
if [ "$1" == "-n" ]; then
echo "Flag set";
fi
Here is a variation on the most voted answer that won't pick up false positives
if [[ " $* " == *" -r "* ]]; then
Not an alternative, but an improvement, though.
if echo $* | grep -e "\b--flag\b" -q
Looking for word boundaries will make sure to really get the option --flag and neither --flagstaff nor --not-really--flag

Resources