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