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