findyml 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +12 -0
- data/exe/findyml +17 -0
- data/lib/findyml/version.rb +5 -0
- data/lib/findyml.rb +263 -0
- data/sig/findyml.rbs +4 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 24167ff7d45f6430782e0a071dde14d82c123152a4fc80015de958636a09af66
|
4
|
+
data.tar.gz: cbd75aa4977eda3ee732361e41af67383cfe34dda933390e2dce3d4ebd44a92d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 80fc4923eafba7bce45ace925a9c3f1becd25cbe1da00b7c219a1f90fb50074eb93642a7b260b18c7eb7c803058d8026b4b552ec196de8b68c6bd32a028fbe1d
|
7
|
+
data.tar.gz: 95c2d6dedf85c7313f5f9e7871740f83b1265e763eb2bc494c66231bf37dca5ce10935ab2e0dc4e08cb65bb2f6da6a8e7774c8568289dfd54e23b94f289e19be
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Danielle Smith
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Findyml
|
2
|
+
|
3
|
+
Search for yaml keys across multiple files
|
4
|
+
|
5
|
+
Ever wondered where that i18n locale was defined but your project is massive and legacy and has multiple competing inconsistent standards of organisation? Let findyml ease your pain by finding that key for you!
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```sh
|
10
|
+
gem install findyml
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```sh
|
16
|
+
findyml [path] query
|
17
|
+
```
|
18
|
+
|
19
|
+
Outputs all matching keys across all `*.yml` files in the given directory, including line and column number.
|
20
|
+
|
21
|
+
(Defaults to current directory if you don't specify a path)
|
22
|
+
|
23
|
+
Example:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
findyml config/locales en.activerecord.attributes
|
27
|
+
# config/locales/active_record.en.yml:3:6
|
28
|
+
```
|
29
|
+
|
30
|
+
You can also do partial matches, by starting/ending with a dot or putting an asterisk (`*`) in place of a key.
|
31
|
+
|
32
|
+
```sh
|
33
|
+
findyml .activerecord.attributes
|
34
|
+
findyml 'en.*.attributes'
|
35
|
+
```
|
36
|
+
|
37
|
+
(You have to quote the query if you use `*` because your shell might thing it is a dir glob)
|
38
|
+
|
39
|
+
**NOTE**: if you end with a dot, or the last key is an asterisk, it will return _every single sub key_. i.e. careful if you try `findyml en.` or `findyml en.*`, you will get every line of every locale file 🙃.
|
40
|
+
|
41
|
+
## TODO
|
42
|
+
|
43
|
+
- Allow optional keys in query: `foo.[bar,baz].qux` (`qux` key in either `bar` or `baz` parent key)
|
44
|
+
- Allow negated keys in query: `foo.!bar.baz` (`baz` with any parent but `bar`)
|
45
|
+
- Partial key matches: `foo.bar_*` (any key starting with `bar_`)
|
46
|
+
- Allow `*` and `**` like directory globbing.
|
47
|
+
- Fuzzy matching?
|
48
|
+
- Find and fix bugs
|
49
|
+
- Optimisation, caching
|
50
|
+
|
51
|
+
## Development
|
52
|
+
|
53
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
54
|
+
|
55
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
56
|
+
|
57
|
+
## Contributing
|
58
|
+
|
59
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/danini-the-panini/findyml.
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/findyml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "findyml"
|
6
|
+
|
7
|
+
dir, query = case ARGV.size
|
8
|
+
when 1
|
9
|
+
[Dir.pwd, ARGV.last]
|
10
|
+
when 2
|
11
|
+
ARGV
|
12
|
+
else
|
13
|
+
puts "Usage: #{$0} [path] query"
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
Findyml.find_and_print(query, dir)
|
data/lib/findyml.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
require_relative "findyml/version"
|
6
|
+
|
7
|
+
module Findyml
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
Key = Struct.new(:file, :node, :path, :terminal, :alias_path) do
|
11
|
+
def terminal? = terminal
|
12
|
+
|
13
|
+
def line
|
14
|
+
node.start_line + 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def col
|
18
|
+
node.start_column + 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#{path.map(&:inspect).join('.')} -> #{self}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class KeyNode
|
27
|
+
attr_reader :node
|
28
|
+
|
29
|
+
def initialize(node)
|
30
|
+
@node = node
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.is_a?(KeyNode) &&
|
35
|
+
node.class == other.node.class &&
|
36
|
+
to_s == other.to_s
|
37
|
+
end
|
38
|
+
alias :eql? :==
|
39
|
+
|
40
|
+
def hash
|
41
|
+
[node.class, to_s].hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def line
|
45
|
+
node.start_line + 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
@to_s ||= case node
|
50
|
+
when Psych::Nodes::Scalar
|
51
|
+
node.value
|
52
|
+
when Psych::Nodes::Mapping
|
53
|
+
"{#{node.children.each_slice(2).map { |k, v| "#{KeyNode.new(k).to_s}:#{KeyNode.new(v).to_s}" }.join(',')}}"
|
54
|
+
when Psych::Nodes::Sequence
|
55
|
+
"[#{node.children.map { KeyNode.new(_1).to_s }.join(',')}]"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
"#<Findyml::KeyNode @node=#{self}>"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class IndexNode < KeyNode
|
65
|
+
def initialize(node, index)
|
66
|
+
super(node)
|
67
|
+
@index = index
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
@index.to_s
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class FileExtractor
|
76
|
+
def self.call(file, &block)
|
77
|
+
new(file).extract(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(file)
|
81
|
+
@file = File.expand_path(file)
|
82
|
+
@yaml = YAML.parse_file(@file)
|
83
|
+
@anchors = {}
|
84
|
+
end
|
85
|
+
|
86
|
+
def extract(&block)
|
87
|
+
all_nodes = @yaml.children.map { construct_nodes(_1) }
|
88
|
+
|
89
|
+
all_nodes.each do |nodes, _|
|
90
|
+
extract_nodes(nodes, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_nodes(nodes, path=[], alias_path=[], &block)
|
95
|
+
nodes.each do |key_node, (children, yaml_node, alias_node)|
|
96
|
+
new_path = [*path, key_node.to_s]
|
97
|
+
new_alias_path = [*alias_path, *alias_node]
|
98
|
+
yield Key.new(@file, key_node.node, new_path, children.nil?, new_alias_path)
|
99
|
+
extract_nodes(children, new_path, new_alias_path, &block) if children
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def construct_nodes(current_node=yaml)
|
104
|
+
nodes = case current_node
|
105
|
+
when Psych::Nodes::Document
|
106
|
+
return construct_nodes(current_node.children.first)
|
107
|
+
when Psych::Nodes::Mapping
|
108
|
+
current_node
|
109
|
+
.children
|
110
|
+
.each_slice(2)
|
111
|
+
.each_with_object({}) { |(key, node), h|
|
112
|
+
if key.is_a?(Psych::Nodes::Scalar) && key.value == '<<'
|
113
|
+
h.merge!(construct_nodes(node).first)
|
114
|
+
else
|
115
|
+
key_node = KeyNode.new(key)
|
116
|
+
h.delete(key_node)
|
117
|
+
h[key_node] = construct_nodes(node)
|
118
|
+
end
|
119
|
+
}
|
120
|
+
when Psych::Nodes::Sequence
|
121
|
+
current_node
|
122
|
+
.children
|
123
|
+
.each_with_index
|
124
|
+
.map { |node, index| [IndexNode.new(node, index), construct_nodes(node)] }
|
125
|
+
when Psych::Nodes::Scalar
|
126
|
+
nil
|
127
|
+
when Psych::Nodes::Alias
|
128
|
+
a_nodes, anchor = @anchors[current_node.anchor]
|
129
|
+
return [a_nodes.transform_values { |(a, b, c)| [a, b, [*c, current_node]] }, anchor]
|
130
|
+
end
|
131
|
+
|
132
|
+
case current_node
|
133
|
+
when Psych::Nodes::Mapping, Psych::Nodes::Sequence, Psych::Nodes::Scalar
|
134
|
+
@anchors[current_node.anchor] = [nodes, current_node, []] if current_node.anchor
|
135
|
+
end
|
136
|
+
|
137
|
+
[nodes, current_node, []]
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_s
|
141
|
+
"FileExtractor(#{@file})"
|
142
|
+
end
|
143
|
+
|
144
|
+
def inspect
|
145
|
+
"#<Findyml::FileExtractor @file=#{@file.inspect}>"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.find(query_string, dir = Dir.pwd)
|
150
|
+
return to_enum(:find, query_string, dir) unless block_given?
|
151
|
+
|
152
|
+
# TODO: cache fast key lookup in a temporary sqlite db?
|
153
|
+
query = parse_key(query_string)
|
154
|
+
files = Dir.glob(File.join(dir, '**', '*.yml'))
|
155
|
+
files.each do |file|
|
156
|
+
FileExtractor.call(file) do |key|
|
157
|
+
yield key if key_match?(key.path, query)
|
158
|
+
end
|
159
|
+
rescue YAML::SyntaxError
|
160
|
+
# just skip files we can't parse
|
161
|
+
# TODO: silence warnings?
|
162
|
+
warn "Skipping #{file} due to parse error"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.find_and_print(*args)
|
167
|
+
find(*args) do |key|
|
168
|
+
puts "#{key.file}:#{key.line}:#{key.col}#{key.alias_path.map{"(#{_1.start_line+1})"}.join('')}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.parse_key(key)
|
173
|
+
pre = []
|
174
|
+
post = []
|
175
|
+
if key =~ /\A\./ # start with a dot
|
176
|
+
key = $' # everything after the dot
|
177
|
+
pre << :splat
|
178
|
+
end
|
179
|
+
if key =~ /\.\z/ # end with a dot
|
180
|
+
key = $` # everything before the dot
|
181
|
+
post << :splat
|
182
|
+
end
|
183
|
+
[*pre, *parse_key_parts(key), *post]
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.parse_key_parts(key)
|
187
|
+
invalid_key! if key.empty?
|
188
|
+
|
189
|
+
case key
|
190
|
+
# starts with quote
|
191
|
+
when /\A['"]/
|
192
|
+
# invalid unless rest has matching quote
|
193
|
+
invalid_key! unless $' =~ /#{$&}/
|
194
|
+
|
195
|
+
# everything before the next matching quote (i.e. contents of quotes)
|
196
|
+
quoted_key = $`
|
197
|
+
|
198
|
+
case $'
|
199
|
+
# quote was at end of string
|
200
|
+
when '' then [quoted_key]
|
201
|
+
|
202
|
+
# dot follows quote, parse everything after the dot
|
203
|
+
when /\A\./ then [quoted_key, *parse_key_parts($')]
|
204
|
+
|
205
|
+
# anything else after the quote is not allowed
|
206
|
+
else invalid_key!
|
207
|
+
end
|
208
|
+
|
209
|
+
# includes a dot
|
210
|
+
when /\./
|
211
|
+
# invalid unless something before the dot
|
212
|
+
invalid_key! if $`.empty?
|
213
|
+
|
214
|
+
k = $` == '*' ? :splat : $`
|
215
|
+
|
216
|
+
# parse everything after the dot
|
217
|
+
[k, *parse_key_parts($')]
|
218
|
+
|
219
|
+
# splat at the end of the string
|
220
|
+
when '*' then [:splat]
|
221
|
+
|
222
|
+
# single key query
|
223
|
+
else [key]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.invalid_key!
|
228
|
+
raise Error, "invalid key"
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.key_match?(path, query)
|
232
|
+
path == query unless query.include? :splat
|
233
|
+
|
234
|
+
query_parts = query.slice_before(:splat)
|
235
|
+
|
236
|
+
query_parts.each do |q|
|
237
|
+
case q
|
238
|
+
in [:splat]
|
239
|
+
return false if path.empty?
|
240
|
+
path = []
|
241
|
+
in [:splat, *partial]
|
242
|
+
return false if path.empty?
|
243
|
+
rest = munch(path[1..], partial)
|
244
|
+
return false unless rest
|
245
|
+
path = rest
|
246
|
+
else
|
247
|
+
return false unless path[0...q.size] == q
|
248
|
+
path = path.drop(q.size)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
path.empty?
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.munch(arr, part)
|
256
|
+
raise ArgumentError, "part must not be empty" if part.empty?
|
257
|
+
return if arr.empty?
|
258
|
+
return if arr.size < part.size
|
259
|
+
return arr[part.size..] if arr[0...part.size] == part
|
260
|
+
|
261
|
+
munch(arr[1..], part)
|
262
|
+
end
|
263
|
+
end
|
data/sig/findyml.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: findyml
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Danielle Smith
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-09-21 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Even wondered where that i18n locale was defined but your project is
|
14
|
+
massive and legacy and has multiple competing inconsistent standards of organisation?
|
15
|
+
Let findyml ease your pain by finding that key for you!
|
16
|
+
email:
|
17
|
+
- code@danini.dev
|
18
|
+
executables:
|
19
|
+
- findyml
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- exe/findyml
|
27
|
+
- lib/findyml.rb
|
28
|
+
- lib/findyml/version.rb
|
29
|
+
- sig/findyml.rbs
|
30
|
+
homepage: https://github.com/danini-the-panini/findyml
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
homepage_uri: https://github.com/danini-the-panini/findyml
|
35
|
+
source_code_uri: https://github.com/danini-the-panini/findyml
|
36
|
+
changelog_uri: https://github.com/danini-the-panini/findyml/releases
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.0.0
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubygems_version: 3.4.17
|
53
|
+
signing_key:
|
54
|
+
specification_version: 4
|
55
|
+
summary: Search for yaml keys across multiple files
|
56
|
+
test_files: []
|