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 import errno, getopt, os, re, select, signal, subprocess, sys
34 from subprocess import PIPE
36 from bup.compat import environ, fsdecode
37 from bup.io import path_msg
38 from bup import compat, path, helpers
39 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
40 from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width
41 from bup.io import byte_stream, path_msg
43 cmdpath = path.cmddir()
45 # We manipulate the subcmds here as strings, but they must be ASCII
46 # compatible, since we're going to be looking for exactly
47 # b'bup-SUBCMD' to exec.
50 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
51 '<command> [options...]\n\n')
53 ftp = 'Browse backup sets using an ftp-like client',
54 fsck = 'Check backup sets for damage and add redundancy information',
55 fuse = 'Mount your backup sets as a filesystem',
56 help = 'Print detailed help for the given command',
57 index = 'Create or display the index of files to back up',
58 on = 'Backup a remote machine to the local one',
59 restore = 'Extract files from a backup set',
60 save = 'Save files into a backup set (note: run "bup index" first)',
61 tag = 'Tag commits for easier access',
62 web = 'Launch a web server to examine backup sets',
65 log('Common commands:\n')
66 for cmd,synopsis in sorted(common.items()):
67 log(' %-10s %s\n' % (cmd, synopsis))
70 log('Other available commands:\n')
72 for c in sorted(os.listdir(cmdpath)):
73 if c.startswith(b'bup-') and c.find(b'.') < 0:
74 cname = fsdecode(c[4:])
75 if cname not in common:
76 cmds.append(c[4:].decode(errors='backslashreplace'))
77 log(columnate(cmds, ' '))
80 log("See 'bup help COMMAND' for more information on " +
81 "a specific command.\n")
90 # Handle global options.
92 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
93 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
94 except getopt.GetoptError as ex:
95 usage('error: %s' % ex.msg)
97 subcmd = [argv_bytes(x) for x in subcmd]
102 for opt in global_args:
103 if opt[0] in ['-?', '--help']:
104 help_requested = True
105 elif opt[0] in ['-V', '--version']:
106 subcmd = [b'version']
107 elif opt[0] in ['-D', '--debug']:
109 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
110 elif opt[0] in ['--profile']:
112 elif opt[0] in ['-d', '--bup-dir']:
113 bup_dir = argv_bytes(opt[1])
115 usage('error: unexpected option "%s"' % opt[0])
118 bup_dir = argv_bytes(bup_dir)
120 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
122 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
130 if help_requested and subcmd[0] != b'help':
131 subcmd = [b'help'] + subcmd
133 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
134 subcmd = [b'help', subcmd[0]] + subcmd[2:]
136 subcmd_name = subcmd[0]
141 return os.path.join(cmdpath, b'bup-' + subcmd)
143 subcmd[0] = subpath(subcmd_name)
144 if not os.path.exists(subcmd[0]):
145 usage('error: unknown command "%s"' % path_msg(subcmd_name))
147 already_fixed = atoi(environ.get(b'BUP_FORCE_TTY'))
148 if subcmd_name in [b'mux', b'ftp', b'help']:
150 fix_stdout = not already_fixed and os.isatty(1)
151 fix_stderr = not already_fixed and os.isatty(2)
153 if fix_stdout or fix_stderr:
154 tty_env = merge_dict(environ,
155 {b'BUP_FORCE_TTY': (b'%d'
156 % ((fix_stdout and 1 or 0)
157 + (fix_stderr and 2 or 0)))})
162 sep_rx = re.compile(br'([\r\n])')
164 def print_clean_line(dest, content, width, sep=None):
165 """Write some or all of content, followed by sep, to the dest fd after
166 padding the content with enough spaces to fill the current
167 terminal width or truncating it to the terminal width if sep is a
170 assert sep in (b'\r', b'\n', None)
176 assert not sep_rx.match(x)
177 content = b''.join(content)
178 if sep == b'\r' and len(content) > width:
179 content = content[width:]
180 os.write(dest, content)
181 if len(content) < width:
182 os.write(dest, b' ' * (width - len(content)))
186 def filter_output(src_out, src_err, dest_out, dest_err):
187 """Transfer data from src_out to dest_out and src_err to dest_err via
188 print_clean_line until src_out and src_err close."""
190 assert not isinstance(src_out, bool)
191 assert not isinstance(src_err, bool)
192 assert not isinstance(dest_out, bool)
193 assert not isinstance(dest_err, bool)
194 assert src_out is not None or src_err is not None
195 assert (src_out is None) == (dest_out is None)
196 assert (src_err is None) == (dest_err is None)
200 fds = tuple([x for x in (src_out, src_err) if x is not None])
202 ready_fds, _, _ = select.select(fds, [], [])
205 buf = os.read(fd, 4096)
206 dest = dest_out if fd == src_out else dest_err
208 fds = tuple([x for x in fds if x is not fd])
209 print_clean_line(dest, pending.pop(fd, []), width)
211 split = sep_rx.split(buf)
212 while len(split) > 1:
213 content, sep = split[:2]
215 print_clean_line(dest,
216 pending.pop(fd, []) + [content],
219 assert(len(split) == 1)
221 pending.setdefault(fd, []).extend(split)
222 except BaseException as ex:
223 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
225 # Try to finish each of the streams
226 for fd, pending_items in compat.items(pending):
227 dest = dest_out if fd == src_out else dest_err
229 print_clean_line(dest, pending_items, width)
230 except (EnvironmentError, EOFError) as ex:
231 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
232 except BaseException as ex:
233 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
237 def run_subcmd(subcmd):
239 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + subcmd
240 if not (fix_stdout or fix_stderr):
245 out = byte_stream(sys.stdout)
246 err = byte_stream(sys.stderr)
249 p = subprocess.Popen(c,
250 stdout=PIPE if fix_stdout else out,
251 stderr=PIPE if fix_stderr else err,
252 env=tty_env, bufsize=4096, close_fds=True)
253 # Assume p will receive these signals and quit, which will
254 # then cause us to quit.
255 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
256 signal.signal(sig, signal.SIG_IGN)
258 filter_output(fix_stdout and p.stdout.fileno() or None,
259 fix_stderr and p.stderr.fileno() or None,
260 fix_stdout and out.fileno() or None,
261 fix_stderr and err.fileno() or None)
263 except BaseException as ex:
266 if p and p.poll() == None:
267 os.kill(p.pid, signal.SIGTERM)
269 except BaseException as kill_ex:
270 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
273 wrap_main(lambda : run_subcmd(subcmd))