|
| 1 | +"""A persistent shell connection |
| 2 | +
|
| 3 | +This module provides a context manager that establishes a connection to a shell |
| 4 | +and can be used to execute multiple commands in that shell. Shells are usually |
| 5 | +remote shells, e.g. connected via an ``ssh``-client, but local shells like |
| 6 | +``zsh``, ``bash`` or ``PowerShell`` can also be used. |
| 7 | +
|
| 8 | +The context manager returns an instance of :class:`ShellCommandExecutor` that |
| 9 | +can be used to execute commands in the shell via the method |
| 10 | +:meth:`ShellCommandExecutor.__call__`. The method will return an instance of |
| 11 | +a subclass of :class:`ShellCommandResponseGenerator` that can be used to |
| 12 | +retrieve the output of the command, the result code of the command, and the |
| 13 | +stderr-output of the command. |
| 14 | +
|
| 15 | +Every response generator expects a certain output structure. It is responsible |
| 16 | +for ensuring that the output structure is generated. To this end every |
| 17 | +response generator provides a method |
| 18 | +:meth:`ShellCommandResponseGenerator.get_command_list`. The method |
| 19 | +:class:`ShellCommandExecutor.__call__` will pass the user-provided command to |
| 20 | +:meth:`ShellCommandResponseGenerator.get_command_list` and receive a list of |
| 21 | +final commands that should be executed in the connected shell and that will |
| 22 | +generate the expected output structure. Instances of |
| 23 | +:class:`ShellCommandResponseGenerator` have therefore four tasks: |
| 24 | +
|
| 25 | + 1. Create a final command list that is used to execute the user provided |
| 26 | + command. This could, for example, execute the command, print an |
| 27 | + end marker, and print the return code of the command. |
| 28 | +
|
| 29 | + 2. Parse the output of the command, yield it to the user. |
| 30 | +
|
| 31 | + 3. Read the return code and provide it to the user. |
| 32 | +
|
| 33 | + 4. Provide stderr-output to the user. |
| 34 | +
|
| 35 | +A very versatile example of a response generator is the class |
| 36 | +:class:`VariableLengthResponseGenerator`. It can be used to execute a command |
| 37 | +that will result in an output of unknown length, e.g. ``ls``, and will yield |
| 38 | +the output of the command to the user. It does that by using a random |
| 39 | +*end marker* to detect the end of the output and read the trailing return code. |
| 40 | +This is suitable for almost all commands. |
| 41 | +
|
| 42 | +If :class:`VariableLengthResponseGenerator` is so versatile, why not just |
| 43 | +implement its functionality in :class:`ShellCommandExecutor`? There are two |
| 44 | +major reasons for that: |
| 45 | +
|
| 46 | +1. Although the :class:`VariableLengthResponseGenerator` is very versatile, |
| 47 | + it is not the most efficient implementation for commands that produce large |
| 48 | + amounts of output. In addition, there is also a minimal risk that the end |
| 49 | + marker is part of the output of the command, which would trip up the response |
| 50 | + generator. Putting response generation into a separate class allows to |
| 51 | + implement specific operations more efficiently and more safely. |
| 52 | + For example, |
| 53 | + :class:`DownloadResponseGenerator` implements the download of files. It |
| 54 | + takes a remote file name as user "command" and creates a final command list |
| 55 | + that emits the length of the file, a newline, the file content, a return |
| 56 | + code, and a newline. This allows :class:`DownloadResponseGenerator` |
| 57 | + to parse the output without relying on an end marker, thus increasing |
| 58 | + efficiency and safety |
| 59 | +
|
| 60 | +2. Factoring out the response generation creates an interface that can be used |
| 61 | + to support the syntax of different shells and the difference in command |
| 62 | + names and options in different operating systems. For example, the response |
| 63 | + generator class :class:`VariableLengthResponseGeneratorPowerShell` supports |
| 64 | + the invocation of commands with variable length output in a ``PowerShell``. |
| 65 | +
|
| 66 | +In short, parser generator classes encapsulate details of shell-syntax and |
| 67 | +operation implementation. That allows support of different shell syntax, and |
| 68 | +the efficient implementation of specific higher level operations, e.g. |
| 69 | +``download``. It also allows users to extend the functionality of |
| 70 | +:class:`ShellCommandExecutor` by providing their own response generator |
| 71 | +classes. |
| 72 | +
|
| 73 | +The module :mod:`datalad_next.shell.response_generators` provides two generally |
| 74 | +applicable abstract response generator classes: |
| 75 | +
|
| 76 | + - :class:`VariableLengthResponseGenerator` |
| 77 | +
|
| 78 | + - :class:`FixedLengthResponseGenerator` |
| 79 | +
|
| 80 | +The functionality of the former is described above. The latter can be used to |
| 81 | +execute a command that will result in output of known |
| 82 | +length, e.g. ``echo -n 012345``. It reads the specified number of bytes and a |
| 83 | +trailing return code. This is more performant than the variable length response |
| 84 | +generator (because it does not have to search for the end marker). In addition, |
| 85 | +it does not rely on the uniqueness of the end marker. It is most useful for |
| 86 | +operation like ``download``, where the length of the output can be known in |
| 87 | +advance. |
| 88 | +
|
| 89 | +As mentioned above, the classes :class:`VariableLengthResponseGenerator` and |
| 90 | +:class:`FixedLengthResponseGenerator` are abstract. The module |
| 91 | +:mod:`datalad_next.shell.response_generators` provides the following concrete |
| 92 | +implementations for them: |
| 93 | +
|
| 94 | + - :class:`VariableLengthResponseGeneratorPosix` |
| 95 | +
|
| 96 | + - :class:`VariableLengthResponseGeneratorPowerShell` |
| 97 | +
|
| 98 | + - :class:`FixedLengthResponseGeneratorPosix` |
| 99 | +
|
| 100 | + - :class:`FixedLengthResponseGeneratorPowerShell` |
| 101 | +
|
| 102 | +When :func:`shell` is executed it will use a |
| 103 | +:class:`VariableLengthResponseClass` to skip the login message of the shell. |
| 104 | +This is done by executing a *zero command* (a command that will possibly |
| 105 | +generate some output, and successfully return) in the shell. The zero command is |
| 106 | +provided by the concrete implementation of class |
| 107 | +:class:`VariableLengthResponseGenerator`. For example, the zero command for |
| 108 | +POSIX shells is ``test 0 -eq 0``, for PowerShell it is ``Write-Host hello``. |
| 109 | +
|
| 110 | +Because there is no way for func:`shell` to determine the kind of shell it |
| 111 | +connects to, the user can provide an alternative response generator class, in |
| 112 | +the ``zero_command_rg_class``-parameter. Instance of that class |
| 113 | +will then be used to execute the zero command. Currently, the following two |
| 114 | +response generator classes are available: |
| 115 | +
|
| 116 | + - :class:`VariableLengthResponseGeneratorPosix`: works with POSIX-compliant |
| 117 | + shells, e.g. ``sh`` or ``bash``. This is the default. |
| 118 | + - :class:`VariableLengthResponseGeneratorPowerShell`: works with PowerShell. |
| 119 | +
|
| 120 | +Whenever a command is executed via :meth:`ShellCommandExecutor.__call__`, the |
| 121 | +class identified by ``zero_command_rg_class`` will be used by default to create |
| 122 | +the final command list and to parse the result. Users can override this on a |
| 123 | +per-call basis by providing a different response generator class in the |
| 124 | +``response_generator``-parameter of :meth:`ShellCommandExecutor.__call__`. |
| 125 | +
|
| 126 | +.. currentmodule:: datalad_next.shell |
| 127 | +
|
| 128 | +.. autosummary:: |
| 129 | + :toctree: generated |
| 130 | + :recursive: |
| 131 | +
|
| 132 | + ShellCommandExecutor |
| 133 | + ShellCommandResponseGenerator |
| 134 | + VariableLengthResponseGenerator |
| 135 | + VariableLengthResponseGeneratorPosix |
| 136 | + VariableLengthResponseGeneratorPowerShell |
| 137 | + FixedLengthResponseGenerator |
| 138 | + FixedLengthResponseGeneratorPosix |
| 139 | + FixedLengthResponseGeneratorPowerShell |
| 140 | + DownloadResponseGenerator |
| 141 | + DownloadResponseGeneratorPosix |
| 142 | + operations.posix.upload |
| 143 | + operations.posix.download |
| 144 | + operations.posix.delete |
| 145 | +""" |
| 146 | + |
| 147 | + |
| 148 | +__all__ = [ |
| 149 | + 'shell', |
| 150 | + 'posix', |
| 151 | +] |
| 152 | + |
| 153 | +from .shell import ( |
| 154 | + shell, |
| 155 | + ShellCommandExecutor, |
| 156 | +) |
| 157 | + |
| 158 | +from .operations import posix |
| 159 | +from .operations.posix import ( |
| 160 | + DownloadResponseGenerator, |
| 161 | + DownloadResponseGeneratorPosix, |
| 162 | +) |
| 163 | +from .response_generators import ( |
| 164 | + FixedLengthResponseGenerator, |
| 165 | + FixedLengthResponseGeneratorPosix, |
| 166 | + FixedLengthResponseGeneratorPowerShell, |
| 167 | + ShellCommandResponseGenerator, |
| 168 | + VariableLengthResponseGenerator, |
| 169 | + VariableLengthResponseGeneratorPosix, |
| 170 | + VariableLengthResponseGeneratorPowerShell, |
| 171 | +) |
0 commit comments