mastupristi
mastupristi

Reputation: 1478

How to capture command in a variable and exit code in another when using pipe?

in a bash script I need to capture the output of a command executed with eval and also the exit code of the same command.

    output=$(eval "$test_cmd" 2>&1)
    result=$?

$test_cmd is actually the command line for running a python script. The command string itself contains strings surrounded by quotation marks. I tried running it without eval and it did not work. It may be that the problem was trivial but I didn't investigate, knowing that it worked with eval.

The command above works but I often get a warning:

warning: command substitution: ignored null byte in input

If I have interpreted correctly it depends on the output of the command executed.
So I tried this:

    output=$(eval "$test_cmd" 2>&1 | tr -d '\0')
    result=$?

The warning disappears, but this way $? is that of tr and not of my command.

I considered using ${PIPESTATUS[0]} instead of $?, but it wouldn't work because the pipe is inside another shell.


Is this the solution?

I ended up doing this:

    output=$( eval "$test_cmd" 2>&1 | tr -d '\0'; return ${PIPESTATUS[0]} )
    result=$?

From early tests it seems to work, but I ask are there drawbacks, is this the right way or are there better ways.

Upvotes: 1

Views: 85

Answers (3)

Ed Morton
Ed Morton

Reputation: 203502

It sounds like you got to this point by storing a command in a scalar variable and executing it with eval. Rather than investigating a workaround for this aspect of the problems introduced by that workaround (bandaids on top of bandaids) you should instead just fix the problem that got you here in the first place, i.e. storing a command in a scalar variable, so you don't need eval and then you don't have your current problem (plus others you may not have tripped over yet).

I assume you're doing something like this:

test_cmd='date +"%F %T"'
output=$(eval "$test_cmd" 2>&1)
result=$?

when you just need to store the command in an array instead, see http://mywiki.wooledge.org/BashFAQ/050, something like:

test_cmd=( date +"%F %T" )
output=$( "${test_cmd[@]}" 2>&1 ); result=$?

I also moved result=$? to the end of the line where test_cmd executes as that's best practice (after just not saving the exit status and instead testing the result of executing the command inline) so it's harder for anyone to accidentally add a echo "$output" or similar between the two which would change the value of $? before you save it.

As for the warning: command substitution: ignored null byte in input warning - if test_cmd is producing undesirable NULs, don't try to solve that problem everywhere you call test_cmd, wrap it in a function (or script) and then call THAT instead, e.g.:

run_test_cmd() {
    "${test_cmd[@]}" 2>&1 | tr -d '\0'
    return "${PIPESTATUS[0]}"
}

output=$( run_test_cmd ); result=$?

Upvotes: 1

Léa Gris
Léa Gris

Reputation: 19555

Avoiding the use of eval has already been discussed by multiple contributors, so let's skip that part and focus on addressing your problem.

For documentation and testing purposes, let’s create a simple function that outputs some text containing a null character and sets a specific return code:

test_cmd() {
  printf 'Hello World\0\n'
  return 42
}

If you run the following commands:

output=$(test_cmd)
result=$?

You will indeed see a warning about the null character in the output.

To suppress this warning, you can execute the function within a command group and redirect the stderr of the entire group, like so:

#!/usr/bin/env bash

{ output=$(test_cmd);} 2>/dev/null # Suppresses the null character warning
result=$?

# Check the captured output and return value
declare -p output result

The output confirms that these commands correctly capture the output (ignoring the null character and removing the trailing newline) without any kind of warning, and also capture the return value:

declare -- output="Hello World"
declare -- result="42"

Upvotes: 0

KamilCuk
KamilCuk

Reputation: 141010

You could:

output=$( eval "$test_cmd" 2>&1 > >(tr -d '\0') )

Upvotes: 2

Related Questions