shellopts 2.0.0.pre.14 → 2.0.2

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 (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
+