shell_helpers.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env python3
  2. import distutils.file_util
  3. import itertools
  4. import os
  5. import shlex
  6. import shutil
  7. import signal
  8. import stat
  9. import subprocess
  10. import sys
  11. class LF:
  12. '''
  13. LineFeed (AKA newline).
  14. Singleton class. Can be used in print_cmd to print out nicer command lines
  15. with --key on the same line as "--key value".
  16. '''
  17. pass
  18. class ShellHelpers:
  19. '''
  20. Helpers to do things which are easy from the shell,
  21. usually filesystem, process or pipe operations.
  22. Attempt to print shell equivalents of all commands to make things
  23. easy to debug and understand what is going on.
  24. '''
  25. def __init__(self, dry_run=False):
  26. '''
  27. :param dry_run: don't run the commands, just potentially print them. Debug aid.
  28. :type dry_run: Bool
  29. '''
  30. self.dry_run = dry_run
  31. def add_newlines(self, cmd):
  32. out = []
  33. for arg in cmd:
  34. out.extend([arg, LF])
  35. return out
  36. def cp(self, src, dest, **kwargs):
  37. self.print_cmd(['cp', src, dest])
  38. if not self.dry_run:
  39. shutil.copy2(src, dest)
  40. def cmd_to_string(self, cmd, cwd=None, extra_env=None, extra_paths=None):
  41. '''
  42. Format a command given as a list of strings so that it can
  43. be viewed nicely and executed by bash directly and print it to stdout.
  44. '''
  45. last_newline = ' \\\n'
  46. newline_separator = last_newline + ' '
  47. out = []
  48. if extra_env is None:
  49. extra_env = {}
  50. if cwd is not None:
  51. out.append('cd {} &&'.format(shlex.quote(cwd)))
  52. if extra_paths is not None:
  53. out.append('PATH="{}:${{PATH}}"'.format(':'.join(extra_paths)))
  54. for key in extra_env:
  55. out.append('{}={}'.format(shlex.quote(key), shlex.quote(extra_env[key])))
  56. cmd_quote = []
  57. newline_count = 0
  58. for arg in cmd:
  59. if arg == LF:
  60. cmd_quote.append(arg)
  61. newline_count += 1
  62. else:
  63. cmd_quote.append(shlex.quote(arg))
  64. if newline_count > 0:
  65. cmd_quote = [' '.join(list(y)) for x, y in itertools.groupby(cmd_quote, lambda z: z == LF) if not x]
  66. out.extend(cmd_quote)
  67. if newline_count == 1 and cmd[-1] == LF:
  68. ending = ''
  69. else:
  70. ending = last_newline + ';'
  71. return newline_separator.join(out) + ending
  72. def copy_dir_if_update_non_recursive(self, srcdir, destdir, filter_ext=None):
  73. # TODO print rsync equivalent.
  74. os.makedirs(destdir, exist_ok=True)
  75. for basename in os.listdir(srcdir):
  76. src = os.path.join(srcdir, basename)
  77. if os.path.isfile(src):
  78. noext, ext = os.path.splitext(basename)
  79. if filter_ext is not None and ext == filter_ext:
  80. distutils.file_util.copy_file(
  81. src,
  82. os.path.join(destdir, basename),
  83. update=1,
  84. )
  85. def print_cmd(self, cmd, cwd=None, cmd_file=None, extra_env=None, extra_paths=None):
  86. '''
  87. Print cmd_to_string to stdout.
  88. Optionally save the command to cmd_file file, and add extra_env
  89. environment variables to the command generated.
  90. If cmd contains at least one LF, newlines are only added on LF.
  91. Otherwise, newlines are added automatically after every word.
  92. '''
  93. if type(cmd) is str:
  94. cmd_string = cmd
  95. else:
  96. cmd_string = self.cmd_to_string(cmd, cwd=cwd, extra_env=extra_env, extra_paths=extra_paths)
  97. print('+ ' + cmd_string)
  98. if cmd_file is not None:
  99. with open(cmd_file, 'w') as f:
  100. f.write('#!/usr/bin/env bash\n')
  101. f.write(cmd_string)
  102. st = os.stat(cmd_file)
  103. os.chmod(cmd_file, st.st_mode | stat.S_IXUSR)
  104. def run_cmd(
  105. self,
  106. cmd,
  107. cmd_file=None,
  108. out_file=None,
  109. show_stdout=True,
  110. show_cmd=True,
  111. extra_env=None,
  112. extra_paths=None,
  113. delete_env=None,
  114. raise_on_failure=True,
  115. **kwargs
  116. ):
  117. '''
  118. Run a command. Write the command to stdout before running it.
  119. Wait until the command finishes execution.
  120. :param cmd: command to run. LF entries are magic get skipped.
  121. :type cmd: List[str]
  122. :param cmd_file: if not None, write the command to be run to that file
  123. :type cmd_file: str
  124. :param out_file: if not None, write the stdout and stderr of the command the file
  125. :type out_file: str
  126. :param show_stdout: wether to show stdout and stderr on the terminal or not
  127. :type show_stdout: bool
  128. :param extra_env: extra environment variables to add when running the command
  129. :type extra_env: Dict[str,str]
  130. '''
  131. if out_file is not None:
  132. stdout = subprocess.PIPE
  133. stderr = subprocess.STDOUT
  134. else:
  135. if show_stdout:
  136. stdout = None
  137. stderr = None
  138. else:
  139. stdout = subprocess.DEVNULL
  140. stderr = subprocess.DEVNULL
  141. if extra_env is None:
  142. extra_env = {}
  143. if delete_env is None:
  144. delete_env = []
  145. if 'cwd' in kwargs:
  146. cwd = kwargs['cwd']
  147. else:
  148. cwd = None
  149. env = os.environ.copy()
  150. env.update(extra_env)
  151. if extra_paths is not None:
  152. path = ':'.join(extra_paths)
  153. if 'PATH' in os.environ:
  154. path += ':' + os.environ['PATH']
  155. env['PATH'] = path
  156. for key in delete_env:
  157. if key in env:
  158. del env[key]
  159. if show_cmd:
  160. self.print_cmd(cmd, cwd=cwd, cmd_file=cmd_file, extra_env=extra_env, extra_paths=extra_paths)
  161. # Otherwise Ctrl + C gives:
  162. # - ugly Python stack trace for gem5 (QEMU takes over terminal and is fine).
  163. # - kills Python, and that then kills GDB: https://stackoverflow.com/questions/19807134/does-python-always-raise-an-exception-if-you-do-ctrlc-when-a-subprocess-is-exec
  164. sigint_old = signal.getsignal(signal.SIGINT)
  165. signal.signal(signal.SIGINT, signal.SIG_IGN)
  166. # Otherwise BrokenPipeError when piping through | grep
  167. # But if I do this_module, my terminal gets broken at the end. Why, why, why.
  168. # https://stackoverflow.com/questions/14207708/ioerror-errno-32-broken-pipe-python
  169. # Ignoring the exception is not enough as it prints a warning anyways.
  170. #sigpipe_old = signal.getsignal(signal.SIGPIPE)
  171. #signal.signal(signal.SIGPIPE, signal.SIG_DFL)
  172. cmd = self.strip_newlines(cmd)
  173. if not self.dry_run:
  174. # https://stackoverflow.com/questions/15535240/python-popen-write-to-stdout-and-log-file-simultaneously/52090802#52090802
  175. with subprocess.Popen(cmd, stdout=stdout, stderr=stderr, env=env, **kwargs) as proc:
  176. if out_file is not None:
  177. os.makedirs(os.path.split(os.path.abspath(out_file))[0], exist_ok=True)
  178. with open(out_file, 'bw') as logfile:
  179. while True:
  180. byte = proc.stdout.read(1)
  181. if byte:
  182. if show_stdout:
  183. sys.stdout.buffer.write(byte)
  184. try:
  185. sys.stdout.flush()
  186. except BlockingIOError:
  187. # TODO understand. Why, Python, why.
  188. pass
  189. logfile.write(byte)
  190. else:
  191. break
  192. signal.signal(signal.SIGINT, sigint_old)
  193. #signal.signal(signal.SIGPIPE, sigpipe_old)
  194. returncode = proc.returncode
  195. if returncode != 0 and raise_on_failure:
  196. raise Exception('Command exited with status: {}'.format(returncode))
  197. return returncode
  198. else:
  199. return 0
  200. def shlex_split(self, string):
  201. '''
  202. shlex_split, but also add Newline after every word.
  203. Not perfect since it does not group arguments, but I don't see a solution.
  204. '''
  205. return self.add_newlines(shlex.split(string))
  206. def strip_newlines(self, cmd):
  207. return [x for x in cmd if x != LF]
  208. def rmrf(self, path):
  209. self.print_cmd(['rm', '-r', '-f', path, LF])
  210. if not self.dry_run and os.path.exists(path):
  211. shutil.rmtree(path)
  212. def write_configs(self, config_path, configs, config_fragments=None):
  213. '''
  214. Write extra KEY=val configs into the given config file.
  215. '''
  216. if config_fragments is None:
  217. config_fragments = []
  218. with open(config_path, 'a') as config_file:
  219. for config_fragment in config_fragments:
  220. with open(config_fragment, 'r') as config_fragment_file:
  221. self.print_cmd(['cat', config_fragment, '>>', config_path])
  222. if not self.dry_run:
  223. for line in config_fragment_file:
  224. config_file.write(line)
  225. write_string_to_file(config_path, '\n'.join(configs), mode='a')
  226. def write_string_to_file(self, path, string, mode='w'):
  227. if mode == 'a':
  228. redirect = '>>'
  229. else:
  230. redirect = '>'
  231. self.print_cmd("cat << 'EOF' {} {}\n{}\nEOF".format(redirect, path, string))
  232. if not self.dry_run:
  233. with open(path, 'a') as f:
  234. f.write(string)