shellopts 2.0.0.pre.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +46 -134
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +58 -5
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +29 -21
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +325 -0
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +269 -82
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +2 -2
  28. data/lib/shellopts.rb +439 -220
  29. data/main +1180 -0
  30. data/shellopts.gemspec +9 -15
  31. metadata +85 -42
  32. data/lib/main.rb +0 -1
  33. data/lib/shellopts/ast/command.rb +0 -41
  34. data/lib/shellopts/ast/node.rb +0 -37
  35. data/lib/shellopts/ast/option.rb +0 -21
  36. data/lib/shellopts/ast/program.rb +0 -14
  37. data/lib/shellopts/compiler.rb +0 -128
  38. data/lib/shellopts/generator.rb +0 -15
  39. data/lib/shellopts/grammar/command.rb +0 -80
  40. data/lib/shellopts/grammar/node.rb +0 -33
  41. data/lib/shellopts/grammar/option.rb +0 -66
  42. data/lib/shellopts/grammar/program.rb +0 -65
  43. data/lib/shellopts/idr.rb +0 -236
  44. data/lib/shellopts/main.rb +0 -10
  45. data/lib/shellopts/option_struct.rb +0 -148
  46. data/lib/shellopts/shellopts.rb +0 -123
data/TODO CHANGED
@@ -1,136 +1,48 @@
1
1
 
2
- TODO
3
- o Rethink #error and #fail <- The use-case is one-file ruby scripts. Idea: Only use in main exe file?
4
- o Create exceptions: UserError SystemFail and allow them to be used instead of #error and #fail
5
- ? Remove ! from OptionStruct#subcommand return value. We know we're
6
- processing commands so there is no need to have a distinct name and it
7
- feels a lot more intuitive without it
8
- o Add validation block to ShellOpts class methods
9
- o Get rid of key_name. Define #name on Grammar::Node instead
10
- o Define #name to the string name of the option/command without prefixed '--'
11
- for options. This can cause collisions but they can be avoided using aliases
12
- o Clean-up
13
- o Grammar::options -> Grammar::option_multihash
14
- o Clean-up identifiers etc.
15
- o Un-multi-izing Grammar::option_multihash and turn it into a regular hash from key to option
16
- o subcommand vs. command consistency
17
- o Implement ObjectStruct#key! and ObjectStruct#value! (?)
18
- o Allow command_alias == nil to suppress the method
19
- o Raise on non-existing names/keys. Only return nil for declared names/keys that are not present
20
- o Use hash_tree
21
- o Also allow assignment to usage string for ShellOpts::ShellOpts objects
22
- o Create a ShellOpts.args method? It would be useful when processing commands:
23
- case opt
24
- when "command"
25
- call_command_method(ShellOpts.args[1], ShellOpts.args[2])
26
- end
27
- ShellOpts.args would be a shorthand for ShellOpts.shellopts.args
28
- Another option would be to create an argument-processing method:
29
- shellopts.argv(2) -> call error if not exactly two arguments else return elements
30
- o Add a ShellOpts.option method:
31
- file = ShellOpts.option("--file")
32
- This will only work for options on the outermost level... maybe:
33
- file = ShellOpts.option("load! --file")
34
- o Check on return value from #process block to see if all options was handled:
35
- case opt
36
- when '-v'; verbose = true # Return value 'true' is ok
37
- # Unhandled option means return value is nil
38
- end
39
- o Consolidate some of the 3 variations of #error and #fail
40
- o Add a option flag for solitary options (--help)
41
- o Make a #to_yaml
42
- o Make an official dump method for debug
43
- o Make a note that all options are processed at once and not as-you-go
44
- o Test that arguments with spaces work
45
- o Long version usage strings (major release)
46
- o Doc: Example of processing of sub-commands and sub-sub-commands
47
-
48
- + Add a 'mandatory' argument to #subcommand
49
- + More tests
50
- + More doc
51
- + Implement value-name-before-flags rule
52
- + Kill option array values
53
- + Kill key forms
54
- + Rename Option#opt to Option#name
55
- + Have all Ast objects to be on [key, name, value] form
56
- + Change #=>i, $=>f and introduce b (boolean)
57
- + Unshift program name to usage definition string before compiling
58
- + Rename to UsageCompiler and ArgvParser
59
- + Make usage-string handle commands
60
- + Change !cmd to cmd!
61
- + Clean-up terminology: Option-name is used for names with and without the prefixed dashes
62
- + Rename Option#has_argument? and #optional? to something else
63
- + Fix location reporting of compiler errors
64
- + Allow '--' in usage so that everything after can be used as USAGE in error messages
65
- + Handle pretty-printing of usage string in handling of ParserError
66
- + Compiler.new.compile(usage), Parser.new(compiled_program).parse(argv)
67
- + Check for duplicate option in the parser
68
- + Handle CompilerError
69
- + Use nil value as the name of the top 'command'
70
- + Refactor compilation to avoid having the Command objects throw CompilerErrors
71
- + Change to 'parser.parse' / 'parser.parse3'
72
- + Use first long option as symbolic key
73
- + Use full option names everywhere (eg. '--all' instead of 'all')
74
-
75
- - Revert change from '#' -> 'i'
76
- - Guard against reserved 'object_id' name in OpenStruct
77
- - Default value ('=' -> ':')
78
- Default values are better handled in the calling program
79
-
80
- ? More specialized exceptions: "MissingArgumentError" etc.
81
-
82
- LATER
83
- o Allow '-a' and '--aa' in usage
84
- o Allow single-line comments
85
- o Allow multi-line comments
86
- o Regex as option value spec
87
- o "FILE", "DIR", "NEWFILE", "NEWDIR" as keyword in option value spec
88
- RFILE, RDIR
89
- WFILE, WDIR
90
- EFILE, EDIR
91
- o Octal and hexadecimal integers
92
- o Escape of separator in lists
93
- o Handle output of subcommand usage like "cmd1 cmd1.cmd2 cmd2"
94
- o Command-specific arguments: clone! o,opt ++ ARG1 ARG2...
95
- o Hostname and email as basic types
96
-
97
- ON TO_H BRANCH
98
- ShellOpts.process(usage, argv) { |opt,val| ... } => args
99
- ShellOpts.process(usage, argv) { |key,opt,val| ... } => args
100
-
101
- opts = ShellOpts.new(usage, argv, defaults = {})
102
- opts = ShellOpts.new(usage, argv, defaults = OpenStruct.new)
103
-
104
- opts.args
105
- opts.to_a
106
- opts.to_h
107
- opts.to_openstruct
108
-
109
- opts.each { |opt,val| ... }
110
- opts.each { |key,opt,val| ... }
111
-
112
- LONG FORMAT
113
-
114
- PROGRAM = File.basename(ARGV.first)
115
- USAGE = "-a -f FILE -lvh FILE..."
116
- DESCR = %(
117
- Short description
118
-
119
- Longer description
120
- )
121
- OPTIONS = %(
122
- -a,--all
123
- Process all files
124
-
125
- -f, --file=FILE
126
- Process file
127
-
128
- !command
129
- This is a command
130
-
131
- --this-is-a-command-option
132
- Options for commands are nested
133
-
2
+ o Ignore all text after ' # ' (doesn't conflict with option flag)
3
+ o Command aliases
4
+ o Add user-defined setions
5
+ o Add a SOURCE section with link to git repo
6
+ o Bullet-lists
7
+ o Allow a USAGE section (and NAME)
8
+ o Find source in code an adjust line number in error messages
9
+ o Rename line and char to lineno and charno
10
+ o Client-defined argument types
11
+ o Rename Expr -> ?
12
+ o Find clean(er) procedural object model
13
+ o Allow assignment to options (this makes practical stuff easier)
14
+ o Special handling of --help arguments so that '--help command' is possible
15
+ o Support for paging of help:
16
+ begin
17
+ file = Tempfile.new("prick")
18
+ file.puts HELP.split("\n").map { |l| l.sub(/^ /, "") }
19
+ file.flush
20
+ system "less #{file.path}"
21
+ ensure
22
+ file.close
23
+ end
24
+
25
+ + Bold text output
26
+ + Recursive format of commands
27
+ + Rename Compiler -> Interpreter
28
+
29
+
30
+ OLD
31
+
32
+ o Fix that it is near unsable when the user doesn't do 'include ShellOpts'
33
+ o Subcommands can be parsed as well:
34
+ opts, args = Shellopts.process(OPTIONS, ARGV)
134
35
  ...
135
- )
136
-
36
+ opts, command = opts.process
37
+ case command
38
+ when ...
39
+ end
40
+ o 'help' should list commands in declaration order
41
+ o 'help' should use all levels by default
42
+ o 'help' should always include top-level options (try setting levels: 10 and
43
+ see top-level options are gone
44
+ o A ShellOpts#shift command that makes it possible for Array#expect to emit
45
+ relevant error messages
46
+
47
+ + Somehow escape comments where a line starts with an option name
48
+ + Parse and check enumeration arguments ('--debug=tokens|ast|idr|...')
data/doc/format.rb ADDED
@@ -0,0 +1,95 @@
1
+ # One-line format in prioritized order
2
+ # ------------------------------------
3
+ #
4
+ # cmd -a -b -c [CMD|CMD] ARGS
5
+ # cmd -a -b -c <command> ARGS
6
+ # cmd <options> [CMD|CMD] <ARGS>
7
+ # cmd <options> <command> <ARGS>
8
+ #
9
+ # Multiline format
10
+ # ----------------
11
+ #
12
+ # cmd -a -b -c [CMD|CMD] ARGS # <- only if no subcommand options or arguments
13
+ #
14
+ # cmd -a -b -c <command> ARGS
15
+ # subcmd -e -f -g ARGS
16
+ # subcmd -h -i -j ARGS
17
+ #
18
+ # cmd -a -b -c
19
+ # -d -e
20
+ # [CMD|CMD] ARGS
21
+ #
22
+ # cmd -a -b -c
23
+ # -d -e
24
+ # <command> ARGS
25
+ #
26
+ # Brief format
27
+ # ------------
28
+ #
29
+ # Name - Brief
30
+ #
31
+ # Usage:
32
+ # cmd -a -b -c [CMD|CMD] ARGS
33
+ #
34
+ # Options:
35
+ # -a Brief
36
+ # -b Brief
37
+ # -c Brief
38
+ #
39
+ # Commands:
40
+ # CMD --opts ARGS Brief
41
+ # CMD --opts ARGS_THAT_TAKES_UP_A_LOT_OF_SPACE
42
+ # Brief
43
+ #
44
+ # Brief Command
45
+ # CMD --opts ARGS Brief
46
+ # CMD --opts ARGS_THAT_TAKES_UP_A_LOT_OF_SPACE
47
+ # Brief
48
+ #
49
+ # Brief Option
50
+ # -a Brief
51
+ # -b=a_very_long_option
52
+ # Brief
53
+ #
54
+ #
55
+ # Doc format
56
+ # ----------
57
+ #
58
+ # Name
59
+ # Name - Brief
60
+ #
61
+ # Usage:
62
+ # cmd -a -b -c [CMD|CMD] ARGS
63
+ #
64
+ # Description
65
+ # Descr
66
+ #
67
+ # Options:
68
+ # -a
69
+ # Descr
70
+ # -b
71
+ # Descr
72
+ # -c, --aliases
73
+ # Descr
74
+ #
75
+ # Commands:
76
+ # CMD -d -e -f ARGS
77
+ # Descr
78
+ #
79
+ # -d
80
+ # Descr
81
+ # -e
82
+ # Descr
83
+ # -f
84
+ # Descr
85
+ #
86
+ # CMD -g -h -i ARGS
87
+ # Descr
88
+ #
89
+ # -g
90
+ # Descr
91
+ # -h
92
+ # Descr
93
+ # -i
94
+ # Descr
95
+ #
data/doc/grammar.txt ADDED
@@ -0,0 +1,27 @@
1
+
2
+ Optionn Grammar
3
+ [ "+" ] name-list [ "=" [ label ] [ ":" [ "#" | "$" | enum | special-constant ] ] [ "?" ] ]
4
+
5
+ -a= # Renders as -a=
6
+ -a=# # Renders as -a=INT
7
+ -b=$ # Renders as -b=NUM
8
+ -c=a,b,c # Renders as -c=a|b|c
9
+ -d=3..5 # Renders as -d=3..5
10
+ -e=:DIR # Renders as -e=DIR
11
+ -f=:FILE # Renders as -f=FILE
12
+
13
+ -o=COUNT
14
+ -a=COUNT:#
15
+ -b=COUNT:$
16
+ -c=COUNT:a,b,c
17
+ -e=DIR:DIRPATH
18
+ -f=FILE:FILEPATH
19
+
20
+ Special constants
21
+
22
+ Exist Missing Optional
23
+
24
+ File FILE - FILEPATH
25
+ Dir DIR - DIRPATH
26
+ Node NODE NEW PATH
27
+
data/doc/syntax.rb ADDED
@@ -0,0 +1,110 @@
1
+
2
+ # MAN pages
3
+ #
4
+ # NAME
5
+ # #{$PROGRAM_NAME} - #{BRIEF || spec.summary}
6
+ #
7
+ # USAGE
8
+ # #{USAGE || shellopts.usage}
9
+ #
10
+ # DESCRIPTION
11
+ # #{DESCRIPTION || spec.description}
12
+ #
13
+ # OPTIONS
14
+ # #{OPTIONS || shellopts.options}
15
+ #
16
+ # COMMANDS
17
+ # #{COMMANDS || shellopts.commands}
18
+ #
19
+
20
+ # Help output
21
+ #
22
+ # #{BRIEF}
23
+ #
24
+ # Usage: #{USAGE || shellopts.usage}
25
+ #
26
+ # Options:
27
+ # #{OPTIONS_IN_SHORT_FORMAT | shellopts.options_in_short_format}
28
+ #
29
+ # Commands
30
+ # #{COMMANDS_IN_SHORT_FORMMAT | shellopts.commands_in_short_format}
31
+ #
32
+
33
+ h = %(
34
+ -a,all # Include all files
35
+ -f=FILE # Use this file
36
+ )
37
+
38
+ # Default options
39
+ # <defaults>
40
+ # Can be processed by ShellOpts.process_defaults
41
+ # ShellOpts.make(SPEC, ARGV, defaults: true)
42
+ #
43
+ # Options
44
+ # -a,b,long
45
+ # --long
46
+ # -a=FILE
47
+ # -e,env,environment=ENVIRONMENT:p,d,t,prod,dev,test,production,development
48
+ # -i=# # Integer
49
+ # -o=optional?
50
+ # --verbose* # Repeated
51
+ # -a=V* # Repeated with argument
52
+ # -a=V?* # Repeated, optionally with argument
53
+ #
54
+ # Arguments
55
+ # Arguments has to be on one line (per ++ or --)
56
+ #
57
+ # ++ ARG...
58
+ # Subcommand arguments
59
+ # -- ARG...
60
+ # Command arguments. Multiple -- definitions are allowed
61
+ #
62
+ # [ARG]
63
+ # Optional argument
64
+ # ARG
65
+ # Mandatory argument
66
+ # ARG...
67
+ # Repeated argument. At least one argument is mandatory
68
+ # [ARG...]
69
+ # Optionally repeated argument
70
+ #
71
+ # SPECIAL ALTERNATE VALUES OR ARGUMENTS
72
+ # FILE:EFILE
73
+ # Existing file. "FILE" will be used as name of the value
74
+ # PATH:EPATH
75
+ # Existing file or directory. "PATH" will be used as the name of the value
76
+ # DIRECTORY:EDIR
77
+ # Existing directory. "DIRECTORY" will be used as name of the value
78
+ #
79
+ # SPEC = %(
80
+ # # Common options
81
+ # -f,file=FILE
82
+ # -- ARG ARG
83
+ #
84
+ # # More options
85
+ # -m,mode=MODE
86
+ # -- ARG ARG ARG
87
+ # )
88
+ #
89
+ # How to make
90
+ # cp [OPTION]... [-T] SOURCE DEST
91
+ # cp [OPTION]... SOURCE... DIRECTORY
92
+ # cp [OPTION]... -t DIRECTORY SOURCE...
93
+ #
94
+ # USAGE = %(
95
+ # cp [OPTION]... [-T] SOURCE DEST
96
+ # cp [OPTION]... SOURCE... DIRECTORY
97
+ # cp [OPTION]... -t DIRECTORY SOURCE...
98
+ # )
99
+ #
100
+ # SPEC = %(
101
+ # -r,recursive
102
+ #
103
+ # -T,no-target-directory
104
+ #
105
+ # -t,target_directory=DIRECTORY:EDIR
106
+ # )
107
+ #
108
+
109
+
110
+
data/doc/syntax.txt ADDED
@@ -0,0 +1,10 @@
1
+
2
+ One-line syntax:
3
+
4
+ <options> <commands> <spec|descr> # <brief>
5
+
6
+ Grammar
7
+
8
+ <program> ::= <options-spec> <commands-spec>
9
+
10
+
data/lib/ext/array.rb CHANGED
@@ -1,9 +1,62 @@
1
1
 
2
- module XArray
3
- refine Array do
4
- # Find and return first duplicate. Return nil if not found
5
- def find_dup
6
- detect { |e| rindex(e) != index(e) }
2
+ module Ext
3
+ module Array
4
+ module ShiftWhile
5
+ refine ::Array do
6
+ # The algorithm ensures that the block sees the array as if the current
7
+ # element has already been removed
8
+ def shift_while(&block)
9
+ r = []
10
+ while value = self.shift
11
+ if !block.call(value)
12
+ self.unshift value
13
+ break
14
+ else
15
+ r << value
16
+ end
17
+ end
18
+ r
19
+ end
20
+ end
21
+ end
22
+
23
+ module PopWhile
24
+ refine ::Array do
25
+ # The algorithm ensures that the block sees the array as if the current
26
+ # element has already been removed
27
+ def pop_while(&block)
28
+ r = []
29
+ while value = self.pop
30
+ if !block.call(value)
31
+ self.push value
32
+ break
33
+ else
34
+ r << value
35
+ end
36
+ end
37
+ r
38
+ end
39
+ end
40
+ end
41
+
42
+ module Wrap
43
+ refine ::Array do
44
+ # Concatenate strings into lines that are at most +width+ characters wide
45
+ def wrap(width, curr = 0)
46
+ lines = [[]]
47
+ curr -= 1 # Simplifies conditions below
48
+ each { |word|
49
+ if curr + 1 + word.size <= width
50
+ lines.last << word
51
+ curr += 1 + word.size
52
+ else
53
+ lines << [word]
54
+ curr = word.size
55
+ end
56
+ }
57
+ lines.map! { |words| words.join(" ") }
58
+ end
59
+ end
7
60
  end
8
61
  end
9
62
  end
@@ -0,0 +1,15 @@
1
+
2
+ require 'forward_to'
3
+
4
+ module ForwardTo
5
+ def forward_self_to(target, *methods)
6
+ for method in Array(methods).flatten
7
+ if method =~ /=$/
8
+ class_eval("def self.#{method}(*args) #{target}.#{method}(*args) end")
9
+ else
10
+ class_eval("def self.#{method}(*args, &block) #{target}.#{method}(*args, &block) end")
11
+ end
12
+ end
13
+ end
14
+ end
15
+
data/lib/ext/lcs.rb ADDED
@@ -0,0 +1,34 @@
1
+
2
+ # https://gist.github.com/Joseph-N/fbf061aa2347ed2c104f0b3fe1a5b9f2
3
+ #
4
+ # TODO: Move to String and make find_longest_command_substring_index return a
5
+ # range
6
+ module LCS
7
+ def self.find_longest_common_substring(s1, s2)
8
+ i, z = find_longest_command_substring_index(s1, s2)
9
+ s1[i .. i + z]
10
+ end
11
+
12
+ def self.find_longest_common_substring_index(s1, s2)
13
+ if (s1 == "" || s2 == "")
14
+ return [0,0]
15
+ end
16
+ m = Array.new(s1.length){ [0] * s2.length }
17
+ longest_length, longest_end_pos = 0,0
18
+ (0 .. s1.length - 1).each do |x|
19
+ (0 .. s2.length - 1).each do |y|
20
+ if s1[x] == s2[y]
21
+ m[x][y] = 1
22
+ if (x > 0 && y > 0)
23
+ m[x][y] += m[x-1][y-1]
24
+ end
25
+ if m[x][y] > longest_length
26
+ longest_length = m[x][y]
27
+ longest_end_pos = x
28
+ end
29
+ end
30
+ end
31
+ end
32
+ [longest_end_pos - longest_length + 1, longest_length]
33
+ end
34
+ end
@@ -0,0 +1,130 @@
1
+
2
+ module ShellOpts
3
+ module Grammar
4
+ class Node
5
+ def remove_brief_nodes
6
+ children.delete_if { |node| node.is_a?(Brief) }
7
+ end
8
+
9
+ def remove_arg_descr_nodes
10
+ children.delete_if { |node| node.is_a?(ArgDescr) }
11
+ end
12
+
13
+ def remove_arg_spec_nodes
14
+ children.delete_if { |node| node.is_a?(ArgSpec) }
15
+ end
16
+
17
+ def analyzer_error(token, message)
18
+ raise AnalyzerError.new(token), message
19
+ end
20
+ end
21
+
22
+ class Command
23
+ def set_supercommand
24
+ commands.each { |child| child.instance_variable_set(:@supercommand, self) }
25
+ end
26
+
27
+ def collect_options
28
+ @options = option_groups.map(&:options).flatten
29
+ end
30
+
31
+ # Move options before first command
32
+ def reorder_options
33
+ if commands.any?
34
+ if i = children.find_index { |child| child.is_a?(Command) }
35
+ options, rest = children[i+1..-1].partition { |child| child.is_a?(OptionGroup) }
36
+ @children = children[0, i] + options + children[i..i] + rest
37
+ end
38
+ end
39
+ end
40
+
41
+ def compute_option_hashes
42
+ options.each { |option|
43
+ option.idents.zip(option.names).each { |ident, name|
44
+ !@options_hash.key?(name) or
45
+ analyzer_error option.token, "Duplicate option name: #{name}"
46
+ @options_hash[name] = option
47
+ !@options_hash.key?(ident) or
48
+ analyzer_error option.token, "Can't use both #{@options_hash[ident].name} and #{name}"
49
+ @options_hash[ident] = option
50
+ }
51
+ }
52
+ end
53
+
54
+ def compute_command_hashes
55
+ commands.each { |command|
56
+ # TODO Check for dash-collision
57
+ !@commands_hash.key?(command.name) or
58
+ analyzer_error command.token, "Duplicate command name: #{command.name}"
59
+ @commands_hash[command.name] = command
60
+ @commands_hash[command.ident] = command
61
+ command.compute_command_hashes
62
+ }
63
+ end
64
+ end
65
+ end
66
+
67
+ class Analyzer
68
+ include Grammar
69
+
70
+ attr_reader :grammar
71
+
72
+ def initialize(grammar)
73
+ @grammar = grammar
74
+ end
75
+
76
+ # Move commands that are nested within a different command than it belongs to
77
+ def move_commands
78
+ # We can't use Command#[] at this point so we collect the commands here
79
+ h = {}
80
+ @grammar.traverse(Grammar::Command) { |command|
81
+ h[command.path] = command
82
+ }
83
+
84
+ # Find commands to move
85
+ #
86
+ # Commands are moved in two steps because the behaviour of #traverse is
87
+ # not defined when the data structure changes beneath it
88
+ move = []
89
+ @grammar.traverse(Grammar::Command) { |command|
90
+ if command.path.size > 1 && command.parent && command.parent.path != command.path[0..-2]
91
+ move << command
92
+ else
93
+ command.instance_variable_set(:@command, command.parent)
94
+ end
95
+ }
96
+
97
+ # Move commands but do not change parent/child relationship
98
+ move.each { |command|
99
+ supercommand = h[command.path[0..-2]] or analyzer_error "Can't find #{command.ident}!"
100
+ command.parent.commands.delete(command)
101
+ supercommand.commands << command
102
+ command.instance_variable_set(:@command, supercommand)
103
+ }
104
+ end
105
+
106
+ def analyze()
107
+ move_commands
108
+
109
+ @grammar.traverse(Grammar::Command) { |command|
110
+ command.set_supercommand
111
+ command.reorder_options
112
+ command.collect_options
113
+ command.compute_option_hashes
114
+ }
115
+
116
+ @grammar.compute_command_hashes
117
+
118
+ @grammar.traverse { |node|
119
+ node.remove_brief_nodes
120
+ node.remove_arg_descr_nodes
121
+ node.remove_arg_spec_nodes
122
+ }
123
+
124
+ @grammar
125
+ end
126
+
127
+ def Analyzer.analyze(source) self.new(source).analyze end
128
+ end
129
+ end
130
+
@@ -0,0 +1,8 @@
1
+ module ShellOpts
2
+ class Ansi
3
+ def self.bold(bold = true, s)
4
+ bold && $stdout.tty? ? "#{s}" : s
5
+ end
6
+ end
7
+ end
8
+