]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
help: convert to internal command
[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 (b'bloom',
189                            b'cat-file',
190                            b'help',
191                            b'ls',
192                            b'meta',
193                            b'version',
194                            b'xstat'):
195         raise ModuleNotFoundError()
196     cmd_module = import_module('bup.cmd.'
197                                + subcmd_name.decode('ascii').replace('-', '_'))
198 except ModuleNotFoundError as ex:
199     cmd_module = None
200
201 if not cmd_module:
202     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
203     if not os.path.exists(subcmd[0]):
204         subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
205                                    subcmd_name.replace(b'-', b'_'))
206     if not os.path.exists(subcmd[0]):
207         usage('error: unknown command "%s"' % path_msg(subcmd_name))
208
209 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
210 if subcmd_name in [b'mux', b'ftp', b'help']:
211     already_fixed = True
212 fix_stdout = not already_fixed and os.isatty(1)
213 fix_stderr = not already_fixed and os.isatty(2)
214
215 if fix_stdout or fix_stderr:
216     tty_env = merge_dict(environ,
217                          {b'BUP_FORCE_TTY': (b'%d'
218                                              % ((fix_stdout and 1 or 0)
219                                                 + (fix_stderr and 2 or 0))),
220                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
221 else:
222     tty_env = environ
223
224
225 sep_rx = re.compile(br'([\r\n])')
226
227 def print_clean_line(dest, content, width, sep=None):
228     """Write some or all of content, followed by sep, to the dest fd after
229     padding the content with enough spaces to fill the current
230     terminal width or truncating it to the terminal width if sep is a
231     carriage return."""
232     global sep_rx
233     assert sep in (b'\r', b'\n', None)
234     if not content:
235         if sep:
236             os.write(dest, sep)
237         return
238     for x in content:
239         assert not sep_rx.match(x)
240     content = b''.join(content)
241     if sep == b'\r' and len(content) > width:
242         content = content[width:]
243     os.write(dest, content)
244     if len(content) < width:
245         os.write(dest, b' ' * (width - len(content)))
246     if sep:
247         os.write(dest, sep)
248
249 def filter_output(srcs, dests):
250     """Transfer data from file descriptors in srcs to the corresponding
251     file descriptors in dests print_clean_line until all of the srcs
252     have closed.
253
254     """
255     global sep_rx
256     assert all(type(x) in int_types for x in srcs)
257     assert all(type(x) in int_types for x in srcs)
258     assert len(srcs) == len(dests)
259     srcs = tuple(srcs)
260     dest_for = dict(zip(srcs, dests))
261     pending = {}
262     pending_ex = None
263     try:
264         while srcs:
265             ready_fds, _, _ = select.select(srcs, [], [])
266             width = tty_width()
267             for fd in ready_fds:
268                 buf = os.read(fd, 4096)
269                 dest = dest_for[fd]
270                 if not buf:
271                     srcs = tuple([x for x in srcs if x is not fd])
272                     print_clean_line(dest, pending.pop(fd, []), width)
273                 else:
274                     split = sep_rx.split(buf)
275                     while len(split) > 1:
276                         content, sep = split[:2]
277                         split = split[2:]
278                         print_clean_line(dest,
279                                          pending.pop(fd, []) + [content],
280                                          width,
281                                          sep)
282                     assert len(split) == 1
283                     if split[0]:
284                         pending.setdefault(fd, []).extend(split)
285     except BaseException as ex:
286         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
287     try:
288         # Try to finish each of the streams
289         for fd, pending_items in compat.items(pending):
290             dest = dest_for[fd]
291             width = tty_width()
292             try:
293                 print_clean_line(dest, pending_items, width)
294             except (EnvironmentError, EOFError) as ex:
295                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
296     except BaseException as ex:
297         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
298     if pending_ex:
299         raise pending_ex
300
301
302 def import_and_run_main(module, args):
303     if do_profile:
304         import cProfile
305         f = compile('module.main(args)', __file__, 'exec')
306         cProfile.runctx(f, globals(), locals())
307     else:
308         module.main(args)
309
310
311 def run_module_cmd(module, args):
312     if not (fix_stdout or fix_stderr):
313         import_and_run_main(module, args)
314         return
315     # Interpose filter_output between all attempts to write to the
316     # stdout/stderr and the real stdout/stderr (e.g. the fds that
317     # connect directly to the terminal) via a thread that runs
318     # filter_output in a pipeline.
319     srcs = []
320     dests = []
321     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
322     filter_thread = filter_thread_started = None
323     pending_ex = None
324     try:
325         if fix_stdout:
326             sys.stdout.flush()
327             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
328             real_out_fd = os.dup(sys.stdout.fileno())
329             os.dup2(stdout_pipe[1], sys.stdout.fileno())
330             srcs.append(stdout_pipe[0])
331             dests.append(real_out_fd)
332         if fix_stderr:
333             sys.stderr.flush()
334             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
335             real_err_fd = os.dup(sys.stderr.fileno())
336             os.dup2(stderr_pipe[1], sys.stderr.fileno())
337             srcs.append(stderr_pipe[0])
338             dests.append(real_err_fd)
339
340         filter_thread = Thread(name='output filter',
341                                target=lambda : filter_output(srcs, dests))
342         filter_thread.start()
343         filter_thread_started = True
344         import_and_run_main(module, args)
345     except Exception as ex:
346         add_ex_tb(ex)
347         pending_ex = ex
348         raise
349     finally:
350         # Try to make sure that whatever else happens, we restore
351         # stdout and stderr here, if that's possible, so that we don't
352         # risk just losing some output.
353         try:
354             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
355         except Exception as ex:
356             add_ex_tb(ex)
357             add_ex_ctx(ex, pending_ex)
358         try:
359             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
360         except Exception as ex:
361             add_ex_tb(ex)
362             add_ex_ctx(ex, pending_ex)
363         # Kick filter loose
364         try:
365             stdout_pipe is not None and os.close(stdout_pipe[1])
366         except Exception as ex:
367             add_ex_tb(ex)
368             add_ex_ctx(ex, pending_ex)
369         try:
370             stderr_pipe is not None and os.close(stderr_pipe[1])
371         except Exception as ex:
372             add_ex_tb(ex)
373             add_ex_ctx(ex, pending_ex)
374         try:
375             close_catpipes()
376         except Exception as ex:
377             add_ex_tb(ex)
378             add_ex_ctx(ex, pending_ex)
379     if pending_ex:
380         raise pending_ex
381     # There's no point in trying to join unless we finished the finally block.
382     if filter_thread_started:
383         filter_thread.join()
384
385
386 def run_subproc_cmd(args):
387
388     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
389     if not (fix_stdout or fix_stderr):
390         os.execvp(c[0], c)
391
392     sys.stdout.flush()
393     sys.stderr.flush()
394     out = byte_stream(sys.stdout)
395     err = byte_stream(sys.stderr)
396     p = None
397     try:
398         p = subprocess.Popen(c,
399                              stdout=PIPE if fix_stdout else out,
400                              stderr=PIPE if fix_stderr else err,
401                              env=tty_env, bufsize=4096, close_fds=True)
402         # Assume p will receive these signals and quit, which will
403         # then cause us to quit.
404         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
405             signal.signal(sig, signal.SIG_IGN)
406
407         srcs = []
408         dests = []
409         if fix_stdout:
410             srcs.append(p.stdout.fileno())
411             dests.append(out.fileno())
412         if fix_stderr:
413             srcs.append(p.stderr.fileno())
414             dests.append(err.fileno())
415         filter_output(srcs, dests)
416         return p.wait()
417     except BaseException as ex:
418         add_ex_tb(ex)
419         try:
420             if p and p.poll() == None:
421                 os.kill(p.pid, signal.SIGTERM)
422                 p.wait()
423         except BaseException as kill_ex:
424             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
425         raise ex
426
427
428 def run_subcmd(module, args):
429     if module:
430         run_module_cmd(module, args)
431     else:
432         run_subproc_cmd(args)
433
434
435 wrap_main(lambda : run_subcmd(cmd_module, subcmd))