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