#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see http://www.gnu.org/licenses/.
##
"""Executing external commands.
This module provides some functions for executing external commands in
a subprocess. They are based on the standard Python :mod:`subprocess`
module, but provide some practical enhancements.
Contents:
- :class:`DoneProcess` is a class to hold information about a terminated
process.
- :func:`run` runs a command in a subprocess and waits for it to finish.
Returns a :class:`DoneProcess` about the outcome.
- :func:`start` starts a subprocess and does not wait for it.
Returns a :class:`subprocess.Popen` instance that can be used to communicate
with the process.
This module can be used independently from pyFormex.
"""
import subprocess
import shlex
import os
[docs]class DoneProcess(subprocess.CompletedProcess):
"""A class to return the outcome of a subprocess.
This is a subclass of :class:`subprocess.CompletedProcess` with some
extra attributes.
Attributes
----------
args: str or sequence of arguments
The args that were used to create the subprocess.
returncode: int
The exit code of the subprocess. Typical a 0 means it ran
succesfully. If the subprocess fails to start, the value
is 127. Other non-zero values can be returned by the child
process to flag some error condition.
stdout: str or bytes or None
The standard output captured from the child process.
stderr: str or bytes or None
The error output captured from the child process.
failed: bool
True if the child process failed to start, e.g. due to a
non-existing or non-loadable executable. The returncode
will be 127 in this case.
timedout: bool
True if the child process exited due to a timeout condition.
"""
def __init__(self, args, returncode, *, failed=False, timedout=False,
**kargs):
super().__init__(args, returncode, **kargs)
self.failed = failed
self.timedout = timedout
def __repr__(self):
"""Textual representation of the DoneProcess
This only shows the non-default values.
"""
s = super().__repr__()
i = s.rfind(')')
s = [s[:i], s[i:]]
if self.failed:
s.insert(-1, f", failed={self.failed!r}")
if self.timedout:
s.insert(-1, f", timedout={self.timedout!r}")
return ''.join(s)
def __str__(self):
"""User-friendly full textual representation
Returns
-------
str
An extensive report about the last run command, including
output and error messages.
Notes
-----
This is mostly useful in interactive work, to find out
why a command failed.
"""
s = f"DoneProcess report\nargs: {self.args}\n"
if self.failed:
s += "Command failed to run!\n"
s += f"returncode: {self.returncode}\n"
for atr in ['stdout', 'stderr']:
val = getattr(self, atr)
if val is not None:
s += f"{atr}:\n{val}"
if s[-1] != '\n':
s += '\n'
if self.timedout:
s += f"timedout: {self.timedout}\n"
return s
[docs]def run(args, *, input=None, capture_output=True, timeout=None,
wait=True, **kargs):
"""Execute a command through the operating system.
Run an external command in a subprocess, waiting for its termination
or not. This is similar to Python3's :func:`subprocess.run`, but
provides the following enhancements:
- If `shell` is True, and no `executable` is provided, the shell
will default to the value in the user's SHELL environment variable,
and if that isn't set, to '/bin/sh'.
- If a string is passed as `args` and `shell` is False, the string is
automatically tokenized into an args list.
- If no stdout nor stderr are specified, the output of the command
is captured by default.
- The `encoding` is set to 'utf-8' by default, so that stdout
and stderr are returned as strings.
- stdin, stdout and stderr can be file names. They will be replaced
with the opened files (in mode 'r' for stdin, 'w' for the others).
- Exceptions are captured by default and can be detected from
the return value.
- A `wait` parameter allows the same function to be used to just
start a subprocess and not wait for its outcome.
Only the new and changed parameters are described hereafter. Except for
the first (`args`), all parameters should be specified by keyword.
For more parameters, see :class:`subprocess.run`.
Parameters
----------
args: str or list of str.
If a string is provided, it is the command as it should be entered
in a shell. If it is a list, args[0] is the executable to run
and the remainder of args are the arguments to be passed to it.
If a string is provided and `shell` is False (default), the string is
split into an args list using :func:`shlex.split`.
capture_output: bool
If True (default), both stdout and stderr are captured and available
from the returned :class:`DoneProcess`. Specifying a value for any of
`stdout` or `stderr` values will however override the capture_output
setting.
shell: bool
Default is False. If True, the command will be run in a new shell.
The `args` argument should be a string in this case.
Note that this uses more resources, may cause a problem in killing the
subprocess and and may pose a security risk.
Unless you need some shell functionality (like parameter expansion,
compound commands, pipes), the advised way to execute a command is
to use the default False.
executable: str
The full path name of the program to be executed.
This can be used to specify the real executable if the
program specified in `args` is not in your PATH, or,
when `shell` is True, if you want to use a shell other than
the default: the value of the 'SHELL' environment variable,
and if that is not set, '/bin/sh/'.
encoding: str
The encoding for the returned stdout and stderr. The default is
'utf-8', making the returned values Python3 str types. Specifying
encoding=None will return bytes. Another encoding may be specified
if needed.
input: str
If provided, passes the provided string as stdin to the started
process. Only available with wait=True.
timeout: int
If provided, this is the maximum number of seconds the process is
allowed to run. After this time the Process will be killed.
check: bool
Default is False.
If True, an exception will be raised when the command did not
terminate normally: a non-zero returncode, a timeout condition
or a failure to start the executable. With the default, these
conditions are captured and reported in the return value.
**kargs: keyword arguments
Any other keyword arguments accepted by :class:`subprocess.Popen`.
See the Python documentation for :class:`subprocess.Popen` for full info.
Returns
-------
:class:`DoneProcess` | :class:`subprocess.Popen`
If ``wait`` is True (default), returns a :class:`DoneProcess`
collecting all relevant info about the finished subprocess.
See :class:`DoneProcess` for details.
If ``wait`` is False, returns the created :class:`subprocess.Popen`.
In this case the user if fully responsible for handling the
communication with the process, its termination, and the processing
of its outcome.
Examples
--------
>>> P = run("pwd")
>>> P.stdout.strip('\\n') == os.getcwd()
True
>>> P = run("pwd", stdout=subprocess.PIPE)
>>> P.stdout.strip('\\n') == os.getcwd()
True
>>> P = run("echo 'This is stderr' > /dev/stderr", shell=True)
>>> P.stderr
'This is stderr\\n'
>>> P = run('false')
>>> P
DoneProcess(args=['false'], returncode=1, stdout='', stderr='')
>>> P = run('false', capture_output=False)
>>> P
DoneProcess(args=['false'], returncode=1)
>>> P.stdout is None
True
>>> P = run('False')
>>> P
DoneProcess(args=['False'], returncode=127, failed=True)
>>> P = run("sleep 5", timeout=1, capture_output=False)
>>> P
DoneProcess(args=['sleep', '5'], returncode=-1, timedout=True)
>>> print(P)
DoneProcess report
args: ['sleep', '5']
returncode: -1
timedout: True
>>> P = run("sleep 10", wait=False)
>>> P.poll() is None
True
"""
if capture_output and 'stdout' not in kargs and 'stderr' not in kargs:
kargs['stdout'] = subprocess.PIPE
kargs['stderr'] = subprocess.PIPE
kargs.setdefault('encoding', 'utf-8')
for f in ['stdin', 'stdout', 'stderr']:
if f in kargs and isinstance(kargs[f], str):
if f[-1] == 'n':
mode = 'r'
else:
mode = 'w'
kargs[f] = open(kargs[f], mode)
shell = bool(kargs.get('shell', False))
if shell and 'executable' not in kargs:
kargs['executable'] = os.environ.get('SHELL', '/bin/sh')
if isinstance(args, str) and shell is False:
# Tokenize the command line
args = shlex.split(args)
check = kargs.pop('check', False)
try:
P = subprocess.Popen(args, **kargs)
except Exception as e:
if check:
raise e
return DoneProcess(args, 127, failed=True)
if not wait:
return P
with P:
try:
stdout, stderr = P.communicate(input, timeout=timeout)
except subprocess.TimeoutExpired as e:
if check:
raise e
P.kill()
stdout, stderr = P.communicate()
return DoneProcess(args, -1, stdout=stdout, stderr=stderr,
timedout=True)
except Exception as e:
# Unexpected error
P.kill()
raise e
retcode = P.poll()
if retcode and check:
raise subprocess.CalledProcessError(
retcode, args, output=stdout, stderr=stderr)
return DoneProcess(args, retcode, stdout=stdout, stderr=stderr)
# deprecated: for ccompatibility
def start(args, **kargs):
return run(args, wait=False, **kargs)
if __name__ == "__main__":
print("This is process.py")
P = run("pwd", shell=True)
print(P)
P = run("ls -l", stdout="filelist.txt")
print(P)
P = run("sleep 50", timeout=2)
print(P)
### End