Search code examples
bashsubshellzenity

Proper way to store the return value of a piped function in bash


I have an array of functions which I call inside a loop in bash. Whenever one of these functions returns an error, I keep track of it storing that function's name inside an error array to show to the user. This is the current working script.

#!/bin/bash

funA()
{ 
    ls -e &> /dev/null
    return $?
}

funB()
{ 
    ls -e &> /dev/null
    return $?
}

funC()
{ 
    true
    return $?
}

taskNames=("task 1" "task 2" "task 3")
taskMessages=("performing task 1" "performing task 2" "performing task 3")
tasks=("funA" "funB" "funC")

progress=0
taskpercentage=33
errors=()

for i in ${!tasks[@]}; do

    ${tasks[i]}

    if [[ $? != 0 ]]; then
        errors+=(${taskNames[$i]})
    fi

    progress=$(expr $progress + $taskpercentage)

done

echo ${errors[@]}

exit 0

Now, I need to pipe the loop to zenity so I can show the user a progress bar. Something like this:

(
  progress=0
  taskpercentage=33
  errors=()

  for i in ${!tasks[@]}; do
    echo "# ${taskMessages[$i]}"

    ${funcs[i]}

    if [[ $? != 0 ]]; then
      errors+=(${taskNames[$i]})
    fi

    sleep 2

    progress=$(expr $progress + $taskpercentage)
    echo $progress
  done

  echo "# All tasks completed"
) |
zenity --progress \
       --no-cancel \
       --title="Performing all tasks" \
       --text="Performing all tasks" \
       --percentage=0 \
       --height 200 \
       --width 500

The problem is, if I wrap my code inside a subshell, I lose access to the errors variable. Is there a proper way to do this and keep the changes to error array?

Edit: I do not intend to just print the error array, but to show it to the user through Zenity again, similar to this:

# Show error list
message="Some of the tasks ended with errors and could not be  completed."

if [[ ${#errors[@]} > 0 ]]; then
    zenity --list --height 500 --width 700 --title="title" \
    --text="$message" \
    --hide-header --column "Tasks with errors" "${errors[@]}"  
fi

Solution

  • If all you need to do is print the error message, you can put it on a separate file descriptor or in a separate file. The most readable way I know of us to use a temporary file:

    tempname=$(mktemp)     # Create a zero-length file
    (
        # ... your subshell
    
        if [[ ${#errors[@]} -gt 0 ]]; then       # save all $errors entries to the
            printf '%s\n' "${errors[@]}" > "$tempname"     # file called $tempname
        fi
    ) | zenity # ... your progress code
    
    # After the zenity call, report errors
    if [[ -s $tempname ]]; then   # -s: size > 0
        message="Some of the tasks ended with errors and could not be  completed."  
        zenity --list --height 500 --width 700 --title="title" --text="$message" \
            --hide-header --column "Tasks with errors" < "$tempname"
    fi        # Provide the saved errors to the dialog ^^^^^^^^^^^^^
    rm -f "$tempname"    # always remove, since mktemp creates the file.
    

    Edit:

    1. All the error entries can be printed, separated by newlines, using printf per this answer (another option).

    2. [[ -s $tempname ]] checks whether the file called $tempname exists and has a size greater than zero. If so, it means there were some errors, which we saved to that file.

    3. Per the Zenity list-dialog documentation,

      Data can be provided to the dialog through standard input. Each entry must be separated by a newline character.

      So, zenity --list ... < "$tempname" provides the items that were formerly in ${errors[@]}, and that were saved to the temporary file, to the list dialog.

    Alternative: You can also move information through the pipeline with, e.g., 2>&3 and the like, but I am not confident enough in my bash hackery to try that right now. :) Here is a related question and a detailed walkthrough of bash redirection.