completely 0.8.0.rc5 → 0.8.0.rc6
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/README.md +30 -0
- data/lib/completely/commands/init.rb +1 -1
- data/lib/completely/completions.rb +35 -22
- data/lib/completely/pattern_config.rb +64 -23
- data/lib/completely/templates/pattern-config/sample.yaml +8 -0
- data/lib/completely/templates/pattern-config/template.erb +77 -47
- data/lib/completely/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd6a89f32abe46975fa2f733f3ec78ac814a9af33695e2527a1df270a3693f80
|
|
4
|
+
data.tar.gz: 3452a9ffa65f0fdbc70d96c3163c19036b4fae67dbad8377d1c6525735287b1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 137940d533ad4b1565d6447bbcea7fec87abbfda0df9956e6a876339d0aebe18e072282e32d92df5003c5a5b6eb0a3f070c5b4b24598ee09ab43502bbc34e248
|
|
7
|
+
data.tar.gz: 4accdb72c5cbe8921b88cfc1429b7bbcf0ac85ef24e607cc2b700f1f38080759ff5f54ae21b624317ee1ff4583885fc1ee5e446c930243381d8ecd3179eaa852
|
data/README.md
CHANGED
|
@@ -115,6 +115,36 @@ The `patterns` section describes valid command shapes:
|
|
|
115
115
|
- `<token>` references `tokens.token`.
|
|
116
116
|
- `<token>...` marks the final positional as repeatable.
|
|
117
117
|
|
|
118
|
+
Pattern config is compiled as a command tree. Option group placement is
|
|
119
|
+
therefore meaningful: an option group belongs to the command word immediately
|
|
120
|
+
before it. In the example above, `root` options belong to `mygit`, `init`
|
|
121
|
+
options belong to `mygit init`, and `status` options belong to `mygit status`.
|
|
122
|
+
|
|
123
|
+
This is useful for commands that have global options and command-specific
|
|
124
|
+
options:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
patterns:
|
|
128
|
+
- docker [global options] container [container options]
|
|
129
|
+
- docker [global options] container cp [cp options] <source> <dest>
|
|
130
|
+
|
|
131
|
+
options:
|
|
132
|
+
global:
|
|
133
|
+
- --config <file>
|
|
134
|
+
container:
|
|
135
|
+
- --latest
|
|
136
|
+
cp:
|
|
137
|
+
- -a|--archive
|
|
138
|
+
|
|
139
|
+
tokens:
|
|
140
|
+
file: +file
|
|
141
|
+
source: [container:/app, local.txt]
|
|
142
|
+
dest: [container:/tmp, ./out]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
If a completed command line contains an option that is not valid at the current
|
|
146
|
+
node, Completely stops offering suggestions for that command line.
|
|
147
|
+
|
|
118
148
|
The `options` section defines option groups:
|
|
119
149
|
|
|
120
150
|
```yaml
|
|
@@ -32,7 +32,7 @@ module Completely
|
|
|
32
32
|
|
|
33
33
|
def sample_path
|
|
34
34
|
@sample_path ||= begin
|
|
35
|
-
raise Error, "Invalid format: #{format}" unless sample_filenames.
|
|
35
|
+
raise Error, "Invalid format: #{format}" unless sample_filenames.has_key? format
|
|
36
36
|
|
|
37
37
|
File.expand_path "../templates/#{sample_filename}", __dir__
|
|
38
38
|
end
|
|
@@ -101,48 +101,61 @@ module Completely
|
|
|
101
101
|
config.is_a? PatternConfig
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def
|
|
105
|
-
config.model[:
|
|
104
|
+
def pattern_tree
|
|
105
|
+
config.model[:tree]
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
def
|
|
109
|
-
|
|
108
|
+
def pattern_nodes
|
|
109
|
+
@pattern_nodes ||= flatten_pattern_tree pattern_tree
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
word = route[:words][1]
|
|
115
|
-
word ? [word[:name], *word[:aliases]] : []
|
|
116
|
-
end.uniq
|
|
112
|
+
def pattern_programs
|
|
113
|
+
config.model[:programs]
|
|
117
114
|
end
|
|
118
115
|
|
|
119
|
-
def
|
|
120
|
-
|
|
116
|
+
def pattern_node_id(node)
|
|
117
|
+
pattern_nodes.index { |entry| entry[:node].equal? node }
|
|
121
118
|
end
|
|
122
119
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
names.map { |name| %["${non_options[#{index}]}" == "#{bash_escape name}"] }.join(' || ')
|
|
120
|
+
def pattern_node_options(node)
|
|
121
|
+
node[:option_groups].flat_map do |name|
|
|
122
|
+
config.model[:options][name] || []
|
|
127
123
|
end
|
|
128
124
|
end
|
|
129
125
|
|
|
130
|
-
def
|
|
131
|
-
|
|
126
|
+
def pattern_node_depth(node)
|
|
127
|
+
pattern_nodes.dig(pattern_node_id(node), :depth)
|
|
132
128
|
end
|
|
133
129
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
def pattern_child_transitions(node)
|
|
131
|
+
node[:children].flat_map do |child|
|
|
132
|
+
pattern_word_names(child[:word]).map do |name|
|
|
133
|
+
{ name: name, node: child }
|
|
134
|
+
end
|
|
137
135
|
end
|
|
138
136
|
end
|
|
139
137
|
|
|
138
|
+
def pattern_node_child_words(node)
|
|
139
|
+
node[:children].flat_map { |child| pattern_word_names child[:word] }.uniq
|
|
140
|
+
end
|
|
141
|
+
|
|
140
142
|
def pattern_has_unique_options?
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
pattern_nodes.any? do |entry|
|
|
144
|
+
pattern_node_options(entry[:node]).any? { |option| !option[:repeatable] }
|
|
143
145
|
end
|
|
144
146
|
end
|
|
145
147
|
|
|
148
|
+
def pattern_word_names(word)
|
|
149
|
+
[word[:name], *word[:aliases]]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def flatten_pattern_tree(node, depth = 0)
|
|
153
|
+
[
|
|
154
|
+
{ node: node, depth: depth },
|
|
155
|
+
*node[:children].flat_map { |child| flatten_pattern_tree child, depth + 1 },
|
|
156
|
+
]
|
|
157
|
+
end
|
|
158
|
+
|
|
146
159
|
def pattern_source_empty?(source)
|
|
147
160
|
source[:items].empty?
|
|
148
161
|
end
|
|
@@ -11,10 +11,11 @@ module Completely
|
|
|
11
11
|
validate!
|
|
12
12
|
|
|
13
13
|
@model ||= {
|
|
14
|
-
program:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
program: program,
|
|
15
|
+
programs: programs,
|
|
16
|
+
tree: tree,
|
|
17
|
+
options: parsed_options,
|
|
18
|
+
tokens: tokens,
|
|
18
19
|
}
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -37,11 +38,63 @@ module Completely
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def program
|
|
40
|
-
|
|
41
|
+
programs.first
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
def
|
|
44
|
-
@
|
|
44
|
+
def programs
|
|
45
|
+
@programs ||= patterns.filter_map do |pattern|
|
|
46
|
+
part = pattern_parts(pattern).find { |pattern_part| command_word? pattern_part }
|
|
47
|
+
parse_word(part)[:name] if part
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def tree
|
|
52
|
+
@tree ||= begin
|
|
53
|
+
root = nil
|
|
54
|
+
|
|
55
|
+
patterns.each do |pattern|
|
|
56
|
+
current = nil
|
|
57
|
+
|
|
58
|
+
pattern_parts(pattern).each do |part|
|
|
59
|
+
if option_group?(part)
|
|
60
|
+
add_option_group current, option_group_name(part)
|
|
61
|
+
elsif token?(part)
|
|
62
|
+
current[:positionals] << parse_token(part)
|
|
63
|
+
else
|
|
64
|
+
word = parse_word part
|
|
65
|
+
current = current ? find_or_create_child(current, word) : (root ||= build_tree_node(word))
|
|
66
|
+
merge_word! current[:word], word
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
root
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_tree_node(word)
|
|
76
|
+
{ word: word, option_groups: [], positionals: [], children: [] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_option_group(node, name)
|
|
80
|
+
node[:option_groups] << name unless node[:option_groups].include? name
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find_or_create_child(node, word)
|
|
84
|
+
node[:children].find { |child| same_word? child[:word], word } ||
|
|
85
|
+
node[:children].tap { |children| children << build_tree_node(word) }.last
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def same_word?(left, right)
|
|
89
|
+
word_names(left).intersect? word_names(right)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def word_names(word)
|
|
93
|
+
[word[:name], *word[:aliases]]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def merge_word!(target, source)
|
|
97
|
+
target[:aliases] = (word_names(target) | word_names(source)) - [target[:name]]
|
|
45
98
|
end
|
|
46
99
|
|
|
47
100
|
def parsed_options
|
|
@@ -99,22 +152,6 @@ module Completely
|
|
|
99
152
|
end
|
|
100
153
|
end
|
|
101
154
|
|
|
102
|
-
def parse_pattern(pattern)
|
|
103
|
-
result = { words: [], option_groups: [], positionals: [] }
|
|
104
|
-
|
|
105
|
-
pattern_parts(pattern).each do |part|
|
|
106
|
-
if option_group?(part)
|
|
107
|
-
result[:option_groups] << option_group_name(part)
|
|
108
|
-
elsif token?(part)
|
|
109
|
-
result[:positionals] << parse_token(part)
|
|
110
|
-
else
|
|
111
|
-
result[:words] << parse_word(part)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
result
|
|
116
|
-
end
|
|
117
|
-
|
|
118
155
|
def parse_word(part)
|
|
119
156
|
names = part.split('|')
|
|
120
157
|
{ name: names.first, aliases: names[1..] || [] }
|
|
@@ -182,6 +219,10 @@ module Completely
|
|
|
182
219
|
part.start_with?('[') && part.end_with?(']')
|
|
183
220
|
end
|
|
184
221
|
|
|
222
|
+
def command_word?(part)
|
|
223
|
+
!option_group?(part) && !token?(part)
|
|
224
|
+
end
|
|
225
|
+
|
|
185
226
|
def option_group_name(part)
|
|
186
227
|
part[1..-2].sub(/\s+options\z/, '')
|
|
187
228
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
patterns:
|
|
2
2
|
- mygit [root options]
|
|
3
|
+
- mygit [root options] clone [clone options] <source> <dest>
|
|
3
4
|
- mygit init [init options] <directory>
|
|
4
5
|
- mygit status [status options]
|
|
5
6
|
|
|
@@ -7,6 +8,9 @@ options:
|
|
|
7
8
|
root:
|
|
8
9
|
- -h|--help
|
|
9
10
|
- -v|--version
|
|
11
|
+
- --config <file>
|
|
12
|
+
clone:
|
|
13
|
+
- --depth <depth>
|
|
10
14
|
init:
|
|
11
15
|
- --bare
|
|
12
16
|
status:
|
|
@@ -16,6 +20,10 @@ options:
|
|
|
16
20
|
- --verbose (repeatable)
|
|
17
21
|
|
|
18
22
|
tokens:
|
|
23
|
+
file: +file
|
|
24
|
+
source: $(git branch --format='%(refname:short)' 2>/dev/null)
|
|
25
|
+
dest: +directory
|
|
26
|
+
depth: [1, 10, 100]
|
|
19
27
|
directory: +directory
|
|
20
28
|
branch: $(git branch --format='%(refname:short)' 2>/dev/null)
|
|
21
29
|
format: [short, long]
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
# completely (https://github.com/bashly-framework/completely)
|
|
5
5
|
# Modifying it manually is not recommended
|
|
6
6
|
|
|
7
|
-
<%= function_name %>
|
|
7
|
+
<%= function_name %>_node_flag_state() {
|
|
8
8
|
case "$1:$2" in
|
|
9
|
-
%
|
|
10
|
-
%
|
|
11
|
-
|
|
9
|
+
% pattern_nodes.each do |entry|
|
|
10
|
+
% node = entry[:node]
|
|
11
|
+
% pattern_node_options(node).select { |option| option[:value] }.each do |option|
|
|
12
|
+
<%= pattern_node_id node %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_node_id node}:") %>) return 2 ;;
|
|
13
|
+
% end
|
|
14
|
+
% pattern_node_options(node).reject { |option| option[:value] }.each do |option|
|
|
15
|
+
<%= pattern_node_id node %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_node_id node}:") %>) return 0 ;;
|
|
12
16
|
% end
|
|
13
17
|
% end
|
|
14
18
|
esac
|
|
@@ -29,26 +33,30 @@
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
% end
|
|
32
|
-
<%= function_name %>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
route_has_positionals=0
|
|
36
|
+
<%= function_name %>_resolve_node() {
|
|
37
|
+
node_id=0
|
|
38
|
+
node_word_count=0
|
|
36
39
|
positional_index=0
|
|
37
|
-
% pattern_routes.each do |route|
|
|
38
|
-
% conditions = pattern_route_conditions(route)
|
|
39
|
-
if (( ${#non_options[@]} >= <%= pattern_route_word_count route %> )) &&
|
|
40
|
-
(( <%= pattern_route_word_count route %> > route_word_count ))<%= conditions.empty? ? '' : ' &&' %>
|
|
41
|
-
% conditions.each_with_index do |condition, index|
|
|
42
|
-
[[ <%= condition %> ]]<%= index == conditions.size - 1 ? '' : ' &&' %>
|
|
43
|
-
% end
|
|
44
|
-
then
|
|
45
|
-
route_id=<%= pattern_route_id route %>
|
|
46
|
-
route_word_count=<%= pattern_route_word_count route %>
|
|
47
|
-
route_has_positionals=<%= route[:positionals].empty? ? 0 : 1 %>
|
|
48
|
-
positional_index=$((${#non_options[@]} - <%= pattern_route_word_count route %>))
|
|
49
|
-
fi
|
|
50
40
|
|
|
41
|
+
local word
|
|
42
|
+
for word in "${non_options[@]}"; do
|
|
43
|
+
case "$node_id:$word" in
|
|
44
|
+
% pattern_nodes.each do |entry|
|
|
45
|
+
% node = entry[:node]
|
|
46
|
+
% pattern_child_transitions(node).each do |transition|
|
|
47
|
+
<%= pattern_node_id node %>:<%= bash_escape transition[:name] %>)
|
|
48
|
+
node_id=<%= pattern_node_id transition[:node] %>
|
|
49
|
+
node_word_count=<%= pattern_node_depth transition[:node] %>
|
|
50
|
+
;;
|
|
51
51
|
% end
|
|
52
|
+
% end
|
|
53
|
+
*)
|
|
54
|
+
break
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
positional_index=$((${#non_options[@]} - node_word_count))
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
<%= function_name %>() {
|
|
@@ -65,11 +73,12 @@
|
|
|
65
73
|
|
|
66
74
|
local non_options=()
|
|
67
75
|
local completed_options=()
|
|
68
|
-
local
|
|
69
|
-
local
|
|
70
|
-
local route_has_positionals=0
|
|
76
|
+
local node_id=
|
|
77
|
+
local node_word_count=-1
|
|
71
78
|
local positional_index=0
|
|
72
|
-
|
|
79
|
+
local invalid_completion=0
|
|
80
|
+
local flag_state=0
|
|
81
|
+
<%= function_name %>_resolve_node
|
|
73
82
|
|
|
74
83
|
local skip_next=0
|
|
75
84
|
for word in "${completed[@]}"; do
|
|
@@ -79,28 +88,32 @@
|
|
|
79
88
|
fi
|
|
80
89
|
|
|
81
90
|
if [[ "${word:0:1}" == "-" ]]; then
|
|
91
|
+
<%= function_name %>_node_flag_state "$node_id" "$word"
|
|
92
|
+
flag_state=$?
|
|
93
|
+
if (( flag_state == 1 )); then
|
|
94
|
+
invalid_completion=1
|
|
95
|
+
break
|
|
96
|
+
fi
|
|
97
|
+
|
|
82
98
|
completed_options+=("$word")
|
|
83
|
-
if
|
|
99
|
+
if (( flag_state == 2 )); then
|
|
84
100
|
skip_next=1
|
|
85
101
|
fi
|
|
86
102
|
continue
|
|
87
103
|
fi
|
|
88
104
|
|
|
89
105
|
non_options+=("$word")
|
|
90
|
-
<%= function_name %>
|
|
106
|
+
<%= function_name %>_resolve_node
|
|
91
107
|
done
|
|
92
108
|
|
|
93
109
|
COMPREPLY=()
|
|
110
|
+
(( invalid_completion )) && return
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case "$route_id:$prev" in
|
|
101
|
-
% pattern_routes.each do |route|
|
|
102
|
-
% pattern_route_options(route).select { |option| option[:value] }.each do |option|
|
|
103
|
-
<%= pattern_route_id route %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_route_id route}:") %>)
|
|
112
|
+
case "$node_id:$prev" in
|
|
113
|
+
% pattern_nodes.each do |entry|
|
|
114
|
+
% node = entry[:node]
|
|
115
|
+
% pattern_node_options(node).select { |option| option[:value] }.each do |option|
|
|
116
|
+
<%= pattern_node_id node %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_node_id node}:") %>)
|
|
104
117
|
% if pattern_source_empty? option[:value][:source]
|
|
105
118
|
return
|
|
106
119
|
% else
|
|
@@ -112,12 +125,27 @@
|
|
|
112
125
|
% end
|
|
113
126
|
esac
|
|
114
127
|
|
|
128
|
+
if [[ "${cur:0:1}" != "-" ]] && (( positional_index == 0 )); then
|
|
129
|
+
case "$node_id" in
|
|
130
|
+
% pattern_nodes.each do |entry|
|
|
131
|
+
% node = entry[:node]
|
|
132
|
+
% child_words = pattern_node_child_words(node)
|
|
133
|
+
% next if child_words.empty?
|
|
134
|
+
<%= pattern_node_id node %>)
|
|
135
|
+
while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "<%= bash_double_quote_escape child_words.join(' ') %>" -- "$cur")
|
|
136
|
+
return
|
|
137
|
+
;;
|
|
138
|
+
% end
|
|
139
|
+
esac
|
|
140
|
+
fi
|
|
141
|
+
|
|
115
142
|
if [[ "${cur:0:1}" == "-" ]]; then
|
|
116
|
-
case "$
|
|
117
|
-
%
|
|
118
|
-
|
|
143
|
+
case "$node_id" in
|
|
144
|
+
% pattern_nodes.each do |entry|
|
|
145
|
+
% node = entry[:node]
|
|
146
|
+
<%= pattern_node_id node %>)
|
|
119
147
|
local words=()
|
|
120
|
-
%
|
|
148
|
+
% pattern_node_options(node).each do |option|
|
|
121
149
|
% if option[:repeatable]
|
|
122
150
|
words+=(<%= option[:names].map { |name| %["#{bash_escape name}"] }.join(' ') %>)
|
|
123
151
|
% else
|
|
@@ -131,10 +159,11 @@
|
|
|
131
159
|
esac
|
|
132
160
|
fi
|
|
133
161
|
|
|
134
|
-
%
|
|
135
|
-
%
|
|
162
|
+
% pattern_nodes.each do |entry|
|
|
163
|
+
% node = entry[:node]
|
|
164
|
+
% node[:positionals].each_with_index do |positional, index|
|
|
136
165
|
% next unless positional[:repeatable]
|
|
137
|
-
if [[ "$
|
|
166
|
+
if [[ "$node_id" == "<%= pattern_node_id node %>" ]] && (( positional_index >= <%= index %> )); then
|
|
138
167
|
% if pattern_source_empty? positional[:source]
|
|
139
168
|
return
|
|
140
169
|
% else
|
|
@@ -145,11 +174,12 @@
|
|
|
145
174
|
|
|
146
175
|
% end
|
|
147
176
|
% end
|
|
148
|
-
case "$
|
|
149
|
-
%
|
|
150
|
-
%
|
|
177
|
+
case "$node_id:$positional_index" in
|
|
178
|
+
% pattern_nodes.each do |entry|
|
|
179
|
+
% node = entry[:node]
|
|
180
|
+
% node[:positionals].each_with_index do |positional, index|
|
|
151
181
|
% next if positional[:repeatable]
|
|
152
|
-
<%=
|
|
182
|
+
<%= pattern_node_id node %>:<%= index %>)
|
|
153
183
|
% if pattern_source_empty? positional[:source]
|
|
154
184
|
return
|
|
155
185
|
% else
|
data/lib/completely/version.rb
CHANGED