|
| 1 | + |
| 2 | +import os |
| 3 | +import os.path |
| 4 | +import subprocess |
| 5 | + |
| 6 | + |
| 7 | +class CalledProcessError(RuntimeError): |
| 8 | + def __init__(self, returncode, cmd, expected_returncode, output=None): |
| 9 | + self.returncode = returncode |
| 10 | + self.cmd = cmd |
| 11 | + self.expected_returncode = expected_returncode |
| 12 | + self.output = output |
| 13 | + |
| 14 | + def __str__(self): |
| 15 | + return ( |
| 16 | + 'Command: {0!r}\n' |
| 17 | + 'Return code: {1}\n' |
| 18 | + 'Expected return code {2}\n', |
| 19 | + 'Output: {3!r}\n'.format( |
| 20 | + self.cmd, |
| 21 | + self.returncode, |
| 22 | + self.expected_returncode, |
| 23 | + self.output, |
| 24 | + ), |
| 25 | + ) |
| 26 | + |
| 27 | + |
| 28 | +def _replace_cmd(cmd, **kwargs): |
| 29 | + return [part.format(**kwargs) for part in cmd] |
| 30 | + |
| 31 | + |
| 32 | +class PrefixedCommandRunner(object): |
| 33 | + """A PrefixedCommandRunner allows you to run subprocess commands with |
| 34 | + comand substitution. |
| 35 | +
|
| 36 | + For instance: |
| 37 | + PrefixedCommandRunner('/tmp/foo').run(['{prefix}foo.sh', 'bar', 'baz']) |
| 38 | +
|
| 39 | + will run ['/tmpl/foo/foo.sh', 'bar', 'baz'] |
| 40 | + """ |
| 41 | + def __init__(self, prefix_dir, popen=subprocess.Popen, makedirs=os.makedirs): |
| 42 | + self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep |
| 43 | + self.__popen = popen |
| 44 | + self.__makedirs = makedirs |
| 45 | + |
| 46 | + def _create_path_if_not_exists(self): |
| 47 | + if not os.path.exists(self.prefix_dir): |
| 48 | + self.__makedirs(self.prefix_dir) |
| 49 | + |
| 50 | + def run(self, cmd, retcode=0, stdin=None, **kwargs): |
| 51 | + self._create_path_if_not_exists() |
| 52 | + replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir) |
| 53 | + proc = self.__popen( |
| 54 | + replaced_cmd, |
| 55 | + stdin=subprocess.PIPE, |
| 56 | + stdout=subprocess.PIPE, |
| 57 | + stderr=subprocess.PIPE, |
| 58 | + **kwargs |
| 59 | + ) |
| 60 | + stdout, stderr = proc.communicate(stdin) |
| 61 | + returncode = proc.returncode |
| 62 | + |
| 63 | + if retcode is not None and retcode != returncode: |
| 64 | + raise CalledProcessError( |
| 65 | + returncode, replaced_cmd, retcode, output=(stdout, stderr), |
| 66 | + ) |
| 67 | + |
| 68 | + return proc.returncode, stdout, stderr |
| 69 | + |
| 70 | + def path(self, *parts): |
| 71 | + path = os.path.join(self.prefix_dir, *parts) |
| 72 | + return os.path.normpath(path) |
| 73 | + |
| 74 | + def exists(self, *parts): |
| 75 | + return os.path.exists(self.path(*parts)) |
| 76 | + |
| 77 | + @classmethod |
| 78 | + def from_command_runner(cls, command_runner, path_end): |
| 79 | + """Constructs a new command runner from an existing one by appending |
| 80 | + `path_end` to the command runner's prefix directory. |
| 81 | + """ |
| 82 | + return cls(command_runner.path(path_end), popen=command_runner.__popen) |
0 commit comments