simple_scripting 0.9.4 → 0.10.0
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/.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
|