arraysbashdialogifs

How to parse an array, that uses space delimiter, and has spaces in the values?


I'm using Dialog, in bash, to create a simple multiple choice question. My options all include spaces in the values. When the Dialog is constructed, it appears to be building the array with a space delimiter.

My options are as such:

TITLE="Setup Script..."
VER="0.2Β"

{ #   Default Dialog Values.
    # └──These can be overwritten if need be.
    DIALOG_TOGGLE="OFF"                     # Used for dialog multiple choice.
    DIALOG_HEIGHT=15                        # Sets the height.
    DIALOG_WIDTH=78                         # Sets the width.
    DIALOG_CHOICES=4                        # Sets the number of choices.
    DIALOG_OK_LABEL="Submit"                # Sets the OK button label.
    DIALOG_CANCEL_LABEL="Cancel"            # Sets the Cancel button label.
    DIALOG_EXTRA_BUTTON=true                # Enable/Disable the extra button.
    DIALOG_EXTRA_LABEL="Select All"         # Sets the "EXTRA" button label.
    DIALOG_BACKTITLE=$TITLE"     v:"$VER    # Sets the background title.
    DIALOG_TITLE="Example Title..."         # Sets the dialog title.

    DIALOG_DESCRIPTION="\n\
    Welcome to the $TITLE\n\n\
    Example Description" 

    declare -a DIALOG_OPTIONS=(
    "Example Item 1" "This is an example option" $DIALOG_TOGGLE
    "Example Item 2" "This is an example option" $DIALOG_TOGGLE
    "Example Item 3" "This is an example option" $DIALOG_TOGGLE
    )
}

My Dialog is then constructed as such:

load_dialog() {
    # Check whether or not to display the extra button
    if [ "$DIALOG_EXTRA_BUTTON" = true ] ; then
        EB_TOGGLE='--extra-button'
    else
        EB_TOGGLE=''
    fi

    # Dialog Framework
    cmd=(dialog
         --colors
         --clear
         --keep-tite
         --backtitle "$DIALOG_BACKTITLE"
         --title "$DIALOG_TITLE"
         --ok-label "$DIALOG_OK_LABEL"
         --cancel-label "$DIALOG_CANCEL_LABEL"
         $EB_TOGGLE
         --extra-label "$DIALOG_EXTRA_LABEL"
         --checklist "${DIALOG_DESCRIPTION//$'\t'/}"
         "$DIALOG_HEIGHT"
         "$DIALOG_WIDTH"
         "$DIALOG_CHOICES"
        )

    # Build Dialog
    CHOICES="$("${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty)"

    # Detect the exit status of the Dialog (what button was pressed)
    exitStatus=$?
    
    # Backup existing IFS
    SAVEIFS=$IFS

    # Set new IFS
    IFS=$'\n'

    # Determine what choices were selected
    if [ -z "${CHOICES}" ]; then
        echo  "No option was selected..."
        echo  "└── User hit Cancel or unselected all options."
    else
        for CHOICE in ${CHOICES[@]}; do
            case  $CHOICE in
                $CHOICE)
                    echo "${CHOICE} enabled."
                    ;;
                *)
                    echo "Something went wrong!"
                    ;;
            esac
        done
    fi

    # Restore backed up IFS
    IFS=$SAVEIFS

    case $exitStatus in
        0)
            echo $DIALOG_OK_LABEL 'chosen';
            ;; 
        1) 
            echo $DIALOG_CANCEL_LABEL 'chosen';
            exit
            ;;

        3) 
            echo $DIALOG_EXTRA_LABEL 'chosen';
            DIALOG_TOGGLE=on    # set all to on
            load_dialog
            ;;
        
        *)
            echo 'unexpected (ESC?)'; exit ;;
    esac
}
}

I'm attempting to build this dialog as a sort of "constructor" so that I can pass it some different data, and build different dialogs on the fly. I'm attempting to not have to repeat the code of building the dialog each time. It's possible I may have a few prompts in my script along the way.

Ultimately, I'm trying to be able to differentiate the options picked by the user, as well as handling the exitstatus of the dialog when a button is selected.

As it stands now, when I run this, the CHOICES="$("${cmd[@]}"... seems to collect the values and build the array as such:

"Example Item 1" "Example Item 2" "Example Item 3"

Assuming all 3 options are selected of course.

I want to be able to handle the array as such: (Maybe even as CSV, instead of SSV)

"Example Item 1"
"Example Item 2"
"Example Item 3"

The issue I'm having, is trying to find a way to parse these items individually. I've been playing around with the IFS value, and that seems to work for ignoring the spaces in the values themselves, but from what I gather, this also then ignores the spaces separating the values themselves.

I'm a bit confused, and I've been stuck on this for a while now. I feel like I'm stuck in a catch 22 here. Any advice or help on how I can better handle this data?


Solution

  • Assumptions:

    First item of interest:

    CHOICES="$("${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty)"
    

    This does not populate an array but rather populates the CHOICES variable with a single string of text (in this case the output from the dialog call):

    $ typeset -p CHOICES
    declare -- CHOICES="\"Example Item 1\" \"Example Item 2\" \"Example Item 3\""
    

    The normal approach for populating an array would look like:

    CHOICES=( $("${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty) )
    

    But upon pushing this change into OP's code we find ourselves with the following array structure:

    $ typeset -p CHOICES
    declare -a CHOICES=([0]="\"Example" [1]="Item" [2]="1\"" [3]="\"Example" [4]="Item" [5]="2\"" [6]="\"Example" [7]="Item" [8]="3\"")
    

    Primary issue appears to be that dialog is passing the data back in a format that bash cannot (easily) parse into 3 array elements.

    While it's possible to write some (bash) code to manually parse the dialog results into 3 separate elements, let's first see if dialog has any flags for passing the data back in a format that's easier to parse in bash. From man dialog I found the following:

    --separate-output
           For certain widgets (buildlist, checklist, treeview), output result one line
           at a time, with no quoting.  This facilitates parsing by another program.
    

    Since OP's code is using the checklist widget (--checklist) this looks like a possible solution. (NOTE: another option that may work, especially if using something other than the buildlist/checklist/treeview widgets, would be --output-separator.)

    If we add --separate-output to OP's cmd=( ... ) construct, and switch back to OP's original string population of the CHOICES variable, we see that our selected items are in fact returned on separate lines:

    cmd=(dialog --colors --clear --separate-output ... )
    CHOICES="$("${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty)"
    
    $ typeset -p CHOICES
    declare -- CHOICES="Example Item 1
    Example Item 2
    Example Item 3"
    

    We can now make use of the bash builtin mapfile, and process substitution, to capture each of these lines of output as a separate array element:

    mapfile -t CHOICES < <( "${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty )
    
    $ typeset -p CHOICES
    declare -a CHOICES=([0]="Example Item 1" [1]="Example Item 2" [2]="Example Item 3")
    
    $ for choice in "${CHOICES[@]}"; do echo "you chose: ${choice}"; done
    you chose: Example Item 1
    you chose: Example Item 2
    you chose: Example Item 3
    

    As for the exitStatus issue ...

    Capturing the return status of a process substitution requires a bit of wrangling. (A web search on bash return status process substitution should bring up a wide range of approaches.)

    We'll take a simple approach and just write the return/exit code to a local file (.exit_status):

    mapfile -t CHOICES < <( "${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty; echo $? > .exit_status )
    
    read -r exitStatus < .exit_status
    
    #rm .exit_status              # uncomment line to remove the file
    

    NOTES:


    Net changes to OP's current code: