]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
bup: include built-in subcommands in help
[bup.git] / lib / cmd / bup
1 #!/bin/sh
2 """": # -*-python-*-
3 set -e
4 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
5 export "BUP_ARGV_0"="$0"
6 arg_i=1
7 for arg in "$@"; do
8     export "BUP_ARGV_${arg_i}"="$arg"
9     shift
10     arg_i=$((arg_i + 1))
11 done
12 # Here to end of preamble replaced during install
13 # Find our directory
14 top="$(pwd)"
15 cmdpath="$0"
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")"
20     cmdpath="$link"
21 done
22 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
23 cd "$top"
24 exec "$script_home/../../config/bin/python" "$0"
25 """
26 # end of bup preamble
27
28 from __future__ import absolute_import, print_function
29
30 import os, sys
31 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
32
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
38
39 from bup import compat, path, helpers
40 from bup.compat import (
41     ModuleNotFoundError,
42     add_ex_ctx,
43     add_ex_tb,
44     argv_bytes,
45     environ,
46     fsdecode,
47     int_types,
48     wrap_main
49 )
50 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
51 from bup.helpers import (
52     columnate,
53     debug1,
54     handle_ctrl_c,
55     log,
56     merge_dict,
57     tty_width
58 )
59 from bup.git import close_catpipes
60 from bup.io import byte_stream, path_msg
61 from bup.options import _tty_width
62 import bup.cmd
63
64 def maybe_import_early(argv):
65     """Scan argv and import any modules specified by --import-py-module."""
66     while argv:
67         if argv[0] != '--import-py-module':
68             argv = argv[1:]
69             continue
70         if len(argv) < 2:
71             log("bup: --import-py-module must have an argument\n")
72             exit(2)
73         mod = argv[1]
74         import_module(mod)
75         argv = argv[2:]
76
77 maybe_import_early(compat.argv)
78
79 handle_ctrl_c()
80
81 cmdpath = path.cmddir()
82
83 # Remove once we finish the internal command transition
84 transition_cmdpath = path.libdir() + b'/bup/cmd'
85
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.
89
90 def usage(msg=""):
91     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
92         '<command> [options...]\n\n')
93     common = dict(
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',
104     )
105
106     log('Common commands:\n')
107     for cmd,synopsis in sorted(common.items()):
108         log('    %-10s %s\n' % (cmd, synopsis))
109     log('\n')
110     
111     log('Other available commands:\n')
112     cmds = set()
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:
122             cmds.add(name)
123
124     log(columnate(sorted(cmds), '    '))
125     log('\n')
126     
127     log("See 'bup help COMMAND' for more information on " +
128         "a specific command.\n")
129     if msg:
130         log("\n%s\n" % msg)
131     sys.exit(99)
132
133 argv = compat.argv
134 if len(argv) < 2:
135     usage()
136
137 # Handle global options.
138 try:
139     optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
140                'import-py-module=']
141     global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
142 except getopt.GetoptError as ex:
143     usage('error: %s' % ex.msg)
144
145 subcmd = [argv_bytes(x) for x in subcmd]
146 help_requested = None
147 do_profile = False
148 bup_dir = None
149
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']:
156         helpers.buglvl += 1
157         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
158     elif opt[0] in ['--profile']:
159         do_profile = True
160     elif opt[0] in ['-d', '--bup-dir']:
161         bup_dir = argv_bytes(opt[1])
162     elif opt[0] == '--import-py-module':
163         pass
164     else:
165         usage('error: unexpected option "%s"' % opt[0])
166
167 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
168 if bup_dir:
169     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
170
171 if len(subcmd) == 0:
172     if help_requested:
173         subcmd = [b'help']
174     else:
175         usage()
176
177 if help_requested and subcmd[0] != b'help':
178     subcmd = [b'help'] + subcmd
179
180 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
181     subcmd = [b'help', subcmd[0]] + subcmd[2:]
182
183 subcmd_name = subcmd[0]
184 if not subcmd_name:
185     usage()
186
187 try:
188     if subcmd_name not in []:
189         raise ModuleNotFoundError()
190     cmd_module = import_module('bup.cmd.'
191                                + subcmd_name.decode('ascii').replace('-', '_'))
192 except ModuleNotFoundError as ex:
193     cmd_module = None
194
195 if not cmd_module:
196     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
197     if not os.path.exists(subcmd[0]):
198         subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
199                                    subcmd_name.replace(b'-', b'_'))
200     if not os.path.exists(subcmd[0]):
201         usage('error: unknown command "%s"' % path_msg(subcmd_name))
202
203 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
204 if subcmd_name in [b'mux', b'ftp', b'help']:
205     already_fixed = True
206 fix_stdout = not already_fixed and os.isatty(1)
207 fix_stderr = not already_fixed and os.isatty(2)
208
209 if fix_stdout or fix_stderr:
210     tty_env = merge_dict(environ,
211                          {b'BUP_FORCE_TTY': (b'%d'
212                                              % ((fix_stdout and 1 or 0)
213                                                 + (fix_stderr and 2 or 0))),
214                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
215 else:
216     tty_env = environ
217
218
219 sep_rx = re.compile(br'([\r\n])')
220
221 def print_clean_line(dest, content, width, sep=None):
222     """Write some or all of content, followed by sep, to the dest fd after
223     padding the content with enough spaces to fill the current
224     terminal width or truncating it to the terminal width if sep is a
225     carriage return."""
226     global sep_rx
227     assert sep in (b'\r', b'\n', None)
228     if not content:
229         if sep:
230             os.write(dest, sep)
231         return
232     for x in content:
233         assert not sep_rx.match(x)
234     content = b''.join(content)
235     if sep == b'\r' and len(content) > width:
236         content = content[width:]
237     os.write(dest, content)
238     if len(content) < width:
239         os.write(dest, b' ' * (width - len(content)))
240     if sep:
241         os.write(dest, sep)
242
243 def filter_output(srcs, dests):
244     """Transfer data from file descriptors in srcs to the corresponding
245     file descriptors in dests print_clean_line until all of the srcs
246     have closed.
247
248     """
249     global sep_rx
250     assert all(type(x) in int_types for x in srcs)
251     assert all(type(x) in int_types for x in srcs)
252     assert len(srcs) == len(dests)
253     srcs = tuple(srcs)
254     dest_for = dict(zip(srcs, dests))
255     pending = {}
256     pending_ex = None
257     try:
258         while srcs:
259             ready_fds, _, _ = select.select(srcs, [], [])
260             width = tty_width()
261             for fd in ready_fds:
262                 buf = os.read(fd, 4096)
263                 dest = dest_for[fd]
264                 if not buf:
265                     srcs = tuple([x for x in srcs if x is not fd])
266                     print_clean_line(dest, pending.pop(fd, []), width)
267                 else:
268                     split = sep_rx.split(buf)
269                     while len(split) > 1:
270                         content, sep = split[:2]
271                         split = split[2:]
272                         print_clean_line(dest,
273                                          pending.pop(fd, []) + [content],
274                                          width,
275                                          sep)
276                     assert len(split) == 1
277                     if split[0]:
278                         pending.setdefault(fd, []).extend(split)
279     except BaseException as ex:
280         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
281     try:
282         # Try to finish each of the streams
283         for fd, pending_items in compat.items(pending):
284             dest = dest_for[fd]
285             width = tty_width()
286             try:
287                 print_clean_line(dest, pending_items, width)
288             except (EnvironmentError, EOFError) as ex:
289                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
290     except BaseException as ex:
291         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
292     if pending_ex:
293         raise pending_ex
294
295
296 def import_and_run_main(module, args):
297     if do_profile:
298         import cProfile
299         f = compile('module.main(args)', __file__, 'exec')
300         cProfile.runctx(f, globals(), locals())
301     else:
302         module.main(args)
303
304
305 def run_module_cmd(module, args):
306     if not (fix_stdout or fix_stderr):
307         import_and_run_main(module, args)
308         return
309     # Interpose filter_output between all attempts to write to the
310     # stdout/stderr and the real stdout/stderr (e.g. the fds that
311     # connect directly to the terminal) via a thread that runs
312     # filter_output in a pipeline.
313     srcs = []
314     dests = []
315     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
316     filter_thread = filter_thread_started = None
317     pending_ex = None
318     try:
319         if fix_stdout:
320             sys.stdout.flush()
321             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
322             real_out_fd = os.dup(sys.stdout.fileno())
323             os.dup2(stdout_pipe[1], sys.stdout.fileno())
324             srcs.append(stdout_pipe[0])
325             dests.append(real_out_fd)
326         if fix_stderr:
327             sys.stderr.flush()
328             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
329             real_err_fd = os.dup(sys.stderr.fileno())
330             os.dup2(stderr_pipe[1], sys.stderr.fileno())
331             srcs.append(stderr_pipe[0])
332             dests.append(real_err_fd)
333
334         filter_thread = Thread(name='output filter',
335                                target=lambda : filter_output(srcs, dests))
336         filter_thread.start()
337         filter_thread_started = True
338         import_and_run_main(module, args)
339     except Exception as ex:
340         add_ex_tb(ex)
341         pending_ex = ex
342         raise
343     finally:
344         # Try to make sure that whatever else happens, we restore
345         # stdout and stderr here, if that's possible, so that we don't
346         # risk just losing some output.
347         try:
348             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
349         except Exception as ex:
350             add_ex_tb(ex)
351             add_ex_ctx(ex, pending_ex)
352         try:
353             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
354         except Exception as ex:
355             add_ex_tb(ex)
356             add_ex_ctx(ex, pending_ex)
357         # Kick filter loose
358         try:
359             stdout_pipe is not None and os.close(stdout_pipe[1])
360         except Exception as ex:
361             add_ex_tb(ex)
362             add_ex_ctx(ex, pending_ex)
363         try:
364             stderr_pipe is not None and os.close(stderr_pipe[1])
365         except Exception as ex:
366             add_ex_tb(ex)
367             add_ex_ctx(ex, pending_ex)
368         try:
369             close_catpipes()
370         except Exception as ex:
371             add_ex_tb(ex)
372             add_ex_ctx(ex, pending_ex)
373     if pending_ex:
374         raise pending_ex
375     # There's no point in trying to join unless we finished the finally block.
376     if filter_thread_started:
377         filter_thread.join()
378
379
380 def run_subproc_cmd(args):
381
382     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
383     if not (fix_stdout or fix_stderr):
384         os.execvp(c[0], c)
385
386     sys.stdout.flush()
387     sys.stderr.flush()
388     out = byte_stream(sys.stdout)
389     err = byte_stream(sys.stderr)
390     p = None
391     try:
392         p = subprocess.Popen(c,
393                              stdout=PIPE if fix_stdout else out,
394                              stderr=PIPE if fix_stderr else err,
395                              env=tty_env, bufsize=4096, close_fds=True)
396         # Assume p will receive these signals and quit, which will
397         # then cause us to quit.
398         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
399             signal.signal(sig, signal.SIG_IGN)
400
401         srcs = []
402         dests = []
403         if fix_stdout:
404             srcs.append(p.stdout.fileno())
405             dests.append(out.fileno())
406         if fix_stderr:
407             srcs.append(p.stderr.fileno())
408             dests.append(err.fileno())
409         filter_output(srcs, dests)
410         return p.wait()
411     except BaseException as ex:
412         add_ex_tb(ex)
413         try:
414             if p and p.poll() == None:
415                 os.kill(p.pid, signal.SIGTERM)
416                 p.wait()
417         except BaseException as kill_ex:
418             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
419         raise ex
420
421
422 def run_subcmd(module, args):
423     if module:
424         run_module_cmd(module, args)
425     else:
426         run_subproc_cmd(args)
427
428
429 wrap_main(lambda : run_subcmd(cmd_module, subcmd))