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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a906047322b5ee01c3e8fe05e16d4ad9155032ad2d6ffa1756270de8f3339a2
4
- data.tar.gz: 07d41acd512f19c0c0b0c03cd3be90261faada0789dc3db66c4f9b4fefc0c1be
3
+ metadata.gz: 7864bfbf6505512a8f41e61b75efdb1081a62677f8686c37276a631f51572e96
4
+ data.tar.gz: d40ab3fd44a41e0cbd091e3dfbc62de98b931318fdda2efbbb8d89e206e20799
5
5
  SHA512:
6
- metadata.gz: 3be43b0a5232da51a840cf9cc5b79de24dc0159af605255552299a4bd988f6ec1e9b1a2d2df0d1c450df0d354a8bb8f43d9c9f257cc02307be20a77352de0a0d
7
- data.tar.gz: 7e04d4621eee22bac06148cad9cebf83de29b1b3d299bd0cd5c162a797c447e6ae922f903784ecfc8678781121852593326d21f07545528b2d64237db15e5578
6
+ metadata.gz: 263878449f3724e3d1b46c8a18eebfad0445504a080726cb38b29a25675ea23db278ab0f5157041f6effff62b3ec7d50eb97d76e531ad1ba412e0d603c0a8607
7
+ data.tar.gz: 3c04d9bf798fd85b72af32946c03dabc888355c5b315a300bdff1aa4c8a7c0fcbcb3aa4db24c3a3e94fe27bba8e48afd19a1c38cb7a09ff9f63391c3a115cf9d
data/.rubocop.yml CHANGED
@@ -1,9 +1,12 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.0
3
3
  NewCops: enable
4
+ Exclude:
5
+ - "benchmark.rb"
6
+ - "vendor/**/*"
4
7
 
5
8
 
6
- require:
9
+ plugins:
7
10
  - rubocop-performance
8
11
  - rubocop-minitest
9
12
  - rubocop-rake
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 for a key and get the value and the line number where it is located.
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
  ![Example](docs/screenshot.png)
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 string
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 string") do |s|
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
- Flatito.flat_content(stdin, options)
50
- else
51
- Flatito.search(ARGV, options)
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
@@ -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, :stder, :stdin
8
+ attr_accessor :renderer, :stdout
11
9
 
12
10
  def prepare_with_options(options)
13
11
  self.renderer = Renderer.build(options)
@@ -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
- paths.each do |path|
25
- TreeIterator.new(path, options).each do |pathname|
26
- renderer.print_file_progress(pathname)
32
+ files = collect_candidate_files
27
33
 
28
- if extensions.include?(pathname.extname)
29
- flat_and_filter(pathname)
30
- end
31
- end
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
- items = FlattenYaml.items_from_path(pathname)
45
- print_items.print(items, pathname)
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)
@@ -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
- with_line_numbers.compact.flat_map do |line|
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.compact
39
+ end.flatten
31
40
  end
32
41
 
33
42
  def flatten_hash(hash, prefix = nil)
34
- hash.flat_map do |key, value|
35
- if value.is_a?(YAMLWithLineNumber::ValueWithLineNumbers)
36
- if value.value.is_a?(Hash)
37
- flatten_hash(value.value, [prefix, key].compact.join("."))
38
- else
39
- Item.new(key: [prefix, key].compact.join("."), value: truncate(value.value.to_s), line: value.line)
40
- end
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
@@ -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
- return unless items.any?
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
@@ -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
- colorize("=> #{item.value}", :gray)
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
- regex.match(string).to_a.each do |match|
64
- string = string.gsub(/#{match}/, match.colorize(:light_red))
65
- end
66
- string
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
- ENV["TERM"] == "dumb" || ENV["NO_COLOR"] == "true" || no_color == true
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[:skip_hidden] || true
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 = 50)
6
- string.length > max ? "#{string[0...max]}..." : string
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flatito
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
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.2
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