Choose more than one option in script shell? - bash

I want to create a menu with a script with multi choice .
like :
1)
2)
3)
4)
5)
I can choose 1 , and 3 , and 5 in the same time .

bash's select compound command doesn't directly support multiple choices, but you can still base your solution on it, taking advantage of the fact that whatever the user enters is recorded in the special $REPLY variable:
#!/usr/bin/env bash
choices=( 'one' 'two' 'three' 'four' 'five' ) # sample choices
select dummy in "${choices[#]}"; do # present numbered choices to user
# Parse ,-separated numbers entered into an array.
# Variable $REPLY contains whatever the user entered.
IFS=', ' read -ra selChoices <<<"$REPLY"
# Loop over all numbers entered.
for choice in "${selChoices[#]}"; do
# Validate the number entered.
(( choice >= 1 && choice <= ${#choices[#]} )) || { echo "Invalid choice: $choice. Try again." >&2; continue 2; }
# If valid, echo the choice and its number.
echo "Choice #$(( ++i )): ${choices[choice-1]} ($choice)"
done
# All choices are valid, exit the prompt.
break
done
echo "Done."
As for how the select command normally works, with a single selection:
Run man bash and look under the heading 'Compound Commands'
For an annotated example, see this answer.
This answer implements custom logic as follows:
The designated target variable of the select command, dummy, is ignored, and the $REPLY variable is used instead, because Bash sets it to whatever the user entered (unvalidated).
IFS=', ' read -ra selChoices <<<"$REPLY" tokenizes the user-entered value:
It is fed via a here-string (<<<) to the read command
using instance of commas and space (,<space>) as the [Internal] Field Separator (IFS=...)
Note that, as a side effect, the user could use spaces only to separate their choices.
and the resulting tokens are stored as elements of array (-a) selChoices; -r simply turns off interpretation of \ chars. in the input
for choice in "${selChoices[#]}"; do loops over all tokens, i.e., the individual numbers the user chose.
(( choice >= 1 && choice <= ${#choices[#]} )) || { echo "Invalid choice: $choice. Try again." >&2; continue 2; } ensures that each token is valid, i.e., that it is a number between 1 and the count of choices presented.
echo "Choice #$(( ++i )): ${choices[choice-1]} ($choice)" outputs each choice and choice number
prefixed with a running index (i), which is incremented (++i) using an arithmetic expansion ($((...))) - since a variable defaults to 0 in an arithmetic context, the first index output will be 1;
followed by ${choices[choice-1]}, i.e., the choice string indicated by the number entered, decremented by 1, because Bash arrays are 0-based; note how choice needs no $ prefix in the array subscript, because a subscript is evaluated in an arithmetic context (as if inside $(( ... ))), as above.
terminated with ($choice), the chosen number in parentheses.
break is needed to exit the prompt; by default, select will keep prompting.

Related

Select command separates the options with spaces listed from a file [duplicate]

This question already has answers here:
Creating an array from a text file in Bash
(7 answers)
How can I write a Bash Script to display a menu, accept user input and display data?
(3 answers)
Closed 4 months ago.
How can I display a selection menu for a user with options I have stored on individual lines of a text file?
For example, my text file (ingestion.txt) looks like this.
SOUP
FTS/CTS
JDBC
NEW
And I want the user to see this
Please select an option by typing in the corresponding number
1) SOUP
2) FTS/CTS
3) JDBC
4) NEW
And then if they didn't type a valid number they would be prompted again.
#!/bin/bash
unset option menu ERROR # prevent inheriting values from the shell
declare -a menu # create an array called $menu
menu[0]="" # set and ignore index zero so we can count from 1
# read menu file line-by-line, save as $line
while IFS= read -r line; do
menu[${#menu[#]}]="$line" # push $line onto $menu[]
done < ingestion.txt
# function to show the menu
menu() {
echo "Please select an option by typing in the corresponding number"
echo ""
for (( i=1; i<${#menu[#]}; i++ )); do
echo "$i) ${menu[$i]}"
done
echo ""
}
# initial menu
menu
read option
# loop until given a number with an associated menu item
while ! [ "$option" -gt 0 ] 2>/dev/null || [ -z "${menu[$option]}" ]; do
echo "No such option '$option'" >&2 # output this to standard error
menu
read option
done
echo "You said '$option' which is '${menu[$option]}'"
This reads through ingestion.txt line by line, then pushes the contents of that line into the $menu array. ${#variable} gives you the length of $variable. When given the entirety of an array ("${array[#]}" is akin to "$#"), such as ${#array[#]}, you get the number of elements in that array. Because bash is zero-indexed, that number is the first available item you can add to, starting with zero given the newly defined (and therefore empty) array.
The menu() function iterates through the menu items and displays them on the screen.
We then loop until we get a valid input. Since bash will interpret a non-number as zero, we first determine if it is actually a (natural) number, discarding any error that would come from non-numbers, then we actually ensure the index exists. If it's not the first iteration, we complain about the invalid input given before. Display the menu, then read user input into $option.
Upon a valid option index, the loop stops and the code then gives you back the index and its corresponding value.

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 to loop through a range of characters in a bash script using ASCII values?

I am trying to write a bash script which will read two letter variables (startletter/stopletter) and after that I need to print from the start letter to the stop letter with a for or something else. How can I do that?
I tried to do
#! /bin/bash
echo "give start letter"
read start
echo "give stop letter" read stop
But none of the for constructs work
#for value in {a..z}
#for value in {$start..$stop}
#for (( i=$start; i<=$stop; i++)) do echo "Letter: $c" done
This question is very well explained in BashFAQ/071 How do I convert an ASCII character to its decimal (or hexadecimal) value and back?
# POSIX
# chr() - converts decimal value to its ASCII character representation
# ord() - converts ASCII character to its decimal value
chr () {
local val
[ "$1" -lt 256 ] || return 1
printf -v val %o "$1"; printf "\\$val "
# That one requires bash 3.1 or above.
}
ord() {
# POSIX
LC_CTYPE=C printf %d "'$1"
}
Re-using them for your requirement, a proper script would be written as
read -p "Input two variables: " startLetter stopLetter
[[ -z "$startLetter" || -z "$stopLetter" ]] && { printf 'one of the inputs is empty\n' >&2 ; }
asciiStart=$(ord "$startLetter")
asciiStop=$(ord "$stopLetter")
for ((i=asciiStart; i<=asciiStop; i++)); do
chr "$i"
done
Would print the letters as expected.
Adding it to community-wiki since this is also a cross-site duplicate from Unix.SE - Bash script to get ASCII values for alphabet
In case you feel adventurous and want to use zsh instead of bash, you can use the following:
For zsh versions below 5.0.7 you can use the BRACE_CCL option:
(snip man zshall) If a brace expression matches none of the above forms, it is left
unchanged, unless the option BRACE_CCL (an abbreviation for 'brace character class') is set. In that case, it is expanded to a list of the individual characters between the braces sorted into the order of the characters in the ASCII character set (multibyte characters are not currently handled). The syntax is similar to a [...] expression in filename generation: - is treated specially to denote a range of characters, but ^ or ! as the first character is treated normally. For example, {abcdef0-9}
expands to 16 words 0 1 2 3 4 5 6 7 8 9 a b c d e f.
#!/usr/bin/env zsh
setopt brace_ccl
echo "give start letter"
read cstart
echo "give stop letter"
read cstop
for char in {${cstart}-${cstop}}; do echo $char; done
For zsh versions from 5.0.7 onwards you can use the default brace expansion :
An expression of the form {c1..c2}, where c1 and c2 are single characters (which may be multibyte characters), is expanded to every character in the range from c1 to c2 in whatever character sequence is used internally. For characters with code points below 128 this is US ASCII (this is the only case most users will need). If any intervening character is not printable, appropriate quotation is used to render it printable. If the character sequence is reversed, the output is in reverse order, e.g. {d..a} is substituted as d c b a.
#!/usr/bin/env zsh
echo "give start letter"
read cstart
echo "give stop letter"
read cstop
for char in {${cstart}..${cend}; do echo $char; done
More information on zsh can be found here and the quick reference

stopping 'sed' after match found on a line; don't let sed keep checking all lines to EOF

I have a text file in which each a first block of text on each line is separated by a tab from a second block of text like so:
VERBS, AUXILIARY. "Be," subjunctive and quasi-subjunctive Be, Beest, &c., was used in A.-S. (beon) generally in a future sense.
In case it is hard to tell, tab is long space between "quasi-subjunctive" and "Be".
So I am thinking off the top of my head a 'for' loop in which a var is set using 'sed' to read the first block of text of a line, upto and including the tab (or not, doesn't really matter) and then the 'var' is used to find subsequent matches adding a "(x)" right before the tab to make the line unique. The 'x' of course would be a running counter numbering the first instance '1' incrementing and then each subsequent match one number higher.
One problem I see is stopping 'sed' after each subsequent match so the counter can be incremented. Is there a way to do this, since it is "sed's" normal behaviour to continue on thru without stop (as far as I know) until all lines are processed.
You can set the IFS to TAB character and read the line into variables. Something like:
$ while IFS=$'\t' read block1 block2;do
echo "block1 is $block1"
echo "block2 is $block2"
done < file
block1 is VERBS, AUXILIARY. "Be," subjunctive and quasi-subjunctive
block2 is Be, Beest, &c., was used in A.-S. (beon) generally in a future sense.
Ok so I got the job done with this little (or perhaps big if too much overkill?) script I whipped up:
#!/bin/bash
sedLnCnt=1
while [[ "$sedLnCnt" -lt 521 ]] ; do
lN=$(sed -n "${sedLnCnt} p" sGNoSecNums.html|sed -r 's/^([^\t]*\t).*$/\1/') #; echo "\$lN: $lN"
lnNum=($(grep -n "$lN" sGNoSecNums.html|sed -r 's/^([0-9]+):.*$/\1/')) #; echo "num of matches: ${#lnNum[#]}"
if [[ "${#lnNum[#]}" -gt 1 ]] ; then #'if'
lCnt="${#lnNum[#]}"
((eleN = $lCnt-1)) #; echo "\$eleN: ${eleN}" # var $eleN needs to be 1 less than total line count of zero-based array
while [[ "$lCnt" -gt 0 ]] ; do
sed -ri "${lnNum[$eleN]}s/^([^\t]*)\t/\1 \(${lCnt}\)\t/" sGNoSecNums.html
((lCnt--))
((eleN--))
done
fi
((sedLnCnt++))
done
Grep was the perfect way to find line numbers of matches, jamming them into an array and then editing each line appending the unique identifier.

UNIX:Create array from space delimited string while ignoring space in quotes

I'm trying to create an array from a space delimited string, this works fine till i have to ignore the space within double quotes for splitting the string.
I Tried:
inp='ROLE_NAME="Business Manager" ROLE_ID=67686'
arr=($(echo $inp | awk -F" " '{$1=$1; print}'))
This splits the array like:
${arr[0]}: ROLE_NAME=Business
${arr[1]}: Manager
${arr[2]}: ROLE_ID=67686
when actually i want it:
${arr[0]}: ROLE_NAME=Business Manager
${arr[1]}: ROLE_ID=67686
Im not really good with awk so can't figure out how to fix it.
Thanks
This is bash specific, may work with ksh/zsh
inp='ROLE_NAME="Business Manager" ROLE_ID=67686'
set -- $inp
arr=()
while (( $# > 0 )); do
word=$1
shift
# count the number of quotes
tmp=${word//[^\"]/}
if (( ${#tmp}%2 == 1 )); then
# if the word has an odd number of quotes, join it with the next
# word, re-set the positional parameters and keep looping
word+=" $1"
shift
set -- "$word" "$#"
else
# this word has zero or an even number of quotes.
# add it to the array and continue with the next word
arr+=("$word")
fi
done
for i in ${!arr[#]}; do printf "%d\t%s\n" $i "${arr[i]}"; done
0 ROLE_NAME="Business Manager"
1 ROLE_ID=67686
This specifically breaks words on arbitrary whitespace but joins with a single space, so your custom whitespace within quotes will be lost.

Resources