flatito 0.1.2 → 0.1.3
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/.rubocop.yml +4 -1
- data/README.md +15 -2
- data/benchmark.rb +121 -0
- data/exe/flatito +21 -6
- data/lib/flatito/config.rb +1 -3
- data/lib/flatito/finder.rb +132 -11
- data/lib/flatito/flatten_yaml.rb +28 -14
- data/lib/flatito/json_scanner.rb +57 -0
- data/lib/flatito/print_items.rb +16 -4
- data/lib/flatito/regex_from_search.rb +5 -1
- data/lib/flatito/renderer.rb +22 -9
- data/lib/flatito/tree_iterator.rb +31 -2
- data/lib/flatito/utils.rb +13 -2
- data/lib/flatito/version.rb +1 -1
- data/lib/flatito.rb +3 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7864bfbf6505512a8f41e61b75efdb1081a62677f8686c37276a631f51572e96
|
|
4
|
+
data.tar.gz: d40ab3fd44a41e0cbd091e3dfbc62de98b931318fdda2efbbb8d89e206e20799
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 263878449f3724e3d1b46c8a18eebfad0445504a080726cb38b29a25675ea23db278ab0f5157041f6effff62b3ec7d50eb97d76e531ad1ba412e0d603c0a8607
|
|
7
|
+
data.tar.gz: 3c04d9bf798fd85b72af32946c03dabc888355c5b315a300bdff1aa4c8a7c0fcbcb3aa4db24c3a3e94fe27bba8e48afd19a1c38cb7a09ff9f63391c3a115cf9d
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Flatito: Grep for YAML and JSON files
|
|
2
2
|
|
|
3
|
-
A kind of grep for YAML and JSON files. It allows you to search
|
|
3
|
+
A kind of grep for YAML and JSON files. It allows you to search by key or value and get the matching entries with their line numbers.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -30,14 +30,27 @@ It is also available as [nixpkgs](https://search.nixos.org/packages?channel=unst
|
|
|
30
30
|
```sh
|
|
31
31
|
Usage: flatito PATH [options]
|
|
32
32
|
Example: flatito . -k "search string" -e "json,yaml"
|
|
33
|
+
Example: flatito . -c "search value"
|
|
33
34
|
Example: cat file.yaml | flatito -k "search string"
|
|
34
35
|
|
|
35
36
|
-h, --help Prints this help
|
|
36
37
|
-V, --version Show version
|
|
37
|
-
-k, --search-key=SEARCH Search
|
|
38
|
+
-k, --search-key=SEARCH Search by key
|
|
39
|
+
-c, --search-value=SEARCH Search by value
|
|
40
|
+
-s, --case-sensitive Case sensitive search
|
|
38
41
|
--no-color Disable color output
|
|
39
42
|
-e, --extensions=EXTENSIONS File extensions to search, separated by comma, default: (json,yaml,yaml)
|
|
40
43
|
--no-skipping Do not skip hidden files
|
|
44
|
+
--no-gitignore Do not respect .gitignore
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Searches are case-insensitive by default. Use `-s` to force exact case matching.
|
|
48
|
+
|
|
49
|
+
Both `-k` and `-c` support regular expressions and can be combined:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
# Find keys matching "database" with values containing "production"
|
|
53
|
+
flatito . -k "database" -c "production"
|
|
41
54
|
```
|
|
42
55
|
|
|
43
56
|
## Development
|
data/benchmark.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "flatito"
|
|
5
|
+
|
|
6
|
+
# --- Helpers ---
|
|
7
|
+
|
|
8
|
+
def measure_time(times, &block)
|
|
9
|
+
gc_was_disabled = GC.disable
|
|
10
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
11
|
+
times.times(&block)
|
|
12
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
13
|
+
GC.enable unless gc_was_disabled
|
|
14
|
+
elapsed
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def measure_memory
|
|
18
|
+
GC.start
|
|
19
|
+
GC.compact if GC.respond_to?(:compact)
|
|
20
|
+
before_mem = `ps -o rss= -p #{$PROCESS_ID}`.strip.to_i
|
|
21
|
+
before_gc = GC.stat[:total_allocated_objects]
|
|
22
|
+
|
|
23
|
+
yield
|
|
24
|
+
|
|
25
|
+
after_gc = GC.stat[:total_allocated_objects]
|
|
26
|
+
after_mem = `ps -o rss= -p #{$PROCESS_ID}`.strip.to_i
|
|
27
|
+
|
|
28
|
+
{ rss_kb: after_mem - before_mem, objects: after_gc - before_gc }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def report(label, times, &block)
|
|
32
|
+
# Warmup
|
|
33
|
+
3.times(&block)
|
|
34
|
+
|
|
35
|
+
elapsed = measure_time(times, &block)
|
|
36
|
+
mem = measure_memory { times.times(&block) }
|
|
37
|
+
per_iter = (elapsed / times * 1000).round(3)
|
|
38
|
+
|
|
39
|
+
puts format(
|
|
40
|
+
" %-40s %8.3f ms/iter | RSS: %+6d KB | Allocs: %d",
|
|
41
|
+
label, per_iter, mem[:rss_kb], mem[:objects]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# --- Setup ---
|
|
46
|
+
|
|
47
|
+
yaml_small = (1..100).map { |i| "key_#{i}: Value number #{i}" }.join("\n")
|
|
48
|
+
yaml_medium = (1..1000).map { |i| "key_#{i}: Value number #{i} with some extra text" }.join("\n")
|
|
49
|
+
|
|
50
|
+
nested_lines = (1..200).map do |i|
|
|
51
|
+
"group_#{i / 10}:\n key_#{i}: Value number #{i} with nested content"
|
|
52
|
+
end
|
|
53
|
+
yaml_nested = nested_lines.join("\n")
|
|
54
|
+
|
|
55
|
+
json_content = "{\n" + (1..1000).map { |i| " \"key_#{i}\": \"Value number #{i}\"" }.join(",\n") + "\n}"
|
|
56
|
+
|
|
57
|
+
items_medium = Flatito::FlattenYaml.items_from_content(yaml_medium)
|
|
58
|
+
|
|
59
|
+
null_io = File.open(File::NULL, "w")
|
|
60
|
+
Flatito::Config.stdout = null_io
|
|
61
|
+
Flatito::Config.prepare_with_options({ no_color: true })
|
|
62
|
+
|
|
63
|
+
# --- Benchmark ---
|
|
64
|
+
|
|
65
|
+
puts "Flatito Benchmark"
|
|
66
|
+
puts "Ruby #{RUBY_VERSION} | #{RUBY_PLATFORM}"
|
|
67
|
+
puts "=" * 90
|
|
68
|
+
|
|
69
|
+
puts "\n[Parsing]"
|
|
70
|
+
report("YAML 100 keys", 1000) { Flatito::FlattenYaml.items_from_content(yaml_small) }
|
|
71
|
+
report("YAML 1000 keys", 500) { Flatito::FlattenYaml.items_from_content(yaml_medium) }
|
|
72
|
+
report("YAML 200 nested keys", 500) { Flatito::FlattenYaml.items_from_content(yaml_nested) }
|
|
73
|
+
report("JSON 1000 keys", 500) { Flatito::FlattenYaml.items_from_content(json_content) }
|
|
74
|
+
|
|
75
|
+
puts "\n[Filtering]"
|
|
76
|
+
pi = Flatito::PrintItems.new("key_50")
|
|
77
|
+
report("by key (literal)", 5000) { pi.filter_by_search(items_medium) }
|
|
78
|
+
|
|
79
|
+
pi = Flatito::PrintItems.new("key_(5|9)\\d\\d")
|
|
80
|
+
report("by key (regex)", 5000) { pi.filter_by_search(items_medium) }
|
|
81
|
+
|
|
82
|
+
if Flatito::PrintItems.instance_method(:initialize).arity.abs > 1
|
|
83
|
+
pi = Flatito::PrintItems.new(nil, "number 50")
|
|
84
|
+
report("by value (literal)", 5000) { pi.filter_by_value(items_medium) }
|
|
85
|
+
|
|
86
|
+
pi = Flatito::PrintItems.new(nil, "number (5|9)\\d\\d")
|
|
87
|
+
report("by value (regex)", 5000) { pi.filter_by_value(items_medium) }
|
|
88
|
+
|
|
89
|
+
pi = Flatito::PrintItems.new("key_5", "number 5")
|
|
90
|
+
report("by key + value", 5000) do
|
|
91
|
+
filtered = pi.filter_by_search(items_medium)
|
|
92
|
+
pi.filter_by_value(filtered)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "\n[Rendering]"
|
|
97
|
+
Flatito::Config.prepare_with_options({})
|
|
98
|
+
pi = Flatito::PrintItems.new("key_50")
|
|
99
|
+
report("filter + print (key, 11 matches)", 2000) { pi.print(items_medium) }
|
|
100
|
+
|
|
101
|
+
if Flatito::PrintItems.instance_method(:initialize).arity.abs > 1
|
|
102
|
+
pi = Flatito::PrintItems.new(nil, "number 50")
|
|
103
|
+
report("filter + print (value, 11 matches)", 2000) { pi.print(items_medium) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
pi = Flatito::PrintItems.new(nil)
|
|
107
|
+
report("print all (1000 items, no filter)", 20) { pi.print(items_medium) }
|
|
108
|
+
|
|
109
|
+
puts "\n[Memory - large parse]"
|
|
110
|
+
GC.start
|
|
111
|
+
GC.compact if GC.respond_to?(:compact)
|
|
112
|
+
before = GC.stat[:total_allocated_objects]
|
|
113
|
+
rss_before = `ps -o rss= -p #{$PROCESS_ID}`.strip.to_i
|
|
114
|
+
large_yaml = (1..5000).map { |i| "key_#{i}: Value number #{i} with a longer description to simulate real data" }.join("\n")
|
|
115
|
+
Flatito::FlattenYaml.items_from_content(large_yaml)
|
|
116
|
+
after = GC.stat[:total_allocated_objects]
|
|
117
|
+
rss_after = `ps -o rss= -p #{$PROCESS_ID}`.strip.to_i
|
|
118
|
+
puts format(" %-40s RSS: %+6d KB | Allocs: %d", "parse 5000 keys YAML", rss_after - rss_before, after - before)
|
|
119
|
+
|
|
120
|
+
null_io.close
|
|
121
|
+
puts "\n#{"=" * 90}"
|
data/exe/flatito
CHANGED
|
@@ -13,6 +13,7 @@ OptionParser.new do |opts|
|
|
|
13
13
|
opts.banner = <<~HEREDOC
|
|
14
14
|
Usage: flatito PATH [options]
|
|
15
15
|
Example: flatito . -k "search string" -e "json,yaml"
|
|
16
|
+
Example: flatito . -c "search value"
|
|
16
17
|
Example: cat file.yaml | flatito -k "search string"
|
|
17
18
|
HEREDOC
|
|
18
19
|
|
|
@@ -26,10 +27,18 @@ OptionParser.new do |opts|
|
|
|
26
27
|
exit
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
opts.on("-kSEARCH", "--search-key=SEARCH", "Search
|
|
30
|
+
opts.on("-kSEARCH", "--search-key=SEARCH", "Search by key") do |s|
|
|
30
31
|
options[:search] = s
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
opts.on("-cSEARCH", "--search-value=SEARCH", "Search by value") do |s|
|
|
35
|
+
options[:search_value] = s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-s", "--case-sensitive", "Case sensitive search") do
|
|
39
|
+
options[:case_sensitive] = true
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
opts.on("--no-color", "Disable color output") do
|
|
34
43
|
options[:no_color] = true
|
|
35
44
|
end
|
|
@@ -41,12 +50,18 @@ OptionParser.new do |opts|
|
|
|
41
50
|
opts.on("--no-skipping", "Do not skip hidden files") do
|
|
42
51
|
options[:skip_hidden] = false
|
|
43
52
|
end
|
|
53
|
+
|
|
54
|
+
opts.on("--no-gitignore", "Do not respect .gitignore") do
|
|
55
|
+
options[:gitignore] = false
|
|
56
|
+
end
|
|
44
57
|
end.parse!
|
|
45
58
|
|
|
46
59
|
Flatito::Config.prepare_with_options(options)
|
|
47
60
|
|
|
48
|
-
if stdin
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
|
|
52
|
-
end
|
|
61
|
+
matched = if stdin
|
|
62
|
+
Flatito.flat_content(stdin, options)
|
|
63
|
+
else
|
|
64
|
+
Flatito.search(ARGV, options)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
exit 1 unless matched
|
data/lib/flatito/config.rb
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
module Flatito
|
|
4
4
|
module Config
|
|
5
5
|
@stdout = $stdout
|
|
6
|
-
@stderr = $stderr
|
|
7
|
-
@stdin = $stdin
|
|
8
6
|
|
|
9
7
|
class << self
|
|
10
|
-
attr_accessor :renderer, :stdout
|
|
8
|
+
attr_accessor :renderer, :stdout
|
|
11
9
|
|
|
12
10
|
def prepare_with_options(options)
|
|
13
11
|
self.renderer = Renderer.build(options)
|
data/lib/flatito/finder.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
4
|
+
require "etc"
|
|
5
|
+
require "set"
|
|
3
6
|
require_relative "regex_from_search"
|
|
4
7
|
|
|
5
8
|
module Flatito
|
|
@@ -8,28 +11,33 @@ module Flatito
|
|
|
8
11
|
|
|
9
12
|
DEFAULT_EXTENSIONS = %w[json yml yaml].freeze
|
|
10
13
|
|
|
11
|
-
attr_reader :paths, :search, :extensions, :options, :print_items
|
|
14
|
+
attr_reader :paths, :search, :search_value, :case_sensitive, :extensions, :options, :print_items
|
|
12
15
|
|
|
13
16
|
def initialize(paths, options = {})
|
|
14
17
|
@paths = paths
|
|
15
18
|
@search = options[:search]
|
|
19
|
+
@search_value = options[:search_value]
|
|
20
|
+
@case_sensitive = options[:case_sensitive]
|
|
16
21
|
@extensions = prepare_extensions(options[:extensions] || DEFAULT_EXTENSIONS)
|
|
17
22
|
@options = options
|
|
18
|
-
@print_items = PrintItems.new(search)
|
|
23
|
+
@print_items = PrintItems.new(search, search_value, case_sensitive: case_sensitive)
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
FORK_THRESHOLD = 100
|
|
27
|
+
|
|
21
28
|
def call
|
|
29
|
+
@matched = false
|
|
22
30
|
renderer.prepare
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
TreeIterator.new(path, options).each do |pathname|
|
|
26
|
-
renderer.print_file_progress(pathname)
|
|
32
|
+
files = collect_candidate_files
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
if files.size >= FORK_THRESHOLD && Process.respond_to?(:fork)
|
|
35
|
+
process_with_forks(files)
|
|
36
|
+
else
|
|
37
|
+
files.each { |pathname| flat_and_filter(pathname) }
|
|
32
38
|
end
|
|
39
|
+
|
|
40
|
+
@matched
|
|
33
41
|
ensure
|
|
34
42
|
renderer.ending
|
|
35
43
|
end
|
|
@@ -40,9 +48,122 @@ module Flatito
|
|
|
40
48
|
Config.renderer
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
def collect_candidate_files
|
|
52
|
+
files = []
|
|
53
|
+
paths.each do |path|
|
|
54
|
+
TreeIterator.new(path, options).each do |pathname|
|
|
55
|
+
renderer.print_file_progress(pathname)
|
|
56
|
+
next unless extensions.include?(pathname.extname)
|
|
57
|
+
next if git_candidates && !git_candidates.include?(File.expand_path(pathname.to_s))
|
|
58
|
+
|
|
59
|
+
files << pathname
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
files
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def process_with_forks(files)
|
|
66
|
+
workers = [Etc.nprocessors, files.size].min
|
|
67
|
+
chunks = files.each_slice((files.size / workers.to_f).ceil).to_a
|
|
68
|
+
|
|
69
|
+
readers = chunks.map do |chunk|
|
|
70
|
+
rd, wr = IO.pipe
|
|
71
|
+
Process.fork do
|
|
72
|
+
rd.close
|
|
73
|
+
results = chunk.filter_map { |pathname| read_and_parse(pathname) }
|
|
74
|
+
Marshal.dump(results, wr)
|
|
75
|
+
wr.close
|
|
76
|
+
end
|
|
77
|
+
wr.close
|
|
78
|
+
rd
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
readers.each do |rd|
|
|
82
|
+
data = rd.read
|
|
83
|
+
rd.close
|
|
84
|
+
next if data.empty?
|
|
85
|
+
|
|
86
|
+
Marshal.load(data).each do |pathname, items| # rubocop:disable Security/MarshalLoad
|
|
87
|
+
@matched = true if print_items.print(items, pathname)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Process.waitall
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def read_and_parse(pathname)
|
|
95
|
+
content = File.read(pathname)
|
|
96
|
+
return unless content_may_match?(content)
|
|
97
|
+
|
|
98
|
+
items = FlattenYaml.items_from_content(content, pathname: pathname)
|
|
99
|
+
[pathname, items]
|
|
100
|
+
end
|
|
101
|
+
|
|
43
102
|
def flat_and_filter(pathname)
|
|
44
|
-
|
|
45
|
-
|
|
103
|
+
return if git_candidates && !git_candidates.include?(File.expand_path(pathname.to_s))
|
|
104
|
+
|
|
105
|
+
content = File.read(pathname)
|
|
106
|
+
return unless git_candidates || content_may_match?(content)
|
|
107
|
+
|
|
108
|
+
items = FlattenYaml.items_from_content(content, pathname: pathname)
|
|
109
|
+
@matched = true if print_items.print(items, pathname)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def git_candidates
|
|
113
|
+
return @git_candidates if defined?(@git_candidates)
|
|
114
|
+
|
|
115
|
+
@git_candidates = build_git_candidates
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_git_candidates
|
|
119
|
+
return nil if search.nil? && search_value.nil?
|
|
120
|
+
|
|
121
|
+
patterns = []
|
|
122
|
+
patterns.concat(search.split(".")) if search
|
|
123
|
+
patterns << search_value if search_value
|
|
124
|
+
|
|
125
|
+
candidates = Set.new
|
|
126
|
+
paths.each do |path|
|
|
127
|
+
dir = File.directory?(path) ? path : File.dirname(path)
|
|
128
|
+
files = git_grep(dir, patterns)
|
|
129
|
+
return nil if files.nil?
|
|
130
|
+
|
|
131
|
+
candidates.merge(files)
|
|
132
|
+
end
|
|
133
|
+
candidates
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def git_grep(dir, patterns)
|
|
137
|
+
expanded_dir = File.expand_path(dir)
|
|
138
|
+
args = ["git", "-C", expanded_dir, "grep", "--untracked", "-l"]
|
|
139
|
+
args << "-i" unless case_sensitive
|
|
140
|
+
args << "--all-match" if patterns.size > 1
|
|
141
|
+
patterns.each { |p| args.push("-e", p) }
|
|
142
|
+
args.push("--", ".")
|
|
143
|
+
|
|
144
|
+
output = IO.popen(args, err: File::NULL, &:read)
|
|
145
|
+
return nil unless [0, 1].include?($CHILD_STATUS.exitstatus)
|
|
146
|
+
|
|
147
|
+
Set.new(output.lines.map { |f| File.expand_path(f.chomp, expanded_dir) })
|
|
148
|
+
rescue Errno::ENOENT
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def content_may_match?(content)
|
|
153
|
+
return true if search.nil? && search_value.nil?
|
|
154
|
+
|
|
155
|
+
(!search || key_parts_match?(content)) &&
|
|
156
|
+
(!search_value || value_regex.match?(content))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def key_parts_match?(content)
|
|
160
|
+
key_part_regexes.all? { |part| part.match?(content) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def key_part_regexes
|
|
164
|
+
@key_part_regexes ||= search.split(".").map do |part|
|
|
165
|
+
Regexp.new(part, case_sensitive ? nil : Regexp::IGNORECASE)
|
|
166
|
+
end
|
|
46
167
|
end
|
|
47
168
|
|
|
48
169
|
def prepare_extensions(extensions)
|
data/lib/flatito/flatten_yaml.rb
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "utils"
|
|
4
3
|
module Flatito
|
|
5
4
|
class FlattenYaml
|
|
6
|
-
include Utils
|
|
7
|
-
|
|
8
5
|
Item = Struct.new(:key, :value, :line, keyword_init: true)
|
|
9
6
|
class << self
|
|
10
7
|
def items_from_path(pathname)
|
|
@@ -12,8 +9,8 @@ module Flatito
|
|
|
12
9
|
new(content, pathname: pathname).items
|
|
13
10
|
end
|
|
14
11
|
|
|
15
|
-
def items_from_content(content)
|
|
16
|
-
new(content).items
|
|
12
|
+
def items_from_content(content, pathname: nil)
|
|
13
|
+
new(content, pathname: pathname).items
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
16
|
|
|
@@ -25,23 +22,40 @@ module Flatito
|
|
|
25
22
|
end
|
|
26
23
|
|
|
27
24
|
def items
|
|
28
|
-
|
|
25
|
+
if json?
|
|
26
|
+
result = JsonScanner.scan(content)
|
|
27
|
+
return result if result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
psych_items
|
|
31
|
+
rescue StandardError
|
|
32
|
+
warn "Error parsing #{pathname}" if pathname
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def psych_items
|
|
37
|
+
with_line_numbers.filter_map do |line|
|
|
29
38
|
flatten_hash(line) if line.is_a?(Hash)
|
|
30
|
-
end.
|
|
39
|
+
end.flatten
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def flatten_hash(hash, prefix = nil)
|
|
34
|
-
hash.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
hash.filter_map do |key, value|
|
|
44
|
+
next unless value.is_a?(YAMLWithLineNumber::ValueWithLineNumbers)
|
|
45
|
+
|
|
46
|
+
full_key = prefix ? "#{prefix}.#{key}" : key
|
|
47
|
+
if value.value.is_a?(Hash)
|
|
48
|
+
flatten_hash(value.value, full_key)
|
|
49
|
+
else
|
|
50
|
+
Item.new(key: full_key, value: value.value.to_s, line: value.line)
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
|
|
55
|
+
def json?
|
|
56
|
+
content.lstrip.start_with?("{", "[")
|
|
57
|
+
end
|
|
58
|
+
|
|
45
59
|
def with_line_numbers
|
|
46
60
|
handler = YAMLWithLineNumber::TreeBuilder.new
|
|
47
61
|
handler.parser = Psych::Parser.new(handler)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Flatito
|
|
6
|
+
class JsonScanner
|
|
7
|
+
def self.scan(content)
|
|
8
|
+
new(content).items
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :content
|
|
12
|
+
|
|
13
|
+
def initialize(content)
|
|
14
|
+
@content = content
|
|
15
|
+
@line_index = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def items
|
|
19
|
+
hash = JSON.parse(content)
|
|
20
|
+
flatten(hash)
|
|
21
|
+
rescue JSON::ParserError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def flatten(hash, prefix = nil)
|
|
28
|
+
hash.flat_map do |key, value|
|
|
29
|
+
full_key = prefix ? "#{prefix}.#{key}" : key
|
|
30
|
+
if value.is_a?(Hash)
|
|
31
|
+
flatten(value, full_key)
|
|
32
|
+
else
|
|
33
|
+
line = find_line(key)
|
|
34
|
+
FlattenYaml::Item.new(key: full_key, value: value.to_s, line: line)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find_line(key)
|
|
40
|
+
line_index[key] || 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line_index
|
|
44
|
+
@line_index ||= build_line_index
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_line_index
|
|
48
|
+
index = {}
|
|
49
|
+
content.each_line.with_index(1) do |line, num|
|
|
50
|
+
if (match = line.match(/"([^"]+)"\s*:/))
|
|
51
|
+
index[match[1]] ||= num
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
index
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/flatito/print_items.rb
CHANGED
|
@@ -4,18 +4,22 @@ module Flatito
|
|
|
4
4
|
class PrintItems
|
|
5
5
|
include RegexFromSearch
|
|
6
6
|
|
|
7
|
-
attr_reader :search
|
|
7
|
+
attr_reader :search, :search_value, :case_sensitive
|
|
8
8
|
|
|
9
|
-
def initialize(search)
|
|
9
|
+
def initialize(search, search_value = nil, case_sensitive: false)
|
|
10
10
|
@search = search
|
|
11
|
+
@search_value = search_value
|
|
12
|
+
@case_sensitive = case_sensitive
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
def print(items, pathname = nil)
|
|
15
|
+
def print(items, pathname = nil) # rubocop:disable Naming/PredicateMethod
|
|
14
16
|
items = filter_by_search(items) if search
|
|
15
|
-
|
|
17
|
+
items = filter_by_value(items) if search_value
|
|
18
|
+
return false unless items.any?
|
|
16
19
|
|
|
17
20
|
renderer.print_pathname(pathname) if pathname
|
|
18
21
|
renderer.print_items(items)
|
|
22
|
+
true
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def filter_by_search(items)
|
|
@@ -24,6 +28,14 @@ module Flatito
|
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
30
|
|
|
31
|
+
def filter_by_value(items)
|
|
32
|
+
items.select do |item|
|
|
33
|
+
value_regex.match?(item.value)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
27
39
|
def renderer
|
|
28
40
|
Config.renderer
|
|
29
41
|
end
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module Flatito
|
|
4
4
|
module RegexFromSearch
|
|
5
5
|
def regex
|
|
6
|
-
@regex ||= Regexp.new(search)
|
|
6
|
+
@regex ||= Regexp.new(search, case_sensitive ? nil : Regexp::IGNORECASE)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def value_regex
|
|
10
|
+
@value_regex ||= Regexp.new(search_value, case_sensitive ? nil : Regexp::IGNORECASE)
|
|
7
11
|
end
|
|
8
12
|
end
|
|
9
13
|
end
|
data/lib/flatito/renderer.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "io/console"
|
|
4
3
|
require_relative "regex_from_search"
|
|
5
4
|
require_relative "utils"
|
|
6
5
|
|
|
@@ -19,11 +18,14 @@ module Flatito
|
|
|
19
18
|
include Utils
|
|
20
19
|
include RegexFromSearch
|
|
21
20
|
|
|
22
|
-
attr_reader :search, :no_color
|
|
21
|
+
attr_reader :search, :search_value, :no_color, :case_sensitive
|
|
23
22
|
|
|
24
23
|
def initialize(options)
|
|
25
24
|
@no_color = options[:no_color] || false
|
|
26
25
|
@search = options[:search]
|
|
26
|
+
@search_value = options[:search_value]
|
|
27
|
+
@case_sensitive = options[:case_sensitive]
|
|
28
|
+
@no_color_resolved = nil
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def prepare; end
|
|
@@ -47,7 +49,8 @@ module Flatito
|
|
|
47
49
|
def print_item(item, line_number_padding)
|
|
48
50
|
line_number = colorize("#{item.line.to_s.rjust(line_number_padding)}: ", :yellow)
|
|
49
51
|
value = if item.value.length.positive?
|
|
50
|
-
|
|
52
|
+
display_value = truncate_value(item.value)
|
|
53
|
+
colorize("=> ", :gray) + matched_value(display_value, :gray)
|
|
51
54
|
else
|
|
52
55
|
""
|
|
53
56
|
end
|
|
@@ -60,14 +63,24 @@ module Flatito
|
|
|
60
63
|
def matched_string(string)
|
|
61
64
|
return string if search.nil? || no_color?
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
string.gsub(regex) { |match| match.colorize(:light_red) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def matched_value(string, default_color)
|
|
70
|
+
return colorize(string, default_color) if search_value.nil? || no_color?
|
|
71
|
+
|
|
72
|
+
string.gsub(value_regex) { |match| match.colorize(:light_red) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def truncate_value(string)
|
|
76
|
+
match_position = search_value && value_regex.match(string)&.begin(0)
|
|
77
|
+
truncate(string, match_position: match_position)
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
def no_color?
|
|
70
|
-
|
|
81
|
+
return @no_color_resolved unless @no_color_resolved.nil?
|
|
82
|
+
|
|
83
|
+
@no_color_resolved = ENV["TERM"] == "dumb" || ENV["NO_COLOR"] == "true" || no_color == true
|
|
71
84
|
end
|
|
72
85
|
|
|
73
86
|
def stdout
|
|
@@ -102,7 +115,7 @@ module Flatito
|
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
def print_file_progress(pathname)
|
|
105
|
-
print truncate(pathname.to_s, stdout_width - 4)
|
|
118
|
+
stdout.print truncate(pathname.to_s, max: stdout_width - 4)
|
|
106
119
|
clear_line
|
|
107
120
|
end
|
|
108
121
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
3
6
|
module Flatito
|
|
4
7
|
class TreeIterator
|
|
5
8
|
include Enumerable
|
|
6
9
|
|
|
7
|
-
attr_reader :base_path, :skip_hidden
|
|
10
|
+
attr_reader :base_path, :skip_hidden, :gitignore
|
|
8
11
|
|
|
9
12
|
def initialize(base_path, options = {})
|
|
10
13
|
@base_path = base_path
|
|
11
|
-
@skip_hidden = options
|
|
14
|
+
@skip_hidden = options.fetch(:skip_hidden, true)
|
|
15
|
+
@gitignore = options.fetch(:gitignore, true)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def each(&block)
|
|
@@ -18,6 +22,7 @@ module Flatito
|
|
|
18
22
|
def tree(parent, &block)
|
|
19
23
|
if parent.directory?
|
|
20
24
|
return if parent.symlink?
|
|
25
|
+
return if gitignore && ignored_dir?(parent)
|
|
21
26
|
|
|
22
27
|
parent.each_child.each do |pathname|
|
|
23
28
|
next if skip_hidden && pathname.basename.to_s[0] == "."
|
|
@@ -32,5 +37,29 @@ module Flatito
|
|
|
32
37
|
|
|
33
38
|
[]
|
|
34
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def ignored_dir?(dir)
|
|
44
|
+
expanded = File.expand_path(dir.to_s)
|
|
45
|
+
ignored_dirs.any? { |ignored| expanded.start_with?(ignored) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ignored_dirs
|
|
49
|
+
@ignored_dirs ||= build_ignored_dirs
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_ignored_dirs
|
|
53
|
+
base = File.expand_path(base_path)
|
|
54
|
+
output = `git -C "#{base}" ls-files --others --ignored --exclude-standard --directory 2>/dev/null`
|
|
55
|
+
return [] unless $CHILD_STATUS.success?
|
|
56
|
+
|
|
57
|
+
output.lines.filter_map do |line|
|
|
58
|
+
path = line.chomp
|
|
59
|
+
File.join(base, path.delete_suffix("/")) if path.end_with?("/")
|
|
60
|
+
end
|
|
61
|
+
rescue Errno::ENOENT
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
35
64
|
end
|
|
36
65
|
end
|
data/lib/flatito/utils.rb
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Flatito
|
|
4
4
|
module Utils
|
|
5
|
-
def truncate(string, max
|
|
6
|
-
string.length
|
|
5
|
+
def truncate(string, max: 50, match_position: nil)
|
|
6
|
+
return string if string.length <= max
|
|
7
|
+
|
|
8
|
+
if match_position && match_position > max
|
|
9
|
+
start = [match_position - (max / 2), 0].max
|
|
10
|
+
ending = start + max
|
|
11
|
+
result = string[start...ending]
|
|
12
|
+
result = "...#{result}" if start.positive?
|
|
13
|
+
result = "#{result}..." if ending < string.length
|
|
14
|
+
result
|
|
15
|
+
else
|
|
16
|
+
"#{string[0...max]}..."
|
|
17
|
+
end
|
|
7
18
|
end
|
|
8
19
|
end
|
|
9
20
|
end
|
data/lib/flatito/version.rb
CHANGED
data/lib/flatito.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "colorize"
|
|
|
5
5
|
require_relative "flatito/version"
|
|
6
6
|
require_relative "flatito/tree_iterator"
|
|
7
7
|
require_relative "flatito/flatten_yaml"
|
|
8
|
+
require_relative "flatito/json_scanner"
|
|
8
9
|
require_relative "flatito/finder"
|
|
9
10
|
require_relative "flatito/yaml_with_line_number"
|
|
10
11
|
require_relative "flatito/renderer"
|
|
@@ -18,11 +19,12 @@ module Flatito
|
|
|
18
19
|
Finder.new(paths, options).call
|
|
19
20
|
rescue Interrupt
|
|
20
21
|
warn "\nInterrupted"
|
|
22
|
+
nil
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def flat_content(content, options = {})
|
|
24
26
|
items = FlattenYaml.items_from_content(content)
|
|
25
|
-
PrintItems.new(options[:search]).print(items)
|
|
27
|
+
PrintItems.new(options[:search], options[:search_value], case_sensitive: options[:case_sensitive]).print(items) || false
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flatito
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- José Galisteo
|
|
@@ -37,12 +37,14 @@ files:
|
|
|
37
37
|
- LICENSE.txt
|
|
38
38
|
- README.md
|
|
39
39
|
- Rakefile
|
|
40
|
+
- benchmark.rb
|
|
40
41
|
- docs/screenshot.png
|
|
41
42
|
- exe/flatito
|
|
42
43
|
- lib/flatito.rb
|
|
43
44
|
- lib/flatito/config.rb
|
|
44
45
|
- lib/flatito/finder.rb
|
|
45
46
|
- lib/flatito/flatten_yaml.rb
|
|
47
|
+
- lib/flatito/json_scanner.rb
|
|
46
48
|
- lib/flatito/print_items.rb
|
|
47
49
|
- lib/flatito/regex_from_search.rb
|
|
48
50
|
- lib/flatito/renderer.rb
|