I have an array that may or may not contain BMC
, and I'm looping through each element of the array. If it does contain BMC
I want BMC
to be the last iteration of the loop.
The array is created with command line options so I don't think I can enforce the order of creation without making significant changes.
Example:
#!/usr/bin/env bash
while getopts cnBba opt; do
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
B) options+=(BMC);;
b) options+=(BIOS);;
a) all=true;;
esac
done
# This is probably really confusing because it's redacted but this script also
# allows for -a to be used and then I'll query an endpoint that gives me a list
# of all available firmware for the given server. It could return some or all
# of the possible options in no particular order.
mapfile -t types < <(curl some_firmware_endpoint)
if [[ $all == true ]]; then
options=($(printf '%s\n' "${types[@]}" NIC | sort -u))
fi
for opt in "${options[@]}"; do
echo "option is $opt"
done
Which produces this output:
$ ./script.sh -Bbcn
option is BMC
option is BIOS
option is CPLD
option is NIC
But I would like this output:
$ ./script.sh -Bbcn
option is BIOS
option is CPLD
option is NIC
option is BMC
I sort of have a solution but it's creating a null element in the array, which I'm sure I could further deal with but there may be a more correct way to do this.
My solution:
#!/usr/bin/env bash
while getopts cnBba opt; do
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
B) options+=(BMC);;
b) options+=(BIOS);;
a) all=true;;
esac
done
# This is probably really confusing because it's redacted but this script also
# allows for -a to be used and then I'll query an endpoint that gives me a list
# of all available firmware for the given server. It could return some or all
# of the possible options in no particular order.
mapfile -t types < <(curl some_firmware_endpoint)
if [[ $all == true ]]; then
options=($(printf '%s\n' "${types[@]}" NIC | sort -u))
fi
if [[ "${options[@]}" =~ BMC ]]; then
options=("${options[@]/BMC/}" BMC)
fi
for opt in "${options[@]}"; do
echo "option is $opt"
done
Which produces:
$ ./script.sh -Bbcn
option is
option is BIOS
option is CPLD
option is NIC
option is BMC
The -a all option:
The all option will query an endpoint (basically just a json object) that will have a list of firmware available for a server type. Some servers may only return BIOS BMC
, some may return BIOS BMC NIC CPLD SATA StorageController
or even more. Because of the way the json object is organized most of them will list NIC firmware separately so I'm manually adding that when -a is used and I handle that error separately if there is no firmware for that particular NIC, and then since some of them also have NIC I do a sort -u
to remove NIC if it's listed twice.
4 Answers 4
Create the array later. If the -a
option is used, call your enpoint and rearrange the BMC
string last using sed
. If it's not used, create the array from the given command line options.
(Note: I'm using literal tabs for indentation here. The here-document won't work properly with spaces. Or you can manually remove the indentation.)
#!/bin/bash
# Fake endpoint response.
get_valid () {
cat <<-END_LIST
BIOS
BMC
tiny green peas
CPLD
MICROBE
END_LIST
}
a_opt=false
unset -v c_opt n_opt B_opt b_opt
while getopts acnBb opt; do
case $opt in
a) a_opt=true ;;
c) c_opt=1 ;;
n) n_opt=1 ;;
B) B_opt=1 ;;
b) b_opt=1 ;;
*) : # error handling here
esac
done
if "$a_opt"; then
readarray -t options < <(
get_valid |
sed -e '/^BMC$/{ h; d; }' \
-e '${ p; g; /^$/d; }'
)
else
options=(
${c_opt:+CPLD}
${n_opt:+NIC}
${b_opt:+BIOS}
${B_opt:+BMC}
)
fi
printf 'Option: "%s"\n' "${options[@]}"
Without -a
: The options
array in the code above is created with the strings corresponding to each parsed option in the order listed in the assignment. A string is included in the array only if the corresponding _opt
variable is set (the actual value is unimportant).
With -a
: The options
array is created from the strings returned from the endpoint. If the string BMC
is found in the output of the endpoint (on a line by itself), it is moved to the end by saving it in the hold space until the last line is processed.
$ bash script -bBcn
Option: "CPLD"
Option: "NIC"
Option: "BIOS"
Option: "BMC"
$ bash script -a
Option: "BIOS"
Option: "tiny green peas"
Option: "CPLD"
Option: "MICROBE"
Option: "BMC"
-
Thanks, I've modified my question to include another wrench thrown into the gears of my poor coding choices.jesse_b– jesse_b2023年09月27日 20:27:59 +00:00Commented Sep 27, 2023 at 20:27
-
@jesse_b Updated.2023年09月27日 20:31:45 +00:00Commented Sep 27, 2023 at 20:31
-
Yes but the endpoint I'm querying wont always return all available options, for example on some machines might only return BIOS and BMC.jesse_b– jesse_b2023年09月27日 20:33:23 +00:00Commented Sep 27, 2023 at 20:33
-
@jesse_b Updated.2023年09月27日 21:11:29 +00:00Commented Sep 27, 2023 at 21:11
Special case being special, you could just manually punt that one after the loop:
...
handle_opt() {
echo "option is 1ドル"
}
do_BMC=0
for opt in "${options[@]}"; do
if [[ $opt = BMC ]]; then do_BMC=1; continue; fi
handle_opt "$opt"
done
if [[ $do_BMC= 1 ]]; then
handle_opt "BMC"
fi
Alternatively, fix the order of processing for the options in a constant array, and have e.g. an associative array telling which ones are enabled. Though this does mean reading the input from the external program isn't as simple as one mapfile
. (This can be seen as a variant of Kusalananda's solution).
Untested, but you get the idea:
order=(CPLD BIOS NIC BMC) # keep BMC last
declare -A options # associative array
while getopts cnBba opt; do
case $opt in
c) options[CPLD]=1 ;;
n) options[NIC]=1 ;;
B) options[BMC]=1 ;;
b) options[BIOS]=1 ;;
a) all=true ;;
esac
done
if [[ $all == true ]]; then
while IFS= read -r line; do
options[$line]=1;
done < <(curl some_firmware_endpoint)
fi
for opt in "${order[@]}"; do
if [[ "${options[$opt]}" != 1 ]]; then continue; fi
handle_opt "$opt"
done
Just remember you got B
when looping through the options then populate options[]
later, e.g. using a scalar variable:
$ cat script.sh
#!/usr/bin/env bash
while getopts cnBba opt; do
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
B) gotB=1;;
b) options+=(BIOS);;
a) all=true;;
esac
done
# mapfile and it's associated loop would go here
(( gotB )) && options+=(BMC)
for opt in "${options[@]}"; do
echo "option is $opt"
done
$ ./script.sh -Bbcn
option is BIOS
option is CPLD
option is NIC
option is BMC
$ ./script.sh -bcn
option is BIOS
option is CPLD
option is NIC
or if your bash version supports -v
you could keep a separate array of options as you getopts
them and later test for B
being an index in that:
$ cat script.sh
#!/usr/bin/env bash
declare -A opts
while getopts cnBba opt; do
opts["$opt"]=''
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
b) options+=(BIOS);;
a) all=true;;
esac
done
# mapfile and it's associated loop would go here
[[ -v opts[B] ]] && options+=(BMC)
for opt in "${options[@]}"; do
echo "option is $opt"
done
or save any options you want to add on at the end in a different array first then populate options[]
from that array later:
$ cat script.sh
#!/usr/bin/env bash
while getopts cnBba opt; do
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
B) deferred+=(BMC);;
b) options+=(BIOS);;
a) all=true;;
esac
done
# mapfile and it's associated loop would go here
for opt in "${deferred[@]}"; do
options+=("$opt")
done
for opt in "${options[@]}"; do
echo "option is $opt"
done
Add the mapfile
and associated loop between the while getopts
and where I add B
to options[]
in any of the above scripts as mentioned in the comments.
Thanks for the answers all. I ended up going with this:
#!/usr/bin/env bash
while getopts cnBba opt; do
case $opt in
c) options+=(CPLD);;
n) options+=(NIC);;
B) options+=(BMC);;
b) options+=(BIOS);;
a) all=true;;
esac
done
mapfile -t types < <(curl some_firmware_endpoint)
if [[ $all == true ]]; then
options=($(printf '%s\n' "${types[@]}" NIC | sort -u))
fi
if [[ "${options[@]}" =~ BMC ]]; then
options=("${options[@]/BMC/}" BMC)
fi
for opt in "${options[@]}"; do
[[ -z "$opt" ]] && continue
echo "option is $opt"
done
If options contains BMC
it will be removed and then readded at the end, and then since that creates a null element I just check if the element is null within my loop and skip that iteration.