]> arthur.barton.de Git - bup.git/blob - lib/bup/options.py
options.py: get the real tty width for word wrapping purposes.
[bup.git] / lib / bup / options.py
1 """Command-line options parser.
2 With the help of an options spec string, easily parse command-line options.
3 """
4 import sys, os, textwrap, getopt, re, struct
5
6 class OptDict:
7     def __init__(self):
8         self._opts = {}
9
10     def __setitem__(self, k, v):
11         if k.startswith('no-') or k.startswith('no_'):
12             k = k[3:]
13             v = not v
14         self._opts[k] = v
15
16     def __getitem__(self, k):
17         if k.startswith('no-') or k.startswith('no_'):
18             return not self._opts[k[3:]]
19         return self._opts[k]
20
21     def __getattr__(self, k):
22         return self[k]
23
24
25 def _default_onabort(msg):
26     sys.exit(97)
27
28
29 def _intify(v):
30     try:
31         vv = int(v or '')
32         if str(vv) == v:
33             return vv
34     except ValueError:
35         pass
36     return v
37
38
39 def _atoi(v):
40     try:
41         return int(v or 0)
42     except ValueError:
43         return 0
44
45
46 def _remove_negative_kv(k, v):
47     if k.startswith('no-') or k.startswith('no_'):
48         return k[3:], not v
49     return k,v
50
51 def _remove_negative_k(k):
52     return _remove_negative_kv(k, None)[0]
53
54
55 def _tty_width():
56     s = struct.pack("HHHH", 0, 0, 0, 0)
57     try:
58         import fcntl, termios
59         s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
60     except (IOError, ImportError):
61         return _atoi(os.environ.get('WIDTH')) or 70
62     (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
63     return xsize
64
65
66 class Options:
67     """Option parser.
68     When constructed, two strings are mandatory. The first one is the command
69     name showed before error messages. The second one is a string called an
70     optspec that specifies the synopsis and option flags and their description.
71     For more information about optspecs, consult the bup-options(1) man page.
72
73     Two optional arguments specify an alternative parsing function and an
74     alternative behaviour on abort (after having output the usage string).
75
76     By default, the parser function is getopt.gnu_getopt, and the abort
77     behaviour is to exit the program.
78     """
79     def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt,
80                  onabort=_default_onabort):
81         self.exe = exe
82         self.optspec = optspec
83         self._onabort = onabort
84         self.optfunc = optfunc
85         self._aliases = {}
86         self._shortopts = 'h?'
87         self._longopts = ['help']
88         self._hasparms = {}
89         self._defaults = {}
90         self._usagestr = self._gen_usage()
91
92     def _gen_usage(self):
93         out = []
94         lines = self.optspec.strip().split('\n')
95         lines.reverse()
96         first_syn = True
97         while lines:
98             l = lines.pop()
99             if l == '--': break
100             out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
101             first_syn = False
102         out.append('\n')
103         last_was_option = False
104         while lines:
105             l = lines.pop()
106             if l.startswith(' '):
107                 out.append('%s%s\n' % (last_was_option and '\n' or '',
108                                        l.lstrip()))
109                 last_was_option = False
110             elif l:
111                 (flags, extra) = l.split(' ', 1)
112                 extra = extra.strip()
113                 if flags.endswith('='):
114                     flags = flags[:-1]
115                     has_parm = 1
116                 else:
117                     has_parm = 0
118                 g = re.search(r'\[([^\]]*)\]$', extra)
119                 if g:
120                     defval = g.group(1)
121                 else:
122                     defval = None
123                 flagl = flags.split(',')
124                 flagl_nice = []
125                 for f in flagl:
126                     f,dvi = _remove_negative_kv(f, _intify(defval))
127                     self._aliases[f] = _remove_negative_k(flagl[0])
128                     self._hasparms[f] = has_parm
129                     self._defaults[f] = dvi
130                     if len(f) == 1:
131                         self._shortopts += f + (has_parm and ':' or '')
132                         flagl_nice.append('-' + f)
133                     else:
134                         f_nice = re.sub(r'\W', '_', f)
135                         self._aliases[f_nice] = _remove_negative_k(flagl[0])
136                         self._longopts.append(f + (has_parm and '=' or ''))
137                         self._longopts.append('no-' + f)
138                         flagl_nice.append('--' + f)
139                 flags_nice = ', '.join(flagl_nice)
140                 if has_parm:
141                     flags_nice += ' ...'
142                 prefix = '    %-20s  ' % flags_nice
143                 argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
144                                                 initial_indent=prefix,
145                                                 subsequent_indent=' '*28))
146                 out.append(argtext + '\n')
147                 last_was_option = True
148             else:
149                 out.append('\n')
150                 last_was_option = False
151         return ''.join(out).rstrip() + '\n'
152
153     def usage(self, msg=""):
154         """Print usage string to stderr and abort."""
155         sys.stderr.write(self._usagestr)
156         e = self._onabort and self._onabort(msg) or None
157         if e:
158             raise e
159
160     def fatal(self, s):
161         """Print an error message to stderr and abort with usage string."""
162         msg = 'error: %s\n' % s
163         sys.stderr.write(msg)
164         return self.usage(msg)
165
166     def parse(self, args):
167         """Parse a list of arguments and return (options, flags, extra).
168
169         In the returned tuple, "options" is an OptDict with known options,
170         "flags" is a list of option flags that were used on the command-line,
171         and "extra" is a list of positional arguments.
172         """
173         try:
174             (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
175         except getopt.GetoptError, e:
176             self.fatal(e)
177
178         opt = OptDict()
179
180         for k,v in self._defaults.iteritems():
181             k = self._aliases[k]
182             opt[k] = v
183
184         for (k,v) in flags:
185             k = k.lstrip('-')
186             if k in ('h', '?', 'help'):
187                 self.usage()
188             if k.startswith('no-'):
189                 k = self._aliases[k[3:]]
190                 v = 0
191             else:
192                 k = self._aliases[k]
193                 if not self._hasparms[k]:
194                     assert(v == '')
195                     v = (opt._opts.get(k) or 0) + 1
196                 else:
197                     v = _intify(v)
198             opt[k] = v
199         for (f1,f2) in self._aliases.iteritems():
200             opt[f1] = opt._opts.get(f2)
201         return (opt,flags,extra)