]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
Add handle_ctrl_c to bup itself
[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 subprocess import PIPE
35 import errno, getopt, os, re, select, signal, subprocess, sys
36
37 from bup import compat, path, helpers
38 from bup.compat import (
39     ModuleNotFoundError,
40     add_ex_ctx,
41     add_ex_tb,
42     argv_bytes,
43     environ,
44     fsdecode,
45     wrap_main
46 )
47 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
48 from bup.helpers import (
49     columnate,
50     debug1,
51     handle_ctrl_c,
52     log,
53     merge_dict,
54     tty_width
55 )
56 from bup.io import byte_stream, path_msg
57 from bup.options import _tty_width
58
59 handle_ctrl_c()
60
61 cmdpath = path.cmddir()
62
63 # Remove once we finish the internal command transition
64 transition_cmdpath = path.libdir() + b'/bup/cmd'
65
66 # We manipulate the subcmds here as strings, but they must be ASCII
67 # compatible, since we're going to be looking for exactly
68 # b'bup-SUBCMD' to exec.
69
70 def usage(msg=""):
71     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
72         '<command> [options...]\n\n')
73     common = dict(
74         ftp = 'Browse backup sets using an ftp-like client',
75         fsck = 'Check backup sets for damage and add redundancy information',
76         fuse = 'Mount your backup sets as a filesystem',
77         help = 'Print detailed help for the given command',
78         index = 'Create or display the index of files to back up',
79         on = 'Backup a remote machine to the local one',
80         restore = 'Extract files from a backup set',
81         save = 'Save files into a backup set (note: run "bup index" first)',
82         tag = 'Tag commits for easier access',
83         web = 'Launch a web server to examine backup sets',
84     )
85
86     log('Common commands:\n')
87     for cmd,synopsis in sorted(common.items()):
88         log('    %-10s %s\n' % (cmd, synopsis))
89     log('\n')
90     
91     log('Other available commands:\n')
92     cmds = []
93     for c in sorted(os.listdir(cmdpath)):
94         if c.startswith(b'bup-') and c.find(b'.') < 0:
95             cname = fsdecode(c[4:])
96             if cname not in common:
97                 cmds.append(c[4:].decode(errors='backslashreplace'))
98     log(columnate(cmds, '    '))
99     log('\n')
100     
101     log("See 'bup help COMMAND' for more information on " +
102         "a specific command.\n")
103     if msg:
104         log("\n%s\n" % msg)
105     sys.exit(99)
106
107 argv = compat.argv
108 if len(argv) < 2:
109     usage()
110
111 # Handle global options.
112 try:
113     optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
114     global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
115 except getopt.GetoptError as ex:
116     usage('error: %s' % ex.msg)
117
118 subcmd = [argv_bytes(x) for x in subcmd]
119 help_requested = None
120 do_profile = False
121 bup_dir = None
122
123 for opt in global_args:
124     if opt[0] in ['-?', '--help']:
125         help_requested = True
126     elif opt[0] in ['-V', '--version']:
127         subcmd = [b'version']
128     elif opt[0] in ['-D', '--debug']:
129         helpers.buglvl += 1
130         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
131     elif opt[0] in ['--profile']:
132         do_profile = True
133     elif opt[0] in ['-d', '--bup-dir']:
134         bup_dir = argv_bytes(opt[1])
135     else:
136         usage('error: unexpected option "%s"' % opt[0])
137
138 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
139 if bup_dir:
140     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
141
142 if len(subcmd) == 0:
143     if help_requested:
144         subcmd = [b'help']
145     else:
146         usage()
147
148 if help_requested and subcmd[0] != b'help':
149     subcmd = [b'help'] + subcmd
150
151 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
152     subcmd = [b'help', subcmd[0]] + subcmd[2:]
153
154 subcmd_name = subcmd[0]
155 if not subcmd_name:
156     usage()
157
158 try:
159     if subcmd_name not in []:
160         raise ModuleNotFoundError()
161     cmd_module = import_module('bup.cmd.'
162                                + subcmd_name.decode('ascii').replace('-', '_'))
163 except ModuleNotFoundError as ex:
164     cmd_module = None
165
166 if not cmd_module:
167     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
168     if not os.path.exists(subcmd[0]):
169         subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
170                                    subcmd_name.replace(b'-', b'_'))
171     if not os.path.exists(subcmd[0]):
172         usage('error: unknown command "%s"' % path_msg(subcmd_name))
173
174 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
175 if subcmd_name in [b'mux', b'ftp', b'help']:
176     already_fixed = True
177 fix_stdout = not already_fixed and os.isatty(1)
178 fix_stderr = not already_fixed and os.isatty(2)
179
180 if fix_stdout or fix_stderr:
181     tty_env = merge_dict(environ,
182                          {b'BUP_FORCE_TTY': (b'%d'
183                                              % ((fix_stdout and 1 or 0)
184                                                 + (fix_stderr and 2 or 0))),
185                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
186 else:
187     tty_env = environ
188
189
190 sep_rx = re.compile(br'([\r\n])')
191
192 def print_clean_line(dest, content, width, sep=None):
193     """Write some or all of content, followed by sep, to the dest fd after
194     padding the content with enough spaces to fill the current
195     terminal width or truncating it to the terminal width if sep is a
196     carriage return."""
197     global sep_rx
198     assert sep in (b'\r', b'\n', None)
199     if not content:
200         if sep:
201             os.write(dest, sep)
202         return
203     for x in content:
204         assert not sep_rx.match(x)
205     content = b''.join(content)
206     if sep == b'\r' and len(content) > width:
207         content = content[width:]
208     os.write(dest, content)
209     if len(content) < width:
210         os.write(dest, b' ' * (width - len(content)))
211     if sep:
212         os.write(dest, sep)
213
214 def filter_output(src_out, src_err, dest_out, dest_err):
215     """Transfer data from src_out to dest_out and src_err to dest_err via
216     print_clean_line until src_out and src_err close."""
217     global sep_rx
218     assert not isinstance(src_out, bool)
219     assert not isinstance(src_err, bool)
220     assert not isinstance(dest_out, bool)
221     assert not isinstance(dest_err, bool)
222     assert src_out is not None or src_err is not None
223     assert (src_out is None) == (dest_out is None)
224     assert (src_err is None) == (dest_err is None)
225     pending = {}
226     pending_ex = None
227     try:
228         fds = tuple([x for x in (src_out, src_err) if x is not None])
229         while fds:
230             ready_fds, _, _ = select.select(fds, [], [])
231             width = tty_width()
232             for fd in ready_fds:
233                 buf = os.read(fd, 4096)
234                 dest = dest_out if fd == src_out else dest_err
235                 if not buf:
236                     fds = tuple([x for x in fds if x is not fd])
237                     print_clean_line(dest, pending.pop(fd, []), width)
238                 else:
239                     split = sep_rx.split(buf)
240                     while len(split) > 1:
241                         content, sep = split[:2]
242                         split = split[2:]
243                         print_clean_line(dest,
244                                          pending.pop(fd, []) + [content],
245                                          width,
246                                          sep)
247                     assert(len(split) == 1)
248                     if split[0]:
249                         pending.setdefault(fd, []).extend(split)
250     except BaseException as ex:
251         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
252     try:
253         # Try to finish each of the streams
254         for fd, pending_items in compat.items(pending):
255             dest = dest_out if fd == src_out else dest_err
256             try:
257                 print_clean_line(dest, pending_items, width)
258             except (EnvironmentError, EOFError) as ex:
259                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
260     except BaseException as ex:
261         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
262     if pending_ex:
263         raise pending_ex
264
265
266 def run_subcmd(module, args):
267
268     if module:
269         if do_profile:
270             import cProfile
271             f = compile('module.main(args)', __file__, 'exec')
272             cProfile.runctx(f, globals(), locals())
273         else:
274             module.main(args)
275         return
276
277     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
278     if not (fix_stdout or fix_stderr):
279         os.execvp(c[0], c)
280
281     sys.stdout.flush()
282     sys.stderr.flush()
283     out = byte_stream(sys.stdout)
284     err = byte_stream(sys.stderr)
285     p = None
286     try:
287         p = subprocess.Popen(c,
288                              stdout=PIPE if fix_stdout else out,
289                              stderr=PIPE if fix_stderr else err,
290                              env=tty_env, bufsize=4096, close_fds=True)
291         # Assume p will receive these signals and quit, which will
292         # then cause us to quit.
293         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
294             signal.signal(sig, signal.SIG_IGN)
295
296         filter_output(fix_stdout and p.stdout.fileno() or None,
297                       fix_stderr and p.stderr.fileno() or None,
298                       fix_stdout and out.fileno() or None,
299                       fix_stderr and err.fileno() or None)
300         return p.wait()
301     except BaseException as ex:
302         add_ex_tb(ex)
303         try:
304             if p and p.poll() == None:
305                 os.kill(p.pid, signal.SIGTERM)
306                 p.wait()
307         except BaseException as kill_ex:
308             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
309         raise ex
310
311 wrap_main(lambda : run_subcmd(cmd_module, subcmd))