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