shellopts 2.0.0.pre.14 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) 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 +37 -5
  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 +62 -0
  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 +292 -92
  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 +293 -0
  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 +1 -1
  28. data/lib/shellopts.rb +360 -3
  29. data/main +1180 -0
  30. data/shellopts.gemspec +8 -14
  31. metadata +86 -41
  32. data/lib/ext/algorithm.rb +0 -14
  33. data/lib/ext/ruby_env.rb +0 -8
  34. data/lib/shellopts/ast/command.rb +0 -112
  35. data/lib/shellopts/ast/dump.rb +0 -28
  36. data/lib/shellopts/ast/option.rb +0 -15
  37. data/lib/shellopts/ast/parser.rb +0 -106
  38. data/lib/shellopts/constants.rb +0 -88
  39. data/lib/shellopts/exceptions.rb +0 -21
  40. data/lib/shellopts/grammar/analyzer.rb +0 -76
  41. data/lib/shellopts/grammar/command.rb +0 -87
  42. data/lib/shellopts/grammar/dump.rb +0 -56
  43. data/lib/shellopts/grammar/lexer.rb +0 -56
  44. data/lib/shellopts/grammar/option.rb +0 -55
  45. data/lib/shellopts/grammar/parser.rb +0 -78
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 ADDED
@@ -0,0 +1,62 @@
1
+
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
60
+ end
61
+ end
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
+
@@ -3,36 +3,36 @@ module ShellOpts
3
3
  # Specialization of Array for arguments lists. Args extends Array with a
4
4
  # #extract and an #expect method to extract elements from the array. The
5
5
  # methods raise a ShellOpts::UserError exception in case of errors
6
+ #
6
7
  class Args < Array
7
- def initialize(shellopts, *args)
8
- @shellopts = shellopts
9
- super(*args)
10
- end
11
-
12
8
  # Remove and return elements from beginning of the array
13
9
  #
14
10
  # If +count_or_range+ is a number, that number of elements will be
15
11
  # returned. If the count is one, a simple value is returned instead of an
16
- # array. If the count is negative, the elements will be removed from the
12
+ # array. If the count is negative, the elements will be removed from the
17
13
  # end of the array. If +count_or_range+ is a range, the number of elements
18
14
  # returned will be in that range. The range can't contain negative numbers
19
15
  #
20
16
  # #extract raise a ShellOpts::UserError exception if there's is not enough
21
17
  # elements in the array to satisfy the request
18
+ #
22
19
  def extract(count_or_range, message = nil)
23
- if count_or_range.is_a?(Range)
24
- range = count_or_range
25
- range.min <= self.size or inoa(message)
26
- n_extract = [self.size, range.max].min
27
- n_extend = range.max > self.size ? range.max - self.size : 0
28
- r = self.shift(n_extract) + Array.new(n_extend)
29
- range.max <= 1 ? r.first : r
30
- else
31
- count = count_or_range
32
- self.size >= count.abs or inoa(message)
33
- start = count >= 0 ? 0 : size + count
34
- r = slice!(start, count.abs)
35
- r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
20
+ case count_or_range
21
+ when Range
22
+ range = count_or_range
23
+ range.min <= self.size or inoa(message)
24
+ n_extract = [self.size, range.max].min
25
+ n_extend = range.max > self.size ? range.max - self.size : 0
26
+ r = self.shift(n_extract) + Array.new(n_extend)
27
+ range.max <= 1 ? r.first : r
28
+ when Integer
29
+ count = count_or_range
30
+ count.abs <= self.size or inoa(message)
31
+ start = count >= 0 ? 0 : size + count
32
+ r = slice!(start, count.abs)
33
+ r.size <= 0 ? nil : (r.size == 1 ? r.first : r)
34
+ else
35
+ raise ArgumentError
36
36
  end
37
37
  end
38
38
 
@@ -41,14 +41,22 @@ module ShellOpts
41
41
  #
42
42
  # #expect raise a ShellOpts::UserError exception if the array is not emptied
43
43
  # by the operation
44
+ #
44
45
  def expect(count_or_range, message = nil)
45
- count_or_range === self.size or inoa(message)
46
+ case count_or_range
47
+ when Range
48
+ count_or_range === self.size or inoa(message)
49
+ when Integer
50
+ count_or_range >= 0 or raise ArgumentError, "Count can't be negative"
51
+ count_or_range.abs == self.size or inoa(message)
52
+ end
46
53
  extract(count_or_range) # Can't fail
47
54
  end
48
55
 
49
56
  private
50
57
  def inoa(message = nil)
51
- raise Error.new(nil), message || "Illegal number of arguments"
58
+ raise ArgumentError, message || "Illegal number of arguments"
52
59
  end
53
60
  end
54
61
  end
62
+