-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstringslator.py
More file actions
executable file
·699 lines (609 loc) · 25.8 KB
/
stringslator.py
File metadata and controls
executable file
·699 lines (609 loc) · 25.8 KB
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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
#!/usr/bin/env python3
import os
import sys
import sqlite3 # v3.3+
import plistlib
import codecs
import re
import argparse
scriptFilePath = os.path.realpath(__file__)
scriptRoot = os.path.abspath(os.path.join(scriptFilePath, os.pardir))
PATH_DB = os.path.join(scriptRoot, 'stringslator.db')
def main():
ARGSParser().parse()
# --------------------------------------------
#
# StringsDB
#
# --------------------------------------------
class StringsDB(object):
""" Communication with, and processing of stringslator db """
db = None
sql = None
def __init__(self):
super(StringsDB, self).__init__()
self.db = sqlite3.connect(PATH_DB)
self.sql = self.db.cursor()
self.createTablesIfNeeded()
def __del__(self):
self.db.commit()
self.db.close()
# --------------------------------------------
# stringslator API
# --------------------------------------------
def apiInfo(self, f_id, isComponent=False):
""" Return 3-tuple (dbFetchFile(), dbFetchComponents(), dbFetchCounts())
"""
if isComponent:
f_id = self.fetchFileFromComponent(f_id)
file = self.fetchFile(f_id)
if file is None:
return None, None, None
return file, self.fetchComponents(f_id), self.fetchCounts(f_id)
def apiSearch(self, term, titlesearch=False, langs=["en%"]):
""" Argument takes search term (use '%' for ambiguous matching).
If titlesearch = True, match all rows where title matches exactly.
Return array of tuples (f_id, c_id, l_id, title, translation)
"""
if titlesearch:
self.sql.execute("SELECT * FROM _trans WHERE key LIKE ?", [term])
else:
langIds = self.fetchLanguageIDs(langs)
lParam = ','.join('?' * len(langIds))
self.sql.execute('''SELECT * FROM _trans WHERE value LIKE ?
AND lid IN (%s)''' % lParam, [term] + langIds)
return self.sql.fetchall()
def apiExport(self, c_id, key):
""" Return array of tuples (lang, translation) """
self.sql.execute('''
SELECT l.name, t.value
FROM _trans t INNER JOIN _lang l ON l.id = t.lid
WHERE cid = ? and key = ?
ORDER BY l.name COLLATE NOCASE''', [c_id, key])
return self.sql.fetchall()
def apiAdd(self, path, recursive=False):
""" Add path to db by enumerating all string files (recursively) """
for resPath in enumerateResourcePaths(path, recursive):
self.insertResourceIntoDB(resPath)
def apiDelete(self, idOrPath, recursive=False):
""" Delete an application with given file-id or path.
Recursive = True is used for paths only.
"""
if idOrPath.isdigit():
file = self.fetchFile(idOrPath)
if not file:
print("id %s does not exist." % idOrPath)
else:
yield self.deleteFile(idOrPath), file[1]
else:
for f_id, name in self.fetchFileIdsWithPath(idOrPath, recursive):
yield self.deleteFile(f_id), name
def apiList(self, table, term=None):
""" table is either 'file', 'comp', 'lang', or 'title'
term is either row-id or name like %string%
"""
TB = {"file": "_file", "comp": "_comp", "lang": "_lang"}
if table not in TB:
return None
tbl = TB[table]
stmt = ""
if term:
if term.isdigit():
stmt = "WHERE id = %d" % int(term)
else:
stmt = "WHERE name like '%%%s%%'" % term
self.sql.execute('''SELECT id,name FROM %s %s
ORDER BY name COLLATE NOCASE''' % (tbl, stmt))
return self.sql.fetchall()
def apiListTitles(self, f_id):
""" Return a list of all keys for a given component-id """
self.sql.execute('''SELECT cid,key FROM _trans WHERE fid=? GROUP BY key
ORDER BY key COLLATE NOCASE''', [f_id])
return self.sql.fetchall()
# --------------------------------------------
# SQLite management helper
# --------------------------------------------
def createTablesIfNeeded(self):
""" Set schema if not already present """
self.sql.execute('''CREATE TABLE IF NOT EXISTS _file (
id integer NOT NULL PRIMARY KEY,
name text,
dir text
)''')
self.sql.execute('''CREATE TABLE IF NOT EXISTS _comp (
id integer NOT NULL PRIMARY KEY,
fid integer NOT NULL REFERENCES _file(id),
name text
)''')
self.sql.execute('''CREATE TABLE IF NOT EXISTS _lang (
id integer NOT NULL PRIMARY KEY,
name text
)''')
self.sql.execute('''CREATE TABLE IF NOT EXISTS _trans (
fid integer NOT NULL REFERENCES _file(id),
cid integer NOT NULL REFERENCES _comp(id),
lid integer NOT NULL REFERENCES _lang(id),
key text,
value text
)''')
def fetchIdForTable(self, table, cols=[], vals=[]):
""" Fetch row-id for given table, columns, and values """
if len(cols) != len(vals):
raise Exception("COLS and VALS are not of same length")
cols = [x + "=?" for x in cols]
self.sql.execute('SELECT id FROM %s WHERE %s' %
(table, " AND ".join(cols)), vals)
return self.sql.fetchone()
def insertIdIntoTable(self, table, cols=[], vals=[]):
""" Insert new row into table with given values """
if len(cols) != len(vals):
raise Exception("COLS and VALS are not of same length")
self.sql.execute('INSERT INTO %s(%s) VALUES (%s)' % (
table, ','.join(cols), ','.join('?' * len(vals))), vals)
return self.sql.lastrowid
def insertOrReturnRowID(self, table, cols, vals=[]):
""" Return tuple (row-id, didExistBeforeFlag) """
idn = self.fetchIdForTable(table, cols.split(','), vals)
if idn is None:
return self.insertIdIntoTable(table, cols.split(','), vals), False
else:
return idn[0], True
def insertFile(self, path, name):
""" Return row id of _file table. Insert new one if necessary. """
return self.insertOrReturnRowID('_file', 'name,dir', [name, path])
def insertComponent(self, f_id, comp):
""" Return row id of _comp table. Insert new one if necessary. """
return self.insertOrReturnRowID('_comp', 'fid,name', [f_id, comp])[0]
def insertLang(self, lang):
""" Return row id of _lang table. Insert new one if necessary. """
return self.insertOrReturnRowID('_lang', 'name', [lang])[0]
def fetchLanguageIDs(self, langs=[]):
""" Return list of ids matching provided langs array """
if not langs or type(langs) is not list or len(langs) == 0:
raise Exception("fetchLanguageIDs arg is not a list or empty")
self.sql.execute("SELECT id FROM _lang WHERE %s" %
" OR ".join(["name LIKE ?"] * len(langs)), langs)
return [x[0] for x in self.sql]
def fetchFileFromComponent(self, c_id):
""" Return file-id with given component-id """
self.sql.execute('SELECT fid FROM _comp WHERE id=?', [c_id])
return self.sql.fetchone()[0]
def fetchFile(self, f_id):
""" Return tuple (file-id, file-name, file-dir) """
self.sql.execute('SELECT id,name,dir FROM _file WHERE id=?', [f_id])
return self.sql.fetchone()
def fetchComponents(self, f_id):
""" Return array of tuple (compunent-id, component-name) """
self.sql.execute('''SELECT id,name FROM _comp WHERE fid=?
ORDER BY name''', [f_id])
return self.sql.fetchall()
def fetchCounts(self, f_id):
""" Return tuple [languages, translations, total] """
self.sql.execute('''SELECT count(*) FROM _trans WHERE fid=?
GROUP BY lid''', [f_id])
counts = [0, 0, 0]
for x in self.sql:
counts[0] += 1
counts[1] = max(x[0], counts[1])
counts[2] += x[0]
return counts
def fetchFileIdsWithPath(self, path, recursive=False):
""" Return (file-id, file-name) for matching rows.
If recursive = True also match all subdirectories.
"""
path = os.path.abspath(path)
if recursive:
path = "%s%%" % path
self.sql.execute('SELECT id, name FROM _file WHERE dir LIKE ?', [path])
return self.sql.fetchall()
def deleteFile(self, f_id):
""" Delete rows in _file, _comp, and _trans where f_id matches """
self.sql.execute('DELETE FROM _file WHERE id=?', [f_id])
self.sql.execute('DELETE FROM _comp WHERE fid=?', [f_id])
self.sql.execute('DELETE FROM _trans WHERE fid=?', [f_id])
return self.sql.rowcount # only translations are relevant
# --------------------------------------------
# Insert new application
# --------------------------------------------
def insertResourceIntoDB(self, path):
""" Parse 'Resources' folder and insert all localizable strings to db.
If path was processed before it will be skipped immediatelly.
"""
sfe = StringsFileEnumerator(self, path)
if not sfe.validPath:
print("ERROR: '%s' has no 'Resources' folder." % path)
return False
if sfe.existing:
print("skip existing. '%s'" % sfe.appName)
return False
print("processing '%s'" % sfe.appName)
langs, trns = sfe.processResourcesFolder()
if len(trns) > 0 and len(langs) > 1:
self.sql.executemany('INSERT INTO _trans VALUES (?,?,?,?,?)', trns)
self.db.commit()
print("added id %d '%s' (%d strings, %d languages)" %
(sfe.fid, sfe.appName, len(trns), len(langs)))
return True
else:
print("ignored, empty.")
self.db.rollback()
return False
# --------------------------------------------
#
# StringsFileEnumerator
#
# --------------------------------------------
class StringsFileEnumerator(object):
""" Helper to find all .strings files in directory 'resPath'.
Will return array of extracted languages and translations
"""
db = None
resPath = None
appName = None
fid = 0
existing = True
validPath = False
def __init__(self, stringsDB, path):
super(StringsFileEnumerator, self).__init__()
self.db = stringsDB
self.resPath = self.resourcesPathForPath(path)
if self.resPath is not None:
self.validPath = True
appPath = self.appDirForResourcePath(self.resPath)
self.appName = os.path.basename(appPath)
self.fid, self.existing = self.db.insertFile(appPath, self.appName)
# --------------------------------------------
# Process .strings files
# --------------------------------------------
def processResourcesFolder(self):
""" Enumerate .strings files for all languages (.lproj subfolders) """
translations = list()
languages = set()
for f1, localePath in self.enumerateWithExt(self.resPath, "lproj"):
l_id = self.db.insertLang(f1)
for f2, locFile in self.enumerateWithExt(localePath, "strings"):
languages.add(l_id)
c_id = self.db.insertComponent(self.fid, f2)
for key, val in self.processStringsFile(locFile):
translations.append([self.fid, c_id, l_id, key, val])
return languages, translations
def processStringsFile(self, stringsFile):
""" Parse strings file (try XML, then C-source) """
with open(stringsFile, 'rb') as fp:
try: # try XML format first
plist = plistlib.load(fp)
for key, val in self.parseStringsFileXML(plist):
yield key, val
return
except plistlib.InvalidFileException:
pass
try: # then try c-style formatting
for key, val in self.parseStringsFileCSource(stringsFile):
yield key, val
return
except Exception as e:
print("ERROR: Couldn't read plist '%s'" % stringsFile)
raise e
def parseStringsFileXML(self, xml, prefix=''):
""" Parse XML style strings file with nested dicts """
for key in xml:
val = xml[key]
if len(prefix) > 0:
key = "%s.%s" % (prefix, key)
if type(val) is dict:
for key2, val2 in self.parseStringsFileXML(val, prefix=key):
yield key2, val2
else:
yield key, val
def parseStringsFileCSource(self, filePath):
""" Parse C-source-code style strings file.
Regex will find assignments and ignore (block-)comments.
"""
prog = re.compile(r'(?:(?!\s*/\*)(.*?)=(.*?);)|(/\*)|(\*/)')
enc = self.findFileEncoding(filePath)
with open(filePath, 'r', encoding=enc) as fp:
content = fp.read()
blockComment = False
quotes = ["''", '""']
for key, val, cmntA, cmntB in prog.findall(content):
if cmntA:
blockComment = True
elif cmntB:
blockComment = False
elif not blockComment:
key = key.strip()
if key.startswith("//"): # single line comment
continue
val = val.strip()
if key[0] + key[-1] in quotes:
key = key[1:-1]
if val[0] + val[-1] in quotes:
val = val[1:-1]
yield key, val
def findFileEncoding(self, path):
""" Auto detect UTF-8/-16/-32 encoding with BOM """
with open(path, 'rb') as fp:
header = fp.read(4)
for bom, encoding in (
(codecs.BOM_UTF32_BE, "utf-32-be"),
(codecs.BOM_UTF32_LE, "utf-32-le"),
(codecs.BOM_UTF16_BE, "utf-16-be"),
(codecs.BOM_UTF16_LE, "utf-16-le"),
(codecs.BOM_UTF8, "utf-8")
):
if header.startswith(bom):
break
return encoding
# --------------------------------------------
# Folder properties & enumeration
# --------------------------------------------
def resourcesPathForPath(self, path):
""" Always navigate into '../Contents/Resources/' folder """
try:
actual = os.path.basename(os.path.normpath(path))
except Exception:
return None
if actual == "Resources":
newPath = path
elif actual == "Contents":
newPath = os.path.join(path, "Resources")
else:
newPath = os.path.join(path, "Contents", "Resources")
if os.path.exists(newPath):
return newPath
return None
def appDirForResourcePath(self, resPath):
""" Navigate to '../../' from Resources folder """
parent = os.path.normpath(resPath)
while os.path.basename(parent) in ["Contents", "Resources"]:
parent = os.path.abspath(os.path.join(parent, os.pardir))
return parent
def enumerateWithExt(self, resPath, extension):
""" Enumerate all files and folders in resPath with given extension """
for x in os.listdir(resPath):
f, e = os.path.splitext(x)
if e.endswith(extension):
yield f, os.path.join(resPath, x)
# --------------------------------------------
#
# UserIO
#
# --------------------------------------------
class UserIO(object):
""" Helper class for user CLI input / output """
def __init__(self):
super(UserIO, self).__init__()
# https://stackoverflow.com/a/3041990
def ask(self, question, default="yes"):
"""Ask a yes/no question via raw_input() and return their answer.
"question" is a string that is presented to the user.
"default" is the presumed answer if the user just hits <Enter>.
It must be "yes" (the default), "no" or None (meaning
an answer is required of the user).
The "answer" return value is True for "yes" or False for "no".
"""
valid = {"yes": True, "y": True, "ye": True,
"no": False, "n": False}
if default is None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
elif default == "no":
prompt = " [y/N] "
else:
raise ValueError("invalid default answer: '%s'" % default)
while True:
sys.stdout.write(question + prompt)
choice = input().lower()
if default is not None and choice == '':
return valid[default]
elif choice in valid:
return valid[choice]
else:
sys.stdout.write("Please respond with 'yes' or 'no' "
"(or 'y' or 'n').\n")
def printResults(self, arr, verbose=True):
""" Print formatted results dict or 'Nothing found' message.
If result array contains more than 100 entries, ask user beforehand.
"""
if arr is None or len(arr) == 0:
print(" Nothing found.")
return
# s = len(arr)
# if s > 100 and not ask("Found %d entries. Show complete list?" % s):
# return
if verbose:
print()
if len(arr[0]) == 2:
for i, n in arr:
print("%5d | %s" % (i, n))
elif len(arr[0]) == 5:
for f, c, l, key, value in arr:
value = value.replace('\n', '\\n')
print("%5d | %s --- ('%s')" % (c, value, key))
if verbose:
print("\n%d results.\n" % len(arr))
def printInfoForFile(self, file, components, counts):
print('Info for file:')
print(' id: %d' % file[0])
print(" name: '%s'" % file[1])
print(" path: '%s'" % file[2])
print('components:')
self.printResults(components, verbose=False)
print("localizable strings:")
print(" languages: %d" % counts[0])
print(" translations: %d" % counts[1])
print(" total: %d" % counts[2])
def printDeletingFiles(self, delFiles):
print("Deleting:")
for x in sorted(delFiles):
print(" - %s" % x)
print()
def enumerateResourcePaths(anyPath, recursive=False):
""" Find all subdirectories that contain '../Contents/Resources/'.
If recursive = False just yield anyPath.
"""
if recursive:
# if os.path.isdir(anyPath):
for x in os.walk(anyPath):
# make sure ../Contents/Resources/.. exists
if os.path.basename(x[0]) != "Contents":
continue
if "Resources" in x[1]:
yield x[0]
else:
yield anyPath
# --------------------------------------------
#
# ARGSParser
#
# --------------------------------------------
class ARGSParser(object):
""" Handle CLI parameter parsing and command calls """
parser = None
def __init__(self):
super(ARGSParser, self).__init__()
self.parser = self.initCLIParser(
[self.cli_add, self.cli_delete, self.cli_list,
self.cli_search, self.cli_export, self.cli_info])
self.parser.epilog = '''
examples:
{0} add -r '/System/' '/Applications/baRSS.app'
{0} search '% Update%'
{0} export 714 kWDLocPerfSignalGraphToolTip
run <command> -h to show help for command arguments'''.format(__file__)
# ------------------------------------------------------
# CLI interface
# ------------------------------------------------------
def cli_add(self, args):
""" Add new application to db """
if not args:
return 'add', ['a'], 'Add new application to db', {
'--recursive': (bool, 'Repeat for subdirectories'),
'path+': (str, '<path>', 'App or Resources directory'),
}
sdb = StringsDB()
for path in args.path:
sdb.apiAdd(path, args.recursive)
def cli_delete(self, args):
""" Delete application from db """
if not args:
return 'delete', ['rm'], 'Delete application from db', {
'--recursive': (bool, 'Delete apps in subdirectories as well'),
'path+': (str, '<file-id|path>', 'Row-id or application path'),
}
sdb = StringsDB()
Del = 0
delFiles = []
for path in args.path:
for changes, filename in sdb.apiDelete(path, args.recursive):
Del += changes
delFiles.append(filename)
if len(delFiles) == 0 or Del == 0:
print("Nothing to do.")
return
UserIO().printDeletingFiles(delFiles)
if not UserIO().ask("Deleting %d translations. Continue?" % Del, None):
sdb.db.rollback()
def cli_list(self, args):
""" List files, components, languages, keys """
if not args:
return 'list', ['ls'], 'List files, components, languages, keys', {
'mutually_exclusive': True,
'-f?': (str, '<term>', 'list files'),
'-c?': (str, '<term>', 'list components'),
'-l?': (str, '<term>', 'list languages'),
'-k': (int, '<file-id>', 'list translation keys'),
}
sdb = StringsDB()
if hasattr(args, 'k'):
UserIO().printResults(sdb.apiListTitles(args.k))
else:
for x, tbl in {'f': 'file', 'c': 'comp', 'l': 'lang'}.items():
if hasattr(args, x):
UserIO().printResults(sdb.apiList(tbl, getattr(args, x)))
def cli_search(self, args):
""" Search db for translation or title-key """
if not args:
return 'search', ['s'], 'Search db for translation or title-key', {
'--keys': (bool, 'search title-keys instead of translations'),
'term': (str, '<search-term>',
'Search pattern using %% and _ wildcards'),
}
sdb = StringsDB()
UserIO().printResults(sdb.apiSearch(
args.term, titlesearch=args.keys, langs=["en%", "de%", "Ger%"]))
def cli_export(self, args):
""" Export translations for title-key """
if not args:
return 'export', ['e'], 'Export translations for title-key', {
'id': (int, '<comp-id>', 'Row-id of a component'),
'key': (str, '<title-key>',
'Title-key within the same component'),
}
sdb = StringsDB()
for lang, text in sdb.apiExport(args.id, args.key):
print("%s|%s" % (lang, text))
def cli_info(self, args):
""" Display info for file-id or component-id """
if not args:
return 'info', ['i'], 'Display info for file-id or component-id', {
'id': (int, '<file-id|comp-id>',
'Row id of file or component (-c)'),
'--component': (bool, 'search component-id instead of file-id')
}
sdb = StringsDB()
app, components, counts = sdb.apiInfo(args.id, args.component)
if app is None:
print("\nFile id does not exist. Try search for an id:")
print(" %s list -f %%Finder%%\n" % os.path.basename(__file__))
else:
UserIO().printInfoForFile(app, components, counts)
# ------------------------------------------------------
# argparse stuff
# ------------------------------------------------------
def initCLIParser(self, methods):
""" Initialize argparse with commands from method dictionary """
parser = argparse.ArgumentParser(add_help=False)
parser.formatter_class = argparse.RawTextHelpFormatter
parser.set_defaults(func=lambda x: parser.print_help(sys.stderr))
subPrs = parser.add_subparsers(title='commands', metavar=" " * 13)
for fn in methods:
info = fn(None) # call function w/o params to get info dict
self.initCLICommand(subPrs, fn, *info)
parser.usage = "%(prog)s <command>"
return parser
def initCLICommand(self, parentParser, fn, name, alias, hlp, args):
""" Add new command (e.g., add, delete, ...) to parser """
cmd = parentParser.add_parser(name, aliases=alias, help=hlp)
cmd.set_defaults(func=fn)
if name == 'list':
cmd.epilog = '<term> can be either row-id or search string.'
for param, options in args.items():
if param.lower() == "mutually_exclusive":
cmd = cmd.add_mutually_exclusive_group(required=True)
continue
self.initCLICommandArgument(cmd, param, options)
def initCLICommandArgument(self, commandParser, param, options):
""" Add command argument (e.g., -k, path, ...) to given cli command """
args = {'help': options[-1]}
typ = options[0]
if param[-1] in ['?', '*', '+']:
args['nargs'] = param[-1]
param = param[:-1]
if typ == bool:
args['action'] = 'store_true'
else:
args['type'] = typ
args['metavar'] = options[1]
# make short form and prepare for unpacking
param = [param[1:3], param] if param.startswith('--') else [param]
opt = commandParser.add_argument(*param, **args)
if typ != bool:
opt.default = argparse.SUPPRESS
def parse(self):
""" Parse the args and call whatever function was selected """
args = self.parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()