case statement and comparison operator - shell

Having trouble with a case statement where the length of a string is evaluated. More in particular the part that isn´t working corresponds with the line containing [[ ${#numPase} -lt 8 ]]).
read numPase
case $numPase in
q|Q) var_cntrl_pase_ok=false
;;
(*[!0-9]*|'')
echo " "
echo "Variable must contain integers. Press key to continue."
read
;;
[[ ${#numPase} -lt 8 ]])
echo " "
echo "Variable must have 8 digits. Press key to continue."
read
;;
esac
NOTE:
Variable numPase takes value from user input.
First case determines if user has pressed q or Q to quit menu loop. Second and third cases are self-explanatory
Why isn´t the 8 digit validation working?

The pattern must be one or more globs separated by vbars. Any text in the pattern will be treated as a glob and never as a command.

Related

How to can I count the lenght of an parameter without any spaces or numbers?

I am trying to count the seize of an parameter without the numbers and spaces, like if someone types in "Hello player1" it has to echo "11 characters". I have tried using ${#value} but this counts numbers and spaces.
if [ -z "$1" ]
then
echo "write at least 1 word"
else
for value in "$#"
do
echo "${value//[[:digit:]]/}"
done
echo ${#value}
fi
The result of the code
as you can see in the image it counts only the last parameter and counts the numbers what I don't want
Results of the second code
Break it into two steps.
set -- "abc 123" "d12"
for value do # in "$#" is default, don't need to say it
valueTrimmed=${value//[[:digit:][:space:]]/}
echo "The string '$value' has ${#valueTrimmed} valid characters"
done
...properly emits as output:
The string 'abc 123' has 3 valid characters
The string 'd12' has 1 valid characters
bash does not support nesting parameter expansions; you cannot do this in one step as a shell builtin (it could be done in a pipeline, but only with an unreasonably high performance cost).

Shell Scripting - Numeric Checks and if statement questions

I'm relatively new here and to the coding world. I'm currently taking a class in Shell Scripting and I'm a bit stuck.
I'm trying to do a little extra credit and get the script to check for command line arguments and if none or only 1 is given, prompt the user to input the missing values.
For the most part I've been able to get most of it to work except for when it comes to the numeric check part. I'm not completely sure that I am doing the nested if statements correctly because it's displaying both the "if" echo and the "else" echo.
My script so far:
q=y
# Begins loop
until [[ $q == n ]];do
# Checks command line arguments
if [[ $# -lt 2 ]];then
# Asks for second number if only 1 argument.
if [[ $# == 1 ]];then
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
echo "You've chosen $1 as your first number and $y as your second number."
break
else
echo "This is not a valid value, please try again."
fi
# Asks for both numbers if no arguments.
else
read -r -p "Please enter your first number: " x
if [[ x =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
read -r -p "Please enter your second number: " y
if [[ y =~ [1-9] ]];then
break
else
echo "This is not a valid value, please try again."
fi
echo "You've chosen $x as your first number and $y as your second number."
fi
# If both command line arguments are provided, echo's arguments, and sets arguments as x and y values.
else
echo "You've chosen $1 as your first number and $2 as your second number."
x=$1
y=$2
fi
read -r -p "Would you like to try again? (n to exit): " q
done
When I run it I get this for output:
Please enter your first number: 1
This is not a valid value, please try again.
Please enter your second number: 2
This is not a valid value, please try again.
You've chosen 1 as your first number and 2 as your second number.
Please enter your first number:
And will just continue to loop without breaking. Any help/guidance would be greatly appreciated, thank you.
In your expression:
if [[ x =~ [1-9] ]]; then
You are actually comparing the string literal "x" with the regex. What you want is the variable:
if [[ $x =~ [1-9] ]]; then
This will interpolate the variable first in order to compare the variable's value with the regex. I think this change also applies to some of the other comparison expressions in your code.
However, as glenn jackman and user1934428 have commented, this will also match things like foo1bar, which is probably not what you want. To fix this, you can add start/end matchers to your regex. Finally, you may want to match even if the input has leading or trailing spaces. One way to do this is to add some [[:space:]]*'s to match zero or more spaces around your [1-9]:
if [[ $x =~ ^[[:space:]]*[1-9][[:space:]]*$ ]]; then
So, to break down the regex:
^ start of input
[[:space:]]* zero or more whitespaces
[1-9] a single digit, 1-9
[[:space:]]* zero or more whitespaces
$ end of the input
I'm assuming from your question than you only want to match on a single digit, not, for example, 12, or the digit 0. To match those would require a couple more regex tweaks.
and...glob pattern
Just because glen jackman's answer led me down a bash man page adventure 🏄 and I wanted to try them out, this is a glob pattern version (note the == instead of =~):
if [[ $x == *([[:space:]])[1-9]*([[:space:]]) ]]; then
It's basically the same pattern. But notably, glob patterns seem to be implicitly anchored to the start/end of the string being matched (they are tested against the entire string) so they don't need the ^ or $, while regular expressions match against substrings by default, so they do need those additions to avoid foo1bar matching. Anyway, probably more than you cared to know.
Here's an alternate implementation, for your consideration: hit me up with any questions
#!/usr/bin/env bash
get_number() {
local n
while true; do
read -rp "Enter a number between 1 and 9: " n
if [[ $n == [1-9] ]]; then
echo "$n"
return
fi
done
}
case $# in
0) first=$(get_number)
second=$(get_number)
;;
1) first=$1
second=$(get_number)
;;
*) first=$1
second=$2
;;
esac
# or, more compact but harder to grok
[[ -z ${first:=$1} ]] && first=$(get_number)
[[ -z ${second:=$2} ]] && second=$(get_number)
echo "You've chosen $first as your first number and $second as your second number."
This uses:
a function to get a a number from the user, so you don't have so much duplicated code,
a case statement to switch over the $# variable
input validation with the == operator within [[...]] -- this operator is a pattern matching operator, not string equality (unless the right-hand operand is quoted)
Note that [[ $x =~ [1-9] ]] means: "$x contains a character in the range 1 to 9" -- it does not mean that the variable is a single digit. If x=foo1bar, then the regex test passes.

Why does a string equal zero in bash test

I have an array which I am using to generate a list which a user can choose from made like this.
list=(a b c d)
n=0
for x in ${list[#]}
do echo $n\)$x
n=$((n++)
done
read -p "Pick an item:" choice
I want to allow only valid options to be chosen so I am checking like this.
if [[ $choice -gt ${#list[#]} && $choice -lt -1 ]]
then ...
else
echo "not a valid choice"
The issue I am having is all strings evaluate at equal to zero. ie [[ "I am a duck" -eq 0 ]] is True, as is (( "I am a duck" == 0 )). Is there a way to make all string and number comparison evaluate to false? I know I can check for a string with [[ $choice =~ [A-Za-z]+ ]], but I am wondering if there is a way without regular expressions?
EDIT
Sorry, I should have tested the "I am a duck" statement before I put it down. It doesn't like the spaces. However, "I_am_a_duck" does evaluate to 0. This explained by chepner below.
-gt, because it is intended to compare integers, triggers the same behavior for strings as seen in an arithmetic expression: the string is treated as the name of a parameter, and that parameter is expanded (repeat the process if necessary) until you get an integer. If there is no parameter by that name, 0 is substituted.
That said, you could weasel your way out of the problem by number your items starting at one and using
if (( choice < 1 || choice > ${#list[#]} )); then
echo "not a valid choice"
since now any non-integer inputs will be treated as 0, which is indeed less than the lower limit of 1.
I would use select for this and not deal with behaviour of strings in arithmetic contexts (as explained in chepner's answer) at all:
list=(a b c d)
PS3='Pick an item: '
select opt in "${list[#]}"; do
case $opt in
[abcd]) echo "Choice: $opt ($REPLY)" ;;
*) echo "Illegal choice" ;;
esac
done
This will keep looping; to continue after a valid selection, there has to be a break somewhere. The main benefit is select taking care of invalid inputs for you, and you don't have to build the menu yourself.

bash: choose default from case when enter is pressed in a "select" prompt

I'm prompting questions in a bash script like this:
optionsAudits=("Yep" "Nope")
echo "Include audits?"
select opt in "${optionsAudits[#]}"; do
case $REPLY in
1) includeAudits=true; break ;;
2) includeAudits=false; break ;;
"\n") echo "You pressed enter"; break ;; # <--- doesn't work
*) echo "What's that?"; exit;;
esac
done
How can I select a default option when enter is pressed? The "\n" case does not catch the enter key.
To complement Aserre's helpful answer, which explains the problem with your code and offers an effective workaround, with background information and a generic, reusable custom select implementation that allows empty input:
Background information
To spell it out explicitly: select itself ignores empty input (just pressing Enter) and simply re-prompts - user code doesn't even get to run in response.
In fact, select uses the empty string to signal to user code that an invalid choice was typed.
That is, if the output variable - $opt, int this case - is empty inside the select statement, the implication is that an invalid choice index was typed by the user.
The output variable receives the chosen option's text - either 'Yep' or 'Nope' in this case - not the index typed by the user.
(By contrast, your code examines $REPLY instead of the output variable, which contains exactly what the user typed, which is the index in case of a valid choice, but may contain extra leading and trailing whitespace).
Note that in the event that you didn't want to allow empty input, you could
simply indicate to the user in the prompt text that ^C (Ctrl+C) can be used to abort the prompt.
Generic custom select function that also accepts empty input
The following function closely emulates what select does while also allowing empty input (just pressing Enter). Note that the function intercepts invalid input, prints a warning, and re-prompts:
# Custom `select` implementation that allows *empty* input.
# Pass the choices as individual arguments.
# Output is the chosen item, or "", if the user just pressed ENTER.
# Example:
# choice=$(selectWithDefault 'one' 'two' 'three')
selectWithDefault() {
local item i=0 numItems=$#
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$#"; do
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && break # empty input
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected item, if any.
[[ -n $index ]] && printf %s "${#: index:1}"
}
You could call it as follows:
# Print the prompt message and call the custom select function.
echo "Include audits (default is 'Nope')?"
optionsAudits=('Yep' 'Nope')
opt=$(selectWithDefault "${optionsAudits[#]}")
# Process the selected item.
case $opt in
'Yep') includeAudits=true; ;;
''|'Nope') includeAudits=false; ;; # $opt is '' if the user just pressed ENTER
esac
Alternative implementation that lets the function itself handle the default logic:Thanks, RL-S
This implementation differs from the above in two respects:
It allows you to designate a default among the choices, by prefixing it with !, with the first choice becoming the default otherwise. The default choice is printed with a trailing [default] (and without the leading !). The function then translates empty input into the default choice.
The selected choice is returned as a 1-based index rather than the text. In other words: you can assume that a valid choice was made when the function returns, and that choice is indicated by its position among the choices given.
# Custom `select` implementation with support for a default choice
# that the user can make by pressing just ENTER.
# Pass the choices as individual arguments; e.g. `selectWithDefault Yes No``
# The first choice is the default choice, unless you designate
# one of the choices as the default with a leading '!', e.g.
# `selectWithDefault Yes !No`
# The default choice is printed with a trailing ' [default]'
# Output is the 1-based *index* of the selected choice, as shown
# in the UI.
# Example:
# choice=$(selectWithDefault 'Yes|No|!Abort' )
selectWithDefault() {
local item i=0 numItems=$# defaultIndex=0
# Print numbered menu items, based on the arguments passed.
for item; do # Short for: for item in "$#"; do
[[ "$item" == !* ]] && defaultIndex=$(( $i + 1)) && item="${item:1} [default]"
printf '%s\n' "$((++i))) $item"
done >&2 # Print to stderr, as `select` does.
# Prompt the user for the index of the desired item.
while :; do
printf %s "${PS3-#? }" >&2 # Print the prompt string to stderr, as `select` does.
read -r index
# Make sure that the input is either empty or that a valid index was entered.
[[ -z $index ]] && index=$defaultIndex && break # empty input == default choice
(( index >= 1 && index <= numItems )) 2>/dev/null || { echo "Invalid selection. Please try again." >&2; continue; }
break
done
# Output the selected *index* (1-based).
printf $index
}
Sample call:
# Print the prompt message and call the custom select function,
# designating 'Abort' as the default choice.
echo "Include audits?"
ndx=$(selectWithDefault 'Yes' 'No', '!Abort')
case $ndx in
1) echo "include";;
2) echo "don't include";;
3) echo "abort";;
esac
Optional reading: A more idiomatic version of your original code
Note: This code doesn't solve the problem, but shows more idiomatic use of the select statement; unlike the original code, this code re-displays the prompt if an invalid choice was made:
optionsAudits=("Yep" "Nope")
echo "Include audits (^C to abort)?"
select opt in "${optionsAudits[#]}"; do
# $opt being empty signals invalid input.
[[ -n $opt ]] || { echo "What's that? Please try again." >&2; continue; }
break # a valid choice was made, exit the prompt.
done
case $opt in # $opt now contains the *text* of the chosen option
'Yep')
includeAudits=true
;;
'Nope') # could be just `*` in this case.
includeAudits=false
;;
esac
Note:
The case statement was moved out of the select statement, because the latter now guarantees that only valid inputs can be made.
The case statement tests the output variable ($opt) rather than the raw user input ($REPLY), and that variable contains the choice text, not its index.
Your problem is due to the fact that select will ignore empty input. For your case, read will be more suitable, but you will lose the utility select provides for automated menu creation.
To emulate the behaviour of select, you could do something like that :
#!/bin/bash
optionsAudits=("Yep" "Nope")
while : #infinite loop. be sure to break out of it when a valid choice is made
do
i=1
echo "Include Audits?"
#we recreate manually the menu here
for o in "${optionsAudits[#]}"; do
echo "$i) $o"
let i++
done
read reply
#the user can either type the option number or copy the option text
case $reply in
"1"|"${optionsAudits[0]}") includeAudits=true; break;;
"2"|"${optionsAudits[1]}") includeAudits=false; break;;
"") echo "empty"; break;;
*) echo "Invalid choice. Please choose an existing option number.";;
esac
done
echo "choice : \"$reply\""
Updated answer:
echo "Include audits? 1) Yep, 2) Nope"
read ans
case $ans in
Yep|1 ) echo "yes"; includeAudits=true; v=1 ;;
Nope|2 ) echo "no"; includeAudits=false; v=2 ;;
"" ) echo "default - yes"; includeAudits=true; v=1 ;;
* ) echo "Whats that?"; exit ;;
esac
This accepts either "Yep" or "1" or "enter" to select yes, and accepts "Nope" or "2" for no, and throws away anything else. It also sets v to 1 or 2 depending on whether the user wanted yes or no.
This will do what you are asking for.
options=("option 1" "option 2");
while :
do
echo "Select your option:"
i=1;
for opt in "${options[#]}"; do
echo "$i) $opt";
let i++;
done
read reply
case $reply in
"1"|"${options[0]}"|"")
doSomething1();
break;;
"2"|"${options[1]}")
doSomething2();
break;;
*)
echo "Invalid choice. Please choose 1 or 2";;
esac
done
Assuming that your default option is Yep:
#!/bin/bash
optionsAudits=("Yep" "Nope")
while : #infinite loop. be sure to break out of it when a valid choice is made
do
i=1
echo "Include Audits?: "
#we recreate manually the menu here
for o in "${optionsAudits[#]}"; do
echo " $i) $o"
let i++
done
read -rp "Audit option: " -iYep
#the user can either type the option number or copy the option text
case $REPLY in
"1"|"${optionsAudits[0]}") includeAudits=true; break;;
"2"|"${optionsAudits[1]}") includeAudits=false; break;;
"") includeAudits=true; break;;
*) echo "Invalid choice. Please choose an existing option number.";;
esac
done
echo "choice : \"$REPLY\""
echo "includeAudits : \"$includeAudits\""
Noticed the line:
read -rp "Audit option: " -eiYep
Also I pulled up $reply to $REPLY so that the case decision works better.
The output would now look like this upon hitting ENTER:
Include Audits?:
1) Yep
2) Nope
Audit option: Yep
choice : ""
includeAudits : "true"
#
As an enhancement over select, read -eiYep will supply Yep default value into the input buffer up front.
Only downside of fronting the default value is that one would have to press backspace a few times to enter in their own answer.

substring extraction in bash

iamnewbie: this code is inefficient but it should extract the substring, the problem is with last echo statement,need some insight.
function regex {
#this function gives the regular expression needed
echo -n \'
for (( i = 1 ; i <= $1 ; i++ ))
do
echo -n .
done
echo -n '\('
for (( i = 1 ; i <= $2 ; i++ ))
do
echo -n .
done
echo -n '\)'
echo -n \'
}
# regex function ends
echo "Enter the string:"
read stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit
else
echo "Length of the $stg is:"
z=`expr "$stg" : '.*' `
#variable z holds the length of given string
echo $z
fi
echo "Enter the number of trailing characters to be extracted from $stg:"
read n
m=`expr $z - $n `
#variable m holds an integer value which is equal to total length - length of characters to be extracted
x=$(regex $m $n)
echo ` expr "$stg" : "$x" `
#the echo statement(above) is just printing a newline!! But not the result
What I intend to do with this code is, if I enter "racecar" and give "3" , it should display "car" which are the last three characters. Instead of displaying "car" its just printing a newline. Please correct this code rather than giving a better one.
Although you didn't ask for a better solution, it's worth mentioning:
$ n=3
$ stg=racecar
$ echo "${stg: -n}"
car
Note that the space after the : in ${stg: -n} is required. Without the space, the parameter expansion is a default-value expansion rather than a substring expansion. With the space, it's a substring expansion; -n is interpreted as an arithmetic expression (which means that n is interpreted as $n) and since the result is a negative number, it specifies the number of characters from the end to start the substring. See the Bash manual for details.
Your solution is based on evaluating the equivalent of:
expr "$stg" : '......\(...\)'
with an appropriate number of dots. It's important to understand what the above bash syntax actually means. It invokes the command expr, passing it three arguments:
arg 1: the contents of the variable stg
arg 2: :
arg 3: ......\(...\)
Note that there are no quotes visible. That's because the quotes are part of bash syntax, not part of the argument values.
If the value of stg had enough characters, the result of the above expr invocation would be to print out the 7th, 8th and 9th character of the value of stg`. Otherwise, it would print a blank line, and fail.
But that's not what you are doing. You're creating the regular expression:
'......\(...\)'
which has single quotes in it. Since single-quotes are not special characters in a regex, they match themselves; in other words, that pattern will match a string which starts with a single quote, followed by nine arbitrary characters, followed by another single quote. And if the string does match, it will print the three characters prior to the second single-quote.
Of course, since the regular expression you make has a . for every character in the target string, it won't match the target even if the target started and begun with a single-quote, since there would be too many dots in the regex to match that.
If you don't put single quotes into the regex, then your program will work, but I have to say that few times have I seen such an intensely circuitous implementation of the substring function. If you're not trying to win an obfuscated bash competition (a difficult challenge since most production bash code is obfuscated by nature), I'd suggest you use normal bash features instead of trying to do everything with regexen.
One of those is the syntax to determine the length of a string:
$ stg=racecar
$ echo ${#stg}
7
(although, as shown at the beginning, you don't actually even need that.)
What about:
$ n=3
$ string="racecar"
$ [[ "$string" =~ (.{$n})$ ]]
$ echo ${BASH_REMATCH[1]}
car
This looks for the last n characters at the end of the line. In a script:
#!/bin/bash
read -p "Enter a string: " string
read -p "Enter the number of characters you want from the end: " n
[[ "$string" =~ (.{$n})$ ]]
echo "These are the last $n characters: ${BASH_REMATCH[1]}"
You may want to add some more error handling, but this'll do it.
I'm not sure you need loops for this task. I wrote some example to get two parameters from user and cut the word according to it.
#!/bin/bash
read -p "Enter some word? " -e stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit 1
fi
read -p "Enter some number to set word length? " -e cutNumber
# check that cutNumber is a number
if ! [ "$cutNumber" -eq "$cutNumber" ]; then
echo "Not a number!"
exit 1
fi
echo "Cut first n characters:"
echo ${stg:$cutNumber}
echo
echo "Show first n characters:"
echo ${stg:0:$cutNumber}
echo "Alternative get last n characters:"
echo -n "$stg" | tail -c $cutNumber
echo
Example:
Enter some word? TheRaceCar
Enter some number to set word length? 7
Cut first n characters:
Car
Show first n characters:
TheRace
Alternative get last n characters:
RaceCar

Resources