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 subprocess import PIPE
35 from threading import Thread
36 import errno, getopt, os, re, select, signal, subprocess, sys
38 from bup import compat, path, helpers
39 from bup.compat import (
49 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
50 from bup.helpers import (
58 from bup.git import close_catpipes
59 from bup.io import byte_stream, path_msg
60 from bup.options import _tty_width
62 def maybe_import_early(argv):
63 """Scan argv and import any modules specified by --import-py-module."""
65 if argv[0] != '--import-py-module':
69 log("bup: --import-py-module must have an argument\n")
75 maybe_import_early(compat.argv)
79 cmdpath = path.cmddir()
81 # Remove once we finish the internal command transition
82 transition_cmdpath = path.libdir() + b'/bup/cmd'
84 # We manipulate the subcmds here as strings, but they must be ASCII
85 # compatible, since we're going to be looking for exactly
86 # b'bup-SUBCMD' to exec.
89 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
90 '<command> [options...]\n\n')
92 ftp = 'Browse backup sets using an ftp-like client',
93 fsck = 'Check backup sets for damage and add redundancy information',
94 fuse = 'Mount your backup sets as a filesystem',
95 help = 'Print detailed help for the given command',
96 index = 'Create or display the index of files to back up',
97 on = 'Backup a remote machine to the local one',
98 restore = 'Extract files from a backup set',
99 save = 'Save files into a backup set (note: run "bup index" first)',
100 tag = 'Tag commits for easier access',
101 web = 'Launch a web server to examine backup sets',
104 log('Common commands:\n')
105 for cmd,synopsis in sorted(common.items()):
106 log(' %-10s %s\n' % (cmd, synopsis))
109 log('Other available commands:\n')
111 for c in sorted(os.listdir(cmdpath)):
112 if c.startswith(b'bup-') and c.find(b'.') < 0:
113 cname = fsdecode(c[4:])
114 if cname not in common:
115 cmds.append(c[4:].decode(errors='backslashreplace'))
116 log(columnate(cmds, ' '))
119 log("See 'bup help COMMAND' for more information on " +
120 "a specific command.\n")
129 # Handle global options.
131 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
133 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
134 except getopt.GetoptError as ex:
135 usage('error: %s' % ex.msg)
137 subcmd = [argv_bytes(x) for x in subcmd]
138 help_requested = None
142 for opt in global_args:
143 if opt[0] in ['-?', '--help']:
144 help_requested = True
145 elif opt[0] in ['-V', '--version']:
146 subcmd = [b'version']
147 elif opt[0] in ['-D', '--debug']:
149 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
150 elif opt[0] in ['--profile']:
152 elif opt[0] in ['-d', '--bup-dir']:
153 bup_dir = argv_bytes(opt[1])
154 elif opt[0] == '--import-py-module':
157 usage('error: unexpected option "%s"' % opt[0])
159 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
161 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
169 if help_requested and subcmd[0] != b'help':
170 subcmd = [b'help'] + subcmd
172 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
173 subcmd = [b'help', subcmd[0]] + subcmd[2:]
175 subcmd_name = subcmd[0]
180 if subcmd_name not in []:
181 raise ModuleNotFoundError()
182 cmd_module = import_module('bup.cmd.'
183 + subcmd_name.decode('ascii').replace('-', '_'))
184 except ModuleNotFoundError as ex:
188 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
189 if not os.path.exists(subcmd[0]):
190 subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
191 subcmd_name.replace(b'-', b'_'))
192 if not os.path.exists(subcmd[0]):
193 usage('error: unknown command "%s"' % path_msg(subcmd_name))
195 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
196 if subcmd_name in [b'mux', b'ftp', b'help']:
198 fix_stdout = not already_fixed and os.isatty(1)
199 fix_stderr = not already_fixed and os.isatty(2)
201 if fix_stdout or fix_stderr:
202 tty_env = merge_dict(environ,
203 {b'BUP_FORCE_TTY': (b'%d'
204 % ((fix_stdout and 1 or 0)
205 + (fix_stderr and 2 or 0))),
206 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
211 sep_rx = re.compile(br'([\r\n])')
213 def print_clean_line(dest, content, width, sep=None):
214 """Write some or all of content, followed by sep, to the dest fd after
215 padding the content with enough spaces to fill the current
216 terminal width or truncating it to the terminal width if sep is a
219 assert sep in (b'\r', b'\n', None)
225 assert not sep_rx.match(x)
226 content = b''.join(content)
227 if sep == b'\r' and len(content) > width:
228 content = content[width:]
229 os.write(dest, content)
230 if len(content) < width:
231 os.write(dest, b' ' * (width - len(content)))
235 def filter_output(srcs, dests):
236 """Transfer data from file descriptors in srcs to the corresponding
237 file descriptors in dests print_clean_line until all of the srcs
242 assert all(type(x) in int_types for x in srcs)
243 assert all(type(x) in int_types for x in srcs)
244 assert len(srcs) == len(dests)
246 dest_for = dict(zip(srcs, dests))
251 ready_fds, _, _ = select.select(srcs, [], [])
254 buf = os.read(fd, 4096)
257 srcs = tuple([x for x in srcs if x is not fd])
258 print_clean_line(dest, pending.pop(fd, []), width)
260 split = sep_rx.split(buf)
261 while len(split) > 1:
262 content, sep = split[:2]
264 print_clean_line(dest,
265 pending.pop(fd, []) + [content],
268 assert len(split) == 1
270 pending.setdefault(fd, []).extend(split)
271 except BaseException as ex:
272 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
274 # Try to finish each of the streams
275 for fd, pending_items in compat.items(pending):
279 print_clean_line(dest, pending_items, width)
280 except (EnvironmentError, EOFError) as ex:
281 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
282 except BaseException as ex:
283 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
288 def import_and_run_main(module, args):
291 f = compile('module.main(args)', __file__, 'exec')
292 cProfile.runctx(f, globals(), locals())
297 def run_module_cmd(module, args):
298 if not (fix_stdout or fix_stderr):
299 import_and_run_main(module, args)
301 # Interpose filter_output between all attempts to write to the
302 # stdout/stderr and the real stdout/stderr (e.g. the fds that
303 # connect directly to the terminal) via a thread that runs
304 # filter_output in a pipeline.
307 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
308 filter_thread = filter_thread_started = None
313 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
314 real_out_fd = os.dup(sys.stdout.fileno())
315 os.dup2(stdout_pipe[1], sys.stdout.fileno())
316 srcs.append(stdout_pipe[0])
317 dests.append(real_out_fd)
320 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
321 real_err_fd = os.dup(sys.stderr.fileno())
322 os.dup2(stderr_pipe[1], sys.stderr.fileno())
323 srcs.append(stderr_pipe[0])
324 dests.append(real_err_fd)
326 filter_thread = Thread(name='output filter',
327 target=lambda : filter_output(srcs, dests))
328 filter_thread.start()
329 filter_thread_started = True
330 import_and_run_main(module, args)
331 except Exception as ex:
336 # Try to make sure that whatever else happens, we restore
337 # stdout and stderr here, if that's possible, so that we don't
338 # risk just losing some output.
340 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
341 except Exception as ex:
343 add_ex_ctx(ex, pending_ex)
345 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
346 except Exception as ex:
348 add_ex_ctx(ex, pending_ex)
351 stdout_pipe is not None and os.close(stdout_pipe[1])
352 except Exception as ex:
354 add_ex_ctx(ex, pending_ex)
356 stderr_pipe is not None and os.close(stderr_pipe[1])
357 except Exception as ex:
359 add_ex_ctx(ex, pending_ex)
362 except Exception as ex:
364 add_ex_ctx(ex, pending_ex)
367 # There's no point in trying to join unless we finished the finally block.
368 if filter_thread_started:
372 def run_subproc_cmd(args):
374 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
375 if not (fix_stdout or fix_stderr):
380 out = byte_stream(sys.stdout)
381 err = byte_stream(sys.stderr)
384 p = subprocess.Popen(c,
385 stdout=PIPE if fix_stdout else out,
386 stderr=PIPE if fix_stderr else err,
387 env=tty_env, bufsize=4096, close_fds=True)
388 # Assume p will receive these signals and quit, which will
389 # then cause us to quit.
390 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
391 signal.signal(sig, signal.SIG_IGN)
396 srcs.append(p.stdout.fileno())
397 dests.append(out.fileno())
399 srcs.append(p.stderr.fileno())
400 dests.append(err.fileno())
401 filter_output(srcs, dests)
403 except BaseException as ex:
406 if p and p.poll() == None:
407 os.kill(p.pid, signal.SIGTERM)
409 except BaseException as kill_ex:
410 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
414 def run_subcmd(module, args):
416 run_module_cmd(module, args)
418 run_subproc_cmd(args)
421 wrap_main(lambda : run_subcmd(cmd_module, subcmd))