3

I have a command with a number of arguments:

my_command --arg1 <arg1> \
    --arg2 <arg2> \
    --arg3 <arg3>

I've wrapped the invocation of my_command in a function that sets up the values for the various arguments, which may or may not be passed:

run_my_command() {
    if [[ ... ]]; then
        ARG1FLAGS="--arg1 $ARG1"
    else
        ARG1FLAGS=''
    fi
    if [[ ... ]]; then
        ARG2FLAGS="--arg2 $ARG2"
    else
        ARG2FLAGS=''
    fi
    if [[ ... ]]; then
        ARG3FLAGS="--arg3 $ARG3"
    else
        ARG3FLAGS=''
    fi

    my_command $ARG1FLAGS \
        $ARG2FLAGS \
        $ARG3FLAGS
}

In some cases, it's necessary to pass a filename argument to my_command:

run_my_command() {
    # ...
    if [[ ... ]]; then
        FILEARG="--input_file $SOME_FILE"
    else
        FILEARG=''
    fi

    my_command $FILEARG \
        ...
}

All of that works fine. Now, I'd like to conditionally use process substitution for FILEARG, but of course this doesn't work:

run_my_command() {
    # ...
    if [[ ... ]]; then
        FILEFLAG='--input_file'
        FILEARG=<(other_cmd)
    else
        FILEARG=''
    fi

    my_command $FILEFLAG $FILEARG \
        ...
}

because by the time my_command runs, $FILEARG, an anonymous pipe which looks like /dev/fd/63, is already closed.

Right now I've solved this by putting all of my_command in a conditional:

run_my_command() {
    # Get ARG1, ARG2, ARG3...
    if [[ ... ]]; then
        my_command --input_file <(other_cmd) \
            $ARG1FLAGS \
            $ARG2FLAGS \
            $ARG3FLAGS
    else
        my_command $ARG1FLAGS \
            $ARG2FLAGS \
            $ARG3FLAGS
    fi
}

but I don't like the duplication. I feel like there's maybe something I can do with eval, or possibly I can put my_command into another function, but I haven't figured it out yet. How can I conditionally use process substitution to generate a file from which my_command can read without duplicating the entire call to my_command?

Note that I'm running bash 4.4.19. To my considerable surprise, bash 3.2.57 does appear to behave the way that the Bash Hackers wiki, below, suggests:

doit() {
    local -r FOO=<(echo hi)
    cat $FOO
}

doit

# bash 3.2.57:
$ ./test.sh
hi
# bash 4.4.19:
$ ./test.sh
cat: /dev/fd/63: Bad file descriptor

Here are some questions I've examined but from which I haven't been able to get a working answer:

Also, the Bash Hackers wiki notes rather cryptically:

If a process substitution is expanded as an argument to a function, expanded to an environment variable during calling of a function, or expanded to any assignment within a function, the process substitution will be "held open" for use by any command within the function or its callees, until the function in which it was set returns. If the same variable is set again within a callee, unless the new variable is local, the previous process substitution is closed and will be unavailable to the caller when the callee returns.

In essence, process substitutions expanded to variables within functions remain open until the function in which the process substitution occured returns - even when assigned to locals that were set by a function's caller. Dynamic scope doesn't protect them from closing.

but I haven't been able to get a process substitution "held open" just by assigning it to a variable in a function.

2
  • 2
    I guess you meant this link in the last quote. The part about process substitution being held open "if expanded to an assignment within a function" seems to work on 3.2 and 4.2.53, but not in my copy of 4.3 or 4.4.
    – ilkkachu
    Feb 20, 2018 at 18:01
  • @ilkkachu Whoa! I'm seeing the same thing. I've updated the question to fix the link and add that context, thank you!
    – ravron
    Feb 20, 2018 at 18:06

3 Answers 3

3

This more like sidestepping the issue, but there's less duplication (of $ARGnFLAGS) if you put the other arguments in an array instead of individual variables.

It's also the proper thing to do in any case, as you can get the arguments to the command without word splitting or filename globbing getting in the way.

run_my_command() {
    args=()
    if [[ ... ]]; then
        args+=(--arg1 "$ARG1")
    fi
    if [[ ... ]]; then
        args+=(--arg2 "$ARG2")
    fi

    if [[ ... ]]; then
        my_command --input_file <(other_cmd) "${args[@]}"
    else
        my_command "${args[@]}" 
    fi
}
1
run_my_command() {
    # ...
    if [[ ... ]]; then
        tmpfile=$(mktemp)
        trap 'rm -f "$tmpfile"' RETURN
        other_cmd >"$tmpfile"
        FILEFLAG='--input_file'
        FILEARG="$tmpfile"
    else
        FILEARG=''
    fi

    my_command $FILEFLAG $FILEARG \
        ...

    return
}

... or something to that effect.

That is, use a temporary file instead of trying to use a process substitution.

1

Here's how you can use eval to get what you want:

run_my_command() {
    # Get ARG1, ARG2, ARG3...
    commandline="my_command $ARG1FLAGS $ARG2FLAGS $ARG3FLAGS"
    if [[ ... ]]; then
        commandline="$commandline --input_file <(other_cmd)"
    fi
    eval $commandline
}

You must log in to answer this question.

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