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