aboutsummaryrefslogtreecommitdiffstats
path: root/arg.py
blob: bb83df130d24ae1766f25c91a1a3776b5a40fadd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# See LICENSE file for copyright and license details.
# -*- coding: utf-8 -*-

from __future__ import print_function
import sys as _sys
import os as _os
import errno as _errno

MAY_HAVE_ATTACHED_ARGUMENT = 0
NEED_ARGUMENT = 1
NEED_ATTACHED_ARGUMENT = 2
NEED_DETACHED_ARGUMENT = 3
NEED_NO_ARGUMENT = 4

class UsageError(Exception):
    pass

class Parser(object):
    '''
    Command line parser

    Example usage:
        import sys
        import arg
        
        def usage():
            print('usage: %s [-v value] [-xy]' % sys.argv[0], file = sys.stderr)
            sys.exit(1)
        
        xflag = False
        yflag = False
        vflag = None
        
        parser = arg.Parser(usage = usage)
        for c in parser.flags:
            if c == 'x':
                xflag = True
            elif c == 'y':
                yflag = True
            elif c == 'v':
                vflag = parser.arg
            else:
                usage()
    '''

    def __init__(self, argv = None, symbols = '-', keep_dashdash = False, store_nonflags = False, usage = None):
        '''
        @param  argv           A list with the arguments to parse, if `None`, `sys.argv[1:]` is used
        @param  symbols        A string or list contain the characters that will cause an argument
                               to be parsed as being a flag, multicharacter strings will be ignored
        @param  keep_dashdash  If `True`, `--` will be returned in `self.argv`
        @param  keep_nonflags  During parsing, non-flag arguments are ignored, and after the last
                               flag has been parsed, `self.argv` will have the ignored arguments at
                               the beginning
        @param  usage          Function (without arguments) that is to be called if parsing fails
                               due to usage error, if `None`, `UsageError` will be raised instead
                               on usage error
        '''
        if argv is None:
            argv = _sys.argv[1:]
        self._argv = list(argv)
        self._usage_func = usage
        self._symbols = symbols
        self._keep_ddash = keep_dashdash
        self._stored = [] if store_nonflags else None

    def _usage(self):
        if self._usage_func is None:
            raise UsageError()
        else:
            self._usage_func()

    @property
    def flags(self):
        '''
        Return a generator that returns the flags in the command line
        
        Properties and functions in `self` can be used to after each
        time a flag is returned
        
        Each returned value is a single character
        '''
        self._brk = False
        self._accept_none_arg = False
        while len(self._argv):
            if len(self._argv[0]) < 2:
                if self._stored is not None:
                    self._stored.append(self._argv[0])
                    self._argv = self._argv[1:]
                    continue
                break
            self._symbol = self._argv[0][0]
            self._lflag = self._argv[0]
            if self._symbol not in self._symbols:
                if self._stored is not None:
                    self._stored.append(self._argv[0])
                    self._argv = self._argv[1:]
                    continue
                break
            if self._argv[0] == 2 * self._symbol:
                if self._symbol == '-':
                    if not self._keep_ddash:
                        self._argv = self._argv[1:]
                elif self._stored is not None:
                    self._stored.append(self._argv[0])
                    self._argv = self._argv[1:]
                    continue
                break
            self._i = 1
            n = len(self._argv[0])
            while self._i < n:
                self._flag = self._argv[0][self._i]
                if self._flag == self._symbol and self._i != 1:
                    self._usage()
                if self._i + 1 < n:
                    self._arg = self._argv[0][self._i + 1:]
                elif len(self._argv) > 1:
                    self._arg = self._argv[1]
                else:
                    self._arg = None
                yield self._flag
                self._lflag = None
                self._accept_none_arg = False
                if self._brk:
                    if len(self._argv) > 1 and self._arg is self._argv[1]:
                        self._argv = self._argv[1:]
                    self._brk = False
                    break
                self._i += 1
            self._argv = self._argv[1:]
        if self._stored:
            self._argv = self._stored + self._argv
        self._stored = None

    @property
    def flag(self):
        '''
        Return the current short flag, for example '-a',
        this string is always two characters long
        '''
        return self._symbol + self._flag

    @property
    def symbol(self):
        '''
        Get the first character in the flag, normally '-'
        '''
        return self._symbol

    @property
    def lflag(self):
        '''
        Get the entire current argument, for example
        '--mode=755', `None` not at the beginning
        '''
        return self._lflag

    @property
    def argv(self):
        '''
        Return a list of all remaining arguments
        '''
        return self._argv

    @property
    def argc(self):
        '''
        Return the number of remaining arguments
        '''
        return len(self._argv)

    @property
    def arg(self):
        '''
        Get the argument specified for the flag, can only be
        `None` (no argument) if `self.testlong` has returned
        `True` with `MAY_HAVE_ATTACHED_ARGUMENT` as the second
        argument for the currnet flag
        
        Reading this property will cause the parser to assume
        that the flag should have an argument, if these is
        no argument UsageError will be raised (or the specified
        usage function will be called) unless `self.testlong`
        has returned `True` with `MAY_HAVE_ATTACHED_ARGUMENT`
        as the second argument for the currnet flag
        '''
        if self._arg is None and not self._accept_none_arg:
            self._usage()
        self._brk = True
        return self._arg

    @property
    def arghere(self):
        '''
        Return the current argument with an offset such that the
        first character is the character associated with the
        current flag, for example, if the current argument is
        '-123' and the current flag is '1', '123' is returned
        
        Reading this property will cause the parser to continue
        with the next argument when getting the next flag
        '''
        self._brk = True
        self._arg = None
        return self._argv[0][self._i:]

    @property
    def isargnum(self):
        '''
        Check whether the value returned by `arghere` will be a number
        
        Calling this function does not affect the parser
        '''
        return self._argv[0][self._i:].isdigit()

    @property
    def argnum(self):
        '''
        Identical to `arghere`, except the returned value will
        be converted to an integer
        
        If the value returned by `self.arghere` is not a
        number, UsageError will be raised (or the specified
        usage function will be called)
        '''
        if not self.isargnum:
            self._usage()
        return int(self.arghere)

    def consume(self):
        '''
        Cause the parser to skip the rest of current argument
        and continue with the next argument
        
        If `self.arg` has been read and returned the next argument,
        the parser will still read that argument
        '''
        self._arg = None
        self._brk = True

    def testlong(self, flag, argument = 0):
        '''
        Check whether the current flag is specific long flag
        
        It is important to use this function, because it will
        set the parser's state appropriately whe it finds a match
        
        If the flag is the specified long flag, but its argument
        status does not match `argument`, UsageError will be raised
        (or the specified usage function will be called)
        
        @param  flag      The long flag, should start with its symbol prefix (usually '--')
        @param  argument  arg.MAY_HAVE_ATTACHED_ARGUMENT (default):
                                  The flag may have an argument attached to the flag
                                  separated by a '=', for example '--value=2', but
                                  it is not necessary, so just '--value' is also accepted,
                                  but the next argument may not be parsed as a value
                                  associated with the flag
                          arg.NEED_ARGUMENT:
                                  The flag must have an attached argument or a detached
                                  argument, for example '--value=2' is accepted, but
                                  '--value' is only accepted if it is not the last argument,
                                  if the current argument does not contain a '='
                          arg.NEED_ATTACHED_ARGUMENT:
                                  The flag must have an attached argument, for example
                                  '--value=2' is accepted but '--value' is not
                          arg.NEED_DETACHED_ARGUMENT:
                                  The flag must have a detached argument, for example
                                  '--value' is accepted, but only if it is not the last
                                  argument, but '--value=2' is not accepted
                          arg.NEED_NO_ARGUMENT:
                                  The flag must not have an argument, for example
                                  '--value' is accepted but '--value=2' is not
        '''
        if self._lflag is None:
            return False
        attached = self._lflag.startswith(flag + '=')
        if attached:
            arg = self._lflag[self._lflag.index('=') + 1:]
            lflag = self._lflag[:self._lflag.index('=')]
        else:
            arg = self._argv[1] if len(self._argv) > 1 else None
            lflag = self._lflag
        if lflag != flag:
            return False
        elif argument == MAY_HAVE_ATTACHED_ARGUMENT:
            if not attached:
                arg = None
                self._accept_none_arg = True
        elif argument == NEED_ARGUMENT:
            if arg is None:
                self._usage()
        elif argument == NEED_ATTACHED_ARGUMENT:
            if not attached:
                self._usage()
        elif argument == NEED_DETACHED_ARGUMENT:
            if attached or arg is None:
                self._usage()
        elif argument == NEED_NO_ARGUMENT:
            arg = None
            if attached:
                self._usage()
        else:
            raise OSError(_os.strerror(_errno.EINVAL), _errno.EINVAL)
        self._arg = arg
        self._brk = True
        return True