4 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
5 export "BUP_ARGV_0"="$0"
8 export "BUP_ARGV_${arg_i}"="$arg"
12 # Here to end of preamble replaced during install
16 # loop because macos doesn't have recursive readlink/realpath utils
17 while test -L "$cmdpath"; do
18 link="$(readlink "$cmdpath")"
19 cd "$(dirname "$cmdpath")"
22 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
24 exec "$script_home/../../config/bin/python" "$0"
28 from __future__ import absolute_import, print_function
31 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
33 from importlib import import_module
34 from pkgutil import iter_modules
35 from subprocess import PIPE
36 from threading import Thread
37 import errno, getopt, os, re, select, signal, subprocess, sys
39 from bup import compat, path, helpers
40 from bup.compat import (
50 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
51 from bup.helpers import (
59 from bup.git import close_catpipes
60 from bup.io import byte_stream, path_msg
61 from bup.options import _tty_width
64 def maybe_import_early(argv):
65 """Scan argv and import any modules specified by --import-py-module."""
67 if argv[0] != '--import-py-module':
71 log("bup: --import-py-module must have an argument\n")
77 maybe_import_early(compat.argv)
81 cmdpath = path.cmddir()
83 # Remove once we finish the internal command transition
84 transition_cmdpath = path.libdir() + b'/bup/cmd'
86 # We manipulate the subcmds here as strings, but they must be ASCII
87 # compatible, since we're going to be looking for exactly
88 # b'bup-SUBCMD' to exec.
91 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
92 '<command> [options...]\n\n')
94 ftp = 'Browse backup sets using an ftp-like client',
95 fsck = 'Check backup sets for damage and add redundancy information',
96 fuse = 'Mount your backup sets as a filesystem',
97 help = 'Print detailed help for the given command',
98 index = 'Create or display the index of files to back up',
99 on = 'Backup a remote machine to the local one',
100 restore = 'Extract files from a backup set',
101 save = 'Save files into a backup set (note: run "bup index" first)',
102 tag = 'Tag commits for easier access',
103 web = 'Launch a web server to examine backup sets',
106 log('Common commands:\n')
107 for cmd,synopsis in sorted(common.items()):
108 log(' %-10s %s\n' % (cmd, synopsis))
111 log('Other available commands:\n')
113 for c in sorted(os.listdir(cmdpath)):
114 if c.startswith(b'bup-') and c.find(b'.') < 0:
115 cname = fsdecode(c[4:])
116 if cname not in common:
117 cmds.add(c[4:].decode(errors='backslashreplace'))
118 # built-in commands take precedence
119 for _, name, _ in iter_modules(path=bup.cmd.__path__):
120 name = name.replace('_','-')
121 if name not in common:
124 log(columnate(sorted(cmds), ' '))
127 log("See 'bup help COMMAND' for more information on " +
128 "a specific command.\n")
137 # Handle global options.
139 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
141 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
142 except getopt.GetoptError as ex:
143 usage('error: %s' % ex.msg)
145 subcmd = [argv_bytes(x) for x in subcmd]
146 help_requested = None
150 for opt in global_args:
151 if opt[0] in ['-?', '--help']:
152 help_requested = True
153 elif opt[0] in ['-V', '--version']:
154 subcmd = [b'version']
155 elif opt[0] in ['-D', '--debug']:
157 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
158 elif opt[0] in ['--profile']:
160 elif opt[0] in ['-d', '--bup-dir']:
161 bup_dir = argv_bytes(opt[1])
162 elif opt[0] == '--import-py-module':
165 usage('error: unexpected option "%s"' % opt[0])
167 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
169 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
177 if help_requested and subcmd[0] != b'help':
178 subcmd = [b'help'] + subcmd
180 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
181 subcmd = [b'help', subcmd[0]] + subcmd[2:]
183 subcmd_name = subcmd[0]
188 if subcmd_name not in (b'cat-file',
191 raise ModuleNotFoundError()
192 cmd_module = import_module('bup.cmd.'
193 + subcmd_name.decode('ascii').replace('-', '_'))
194 except ModuleNotFoundError as ex:
198 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
199 if not os.path.exists(subcmd[0]):
200 subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
201 subcmd_name.replace(b'-', b'_'))
202 if not os.path.exists(subcmd[0]):
203 usage('error: unknown command "%s"' % path_msg(subcmd_name))
205 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
206 if subcmd_name in [b'mux', b'ftp', b'help']:
208 fix_stdout = not already_fixed and os.isatty(1)
209 fix_stderr = not already_fixed and os.isatty(2)
211 if fix_stdout or fix_stderr:
212 tty_env = merge_dict(environ,
213 {b'BUP_FORCE_TTY': (b'%d'
214 % ((fix_stdout and 1 or 0)
215 + (fix_stderr and 2 or 0))),
216 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
221 sep_rx = re.compile(br'([\r\n])')
223 def print_clean_line(dest, content, width, sep=None):
224 """Write some or all of content, followed by sep, to the dest fd after
225 padding the content with enough spaces to fill the current
226 terminal width or truncating it to the terminal width if sep is a
229 assert sep in (b'\r', b'\n', None)
235 assert not sep_rx.match(x)
236 content = b''.join(content)
237 if sep == b'\r' and len(content) > width:
238 content = content[width:]
239 os.write(dest, content)
240 if len(content) < width:
241 os.write(dest, b' ' * (width - len(content)))
245 def filter_output(srcs, dests):
246 """Transfer data from file descriptors in srcs to the corresponding
247 file descriptors in dests print_clean_line until all of the srcs
252 assert all(type(x) in int_types for x in srcs)
253 assert all(type(x) in int_types for x in srcs)
254 assert len(srcs) == len(dests)
256 dest_for = dict(zip(srcs, dests))
261 ready_fds, _, _ = select.select(srcs, [], [])
264 buf = os.read(fd, 4096)
267 srcs = tuple([x for x in srcs if x is not fd])
268 print_clean_line(dest, pending.pop(fd, []), width)
270 split = sep_rx.split(buf)
271 while len(split) > 1:
272 content, sep = split[:2]
274 print_clean_line(dest,
275 pending.pop(fd, []) + [content],
278 assert len(split) == 1
280 pending.setdefault(fd, []).extend(split)
281 except BaseException as ex:
282 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
284 # Try to finish each of the streams
285 for fd, pending_items in compat.items(pending):
289 print_clean_line(dest, pending_items, width)
290 except (EnvironmentError, EOFError) as ex:
291 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
292 except BaseException as ex:
293 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
298 def import_and_run_main(module, args):
301 f = compile('module.main(args)', __file__, 'exec')
302 cProfile.runctx(f, globals(), locals())
307 def run_module_cmd(module, args):
308 if not (fix_stdout or fix_stderr):
309 import_and_run_main(module, args)
311 # Interpose filter_output between all attempts to write to the
312 # stdout/stderr and the real stdout/stderr (e.g. the fds that
313 # connect directly to the terminal) via a thread that runs
314 # filter_output in a pipeline.
317 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
318 filter_thread = filter_thread_started = None
323 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
324 real_out_fd = os.dup(sys.stdout.fileno())
325 os.dup2(stdout_pipe[1], sys.stdout.fileno())
326 srcs.append(stdout_pipe[0])
327 dests.append(real_out_fd)
330 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
331 real_err_fd = os.dup(sys.stderr.fileno())
332 os.dup2(stderr_pipe[1], sys.stderr.fileno())
333 srcs.append(stderr_pipe[0])
334 dests.append(real_err_fd)
336 filter_thread = Thread(name='output filter',
337 target=lambda : filter_output(srcs, dests))
338 filter_thread.start()
339 filter_thread_started = True
340 import_and_run_main(module, args)
341 except Exception as ex:
346 # Try to make sure that whatever else happens, we restore
347 # stdout and stderr here, if that's possible, so that we don't
348 # risk just losing some output.
350 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
351 except Exception as ex:
353 add_ex_ctx(ex, pending_ex)
355 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
356 except Exception as ex:
358 add_ex_ctx(ex, pending_ex)
361 stdout_pipe is not None and os.close(stdout_pipe[1])
362 except Exception as ex:
364 add_ex_ctx(ex, pending_ex)
366 stderr_pipe is not None and os.close(stderr_pipe[1])
367 except Exception as ex:
369 add_ex_ctx(ex, pending_ex)
372 except Exception as ex:
374 add_ex_ctx(ex, pending_ex)
377 # There's no point in trying to join unless we finished the finally block.
378 if filter_thread_started:
382 def run_subproc_cmd(args):
384 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
385 if not (fix_stdout or fix_stderr):
390 out = byte_stream(sys.stdout)
391 err = byte_stream(sys.stderr)
394 p = subprocess.Popen(c,
395 stdout=PIPE if fix_stdout else out,
396 stderr=PIPE if fix_stderr else err,
397 env=tty_env, bufsize=4096, close_fds=True)
398 # Assume p will receive these signals and quit, which will
399 # then cause us to quit.
400 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
401 signal.signal(sig, signal.SIG_IGN)
406 srcs.append(p.stdout.fileno())
407 dests.append(out.fileno())
409 srcs.append(p.stderr.fileno())
410 dests.append(err.fileno())
411 filter_output(srcs, dests)
413 except BaseException as ex:
416 if p and p.poll() == None:
417 os.kill(p.pid, signal.SIGTERM)
419 except BaseException as kill_ex:
420 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
424 def run_subcmd(module, args):
426 run_module_cmd(module, args)
428 run_subproc_cmd(args)
431 wrap_main(lambda : run_subcmd(cmd_module, subcmd))