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