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 (
47 from bup.helpers import columnate, debug1, log, merge_dict, tty_width
48 from bup.io import byte_stream, path_msg
49 from bup.options import _tty_width
52 cmdpath = path.cmddir()
54 # Remove once we finish the internal command transition
55 transition_cmdpath = path.libdir() + b'/bup/cmd'
57 # We manipulate the subcmds here as strings, but they must be ASCII
58 # compatible, since we're going to be looking for exactly
59 # b'bup-SUBCMD' to exec.
62 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
63 '<command> [options...]\n\n')
65 ftp = 'Browse backup sets using an ftp-like client',
66 fsck = 'Check backup sets for damage and add redundancy information',
67 fuse = 'Mount your backup sets as a filesystem',
68 help = 'Print detailed help for the given command',
69 index = 'Create or display the index of files to back up',
70 on = 'Backup a remote machine to the local one',
71 restore = 'Extract files from a backup set',
72 save = 'Save files into a backup set (note: run "bup index" first)',
73 tag = 'Tag commits for easier access',
74 web = 'Launch a web server to examine backup sets',
77 log('Common commands:\n')
78 for cmd,synopsis in sorted(common.items()):
79 log(' %-10s %s\n' % (cmd, synopsis))
82 log('Other available commands:\n')
84 for c in sorted(os.listdir(cmdpath)):
85 if c.startswith(b'bup-') and c.find(b'.') < 0:
86 cname = fsdecode(c[4:])
87 if cname not in common:
88 cmds.append(c[4:].decode(errors='backslashreplace'))
89 log(columnate(cmds, ' '))
92 log("See 'bup help COMMAND' for more information on " +
93 "a specific command.\n")
102 # Handle global options.
104 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
105 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
106 except getopt.GetoptError as ex:
107 usage('error: %s' % ex.msg)
109 subcmd = [argv_bytes(x) for x in subcmd]
110 help_requested = None
114 for opt in global_args:
115 if opt[0] in ['-?', '--help']:
116 help_requested = True
117 elif opt[0] in ['-V', '--version']:
118 subcmd = [b'version']
119 elif opt[0] in ['-D', '--debug']:
121 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
122 elif opt[0] in ['--profile']:
124 elif opt[0] in ['-d', '--bup-dir']:
125 bup_dir = argv_bytes(opt[1])
127 usage('error: unexpected option "%s"' % opt[0])
129 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
131 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
139 if help_requested and subcmd[0] != b'help':
140 subcmd = [b'help'] + subcmd
142 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
143 subcmd = [b'help', subcmd[0]] + subcmd[2:]
145 subcmd_name = subcmd[0]
150 if subcmd_name not in []:
151 raise ModuleNotFoundError()
152 cmd_module = import_module('bup.cmd.'
153 + subcmd_name.decode('ascii').replace('-', '_'))
154 except ModuleNotFoundError as ex:
158 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
159 if not os.path.exists(subcmd[0]):
160 subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
161 subcmd_name.replace(b'-', b'_'))
162 if not os.path.exists(subcmd[0]):
163 usage('error: unknown command "%s"' % path_msg(subcmd_name))
165 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
166 if subcmd_name in [b'mux', b'ftp', b'help']:
168 fix_stdout = not already_fixed and os.isatty(1)
169 fix_stderr = not already_fixed and os.isatty(2)
171 if fix_stdout or fix_stderr:
172 tty_env = merge_dict(environ,
173 {b'BUP_FORCE_TTY': (b'%d'
174 % ((fix_stdout and 1 or 0)
175 + (fix_stderr and 2 or 0))),
176 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
181 sep_rx = re.compile(br'([\r\n])')
183 def print_clean_line(dest, content, width, sep=None):
184 """Write some or all of content, followed by sep, to the dest fd after
185 padding the content with enough spaces to fill the current
186 terminal width or truncating it to the terminal width if sep is a
189 assert sep in (b'\r', b'\n', None)
195 assert not sep_rx.match(x)
196 content = b''.join(content)
197 if sep == b'\r' and len(content) > width:
198 content = content[width:]
199 os.write(dest, content)
200 if len(content) < width:
201 os.write(dest, b' ' * (width - len(content)))
205 def filter_output(src_out, src_err, dest_out, dest_err):
206 """Transfer data from src_out to dest_out and src_err to dest_err via
207 print_clean_line until src_out and src_err close."""
209 assert not isinstance(src_out, bool)
210 assert not isinstance(src_err, bool)
211 assert not isinstance(dest_out, bool)
212 assert not isinstance(dest_err, bool)
213 assert src_out is not None or src_err is not None
214 assert (src_out is None) == (dest_out is None)
215 assert (src_err is None) == (dest_err is None)
219 fds = tuple([x for x in (src_out, src_err) if x is not None])
221 ready_fds, _, _ = select.select(fds, [], [])
224 buf = os.read(fd, 4096)
225 dest = dest_out if fd == src_out else dest_err
227 fds = tuple([x for x in fds if x is not fd])
228 print_clean_line(dest, pending.pop(fd, []), width)
230 split = sep_rx.split(buf)
231 while len(split) > 1:
232 content, sep = split[:2]
234 print_clean_line(dest,
235 pending.pop(fd, []) + [content],
238 assert(len(split) == 1)
240 pending.setdefault(fd, []).extend(split)
241 except BaseException as ex:
242 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
244 # Try to finish each of the streams
245 for fd, pending_items in compat.items(pending):
246 dest = dest_out if fd == src_out else dest_err
248 print_clean_line(dest, pending_items, width)
249 except (EnvironmentError, EOFError) as ex:
250 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
251 except BaseException as ex:
252 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
257 def run_subcmd(module, args):
260 return module.main(args)
262 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
263 if not (fix_stdout or fix_stderr):
268 out = byte_stream(sys.stdout)
269 err = byte_stream(sys.stderr)
272 p = subprocess.Popen(c,
273 stdout=PIPE if fix_stdout else out,
274 stderr=PIPE if fix_stderr else err,
275 env=tty_env, bufsize=4096, close_fds=True)
276 # Assume p will receive these signals and quit, which will
277 # then cause us to quit.
278 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
279 signal.signal(sig, signal.SIG_IGN)
281 filter_output(fix_stdout and p.stdout.fileno() or None,
282 fix_stderr and p.stderr.fileno() or None,
283 fix_stdout and out.fileno() or None,
284 fix_stderr and err.fileno() or None)
286 except BaseException as ex:
289 if p and p.poll() == None:
290 os.kill(p.pid, signal.SIGTERM)
292 except BaseException as kill_ex:
293 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
296 wrap_main(lambda : run_subcmd(cmd_module, subcmd))