GH05T GH05T - 2 months ago 7
Bash Question

How to get values back from an external function that requires interactivity?

One of the routines I frequently use is a check for valid arguments passed when invoking scripts. Ideally, I'd like to make these, and other, similar, routines external functions that I could call from any script, for handling these more trivial processes. But, I'm having trouble retrieving the values I need from said function(s), without making the process more complicated.

I have tried using command substitution (e.g., echoing the output of the external function into a variable name local to the calling script), which seems to at least work with simpler functions. However, working with this file checking function, requires the read command in a loop, and, thus, user interactivity, which causes the script to hang when trying to resolve the variable that function call is stored in:

#!/bin/bash
# This is a simple function I want to call from other scripts.
exist(){
# If the first parameter passed is not a directory, then the input is
#+ invalid.
if [ ! -d "$1" ]; then
# Rename $1, so we can manipulate its value.
userDir="$1"
# Ask the user for new input while his input is invalid.
while [ ! -d "$userDir" ]; do
echo "\"$userDir\" does not exist."
echo "Enter the path to the directory: "
read userDir
# Convert any tildes in the variable b/c the shell didn't get to
#+ perform expansion.
userDir=`echo "$userDir" | sed "s|~|$HOME|"`
done
fi
}
exist "$1"


How can I retrieve the value of userDir in the calling script without adding (much) complexity?

Answer

You can have the exist function interact with the user over stderr and still capture the variable with command substitution. Let's take a simplified example:

exist() { read -u2 -p "Enter dir: " dir; echo "$dir"; }

The option -u2 tells read to use file descriptor 2 (stderr) for interacting with the user. This will continue to work even if stdout has been redirected via command substitution. The option -p "Enter dir: " allows read to set the prompt and capture the user input in one command.

As an example of how it works:

$ d=$(exist)
Enter dir: SomeDirectory
$ echo "$d"
SomeDirectory

Complete example

exist() {
    local dir="$1"
    while [ ! -d "$dir" ]; do
        echo "'$dir' is not a directory." >&2
        read -u2 -p "Enter the path to the directory: " dir
    dir="${dir/\~/$HOME}"
    done
    echo "$dir"
}

As an example of this in use:

$ d=$(exist /asdf)
'/asdf' is not a directory.
Enter the path to the directory: /tmp
$ echo "new directory=$d"
new directory=/tmp

Notes:

  1. There is no need for an if statement and a while loop. The while is sufficient on its own.

  2. Single quotes can be put in double-quoted strings without escapes. So, if we write the error message as "'$dir' is not a directory.", escapes are not needed.

  3. All shell variables should be double-quoted unless one wants them to be subject to word splitting and pathname expansion.