1

Introduction

I'm using docopts in my script for parsing args from a help message.

The recommended usage is to use eval to run the command. I assume that's because docopts sets parsed args as variables that will be made available to the calling script.

Simplified example:

#!/usr/bin/env bash

function parse() {

    eval "$(docopts -h "Usage: your_program <arg> [--verbose]" : "$@")"
}

# Parse some args
parse "some arg" --verbose

# Verify that the variable $arg was correctly set by docopts 
if [ -z "${arg+x}" ]; then
    echo "\$arg is not set!"
else
    echo "the value of \$arg is \"$arg"\"
fi

Output: the value of $arg is "some arg"

Problem

When the passed arguments aren't valid, a usage message is printed to stderr and the exit code is set to 64. I need to capture this usage message into a variable so I can further process it.

Partial solution

Running in a subshell and capturing stderr does just that:

#!/usr/bin/env bash

function parse() {

    usage_error_msg=$(eval "$(./docopts -h "Usage: your_program <arg> [--verbose]" : "$@")" 2>&1)

    exit_code=$?

    # Exit the program with an error if the usage isn't valid
    (( exit_code == 0 )) || { echo "$usage_error_msg"; exit $exit_code; }
}

# Parse some args
parse "some arg" --verbose

# Verify that the variable $arg was correctly set by docopts 
if [ -z "${arg+x}" ]; then
    echo "\$arg is not set!"
else
    echo "the value of \$arg is \"$arg"\"
fi

This correctly captures the output and exit_code and makes the script exit with a usage message if I omit a required argument (last line in the function).

However, the variables are no longer available in the script;

Output: $arg is not set!

Question

How can I capture stdout in a variable while still having the variables that docopts sets available in my script? The answers on SO for capturing output into a variable always seem to involve a subshell.

NOTE: I include docopts as a library so I'm also open to solutions that involve modifying the docopts Python script, although I prefer a pure bash solution (I know little about Python and don't want to complicate upgrading the docopts dependency in the future).

UPDATE:

After trying glenn jackman's answer I found out that docopts prints to stdout what eval should execute;

In case of valid syntax:

verbose=true
arg='some arg'

In case of invalid syntax:

echo 'Usage: your_program <arg> [--verbose]' >&2
exit 64

Docopts will always exit with 0, unless the syntax for the docopts command itself is incorrect. Otherwise the non-zero exit code won't be set until eval is executed. I imagine all programs that rely on this principle work like this.

Another thing missing in the problem section of my original question that I should clarify, is that the program exits when eval executes the output that results from passing args with invalid syntax to docopts. So when eval runs the code from the second example, the entire script exits.

1 Answer 1

2

Try delaying the eval until you know you have success:

function parse() {

    output=$(./docopts -h "Usage: your_program <arg> [--verbose]" : "$@" 2>&1)

    exit_code=$?

    # Exit the program with an error if the usage isn't valid
    (( exit_code == 0 )) || { echo "$output"; exit $exit_code; }

    eval "$output"
}

or, structured a little differently

function parse() {
    if output=$(./docopts -h "Usage: your_program <arg> [--verbose]" : "$@" 2>&1)
    then
        eval "$output"
    else
        exit_code=$?
        echo "$output"
        exit $exit_code
    fi
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks! Your solution would work very well, if docopts wouldn't exit with 0 in both cases (it only exits non-zero in case of a syntax error for docopts itself). Your answer helped me understand how this works with eval though: docopts basically prepares all the commands that eval should execute. If parsing is successful, this means setting variables, e.g. verbose=true; arg='some arg' Or. in case of a syntax error: echo 'Usage: your_program <arg> [--verbose]' >&2; exit 64. So in this case it's actually eval "$output" that causes the non-zero exit from docopts' output.
I edited my question to include the above details; Because it doesn't answer my question precisely I didn't accept it. But it's very close so if you can extend it then I'll click the accept button.
Since we don't want the program to exit, I think it should either eval twice (first in a subshell to check for errors, then without to set the variables) or match the output against a pattern to check if the exit command is used.
I ended up matching, using a regex pattern error_pattern="exit [0-9]{1,3}"; if [[ $output =~ $error_pattern ]]; then; .... (there should be a line break between " and exit to work around the missing multiline feature when using =~, see this answer).
Nice one. Works perfectly !

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.