2

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?

4
  • Hmm, some odd behavior, is that it doesn't appear to matter what I set IFS to. As long as it's set to something, say '' or, 'xxxxxxxx' or even NULL, my for loop will catch the CHOICES array as one long string. However, if IFS= simply doesn't exist at all, it will loop through every single space in the array as a separate item. Including the space separated values, and the spaces within the values. Commented May 30, 2024 at 21:31
  • You have an extra } at the end of load_dialogue Commented May 30, 2024 at 21:35
  • 1
    You need quotes around: ${CHOICES[@]}. It should be "${CHOICES[@]}". Otherwise the spaces in the values will be treated as word delimiters. Commented May 30, 2024 at 21:38
  • @Barmar, the extra } is just a copy paste error, because I have the load_dialog function in a region defined above, which wasn't copied. Commented May 31, 2024 at 13:25

1 Answer 1

3

Assumptions:

  • OP wants to capture each item (returned by dialog) as a separate element in a bash array named CHOICES

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:

  • in my testing, upon selecting the extra button (Select All), OP's current code will
  • set exitStatus=3
  • fail to set the checkboxes (if originally unchecked)
  • then issue a recursive call to the load_dialog() function
  • I'll leave it up to OP to debug/troubleshoot the (wrong?) behavior of not setting the checkboxes

Net changes to OP's current code:

  • add --separate-output to the cmd=(dialog --colors ....) array definition
  • replace CHOICES="$("${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty)"
  • with mapfile -t CHOICES < <( "${cmd[@]}" "${DIALOG_OPTIONS[@]}" 2>&1 1>/dev/tty; echo $? > .exit_status )
  • replace exitStatus=$?
  • with read -r exitStatus < .exit_status
  • [optional] remove the local file (rm .exit_status)
  • remove all the shenanigans with changing IFS (this is not needed and just clutters up the code)
  • eliminate word splitting on white space by wrapping variable references in double quotes: ${CHOICES[@]} becomes "${CHOICES[@]}"; $CHOICE becomes "$CHOICE"
  • while optional for other variables (in this case) it's a good habit to get into of wrapping all variable references in double quotes, eg, $exitStatus becomes "$exitStatus"
Sign up to request clarification or add additional context in comments.

1 Comment

Wow! Very much appreciate your assistance here! Everything appears to work as expected. I wasn't familiar with mapfile so I've learned something new here. In terms of the issue with recursively calling load_dialog, I actually call another function which defines $DIALOG_OPTIONS again, reading from the $DIALOG_TOGGLE which appears to set the toggles correctly. I omitted this part in my original request as I didn't think it was overly relevant. Either way, I really appreciate your assistance here!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.