.. py:currentmodule:: psh
psh |version|
=============
psh allows you to spawn processes in Unix shell-style way. It is inspired by
`sh `_ module but has a fully different
implementation and API.
Unix shell is very convenient for spawning processes, connecting them into
pipes, etc., but it has a very limited language which is often not suitable for
writing complex programs. Python is a very flexible and reach language which is
used in a wide variety of application domains, but its standard
:py:mod:`subprocess` module is very limited. psh combines the power of Python
language and an elegant shell-style way to execute processes.
Follow it on GitHub: `https://github.com/KonishchevDmitry/psh
`_.
Examples
--------
Print output of ``echo -n "text"``::
from psh import sh
print sh.echo("-n", "text").execute().stdout()
Get a list of all available network interfaces (``ifconfig | egrep -o "^[^[:space:]:]+"``)::
from psh import sh
interfaces = [ iface.rstrip("\n") for iface in sh.ifconfig() | sh.egrep("-o", "^[^[:space:]:]+") ]
Check free disk space on remote host *myserver.com*::
import re
from psh import sh
# ssh myserver.com 'df | egrep "^/dev/"'
with sh.ssh("myserver.com", sh.df() | sh.egrep("^/dev/"), _shell = True) as ssh:
for line in ssh:
match = re.search(r"^(/dev/[^\s]+)\s+(?:[^\s]+\s+){3}(\d+)%\s+(/.*)$", line.rstrip("\n"))
device, used, mount_point = match.groups()
if int(used) > 80:
print "{0} ({1}) ran out of disk space ({2}%)".format(device, mount_point, used)
Output::
/dev/sda1 (/) ran out of disk space (86%)
/dev/sda2 (/mnt/data) ran out of disk space (95%)
Installation
------------
You can install the latest psh version using `pip `_::
pip install psh
or using `easy_install `_::
easy_install psh
or you can install the latest development version by issuing the following
commands::
git clone https://github.com/KonishchevDmitry/psh.git && cd psh
python setup.py build
sudo python setup.py install
Python versions
---------------
Python 2.6+ and 3.0+ are supported.
Tutorial
========
.. _command-execution:
Command execution
-----------------
:py:mod:`psh` module has an object named :py:data:`sh` which is a factory of
:py:class:`Program` objects. A :py:class:`Program` object represents a program
which can be executed. To obtain a :py:class:`Program` object just write::
from psh import sh
echo = sh.echo
For programs that have dashes in their names, for example ``google-chrome``,
substitute the dash with an underscore::
from psh import sh
sh.google_chrome("http://google.com")
.. note::
For programs with more exotic characters in their names, like ``.`` or
``_`` you may use :py:meth:`sh.__call__` method::
from psh import sh
python = sh("python2.7")
script = sh("/path/to/script.sh")
To execute a program just call it as if it is a function and then call
:py:meth:`~Process.execute` method::
from psh import sh
sh.echo("text").execute()
sh("python2.7")("script.py").execute()
``sh.echo("text")`` returns a :py:class:`Process` instance which holds all
arguments and state of the process which will be executed.
A process is not executed automatically by default when a :py:class:`Process`
object is created. This is done so to support :ref:`piping` and
:ref:`output-iteration`. But if you want just simply run commands, you may use
``_defer = False`` option::
from psh import sh
sh.service("httpd", "start", _defer = False)
In this case ``service httpd start`` will be executed immediately and
``sh.service(...)`` call will return only when the process will be terminated.
If you want to always run processes immediately, you may set ``_defer = False``
as default (see :ref:`default-options`).
Keyword arguments
-----------------
Commands support short-form (``-a``) and long-form (``--arg``) arguments as
keyword arguments::
sh.useradd("ftp", m = True, system = True, shell = "/usr/sbin/nologin")
which is equal to::
sh.useradd("-m", "--system", "--shell", "/usr/sbin/nologin", "ftp")
where both resolve to::
useradd -m --system --shell /usr/sbin/nologin ftp
.. _piping:
Piping
------
Shell-style piping is performed using :py:class:`Process` object composition.
Just pass one command as the input to another, and psh will create a pipe
between the two::
process = sh.du() | sh.sort("-nr") | sh.head("-n", 3)
process.execute()
process.stdout()
In this case ``process.stdout()`` will return output of ``du | sort -nr | head -n 3``.
.. note::
You can't execute a pipe as in the following example because of Python's
evaluation order::
sh.du() | sh.sort("-nr") | sh.head("-n", 3).execute()
You may do this by storing a pipe in a variable::
process = sh.du() | sh.sort("-nr") | sh.head("-n", 3)
process.execute()
or just::
( sh.du() | sh.sort("-nr") | sh.head("-n", 3) ).execute()
.. _io-redirection:
I/O redirection
---------------
psh can redirect the standard input, output and error streams::
# echo text > /dev/null 2>&1
sh.echo("text", _stdout = psh.DEVNULL, _stderr = psh.STDOUT)
# echo -n "text" | cat
sh.cat(_stdin = "text")
# cat < file
sh.cat(_stdin = psh.File("file"))
or even use Python's generators as input::
# Output: "0\n1\n2\n3\n4\n"
sh.cat(_stdin = ( str(i) + "\n" for i in xrange(0, 5) ))
.. _exit-codes:
Exit codes
----------
Normal processes exit with exit code 0. Process' exit code can be obtained
through :py:meth:`~Process.status()`::
assert sh.true().execute().status() == 0
If a process terminates with a nonzero exit code, an exception is raised.
Some programs return nonzero exit codes even though they succeed. If you know
which codes a program might returns and you don't want to deal with doing no-op
exception handling, you can use the ``_ok_statuses`` option::
sh.mount() | sh.egrep("^/dev/", _ok_statuses = [ 0, 1 ]) | sh.sort()
This means that the ``grep`` command will not generate an exception if the
process exit with 0 or 1 exit code.
.. note::
Please notice that if you connect a few processes in a pipe, an exception
will be raised even if a failed command is not the last command in the
pipe. This gives you a great power of controlling process execution in a
very easy way which is not available in the shell.
.. _default-options:
Setting default process options
-------------------------------
As you saw above, you can control process execution via options passed to the
:py:class:`Process` instance, such as ``_defer = False``. But sometimes you may
realize that the default option values is not very suitable for you and you
override them almost in every command.
For example, you want all commands to be executed immediately saving their
original input and output file descriptors. You can do this by overriding the
default option values for the specific command::
from psh import Program, STDIN, STDOUT, STDERR
ssh = Program("ssh", "user@host", _stdin = STDIN, _stdout = STDOUT, _stderr = STDERR, _defer = False)
# Immediatly executes `ssh user@host df -h` preserving the original
# standart file descriptors.
ssh("df", "-h")
or you can override them for all commands you execute::
from psh import Sh, STDIN, STDOUT, STDERR
sh = Sh(_stdin = STDIN, _stdout = STDOUT, _stderr = STDERR, _defer = False)
# Immediatly executes `ssh user@host df -h` preserving the original
# standart file descriptors.
sh.ssh("user@host", "df", "-h")
'With' contexts
---------------
You can use ``with`` statement on :py:class:`Process` objects to guarantee that
the process will be :py:meth:`~Process.wait()`'ed when you leave the ``with`` context, which also
frees all opened file descriptors and other resources (see :py:class:`Process`
reference for details).
Using ``with`` context with :py:class:`Process` objects is the same as with all
other Python's objects::
from psh import sh
with sh.mount() as process:
process.execute(wait = False)
# do some task here
# process will be terminated here
.. _output-iteration:
Iterating over output
---------------------
You can iterate over process output as well you do for all Python's file
objects::
from psh import sh
with sh.cat("/var/log/messages") as cat:
for line in cat:
print line,
The process is automatically executed when iteration is initiated.
.. note::
You should always iterate over process output inside a ``with`` context
(see :py:class:`Process` reference for description why).
.. _working-with-ssh:
Working with SSH
----------------
When you need to run a specific command on a remote host you have to run
``ssh`` and pass commands to it as arguments which breaks the all idea of
creating and piping processes with psh. For this reason psh gives you a way to
run processes on a remote host in the same way you use for the local host. The
only thing you have to do is to run a remote shell process (``ssh``, ``pdsh``,
etc.) with ``_shell = True`` option and pass a :py:class:`Process` object as an
argument to it::
import re
from psh import sh
# ssh myserver.com 'df | egrep "^/dev/"'
with sh.ssh("myserver.com", sh.df() | sh.egrep("^/dev/"), _shell = True) as ssh:
for line in ssh:
match = re.search(r"^(/dev/[^\s+]+)\s+(?:[^\s]+\s+){3}(\d+)%\s+(/.*)$", line.rstrip("\n"))
device, used, mount_point = match.groups()
if int(used) > 80:
print "{0} ({1}) ran out of disk space ({2}%)".format(device, mount_point, used)
When ``_shell = True`` option is passed, all :py:class:`Process` instances that
you specified as arguments will be converted to a shell script, which is equal
to the passed command, and ``ssh`` will execute it on the remote side.
For simple commands a generated script will be quite expectable. For example,
``sh.ssh("host", sh.echo("text", _stderr = psh.STDOUT), _shell = True)``
executes ``ssh host 'echo text 2>&1'``, but for piped commands the script will
be more complex. For example, the
``sh.ssh("myserver.com", sh.df() | sh.egrep("^/dev/"), _shell = True)``
executes something like
``bash -c 'df | egrep '"'"'^/dev/'"'"'; statuses=(${PIPESTATUS[@]}); case ${statuses[0]} in 0);; *) exit 128;; esac; exit ${statuses[1]};'``
on *myserver.com* host. This complexity is required to detect errors in
processes in the middle of the pipe.
.. note::
At this time ``_shell = True`` supports only basic I/O redirections such as
``>&2``, ``< file``, ``2>&1``, etc (see :ref:`io-redirection`). Using other
redirections causes an exception to be raised.
.. note::
Please note that there is a little difference in executing ::
sh.echo("data") | sh.grep("text") | sh.wc("-l")
and ::
ssh("host", sh.echo("data") | sh.grep("text") | sh.wc("-l"), _shell = True)
Both commands will raise :py:class:`ExecutionError`, but for the first one
:py:meth:`ExecutionError.status()` will return 1 from the failed ``grep``
command and for the second one :py:meth:`ExecutionError.status()` will
return 128.
This is because there is no way to pass a pair "failed command, return
status code" from within ssh without making the generated script
ridiculously complex. So all error codes of all processes in the pipe
except the last one is converted to 128.
More info
---------
Please read the :ref:`reference` which explains some important details,
thread-safety guaranties and additional features.