shellopts 2.0.0.pre.13 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+