]> arthur.barton.de Git - bup.git/blob - cmd/bup
metadata: accept only fixed python-xattr in python3
[bup.git] / cmd / bup
1 #!/bin/sh
2 """": # -*-python-*- # -*-python-*-
3 set -e
4 top="$(pwd)"
5 cmdpath="$0"
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")"
10     cmdpath="$link"
11 done
12 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
13 cd "$top"
14 exec "$script_home/bup-python" "$0" ${1+"$@"}
15 """
16 # end of bup preamble
17
18 from __future__ import absolute_import, print_function
19 import errno, getopt, os, re, select, signal, subprocess, sys
20 from subprocess import PIPE
21
22 from bup.compat import environ, restore_lc_env
23
24 if sys.version_info[0] != 2 \
25    and not environ.get(b'BUP_ALLOW_UNEXPECTED_PYTHON_VERSION') == b'true':
26     print('error: bup may crash with python versions other than 2, or eat your data',
27           file=sys.stderr)
28     sys.exit(2)
29
30 restore_lc_env()
31
32 from bup import compat, path, helpers
33 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
34 from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width
35 from bup.io import byte_stream
36
37 cmdpath = path.cmddir()
38
39 def usage(msg=""):
40     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
41         '<command> [options...]\n\n')
42     common = dict(
43         ftp = 'Browse backup sets using an ftp-like client',
44         fsck = 'Check backup sets for damage and add redundancy information',
45         fuse = 'Mount your backup sets as a filesystem',
46         help = 'Print detailed help for the given command',
47         index = 'Create or display the index of files to back up',
48         on = 'Backup a remote machine to the local one',
49         restore = 'Extract files from a backup set',
50         save = 'Save files into a backup set (note: run "bup index" first)',
51         tag = 'Tag commits for easier access',
52         web = 'Launch a web server to examine backup sets',
53     )
54
55     log('Common commands:\n')
56     for cmd,synopsis in sorted(common.items()):
57         log('    %-10s %s\n' % (cmd, synopsis))
58     log('\n')
59     
60     log('Other available commands:\n')
61     cmds = []
62     for c in sorted(os.listdir(cmdpath)):
63         if c.startswith('bup-') and c.find('.') < 0:
64             cname = c[4:]
65             if cname not in common:
66                 cmds.append(c[4:])
67     log(columnate(cmds, '    '))
68     log('\n')
69     
70     log("See 'bup help COMMAND' for more information on " +
71         "a specific command.\n")
72     if msg:
73         log("\n%s\n" % msg)
74     sys.exit(99)
75
76
77 if len(sys.argv) < 2:
78     usage()
79
80 # Handle global options.
81 try:
82     optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
83     global_args, subcmd = getopt.getopt(sys.argv[1:], '?VDd:', optspec)
84 except getopt.GetoptError as ex:
85     usage('error: %s' % ex.msg)
86
87 help_requested = None
88 do_profile = False
89
90 for opt in global_args:
91     if opt[0] in ['-?', '--help']:
92         help_requested = True
93     elif opt[0] in ['-V', '--version']:
94         subcmd = ['version']
95     elif opt[0] in ['-D', '--debug']:
96         helpers.buglvl += 1
97         os.environ['BUP_DEBUG'] = str(helpers.buglvl)
98     elif opt[0] in ['--profile']:
99         do_profile = True
100     elif opt[0] in ['-d', '--bup-dir']:
101         os.environ['BUP_DIR'] = opt[1]
102     else:
103         usage('error: unexpected option "%s"' % opt[0])
104
105 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
106 if 'BUP_DIR' in os.environ:
107     os.environ['BUP_DIR'] = os.path.abspath(os.environ['BUP_DIR'])
108
109 if len(subcmd) == 0:
110     if help_requested:
111         subcmd = ['help']
112     else:
113         usage()
114
115 if help_requested and subcmd[0] != 'help':
116     subcmd = ['help'] + subcmd
117
118 if len(subcmd) > 1 and subcmd[1] == '--help' and subcmd[0] != 'help':
119     subcmd = ['help', subcmd[0]] + subcmd[2:]
120
121 subcmd_name = subcmd[0]
122 if not subcmd_name:
123     usage()
124
125 def subpath(s):
126     return os.path.join(cmdpath, 'bup-%s' % s)
127
128 subcmd[0] = subpath(subcmd_name)
129 if not os.path.exists(subcmd[0]):
130     usage('error: unknown command "%s"' % subcmd_name)
131
132 already_fixed = atoi(os.environ.get('BUP_FORCE_TTY'))
133 if subcmd_name in ['mux', 'ftp', 'help']:
134     already_fixed = True
135 fix_stdout = not already_fixed and os.isatty(1)
136 fix_stderr = not already_fixed and os.isatty(2)
137
138 if fix_stdout or fix_stderr:
139     tty_env = merge_dict(os.environ,
140                          {'BUP_FORCE_TTY': str((fix_stdout and 1 or 0)
141                                                + (fix_stderr and 2 or 0))})
142 else:
143     tty_env = os.environ
144
145
146 sep_rx = re.compile(br'([\r\n])')
147
148 def print_clean_line(dest, content, width, sep=None):
149     """Write some or all of content, followed by sep, to the dest fd after
150     padding the content with enough spaces to fill the current
151     terminal width or truncating it to the terminal width if sep is a
152     carriage return."""
153     global sep_rx
154     assert sep in (b'\r', b'\n', None)
155     if not content:
156         if sep:
157             os.write(dest, sep)
158         return
159     for x in content:
160         assert not sep_rx.match(x)
161     content = b''.join(content)
162     if sep == b'\r' and len(content) > width:
163         content = content[width:]
164     os.write(dest, content)
165     if len(content) < width:
166         os.write(dest, b' ' * (width - len(content)))
167     if sep:
168         os.write(dest, sep)
169
170 def filter_output(src_out, src_err, dest_out, dest_err):
171     """Transfer data from src_out to dest_out and src_err to dest_err via
172     print_clean_line until src_out and src_err close."""
173     global sep_rx
174     assert not isinstance(src_out, bool)
175     assert not isinstance(src_err, bool)
176     assert not isinstance(dest_out, bool)
177     assert not isinstance(dest_err, bool)
178     assert src_out is not None or src_err is not None
179     assert (src_out is None) == (dest_out is None)
180     assert (src_err is None) == (dest_err is None)
181     pending = {}
182     pending_ex = None
183     try:
184         fds = tuple([x for x in (src_out, src_err) if x is not None])
185         while fds:
186             ready_fds, _, _ = select.select(fds, [], [])
187             width = tty_width()
188             for fd in ready_fds:
189                 buf = os.read(fd, 4096)
190                 dest = dest_out if fd == src_out else dest_err
191                 if not buf:
192                     fds = tuple([x for x in fds if x is not fd])
193                     print_clean_line(dest, pending.pop(fd, []), width)
194                 else:
195                     split = sep_rx.split(buf)
196                     while len(split) > 1:
197                         content, sep = split[:2]
198                         split = split[2:]
199                         print_clean_line(dest,
200                                          pending.pop(fd, []) + [content],
201                                          width,
202                                          sep)
203                     assert(len(split) == 1)
204                     if split[0]:
205                         pending.setdefault(fd, []).extend(split)
206     except BaseException as ex:
207         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
208     try:
209         # Try to finish each of the streams
210         for fd, pending_items in compat.items(pending):
211             dest = dest_out if fd == src_out else dest_err
212             try:
213                 print_clean_line(dest, pending_items, width)
214             except (EnvironmentError, EOFError) as ex:
215                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
216     except BaseException as ex:
217         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
218     if pending_ex:
219         raise pending_ex
220
221 def run_subcmd(subcmd):
222
223     c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd
224     if not (fix_stdout or fix_stderr):
225         os.execvp(c[0], c)
226
227     sys.stdout.flush()
228     sys.stderr.flush()
229     out = byte_stream(sys.stdout)
230     err = byte_stream(sys.stderr)
231     p = None
232     try:
233         p = subprocess.Popen(c,
234                              stdout=PIPE if fix_stdout else out,
235                              stderr=PIPE if fix_stderr else err,
236                              env=tty_env, bufsize=4096, close_fds=True)
237         # Assume p will receive these signals and quit, which will
238         # then cause us to quit.
239         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
240             signal.signal(sig, signal.SIG_IGN)
241
242         filter_output(fix_stdout and p.stdout.fileno() or None,
243                       fix_stderr and p.stderr.fileno() or None,
244                       fix_stdout and out.fileno() or None,
245                       fix_stderr and err.fileno() or None)
246         return p.wait()
247     except BaseException as ex:
248         add_ex_tb(ex)
249         try:
250             if p and p.poll() == None:
251                 os.kill(p.pid, signal.SIGTERM)
252                 p.wait()
253         except BaseException as kill_ex:
254             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
255         raise ex
256         
257 wrap_main(lambda : run_subcmd(subcmd))