]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
bup: don't convert bup_dir to bytes twice
[bup.git] / lib / cmd / bup
1 #!/bin/sh
2 """": # -*-python-*-
3 set -e
4 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
5 export "BUP_ARGV_0"="$0"
6 arg_i=1
7 for arg in "$@"; do
8     export "BUP_ARGV_${arg_i}"="$arg"
9     shift
10     arg_i=$((arg_i + 1))
11 done
12 # Here to end of preamble replaced during install
13 # Find our directory
14 top="$(pwd)"
15 cmdpath="$0"
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")"
20     cmdpath="$link"
21 done
22 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
23 cd "$top"
24 exec "$script_home/../../config/bin/python" "$0"
25 """
26 # end of bup preamble
27
28 from __future__ import absolute_import, print_function
29
30 import os, sys
31 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
32
33 from importlib import import_module
34 from subprocess import PIPE
35 import errno, getopt, os, re, select, signal, subprocess, sys
36
37 from bup import compat, path, helpers
38 from bup.compat import (
39     ModuleNotFoundError,
40     add_ex_ctx,
41     add_ex_tb,
42     argv_bytes,
43     environ,
44     fsdecode,
45     wrap_main
46 )
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
50
51
52 cmdpath = path.cmddir()
53
54 # Remove once we finish the internal command transition
55 transition_cmdpath = path.libdir() + b'/bup/cmd'
56
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.
60
61 def usage(msg=""):
62     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
63         '<command> [options...]\n\n')
64     common = dict(
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',
75     )
76
77     log('Common commands:\n')
78     for cmd,synopsis in sorted(common.items()):
79         log('    %-10s %s\n' % (cmd, synopsis))
80     log('\n')
81     
82     log('Other available commands:\n')
83     cmds = []
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, '    '))
90     log('\n')
91     
92     log("See 'bup help COMMAND' for more information on " +
93         "a specific command.\n")
94     if msg:
95         log("\n%s\n" % msg)
96     sys.exit(99)
97
98 argv = compat.argv
99 if len(argv) < 2:
100     usage()
101
102 # Handle global options.
103 try:
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)
108
109 subcmd = [argv_bytes(x) for x in subcmd]
110 help_requested = None
111 do_profile = False
112 bup_dir = None
113
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']:
120         helpers.buglvl += 1
121         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
122     elif opt[0] in ['--profile']:
123         do_profile = True
124     elif opt[0] in ['-d', '--bup-dir']:
125         bup_dir = argv_bytes(opt[1])
126     else:
127         usage('error: unexpected option "%s"' % opt[0])
128
129 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
130 if bup_dir:
131     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
132
133 if len(subcmd) == 0:
134     if help_requested:
135         subcmd = [b'help']
136     else:
137         usage()
138
139 if help_requested and subcmd[0] != b'help':
140     subcmd = [b'help'] + subcmd
141
142 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
143     subcmd = [b'help', subcmd[0]] + subcmd[2:]
144
145 subcmd_name = subcmd[0]
146 if not subcmd_name:
147     usage()
148
149 try:
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:
155     cmd_module = None
156
157 if not cmd_module:
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))
164
165 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
166 if subcmd_name in [b'mux', b'ftp', b'help']:
167     already_fixed = True
168 fix_stdout = not already_fixed and os.isatty(1)
169 fix_stderr = not already_fixed and os.isatty(2)
170
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(), })
177 else:
178     tty_env = environ
179
180
181 sep_rx = re.compile(br'([\r\n])')
182
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
187     carriage return."""
188     global sep_rx
189     assert sep in (b'\r', b'\n', None)
190     if not content:
191         if sep:
192             os.write(dest, sep)
193         return
194     for x in content:
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)))
202     if sep:
203         os.write(dest, sep)
204
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."""
208     global sep_rx
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)
216     pending = {}
217     pending_ex = None
218     try:
219         fds = tuple([x for x in (src_out, src_err) if x is not None])
220         while fds:
221             ready_fds, _, _ = select.select(fds, [], [])
222             width = tty_width()
223             for fd in ready_fds:
224                 buf = os.read(fd, 4096)
225                 dest = dest_out if fd == src_out else dest_err
226                 if not buf:
227                     fds = tuple([x for x in fds if x is not fd])
228                     print_clean_line(dest, pending.pop(fd, []), width)
229                 else:
230                     split = sep_rx.split(buf)
231                     while len(split) > 1:
232                         content, sep = split[:2]
233                         split = split[2:]
234                         print_clean_line(dest,
235                                          pending.pop(fd, []) + [content],
236                                          width,
237                                          sep)
238                     assert(len(split) == 1)
239                     if split[0]:
240                         pending.setdefault(fd, []).extend(split)
241     except BaseException as ex:
242         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
243     try:
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
247             try:
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)
253     if pending_ex:
254         raise pending_ex
255
256
257 def run_subcmd(module, args):
258
259     if module:
260         return module.main(args)
261
262     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
263     if not (fix_stdout or fix_stderr):
264         os.execvp(c[0], c)
265
266     sys.stdout.flush()
267     sys.stderr.flush()
268     out = byte_stream(sys.stdout)
269     err = byte_stream(sys.stderr)
270     p = None
271     try:
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)
280
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)
285         return p.wait()
286     except BaseException as ex:
287         add_ex_tb(ex)
288         try:
289             if p and p.poll() == None:
290                 os.kill(p.pid, signal.SIGTERM)
291                 p.wait()
292         except BaseException as kill_ex:
293             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
294         raise ex
295
296 wrap_main(lambda : run_subcmd(cmd_module, subcmd))