AbsoluteRenamer 0.9.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.
- data/AbsoluteRenamer.gemspec +40 -0
- data/COPYING +340 -0
- data/ChangeLog +0 -0
- data/LICENCE +17 -0
- data/Manifest.txt +42 -0
- data/README +34 -0
- data/Rakefile +20 -0
- data/bin/absrenamer +54 -0
- data/conf/absrenamer/absrenamer.conf +34 -0
- data/lib/absolute_renamer.rb +119 -0
- data/lib/absolute_renamer/config.rb +40 -0
- data/lib/absolute_renamer/external.rb +56 -0
- data/lib/absolute_renamer/external/modules/core/case/module.rb +31 -0
- data/lib/absolute_renamer/external/modules/core/general/module.rb +126 -0
- data/lib/absolute_renamer/external/parsers/core/general/parser.rb +74 -0
- data/lib/absolute_renamer/external/parsers/interactive/parser.rb +20 -0
- data/lib/absolute_renamer/external/parsers/listing/parser.rb +9 -0
- data/lib/absolute_renamer/external/plugins/interactive/plugin.rb +35 -0
- data/lib/absolute_renamer/external/plugins/listing/plugin.rb +14 -0
- data/lib/absolute_renamer/file_info.rb +90 -0
- data/lib/absolute_renamer/imodule.rb +68 -0
- data/lib/absolute_renamer/iparser.rb +8 -0
- data/lib/absolute_renamer/iplugin.rb +11 -0
- data/lib/absolute_renamer/libs/file.rb +11 -0
- data/lib/absolute_renamer/libs/string.rb +27 -0
- data/lib/absolute_renamer/parser.rb +91 -0
- data/lib/absolute_renamer/use_config.rb +11 -0
- data/lib/absolute_renamer/with_children.rb +22 -0
- data/man/man8/absrenamer.8 +67 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/setup.rb +1585 -0
- data/test/unit/features/test_absrenamer.conf +34 -0
- data/test/unit/tc_config.rb +28 -0
- data/test/unit/tc_file.rb +16 -0
- data/test/unit/tc_fileinfo.rb +17 -0
- data/test/unit/tc_imodule.rb +29 -0
- data/test/unit/tc_iplugin.rb +8 -0
- data/test/unit/tc_parser.rb +13 -0
- data/test/unit/tc_string.rb +24 -0
- data/test/unit/test_suite.rb +28 -0
- metadata +120 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# TODO ajouter une option pour la gestion du nombre de points avant l'extension
|
2
|
+
module AbsoluteRenamer
|
3
|
+
class GeneralParser < AbsoluteRenamer::IParser
|
4
|
+
def self.add_options(parser, options)
|
5
|
+
parser.banner << ' [file]...'
|
6
|
+
parser.on_tail('-h', '--help', 'Display this help screen') do
|
7
|
+
puts parser
|
8
|
+
exit 0
|
9
|
+
end
|
10
|
+
|
11
|
+
parser.on('-r', '--replace PATTERN,REPLACEMENT', Array,
|
12
|
+
'String replacement ex:"pattern/replacement" ') do |data|
|
13
|
+
self.add_replacement(data, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
parser.on('-e', '--regexp-replace PATTERN,REPLACEMENT', Array,
|
17
|
+
'String replacement using regexp') do |data|
|
18
|
+
self.add_replacement(data, options, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
parser.on('-f', '--format FORMAT',
|
22
|
+
'Format string used as model') do |format|
|
23
|
+
options[:format] = format
|
24
|
+
@format_given = true
|
25
|
+
end
|
26
|
+
|
27
|
+
parser.on('-x', '--ext-format FORMAT',
|
28
|
+
'Format string used as model for the extension') do |format|
|
29
|
+
options[:ext_format] = format
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.on('-R', '--recursive',
|
33
|
+
'Rename files in subdirectories recursively') do
|
34
|
+
options[:rec] = true
|
35
|
+
end
|
36
|
+
|
37
|
+
parser.on('--maxdepth N', Integer, 'Maximum recursion depth') do |depth|
|
38
|
+
options[:maxdepth] = depth
|
39
|
+
end
|
40
|
+
|
41
|
+
parser.on('-m', '--mode MODE', [:rename, :copy, :move, :link],
|
42
|
+
'Renaming mode. Can be used with --dest DEST.',
|
43
|
+
' rename: simply rename files',
|
44
|
+
' copy: make a copy of each file in DEST with its new name',
|
45
|
+
' move: move each file in DEST with its new name',
|
46
|
+
' link: create a symbolic link to each file in DEST' <<
|
47
|
+
' with its new name') do |mode|
|
48
|
+
options[:mode] = mode
|
49
|
+
end
|
50
|
+
|
51
|
+
parser.on('--dest DEST', 'Destination directory' <<
|
52
|
+
' for copy, move and link modes') do |dest|
|
53
|
+
options[:dest] = File.expand_path(dest)
|
54
|
+
end
|
55
|
+
|
56
|
+
parser.on('-d', '--directories', 'Directories handling') do
|
57
|
+
options[:dir] = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.add_replacement(data, options, regexp = false)
|
62
|
+
pattern,replace = data[0..1]
|
63
|
+
replace ||= ''
|
64
|
+
pattern = Regexp.new(pattern) if regexp
|
65
|
+
moment = @format_given.nil? ? :before : :after
|
66
|
+
options[:replacements] ||= {}
|
67
|
+
options[:replacements][moment] ||= []
|
68
|
+
options[:replacements][moment] << {:type => pattern.class,
|
69
|
+
:pattern => pattern,
|
70
|
+
:replace => replace
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AbsoluteRenamer
|
2
|
+
class InteractiveParser < AbsoluteRenamer::IParser
|
3
|
+
def self.add_options(parser, options)
|
4
|
+
parser.on('-i', 'Prompt before each renamming') do
|
5
|
+
options[:interactive] = :always
|
6
|
+
end
|
7
|
+
|
8
|
+
parser.on('-I', 'Prompt once before batch renamming') do
|
9
|
+
options[:interactive] = :once
|
10
|
+
end
|
11
|
+
|
12
|
+
parser.on('--interactive [WHEN]', [:always, :never, :once],
|
13
|
+
'Prompt according to WHEN: never, once (-I), or always (-i).',
|
14
|
+
'Without WHEN, prompt always') do |w|
|
15
|
+
w = :always if w.nil?
|
16
|
+
options[:interactive] = w
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AbsoluteRenamer
|
2
|
+
class InteractivePlugin < AbsoluteRenamer::IPlugin
|
3
|
+
def before_batch_renaming
|
4
|
+
if conf[:options][:interactive] == :once
|
5
|
+
conf[:files].each do |file|
|
6
|
+
file.display_change
|
7
|
+
end
|
8
|
+
print "Do you want to rename this files ? [y/N] "
|
9
|
+
begin
|
10
|
+
resp = STDIN.readline.chomp.downcase
|
11
|
+
rescue Exception => e
|
12
|
+
puts "\nExiting renamer"
|
13
|
+
exit(0)
|
14
|
+
end
|
15
|
+
return resp == "y"
|
16
|
+
end
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_file_renaming(params)
|
21
|
+
if conf[:options][:interactive] == :always
|
22
|
+
params[:file].display_change
|
23
|
+
print "Do you want to rename this file ? [y/N] "
|
24
|
+
begin
|
25
|
+
resp = STDIN.readline.chomp.downcase
|
26
|
+
rescue Exception => e
|
27
|
+
puts "\nExiting renamer"
|
28
|
+
exit(0)
|
29
|
+
end
|
30
|
+
return resp == "y"
|
31
|
+
end
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module AbsoluteRenamer
|
2
|
+
class ListingPlugin < AbsoluteRenamer::IPlugin
|
3
|
+
def before_batch_renaming
|
4
|
+
listing = conf[:options][:listing] || false
|
5
|
+
if listing
|
6
|
+
conf[:files].each do |file|
|
7
|
+
file.display_change
|
8
|
+
end
|
9
|
+
return false
|
10
|
+
end
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'ftools'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'absolute_renamer/use_config'
|
4
|
+
require 'absolute_renamer/libs/file'
|
5
|
+
|
6
|
+
module AbsoluteRenamer
|
7
|
+
# Class that represents each file to be renamed.
|
8
|
+
# It contains all informations about a file and, in the end,
|
9
|
+
# processes to its renaming.
|
10
|
+
class FileInfo
|
11
|
+
include AbsoluteRenamer::UseConfig
|
12
|
+
|
13
|
+
attr_accessor :name, :new_name,
|
14
|
+
:path, :real_path,
|
15
|
+
:ext, :dir, :dir_path
|
16
|
+
|
17
|
+
# Initializes a FileInfo.
|
18
|
+
# path: the relative or absolute path of the file.
|
19
|
+
def initialize(path)
|
20
|
+
@path = path
|
21
|
+
@real_path = File.expand_path(@path)
|
22
|
+
@dir_path = File.dirname(@real_path)
|
23
|
+
@dir = File.directory?(@real_path)
|
24
|
+
@name = File.basename(@real_path)
|
25
|
+
unless @dir
|
26
|
+
# TODO utiliser une conf :dot
|
27
|
+
@ext = File.extname(@name)
|
28
|
+
@name.gsub!(Regexp.new('.' << @ext << '$'), '') unless @ext.empty?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a description of a FileInfo
|
33
|
+
# some_fileinfo.inspect # => "File: hello_world pdf"
|
34
|
+
def inspect
|
35
|
+
"File: #{@name} #{@ext}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Displays the action that will be done on the file.
|
39
|
+
# some_fileinfo.display_change # => "rename a_file.txt --> A_File.TXT"
|
40
|
+
def display_change
|
41
|
+
puts "#{conf[:options][:mode]} #{@real_path.sub(Dir.pwd+'/', '')} --> #{new_path.sub(Dir.pwd+'/', '')}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the new path of the file.
|
45
|
+
def new_path
|
46
|
+
if conf[:options][:dest].nil? or conf[:options][:dest].empty?
|
47
|
+
File.join(@dir_path, @new_name)
|
48
|
+
else
|
49
|
+
File.join(conf[:options][:dest], @new_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Renames the file.
|
54
|
+
def rename
|
55
|
+
display_change
|
56
|
+
File.rename(@real_path, new_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Copies the file.
|
60
|
+
def copy
|
61
|
+
display_change
|
62
|
+
if @dir
|
63
|
+
if @real_path != conf[:options][:dest]
|
64
|
+
FileUtils.cp_r(@real_path, conf[:options][:dest])
|
65
|
+
else
|
66
|
+
puts "#{real_path} ignored"
|
67
|
+
end
|
68
|
+
else
|
69
|
+
File.copy(@real_path, new_path, false)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Moves a file. Moving to the same directories is just like renaming.
|
74
|
+
def move
|
75
|
+
display_change
|
76
|
+
File.move(@real_path, new_path, false)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creates a symbolic link to the file.
|
80
|
+
def link
|
81
|
+
display_change
|
82
|
+
begin
|
83
|
+
File.symlink(@real_path, new_path)
|
84
|
+
rescue NotImplemented
|
85
|
+
# TODO trouver mieux
|
86
|
+
puts "Error: cannot create symlinks"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'absolute_renamer/with_children'
|
2
|
+
|
3
|
+
module AbsoluteRenamer
|
4
|
+
# Modules parent class.
|
5
|
+
# Modules must inherit of it.
|
6
|
+
class IModule < AbsoluteRenamer::WithChildren
|
7
|
+
def initialize
|
8
|
+
@filters = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the classname symbol
|
12
|
+
def self.symbol
|
13
|
+
name.intern
|
14
|
+
end
|
15
|
+
|
16
|
+
# Process a +file+ by searching for a known pattern in its name
|
17
|
+
# and replacing it by the corresponding value.
|
18
|
+
# The pattern is a regular expression obtained by concatening
|
19
|
+
# the +@filters+ variable with "|".
|
20
|
+
#
|
21
|
+
# file: a FileInfo instance
|
22
|
+
# format: the format string used to rename the file
|
23
|
+
# type: the type of the renaming format (:name or :ext)
|
24
|
+
def process(file, format, type = :name)
|
25
|
+
return format if @filters.empty?
|
26
|
+
|
27
|
+
str = format
|
28
|
+
result = []
|
29
|
+
pattern = Regexp.new(@filters.join('|'))
|
30
|
+
|
31
|
+
idx = str.index(pattern)
|
32
|
+
while idx
|
33
|
+
matched = pattern.match(str).to_a.compact
|
34
|
+
part = str.partition(matched[0])
|
35
|
+
result.push(part[0])
|
36
|
+
val = self.interpret(file, matched, type)
|
37
|
+
result.push(val)
|
38
|
+
str = part[2]
|
39
|
+
idx = str.index(pattern)
|
40
|
+
end
|
41
|
+
result.push(str) unless str.empty?
|
42
|
+
format.replace(result.join)
|
43
|
+
format
|
44
|
+
end
|
45
|
+
|
46
|
+
# Interprets a matched pattern.
|
47
|
+
# Searchs for the corresponding callback
|
48
|
+
# in the current module and call it.
|
49
|
+
#
|
50
|
+
# file: a FileInfo instance
|
51
|
+
# infos: the matched values depending of the pattern
|
52
|
+
# type: the type of the renaming format (:name or :ext)
|
53
|
+
def interpret(file, infos, type)
|
54
|
+
modifier = infos[2]
|
55
|
+
action = infos[3]
|
56
|
+
|
57
|
+
return conf[:options][:default_string] unless self.respond_to?(action.intern)
|
58
|
+
|
59
|
+
ap = self.method(action.intern)
|
60
|
+
val = ap.call(file, infos, type)
|
61
|
+
unless modifier.empty?
|
62
|
+
mp = CaseModule.method(CaseModule.actions[modifier])
|
63
|
+
val = mp.call(val)
|
64
|
+
end
|
65
|
+
val.empty? ? conf[:options][:default_string] : val
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Extension of existing File class.
|
2
|
+
class << File
|
3
|
+
# Returns the extension of a file.
|
4
|
+
# path: the path of the file
|
5
|
+
# dot: starting from the end, number of dots to count before cuting extension.
|
6
|
+
def extname(path, dot = 1)
|
7
|
+
pattern = (0...dot).inject('') { |pat,x| pat << '\.[^\.]+' } << '$'
|
8
|
+
ext = File.basename(path).match(pattern).to_s
|
9
|
+
ext.empty? ? "" : ext[1..ext.length]
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Extension of existing String class
|
2
|
+
class String
|
3
|
+
# Returns a camelized version of a string
|
4
|
+
# word_separator: a regular expression used to separate words.
|
5
|
+
# str = "hello.THE world"
|
6
|
+
# str.camelize # => "Hello.The World"
|
7
|
+
# str.camelize(/[\.]/) # => "Hello.The world"
|
8
|
+
# str # => "hello.THE world"
|
9
|
+
def camelize(word_separators = /[\W_]/)
|
10
|
+
self.clone.camelize!(word_separators)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Camelizes a string and returns it.
|
14
|
+
# str = "Hello.THE World"
|
15
|
+
# str.camelize! # => "Hello.The World"
|
16
|
+
# str.camelize!(/[\.]/) # => "Hello.The World"
|
17
|
+
# str # => "Hello.The World"
|
18
|
+
def camelize!(word_separators = /[\W_]/)
|
19
|
+
self.downcase!
|
20
|
+
self.each_char.each_with_index do |c,i|
|
21
|
+
if self[i-1].chr =~ word_separators or i.zero?
|
22
|
+
self[i] = c.upcase if c =~ /[a-z]/
|
23
|
+
end
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'absolute_renamer/iparser'
|
3
|
+
require 'absolute_renamer/use_config'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module AbsoluteRenamer
|
7
|
+
# Class in charge of the command line parsing.
|
8
|
+
class Parser
|
9
|
+
class << self
|
10
|
+
include AbsoluteRenamer::UseConfig
|
11
|
+
|
12
|
+
# Calls all registred parsers.
|
13
|
+
# The parsers are written in configuration files.
|
14
|
+
# The core parsers are automaticaly added.
|
15
|
+
def parse_cmd_line
|
16
|
+
parsers_dir = conf[:path][:parsers]
|
17
|
+
parsers = conf[:parsers]
|
18
|
+
|
19
|
+
core_parsers = ['general']
|
20
|
+
|
21
|
+
parsers += core_parsers.map! { |core_parser| File.join('core', core_parser) }
|
22
|
+
|
23
|
+
parsers.each do |parser|
|
24
|
+
parser_file = File.join(parsers_dir, parser, 'parser.rb')
|
25
|
+
begin
|
26
|
+
if require parser_file
|
27
|
+
puts "Loaded: #{parser_file}" if conf[:debug]
|
28
|
+
end
|
29
|
+
rescue LoadError => e
|
30
|
+
STDERR.puts(e)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ARGV.options do |parser|
|
35
|
+
begin
|
36
|
+
list = AbsoluteRenamer::IParser.children
|
37
|
+
list.each do |class_name|
|
38
|
+
class_name.add_options(parser, conf[:options])
|
39
|
+
end
|
40
|
+
parser.parse!
|
41
|
+
rescue RuntimeError => ex
|
42
|
+
STDERR.puts(ex)
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
conf[:files] = self.get_files(ARGV, 0) || []
|
46
|
+
conf[:options][:maxdepth] ||= 0
|
47
|
+
conf[:options][:interactive] ||= :never
|
48
|
+
conf[:options][:mode] ||= :rename
|
49
|
+
pp conf.get if conf[:debug]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a list of all files and directories to rename.
|
54
|
+
# All options that have not been matched are considered as path.
|
55
|
+
# For directories, if the recursive otpion is set to true (conf[:rec] == true),
|
56
|
+
# files are searched in sub directories.
|
57
|
+
#
|
58
|
+
# list: a list of path to explore
|
59
|
+
# depth: maximum recursion depth
|
60
|
+
#
|
61
|
+
# Returns the files/directories list
|
62
|
+
def get_files(list, depth)
|
63
|
+
files = []
|
64
|
+
options = conf[:options]
|
65
|
+
|
66
|
+
list.each do |entry|
|
67
|
+
return files unless File.exists?(entry)
|
68
|
+
is_dir = File.directory?(entry)
|
69
|
+
mod_dir = options[:dir]
|
70
|
+
depth_ok = (depth < options[:maxdepth] or options[:maxdepth].zero?)
|
71
|
+
mod_rec = (options[:rec] and depth_ok)
|
72
|
+
|
73
|
+
add_dir = (is_dir and mod_dir)
|
74
|
+
add_file = (!is_dir and !mod_dir)
|
75
|
+
add_sub = (is_dir and (mod_rec or (!mod_dir and depth < 1)))
|
76
|
+
|
77
|
+
files << FileInfo.new(entry) if (add_dir or add_file)
|
78
|
+
files += self.get_files(self.get_subentries(entry), depth + 1) if (add_sub)
|
79
|
+
end
|
80
|
+
files
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns files and directories contained in +path+.
|
84
|
+
def get_subentries(path)
|
85
|
+
files = Dir.entries(path)
|
86
|
+
files.delete_if { |file| file[0,1] == '.' }
|
87
|
+
files.collect! { |file| path + '/' + file }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|