Package: citizenshell
citizenshellcitizenshellis 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 withPyPy. 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 underMITlicense.Installationcitizenshellcan simply installed usingpip install citizenshellObtaining a shellFirst you need a shell. For that you have several options:use the built-inLocalShellfor quick access:fromcitizenshellimportshyou can instanciate your ownLocalShell:fromcitizenshellimportLocalShellshell=LocalShell()you can instanciate theTelnetShellfor shell over telnet:fromcitizenshellimportTelnetShellshell=TelnetShell(hostname="acme.org",username="john",password="secretpassword")you can instanciate theSecureShellfor shell over SSH:fromcitizenshellimportSecureShellshell=SecureShell(hostname="acme.org",username="john",password="secretpassword")you can instanciate theAdbShellfor shell over ADB:if ADB devices is reachable over TCP/IP:fromcitizenshellimportAdbShellshell=AdbShell(hostname="acme.org",port=5555)if ADB device is connected via USB:fromcitizenshellimportAdbShellshell=AdbShell(device="1c123a09dab45cbf")if there is only one ADB device connected via USB:fromcitizenshellimportAdbShellshell=AdbShell()you can instanciate theSerialShellfor shell over serial line:fromserialimportEIGHTBITS,PARITY_NONEfromcitizenshellimportSerialShellshell=SerialShell(port="/dev/ttyUSB3",username="john",password="secretpassword",baudrate=115200,parity=PARITY_NONE,bytesize=EIGHTBITS)you can also obtain shell objects by URI using theShellfunction:fromcitizenshellimportShelllocalshell=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 shellOnce you have shell, any shell, you can call it directly and get the standart output:assertshell("echo Hello World")=="Hello World"You can also iterate over the standard output:result=[int(x)forxinshell("""for i in 1 2 3 4; doecho $i;done""")]assertresult==[1,2,3,4]You don't have to wait for the command to finish to receive the output.This loopforlineinshell("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")assertresult.stdout()==["output"]assertresult.stderr()==["error"]assertresult.exit_code()==13You can inject environment variable to the shellassertshell("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:assertshell("exit 13").exit_code()==13# will not raise any exceptiontry:shell("exit 13",check_xc=True)# will raise an exceptionassertFalse,"will not be reached"exceptShellErrorase:assertTrue,"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 exceptiontry:shell("echo DANGER >&2",check_err=True)# will raise an exceptionassertFalse,"will not be reached"exceptShellErrorase:assertTrue,"will be reached"You can pull file from the remote host (forLocalShellit's just doing a copy):shell("echo -n test > remote_file.txt")shell.pull("local_file.txt","remote_file.txt")assertopen("local_file.txt","r").read()=="test"or push file to the remote host (again, forLocalShellit's just doing a copy):open("local_file.txt","w").write("test")shell.push("local_file.txt","remote_file.txt")assertstr(shell("cat remote_file.txt"))=="test"LogsEvery 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 tologging.CRITICALwhich does not log anything. However, this log level
can be configured either using thelog_level=keyword argument in the shell constructor:fromcitizenshellimportLocalShellfromloggingimportINFOshell=LocalShell(log_level=INFO)or by calling theset_log_level()method:fromcitizenshellimportshfromloggingimportINFOsh.set_log_level(INFO)When configured withlogging.INFO:all commands are logged on stdout prefixed by a$and colored in cyan withtermcolorall characters produced on stdout are logged to stdoutall characters produced on stderr are logged to stderr and colored in red withtermcolorall out of band messages are logged to stdout prefixed with>and colored in yello withtermcolorFor example:fromcitizenshellimportLocalShellfromloggingimportINFOshell=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.DEBUGcan be used.