filter_rename 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c66a6e6a12b5c1cd51491e73a55596795d6d037aef4a1d75261af07bf0431bb7
4
- data.tar.gz: b1979a7d6c40c717b3f2a4b5721ce1e955a83abd8d7ec50607d53b832a57fac8
3
+ metadata.gz: '0772829a453b3d53a307e213ea42d969c6f5f6598a47ca8c19236b2eae7d46fb'
4
+ data.tar.gz: ac03f34f6eca2f5650bc283f7c139e4f8032cf87a25134b810eda5716a9577de
5
5
  SHA512:
6
- metadata.gz: f22d9fd81c17090881c3898067b13e7c5d3e7b8fc3f4799eac73b46d0f96ace2a9aba2a36928231a8d0ba9bd8ec65904cf526f424781def5ba172baf267ee68d
7
- data.tar.gz: 8072a807330c8c2daeb6b1f4a4820f14be9ab70c1e7802967e0980a39dcd5900c127285c843914507c485ef140621b03faa8d31fccc2043e652238bdf004f8d3
6
+ metadata.gz: 4a33e3231cdc186f63cacb21ef752ca78b136a76f64c138ec02cf58470d2ebb02afc35dc5e0a22dbce2a712a93e184faf1a962cce66ab2ef948771fe2f546789
7
+ data.tar.gz: d08e371afb6cea91b7cef9a228b8e8f4f49bcf3c618b6d1ae0b32664539b744d41a9f02d97fc5537bcc60fa00348f3da54cb10679ba23571beda6c1cf18a5293
@@ -0,0 +1,14 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [fabiomux]
4
+ #patreon: # Replace with a single Patreon username
5
+ #open_collective: # Replace with a single Open Collective username
6
+ #ko_fi: # Replace with a single Ko-fi username
7
+ #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ #liberapay: # Replace with a single Liberapay username
10
+ #issuehunt: # Replace with a single IssueHunt username
11
+ #otechie: # Replace with a single Otechie username
12
+ #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13
+ custom: ['https://www.buymeacoffee.com/DCkNYFg']
14
+
@@ -9,16 +9,16 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Fabio Mucciante"]
10
10
  spec.email = ["fabio.mucciante@gmail.com"]
11
11
 
12
- spec.summary = %q{Rename filenames through a chain of filter.}
13
- spec.description = %q{Rename filenames applying a cascade of commands called filters in specified portion of it called targets.}
12
+ spec.summary = %q{File renaming tool which make use of a chain of actions called filters.}
13
+ spec.description = %q{FilterRename is a bulk renaming tool, based on the concept of filters as small operations to perform over sections of the full filename logically represented in targets.}
14
14
  spec.homepage = "https://github.com/fabiomux/filter_rename"
15
15
  spec.license = "GPL-3.0"
16
16
 
17
17
  spec.metadata = {
18
18
  "bug_tracker_uri" => "https://github.com/fabiomux/filter_rename/issues",
19
- #"changelog_uri" => "",
19
+ "changelog_uri" => "https://freeaptitude.altervista.org/projects/filter-rename.htm#changelog",
20
20
  "documentation_uri" => "https://www.rubydoc.info/gems/filter_rename/#{spec.version}",
21
- "homepage_uri" => "https://github.com/fabiomux/filter_rename",
21
+ "homepage_uri" => "https://freeaptitude.altervista.org/projects/filter-rename.html",
22
22
  #"mailing_list_uri" => "",
23
23
  "source_code_uri" => "https://github.com/fabiomux/filter_rename",
24
24
  "wiki_uri" => "https://github.com/fabiomux/filter_rename/wiki"
@@ -113,7 +113,7 @@ module FilterRename
113
113
  opt_parser.parse!(ARGV)
114
114
 
115
115
  (ARGV.empty? ? ARGF : ARGV).each do |f|
116
- f = File.expand_path(f.strip)
116
+ f = File.expand_path(f.chomp)
117
117
 
118
118
  if File.exists?(f)
119
119
  options.files << f
@@ -53,12 +53,14 @@ module FilterRename
53
53
 
54
54
 
55
55
  class GlobalConfig
56
- attr_reader :date_format, :hash_type, :counter_length, :counter_start, :targets,
56
+ attr_reader :date_format, :hash_type, :hash_on_tags, :hash_if_exists, :counter_length, :counter_start, :targets,
57
57
  :pdf_metadata, :image_metadata, :mp3_metadata
58
58
 
59
59
  def initialize(cfg)
60
60
  @date_format = cfg[:date_format] || '%Y-%m-%d'
61
- @hash_type = cfg[:hash_type].to_sym || :none
61
+ @hash_type = cfg[:hash_type].to_sym || :md5
62
+ @hash_on_tags = cfg[:hash_on_tags] || false
63
+ @hash_if_exists = cfg[:hash_if_exists] || false
62
64
  @counter_length = cfg[:counter_length] || 4
63
65
  @counter_start = cfg[:counter_start] || 0
64
66
  @targets = cfg[:targets].to_sym || :short
@@ -70,10 +72,12 @@ module FilterRename
70
72
 
71
73
 
72
74
  class FilterConfig
73
- attr_accessor :word_separator, :target, :ignore_case, :lang, :grep, :grep_on, :grep_exclude, :grep_target
75
+ attr_accessor :word_separator, :number_separator, :occurrence_separator, :target, :ignore_case, :lang, :grep, :grep_on, :grep_exclude, :grep_target
74
76
 
75
77
  def initialize(cfg)
76
78
  @word_separator = cfg[:word_separator] || ' '
79
+ @number_separator = cfg[:number_separator] || '.'
80
+ @occurrence_separator = cfg[:occurrence_separator] || '-'
77
81
  @target = cfg[:target].to_sym || :name
78
82
  @ignore_case = cfg[:ignore_case].nil? ? true : cfg[:ignore_case].to_boolean
79
83
  @lang = (cfg[:lang] || :en).to_sym
@@ -2,6 +2,8 @@ module FilterRename
2
2
 
3
3
  class Filename
4
4
 
5
+ attr_reader :original
6
+
5
7
  def self.has_writable_tags
6
8
  false
7
9
  end
@@ -11,6 +13,7 @@ module FilterRename
11
13
  @@count += 1
12
14
  @cfg = cfg
13
15
 
16
+ @original = fname
14
17
  load_filename_data(fname)
15
18
  end
16
19
 
@@ -100,6 +103,14 @@ module FilterRename
100
103
  res
101
104
  end
102
105
 
106
+ def writable?(tag)
107
+ instance_variable_get("@#{tag}".to_sym).writable?
108
+ end
109
+
110
+ def custom?(tag)
111
+ instance_variable_get("@#{tag}".to_sym).custom?
112
+ end
113
+
103
114
  def values
104
115
  res = {}
105
116
  instance_variables.each do |v|
@@ -135,7 +146,9 @@ module FilterRename
135
146
 
136
147
  [@count, @ctime, @mtime, @size, @pretty_size].map(&:readonly!)
137
148
 
138
- metatag_to_var!('hash', calculate_hash(@cfg.hash_type), true) unless @cfg.hash_type == :none
149
+ [@ext, @name, @path, @folder, @path, @count, @ctime, @size, @pretty_size].map(&:basic!)
150
+
151
+ metatag_to_var!('hash', calculate_hash(@cfg.hash_type), true) if @cfg.hash_on_tags
139
152
  end
140
153
  end
141
154
 
@@ -16,9 +16,9 @@ module FilterRename
16
16
 
17
17
  def ==(dest)
18
18
  super &&
19
- ([ @title, @artist, @album, @track, @comments, @year, @genre ] ==
19
+ ([ @title, @artist, @album, @track, @comments, @year, @genre, @genre_s ] ==
20
20
  [ dest.get_string(:title), dest.get_string(:artist), dest.get_string(:album), dest.get_string(:track),
21
- dest.get_string(:comments), dest.get_string(:year), dest.get_string(:genre) ])
21
+ dest.get_string(:comments), dest.get_string(:year), dest.get_string(:genre), dest.get_string(:genre_s) ])
22
22
  end
23
23
 
24
24
  def rename!(dest)
@@ -26,8 +26,8 @@ module FilterRename
26
26
 
27
27
  Mp3Info.open(full_filename) do |mp3|
28
28
  old_data.merge!({ title: mp3.tag.title, artist: mp3.tag.artist, album: mp3.tag.album,
29
- tracknum: mp3.tag.tracknum, comments: mp3.tag.comments, year: mp3.tag.year,
30
- genre_s: mp3.tag.genre_s })
29
+ track: mp3.tag.tracknum, comments: mp3.tag.comments, year: mp3.tag.year,
30
+ genre: mp3.tag.genre, genre_s: mp3.tag.genre_s })
31
31
 
32
32
  mp3.tag.title = dest.get_string(:title)
33
33
  mp3.tag.artist = dest.get_string(:artist)
@@ -35,8 +35,8 @@ module FilterRename
35
35
  mp3.tag.tracknum = dest.get_string(:track)
36
36
  mp3.tag.comments = dest.get_string(:comments).to_s
37
37
  mp3.tag.year = dest.get_string(:year)
38
- mp3.tag.genre_s = dest.get_string(:genre)
39
-
38
+ mp3.tag.genre = dest.get_string(:genre).to_i % 256
39
+ mp3.tag.genre_s = dest.get_string(:genre_s)
40
40
  end
41
41
 
42
42
  load_mp3_data(full_filename)
@@ -53,6 +53,7 @@ module FilterRename
53
53
  Comments: #{Differ.diff_by_word(dest.get_string(:comments).to_s, @comments.to_s)}
54
54
  Year: #{Differ.diff_by_word(dest.get_string(:year).to_s, @year.to_s)}
55
55
  Genre: #{Differ.diff_by_word(dest.get_string(:genre).to_s, @genre.to_s)}
56
+ GenreS: #{Differ.diff_by_word(dest.get_string(:genre_s).to_s, @genre_s.to_s)}
56
57
  "
57
58
  end
58
59
 
@@ -67,7 +68,8 @@ module FilterRename
67
68
  @track = mp3info.tag.tracknum.to_i
68
69
  @comments = mp3info.tag.comments.to_s
69
70
  @year = mp3info.tag.year.to_i
70
- @genre = mp3info.tag.genre_s.to_s
71
+ @genre = mp3info.tag.genre.to_i
72
+ @genre_s = mp3info.tag.genre_s.to_s
71
73
 
72
74
  # read only stuff
73
75
  @vbr = (mp3info.tag.vbr ? 'vbr' : '')
@@ -71,7 +71,7 @@ module FilterRename
71
71
  super @cfg.target, value
72
72
  else
73
73
  super target, value
74
- end
74
+ end unless value.nil?
75
75
  end
76
76
 
77
77
  def get_string(target = nil)
@@ -91,20 +91,39 @@ module FilterRename
91
91
  str
92
92
  end
93
93
 
94
+ def current_target
95
+ @cfg.target.to_s
96
+ end
94
97
  end
95
98
 
96
99
 
97
100
  module IndexedParams
98
101
 
99
- def get_indexes(params, callback)
102
+ attr_reader :params, :params_expanded, :items
103
+
104
+ def normalize_index(idx)
105
+ max_length = items.length
106
+ raise IndexOutOfRange, [idx, max_length] if idx.to_i > max_length || idx.to_i < -max_length
107
+
108
+ if idx.to_i.positive?
109
+ idx = idx.to_i.pred # % max_length
110
+ elsif idx.to_i.negative?
111
+ idx = idx.to_i + max_length # % max_length
112
+ end
113
+ idx.to_i
114
+ end
115
+
116
+ def get_indexes
100
117
  indexes = []
101
118
  params_length = (indexed_params == 0) ? params.length : indexed_params
102
119
 
103
120
  params[0..params_length.pred].each do |x|
104
121
  if x =~ /\.\./
105
- indexes = indexes + Range.new(*(x.split('..').map {|y| send(callback, y, get_string) })).map { |i| i }
122
+ indexes = indexes + Range.new(*(x.split('..').map { |y| normalize_index(y) })).map { |i| i }
123
+ elsif x =~ /:/
124
+ indexes = indexes + x.split(':').map { |y| normalize_index(y) }
106
125
  else
107
- indexes << send(callback, x, get_string)
126
+ indexes << normalize_index(x)
108
127
  end
109
128
 
110
129
  end
@@ -112,18 +131,37 @@ module FilterRename
112
131
  indexes
113
132
  end
114
133
 
134
+ def string_to_loop
135
+ get_string
136
+ end
137
+
115
138
  def indexed_params
116
139
  1
117
140
  end
141
+
142
+ def self_targeted?
143
+ false
144
+ end
145
+
146
+ def filter(params)
147
+ @params = params
148
+ @items = indexed_items
149
+ @params_expanded = get_indexes
150
+
151
+ res = loop_items
152
+
153
+ if self_targeted?
154
+ super get_string
155
+ else
156
+ super res
157
+ end
158
+ end
118
159
  end
119
160
 
120
161
 
121
162
  class FilterWord < FilterBase
122
- include IndexedParams
123
163
 
124
- def filter(params)
125
- super loop_words(get_string, get_indexes(params, :word_idx), params)
126
- end
164
+ include IndexedParams
127
165
 
128
166
 
129
167
  private
@@ -132,56 +170,99 @@ module FilterRename
132
170
  get_config(:word_separator)
133
171
  end
134
172
 
135
- def word_idx(idx, str)
136
- if idx.to_i.positive?
137
- idx = idx.to_i.pred
138
- elsif idx.to_i.negative?
139
- idx = idx.to_i + str.split(ws).length
140
- end
141
- idx.to_i
173
+ def indexed_items
174
+ string_to_loop.split(ws)
142
175
  end
143
176
 
144
- def loop_words(str, arr_index, params)
145
- str = str.split(ws)
177
+ def loop_items
178
+ str = items.clone
146
179
 
147
- arr_index.each_with_index do |idx, param_num|
148
- str[idx] = send :filtered_word, str[idx], params, param_num.next
180
+ params_expanded.each_with_index do |idx, param_num|
181
+ str[idx] = send :filtered_word, str[idx], param_num.next
149
182
  end
150
183
 
151
184
  str.delete_if(&:nil?).join(ws)
152
185
  end
153
-
154
186
  end
155
187
 
188
+
156
189
  class FilterNumber < FilterBase
157
190
 
158
191
  include IndexedParams
159
192
 
193
+
194
+ private
195
+
196
+ def ns
197
+ get_config(:number_separator)
198
+ end
199
+
200
+ def indexed_items
201
+ string_to_loop.get_numbers
202
+ end
203
+
204
+ def loop_items
205
+ str = string_to_loop
206
+ numbers = items.clone
207
+
208
+ params_expanded.each_with_index do |idx, param_idx|
209
+ numbers[idx] = self.send :filtered_number, numbers[idx], param_idx.next
210
+ end
211
+ str = str.map_number_with_index do |num, i|
212
+ numbers[i]
213
+ end
214
+
215
+ str
216
+ end
217
+ end
218
+
219
+
220
+ class FilterRegExp < FilterBase
221
+
160
222
  def filter(params)
161
- super loop_numbers(get_string, get_indexes(params, :num_idx), params)
223
+ super loop_regex(get_string, params)
162
224
  end
163
225
 
164
226
 
165
227
  private
166
228
 
167
- def num_idx(idx, str)
168
- if idx.to_i < 0
169
- idx = str.scan(/\d+/).length + idx.to_i
170
- elsif idx.to_i > 0
171
- idx = idx.to_i.pred
229
+ def loop_regex(str, params)
230
+ str = str.gsub(Regexp.new(wrap_regex(params[0]), get_config(:ignore_case).to_boolean)) do |x|
231
+ matches = Regexp.last_match.clone
232
+ self.send(:filtered_regexp, matches.to_a.delete_if(&:nil?), params).to_s.gsub(/\\([0-9]+)/) { |y| matches[$1.to_i] }
172
233
  end
173
- idx.to_i
234
+
235
+ str
174
236
  end
175
237
 
176
- def loop_numbers(str, arr_index, params)
177
- arr_index.each_with_index do |idx, param_idx|
178
- str = str.map_number_with_index do |num, i|
179
- if idx == i
180
- num = self.send :filtered_number, num, params, param_idx.next
181
- end
238
+ end
182
239
 
183
- num
184
- end
240
+
241
+ class FilterOccurrence < FilterBase
242
+
243
+ include IndexedParams
244
+
245
+
246
+ private
247
+
248
+ def os
249
+ get_config(:occurrence_separator)
250
+ end
251
+
252
+ def indexed_items
253
+ string_to_loop.scan(Regexp.new(params[indexed_params]))
254
+ end
255
+
256
+ def loop_items
257
+ str = string_to_loop
258
+ regexp = Regexp.new(params[indexed_params])
259
+ occurences = items.clone
260
+
261
+ params_expanded.each_with_index do |idx, param_idx|
262
+ occurences[idx] = self.send :filtered_occurrence, occurences[idx], param_idx.next
263
+ end
264
+ str = str.gsub(regexp).with_index do |item, i|
265
+ occurences[i]
185
266
  end
186
267
 
187
268
  str
@@ -32,12 +32,22 @@ module FilterRename
32
32
  def self.hint; 'Add NUM to the NTH number'; end
33
33
  def self.params; 'NTH,NUM'; end
34
34
 
35
- def filtered_number(num, params, param_num)
35
+ def filtered_number(num, param_num)
36
36
  num.to_i + params[1].to_i
37
37
  end
38
38
  end
39
39
 
40
40
 
41
+ class AddNumberFrom < FilterNumber
42
+ def self.hint; 'Add the number from TARGET to the NTH'; end
43
+ def self.params; 'NTH;TARGET'; end
44
+
45
+ def filtered_number(num, param_num)
46
+ num.to_i + get_string(params[1]).to_i
47
+ end
48
+ end
49
+
50
+
41
51
  class Append < FilterBase
42
52
  def self.hint; 'Append the TEXT to the current target'; end
43
53
  def self.params; 'TEXT'; end
@@ -85,7 +95,11 @@ module FilterRename
85
95
  def self.hint; 'Append the NTH number to TARGET'; end
86
96
  def self.params; 'NTH,TARGET'; end
87
97
 
88
- def filtered_number(num, params, param_num)
98
+ def self_targeted?
99
+ params[-1] == current_target
100
+ end
101
+
102
+ def filtered_number(num, param_num)
89
103
  str = get_string(params[1])
90
104
  set_string("#{str}#{num}", params[1])
91
105
  num
@@ -97,7 +111,7 @@ module FilterRename
97
111
  def self.hint; 'Append the TEXT to the NTH number'; end
98
112
  def self.params; 'NTH,TEXT'; end
99
113
 
100
- def filtered_number(num, params, param_num)
114
+ def filtered_number(num, param_num)
101
115
  "#{num}#{params[1]}"
102
116
  end
103
117
  end
@@ -107,7 +121,7 @@ module FilterRename
107
121
  def self.hint; 'Append the TEXT to the NTH word'; end
108
122
  def self.params; 'NTH,TEXT'; end
109
123
 
110
- def filtered_word(word, params, param_num)
124
+ def filtered_word(word, param_num)
111
125
  word + params[1]
112
126
  end
113
127
  end
@@ -117,10 +131,17 @@ module FilterRename
117
131
  def self.hint; 'Append the NTH word from TARGET'; end
118
132
  def self.params; 'NTH,TARGET'; end
119
133
 
120
- def filter(params)
121
- word = get_string(params[1]).split(ws)
122
- idx = word_idx(params[0], word)
123
- set_string [get_string, word[idx]].join(ws)
134
+ def string_to_loop
135
+ get_string(params[1])
136
+ end
137
+
138
+ def self_targeted?
139
+ true
140
+ end
141
+
142
+ def filtered_word(word, param_num)
143
+ set_string([get_string, word].join(ws))
144
+ word
124
145
  end
125
146
  end
126
147
 
@@ -129,7 +150,11 @@ module FilterRename
129
150
  def self.hint; 'Append the NTH word to TARGET'; end
130
151
  def self.params; 'NTH,TARGET'; end
131
152
 
132
- def filtered_word(word, params, param_num)
153
+ def self_targeted?
154
+ params[-1] == current_target
155
+ end
156
+
157
+ def filtered_word(word, param_num)
133
158
  set_string([get_string(params[1]), word].join(ws), params[1])
134
159
  word
135
160
  end
@@ -147,6 +172,16 @@ module FilterRename
147
172
  end
148
173
 
149
174
 
175
+ class CapitalizeWord < FilterWord
176
+ def self.hint; 'Capitalize the NTH word'; end
177
+ def self.params; 'NTH'; end
178
+
179
+ def filtered_word(word, param_num)
180
+ word.capitalize
181
+ end
182
+ end
183
+
184
+
150
185
  class CopyFrom < FilterBase
151
186
  def self.hint; 'Copy the text from TARGET'; end
152
187
  def self.params; 'TARGET'; end
@@ -158,23 +193,46 @@ module FilterRename
158
193
 
159
194
 
160
195
  class CopyNumberTo < FilterNumber
161
- def self.hint; 'Move the NTH number to TARGET'; end
196
+ def self.hint; 'Copy the NTH number to TARGET'; end
162
197
  def self.params; 'NTH,TARGET'; end
163
198
 
164
- def filtered_number(num, params, param_num)
199
+ def self_targeted?
200
+ params[-1] == current_target
201
+ end
202
+
203
+ def filtered_number(num, param_num)
165
204
  set_string(num, params[1])
166
205
  num
167
206
  end
168
207
  end
169
208
 
170
209
 
171
- class CopyTo < FilterBase
172
- def self.hint; 'Copy the text selected by REGEX to TARGET'; end
210
+ class CopyOccurrenceTo < FilterOccurrence
211
+ def self.hint; 'Copy the NTH occurrence of REGEX to TARGET'; end
212
+ def self.params; 'NTH,REGEX,TARGET'; end
213
+
214
+ def self_targeted?
215
+ params[-1] == current_target
216
+ end
217
+
218
+ def filtered_occurrence(occurrence, param_num)
219
+ set_string(occurrence, params[-1])
220
+ occurrence
221
+ end
222
+ end
223
+
224
+
225
+ class CopyTo < FilterRegExp
226
+ def self.hint; 'Copy the text selected by REGEX to TARGET or TARGET_#'; end
173
227
  def self.params; 'REGEX,TARGET'; end
174
228
 
175
- def filter(params)
176
- set_string(get_string.scan(Regexp.new(wrap_regex(params[0]), get_config(:ignore_case).to_boolean)).pop.to_a.pop, params[1])
177
- super get_string
229
+ def filtered_regexp(matches, params)
230
+ if matches.length > 2
231
+ matches[1..-1].each_with_index { |x, i| set_string(x, "#{params[1]}_#{i.next}") }
232
+ else
233
+ set_string(matches[1], params[1])
234
+ end
235
+ matches[0]
178
236
  end
179
237
  end
180
238
 
@@ -185,12 +243,14 @@ module FilterRename
185
243
 
186
244
  def indexed_params; 2; end
187
245
 
188
- def filtered_word(word, params, param_num)
246
+ def filtered_word(word, param_num)
189
247
  case param_num
190
248
  when 1
191
249
  @word = word
192
- when 2
193
- word = @word + ws + word
250
+ when params_expanded.length
251
+ word = [@word, word].join(ws)
252
+ else
253
+ @word = [@word, word].join(ws)
194
254
  end
195
255
 
196
256
  word
@@ -198,14 +258,12 @@ module FilterRename
198
258
  end
199
259
 
200
260
 
201
- class Delete < FilterBase
261
+ class Delete < FilterRegExp
202
262
  def self.hint; 'Remove the text matching REGEX'; end
203
- def self.params; 'REGEX1[,REGEX2,...]'; end
263
+ def self.params; 'REGEX'; end
204
264
 
205
- def filter(params)
206
- params.each do |par|
207
- super get_string.gsub(Regexp.new(par, get_config(:ignore_case).to_boolean), '')
208
- end
265
+ def filtered_regexp(matches, params)
266
+ ''
209
267
  end
210
268
  end
211
269
 
@@ -214,17 +272,27 @@ module FilterRename
214
272
  def self.hint; 'Remove the NTH number'; end
215
273
  def self.params; 'NTH'; end
216
274
 
217
- def filtered_number(num, params, param_num)
275
+ def filtered_number(num, param_num)
218
276
  ''
219
277
  end
220
278
  end
221
279
 
222
280
 
281
+ class DeleteOccurrence < FilterOccurrence
282
+ def self.hint; 'Delete the NTH occurrence of REGEXP'; end
283
+ def self.params; 'NTH,REGEXP'; end
284
+
285
+ def filtered_occurrence(occurrence, param_num)
286
+ nil
287
+ end
288
+ end
289
+
290
+
223
291
  class DeleteWord < FilterWord
224
292
  def self.hint; 'Remove the NTH word'; end
225
293
  def self.params; 'NTH'; end
226
294
 
227
- def filtered_word(word, params, param_num)
295
+ def filtered_word(word, param_num)
228
296
  nil
229
297
  end
230
298
  end
@@ -234,7 +302,7 @@ module FilterRename
234
302
  def self.hint; 'Format the NTH number adding leading zeroes to have LENGTH'; end
235
303
  def self.params; 'NTH,LENGTH'; end
236
304
 
237
- def filtered_number(num, params, param_num)
305
+ def filtered_number(num, param_num)
238
306
  num.to_i.to_s.rjust(params[1].to_i, '0')
239
307
  end
240
308
  end
@@ -244,7 +312,7 @@ module FilterRename
244
312
  def self.hint; 'Insert the WORD after the NTH word'; end
245
313
  def self.params; 'NTH,WORD'; end
246
314
 
247
- def filtered_word(word, params, param_num)
315
+ def filtered_word(word, param_num)
248
316
  [word, params[1]].join(ws)
249
317
  end
250
318
  end
@@ -254,23 +322,54 @@ module FilterRename
254
322
  def self.hint; 'Insert the WORD after the NTH word'; end
255
323
  def self.params; 'NTH,WORD'; end
256
324
 
257
- def filtered_word(word, params, param_num)
325
+ def filtered_word(word, param_num)
258
326
  [params[1], word].join(ws)
259
327
  end
260
328
  end
261
329
 
262
330
 
331
+ class JoinNumbers < FilterNumber
332
+ def self.hint; 'Join the words NTH1 and NTH2 and replace the NTH3 number with it'; end
333
+ def self.params; 'NTH1,NTH2,NTH3'; end
334
+
335
+ def indexed_params; 3; end
336
+
337
+ def filtered_number(number, param_num)
338
+ case param_num
339
+ when 1
340
+ @number = number
341
+ number = nil
342
+ when params_expanded.length
343
+ number = @number
344
+ else
345
+ @number += number
346
+ number = nil
347
+ end
348
+
349
+ number
350
+ end
351
+ end
352
+
353
+
263
354
  class JoinWords < FilterWord
264
- def self.hint; 'Join the words from NTH1 to NTH2'; end
265
- def self.params; 'NTH1,NTH2'; end
355
+ def self.hint; 'Join the words NTH1 and NTH2 and replace the NTH3 word with it'; end
356
+ def self.params; 'NTH1,NTH2,NTH3'; end
266
357
 
267
- def filter(params)
268
- res = get_string.split(ws)
269
- istart = word_idx(params[0], get_string)
270
- iend = word_idx(params[1], get_string)
358
+ def indexed_params; 3; end
359
+
360
+ def filtered_word(word, param_num)
361
+ case param_num
362
+ when 1
363
+ @word = word
364
+ word = nil
365
+ when params_expanded.length
366
+ word = @word
367
+ else
368
+ @word += word
369
+ word = nil
370
+ end
271
371
 
272
- res = res.insert(istart, res[istart..iend].join)
273
- set_string res.delete_if.with_index { |x, idx| ((istart.next)..(iend.next)).include?(idx) }.join(ws)
372
+ word
274
373
  end
275
374
  end
276
375
 
@@ -295,43 +394,77 @@ module FilterRename
295
394
  end
296
395
 
297
396
 
298
- class MoveTo < FilterBase
299
- def self.hint; 'Move the text selected by REGEX to TARGET'; end
397
+ class LowercaseWord < FilterWord
398
+ def self.hint; 'Lowercase the NTH word'; end
399
+ def self.params; 'NTH'; end
400
+
401
+ def filtered_word(word, param_num)
402
+ word.downcase
403
+ end
404
+ end
405
+
406
+
407
+ class MoveTo < FilterRegExp
408
+ def self.hint; 'Move the text selected by REGEX to TARGET or TARGET_#'; end
300
409
  def self.params; 'REGEX,TARGET'; end
301
410
 
302
- def filter(params)
303
- regex = Regexp.new(wrap_regex(params[0]), get_config(:ignore_case).to_boolean)
304
- set_string(get_string.scan(regex).pop.to_a.pop, params[1])
305
- str = get_string.gsub(regex, '')
306
- super str
411
+ def filtered_regexp(matches, params)
412
+ if matches.length > 2
413
+ matches[1..-1].each_with_index { |x, i| set_string(x, "#{params[1]}_#{i.next}") }
414
+ else
415
+ set_string(matches[1], params[1])
416
+ end
417
+ ''
307
418
  end
308
419
  end
309
420
 
310
421
 
311
422
  class MoveNumberTo < FilterNumber
312
- def self.hint; 'Move the NTH number to TARGET'; end
423
+ def self.hint; 'Move the NTH number to TARGET overwriting it'; end
313
424
  def self.params; 'NTH,TARGET'; end
314
425
 
315
- def filtered_number(num, params, param_num)
426
+ def self_targeted?
427
+ params[-1] == current_target
428
+ end
429
+
430
+ def filtered_number(num, param_num)
316
431
  set_string(num, params[1])
317
432
  ''
318
433
  end
319
434
  end
320
435
 
321
436
 
437
+ class MoveOccurrenceTo < FilterOccurrence
438
+ def self.hint; 'Move the NTH occurrence of REGEX to TARGET overwriting it'; end
439
+ def self.params; 'NTH,REGEX,TARGET'; end
440
+
441
+ def self_targeted?
442
+ params[-1] == current_target
443
+ end
444
+
445
+ def filtered_occurrence(occurrence, param_num)
446
+ set_string(occurrence, params[-1])
447
+ nil
448
+ end
449
+ end
450
+
451
+
322
452
  class MoveWord < FilterWord
323
453
  def self.hint; 'Move the NTH1 word to the NTH2 place'; end
324
454
  def self.params; 'NTH1,NTH2'; end
325
455
 
326
456
  def indexed_params; 2; end
327
457
 
328
- def filtered_word(word, params, param_num)
458
+ def filtered_word(word, param_num)
329
459
  case param_num
330
460
  when 1
331
461
  @word = word
332
462
  res = nil
333
- when 2
463
+ when params_expanded.length
334
464
  res = [word, @word].join(ws)
465
+ else
466
+ @word = [@word, word].join(ws)
467
+ res = nil
335
468
  end
336
469
 
337
470
  res
@@ -340,10 +473,14 @@ module FilterRename
340
473
 
341
474
 
342
475
  class MoveWordTo < FilterWord
343
- def self.hint; 'Move the NTH word to TARGET'; end
476
+ def self.hint; 'Move the NTH word to TARGET overwriting it'; end
344
477
  def self.params; 'NTH,TARGET'; end
345
478
 
346
- def filtered_word(word, params, param_num)
479
+ def self_targeted?
480
+ params[-1] == current_target
481
+ end
482
+
483
+ def filtered_word(word, param_num)
347
484
  set_string(word, params[1])
348
485
  nil
349
486
  end
@@ -354,7 +491,7 @@ module FilterRename
354
491
  def self.hint; 'Multiply the NTH number with NUM'; end
355
492
  def self.params; 'NTH,NUM'; end
356
493
 
357
- def filtered_number(num, params, param_num)
494
+ def filtered_number(num, param_num)
358
495
  num.to_i * params[1].to_i
359
496
  end
360
497
  end
@@ -384,7 +521,7 @@ module FilterRename
384
521
  def self.hint; 'Prepend the TEXT to the NTH number'; end
385
522
  def self.params; 'NTH,TEXT'; end
386
523
 
387
- def filtered_number(num, params, param_num)
524
+ def filtered_number(num, param_num)
388
525
  "#{params[1]}#{num}"
389
526
  end
390
527
  end
@@ -394,32 +531,48 @@ module FilterRename
394
531
  def self.hint; 'Prepend the TEXT to the NTH word'; end
395
532
  def self.params; 'NTH,TEXT'; end
396
533
 
397
- def filtered_word(word, params, param_num)
534
+ def filtered_word(word, param_num)
398
535
  params[1] + word
399
536
  end
400
537
  end
401
538
 
402
539
 
403
- class Replace < FilterBase
540
+ class PrependWordFrom < FilterWord
541
+ def self.hint; 'Prepend with TARGET the NTH word'; end
542
+ def self.params; 'NTH,TARGET'; end
543
+
544
+ def string_to_loop
545
+ get_string(params[1])
546
+ end
547
+
548
+ def self_targeted?
549
+ true
550
+ end
551
+
552
+ def filtered_word(word, param_num)
553
+ set_string([word, get_string].join(ws))
554
+ word
555
+ end
556
+ end
557
+
558
+
559
+ class Replace < FilterRegExp
404
560
  def self.hint; 'Replace the text matching REGEX with REPLACE'; end
405
561
  def self.params; 'REGEX,REPLACE'; end
406
562
 
407
- def filter(params)
408
- regexp = Regexp.new(params[0], get_config(:ignore_case).to_boolean)
409
- super get_string.gsub(regexp, params[1])
563
+ def filtered_regexp(matches, params)
564
+ params[1]
410
565
  end
411
566
  end
412
567
 
413
568
 
414
- class ReplaceFrom < FilterBase
569
+ class ReplaceFrom < FilterRegExp
415
570
  def self.hint; 'Replace the REGEX matching text with the TARGET content'; end
416
571
  def self.params; 'REGEX,TARGET'; end
417
572
 
418
- def filter(params)
419
- regexp = Regexp.new(params[0], get_config(:ignore_case).to_boolean)
420
- super get_string.gsub(regexp, get_string(params[1]).to_s)
573
+ def filtered_regexp(matches, params)
574
+ get_string(params[1]).to_s
421
575
  end
422
-
423
576
  end
424
577
 
425
578
 
@@ -427,17 +580,27 @@ module FilterRename
427
580
  def self.hint; 'Replace the NTH number with NUMBER'; end
428
581
  def self.params; 'NTH,NUMBER'; end
429
582
 
430
- def filtered_number(num, params, param_num)
583
+ def filtered_number(num, param_num)
431
584
  params[1]
432
585
  end
433
586
  end
434
587
 
435
588
 
589
+ class ReplaceOccurrence < FilterOccurrence
590
+ def self.hint; 'Replace the NTH occurrence of REGEXP with TEXT'; end
591
+ def self.params; 'NTH,REGEXP,TEXT'; end
592
+
593
+ def filtered_occurrence(occurrence, param_num)
594
+ params[2]
595
+ end
596
+ end
597
+
598
+
436
599
  class ReplaceWord < FilterWord
437
600
  def self.hint; 'Replace the NTH word with TEXT'; end
438
601
  def self.params; 'NTH,TEXT'; end
439
602
 
440
- def filtered_word(word, params, param_num)
603
+ def filtered_word(word, param_num)
441
604
  params[1]
442
605
  end
443
606
  end
@@ -522,7 +685,7 @@ module FilterRename
522
685
  def self.hint; 'Split the NTH word using a REGEX with capturing groups'; end
523
686
  def self.params; 'NTH,REGEX'; end
524
687
 
525
- def filtered_word(word, params, param_num)
688
+ def filtered_word(word, param_num)
526
689
  word.scan(Regexp.new(wrap_regex(params[1]), get_config(:ignore_case))).pop.to_a.join(ws)
527
690
  end
528
691
  end
@@ -544,13 +707,17 @@ module FilterRename
544
707
 
545
708
  def indexed_params; 2; end
546
709
 
547
- def filtered_number(num, params, param_num)
710
+ def filtered_number(num, param_num)
548
711
  case param_num
549
712
  when 1
550
713
  @number = num.clone
551
- num = get_string.get_number(params[1].to_i.pred)
552
- when 2
714
+ @last_number = get_string.get_number(params_expanded[-1].to_i)
715
+ num = @last_number
716
+ when params_expanded.length
553
717
  num = @number
718
+ else
719
+ @number = [@number, num].join(ns)
720
+ num = @last_number
554
721
  end
555
722
 
556
723
  num
@@ -562,15 +729,19 @@ module FilterRename
562
729
  def self.hint; 'Swap the NTH1 word with the NTH2'; end
563
730
  def self.params; 'NTH1,NTH2'; end
564
731
 
565
- def indexed_words; 2; end
732
+ def indexed_params; 2; end
566
733
 
567
- def filtered_word(word, params, param_num)
734
+ def filtered_word(word, param_num)
568
735
  case param_num
569
736
  when 1
570
737
  @word = word.clone
571
- word = get_string.split(ws)[params[1].to_i.pred]
572
- when 2
738
+ @last_word = get_string.split(ws)[params_expanded[-1]]
739
+ word = @last_word
740
+ when params_expanded.length
573
741
  word = @word
742
+ else
743
+ @word = [@word, word].join(ws)
744
+ word = nil
574
745
  end
575
746
 
576
747
  word
@@ -627,32 +798,32 @@ module FilterRename
627
798
  end
628
799
 
629
800
 
630
- class Wrap < FilterBase
801
+ class UppercaseWord < FilterWord
802
+ def self.hint; 'Uppercase the NTH word'; end
803
+ def self.params; 'NTH'; end
804
+
805
+ def filtered_word(word, param_num)
806
+ word.upcase
807
+ end
808
+ end
809
+
810
+
811
+ class Wrap < FilterRegExp
631
812
  def self.hint; 'Wrap the text matching REGEX with SEPARATOR1 and SEPARATOR2'; end
632
813
  def self.params; 'REGEX,SEPARATOR1,SEPARATOR2'; end
633
814
 
634
- def filter(params)
635
- params[0] = "(#{params[0]})" unless params[0] =~ /[()]+/
636
- regexp = Regexp.new("(#{params[0]})", get_config(:ignore_case).to_boolean)
637
- super get_string.gsub(regexp, "#{params[1]}\\1#{params[2]}")
815
+ def filtered_regexp(matches, params)
816
+ "#{params[1]}#{matches[0]}#{params[2]}"
638
817
  end
639
818
  end
640
819
 
641
820
 
642
821
  class WrapWords < FilterWord
643
- def self.hint; 'Wrap the words between the NTH1 and the NTH2 with SEPARATOR1 and SEPARATOR2'; end
644
- def self.params; 'NTH1,NTH2,SEPARATOR1,SEPARATOR2'; end
822
+ def self.hint; 'Wrap the NTH word with SEPARATOR1 and SEPARATOR2'; end
823
+ def self.params; 'NTH,SEPARATOR1,SEPARATOR2'; end
645
824
 
646
- def indexed_words; 2; end
647
-
648
- def filtered_word(word, params, param_num)
649
- case param_num
650
- when 1
651
- word = "#{params[2]}#{word}"
652
- when 2
653
- word = "#{word}#{params[3]}"
654
- end
655
- word
825
+ def filtered_word(word, param_num)
826
+ "#{params[1]}#{word}#{params[2]}"
656
827
  end
657
828
  end
658
829
  end
@@ -1,4 +1,5 @@
1
1
  require 'date'
2
+ require 'digest'
2
3
 
3
4
  module FilterRename
4
5
 
@@ -20,6 +21,15 @@ module FilterRename
20
21
 
21
22
  module ReadableVariables
22
23
 
24
+ def basic!
25
+ @custom = false
26
+ self
27
+ end
28
+
29
+ def custom?
30
+ @custom == true || @custom == nil
31
+ end
32
+
23
33
  def readonly!
24
34
  @writable = false
25
35
  self
@@ -112,6 +122,10 @@ module FilterRename
112
122
  def get_number(idx)
113
123
  self.scan(/\d+/)[idx]
114
124
  end
125
+
126
+ def get_numbers
127
+ self.scan(/\d+/)
128
+ end
115
129
  end
116
130
 
117
131
 
@@ -171,7 +185,7 @@ module FilterRename
171
185
  old_source = old_data.empty? ? fp.source.values : old_data
172
186
 
173
187
  fp.dest.values.each do |k, v|
174
- puts " #{k}: ".bold.green + (old_source[k] || '-') + ' > '.bold.green + v if ((v != old_source[k]) && (!old_source[k].nil?))
188
+ puts " #{k}: ".rjust(15, ' ').bold.green + (old_source[k].to_s.empty? ? ' ~ '.bold.red : old_source[k].to_s) + ' => '.bold.green + v.to_s if ((v.to_s != old_source[k].to_s) && fp.source.writable?(k) && fp.source.custom?(k))
175
189
  end
176
190
  end
177
191
 
@@ -179,6 +193,16 @@ module FilterRename
179
193
  Messages.error "<#{fp.source.filename}> can't be renamed in <#{fp.dest.filename}>, it exists!"
180
194
  end
181
195
 
196
+ def self.file_hash(fp, hash_type, cached = nil)
197
+ raise UnknownHashCode, hash_type unless [:sha1, :sha2, :md5].include?(hash_type.to_sym)
198
+ klass = Object.const_get("Digest::#{hash_type.to_s.upcase}")
199
+ hash_src = klass.file fp.source.filename
200
+ hash_dest = cached ? klass.file(cached.original) : klass.file(fp.dest.filename)
201
+
202
+ puts " #{hash_src == hash_dest ? '[=]'.green : '[>]'.red} #{hash_type.to_s.upcase} source: #{hash_src.to_s.send(hash_src == hash_dest ? :green : :red)}"
203
+ puts " #{hash_src == hash_dest ? '[=]'.green : '[<]'.red} #{hash_type.to_s.upcase} dest: #{hash_dest.to_s.send(hash_src == hash_dest ? :green : :red)}"
204
+ end
205
+
182
206
  def self.short_targets(ff)
183
207
  self.list [ff.targets[:readonly].map { |s| "<#{s.to_s.delete('@')}>"}.join(', ')], :red, '-'
184
208
  self.list [ff.targets[:writable].map { |s| "<#{s.to_s.delete('@')}>"}.join(', ')], :green, '+'
@@ -208,6 +232,12 @@ module FilterRename
208
232
  end
209
233
  end
210
234
 
235
+ class IndexOutOfRange < StandardError
236
+ def initialize(values)
237
+ super "Invalid index '#{values[0]}' out of the range 1..#{values[1]}, -#{values[1]}..-1"
238
+ end
239
+ end
240
+
211
241
  class UnknownHashCode < StandardError
212
242
  def initialize(hash_type)
213
243
  super "Invalid hash type: #{hash_type}"
@@ -1,3 +1,3 @@
1
1
  module FilterRename
2
- VERSION = "1.0.0"
2
+ VERSION = '1.1.0'
3
3
  end
data/lib/filter_rename.rb CHANGED
@@ -48,8 +48,11 @@ module FilterRename
48
48
  if old_data[:full_filename]
49
49
  Messages.renamed!(old_data, fp.dest)
50
50
  Messages.changed_tags(fp, old_data, false) if fp.source.class.has_writable_tags
51
- else
51
+ elsif fp.source.class.has_writable_tags
52
52
  Messages.changed_tags(fp, old_data)
53
+ else
54
+ Messages.file_exists(fp)
55
+ Messages.file_hash(fp, @cfg.global.hash_type) if @cfg.global.hash_if_exists
53
56
  end
54
57
 
55
58
  else
@@ -77,7 +80,7 @@ module FilterRename
77
80
  raise MissingFiles if @files.empty?
78
81
 
79
82
  @filters.expand_macros!(@cfg.macro)
80
- cache = []
83
+ cache = {}
81
84
 
82
85
  Messages.label "Dry Run:"
83
86
  @files.each do |src|
@@ -86,16 +89,17 @@ module FilterRename
86
89
 
87
90
  if fp.unchanged?
88
91
  Messages.skipping(fp)
89
- elsif (cache.include?(fp.dest.full_filename) || fp.dest.exists?)
92
+ elsif (cache.keys.include?(fp.dest.full_filename) || fp.dest.exists?)
90
93
  if fp.source.full_filename == fp.dest.full_filename
91
94
  Messages.changed_tags(fp)
92
95
  else
93
96
  Messages.file_exists(fp)
97
+ Messages.file_hash(fp, @cfg.global.hash_type, cache[fp.dest.full_filename]) if @cfg.global.hash_if_exists
94
98
  end
95
99
  else
96
100
  Messages.renamed(fp)
97
101
  Messages.changed_tags(fp, {}, false) if fp.source.class.has_writable_tags
98
- cache << fp.dest.full_filename
102
+ cache[fp.dest.full_filename] = fp.dest
99
103
  end
100
104
  end
101
105
  end
@@ -131,7 +135,6 @@ module FilterRename
131
135
  end
132
136
  end
133
137
  end
134
-
135
138
  end
136
139
 
137
140
  end
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  :global:
3
- :hash_type: :none
3
+ :hash_type: :md5
4
+ :hash_on_tags: false
5
+ :hash_if_exists: true
4
6
  :date_format: '%Y-%m-%d'
5
7
  :counter_length: 3
6
8
  :counter_start: 0
@@ -10,6 +12,8 @@
10
12
  :filter:
11
13
  :lang: :en
12
14
  :word_separator: " "
15
+ :number_separator: '.'
16
+ :occurrence_separator: '-'
13
17
  :target: :name
14
18
  :ignore_case: true
15
19
  :grep: '.*'
@@ -113,7 +117,7 @@
113
117
  - '\1 \2'
114
118
  append_issue:
115
119
  :move_to:
116
- - '(Issue|Volume) ([0-9]+)'
120
+ - 'Volume +([0-9]+)|Issue +([0-9]+)'
117
121
  - 'tmp'
118
122
  :template:
119
123
  - '<name> #<tmp>'
@@ -144,7 +148,7 @@
144
148
  - ignore_case:false
145
149
  -
146
150
  :replace:
147
- - "([A-Z])([a-z!']+)"
151
+ - "(?<![A-Z])([A-Z])([a-z!',-]+)"
148
152
  - " \\1\\2 "
149
153
  -
150
154
  :replace:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filter_rename
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabio Mucciante
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-16 00:00:00.000000000 Z
11
+ date: 2022-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,8 +136,9 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- description: Rename filenames applying a cascade of commands called filters in specified
140
- portion of it called targets.
139
+ description: FilterRename is a bulk renaming tool, based on the concept of filters
140
+ as small operations to perform over sections of the full filename logically represented
141
+ in targets.
141
142
  email:
142
143
  - fabio.mucciante@gmail.com
143
144
  executables:
@@ -145,6 +146,7 @@ executables:
145
146
  extensions: []
146
147
  extra_rdoc_files: []
147
148
  files:
149
+ - ".github/FUNDING.yml"
148
150
  - ".gitignore"
149
151
  - ".rspec"
150
152
  - ".travis.yml"
@@ -176,8 +178,9 @@ licenses:
176
178
  - GPL-3.0
177
179
  metadata:
178
180
  bug_tracker_uri: https://github.com/fabiomux/filter_rename/issues
179
- documentation_uri: https://www.rubydoc.info/gems/filter_rename/1.0.0
180
- homepage_uri: https://github.com/fabiomux/filter_rename
181
+ changelog_uri: https://freeaptitude.altervista.org/projects/filter-rename.htm#changelog
182
+ documentation_uri: https://www.rubydoc.info/gems/filter_rename/1.1.0
183
+ homepage_uri: https://freeaptitude.altervista.org/projects/filter-rename.html
181
184
  source_code_uri: https://github.com/fabiomux/filter_rename
182
185
  wiki_uri: https://github.com/fabiomux/filter_rename/wiki
183
186
  post_install_message:
@@ -195,8 +198,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
198
  - !ruby/object:Gem::Version
196
199
  version: '0'
197
200
  requirements: []
198
- rubygems_version: 3.0.8
201
+ rubygems_version: 3.2.19
199
202
  signing_key:
200
203
  specification_version: 4
201
- summary: Rename filenames through a chain of filter.
204
+ summary: File renaming tool which make use of a chain of actions called filters.
202
205
  test_files: []