Commit 386557e646a97464c713aa6ee607008c85f37aad
1 parent
aaf9bcef
Added support for git filter and inflate --all.
Showing
2 changed files
with
139 additions
and
54 deletions
blimp.nim
1 | -import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | 1 | +import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes, tables |
2 | 2 | ||
3 | # blimp is a little utility program for handling large files | 3 | # blimp is a little utility program for handling large files |
4 | # in git repositories. Its inspired by git-fat and s3annex | 4 | # in git repositories. Its inspired by git-fat and s3annex |
@@ -11,7 +11,7 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | @@ -11,7 +11,7 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | ||
11 | # Use "blimp i mybigfile" to inflate it back to original size. | 11 | # Use "blimp i mybigfile" to inflate it back to original size. |
12 | # | 12 | # |
13 | # When deflated the file only has: | 13 | # When deflated the file only has: |
14 | -# "hash:" <filename> + <an md5sum> | 14 | +# "blimphash:" <filename> + <an md5sum> |
15 | # ...inside it. | 15 | # ...inside it. |
16 | # | 16 | # |
17 | # The file is copied over to a local dir: | 17 | # The file is copied over to a local dir: |
@@ -30,14 +30,14 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | @@ -30,14 +30,14 @@ import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes | ||
30 | 30 | ||
31 | const | 31 | const |
32 | versionMajor* = 0 | 32 | versionMajor* = 0 |
33 | - versionMinor* = 2 | ||
34 | - versionPatch* = 1 | 33 | + versionMinor* = 3 |
34 | + versionPatch* = 0 | ||
35 | versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch | 35 | versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch |
36 | 36 | ||
37 | var | 37 | var |
38 | blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword, blimpVersion: string = nil | 38 | blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword, blimpVersion: string = nil |
39 | homeDir, currentDir, gitRootDir: string | 39 | homeDir, currentDir, gitRootDir: string |
40 | - verbose, stdio: bool | 40 | + verbose, stdio, inflateAll: bool |
41 | stdinContent: string = nil | 41 | stdinContent: string = nil |
42 | 42 | ||
43 | let | 43 | let |
@@ -72,10 +72,11 @@ delete = "rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore- | @@ -72,10 +72,11 @@ delete = "rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore- | ||
72 | # version = 0.2 | 72 | # version = 0.2 |
73 | """ | 73 | """ |
74 | 74 | ||
75 | -# Find git root dir or nil | ||
76 | -proc gitRoot(): string = | 75 | + |
76 | + | ||
77 | +proc cmd(cmd: string): string = | ||
77 | try: | 78 | try: |
78 | - let tup = execCmdEx("git rev-parse --show-toplevel") | 79 | + let tup = execCmdEx(cmd) |
79 | if tup[1] == 0: | 80 | if tup[1] == 0: |
80 | result = strip(tup[0]) | 81 | result = strip(tup[0]) |
81 | else: | 82 | else: |
@@ -83,6 +84,29 @@ proc gitRoot(): string = | @@ -83,6 +84,29 @@ proc gitRoot(): string = | ||
83 | except: | 84 | except: |
84 | result = nil | 85 | result = nil |
85 | 86 | ||
87 | +# Find git root dir or nil | ||
88 | +proc gitRoot(): string = | ||
89 | + cmd("git rev-parse --show-toplevel") | ||
90 | + | ||
91 | +# Git config | ||
92 | +proc gitConfigSet(key, val: string) = | ||
93 | + discard cmd("git config " & $key & " " & $val) | ||
94 | + | ||
95 | +proc gitConfigGet(key: string): string = | ||
96 | + cmd("git config --get " & $key) | ||
97 | + | ||
98 | +# Set blimp filter | ||
99 | +proc setBlimpFilter() = | ||
100 | + gitConfigSet("filter.blimp.clean", "\"blimp -s d %f\"") | ||
101 | + gitConfigSet("filter.blimp.smudge", "\"blimp -s i %f\"") | ||
102 | + | ||
103 | + | ||
104 | +# Ensure the filter is set | ||
105 | +proc ensureBlimpFilter() = | ||
106 | + if gitConfigGet("filter.blimp.clean") != "": | ||
107 | + setBlimpFilter() | ||
108 | + | ||
109 | + | ||
86 | # Simple expansion of %home%, %cwd% and %gitroot% | 110 | # Simple expansion of %home%, %cwd% and %gitroot% |
87 | proc expandDirs(templ: string): string = | 111 | proc expandDirs(templ: string): string = |
88 | result = templ.replace("%home%", homeDir) | 112 | result = templ.replace("%home%", homeDir) |
@@ -96,8 +120,7 @@ proc parseConfFile(filename: string) = | @@ -96,8 +120,7 @@ proc parseConfFile(filename: string) = | ||
96 | var f = newFileStream(filename, fmRead) | 120 | var f = newFileStream(filename, fmRead) |
97 | if f != nil: | 121 | if f != nil: |
98 | if verbose: echo "Reading config: " & filename | 122 | if verbose: echo "Reading config: " & filename |
99 | - var p: CfgParser | ||
100 | - | 123 | + var p: CfgParser |
101 | open(p, f, filename) | 124 | open(p, f, filename) |
102 | while true: | 125 | while true: |
103 | var e = next(p) | 126 | var e = next(p) |
@@ -200,8 +223,8 @@ proc deleteFromBlimpStore(blimpFilename, filename: string) = | @@ -200,8 +223,8 @@ proc deleteFromBlimpStore(blimpFilename, filename: string) = | ||
200 | remoteDeleteFile(blimpFilename) | 223 | remoteDeleteFile(blimpFilename) |
201 | 224 | ||
202 | proc blimpFileNameFromString(line: string): string = | 225 | proc blimpFileNameFromString(line: string): string = |
203 | - let hashline = split(strip(line), {':'}) | ||
204 | - if hashline[0] == "hash": | 226 | + let hashline = split(strip(line), ':') |
227 | + if hashline[0] == "blimphash": | ||
205 | result = hashline[1] | 228 | result = hashline[1] |
206 | else: | 229 | else: |
207 | result = nil | 230 | result = nil |
@@ -236,11 +259,18 @@ proc deflate(filename: string) = | @@ -236,11 +259,18 @@ proc deflate(filename: string) = | ||
236 | blimpFilename = computeBlimpFilename(filename) | 259 | blimpFilename = computeBlimpFilename(filename) |
237 | copyToBlimpStore(filename, blimpFilename) | 260 | copyToBlimpStore(filename, blimpFilename) |
238 | if stdio: | 261 | if stdio: |
239 | - write(stdout, "hash:" & blimpFilename) | 262 | + write(stdout, "blimphash:" & blimpFilename) |
240 | else: | 263 | else: |
241 | - writeFile(filename, "hash:" & blimpFilename) | 264 | + writeFile(filename, "blimphash:" & blimpFilename) |
242 | if verbose: echo("\t" & filename & " deflated.") | 265 | if verbose: echo("\t" & filename & " deflated.") |
243 | 266 | ||
267 | +# Iterator over all deflated files in the git clone | ||
268 | +iterator allDeflated() = | ||
269 | + let filenames = cmd("git ls-files " & gitRootDir).split('\l') | ||
270 | + for fn in filenames: | ||
271 | + if not blimpFilename(fn).isNil: | ||
272 | + yield fn | ||
273 | + | ||
244 | # Parse out hash from hash stub and copy back original content from blimpStore. | 274 | # Parse out hash from hash stub and copy back original content from blimpStore. |
245 | proc inflate(filename: string) = | 275 | proc inflate(filename: string) = |
246 | if verbose: echo "Inflating " & filename | 276 | if verbose: echo "Inflating " & filename |
@@ -290,41 +320,45 @@ proc dumpConfig() = | @@ -290,41 +320,45 @@ proc dumpConfig() = | ||
290 | echo "\tblimpVersion: " & $blimpVersion | 320 | echo "\tblimpVersion: " & $blimpVersion |
291 | echo "\n" | 321 | echo "\n" |
292 | 322 | ||
293 | -let help = """ | 323 | +let synopsis = """ |
294 | blimp [options] <command> <filenames...> | 324 | blimp [options] <command> <filenames...> |
295 | -h,--help Show this | 325 | -h,--help Show this |
296 | --version Show version of blimp | 326 | --version Show version of blimp |
297 | -v,--verbose Verbosity, only works without -s | 327 | -v,--verbose Verbosity, only works without -s |
298 | - -s,--stdio If given, use stdin/stdout for content. | 328 | + -i,--init Set blimp filter in git config |
329 | + ---------- | ||
299 | <command> (string) (d)eflate, (i)nflate, remove | 330 | <command> (string) (d)eflate, (i)nflate, remove |
300 | - <filenames> (string...) One or more filepaths to inflate/deflate | ||
301 | - | 331 | + -a,--all Operate on all deflated files in clone |
332 | + ---------- | ||
333 | + -s,--stdio If given, use stdin/stdout for content. | ||
334 | + <filenames> (string...) One or more filepaths to inflate/deflate | ||
335 | +""" | ||
336 | +let help = """ | ||
302 | blimp is a little utility program for handling large files | 337 | blimp is a little utility program for handling large files |
303 | in git repositories. Its inspired by git-fat and s3annex | 338 | in git repositories. Its inspired by git-fat and s3annex |
304 | but doesn't rely on S3 for storage - it uses rsync like git-fat. | 339 | but doesn't rely on S3 for storage - it uses rsync like git-fat. |
305 | - It is a single binary without any dependencies. | 340 | + It is a single binary without any dependencies. Its not as advanced |
341 | + as git-fat but basically does the same thing. | ||
306 | 342 | ||
307 | Manual use: | 343 | Manual use: |
308 | 344 | ||
309 | - Use "blimp d mybigfile" to deflate it before commit. | ||
310 | - Use "blimp i mybigfile" to inflate it back to original size. | 345 | + Use "blimp d mybigfile" to deflate a file, typically before commit. |
346 | + Use "blimp i mybigfile" to inflate it back to original content. | ||
311 | 347 | ||
312 | When deflated the file only has: | 348 | When deflated the file only has: |
313 | - "hash:" <filename> "-" <md5sum> | 349 | + "blimphash:" <filename> "-" <md5sum> |
314 | ...inside it. | 350 | ...inside it. |
315 | 351 | ||
316 | - Deflate is run before you add the big file to the index for committing. | ||
317 | - Deflate will replace the file contents with a hash, and copy the | ||
318 | - real content to your local blimpstore: | 352 | + Deflate also copies the real content to your local blimpstore: |
319 | 353 | ||
320 | - "blimpstore"/<filename>-<md5sum> | 354 | + <blimpstore>/<filename>-<md5sum> |
321 | 355 | ||
322 | - ...and if configured also upload it to "remote", using rsync. | 356 | + ...and if configured also uploads it to "remote", using rsync. |
323 | 357 | ||
324 | Configuration is in these locations in order: | 358 | Configuration is in these locations in order: |
325 | 359 | ||
326 | ./.blimp.conf | 360 | ./.blimp.conf |
327 | - "gitroot"/.blimp.conf | 361 | + <gitroot>/.blimp.conf |
328 | ~/<blimpstore>/.blimp.conf | 362 | ~/<blimpstore>/.blimp.conf |
329 | ~/.blimp.conf | 363 | ~/.blimp.conf |
330 | 364 | ||
@@ -337,18 +371,40 @@ let help = """ | @@ -337,18 +371,40 @@ let help = """ | ||
337 | synced with a master rsync repository that is typically shared. | 371 | synced with a master rsync repository that is typically shared. |
338 | 372 | ||
339 | Inflate will bring back the original content by copying from | 373 | Inflate will bring back the original content by copying from |
340 | - your local blimpstore, and if its not there, first downloading from the remote. | 374 | + your local blimpstore, and if its not there, first download from the remote. |
341 | Use this whenever you need to work/edit the big file - in order to get | 375 | Use this whenever you need to work/edit the big file - in order to get |
342 | its real content. | 376 | its real content. |
343 | 377 | ||
344 | The filenames given are all processed. If -s is used content is processed via | 378 | The filenames given are all processed. If -s is used content is processed via |
345 | stdin/stdout and only one filename can be passed. This is used when running blimp | 379 | stdin/stdout and only one filename can be passed. This is used when running blimp |
346 | - via a git filter (smudge/clean). | 380 | + via a git filter (smudge/clean), see below. |
347 | 381 | ||
348 | The remove command (no single character shortcut) will remove the file(s) content | 382 | The remove command (no single character shortcut) will remove the file(s) content |
349 | both from the local blimpstore and from the remote. This only removes | 383 | both from the local blimpstore and from the remote. This only removes |
350 | the current content version, not older versions. The file itself is first | 384 | the current content version, not older versions. The file itself is first |
351 | inflated, if needed, and not deleted. This only "unblimps" the file. | 385 | inflated, if needed, and not deleted. This only "unblimps" the file. |
386 | + | ||
387 | + In order to have blimp work automatically you can: | ||
388 | + | ||
389 | + * Create a .gitattributes file with lines like: | ||
390 | + *.png filter=blimp -text | ||
391 | + | ||
392 | + * Configure blimp as a filter by running: | ||
393 | + git config filter.blimp.clean "blimp -s d %f" | ||
394 | + git config filter.blimp.smudge "blimp -s i %f" | ||
395 | + | ||
396 | + The above git config can be done by running "blimp --init". | ||
397 | + | ||
398 | + When the above is done (per clone) git will automatically run blimp deflate | ||
399 | + just before committing and blimp inflate when operations are done. | ||
400 | + | ||
401 | + This means that if you clone a git repository that already has a .gitattributes | ||
402 | + file in it that uses the blimp filter, then you should do: | ||
403 | + | ||
404 | + blimp --init inflate --all | ||
405 | + | ||
406 | + This will configure the blimp filter and also find and inflate all deflated | ||
407 | + files throughout the clone. | ||
352 | """ | 408 | """ |
353 | 409 | ||
354 | ################################ main ##################################### | 410 | ################################ main ##################################### |
@@ -359,9 +415,10 @@ currentDir = getCurrentDir() | @@ -359,9 +415,10 @@ currentDir = getCurrentDir() | ||
359 | gitRootDir = gitRoot() | 415 | gitRootDir = gitRoot() |
360 | 416 | ||
361 | # Using lapp to get args, on parsing failure this will show usage automatically | 417 | # Using lapp to get args, on parsing failure this will show usage automatically |
362 | -var args = parse(help) | 418 | +var args = parse(synopsis) |
363 | verbose = args["verbose"].asBool | 419 | verbose = args["verbose"].asBool |
364 | stdio = args["stdio"].asBool | 420 | stdio = args["stdio"].asBool |
421 | +inflateAll = args["all"].asBool | ||
365 | 422 | ||
366 | # Can't do verbose with -s, that messes up stdout, | 423 | # Can't do verbose with -s, that messes up stdout, |
367 | # read in all of stdin once and for all | 424 | # read in all of stdin once and for all |
@@ -376,7 +433,7 @@ if stdio: | @@ -376,7 +433,7 @@ if stdio: | ||
376 | 433 | ||
377 | # Parse configuration files, may shadow and override each other | 434 | # Parse configuration files, may shadow and override each other |
378 | parseConfFile(currentDir / ".blimp.conf") | 435 | parseConfFile(currentDir / ".blimp.conf") |
379 | -if not gitRootDir.isNil: | 436 | +if not gitRootDir.isNil and gitRootDir != currentDir: |
380 | parseConfFile(gitRootDir / ".blimp.conf") | 437 | parseConfFile(gitRootDir / ".blimp.conf") |
381 | 438 | ||
382 | # If we haven't gotten a blimpstore yet, we set a default one | 439 | # If we haven't gotten a blimpstore yet, we set a default one |
@@ -387,35 +444,47 @@ if existsDir(blimpStore): | @@ -387,35 +444,47 @@ if existsDir(blimpStore): | ||
387 | parseConfFile(blimpStore / ".blimp.conf") | 444 | parseConfFile(blimpStore / ".blimp.conf") |
388 | parseConfFile(homeDir / ".blimp.conf") | 445 | parseConfFile(homeDir / ".blimp.conf") |
389 | 446 | ||
447 | +# Let's just show what we have :) | ||
390 | if verbose: dumpConfig() | 448 | if verbose: dumpConfig() |
391 | 449 | ||
392 | # These two are special, they short out | 450 | # These two are special, they short out |
393 | -if args.showHelp: quit(help) | 451 | +if args.showHelp: quit(synopsis & help) |
394 | if args.showVersion: quit("blimp version: " & versionAsString) | 452 | if args.showVersion: quit("blimp version: " & versionAsString) |
395 | 453 | ||
396 | # Check blimpVersion | 454 | # Check blimpVersion |
397 | if not blimpVersion.isNil and blimpVersion != versionAsString: | 455 | if not blimpVersion.isNil and blimpVersion != versionAsString: |
398 | quit("Wrong version of blimp, configuration wants: " & blimpVersion) | 456 | quit("Wrong version of blimp, configuration wants: " & blimpVersion) |
399 | 457 | ||
458 | +# Should we install the filter? | ||
459 | +if args["init"].asBool: | ||
460 | + ensureBlimpFilter() | ||
461 | + if verbose: echo("Installed blimp filter") | ||
462 | + | ||
400 | let command = args["command"].asString | 463 | let command = args["command"].asString |
401 | -let filenames = args["filenames"].asSeq | 464 | +var filenames: seq[PValue] |
465 | +if args.hasKey("filenames"): | ||
466 | + filenames = args["filenames"].asSeq | ||
402 | 467 | ||
403 | # Make sure the local blimpstore is setup. | 468 | # Make sure the local blimpstore is setup. |
404 | setupBlimpStore() | 469 | setupBlimpStore() |
405 | 470 | ||
406 | - | ||
407 | # Do the deed | 471 | # Do the deed |
408 | -if command == "d" or command == "deflate": | ||
409 | - for fn in filenames: | ||
410 | - deflate(fn.asString) | ||
411 | -elif command == "i" or command == "inflate": | ||
412 | - for fn in filenames: | ||
413 | - inflate(fn.asString) | ||
414 | -elif command == "remove": | ||
415 | - for fn in filenames: | ||
416 | - remove(fn.asString) | ||
417 | -else: | ||
418 | - quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6) | 472 | +if command != "": |
473 | + if command == "d" or command == "deflate": | ||
474 | + for fn in filenames: | ||
475 | + deflate(fn.asString) | ||
476 | + elif command == "i" or command == "inflate": | ||
477 | + if inflateAll: | ||
478 | + for fn in allDeflated(): | ||
479 | + inflate(fn) | ||
480 | + else: | ||
481 | + for fn in filenames: | ||
482 | + inflate(fn.asString) | ||
483 | + elif command == "remove": | ||
484 | + for fn in filenames: | ||
485 | + remove(fn.asString) | ||
486 | + else: | ||
487 | + quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6) | ||
419 | 488 | ||
420 | # All good | 489 | # All good |
421 | quit(0) | 490 | quit(0) |
lapp.nim
@@ -17,6 +17,8 @@ type | @@ -17,6 +17,8 @@ type | ||
17 | trange | 17 | trange |
18 | telipsis | 18 | telipsis |
19 | tchar | 19 | tchar |
20 | + toption | ||
21 | + tdivider | ||
20 | 22 | ||
21 | proc thisChar(L: PLexer):char = L.str[L.idx] | 23 | proc thisChar(L: PLexer):char = L.str[L.idx] |
22 | proc next(L: PLexer) = L.idx += 1 | 24 | proc next(L: PLexer) = L.idx += 1 |
@@ -35,9 +37,15 @@ proc get(L: PLexer; t: var TLexType): string = | @@ -35,9 +37,15 @@ proc get(L: PLexer; t: var TLexType): string = | ||
35 | t = tchar | 37 | t = tchar |
36 | case c | 38 | case c |
37 | of '-': # '-", "--" | 39 | of '-': # '-", "--" |
40 | + t = toption | ||
38 | if thisChar(L) == '-': | 41 | if thisChar(L) == '-': |
39 | result.add('-') | 42 | result.add('-') |
40 | next(L) | 43 | next(L) |
44 | + if thisChar(L) == '-': # "---..." | ||
45 | + t = tdivider | ||
46 | + result.add('-') | ||
47 | + while thisChar(L) == '-': | ||
48 | + next(L) | ||
41 | of Letters: # word | 49 | of Letters: # word |
42 | t = tword | 50 | t = tword |
43 | while thisChar(L) in Letters: | 51 | while thisChar(L) in Letters: |
@@ -112,14 +120,13 @@ proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v) | @@ -112,14 +120,13 @@ proc intValue(v: int): PValue = PValue(kind: vInt, asInt: v) | ||
112 | proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v) | 120 | proc floatValue(v: float): PValue = PValue(kind: vFloat, asFloat: v) |
113 | 121 | ||
114 | proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v) | 122 | proc seqValue(v: seq[PValue]): PValue = PValue(kind: vSeq, asSeq: v) |
115 | - | ||
116 | -const MAX_FILES = 30 | ||
117 | 123 | ||
118 | type | 124 | type |
119 | PSpec = ref TSpec | 125 | PSpec = ref TSpec |
120 | TSpec = object | 126 | TSpec = object |
121 | defVal: string | 127 | defVal: string |
122 | ptype: string | 128 | ptype: string |
129 | + group: int | ||
123 | needsValue, multiple, used: bool | 130 | needsValue, multiple, used: bool |
124 | var | 131 | var |
125 | progname, usage: string | 132 | progname, usage: string |
@@ -134,6 +141,7 @@ proc parseSpec(u: string) = | @@ -134,6 +141,7 @@ proc parseSpec(u: string) = | ||
134 | var | 141 | var |
135 | L: PLexer | 142 | L: PLexer |
136 | tok: string | 143 | tok: string |
144 | + groupCounter: int | ||
137 | k = 1 | 145 | k = 1 |
138 | 146 | ||
139 | let lines = u.splitLines | 147 | let lines = u.splitLines |
@@ -173,7 +181,8 @@ proc parseSpec(u: string) = | @@ -173,7 +181,8 @@ proc parseSpec(u: string) = | ||
173 | k += 1 | 181 | k += 1 |
174 | tok = L.get | 182 | tok = L.get |
175 | if tok != ">": fail("argument must be enclosed in <...>") | 183 | if tok != ">": fail("argument must be enclosed in <...>") |
176 | - | 184 | + elif tok == "---": # divider |
185 | + inc(groupCounter) | ||
177 | if getnext: tok = L.get | 186 | if getnext: tok = L.get |
178 | if tok == ":": # allowed to have colon after flags | 187 | if tok == ":": # allowed to have colon after flags |
179 | tok = L.get | 188 | tok = L.get |
@@ -205,8 +214,8 @@ proc parseSpec(u: string) = | @@ -205,8 +214,8 @@ proc parseSpec(u: string) = | ||
205 | defValue = "false" | 214 | defValue = "false" |
206 | 215 | ||
207 | if name != nil: | 216 | if name != nil: |
208 | - # echo("Param: " & name & " type: " & $ftype & " needsvalue: " & $(ftype != "bool") & " default: " & $defValue & " multiple: " & $multiple) | ||
209 | - let spec = PSpec(defVal:defValue, ptype: ftype, needsValue: ftype != "bool",multiple:multiple) | 217 | + #echo("Param: " & name & " type: " & $ftype & " group: " & $groupCounter & " needsvalue: " & $(ftype != "bool") & " default: " & $defValue & " multiple: " & $multiple) |
218 | + let spec = PSpec(defVal:defValue, ptype: ftype, group: groupCounter, needsValue: ftype != "bool",multiple:multiple) | ||
210 | aliases[alias] = name | 219 | aliases[alias] = name |
211 | parm_spec[name] = spec | 220 | parm_spec[name] = spec |
212 | 221 | ||
@@ -297,12 +306,19 @@ proc parseArguments*(usage: string, args: seq[string]): Table[string,PValue] = | @@ -297,12 +306,19 @@ proc parseArguments*(usage: string, args: seq[string]): Table[string,PValue] = | ||
297 | if info.used: | 306 | if info.used: |
298 | if flag == "help" or flag == "version": | 307 | if flag == "help" or flag == "version": |
299 | enableChecks = false | 308 | enableChecks = false |
300 | - | ||
301 | - | 309 | + |
310 | + # Check maximum group used | ||
311 | + var maxGroup = 0 | ||
312 | + for item in flagvalues: | ||
313 | + info = get_spec(item[0]) | ||
314 | + if maxGroup < info.group: | ||
315 | + maxGroup = info.group | ||
316 | + | ||
302 | # any flags not mentioned? | 317 | # any flags not mentioned? |
303 | for flag,info in parm_spec: | 318 | for flag,info in parm_spec: |
304 | if not info.used: | 319 | if not info.used: |
305 | - if info.defVal == "": # no default! | 320 | + # Is there no default and we have used options in this group? |
321 | + if info.defVal == "" and info.group <= maxGroup: | ||
306 | failures.add("required option or argument missing: " & flag) | 322 | failures.add("required option or argument missing: " & flag) |
307 | else: | 323 | else: |
308 | flagvalues.add(@[flag,info.defVal]) | 324 | flagvalues.add(@[flag,info.defVal]) |