shell.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. """Common Shell Utilities."""
  2. import os
  3. import sys
  4. from subprocess import Popen, PIPE
  5. from multiprocessing import Process
  6. from threading import Thread
  7. from ..core.meta import MetaMixin
  8. from ..core.exc import FrameworkError
  9. def exec_cmd(cmd_args, *args, **kw):
  10. """
  11. Execute a shell call using Subprocess. All additional `*args` and
  12. `**kwargs` are passed directly to subprocess.Popen. See `Subprocess
  13. <http://docs.python.org/library/subprocess.html>`_ for more information
  14. on the features of `Popen()`.
  15. :param cmd_args: List of command line arguments.
  16. :type cmd_args: list.
  17. :param args: Additional arguments are passed to Popen().
  18. :param kwargs: Additional keyword arguments are passed to Popen().
  19. :returns: The (stdout, stderror, return_code) of the command.
  20. :rtype: tuple
  21. Usage:
  22. .. code-block:: python
  23. from cement.utils import shell
  24. stdout, stderr, exitcode = shell.exec_cmd(['echo', 'helloworld'])
  25. """
  26. if 'stdout' not in kw.keys():
  27. kw['stdout'] = PIPE
  28. if 'stderr' not in kw.keys():
  29. kw['stderr'] = PIPE
  30. proc = Popen(cmd_args, *args, **kw)
  31. (stdout, stderr) = proc.communicate()
  32. proc.wait()
  33. return (stdout, stderr, proc.returncode)
  34. def exec_cmd2(cmd_args, *args, **kw):
  35. """
  36. Similar to exec_cmd, however does not capture stdout, stderr (therefore
  37. allowing it to print to console). All additional `*args` and
  38. `**kwargs` are passed directly to subprocess.Popen. See `Subprocess
  39. <http://docs.python.org/library/subprocess.html>`_ for more information
  40. on the features of `Popen()`.
  41. :param cmd_args: List of command line arguments.
  42. :type cmd_args: list.
  43. :param args: Additional arguments are passed to Popen().
  44. :param kwargs: Additional keyword arguments are passed to Popen().
  45. :returns: The integer return code of the command.
  46. :rtype: int
  47. Usage:
  48. .. code-block:: python
  49. from cement.utils import shell
  50. exitcode = shell.exec_cmd2(['echo', 'helloworld'])
  51. """
  52. proc = Popen(cmd_args, *args, **kw)
  53. proc.wait()
  54. return proc.returncode
  55. def spawn_process(target, start=True, join=False, *args, **kwargs):
  56. """
  57. A quick wrapper around multiprocessing.Process(). By default the start()
  58. function will be called before the spawned process object is returned.
  59. See `MultiProcessing
  60. <https://docs.python.org/2/library/multiprocessing.html>`_ for more
  61. information on the features of `Process()`.
  62. :param target: The target function to execute in the sub-process.
  63. :param start: Call start() on the process before returning the process
  64. object.
  65. :param join: Call join() on the process before returning the process
  66. object. Only called if start=True.
  67. :param args: Additional arguments are passed to Process().
  68. :param kwargs: Additional keyword arguments are passed to Process().
  69. :returns: The process object returned by Process().
  70. Usage:
  71. .. code-block:: python
  72. from cement.utils import shell
  73. def add(a, b):
  74. print(a + b)
  75. p = shell.spawn_process(add, args=(12, 27))
  76. p.join()
  77. """
  78. proc = Process(target=target, *args, **kwargs)
  79. if start and not join:
  80. proc.start()
  81. elif start and join:
  82. proc.start()
  83. proc.join()
  84. return proc
  85. def spawn_thread(target, start=True, join=False, *args, **kwargs):
  86. """
  87. A quick wrapper around threading.Thread(). By default the start()
  88. function will be called before the spawned thread object is returned
  89. See `Threading
  90. <https://docs.python.org/2/library/threading.html>`_ for more
  91. information on the features of `Thread()`.
  92. :param target: The target function to execute in the thread.
  93. :param start: Call start() on the thread before returning the thread
  94. object.
  95. :param join: Call join() on the thread before returning the thread
  96. object. Only called if start=True.
  97. :param args: Additional arguments are passed to Thread().
  98. :param kwargs: Additional keyword arguments are passed to Thread().
  99. :returns: The thread object returned by Thread().
  100. Usage:
  101. .. code-block:: python
  102. from cement.utils import shell
  103. def add(a, b):
  104. print(a + b)
  105. t = shell.spawn_thread(add, args=(12, 27))
  106. t.join()
  107. """
  108. thr = Thread(target=target, *args, **kwargs)
  109. if start and not join:
  110. thr.start()
  111. elif start and join:
  112. thr.start()
  113. thr.join()
  114. return thr
  115. class Prompt(MetaMixin):
  116. """
  117. A wrapper around `raw_input` or `input` (py3) whose purpose is to limit
  118. the redundent tasks of gather usr input. Can be used in several ways
  119. depending on the use case (simple input, options, and numbered
  120. selection).
  121. :param text: The text displayed at the input prompt.
  122. Usage:
  123. Simple prompt to halt operations and wait for user to hit enter:
  124. .. code-block:: python
  125. p = shell.Prompt("Press Enter To Continue", default='ENTER')
  126. .. code-block:: text
  127. $ python myapp.py
  128. Press Enter To Continue
  129. $
  130. Provide a numbered list for longer selections:
  131. .. code-block:: python
  132. p = Prompt("Where do you live?",
  133. options=[
  134. 'San Antonio, TX',
  135. 'Austin, TX',
  136. 'Dallas, TX',
  137. 'Houston, TX',
  138. ],
  139. numbered = True,
  140. )
  141. .. code-block:: text
  142. Where do you live?
  143. 1: San Antonio, TX
  144. 2: Austin, TX
  145. 3: Dallas, TX
  146. 4: Houston, TX
  147. Enter the number for your selection:
  148. Create a more complex prompt, and process the input from the user:
  149. .. code-block:: python
  150. class MyPrompt(Prompt):
  151. class Meta:
  152. text = "Do you agree to the terms?"
  153. options = ['Yes', 'no', 'maybe-so']
  154. options_separator = '|'
  155. default = 'no'
  156. clear = True
  157. max_attempts = 99
  158. def process_input(self):
  159. if self.input.lower() == 'yes':
  160. # do something crazy
  161. pass
  162. else:
  163. # don't do anything... maybe exit?
  164. print("User doesn't agree! I'm outa here")
  165. sys.exit(1)
  166. MyPrompt()
  167. .. code-block:: text
  168. $ python myapp.py
  169. [TERMINAL CLEAR]
  170. Do you agree to the terms? [Yes|no|maybe-so] no
  171. User doesn't agree! I'm outa here
  172. $ echo $?
  173. $ 1
  174. """
  175. class Meta:
  176. """
  177. Optional meta-data (can also be passed as keyword arguments to the
  178. parent class).
  179. """
  180. # The text that is displayed to prompt the user
  181. text = "Tell me someting interesting:"
  182. #: A default value to use if the user doesn't provide any input
  183. default = None
  184. #: Options to provide to the user. If set, the input must match one
  185. #: of the items in the options selection.
  186. options = None
  187. #: Separator to use within the option selection (non-numbered)
  188. options_separator = ','
  189. #: Display options in a numbered list, where the user can enter a
  190. #: number. Useful for long selections.
  191. numbered = False
  192. #: The text to display along with the numbered selection for user
  193. #: input.
  194. selection_text = "Enter the number for your selection:"
  195. #: Whether or not to automatically prompt() the user once the class
  196. #: is instantiated.
  197. auto = True
  198. #: Whether to treat user input as case insensitive (only used to
  199. #: compare user input with available options).
  200. case_insensitive = True
  201. #: Whether or not to clear the terminal when prompting the user.
  202. clear = False
  203. #: Command to issue when clearing the terminal.
  204. clear_command = 'clear'
  205. #: Max attempts to get proper input from the user before giving up.
  206. max_attempts = 10
  207. #: Raise an exception when max_attempts is hit? If not, Prompt
  208. #: passes the input through as `None`.
  209. max_attempts_exception = True
  210. def __init__(self, text=None, *args, **kw):
  211. if text is not None:
  212. kw['text'] = text
  213. super(Prompt, self).__init__(*args, **kw)
  214. self.input = None
  215. if self._meta.auto:
  216. self.prompt()
  217. def _prompt(self):
  218. if self._meta.clear:
  219. os.system(self._meta.clear_command)
  220. text = ""
  221. if self._meta.options is not None:
  222. if self._meta.numbered is True:
  223. text = text + self._meta.text + "\n\n"
  224. count = 1
  225. for option in self._meta.options:
  226. text = text + "%s: %s\n" % (count, option)
  227. count += 1
  228. text = text + "\n"
  229. text = text + self._meta.selection_text
  230. else:
  231. sep = self._meta.options_separator
  232. text = "%s [%s]" % (self._meta.text,
  233. sep.join(self._meta.options))
  234. else:
  235. text = self._meta.text
  236. if sys.version_info[0] < 3: # pragma: nocover
  237. self.input = raw_input("%s " % text) # pragma: nocover
  238. else: # pragma: nocover
  239. self.input = input("%s " % text) # pragma: nocover
  240. if self.input == '' and self._meta.default is not None:
  241. self.input = self._meta.default
  242. elif self.input == '':
  243. self.input = None
  244. def prompt(self):
  245. """
  246. Prompt the user, and store their input as `self.input`.
  247. """
  248. attempt = 0
  249. while self.input is None:
  250. if attempt >= int(self._meta.max_attempts):
  251. if self._meta.max_attempts_exception is True:
  252. raise FrameworkError("Maximum attempts exceeded getting "
  253. "valid user input")
  254. else:
  255. return self.input
  256. attempt += 1
  257. self._prompt()
  258. if self.input is None:
  259. continue
  260. elif self._meta.options is not None:
  261. if self._meta.numbered:
  262. try:
  263. self.input = self._meta.options[int(self.input) - 1]
  264. except (IndexError, ValueError) as e:
  265. self.input = None
  266. continue
  267. else:
  268. if self._meta.case_insensitive is True:
  269. lower_options = [x.lower()
  270. for x in self._meta.options]
  271. if not self.input.lower() in lower_options:
  272. self.input = None
  273. continue
  274. else:
  275. if self.input not in self._meta.options:
  276. self.input = None
  277. continue
  278. self.process_input()
  279. return self.input
  280. def process_input(self):
  281. """
  282. Does not do anything. Is intended to be used in a sub-class to handle
  283. user input after it is prompted.
  284. """
  285. pass