#!/usr/bin/env python

""" The Commands Plus module

Usage instructions:

If you have used commands.getstatusoutput before, you probably know
it's a matter of impossibility to tell standard output from standard error
output in the return value of the function.  This module provides a
getstatusoutputerror function which returns the exit status, standard
output and standard error as separate items, therefore eliminating.
the known problem with getstatusoutput.

> import commandsplus

> print commandsplus.getstatusoutputerror("ls -lr")
(Exited instance,"something...","")

getstatusoutputerror returns three values:
	- an exit condition instance
	- the standard output
	- the standard error

The exit condition instance can be tested in the following way:
	status,stdout,stderr=getstatusoutput("somecommand")
	if status.command_not_found:
		print "Command not found"
	if status.core_dumped:
		print "Command core dumped (segfaulted")
	if status.killed:
		print "Command was killed with signal",status.signal
	if status.exited:
		if status.return_value == 0:
			print "Command exited successfully"
		else:
			print "Command exited with status",status.return_value
"""

def shell_escape(string):
	"""Return an escaped string suitable for passing as a parameter
	in a BASH shell command line.
	
	Say you have a file named "A*Teens - Mamma mia.mp3", with
	an actual asterisk in the name.  This should work properly:
	
	from commandsplus import shell_escape,getstatusoutputerror
	filename = "A*Teens - Mamma mia.mp3"
	print getstatusoutputerror("rm -f %s"%shell_escape(filename))
	"""
	unallowed_chars = "\$`\\\""
	deststring = ""
	
	for char in string:
		if char in unallowed_chars: newchar = "\\"+char
		else: newchar = char
		deststring = deststring + newchar
	return '"' + deststring + '"'
		

class ExitCondition:
	"""The base exit condition class"""
	pid = None
	def __init__(self,pid):
		self.pid = pid
	killed = False
	core_dumped = False
	exited = False
	command_not_found = False
		
class CoreDumped(ExitCondition):
	"""Exit condition representing a killed process due to coredump"""
	core_dumped = True
	def __init__(self,pid):
		ExitCondition.__init__(self,pid)
	def __str__(self):
		return "process %s dumped core"%self.pid
	def __repr__(self):
		return "CoreDumped(%s)"%(self.pid)
			
class Killed(ExitCondition):
	"""Exit condition representing a killed process with a signal"""
	signal = None
	killed = True
	def __init__(self,pid,signal):
		ExitCondition.__init__(self,pid)
		self.signal = signal
	def __str__(self):
		return "process %s was killed by signal %s"%(self.pid,self.signal)
	def __repr__(self):
		return "Killed(%s,%s)"%(self.pid,self.signal)

class Exited(ExitCondition):
	"""Exit condition representing a normal exit with a return value"""
	return_value = None
	exited = True
	def __init__(self,pid,return_value):
		ExitCondition.__init__(self,pid)
		self.return_value = return_value
	def __str__(self):
		return "process %s exited with status %s"%(self.pid,self.return_value)
	def __repr__(self):
		return "Exited(%s,%s)"%(self.pid,self.return_value)

class CommandNotFound(ExitCondition):
	"""Exit condition representing the command was not found"""
	command_not_found = True
	def __init__(self,pid):
		ExitCondition.__init__(self,pid)
	def __str__(self):
		return "command for process %s was not found"%(self.pid,)
	def __repr__(self):
		return "CommandNotFound(%s)"%(self.pid)

import os
def gen_exit_condition(pid,raw_status):
	if os.WCOREDUMP(raw_status): return CoreDumped(pid)
	if os.WIFSIGNALED(raw_status): return Killed(pid,os.WTERMSIG(raw_status))
	if os.WIFEXITED(raw_status):
		if os.WEXITSTATUS(raw_status) != 127:
			return Exited(pid,os.WEXITSTATUS(raw_status))
		else:
			return CommandNotFound(pid)

def getstatusoutputerror(cmd):
	"""See the getstatusoutput documentation in the
	commands module for a more through explanation
	of what this does.  In the meantime, think of this
	function as a getstatusoutput with the ability to
	return a separate standard error output.

	Note that standard output and standard error will
	have a final carriage return, if the invoked program
	emitted a final carriage return, which is usually the
	case with command line programs.  You will have to
	use the strip() method in the returned string if
	you want the behavior of bash backticks `ls -l` style
	
	See the ExitCondition and descendant classes documentation
	for more information on how to interpret return status.
	"""

	import popen2
	from threading import Thread,currentThread
	object = popen2.Popen3(cmd,capturestderr=True)
	pid = object.pid
	def getall(fileobject):
		currentThread().result = "".join(fileobject.readlines())
	stdout_t = Thread(target=getall,args=(object.fromchild,))
	stderr_t = Thread(target=getall,args=(object.childerr,))
	stdout_t.start()
	stderr_t.start()
	status = object.wait()
	stdout_t.join()
	stderr_t.join()
	return gen_exit_condition(pid,status),stdout_t.result,stderr_t.result
	
__all__ = [ "shell_escape", "getstatusoutputerror","CoreDumped","Killed","Exited" ]

