kiwi.command

  1# Copyright (c) 2015 SUSE Linux GmbH.  All rights reserved.
  2#
  3# This file is part of kiwi.
  4#
  5# kiwi is free software: you can redistribute it and/or modify
  6# it under the terms of the GNU General Public License as published by
  7# the Free Software Foundation, either version 3 of the License, or
  8# (at your option) any later version.
  9#
 10# kiwi is distributed in the hope that it will be useful,
 11# but WITHOUT ANY WARRANTY; without even the implied warranty of
 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13# GNU General Public License for more details.
 14#
 15# You should have received a copy of the GNU General Public License
 16# along with kiwi.  If not, see <http://www.gnu.org/licenses/>
 17#
 18from typing import IO, Callable, Literal, List, MutableMapping, NamedTuple, Optional, overload
 19import logging
 20import os
 21import select
 22import subprocess
 23
 24# project
 25from kiwi.utils.codec import Codec
 26
 27from kiwi.exceptions import (
 28    KiwiCommandError,
 29    KiwiCommandNotFound
 30)
 31
 32log = logging.getLogger('kiwi')
 33
 34
 35class CommandT(NamedTuple):
 36    output: str
 37    error: str
 38    returncode: int
 39
 40
 41class CommandCallT(NamedTuple):
 42    output: IO[bytes]
 43    output_available: Callable[[], bool]
 44    error: IO[bytes]
 45    error_available: Callable[[], bool]
 46    process: subprocess.Popen
 47
 48
 49class Command:
 50    """
 51    **Implements command invocation**
 52
 53    An instance of Command provides methods to invoke external
 54    commands in blocking and non blocking mode. Control of
 55    stdout and stderr is given to the caller
 56    """
 57
 58    @overload
 59    @staticmethod
 60    def run(
 61        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 62        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 63        raise_on_command_not_found: Literal[False] = False
 64    ) -> CommandT:
 65        ...  # pragma: no cover
 66
 67    @overload
 68    @staticmethod
 69    def run(
 70        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 71        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 72        raise_on_command_not_found: bool = True
 73    ) -> Optional[CommandT]:
 74        ...  # pragma: no cover
 75
 76    @staticmethod
 77    def run(
 78        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 79        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 80        raise_on_command_not_found: bool = True
 81    ) -> Optional[CommandT]:
 82        """
 83        Execute a program and block the caller. The return value
 84        is a CommandT namedtuple containing the stdout, stderr
 85        and return code information. Unless raise_on_error is
 86        set to `False` an exception is thrown if the command
 87        exits with an error code not equal to zero. If
 88        raise_on_command_not_found is `False` and the command is
 89        not found, then `None` is returned.
 90
 91        Example:
 92
 93        .. code:: python
 94
 95            result = Command.run(['ls', '-l'])
 96
 97        :param list command: command and arguments
 98        :param dict custom_env: custom os.environ
 99        :param bool raise_on_error: control error behaviour
100        :param bool stderr_to_stdout: redirects stderr to stdout
101
102        :return:
103            Contains call results in command type
104
105            .. code:: python
106
107                CommandT(output='string', error='string', returncode=int)
108
109        :rtype: CommandT
110        """
111        from .path import Path
112        environment = custom_env or os.environ
113        cmd_abspath: Optional[str]
114        if command[0].startswith("/"):
115            cmd_abspath = command[0]
116            if not os.path.exists(cmd_abspath):
117                cmd_abspath = None
118        else:
119            cmd_abspath = Path.which(
120                command[0], custom_env=environment, access_mode=os.X_OK
121            )
122
123        if not cmd_abspath:
124            message = f'Command "{command[0]}" not found in the environment'
125            if raise_on_command_not_found:
126                raise KiwiCommandNotFound(message)
127            log.debug('EXEC: %s', message)
128            return None
129        stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE
130        log.debug('EXEC: [%s]', ' '.join(command))
131        try:
132            process = subprocess.Popen(
133                [cmd_abspath] + command[1:],
134                stdout=subprocess.PIPE,
135                stderr=stderr,
136                env=environment
137            )
138        except (OSError, subprocess.SubprocessError) as e:
139            raise KiwiCommandError(
140                f'{command[0]}: {type(e).__name__}: {format(e)}'
141            ) from e
142
143        output, error = process.communicate()
144        if process.returncode != 0 and raise_on_error:
145            if not error:
146                error = bytes(b'(no output on stderr)')
147            if not output:
148                output = bytes(b'(no output on stdout)')
149            log.debug(
150                'EXEC: Failed with stderr: {0}, stdout: {1}'.format(
151                    Codec.decode(error), Codec.decode(output)
152                )
153            )
154            raise KiwiCommandError(
155                '{0}: stderr: {1}, stdout: {2}'.format(
156                    command[0], Codec.decode(error), Codec.decode(output)
157                )
158            )
159        return CommandT(
160            output=Codec.decode(output),
161            error=Codec.decode(error),
162            returncode=process.returncode
163        )
164
165    @staticmethod
166    def call(
167            command: List[str],
168            custom_env: Optional[MutableMapping[str, str]] = None
169    ) -> CommandCallT:
170        """
171        Execute a program and return an io file handle pair back.
172        stdout and stderr are both on different channels. The caller
173        must read from the output file handles in order to actually
174        run the command. This can be done using the CommandIterator
175        from command_process
176
177        Example:
178
179        .. code:: python
180
181            process = Command.call(['ls', '-l'])
182
183        :param list command: command and arguments
184        :param list custom_env: custom os.environ
185
186        :return:
187            Contains process results in command type
188
189            .. code:: python
190
191                command(
192                    output='string', output_available=bool,
193                    error='string', error_available=bool,
194                    process=subprocess
195                )
196
197        :rtype: namedtuple
198        """
199        from .path import Path
200        log.debug('EXEC: [%s]', ' '.join(command))
201        environment = custom_env or os.environ
202        if not Path.which(
203            command[0], custom_env=environment, access_mode=os.X_OK
204        ):
205            raise KiwiCommandNotFound(
206                f'Command "{command[0]}" not found in the environment'
207            )
208        try:
209            process = subprocess.Popen(
210                command,
211                stdout=subprocess.PIPE,
212                stderr=subprocess.PIPE,
213                env=environment
214            )
215        except Exception as e:
216            raise KiwiCommandError(
217                f'{type(e).__name__}: {format(e)}'
218            ) from e
219
220        # guaranteed to be true as stdout & stderr equal subprocess.PIPE
221        assert process.stdout and process.stderr
222
223        def output_available() -> Callable[[], bool]:
224            def _select():
225                readable, _, exceptional = select.select(
226                    [process.stdout], [], [process.stdout], 1e-4
227                )
228                if readable and not exceptional:
229                    return True
230                return False
231            return _select
232
233        def error_available() -> Callable[[], bool]:
234            def _select():
235                readable, _, exceptional = select.select(
236                    [process.stderr], [], [process.stderr], 1e-4
237                )
238                if readable and not exceptional:
239                    return True
240                return False
241            return _select
242
243        return CommandCallT(
244            output=process.stdout,
245            output_available=output_available(),
246            error=process.stderr,
247            error_available=error_available(),
248            process=process
249        )
log = <Logger kiwi (DEBUG)>
class CommandT(typing.NamedTuple):
36class CommandT(NamedTuple):
37    output: str
38    error: str
39    returncode: int

CommandT(output, error, returncode)

CommandT(output: str, error: str, returncode: int)

Create new instance of CommandT(output, error, returncode)

output: str

Alias for field number 0

error: str

Alias for field number 1

returncode: int

Alias for field number 2

class CommandCallT(typing.NamedTuple):
42class CommandCallT(NamedTuple):
43    output: IO[bytes]
44    output_available: Callable[[], bool]
45    error: IO[bytes]
46    error_available: Callable[[], bool]
47    process: subprocess.Popen

CommandCallT(output, output_available, error, error_available, process)

CommandCallT( output: IO[bytes], output_available: Callable[[], bool], error: IO[bytes], error_available: Callable[[], bool], process: pdoc.extract._PdocDefusedPopen)

Create new instance of CommandCallT(output, output_available, error, error_available, process)

output: IO[bytes]

Alias for field number 0

output_available: Callable[[], bool]

Alias for field number 1

error: IO[bytes]

Alias for field number 2

error_available: Callable[[], bool]

Alias for field number 3

process: pdoc.extract._PdocDefusedPopen

Alias for field number 4

class Command:
 50class Command:
 51    """
 52    **Implements command invocation**
 53
 54    An instance of Command provides methods to invoke external
 55    commands in blocking and non blocking mode. Control of
 56    stdout and stderr is given to the caller
 57    """
 58
 59    @overload
 60    @staticmethod
 61    def run(
 62        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 63        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 64        raise_on_command_not_found: Literal[False] = False
 65    ) -> CommandT:
 66        ...  # pragma: no cover
 67
 68    @overload
 69    @staticmethod
 70    def run(
 71        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 72        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 73        raise_on_command_not_found: bool = True
 74    ) -> Optional[CommandT]:
 75        ...  # pragma: no cover
 76
 77    @staticmethod
 78    def run(
 79        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 80        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 81        raise_on_command_not_found: bool = True
 82    ) -> Optional[CommandT]:
 83        """
 84        Execute a program and block the caller. The return value
 85        is a CommandT namedtuple containing the stdout, stderr
 86        and return code information. Unless raise_on_error is
 87        set to `False` an exception is thrown if the command
 88        exits with an error code not equal to zero. If
 89        raise_on_command_not_found is `False` and the command is
 90        not found, then `None` is returned.
 91
 92        Example:
 93
 94        .. code:: python
 95
 96            result = Command.run(['ls', '-l'])
 97
 98        :param list command: command and arguments
 99        :param dict custom_env: custom os.environ
100        :param bool raise_on_error: control error behaviour
101        :param bool stderr_to_stdout: redirects stderr to stdout
102
103        :return:
104            Contains call results in command type
105
106            .. code:: python
107
108                CommandT(output='string', error='string', returncode=int)
109
110        :rtype: CommandT
111        """
112        from .path import Path
113        environment = custom_env or os.environ
114        cmd_abspath: Optional[str]
115        if command[0].startswith("/"):
116            cmd_abspath = command[0]
117            if not os.path.exists(cmd_abspath):
118                cmd_abspath = None
119        else:
120            cmd_abspath = Path.which(
121                command[0], custom_env=environment, access_mode=os.X_OK
122            )
123
124        if not cmd_abspath:
125            message = f'Command "{command[0]}" not found in the environment'
126            if raise_on_command_not_found:
127                raise KiwiCommandNotFound(message)
128            log.debug('EXEC: %s', message)
129            return None
130        stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE
131        log.debug('EXEC: [%s]', ' '.join(command))
132        try:
133            process = subprocess.Popen(
134                [cmd_abspath] + command[1:],
135                stdout=subprocess.PIPE,
136                stderr=stderr,
137                env=environment
138            )
139        except (OSError, subprocess.SubprocessError) as e:
140            raise KiwiCommandError(
141                f'{command[0]}: {type(e).__name__}: {format(e)}'
142            ) from e
143
144        output, error = process.communicate()
145        if process.returncode != 0 and raise_on_error:
146            if not error:
147                error = bytes(b'(no output on stderr)')
148            if not output:
149                output = bytes(b'(no output on stdout)')
150            log.debug(
151                'EXEC: Failed with stderr: {0}, stdout: {1}'.format(
152                    Codec.decode(error), Codec.decode(output)
153                )
154            )
155            raise KiwiCommandError(
156                '{0}: stderr: {1}, stdout: {2}'.format(
157                    command[0], Codec.decode(error), Codec.decode(output)
158                )
159            )
160        return CommandT(
161            output=Codec.decode(output),
162            error=Codec.decode(error),
163            returncode=process.returncode
164        )
165
166    @staticmethod
167    def call(
168            command: List[str],
169            custom_env: Optional[MutableMapping[str, str]] = None
170    ) -> CommandCallT:
171        """
172        Execute a program and return an io file handle pair back.
173        stdout and stderr are both on different channels. The caller
174        must read from the output file handles in order to actually
175        run the command. This can be done using the CommandIterator
176        from command_process
177
178        Example:
179
180        .. code:: python
181
182            process = Command.call(['ls', '-l'])
183
184        :param list command: command and arguments
185        :param list custom_env: custom os.environ
186
187        :return:
188            Contains process results in command type
189
190            .. code:: python
191
192                command(
193                    output='string', output_available=bool,
194                    error='string', error_available=bool,
195                    process=subprocess
196                )
197
198        :rtype: namedtuple
199        """
200        from .path import Path
201        log.debug('EXEC: [%s]', ' '.join(command))
202        environment = custom_env or os.environ
203        if not Path.which(
204            command[0], custom_env=environment, access_mode=os.X_OK
205        ):
206            raise KiwiCommandNotFound(
207                f'Command "{command[0]}" not found in the environment'
208            )
209        try:
210            process = subprocess.Popen(
211                command,
212                stdout=subprocess.PIPE,
213                stderr=subprocess.PIPE,
214                env=environment
215            )
216        except Exception as e:
217            raise KiwiCommandError(
218                f'{type(e).__name__}: {format(e)}'
219            ) from e
220
221        # guaranteed to be true as stdout & stderr equal subprocess.PIPE
222        assert process.stdout and process.stderr
223
224        def output_available() -> Callable[[], bool]:
225            def _select():
226                readable, _, exceptional = select.select(
227                    [process.stdout], [], [process.stdout], 1e-4
228                )
229                if readable and not exceptional:
230                    return True
231                return False
232            return _select
233
234        def error_available() -> Callable[[], bool]:
235            def _select():
236                readable, _, exceptional = select.select(
237                    [process.stderr], [], [process.stderr], 1e-4
238                )
239                if readable and not exceptional:
240                    return True
241                return False
242            return _select
243
244        return CommandCallT(
245            output=process.stdout,
246            output_available=output_available(),
247            error=process.stderr,
248            error_available=error_available(),
249            process=process
250        )

Implements command invocation

An instance of Command provides methods to invoke external commands in blocking and non blocking mode. Control of stdout and stderr is given to the caller

@staticmethod
def run( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, raise_on_error: bool = True, stderr_to_stdout: bool = False, raise_on_command_not_found: bool = True) -> Optional[CommandT]:
 77    @staticmethod
 78    def run(
 79        command: List[str], custom_env: Optional[MutableMapping[str, str]] = None,
 80        raise_on_error: bool = True, stderr_to_stdout: bool = False,
 81        raise_on_command_not_found: bool = True
 82    ) -> Optional[CommandT]:
 83        """
 84        Execute a program and block the caller. The return value
 85        is a CommandT namedtuple containing the stdout, stderr
 86        and return code information. Unless raise_on_error is
 87        set to `False` an exception is thrown if the command
 88        exits with an error code not equal to zero. If
 89        raise_on_command_not_found is `False` and the command is
 90        not found, then `None` is returned.
 91
 92        Example:
 93
 94        .. code:: python
 95
 96            result = Command.run(['ls', '-l'])
 97
 98        :param list command: command and arguments
 99        :param dict custom_env: custom os.environ
100        :param bool raise_on_error: control error behaviour
101        :param bool stderr_to_stdout: redirects stderr to stdout
102
103        :return:
104            Contains call results in command type
105
106            .. code:: python
107
108                CommandT(output='string', error='string', returncode=int)
109
110        :rtype: CommandT
111        """
112        from .path import Path
113        environment = custom_env or os.environ
114        cmd_abspath: Optional[str]
115        if command[0].startswith("/"):
116            cmd_abspath = command[0]
117            if not os.path.exists(cmd_abspath):
118                cmd_abspath = None
119        else:
120            cmd_abspath = Path.which(
121                command[0], custom_env=environment, access_mode=os.X_OK
122            )
123
124        if not cmd_abspath:
125            message = f'Command "{command[0]}" not found in the environment'
126            if raise_on_command_not_found:
127                raise KiwiCommandNotFound(message)
128            log.debug('EXEC: %s', message)
129            return None
130        stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE
131        log.debug('EXEC: [%s]', ' '.join(command))
132        try:
133            process = subprocess.Popen(
134                [cmd_abspath] + command[1:],
135                stdout=subprocess.PIPE,
136                stderr=stderr,
137                env=environment
138            )
139        except (OSError, subprocess.SubprocessError) as e:
140            raise KiwiCommandError(
141                f'{command[0]}: {type(e).__name__}: {format(e)}'
142            ) from e
143
144        output, error = process.communicate()
145        if process.returncode != 0 and raise_on_error:
146            if not error:
147                error = bytes(b'(no output on stderr)')
148            if not output:
149                output = bytes(b'(no output on stdout)')
150            log.debug(
151                'EXEC: Failed with stderr: {0}, stdout: {1}'.format(
152                    Codec.decode(error), Codec.decode(output)
153                )
154            )
155            raise KiwiCommandError(
156                '{0}: stderr: {1}, stdout: {2}'.format(
157                    command[0], Codec.decode(error), Codec.decode(output)
158                )
159            )
160        return CommandT(
161            output=Codec.decode(output),
162            error=Codec.decode(error),
163            returncode=process.returncode
164        )

Execute a program and block the caller. The return value is a CommandT namedtuple containing the stdout, stderr and return code information. Unless raise_on_error is set to False an exception is thrown if the command exits with an error code not equal to zero. If raise_on_command_not_found is False and the command is not found, then None is returned.

Example:

.. code:: python

result = Command.run(['ls', '-l'])
Parameters
  • list command: command and arguments
  • dict custom_env: custom os.environ
  • bool raise_on_error: control error behaviour
  • bool stderr_to_stdout: redirects stderr to stdout
Returns
Contains call results in command type

.. code:: python

    CommandT(output='string', error='string', returncode=int)
@staticmethod
def call( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None) -> CommandCallT:
166    @staticmethod
167    def call(
168            command: List[str],
169            custom_env: Optional[MutableMapping[str, str]] = None
170    ) -> CommandCallT:
171        """
172        Execute a program and return an io file handle pair back.
173        stdout and stderr are both on different channels. The caller
174        must read from the output file handles in order to actually
175        run the command. This can be done using the CommandIterator
176        from command_process
177
178        Example:
179
180        .. code:: python
181
182            process = Command.call(['ls', '-l'])
183
184        :param list command: command and arguments
185        :param list custom_env: custom os.environ
186
187        :return:
188            Contains process results in command type
189
190            .. code:: python
191
192                command(
193                    output='string', output_available=bool,
194                    error='string', error_available=bool,
195                    process=subprocess
196                )
197
198        :rtype: namedtuple
199        """
200        from .path import Path
201        log.debug('EXEC: [%s]', ' '.join(command))
202        environment = custom_env or os.environ
203        if not Path.which(
204            command[0], custom_env=environment, access_mode=os.X_OK
205        ):
206            raise KiwiCommandNotFound(
207                f'Command "{command[0]}" not found in the environment'
208            )
209        try:
210            process = subprocess.Popen(
211                command,
212                stdout=subprocess.PIPE,
213                stderr=subprocess.PIPE,
214                env=environment
215            )
216        except Exception as e:
217            raise KiwiCommandError(
218                f'{type(e).__name__}: {format(e)}'
219            ) from e
220
221        # guaranteed to be true as stdout & stderr equal subprocess.PIPE
222        assert process.stdout and process.stderr
223
224        def output_available() -> Callable[[], bool]:
225            def _select():
226                readable, _, exceptional = select.select(
227                    [process.stdout], [], [process.stdout], 1e-4
228                )
229                if readable and not exceptional:
230                    return True
231                return False
232            return _select
233
234        def error_available() -> Callable[[], bool]:
235            def _select():
236                readable, _, exceptional = select.select(
237                    [process.stderr], [], [process.stderr], 1e-4
238                )
239                if readable and not exceptional:
240                    return True
241                return False
242            return _select
243
244        return CommandCallT(
245            output=process.stdout,
246            output_available=output_available(),
247            error=process.stderr,
248            error_available=error_available(),
249            process=process
250        )

Execute a program and return an io file handle pair back. stdout and stderr are both on different channels. The caller must read from the output file handles in order to actually run the command. This can be done using the CommandIterator from command_process

Example:

.. code:: python

process = Command.call(['ls', '-l'])
Parameters
  • list command: command and arguments
  • list custom_env: custom os.environ
Returns
Contains process results in command type

.. code:: python

    command(
        output='string', output_available=bool,
        error='string', error_available=bool,
        process=subprocess
    )