4 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
5 export "BUP_ARGV_0"="$0"
8 export "BUP_ARGV_${arg_i}"="$arg"
12 # Here to end of preamble replaced during install
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")"
22 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
24 exec "$script_home/../../config/bin/python" "$0"
28 from __future__ import absolute_import, print_function
31 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
33 from importlib import import_module
34 from subprocess import PIPE
35 import errno, getopt, os, re, select, signal, subprocess, sys
37 from bup import compat, path, helpers
38 from bup.compat import (
48 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
49 from bup.helpers import (
57 from bup.io import byte_stream, path_msg
58 from bup.options import _tty_width
60 def maybe_import_early(argv):
61 """Scan argv and import any modules specified by --import-py-module."""
63 if argv[0] != '--import-py-module':
67 log("bup: --import-py-module must have an argument\n")
73 maybe_import_early(compat.argv)
77 cmdpath = path.cmddir()
79 # Remove once we finish the internal command transition
80 transition_cmdpath = path.libdir() + b'/bup/cmd'
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.
87 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
88 '<command> [options...]\n\n')
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',
102 log('Common commands:\n')
103 for cmd,synopsis in sorted(common.items()):
104 log(' %-10s %s\n' % (cmd, synopsis))
107 log('Other available commands:\n')
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, ' '))
117 log("See 'bup help COMMAND' for more information on " +
118 "a specific command.\n")
127 # Handle global options.
129 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
131 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
132 except getopt.GetoptError as ex:
133 usage('error: %s' % ex.msg)
135 subcmd = [argv_bytes(x) for x in subcmd]
136 help_requested = None
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']:
147 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
148 elif opt[0] in ['--profile']:
150 elif opt[0] in ['-d', '--bup-dir']:
151 bup_dir = argv_bytes(opt[1])
152 elif opt[0] == '--import-py-module':
155 usage('error: unexpected option "%s"' % opt[0])
157 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
159 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
167 if help_requested and subcmd[0] != b'help':
168 subcmd = [b'help'] + subcmd
170 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
171 subcmd = [b'help', subcmd[0]] + subcmd[2:]
173 subcmd_name = subcmd[0]
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:
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))
193 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
194 if subcmd_name in [b'mux', b'ftp', b'help']:
196 fix_stdout = not already_fixed and os.isatty(1)
197 fix_stderr = not already_fixed and os.isatty(2)
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(), })
209 sep_rx = re.compile(br'([\r\n])')
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
217 assert sep in (b'\r', b'\n', None)
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)))
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
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)
244 dest_for = dict(zip(srcs, dests))
249 ready_fds, _, _ = select.select(srcs, [], [])
252 buf = os.read(fd, 4096)
255 srcs = tuple([x for x in srcs if x is not fd])
256 print_clean_line(dest, pending.pop(fd, []), width)
258 split = sep_rx.split(buf)
259 while len(split) > 1:
260 content, sep = split[:2]
262 print_clean_line(dest,
263 pending.pop(fd, []) + [content],
266 assert len(split) == 1
268 pending.setdefault(fd, []).extend(split)
269 except BaseException as ex:
270 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
272 # Try to finish each of the streams
273 for fd, pending_items in compat.items(pending):
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)
286 def run_subcmd(module, args):
291 f = compile('module.main(args)', __file__, 'exec')
292 cProfile.runctx(f, globals(), locals())
297 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
298 if not (fix_stdout or fix_stderr):
303 out = byte_stream(sys.stdout)
304 err = byte_stream(sys.stderr)
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)
319 srcs.append(p.stdout.fileno())
320 dests.append(out.fileno())
322 srcs.append(p.stderr.fileno())
323 dests.append(err.fileno())
324 filter_output(srcs, dests)
326 except BaseException as ex:
329 if p and p.poll() == None:
330 os.kill(p.pid, signal.SIGTERM)
332 except BaseException as kill_ex:
333 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
336 wrap_main(lambda : run_subcmd(cmd_module, subcmd))