simple_scripting 0.9.4 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +8 -0
- data/.travis.yml +1 -4
- data/Gemfile +6 -6
- data/Gemfile.lock +28 -2
- data/README.md +78 -7
- data/lib/simple_scripting/argv.rb +77 -55
- data/lib/simple_scripting/tab_completion.rb +71 -0
- data/lib/simple_scripting/tab_completion/commandline_processor.rb +118 -0
- data/lib/simple_scripting/version.rb +1 -1
- data/simple_scripting.gemspec +2 -2
- data/spec/helpers/tab_completion_custom_rspec_matchers.rb +55 -0
- data/spec/simple_scripting/argv_spec.rb +208 -81
- data/spec/simple_scripting/tab_completion_spec.rb +140 -0
- data/spec/spec_helper.rb +2 -0
- metadata +11 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb1844c9f2da48418b9fc91c3b4026c7fd1a1fbf
|
4
|
+
data.tar.gz: 5a30040d716b0e544027261f5bbe6fba834d75af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39c51fd57bb624894fbb9b0adbfc731d365c2fd36d7b6e9e0a17d2e06fc36ba2f78c014f007b5dd360079daf449beb1ed433d694d827d15f17e664bf10bfdd0e
|
7
|
+
data.tar.gz: 6013724a8eb488ed73b2c8918096af8fc4c37a70e5e04aedcf66abd228412ec19606716b9a80a5b4ec829a99f4b071fe0978079ba35db9e023c58a4ea8265190
|
data/.simplecov
ADDED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
group :development do
|
6
|
-
gem 'rake', '~> 12.0'
|
7
|
-
end
|
3
|
+
gemspec
|
8
4
|
|
9
5
|
group :test do
|
10
|
-
gem 'rspec', '~> 3.6'
|
11
6
|
gem 'coveralls', '~> 0.8.21', require: false
|
12
7
|
end
|
8
|
+
|
9
|
+
group :tools do
|
10
|
+
gem 'byebug', '~> 10.0.2'
|
11
|
+
gem 'rubocop', '= 0.58.1'
|
12
|
+
end
|
data/Gemfile.lock
CHANGED
@@ -1,6 +1,14 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
simple_scripting (0.9.4)
|
5
|
+
parseconfig (~> 1.0)
|
6
|
+
|
1
7
|
GEM
|
2
8
|
remote: https://rubygems.org/
|
3
9
|
specs:
|
10
|
+
ast (2.4.0)
|
11
|
+
byebug (10.0.2)
|
4
12
|
coveralls (0.8.21)
|
5
13
|
json (>= 1.8, < 3)
|
6
14
|
simplecov (~> 0.14.1)
|
@@ -9,8 +17,14 @@ GEM
|
|
9
17
|
tins (~> 1.6)
|
10
18
|
diff-lcs (1.3)
|
11
19
|
docile (1.1.5)
|
20
|
+
jaro_winkler (1.5.1)
|
12
21
|
json (2.1.0)
|
22
|
+
parallel (1.12.1)
|
13
23
|
parseconfig (1.0.8)
|
24
|
+
parser (2.5.1.2)
|
25
|
+
ast (~> 2.4.0)
|
26
|
+
powerpack (0.1.2)
|
27
|
+
rainbow (3.0.0)
|
14
28
|
rake (12.0.0)
|
15
29
|
rspec (3.6.0)
|
16
30
|
rspec-core (~> 3.6.0)
|
@@ -25,6 +39,15 @@ GEM
|
|
25
39
|
diff-lcs (>= 1.2.0, < 2.0)
|
26
40
|
rspec-support (~> 3.6.0)
|
27
41
|
rspec-support (3.6.0)
|
42
|
+
rubocop (0.58.1)
|
43
|
+
jaro_winkler (~> 1.5.1)
|
44
|
+
parallel (~> 1.10)
|
45
|
+
parser (>= 2.5, != 2.5.1.1)
|
46
|
+
powerpack (~> 0.1)
|
47
|
+
rainbow (>= 2.2.2, < 4.0)
|
48
|
+
ruby-progressbar (~> 1.7)
|
49
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
50
|
+
ruby-progressbar (1.9.0)
|
28
51
|
simplecov (0.14.1)
|
29
52
|
docile (~> 1.1.0)
|
30
53
|
json (>= 1.8, < 3)
|
@@ -34,15 +57,18 @@ GEM
|
|
34
57
|
tins (~> 1.0)
|
35
58
|
thor (0.19.4)
|
36
59
|
tins (1.15.0)
|
60
|
+
unicode-display_width (1.4.0)
|
37
61
|
|
38
62
|
PLATFORMS
|
39
63
|
ruby
|
40
64
|
|
41
65
|
DEPENDENCIES
|
66
|
+
byebug (~> 10.0.2)
|
42
67
|
coveralls (~> 0.8.21)
|
43
|
-
parseconfig (~> 1.0)
|
44
68
|
rake (~> 12.0)
|
45
69
|
rspec (~> 3.6)
|
70
|
+
rubocop (= 0.58.1)
|
71
|
+
simple_scripting!
|
46
72
|
|
47
73
|
BUNDLED WITH
|
48
|
-
1.
|
74
|
+
1.16.1
|
data/README.md
CHANGED
@@ -1,21 +1,93 @@
|
|
1
1
|
[![Gem Version][GV img]](https://rubygems.org/gems/simple_scripting)
|
2
2
|
[![Build Status][BS img]](https://travis-ci.org/saveriomiroddi/simple_scripting)
|
3
|
-
[![Dependency Status][DS img]](https://gemnasium.com/saveriomiroddi/simple_scripting)
|
4
3
|
[![Code Climate][CC img]](https://codeclimate.com/github/saveriomiroddi/simple_scripting)
|
5
4
|
[![Coverage Status][CS img]](https://coveralls.io/r/saveriomiroddi/simple_scripting)
|
6
5
|
|
7
6
|
# SimpleScripting
|
8
7
|
|
9
|
-
`
|
8
|
+
`SimpleScripting` is a library composed of three modules (`TabCompletion`, `Argv` and `Configuration`) that simplify three common scripting tasks:
|
10
9
|
|
10
|
+
- writing autocompletion scripts
|
11
11
|
- implementing the commandline options parsing (and the related help)
|
12
12
|
- loading and decoding the configuration for the script/application
|
13
13
|
|
14
|
-
`
|
14
|
+
`SimpleScripting` is an interesting (and useful) exercise in design, aimed at finding the simplest and most expressive data/structures that accomplish the given task(s). For this reason, the library can be useful for people who frequently write small scripts (eg. devops or nerds).
|
15
|
+
|
16
|
+
## SimpleScripting::TabCompletion
|
17
|
+
|
18
|
+
`TabCompletion` makes trivial to define tab-completion for terminal commands on Linux/Mac systems; it's so easy that an example is much simpler than an explanation.
|
19
|
+
|
20
|
+
### Example
|
21
|
+
|
22
|
+
Suppose we have the command:
|
23
|
+
|
24
|
+
```sh
|
25
|
+
open_project [-e|--with-editor EDITOR] <project_name>
|
26
|
+
```
|
27
|
+
|
28
|
+
We want to add tab completion both for the option and the project name. Easy!!
|
29
|
+
|
30
|
+
Install the gem (`simple_scripting`), then create this class (`/my/completion_scripts/open_project_completion.rb`):
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
#!/usr/bin/env ruby
|
34
|
+
|
35
|
+
require 'simple_scripting/tab_completion'
|
36
|
+
|
37
|
+
class OpenProjectTabCompletion
|
38
|
+
SYSTEM_EDITORS = `update-alternatives --list editor`.split("\n").map { |filename| File.basename(filename) }
|
39
|
+
|
40
|
+
def with_editor(prefix, suffix, context)
|
41
|
+
SYSTEM_EDITORS.grep /^#{prefix}/
|
42
|
+
end
|
43
|
+
|
44
|
+
def project_name(prefix, suffix, context)
|
45
|
+
Dir["/my/home/my_projects/#{prefix}*"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if __FILE__ == $PROGRAM_NAME
|
50
|
+
completion_definition = [
|
51
|
+
["-e", "--with-editor EDITOR"],
|
52
|
+
'project_name'
|
53
|
+
]
|
54
|
+
|
55
|
+
SimpleScripting::TabCompletion.new(completion_definition).complete(OpenProjectTabCompletion.new)
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
then chmod and register it:
|
60
|
+
|
61
|
+
```sh
|
62
|
+
$ chmod +x /my/completion_scripts/open_project_completion.rb
|
63
|
+
$ complete -C /my/completion_scripts/open_project_completion.rb -o default open_project
|
64
|
+
```
|
65
|
+
|
66
|
+
Done!
|
67
|
+
|
68
|
+
Now type the following, and get:
|
69
|
+
|
70
|
+
```sh
|
71
|
+
$ open_project g<tab> # lists: "geet", "gitlab-ce", "gnome-terminal"
|
72
|
+
$ open_project --with-editor v # lists: "vim.basic", "vim.tiny"
|
73
|
+
$ open_project --wi<tab> # autocompletes "--with-editor"; this is built-in!
|
74
|
+
```
|
75
|
+
|
76
|
+
Happy completion!
|
77
|
+
|
78
|
+
### Supported shells
|
79
|
+
|
80
|
+
TabCompletion supports Bash, and Zsh with bashcompinit.
|
81
|
+
|
82
|
+
Note that a recent version of Zsh is required - the Ubuntu 16.04 standard version has a bug that breaks bash-compatible completion.
|
83
|
+
|
84
|
+
### More complex use cases
|
85
|
+
|
86
|
+
For a description of the more complex use cases, including edge cases and error handling, see the [wiki](https://github.com/saveriomiroddi/simple_scripting/wiki/SimpleScripting::TabCompletion-Guide).
|
15
87
|
|
16
88
|
## SimpleScripting::Argv
|
17
89
|
|
18
|
-
`
|
90
|
+
`Argv` is a module which acts as frontend to the standard Option Parser library (`optparse`), giving a very convenient format for specifying the arguments. `Argv` also generates the help.
|
19
91
|
|
20
92
|
This is a definition example:
|
21
93
|
|
@@ -76,7 +148,7 @@ For the guide, see the [wiki page](https://github.com/saveriomiroddi/simple_scri
|
|
76
148
|
|
77
149
|
## SimpleScripting::Configuration
|
78
150
|
|
79
|
-
`
|
151
|
+
`Configuration` is a module which acts as frontend to the ParseConfig gem (`parseconfig`), giving compact access to the configuration and its values, and adding a few helpers for common tasks.
|
80
152
|
|
81
153
|
Say one writes a script (`foo_my_bar.rb`), with a corresponding (`$HOME/.foo_my_bar`) configuration, which contains:
|
82
154
|
|
@@ -88,7 +160,7 @@ Say one writes a script (`foo_my_bar.rb`), with a corresponding (`$HOME/.foo_my_
|
|
88
160
|
[a_group]
|
89
161
|
group_key=baz
|
90
162
|
|
91
|
-
This is the workflow and functionality offered by `
|
163
|
+
This is the workflow and functionality offered by `Configuration`:
|
92
164
|
|
93
165
|
require 'simple_scripting/configuration'
|
94
166
|
|
@@ -115,6 +187,5 @@ See the [wiki](https://github.com/saveriomiroddi/simple_scripting/wiki) for addi
|
|
115
187
|
|
116
188
|
[GV img]: https://badge.fury.io/rb/simple_scripting.png
|
117
189
|
[BS img]: https://travis-ci.org/saveriomiroddi/simple_scripting.svg?branch=master
|
118
|
-
[DS img]: https://gemnasium.com/saveriomiroddi/simple_scripting.png
|
119
190
|
[CC img]: https://codeclimate.com/github/saveriomiroddi/simple_scripting.png
|
120
191
|
[CS img]: https://coveralls.io/repos/saveriomiroddi/simple_scripting/badge.png?branch=master
|
@@ -4,32 +4,57 @@ module SimpleScripting
|
|
4
4
|
|
5
5
|
module Argv
|
6
6
|
|
7
|
-
|
7
|
+
# The fact that the following errors don't descend from OptionParser::InvalidOption is somewhat
|
8
|
+
# annoying, however, there should be no practical problem.
|
9
|
+
#
|
10
|
+
class InvalidCommand < StandardError; end
|
11
|
+
class ArgumentError < StandardError; end
|
12
|
+
|
13
|
+
class ExitWithCommandsHelpPrinting < Struct.new(:commands_definition)
|
14
|
+
# Note that :long_help is not used.
|
15
|
+
def print_help(output, long_help)
|
16
|
+
output.puts "Valid commands:", "", " " + commands_definition.keys.join(', ')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ExitWithArgumentsHelpPrinting < Struct.new(:commands_stack, :args, :parser_opts_copy)
|
21
|
+
def print_help(output, long_help)
|
22
|
+
parser_opts_help = parser_opts_copy.to_s
|
23
|
+
|
24
|
+
if commands_stack.size > 0
|
25
|
+
parser_opts_help = parser_opts_help.sub!('[options]', commands_stack.join(' ') + ' [options]')
|
26
|
+
end
|
27
|
+
|
28
|
+
if args.size > 0
|
29
|
+
args_display = args.map { |name, mandatory| mandatory ? "<#{ name }>" : "[<#{ name }>]" }.join(' ')
|
30
|
+
parser_opts_help = parser_opts_help.sub!(/^(Usage: .*)/) { |text| "#{text} #{args_display}" }
|
31
|
+
end
|
32
|
+
|
33
|
+
output.puts parser_opts_help
|
34
|
+
output.puts "", long_help if long_help
|
35
|
+
end
|
36
|
+
end
|
8
37
|
|
9
38
|
extend self
|
10
39
|
|
11
|
-
def decode(*params_definition, arguments: ARGV, long_help: nil, output: $stdout)
|
40
|
+
def decode(*params_definition, arguments: ARGV, long_help: nil, auto_help: true, output: $stdout)
|
12
41
|
# WATCH OUT! @long_help can also be set in :decode_command!. See issue #17.
|
13
42
|
#
|
14
43
|
@long_help = long_help
|
15
|
-
@output = output
|
16
44
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
45
|
+
exit_data = catch(:exit) do
|
46
|
+
if params_definition.first.is_a?(Hash)
|
47
|
+
return decode_command!(params_definition, arguments, auto_help)
|
48
|
+
else
|
49
|
+
return decode_arguments!(params_definition, arguments, auto_help)
|
50
|
+
end
|
21
51
|
end
|
22
|
-
rescue ExitError
|
23
|
-
# return nil, to be used with the 'decode(...) || exit' pattern
|
24
|
-
ensure
|
25
|
-
# Clean up the instance variables.
|
26
|
-
#
|
27
|
-
# There is a balance to strike between instance variables, and local variables
|
28
|
-
# passed around. One of the options, which is this case, is to set and instance
|
29
|
-
# variables only these two, which are constant.
|
30
52
|
|
53
|
+
exit_data.print_help(output, @long_help)
|
54
|
+
|
55
|
+
nil # to be used with the 'decode(...) || exit' pattern
|
56
|
+
ensure
|
31
57
|
@long_help = nil
|
32
|
-
@output = nil
|
33
58
|
end
|
34
59
|
|
35
60
|
private
|
@@ -40,7 +65,7 @@ module SimpleScripting
|
|
40
65
|
#
|
41
66
|
# [{"command1"=>["arg1", {:long_help=>"This is the long help."}], "command2"=>["arg2"]}]
|
42
67
|
#
|
43
|
-
def decode_command!(params_definition, arguments, commands_stack=[])
|
68
|
+
def decode_command!(params_definition, arguments, auto_help, commands_stack=[])
|
44
69
|
commands_definition = params_definition.first
|
45
70
|
|
46
71
|
# Set the `command` variable only after; in the case where we print the help, this variable
|
@@ -48,8 +73,19 @@ module SimpleScripting
|
|
48
73
|
#
|
49
74
|
command_for_check = arguments.shift
|
50
75
|
|
76
|
+
# Note that `--help` is baked into OptParse, so without a workaround, we need to always include
|
77
|
+
# it.
|
78
|
+
#
|
51
79
|
if command_for_check == '-h' || command_for_check == '--help'
|
52
|
-
|
80
|
+
if auto_help
|
81
|
+
throw :exit, ExitWithCommandsHelpPrinting.new(commands_definition)
|
82
|
+
else
|
83
|
+
# This is tricky. Since the common behavior of `--help` is to trigger an unconditional
|
84
|
+
# help, it's not clear what to do with other tokens. For simplicity, we just return
|
85
|
+
# this flag.
|
86
|
+
#
|
87
|
+
return { help: true }
|
88
|
+
end
|
53
89
|
end
|
54
90
|
|
55
91
|
command = command_for_check
|
@@ -57,13 +93,13 @@ module SimpleScripting
|
|
57
93
|
|
58
94
|
case command_params_definition
|
59
95
|
when nil
|
60
|
-
|
96
|
+
raise InvalidCommand.new("Invalid command: #{command}")
|
61
97
|
when Hash
|
62
98
|
commands_stack << command
|
63
99
|
|
64
100
|
# Nested case! Decode recursively
|
65
101
|
#
|
66
|
-
decode_command!([command_params_definition], arguments, commands_stack)
|
102
|
+
decode_command!([command_params_definition], arguments, auto_help, commands_stack)
|
67
103
|
else
|
68
104
|
commands_stack << command
|
69
105
|
|
@@ -74,12 +110,12 @@ module SimpleScripting
|
|
74
110
|
|
75
111
|
[
|
76
112
|
compose_returned_commands(commands_stack),
|
77
|
-
decode_arguments!(command_params_definition, arguments, commands_stack),
|
113
|
+
decode_arguments!(command_params_definition, arguments, auto_help, commands_stack),
|
78
114
|
]
|
79
115
|
end
|
80
116
|
end
|
81
117
|
|
82
|
-
def decode_arguments!(params_definition, arguments, commands_stack=[])
|
118
|
+
def decode_arguments!(params_definition, arguments, auto_help, commands_stack=[])
|
83
119
|
result = {}
|
84
120
|
parser_opts_copy = nil # not available outside the block
|
85
121
|
args = {} # { 'name' => mandatory? }
|
@@ -92,12 +128,24 @@ module SimpleScripting
|
|
92
128
|
when String
|
93
129
|
process_argument_definition!(param_definition, args)
|
94
130
|
else
|
131
|
+
# This is an error in the params definition, so it doesn't follow the user error/help
|
132
|
+
# workflow.
|
133
|
+
#
|
95
134
|
raise "Unrecognized value: #{param_definition}"
|
96
135
|
end
|
97
136
|
end
|
98
137
|
|
138
|
+
# See --help note in :decode_command!.
|
139
|
+
#
|
99
140
|
parser_opts.on('-h', '--help', 'Help') do
|
100
|
-
|
141
|
+
if auto_help
|
142
|
+
throw :exit, ExitWithArgumentsHelpPrinting.new(commands_stack, args, parser_opts_copy)
|
143
|
+
else
|
144
|
+
# Needs to be better handled. When help is required, generally, it trumps the
|
145
|
+
# correctness of the rest of the options/arguments.
|
146
|
+
#
|
147
|
+
result[:help] = true
|
148
|
+
end
|
101
149
|
end
|
102
150
|
|
103
151
|
parser_opts_copy = parser_opts
|
@@ -146,7 +194,7 @@ module SimpleScripting
|
|
146
194
|
# Mandatory argument
|
147
195
|
if args.fetch(first_arg_name.to_sym)
|
148
196
|
if arguments.empty?
|
149
|
-
|
197
|
+
raise ArgumentError.new("Missing mandatory argument(s)")
|
150
198
|
else
|
151
199
|
name = args.keys.first[1..-1].to_sym
|
152
200
|
|
@@ -163,43 +211,17 @@ module SimpleScripting
|
|
163
211
|
def process_regular_argument!(arguments, result, commands_stack, args, parser_opts_copy)
|
164
212
|
min_args_size = args.count { |_, mandatory| mandatory }
|
165
213
|
|
166
|
-
|
167
|
-
|
214
|
+
if arguments.size < min_args_size
|
215
|
+
raise ArgumentError.new("Missing mandatory argument(s)")
|
216
|
+
elsif arguments.size > args.size
|
217
|
+
raise ArgumentError.new("Too many arguments")
|
218
|
+
else
|
168
219
|
arguments.zip(args) do |value, (name, _)|
|
169
220
|
result[name] = value
|
170
221
|
end
|
171
|
-
else
|
172
|
-
print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
|
173
222
|
end
|
174
223
|
end
|
175
224
|
|
176
|
-
# HELP #################################################
|
177
|
-
|
178
|
-
def print_optparse_commands_help(command, commands_definition)
|
179
|
-
@output.print "Invalid command. " if command
|
180
|
-
@output.puts "Valid commands:", "", " " + commands_definition.keys.join(', ')
|
181
|
-
|
182
|
-
raise ExitError
|
183
|
-
end
|
184
|
-
|
185
|
-
def print_optparse_arguments_help(commands_stack, args, parser_opts_copy)
|
186
|
-
parser_opts_help = parser_opts_copy.to_s
|
187
|
-
|
188
|
-
if commands_stack.size > 0
|
189
|
-
parser_opts_help = parser_opts_help.sub!('[options]', commands_stack.join(' ') + ' [options]')
|
190
|
-
end
|
191
|
-
|
192
|
-
if args.size > 0
|
193
|
-
args_display = args.map { |name, mandatory| mandatory ? "<#{ name }>" : "[<#{ name }>]" }.join(' ')
|
194
|
-
parser_opts_help = parser_opts_help.sub!(/^(Usage: .*)/) { |text| "#{text} #{args_display}" }
|
195
|
-
end
|
196
|
-
|
197
|
-
@output.puts parser_opts_help
|
198
|
-
@output.puts "", @long_help if @long_help
|
199
|
-
|
200
|
-
raise ExitError
|
201
|
-
end
|
202
|
-
|
203
225
|
# HELPERS ##############################################
|
204
226
|
|
205
227
|
def compose_returned_commands(commands_stack)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'tab_completion/commandline_processor'
|
4
|
+
|
5
|
+
module SimpleScripting
|
6
|
+
|
7
|
+
# The naming of each of the the commandline units is not standard, therefore we establish the
|
8
|
+
# following arbitrary, but consistent, naming:
|
9
|
+
#
|
10
|
+
# executable --option option_parameter argument
|
11
|
+
#
|
12
|
+
# The commandline is divided into words, as Bash would split them. All the words, except the
|
13
|
+
# executable, compose an array that we call `argv` (as Ruby would do).
|
14
|
+
#
|
15
|
+
# We define each {option name => value} or {argument name => value} as `pair`.
|
16
|
+
#
|
17
|
+
# In the context of a pair, each pair is composed of a `key` and a `value`.
|
18
|
+
#
|
19
|
+
class TabCompletion
|
20
|
+
|
21
|
+
def initialize(switches_definition, output: $stdout)
|
22
|
+
@switches_definition = switches_definition
|
23
|
+
@output = output
|
24
|
+
end
|
25
|
+
|
26
|
+
# Currently, any completion suffix is ignored and stripped.
|
27
|
+
#
|
28
|
+
def complete(execution_target, source_commandline=ENV.fetch('COMP_LINE'), cursor_position=ENV.fetch('COMP_POINT').to_i)
|
29
|
+
commandline_processor = CommandlineProcessor.process_commandline(source_commandline, cursor_position, @switches_definition)
|
30
|
+
|
31
|
+
if commandline_processor.completing_an_option?
|
32
|
+
complete_option(commandline_processor, execution_target)
|
33
|
+
elsif commandline_processor.parsing_error?
|
34
|
+
return
|
35
|
+
else # completing_a_value?
|
36
|
+
complete_value(commandline_processor, execution_target)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
#############################################
|
43
|
+
# Completion!
|
44
|
+
#############################################
|
45
|
+
|
46
|
+
def complete_option(commandline_processor, execution_target)
|
47
|
+
all_switches = @switches_definition.select { |definition| definition.is_a?(Array) }.map { |definition| definition[1][/^--\S+/] }
|
48
|
+
|
49
|
+
matching_switches = all_switches.select { |switch| switch.start_with?(commandline_processor.completing_word_prefix) }
|
50
|
+
|
51
|
+
output_entries(matching_switches)
|
52
|
+
end
|
53
|
+
|
54
|
+
def complete_value(commandline_processor, execution_target)
|
55
|
+
key, value_prefix, value_suffix, other_pairs = commandline_processor.parsed_pairs
|
56
|
+
|
57
|
+
selected_entries = execution_target.send(key, value_prefix, value_suffix, other_pairs)
|
58
|
+
|
59
|
+
output_entries(selected_entries)
|
60
|
+
end
|
61
|
+
|
62
|
+
#############################################
|
63
|
+
# Helpers
|
64
|
+
#############################################
|
65
|
+
|
66
|
+
def output_entries(entries)
|
67
|
+
@output.print entries.join("\n")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
require_relative '../argv'
|
6
|
+
|
7
|
+
module SimpleScripting
|
8
|
+
|
9
|
+
class TabCompletion
|
10
|
+
|
11
|
+
class CommandlineProcessor < Struct.new(:processed_argv, :cursor_marker, :switches_definition)
|
12
|
+
|
13
|
+
# Arbitrary; can be anything (except an empty string).
|
14
|
+
BASE_CURSOR_MARKER = "<tab>"
|
15
|
+
|
16
|
+
OPTIONS_TERMINATOR = "--"
|
17
|
+
LONG_OPTIONS_PREFIX = "--"
|
18
|
+
|
19
|
+
def self.process_commandline(source_commandline, cursor_position, switches_definition)
|
20
|
+
# An input string with infinite "<tabN>" substrings will cause an infinite cycle (hehe).
|
21
|
+
0.upto(Float::INFINITY) do |i|
|
22
|
+
cursor_marker = BASE_CURSOR_MARKER.sub(">", "#{i}>")
|
23
|
+
|
24
|
+
if !source_commandline.include?(cursor_marker)
|
25
|
+
commandline_with_marker = source_commandline[0...cursor_position] + cursor_marker + source_commandline[cursor_position..-1].to_s
|
26
|
+
|
27
|
+
# Remove the executable.
|
28
|
+
processed_argv = Shellwords.split(commandline_with_marker)[1..-1]
|
29
|
+
|
30
|
+
return new(processed_argv, cursor_marker, switches_definition)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# We're abstracted from the commandline, with this exception. This is because while an option
|
36
|
+
# is being completed, the decoder would not recognize the key.
|
37
|
+
#
|
38
|
+
def completing_an_option?
|
39
|
+
processed_argv[marked_word_position].start_with?(LONG_OPTIONS_PREFIX) && marked_word_position < options_terminator_position
|
40
|
+
end
|
41
|
+
|
42
|
+
def parsing_error?
|
43
|
+
parse_argv.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def completing_word_prefix
|
47
|
+
word = processed_argv[marked_word_position]
|
48
|
+
|
49
|
+
# Regex alternative: [/\A(.*?)#{cursor_marker}/m, 1]
|
50
|
+
word[0, word.index(cursor_marker)]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns key, value prefix (before marker), value suffix (after marker), other_pairs
|
54
|
+
#
|
55
|
+
def parsed_pairs
|
56
|
+
parsed_pairs = parse_argv || raise("Parsing error")
|
57
|
+
|
58
|
+
key, value = parsed_pairs.detect do |_, value|
|
59
|
+
!boolean?(value) && value.include?(cursor_marker)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Impossible case, unless there is a programmatic error.
|
63
|
+
#
|
64
|
+
key || raise("Guru meditation! (#{self.class}##{__method__}:#{__LINE__})")
|
65
|
+
|
66
|
+
value_prefix, value_suffix = value.split(cursor_marker)
|
67
|
+
|
68
|
+
parsed_pairs.delete(key)
|
69
|
+
|
70
|
+
[key, value_prefix || "", value_suffix || "", parsed_pairs]
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def marked_word_position
|
76
|
+
processed_argv.index { |word| word.include?(cursor_marker) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns Float::INFINITY when there is no options terminator.
|
80
|
+
#
|
81
|
+
def options_terminator_position
|
82
|
+
processed_argv.index(OPTIONS_TERMINATOR) || Float::INFINITY
|
83
|
+
end
|
84
|
+
|
85
|
+
#############################################
|
86
|
+
# Helpers
|
87
|
+
#############################################
|
88
|
+
|
89
|
+
def parse_argv
|
90
|
+
# We need to convert all the arguments to optional, otherwise it's not possible to
|
91
|
+
# autocomplete arguments when not all the mandatory ones are not typed yet, eg:
|
92
|
+
#
|
93
|
+
# `my_command <tab>` with definition ['mand1', 'mand2']
|
94
|
+
#
|
95
|
+
adapted_switches_definition = switches_definition.dup
|
96
|
+
|
97
|
+
adapted_switches_definition.each_with_index do |definition, i|
|
98
|
+
adapted_switches_definition[i] = "[#{definition}]" if definition.is_a?(String) && !definition.start_with?('[')
|
99
|
+
end
|
100
|
+
|
101
|
+
SimpleScripting::Argv.decode(*adapted_switches_definition, arguments: processed_argv.dup, auto_help: false)
|
102
|
+
rescue Argv::InvalidCommand, Argv::ArgumentError, OptionParser::InvalidOption
|
103
|
+
# OptionParser::InvalidOption: see case "-O<tab>" in test suite.
|
104
|
+
|
105
|
+
# return nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# For the lulz.
|
109
|
+
#
|
110
|
+
def boolean?(value)
|
111
|
+
!!value == value
|
112
|
+
end
|
113
|
+
|
114
|
+
end # CommandlineProcessor
|
115
|
+
|
116
|
+
end # TabCompletion
|
117
|
+
|
118
|
+
end # SimpleScripting
|
data/simple_scripting.gemspec
CHANGED
@@ -8,8 +8,9 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.name = "simple_scripting"
|
9
9
|
s.version = SimpleScripting::VERSION
|
10
10
|
s.platform = Gem::Platform::RUBY
|
11
|
+
s.required_ruby_version = '>= 2.3.0'
|
11
12
|
s.authors = ["Saverio Miroddi"]
|
12
|
-
s.date = "2018-
|
13
|
+
s.date = "2018-07-26"
|
13
14
|
s.email = ["saverio.pub2@gmail.com"]
|
14
15
|
s.homepage = "https://github.com/saveriomiroddi/simple_scripting"
|
15
16
|
s.summary = "Library for simplifying some typical scripting functionalities."
|
@@ -20,7 +21,6 @@ Gem::Specification.new do |s|
|
|
20
21
|
|
21
22
|
s.add_development_dependency "rake", "~> 12.0"
|
22
23
|
s.add_development_dependency "rspec", "~> 3.6"
|
23
|
-
s.add_development_dependency 'coveralls', "~> 0.8.21"
|
24
24
|
|
25
25
|
s.files = `git ls-files`.split("\n")
|
26
26
|
s.test_files = `git ls-files -- spec/*`.split("\n")
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
# Custom matchers for the tab completion test suite.
|
4
|
+
#
|
5
|
+
# Require :subject and :output_buffer to be defined/accessible.
|
6
|
+
#
|
7
|
+
# The matchers are simplistic (but still adequate); the (most) appropriate choice would be
|
8
|
+
# [diffable matchers](https://relishapp.com/rspec/rspec-expectations/v/3-6/docs/custom-matchers/define-diffable-matcher)
|
9
|
+
#
|
10
|
+
# Note that the semantic of the expected value is different from the standard rspec one, since we
|
11
|
+
# process it, therefore, we need to use intermediate instance variables.
|
12
|
+
#
|
13
|
+
module TabCompletionCustomRSpecMatchers
|
14
|
+
|
15
|
+
extend RSpec::Matchers::DSL
|
16
|
+
|
17
|
+
# Doesn't matter in this context.
|
18
|
+
#
|
19
|
+
PHONY_EXECUTABLE = '/path/to/executable'
|
20
|
+
|
21
|
+
matcher :complete_with do |expected_entries|
|
22
|
+
match do |symbolic_commandline_options|
|
23
|
+
commandline = "#{PHONY_EXECUTABLE} #{symbolic_commandline_options}"
|
24
|
+
cursor_position = commandline.index("<tab>")
|
25
|
+
commandline = commandline.sub("<tab>", "")
|
26
|
+
|
27
|
+
subject.complete(execution_target, commandline, cursor_position)
|
28
|
+
|
29
|
+
@actual_output = output_buffer.string
|
30
|
+
expected_output = expected_entries.join("\n")
|
31
|
+
|
32
|
+
expect(@actual_output).to eql(expected_output)
|
33
|
+
end
|
34
|
+
|
35
|
+
failure_message do |actual|
|
36
|
+
actual_entries = @actual_output.split("\n")
|
37
|
+
|
38
|
+
"#{actual} listed #{actual_entries} instead of #{expected}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
matcher :not_complete do
|
43
|
+
match do |symbolic_commandline_options|
|
44
|
+
expect(symbolic_commandline_options).to complete_with([])
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message do |actual|
|
48
|
+
@actual_output = output_buffer.string
|
49
|
+
actual_entries = @actual_output.split("\n")
|
50
|
+
|
51
|
+
"#{actual} listed #{actual_entries} instead of no entries"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end # TabCompletionCustomRSpecMatchers
|
@@ -23,27 +23,59 @@ describe SimpleScripting::Argv do
|
|
23
23
|
output: output_buffer,
|
24
24
|
]}
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
26
|
+
context 'help' do
|
27
|
+
|
28
|
+
it 'should print help automatically by default' do
|
29
|
+
decoder_params.last[:arguments] = ['-h']
|
30
|
+
|
31
|
+
return_value = described_class.decode(*decoder_params)
|
32
|
+
|
33
|
+
expected_output = <<~OUTPUT
|
34
|
+
Usage: rspec [options] <mandatory> [<optional>]
|
35
|
+
-a
|
36
|
+
-b "-b" description
|
37
|
+
-c, --c-switch
|
38
|
+
-d, --d-switch "-d" description
|
39
|
+
-e, --e-switch VALUE
|
40
|
+
-f, --f-switch VALUE "-f" description
|
41
|
+
-h, --help Help
|
42
|
+
|
43
|
+
This is the long help!
|
44
|
+
OUTPUT
|
45
|
+
|
46
|
+
expect(output_buffer.string).to eql(expected_output)
|
47
|
+
expect(return_value).to be(nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should not interpret the --help argument, and not print the help, on auto_help: false' do
|
51
|
+
decoder_params.last.merge!(
|
52
|
+
arguments: ['--help', 'm_arg'],
|
53
|
+
auto_help: false
|
54
|
+
)
|
55
|
+
|
56
|
+
actual_result = described_class.decode(*decoder_params)
|
57
|
+
|
58
|
+
expected_result = {
|
59
|
+
help: true,
|
60
|
+
mandatory: 'm_arg',
|
61
|
+
}
|
62
|
+
|
63
|
+
expect(output_buffer.string).to eql('')
|
64
|
+
expect(actual_result).to eql(expected_result)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should check all the options/arguments when --help is passed, raising an error when they're not correct" do
|
68
|
+
decoder_params.last.merge!(
|
69
|
+
arguments: ['--help'],
|
70
|
+
auto_help: false
|
71
|
+
)
|
72
|
+
|
73
|
+
decoding = -> { described_class.decode(*decoder_params) }
|
74
|
+
|
75
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::ArgumentError, "Missing mandatory argument(s)")
|
76
|
+
end
|
77
|
+
|
78
|
+
end # context 'help'
|
47
79
|
|
48
80
|
it "should implement basic switches and arguments (all set)" do
|
49
81
|
decoder_params.last[:arguments] = ['-a', '-b', '-c', '-d', '-ev_swt', '-fv_swt', 'm_arg', 'o_arg']
|
@@ -76,7 +108,62 @@ This is the long help!
|
|
76
108
|
expect(actual_result).to eql(expected_result)
|
77
109
|
end
|
78
110
|
|
79
|
-
|
111
|
+
context "multiple optional arguments" do
|
112
|
+
|
113
|
+
let(:decoder_params) {[
|
114
|
+
'[optional1]',
|
115
|
+
'[optional2]',
|
116
|
+
output: output_buffer,
|
117
|
+
]}
|
118
|
+
|
119
|
+
it "should correctly decode a single argument passed" do
|
120
|
+
decoder_params.last[:arguments] = ['o_arg1']
|
121
|
+
|
122
|
+
actual_result = described_class.decode(*decoder_params)
|
123
|
+
|
124
|
+
expected_result = {
|
125
|
+
optional1: 'o_arg1',
|
126
|
+
}
|
127
|
+
|
128
|
+
expect(actual_result).to eql(expected_result)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should correctly decode all arguments passed" do
|
132
|
+
decoder_params.last[:arguments] = ['o_arg1', 'o_arg2']
|
133
|
+
|
134
|
+
actual_result = described_class.decode(*decoder_params)
|
135
|
+
|
136
|
+
expected_result = {
|
137
|
+
optional1: 'o_arg1',
|
138
|
+
optional2: 'o_arg2',
|
139
|
+
}
|
140
|
+
|
141
|
+
expect(actual_result).to eql(expected_result)
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
context "error handling" do
|
147
|
+
|
148
|
+
it "should raise an error when mandatory arguments are missing" do
|
149
|
+
decoder_params.last[:arguments] = []
|
150
|
+
|
151
|
+
decoding = -> { described_class.decode(*decoder_params) }
|
152
|
+
|
153
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::ArgumentError, "Missing mandatory argument(s)")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should print an error and the help when there are too many arguments" do
|
157
|
+
decoder_params.last[:arguments] = ['arg1', 'arg2', 'excessive_arg']
|
158
|
+
|
159
|
+
decoding = -> { described_class.decode(*decoder_params) }
|
160
|
+
|
161
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::ArgumentError, "Too many arguments")
|
162
|
+
end
|
163
|
+
|
164
|
+
end # context "error handling"
|
165
|
+
|
166
|
+
end # describe 'Basic functionality'
|
80
167
|
|
81
168
|
describe 'Varargs' do
|
82
169
|
|
@@ -99,17 +186,19 @@ This is the long help!
|
|
99
186
|
expect(actual_result).to eql(expected_result)
|
100
187
|
end
|
101
188
|
|
102
|
-
|
103
|
-
decoder_params.last[:arguments] = []
|
189
|
+
context "error handling" do
|
104
190
|
|
105
|
-
|
191
|
+
it "should exit when they are not specified" do
|
192
|
+
decoder_params.last[:arguments] = []
|
106
193
|
|
107
|
-
|
194
|
+
decoding = -> { described_class.decode(*decoder_params) }
|
108
195
|
|
109
|
-
|
110
|
-
|
196
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::ArgumentError, "Missing mandatory argument(s)")
|
197
|
+
end
|
111
198
|
|
112
|
-
|
199
|
+
end # context "error handling"
|
200
|
+
|
201
|
+
end # describe '(mandatory)'
|
113
202
|
|
114
203
|
describe '(optional)' do
|
115
204
|
|
@@ -142,9 +231,9 @@ This is the long help!
|
|
142
231
|
expect(actual_result).to eql(expected_result)
|
143
232
|
end
|
144
233
|
|
145
|
-
end
|
234
|
+
end # describe '(optional)'
|
146
235
|
|
147
|
-
end
|
236
|
+
end # describe 'Varargs'
|
148
237
|
|
149
238
|
describe 'Commands' do
|
150
239
|
|
@@ -171,50 +260,86 @@ This is the long help!
|
|
171
260
|
expect(actual_result).to eql(expected_result)
|
172
261
|
end
|
173
262
|
|
174
|
-
|
175
|
-
decoder_params[:arguments] = ['pizza']
|
263
|
+
context "error handling" do
|
176
264
|
|
177
|
-
|
265
|
+
it "should raise an error on invalid command" do
|
266
|
+
decoder_params[:arguments] = ['pizza']
|
178
267
|
|
179
|
-
|
180
|
-
Invalid command. Valid commands:
|
268
|
+
decoding = -> { described_class.decode(decoder_params) }
|
181
269
|
|
182
|
-
|
183
|
-
|
270
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::InvalidCommand, "Invalid command: pizza")
|
271
|
+
end
|
184
272
|
|
185
|
-
|
186
|
-
end
|
273
|
+
end # context "error handling"
|
187
274
|
|
188
|
-
|
189
|
-
decoder_params[:arguments] = ['-h']
|
275
|
+
context "help" do
|
190
276
|
|
191
|
-
|
277
|
+
it 'should implement the commands help' do
|
278
|
+
decoder_params[:arguments] = ['-h']
|
192
279
|
|
193
|
-
|
194
|
-
Valid commands:
|
280
|
+
described_class.decode(decoder_params)
|
195
281
|
|
196
|
-
|
197
|
-
|
282
|
+
expected_output = <<~OUTPUT
|
283
|
+
Valid commands:
|
198
284
|
|
199
|
-
|
200
|
-
|
285
|
+
command1, command2
|
286
|
+
OUTPUT
|
201
287
|
|
202
|
-
|
203
|
-
|
204
|
-
$a = true
|
205
|
-
described_class.decode(decoder_params)
|
288
|
+
expect(output_buffer.string).to eql(expected_output)
|
289
|
+
end
|
206
290
|
|
207
|
-
|
208
|
-
|
209
|
-
-h, --help Help
|
291
|
+
it "should display the command given command's help" do
|
292
|
+
decoder_params[:arguments] = ['command1', '-h']
|
210
293
|
|
211
|
-
|
212
|
-
}
|
294
|
+
described_class.decode(decoder_params)
|
213
295
|
|
214
|
-
|
215
|
-
|
296
|
+
expected_output = <<~OUTPUT
|
297
|
+
Usage: rspec command1 [options] <arg1>
|
298
|
+
-h, --help Help
|
216
299
|
|
217
|
-
|
300
|
+
This is the long help.
|
301
|
+
OUTPUT
|
302
|
+
|
303
|
+
expect(output_buffer.string).to eql(expected_output)
|
304
|
+
end
|
305
|
+
|
306
|
+
context 'auto_help: false' do
|
307
|
+
|
308
|
+
it 'should not interpret the --help argument, and not print the help' do
|
309
|
+
decoder_params.merge!(
|
310
|
+
arguments: ['-h'],
|
311
|
+
auto_help: false,
|
312
|
+
)
|
313
|
+
|
314
|
+
actual_result = described_class.decode(decoder_params)
|
315
|
+
|
316
|
+
expected_result = {
|
317
|
+
help: true,
|
318
|
+
}
|
319
|
+
|
320
|
+
expect(actual_result).to eql(expected_result)
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'should ignore and not return all the other arguments' do
|
324
|
+
decoder_params.merge!(
|
325
|
+
arguments: ['-h', 'pizza'],
|
326
|
+
auto_help: false,
|
327
|
+
)
|
328
|
+
|
329
|
+
actual_result = described_class.decode(decoder_params)
|
330
|
+
|
331
|
+
expected_result = {
|
332
|
+
help: true,
|
333
|
+
}
|
334
|
+
|
335
|
+
expect(actual_result).to eql(expected_result)
|
336
|
+
end
|
337
|
+
|
338
|
+
end # context 'auto_help: false'
|
339
|
+
|
340
|
+
end # context 'help'
|
341
|
+
|
342
|
+
end # describe 'regular case'
|
218
343
|
|
219
344
|
describe 'Nested commands' do
|
220
345
|
|
@@ -259,11 +384,11 @@ This is the long help.
|
|
259
384
|
|
260
385
|
actual_result = described_class.decode(decoder_params)
|
261
386
|
|
262
|
-
expected_output =
|
263
|
-
Valid commands:
|
387
|
+
expected_output = <<~OUTPUT
|
388
|
+
Valid commands:
|
264
389
|
|
265
|
-
|
266
|
-
|
390
|
+
nested1a, nested1b
|
391
|
+
OUTPUT
|
267
392
|
|
268
393
|
expect(output_buffer.string).to eql(expected_output)
|
269
394
|
end
|
@@ -273,33 +398,35 @@ Valid commands:
|
|
273
398
|
|
274
399
|
actual_result = described_class.decode(decoder_params)
|
275
400
|
|
276
|
-
expected_output =
|
277
|
-
Usage: rspec command1 nested1a [options] <arg1>
|
278
|
-
|
401
|
+
expected_output = <<~OUTPUT
|
402
|
+
Usage: rspec command1 nested1a [options] <arg1>
|
403
|
+
-h, --help Help
|
279
404
|
|
280
|
-
nested1a long help.
|
281
|
-
|
405
|
+
nested1a long help.
|
406
|
+
OUTPUT
|
282
407
|
|
283
408
|
expect(output_buffer.string).to eql(expected_output)
|
284
409
|
end
|
285
|
-
end
|
410
|
+
end # describe 'Nested commands'
|
286
411
|
|
287
|
-
|
412
|
+
end # describe 'Commands'
|
288
413
|
|
289
|
-
|
290
|
-
|
291
|
-
|
414
|
+
# Special case.
|
415
|
+
#
|
416
|
+
describe 'No definitions given' do
|
292
417
|
|
293
|
-
|
294
|
-
|
418
|
+
let(:decoder_params) {{
|
419
|
+
output: output_buffer,
|
420
|
+
}}
|
295
421
|
|
296
|
-
|
422
|
+
it 'should avoid options being interpreted as definitions' do
|
423
|
+
decoder_params[:arguments] = ['pizza']
|
297
424
|
|
298
|
-
|
299
|
-
end
|
425
|
+
decoding = -> { described_class.decode(decoder_params) }
|
300
426
|
|
427
|
+
expect(decoding).to raise_error(SimpleScripting::Argv::ArgumentError, "Too many arguments")
|
301
428
|
end
|
302
429
|
|
303
|
-
end
|
430
|
+
end # describe 'No definitions given'
|
304
431
|
|
305
432
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../lib/simple_scripting/tab_completion.rb'
|
4
|
+
|
5
|
+
describe SimpleScripting::TabCompletion do
|
6
|
+
|
7
|
+
include TabCompletionCustomRSpecMatchers
|
8
|
+
|
9
|
+
let(:output_buffer) {
|
10
|
+
StringIO.new
|
11
|
+
}
|
12
|
+
|
13
|
+
let(:switches_definition) {
|
14
|
+
[
|
15
|
+
["-o", "--opt1 ARG"],
|
16
|
+
["-O", "--opt2"],
|
17
|
+
"arg1", # this and the following are internally converted to optional, as
|
18
|
+
"arg2", # according to the Argv spec, without brackets they are mandatory.
|
19
|
+
]
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:execution_target) {
|
23
|
+
# Simplistic implementation. In real world, regex are not to be used this way, for multiple
|
24
|
+
# reasons.
|
25
|
+
#
|
26
|
+
Class.new do
|
27
|
+
def opt1(prefix, suffix, context)
|
28
|
+
%w(opt1v1 _opt1v2).select { |entry| entry =~ /^#{prefix}#{suffix}/ }
|
29
|
+
end
|
30
|
+
|
31
|
+
def arg1(prefix, suffix, context)
|
32
|
+
# A value starting with space is valid.
|
33
|
+
#
|
34
|
+
['arg1v1', 'arg1v2', '_arg1v3', ' _argv1spc'].select { |entry| entry =~ /^#{prefix}#{suffix}/ }
|
35
|
+
end
|
36
|
+
|
37
|
+
def arg2(prefix, suffix, context)
|
38
|
+
# A value starting with minus is valid.
|
39
|
+
#
|
40
|
+
%w(arg2v1 arg2v2 --arg2v3).select { |entry| entry =~ /^#{prefix}#{suffix}/ }
|
41
|
+
end
|
42
|
+
end.new
|
43
|
+
}
|
44
|
+
|
45
|
+
subject { described_class.new(switches_definition, output: output_buffer) }
|
46
|
+
|
47
|
+
context "with a correct configuration" do
|
48
|
+
|
49
|
+
context "standard cases" do
|
50
|
+
|
51
|
+
# Note that the conversion of mandatory to optional argument is defined by most of the cases.
|
52
|
+
#
|
53
|
+
STANDARD_CASES = {
|
54
|
+
"<tab>" => ["arg1v1", "arg1v2", "_arg1v3", " _argv1spc"],
|
55
|
+
"a<tab>" => %w(arg1v1 arg1v2),
|
56
|
+
"--opt2 <tab>" => ["arg1v1", "arg1v2", "_arg1v3", " _argv1spc"],
|
57
|
+
"-- <tab>" => ["arg1v1", "arg1v2", "_arg1v3", " _argv1spc"],
|
58
|
+
|
59
|
+
"a <tab>" => %w(arg2v1 arg2v2 --arg2v3),
|
60
|
+
"a -- --<tab>" => %w(--arg2v3),
|
61
|
+
"-- --aaa <tab>" => %w(arg2v1 arg2v2 --arg2v3),
|
62
|
+
|
63
|
+
"--<tab>" => %w(--opt1 --opt2),
|
64
|
+
"--<tab> a" => %w(--opt1 --opt2),
|
65
|
+
"--<tab> -- a" => %w(--opt1 --opt2),
|
66
|
+
"--<tab> -- b" => %w(--opt1 --opt2),
|
67
|
+
"--<tab> --xyz" => %w(--opt1 --opt2),
|
68
|
+
"--opt1 <tab> a" => %w(opt1v1 _opt1v2),
|
69
|
+
"--opt1 o<tab> a" => %w(opt1v1),
|
70
|
+
|
71
|
+
"-o<tab>" => %w(opt1v1 _opt1v2),
|
72
|
+
"-o <tab>" => %w(opt1v1 _opt1v2),
|
73
|
+
"-o -O <tab>" => ["arg1v1", "arg1v2", "_arg1v3", " _argv1spc"],
|
74
|
+
"-O <tab>" => ["arg1v1", "arg1v2", "_arg1v3", " _argv1spc"],
|
75
|
+
}
|
76
|
+
|
77
|
+
STANDARD_CASES.each do |symbolic_commandline_options, expected_entries|
|
78
|
+
it "should output the entries for #{symbolic_commandline_options.inspect}" do
|
79
|
+
expect(symbolic_commandline_options).to complete_with(expected_entries)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # context "standard cases"
|
84
|
+
|
85
|
+
context "suffix management" do
|
86
|
+
|
87
|
+
SUFFIX_CASES = {
|
88
|
+
"arg1<tab>v" => %w(arg1v1 arg1v2), # the execution target of the test suite doesn't
|
89
|
+
"arg1<tab>x" => %w(), # ignore the suffix; programmer-defined
|
90
|
+
|
91
|
+
"--o<tab>p" => %w(--opt1 --opt2), # options ignore the suffix (like bash); can't be
|
92
|
+
"--o<tab>x" => %w(--opt1 --opt2), # currently changed by the programmer.
|
93
|
+
}
|
94
|
+
|
95
|
+
SUFFIX_CASES.each do |symbolic_commandline_options, expected_entries|
|
96
|
+
it "should output the entries for #{symbolic_commandline_options.inspect}, ignoring the suffix" do
|
97
|
+
expect(symbolic_commandline_options).to complete_with(expected_entries)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end # context "suffix management"
|
102
|
+
|
103
|
+
context "escaped cases" do
|
104
|
+
|
105
|
+
ESCAPED_CASES = {
|
106
|
+
"\ <tab>" => [" _argv1spc"],
|
107
|
+
'\-<tab>' => %w(), # this is the result of typing `command "\-<tab>`
|
108
|
+
'a \-<tab>' => %w(--arg2v3),
|
109
|
+
}
|
110
|
+
|
111
|
+
ESCAPED_CASES.each do |symbolic_commandline_options, _|
|
112
|
+
it "should output the entries for #{symbolic_commandline_options.inspect}"
|
113
|
+
end
|
114
|
+
|
115
|
+
end # context "escaped cases"
|
116
|
+
|
117
|
+
it "should support multiple values for an option"
|
118
|
+
|
119
|
+
it "should keep parsing also when --help is passed" do
|
120
|
+
expect("--help a<tab>").to complete_with(%w(arg1v1 arg1v2))
|
121
|
+
end
|
122
|
+
|
123
|
+
end # context "with a correct configuration"
|
124
|
+
|
125
|
+
context "with an incorrect configuration" do
|
126
|
+
|
127
|
+
INCORRECT_CASES = [
|
128
|
+
"a b <tab>", # too many args
|
129
|
+
"-O<tab>", # no values for this option
|
130
|
+
]
|
131
|
+
|
132
|
+
INCORRECT_CASES.each do |symbolic_commandline_options|
|
133
|
+
it "should not output any entries for #{symbolic_commandline_options.inspect}" do
|
134
|
+
expect(symbolic_commandline_options).to not_complete
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end # context "with an incorrect configuration"
|
139
|
+
|
140
|
+
end # describe SimpleScripting::TabCompletion
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_scripting
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Saverio Miroddi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parseconfig
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.6'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: coveralls
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 0.8.21
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 0.8.21
|
69
55
|
description: Simplifies options parsing and configuration loading.
|
70
56
|
email:
|
71
57
|
- saverio.pub2@gmail.com
|
@@ -75,6 +61,7 @@ extra_rdoc_files: []
|
|
75
61
|
files:
|
76
62
|
- ".gitignore"
|
77
63
|
- ".rspec"
|
64
|
+
- ".simplecov"
|
78
65
|
- ".travis.yml"
|
79
66
|
- Gemfile
|
80
67
|
- Gemfile.lock
|
@@ -84,11 +71,15 @@ files:
|
|
84
71
|
- lib/simple_scripting/argv.rb
|
85
72
|
- lib/simple_scripting/configuration.rb
|
86
73
|
- lib/simple_scripting/configuration/value.rb
|
74
|
+
- lib/simple_scripting/tab_completion.rb
|
75
|
+
- lib/simple_scripting/tab_completion/commandline_processor.rb
|
87
76
|
- lib/simple_scripting/version.rb
|
88
77
|
- simple_scripting.gemspec
|
78
|
+
- spec/helpers/tab_completion_custom_rspec_matchers.rb
|
89
79
|
- spec/simple_scripting/argv_spec.rb
|
90
80
|
- spec/simple_scripting/configuration/value_spec.rb
|
91
81
|
- spec/simple_scripting/configuration_spec.rb
|
82
|
+
- spec/simple_scripting/tab_completion_spec.rb
|
92
83
|
- spec/spec_helper.rb
|
93
84
|
homepage: https://github.com/saveriomiroddi/simple_scripting
|
94
85
|
licenses:
|
@@ -102,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
93
|
requirements:
|
103
94
|
- - ">="
|
104
95
|
- !ruby/object:Gem::Version
|
105
|
-
version:
|
96
|
+
version: 2.3.0
|
106
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
98
|
requirements:
|
108
99
|
- - ">="
|
@@ -110,12 +101,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
101
|
version: '0'
|
111
102
|
requirements: []
|
112
103
|
rubyforge_project:
|
113
|
-
rubygems_version: 2.6.
|
104
|
+
rubygems_version: 2.6.13
|
114
105
|
signing_key:
|
115
106
|
specification_version: 4
|
116
107
|
summary: Library for simplifying some typical scripting functionalities.
|
117
108
|
test_files:
|
109
|
+
- spec/helpers/tab_completion_custom_rspec_matchers.rb
|
118
110
|
- spec/simple_scripting/argv_spec.rb
|
119
111
|
- spec/simple_scripting/configuration/value_spec.rb
|
120
112
|
- spec/simple_scripting/configuration_spec.rb
|
113
|
+
- spec/simple_scripting/tab_completion_spec.rb
|
121
114
|
- spec/spec_helper.rb
|