0

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.

0

4 Answers 4

2

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"
4
  • Thanks, I've modified my question to include another wrench thrown into the gears of my poor coding choices.
    – jesse_b
    Commented Sep 27, 2023 at 20:27
  • @jesse_b Updated.
    – Kusalananda
    Commented 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
    Commented Sep 27, 2023 at 20:33
  • @jesse_b Updated.
    – Kusalananda
    Commented Sep 27, 2023 at 21:11
1

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
1

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.

0

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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .