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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fdb300c1ddaaf4ab1cfa6ca3e0b1371f4fbf33f1a22f29357bae06da0a973fc
4
- data.tar.gz: 3c812320dc20d2b9c13bf08e5cff2c596cd873880d8ec5b19b3a0bee68afd875
3
+ metadata.gz: cd6a89f32abe46975fa2f733f3ec78ac814a9af33695e2527a1df270a3693f80
4
+ data.tar.gz: 3452a9ffa65f0fdbc70d96c3163c19036b4fae67dbad8377d1c6525735287b1a
5
5
  SHA512:
6
- metadata.gz: 83f853427d2f54f0b86d736c1d54e526a74806b643c7d4dc12ea857eeacde38e18b8272374253845e697545168d0afcc9ec48d52fad7c00969d4be972c919b26
7
- data.tar.gz: 8f463487ca5084e78b11875f3b1d591359c0252055cdd44bb9df06024076b0dc6627647e9d30cd8beaeab1d97ec8b06eb1d986b634513eb5bb8e63184514daad
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.key? format
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 pattern_routes
105
- config.model[:routes]
104
+ def pattern_tree
105
+ config.model[:tree]
106
106
  end
107
107
 
108
- def pattern_programs
109
- pattern_routes.map { |route| route.dig(:words, 0, :name) }
108
+ def pattern_nodes
109
+ @pattern_nodes ||= flatten_pattern_tree pattern_tree
110
110
  end
111
111
 
112
- def pattern_root_words
113
- pattern_routes.flat_map do |route|
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 pattern_route_id(route)
120
- pattern_routes.index route
116
+ def pattern_node_id(node)
117
+ pattern_nodes.index { |entry| entry[:node].equal? node }
121
118
  end
122
119
 
123
- def pattern_route_conditions(route)
124
- route[:words][1..].map.with_index do |word, index|
125
- names = [word[:name], *word[:aliases]]
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 pattern_route_word_count(route)
131
- route[:words].size - 1
126
+ def pattern_node_depth(node)
127
+ pattern_nodes.dig(pattern_node_id(node), :depth)
132
128
  end
133
129
 
134
- def pattern_route_options(route)
135
- route[:option_groups].flat_map do |name|
136
- config.model[:options][name] || []
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
- pattern_routes.any? do |route|
142
- pattern_route_options(route).any? { |option| !option[:repeatable] }
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: program,
15
- routes: routes,
16
- options: parsed_options,
17
- tokens: tokens,
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
- routes.first.dig(:words, 0, :name)
41
+ programs.first
41
42
  end
42
43
 
43
- def routes
44
- @routes ||= patterns.map { |pattern| parse_pattern pattern }
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 %>_route_flag_expects_value() {
7
+ <%= function_name %>_node_flag_state() {
8
8
  case "$1:$2" in
9
- % pattern_routes.each do |route|
10
- % pattern_route_options(route).select { |option| option[:value] }.each do |option|
11
- <%= pattern_route_id route %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_route_id route}:") %>) return 0 ;;
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 %>_resolve_route() {
33
- route_id=
34
- route_word_count=-1
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 route_id=
69
- local route_word_count=-1
70
- local route_has_positionals=0
76
+ local node_id=
77
+ local node_word_count=-1
71
78
  local positional_index=0
72
- <%= function_name %>_resolve_route
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 <%= function_name %>_route_flag_expects_value "$route_id" "$word"; then
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 %>_resolve_route
106
+ <%= function_name %>_resolve_node
91
107
  done
92
108
 
93
109
  COMPREPLY=()
110
+ (( invalid_completion )) && return
94
111
 
95
- if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then
96
- while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "<%= bash_double_quote_escape pattern_root_words.join(' ') %>" -- "$cur")
97
- return
98
- fi
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 "$route_id" in
117
- % pattern_routes.each do |route|
118
- <%= pattern_route_id route %>)
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
- % pattern_route_options(route).each do |option|
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
- % pattern_routes.each do |route|
135
- % route[:positionals].each_with_index do |positional, index|
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 [[ "$route_id" == "<%= pattern_route_id route %>" ]] && (( positional_index >= <%= index %> )); then
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 "$route_id:$positional_index" in
149
- % pattern_routes.each do |route|
150
- % route[:positionals].each_with_index do |positional, index|
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
- <%= pattern_route_id route %>:<%= index %>)
182
+ <%= pattern_node_id node %>:<%= index %>)
153
183
  % if pattern_source_empty? positional[:source]
154
184
  return
155
185
  % else
@@ -1,3 +1,3 @@
1
1
  module Completely
2
- VERSION = '0.8.0.rc5'
2
+ VERSION = '0.8.0.rc6'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completely
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0.rc5
4
+ version: 0.8.0.rc6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit