如何捕获 Python subprocess.check_output()的异常输出?

我试图在 Python 中进行比特币支付,在 bash 中我通常会这样做:

bitcoin sendtoaddress <bitcoin address> <amount>

例如:

bitcoin sendtoaddress 1HoCUcbK9RbVnuaGQwiyaJGGAG6xrTPC9y 1.4214

如果成功,我得到一个交易 ID 作为输出,但如果我尝试转移一个比我的比特币余额大的数额,我得到以下输出:

error: {"code":-4,"message":"Insufficient funds"}

在我的 Python 程序中,我现在尝试做如下支付:

import subprocess


try:
output = subprocess.check_output(['bitcoin', 'sendtoaddress', address, str(amount)])
except:
print "Unexpected error:", sys.exc_info()

如果有足够的平衡,它工作正常,但如果没有足够的平衡 sys.exc_info()打印出这个:

(<class 'subprocess.CalledProcessError'>, CalledProcessError(), <traceback object at 0x7f339599ac68>)

但是它不包括我在命令行中得到的错误。因此,我的问题是: 如何从 Python 中获得输出错误({"code":-4,"message":"Insufficient funds"}) ?

213960 次浏览

According to the subprocess.check_output() docs, the exception raised on error has an output attribute that you can use to access the error details:

try:
subprocess.check_output(...)
except subprocess.CalledProcessError as e:
print(e.output)

You should then be able to analyse this string and parse the error details with the json module:

if e.output.startswith('error: {'):
error = json.loads(e.output[7:]) # Skip "error: "
print(error['code'])
print(error['message'])

Trying to "transfer an amount larger than my bitcoin balance" is not an unexpected error. You could use Popen.communicate() directly instead of check_output() to avoid raising an exception unnecessarily:

from subprocess import Popen, PIPE


p = Popen(['bitcoin', 'sendtoaddress', ..], stdout=PIPE)
output = p.communicate()[0]
if p.returncode != 0:
print("bitcoin failed %d %s" % (p.returncode, output))

I don't think the accepted solution handles the case where the error text is reported on stderr. From my testing the exception's output attribute did not contain the results from stderr and the docs warn against using stderr=PIPE in check_output(). Instead, I would suggest one small improvement to J.F Sebastian's solution by adding stderr support. We are, after all, trying to handle errors and stderr is where they are often reported.

from subprocess import Popen, PIPE


p = Popen(['bitcoin', 'sendtoaddress', ..], stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if p.returncode != 0:
print("bitcoin failed %d %s %s" % (p.returncode, output, error))

There are good answers here, but in these answers, there has not been an answer that comes up with the text from the stack-trace output, which is the default behavior of an exception.

If you wish to use that formatted traceback information, you might wish to:

import traceback


try:
check_call( args )
except CalledProcessError:
tb = traceback.format_exc()
tb = tb.replace(passwd, "******")
print(tb)
exit(1)

As you might be able to tell, the above is useful in case you have a password in the check_call( args ) that you wish to prevent from displaying.

Based on the answer of @macetw I print the exception directly to stderr in a decorator.

Python 3

from functools import wraps
from sys import stderr
from traceback import format_exc
from typing import Callable, Collection, Any, Mapping




def force_error_output(func: Callable):
@wraps(func)
def forced_error_output(*args: Collection[Any], **kwargs: Mapping[str, Any]):
nonlocal func


try:
func(*args, **kwargs)
except Exception as exception:
stderr.write(format_exc())
stderr.write("\n")
stderr.flush()


raise exception


return forced_error_output

Python 2

from functools import wraps
from sys import stderr
from traceback import format_exc




def force_error_output(func):
@wraps(func)
def forced_error_output(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as exception:
stderr.write(format_exc())
stderr.write("\n")
stderr.flush()


raise exception


return forced_error_output

Then in your worker just use the decorator

@force_error_output
def da_worker(arg1: int, arg2: str):
pass

As mentioned by @Sebastian the default solution should aim to use run(): https://docs.python.org/3/library/subprocess.html#subprocess.run

Here a convenient implementation (feel free to change the log class with print statements or what ever other logging functionality you are using):

import subprocess


def _run_command(command):
log.debug("Command: {}".format(command))
result = subprocess.run(command, shell=True, capture_output=True)
if result.stderr:
raise subprocess.CalledProcessError(
returncode = result.returncode,
cmd = result.args,
stderr = result.stderr
)
if result.stdout:
log.debug("Command Result: {}".format(result.stdout.decode('utf-8')))
return result

And sample usage (code is unrelated, but I think it serves as example of how readable and easy to work with errors it is with this simple implementation):

try:
# Unlock PIN Card
_run_command(
"sudo qmicli --device=/dev/cdc-wdm0 -p --uim-verify-pin=PIN1,{}"
.format(pin)
)


except subprocess.CalledProcessError as error:
if "couldn't verify PIN" in error.stderr.decode("utf-8"):
log.error(
"SIM card could not be unlocked. "
"Either the PIN is wrong or the card is not properly connected. "
"Resetting module..."
)
_reset_4g_hat()
return

This did the trick for me. It captures all the stdout output from the subprocess(For python 3.8):

from subprocess import check_output, STDOUT
cmd = "Your Command goes here"
try:
cmd_stdout = check_output(cmd, stderr=STDOUT, shell=True).decode()
except Exception as e:
print(e.output.decode()) # print out the stdout messages up to the exception
print(e) # To print out the exception message

Since Python 3.5, subprocess.run() supports check argument:

If check is true, and the process exits with a non-zero exit code, a CalledProcessError exception will be raised. Attributes of that exception hold the arguments, the exit code, and stdout and stderr if they were captured.

A simple example that will raise and print out CalledProcessError:

import subprocess
try:
subprocess.run("exit 1", shell=True, check=True, timeout=15, capture_output=True)
except subprocess.CalledProcessError as e:
print(e)  # Output: Command 'exit 1' returned non-zero exit status 1.

The subprocess invoked needs to be told to capture the output in the invoked program and raise the exception. It's simple to do it.

Firstly, Use

subprocess.run() instead of subprocess.call()

Let's assume u wanna python script called "Vijay.py". For raising the exception, use the following;

subprocess.run("py vijay.py", check=True, capture_output=True, shell=True)

The above method then can be put in try and except block to immediately raise the error or can use sys.exit(1) :any non-zero exit is fine

try:
subprocess.call("py vijay.py", check=True, capture_output=True, shell=True)
except Exception as e:
print("Exception raised: ", e)

and body of vijay.py can be as follows;

vijay.py

try:
Your code is here...
except Exception as e:
sys.exit(1) // or can even use raise Exception("ur own exception to raise:)
enter code here

I think most of previous answers are correct, in my case I needed to do this on Windows server and command was a Powershell, for that this worked really nicely for me:

    try:
        

    

print("inpgoress")


cmd_exec="Get-Date"
print(cmd_aws)


subprocess.run(['powershell', '-Command', cmd_exec],shell=False,check=True,capture_output=True,text=True,encoding="utf-8")


        



except Exception as e:
print(e)
print("ERROR: something went wrong executing powershell command")
raise e