2 """": # -*-python-*- # -*-python-*-
6 # loop because macos doesn't have recursive readlink/realpath utils
7 while test -L "$cmdpath"; do
8 link="$(readlink "$cmdpath")"
9 cd "$(dirname "$cmdpath")"
12 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
14 exec "$script_home/bup-python" "$0" ${1+"$@"}
18 from __future__ import absolute_import, print_function
19 import errno, re, sys, os, subprocess, signal, getopt
21 if sys.version_info[0] != 2 \
22 and not os.environ.get('BUP_ALLOW_UNEXPECTED_PYTHON_VERSION') == 'true':
23 print('error: bup may crash with python versions other than 2, or eat your data',
27 from subprocess import PIPE
28 from sys import stderr, stdout
32 exe = os.path.realpath(argv[0])
33 exepath = os.path.split(exe)[0] or '.'
35 # fix the PYTHONPATH to include our lib dir
36 if os.path.exists("%s/../bup/." % exepath):
37 # Everything is relative to exepath (i.e. LIBDIR/cmd/)
39 libpath = os.path.join(exepath, '..')
40 resourcepath = libpath
42 # running from the src directory without being installed first
44 libpath = os.path.join(exepath, '../lib')
45 resourcepath = libpath
46 sys.path[:0] = [libpath]
47 os.environ['PYTHONPATH'] = libpath + ':' + os.environ.get('PYTHONPATH', '')
48 os.environ['BUP_MAIN_EXE'] = os.path.abspath(exe)
49 os.environ['BUP_RESOURCE_PATH'] = resourcepath
52 from bup import compat, helpers
53 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
54 from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width
58 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
59 '<command> [options...]\n\n')
61 ftp = 'Browse backup sets using an ftp-like client',
62 fsck = 'Check backup sets for damage and add redundancy information',
63 fuse = 'Mount your backup sets as a filesystem',
64 help = 'Print detailed help for the given command',
65 index = 'Create or display the index of files to back up',
66 on = 'Backup a remote machine to the local one',
67 restore = 'Extract files from a backup set',
68 save = 'Save files into a backup set (note: run "bup index" first)',
69 tag = 'Tag commits for easier access',
70 web = 'Launch a web server to examine backup sets',
73 log('Common commands:\n')
74 for cmd,synopsis in sorted(common.items()):
75 log(' %-10s %s\n' % (cmd, synopsis))
78 log('Other available commands:\n')
80 for c in sorted(os.listdir(cmdpath) + os.listdir(exepath)):
81 if c.startswith('bup-') and c.find('.') < 0:
83 if cname not in common:
85 log(columnate(cmds, ' '))
88 log("See 'bup help COMMAND' for more information on " +
89 "a specific command.\n")
98 # Handle global options.
100 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
101 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
102 except getopt.GetoptError as ex:
103 usage('error: %s' % ex.msg)
105 help_requested = None
108 for opt in global_args:
109 if opt[0] in ['-?', '--help']:
110 help_requested = True
111 elif opt[0] in ['-V', '--version']:
113 elif opt[0] in ['-D', '--debug']:
115 os.environ['BUP_DEBUG'] = str(helpers.buglvl)
116 elif opt[0] in ['--profile']:
118 elif opt[0] in ['-d', '--bup-dir']:
119 os.environ['BUP_DIR'] = opt[1]
121 usage('error: unexpected option "%s"' % opt[0])
123 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
124 if 'BUP_DIR' in os.environ:
125 os.environ['BUP_DIR'] = os.path.abspath(os.environ['BUP_DIR'])
133 if help_requested and subcmd[0] != 'help':
134 subcmd = ['help'] + subcmd
136 if len(subcmd) > 1 and subcmd[1] == '--help' and subcmd[0] != 'help':
137 subcmd = ['help', subcmd[0]] + subcmd[2:]
139 subcmd_name = subcmd[0]
144 sp = os.path.join(exepath, 'bup-%s' % s)
145 if not os.path.exists(sp):
146 sp = os.path.join(cmdpath, 'bup-%s' % s)
149 subcmd[0] = subpath(subcmd_name)
150 if not os.path.exists(subcmd[0]):
151 usage('error: unknown command "%s"' % subcmd_name)
153 already_fixed = atoi(os.environ.get('BUP_FORCE_TTY'))
154 if subcmd_name in ['mux', 'ftp', 'help']:
156 fix_stdout = not already_fixed and os.isatty(1)
157 fix_stderr = not already_fixed and os.isatty(2)
159 if fix_stdout or fix_stderr:
160 tty_env = merge_dict(os.environ,
161 {'BUP_FORCE_TTY': str((fix_stdout and 1 or 0)
162 + (fix_stderr and 2 or 0))})
167 sep_rx = re.compile(br'([\r\n])')
169 def print_clean_line(dest, content, width, sep=None):
170 """Write some or all of content, followed by sep, to the dest fd after
171 padding the content with enough spaces to fill the current
172 terminal width or truncating it to the terminal width if sep is a
175 assert sep in (b'\r', b'\n', None)
181 assert not sep_rx.match(x)
182 content = b''.join(content)
183 if sep == b'\r' and len(content) > width:
184 content = content[width:]
185 os.write(dest, content)
186 if len(content) < width:
187 os.write(dest, b' ' * (width - len(content)))
191 def filter_output(src_out, src_err, dest_out, dest_err):
192 """Transfer data from src_out to dest_out and src_err to dest_err via
193 print_clean_line until src_out and src_err close."""
195 assert not isinstance(src_out, bool)
196 assert not isinstance(src_err, bool)
197 assert not isinstance(dest_out, bool)
198 assert not isinstance(dest_err, bool)
199 assert src_out is not None or src_err is not None
200 assert (src_out is None) == (dest_out is None)
201 assert (src_err is None) == (dest_err is None)
205 fds = tuple([x for x in (src_out, src_err) if x is not None])
207 ready_fds, _, _ = select.select(fds, [], [])
210 buf = os.read(fd, 4096)
211 dest = dest_out if fd == src_out else dest_err
213 fds = tuple([x for x in fds if x is not fd])
214 print_clean_line(dest, pending.pop(fd, []), width)
216 split = sep_rx.split(buf)
217 while len(split) > 1:
218 content, sep = split[:2]
220 print_clean_line(dest,
221 pending.pop(fd, []) + [content],
224 assert(len(split) == 1)
226 pending.setdefault(fd, []).extend(split)
227 except BaseException as ex:
228 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
230 # Try to finish each of the streams
231 for fd, pending_items in compat.items(pending):
232 dest = dest_out if fd == src_out else dest_err
234 print_clean_line(dest, pending_items, width)
235 except (EnvironmentError, EOFError) as ex:
236 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
237 except BaseException as ex:
238 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
242 def run_subcmd(subcmd):
244 c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd
245 if not (fix_stdout or fix_stderr):
250 p = subprocess.Popen(c,
251 stdout=PIPE if fix_stdout else sys.stdout,
252 stderr=PIPE if fix_stderr else sys.stderr,
253 env=tty_env, bufsize=4096, close_fds=True)
254 # Assume p will receive these signals and quit, which will
255 # then cause us to quit.
256 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
257 signal.signal(sig, signal.SIG_IGN)
259 filter_output(fix_stdout and p.stdout.fileno() or None,
260 fix_stderr and p.stderr.fileno() or None,
261 fix_stdout and sys.stdout.fileno() or None,
262 fix_stderr and sys.stderr.fileno() or None)
264 except BaseException as ex:
267 if p and p.poll() == None:
268 os.kill(p.pid, signal.SIGTERM)
270 except BaseException as kill_ex:
271 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
274 wrap_main(lambda : run_subcmd(subcmd))