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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.ruby-version +1 -1
- data/README.md +201 -267
- data/TODO +37 -5
- data/doc/format.rb +95 -0
- data/doc/grammar.txt +27 -0
- data/doc/syntax.rb +110 -0
- data/doc/syntax.txt +10 -0
- data/lib/ext/array.rb +62 -0
- data/lib/ext/forward_to.rb +15 -0
- data/lib/ext/lcs.rb +34 -0
- data/lib/shellopts/analyzer.rb +130 -0
- data/lib/shellopts/ansi.rb +8 -0
- data/lib/shellopts/args.rb +29 -21
- data/lib/shellopts/argument_type.rb +139 -0
- data/lib/shellopts/dump.rb +158 -0
- data/lib/shellopts/formatter.rb +292 -92
- data/lib/shellopts/grammar.rb +375 -0
- data/lib/shellopts/interpreter.rb +103 -0
- data/lib/shellopts/lexer.rb +175 -0
- data/lib/shellopts/parser.rb +293 -0
- data/lib/shellopts/program.rb +279 -0
- data/lib/shellopts/renderer.rb +227 -0
- data/lib/shellopts/stack.rb +7 -0
- data/lib/shellopts/token.rb +44 -0
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +360 -3
- data/main +1180 -0
- data/shellopts.gemspec +8 -14
- metadata +86 -41
- data/lib/ext/algorithm.rb +0 -14
- data/lib/ext/ruby_env.rb +0 -8
- data/lib/shellopts/ast/command.rb +0 -112
- data/lib/shellopts/ast/dump.rb +0 -28
- data/lib/shellopts/ast/option.rb +0 -15
- data/lib/shellopts/ast/parser.rb +0 -106
- data/lib/shellopts/constants.rb +0 -88
- data/lib/shellopts/exceptions.rb +0 -21
- data/lib/shellopts/grammar/analyzer.rb +0 -76
- data/lib/shellopts/grammar/command.rb +0 -87
- data/lib/shellopts/grammar/dump.rb +0 -56
- data/lib/shellopts/grammar/lexer.rb +0 -56
- data/lib/shellopts/grammar/option.rb +0 -55
- 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
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
|
+
|
data/lib/shellopts/args.rb
CHANGED
@@ -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.
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
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
|
58
|
+
raise ArgumentError, message || "Illegal number of arguments"
|
52
59
|
end
|
53
60
|
end
|
54
61
|
end
|
62
|
+
|