citizenshell
citizenshell is a python library allowing to execute shell commands either locally or remotely over several protocols (telnet, ssh, serial or adb) using a simple and consistent API. This library is compatible with both python 2 (2.7) and 3 (>=3.4) as well as with PyPy. For now, it focuses on POSIX platforms like Linux and MacOS, but may be extended to work to Windows based platform in the future. It is distributed under MIT license.
Installation
citizenshell can simply installed using pip install citizenshell
Obtaining a shell
First you need a shell. For that you have several options:
-
use the built-in
LocalShell
for quick access:from citizenshell import sh
-
you can instanciate your own
LocalShell
:from citizenshell import LocalShell shell = LocalShell()
-
you can instanciate the
TelnetShell
for shell over telnet:from citizenshell import TelnetShell shell = TelnetShell(hostname="acme.org", username="john", password="secretpassword")
-
you can instanciate the
SecureShell
for shell over SSH:from citizenshell import SecureShell shell = SecureShell(hostname="acme.org", username="john", password="secretpassword")
-
you can instanciate the
AdbShell
for shell over ADB:-
if ADB devices is reachable over TCP/IP:
from citizenshell import AdbShell shell = AdbShell(hostname="acme.org", port=5555)
-
if ADB device is connected via USB:
from citizenshell import AdbShell shell = AdbShell(device="1c123a09dab45cbf")
-
if there is only one ADB device connected via USB:
from citizenshell import AdbShell shell = AdbShell()
-
-
you can instanciate the
SerialShell
for shell over serial line:from serial import EIGHTBITS, PARITY_NONE from citizenshell import SerialShell shell = SerialShell(port="/dev/ttyUSB3", username="john", password="secretpassword", baudrate=115200, parity=PARITY_NONE, bytesize=EIGHTBITS)
-
you can also obtain shell objects by URI using the
Shell
function:from citizenshell import Shell localshell = Shell() telnetshell = Shell("telnet://john:secretpassword@acme.org:1234") secureshell = Shell("ssh://john:secretpassword@acme.org:1234") adbshell = Shell("adb://myandroiddevice:5555") adbtcpshell = Shell("adb+tcp://myandroiddevice:5555") adbtcpshell = Shell("adb+usb://1c123a09dab45cbf") serialshell = Shell("serial://jogn:secretpassword@/dev/ttyUSB3?baudrate=115200")
you can also mix and match betweens providing arguments in the URI or via kwargs:
telnetshell = Shell("telnet://john@acme.org", password="secretpassword", port=1234) serialshell = Shell("serial://john:secretpassword@/dev/ttyUSB3", baudrate=115200)
Using a shell
Once you have shell, any shell, you can call it directly and get the standart output:
assert shell("echo Hello World") == "Hello World"
You can also iterate over the standard output:
result = [int(x) for x in shell("""
for i in 1 2 3 4; do
echo $i;
done
""")]
assert result == [1, 2, 3, 4]
You don’t have to wait for the command to finish to receive the output.
This loop
for line in shell("for i in 1 2 3 4; do echo -n 'It is '; date +%H:%M:%S; sleep 1; done", wait=False):
print ">>>", line + "!"
would produce something like:
>>> It is 14:24:52!
>>> It is 14:24:53!
>>> It is 14:24:54!
>>> It is 14:24:55!
You can extract stdout, stderr and exit code seperately:
result = shell(">&2 echo error && echo output && exit 13")
assert result.stdout() == ["output"]
assert result.stderr() == ["error"]
assert result.exit_code() == 13
You can inject environment variable to the shell
assert shell("echo $VAR", VAR="bar") == "bar"
By default, shell inherits “$CWD” from the environment (aka $PWD).
Still, if ever a command needs to be run from a custom path, one way to achieve this is:
shell = LocalShell()
os.chdir(first_custom_path)
shell('first_command')
os.chdir(second_custom_path)
shell('second_command')
This works … but it is ugly! Two levels of abstraction are mixed.
This is better:
shell = LocalShell()
shell('first_command', cwd=first_custom_path)
shell('second_command', cwd=second_custom_path)
The shell can raise an exception if the exit code is non-zero:
assert shell("exit 13").exit_code() == 13 # will not raise any exception
try:
shell("exit 13", check_xc=True) # will raise an exception
assert False, "will not be reached"
except ShellError as e:
assert True, "will be reached"
The shell can also raise an exception if something is printed on the standard error:
shell("echo DANGER >&2").stderr() == ["DANGER"] # will not raise any exception
try:
shell("echo DANGER >&2", check_err=True) # will raise an exception
assert False, "will not be reached"
except ShellError as e:
assert True, "will be reached"
You can pull file from the remote host (for LocalShell
it’s just doing a copy):
shell("echo -n test > remote_file.txt")
shell.pull("local_file.txt", "remote_file.txt")
assert open("local_file.txt", "r").read() == "test"
or push file to the remote host (again, for LocalShell
it’s just doing a copy):
open("local_file.txt", "w").write("test")
shell.push("local_file.txt", "remote_file.txt")
assert str(shell("cat remote_file.txt")) == "test"
Logs
Every shell object has a set of loggers: stdin, stderr and stdout, as well as for out of band logging message.
By default they are all set to logging.CRITICAL
which does not log anything. However, this log level
can be configured either using the log_level=
keyword argument in the shell constructor:
from citizenshell import LocalShell
from logging import INFO
shell = LocalShell(log_level=INFO)
or by calling the set_log_level()
method:
from citizenshell import sh
from logging import INFO
sh.set_log_level(INFO)
When configured with logging.INFO
:
- all commands are logged on stdout prefixed by a
$
and colored in cyan withtermcolor
- all characters produced on stdout are logged to stdout
- all characters produced on stderr are logged to stderr and colored in red with
termcolor
- all out of band messages are logged to stdout prefixed with
>
and colored in yello withtermcolor
For example:
from citizenshell import LocalShell
from logging import INFO
shell = LocalShell(log_level=INFO)
shell(">&2 echo error && echo output && exit 13")
shell("echo Hello > /tmp/from.txt")
shell.push("/tmp/from.txt", "/tmp/to.txt")
will produce the following logs (colors are omitted):
$ >&2 echo error && echo output && exit 13
output
error
$ echo Hello > /tmp/from.txt
> '/tmp/from.txt' -> '/tmp/to.txt'
$ command -v chmod
/bin/chmod
$ chmod 664 '/tmp/to.txt'
For more even more logs messages, logging.DEBUG
can be used.