filter_rename 1.0.0 → 1.1.0

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: 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: []