├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── mvmv ├── lib ├── mvmv.rb └── mvmv │ ├── command.rb │ └── version.rb ├── mvmv.gemspec └── test ├── bin_helper.rb └── test_mvmv.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mvmv.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Junegunn Choi 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mvmv 2 | 3 | Simple batch renaming script. 4 | 5 | ## Installation 6 | 7 | $ gem install mvmv 8 | 9 | ## Usage 10 | 11 | ``` 12 | usage: mvmv [OPTIONS] [] 13 | 14 | commands: 15 | mvmv prefix 16 | mvmv suffix 17 | mvmv name 18 | mvmv name-suffix 19 | mvmv ext <.extension> 20 | mvmv upper 21 | mvmv lower 22 | 23 | mvmv regexp 24 | mvmv regexpi 25 | mvmv name-regexp 26 | mvmv name-regexpi 27 | 28 | options: 29 | -f, --force Force rename 30 | --no-color Disable ANSI color codes 31 | ``` 32 | 33 | ## Examples 34 | 35 | ### Adding simple prefix and suffix to files 36 | 37 | ``` 38 | mvmv prefix old_ *.txt 39 | mvmv suffix .bak *.txt 40 | ``` 41 | 42 | ### Numbering files 43 | 44 | You can attach sequence numbers to files with a series of `#`s. 45 | Depending on the number of `#`s, numbers will be padded with zeros. 46 | 47 | ``` 48 | mvmv name Photo#### *.jpg *.gif *.png 49 | mvmv name-suffix -## *.jpg 50 | ``` 51 | 52 | ### Advanced renaming with regular expressions 53 | 54 | `regexp` command performs regular expression substitutions. (`regexpi` is the case-insensitive version.) 55 | `name-regexp` command performs regular expression subsitution only on the name parts of the given files. 56 | 57 | ``` 58 | mvmv name-regexp '^(.*)_-_(.*)$' '\2 - \1' *.mp3 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require 'rake/testtask' 4 | Rake::TestTask.new(:test) do |test| 5 | test.libs << 'lib' << 'test' 6 | test.pattern = 'test/**/test_*.rb' 7 | test.verbose = true 8 | end 9 | -------------------------------------------------------------------------------- /bin/mvmv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'mvmv' 4 | 5 | options = {} 6 | usage = " 7 | usage: mvmv [OPTIONS] [] 8 | 9 | commands: 10 | mvmv prefix 11 | mvmv suffix 12 | mvmv name 13 | mvmv name-suffix 14 | mvmv ext <.extension> 15 | mvmv upper 16 | mvmv lower 17 | 18 | mvmv regexp 19 | mvmv regexpi 20 | mvmv name-regexp 21 | mvmv name-regexpi 22 | 23 | options: 24 | -f, --force Force rename 25 | --no-color Disable ANSI color codes 26 | ".strip 27 | 28 | options = {} 29 | while ARGV.first =~ /^-/ 30 | case ARGV.shift 31 | when '-f', '--force' 32 | options[:force] = true 33 | when '--no-color' 34 | options[:color] = false 35 | end 36 | end 37 | 38 | if ARGV.length < 2 39 | puts usage 40 | exit 1 41 | end 42 | 43 | command = ARGV[0].to_s.gsub('-', '_').to_sym 44 | args = ARGV[1..-1] 45 | mvmv = Mvmv.new options.fetch(:color, true) 46 | begin 47 | if options[:force] 48 | mvmv.rename! command, *args 49 | else 50 | mvmv.rename command, *args 51 | end 52 | rescue ArgumentError => e 53 | puts usage 54 | exit 1 55 | rescue Exception => e 56 | exit 1 57 | end 58 | -------------------------------------------------------------------------------- /lib/mvmv.rb: -------------------------------------------------------------------------------- 1 | require 'mvmv/version' 2 | require 'mvmv/command' 3 | require 'ansi' 4 | 5 | class Mvmv 6 | def initialize color = true, outstream = $stdout, instream = $stdin 7 | @color = color 8 | @outstream = outstream 9 | @instream = instream 10 | end 11 | 12 | def convert_filenames symb, *args 13 | unless Mvmv::Command.respond_to?(symb) 14 | error "Invalid command: #{symb}" 15 | end 16 | 17 | arg_arity = Mvmv::Command.method(symb).arity - 1 18 | if arg_arity >= args.length 19 | error "Invalid number of parameters for #{symb.to_s.gsub('_', '-')}", ArgumentError 20 | end 21 | 22 | files = args[arg_arity..-1] 23 | dir_files = files.map { |f| [f.include?('/') ? File.dirname(f) : nil, File.basename(f)] } 24 | files.zip( 25 | dir_files.map(&:first).zip( 26 | Mvmv::Command.send(symb, *(args[0, arg_arity] + [dir_files.map(&:last)])) 27 | ).map { |pair| pair.compact.join('/') } 28 | ) 29 | end 30 | 31 | def rename symb, *args 32 | rename_impl false, symb, *args 33 | end 34 | 35 | def rename! symb, *args 36 | rename_impl true, symb, *args 37 | end 38 | 39 | private 40 | def rename_impl force, symb, *args 41 | pairs = convert_filenames symb, *args 42 | 43 | flen = pairs.map(&:first).map(&:length).max 44 | tlen = pairs.map(&:last).map(&:length).max 45 | 46 | pairs.each do |pair| 47 | from, to = pair 48 | next if from == to || to.empty? 49 | skip = false 50 | while true 51 | log [ 52 | ansi('[', :bold), 53 | from.ljust(flen), 54 | ansi('=>', :bold), 55 | ansi(to.ljust(tlen), :blue, :bold), 56 | ansi(']', :bold), 57 | force ? nil : ansi('(', :yellow) + 58 | ansi('y', :yellow, :bold) + 59 | ansi('/', :yellow) + 60 | 'n' + 61 | ansi(')? ', :yellow) 62 | ].compact.join(' ') 63 | 64 | unless force 65 | case @instream.gets 66 | when nil 67 | puts 68 | next 69 | when /^n/i 70 | skip = true 71 | break 72 | when /^[^y\n]/i 73 | next 74 | end 75 | end 76 | 77 | unless File.exists?(from) 78 | error " File not found: #{from}" 79 | break 80 | end 81 | 82 | if !force && File.exists?(to) 83 | skip = true 84 | while true 85 | log ansi(" #{to} already exists. Overwrite " + 86 | ansi('(', :yellow) + 87 | 'y' + 88 | ansi('/', :yellow) + 89 | ansi('n', :yellow, :bold) + 90 | ansi(')? ', :yellow) 91 | ) 92 | 93 | unless 94 | case $stdin.gets 95 | when nil 96 | puts 97 | next 98 | when /^[n\n]/i 99 | break 100 | when /^y/i 101 | skip = false 102 | break 103 | else 104 | next 105 | end 106 | end 107 | end 108 | end 109 | 110 | unless skip 111 | begin 112 | File.rename from, to 113 | logln " Renamed", :green 114 | rescue Exception => e 115 | error " Failed to rename #{from}" 116 | end 117 | end 118 | break 119 | end 120 | end 121 | end 122 | 123 | def ansi msg, *colors 124 | (@color && !colors.empty?) ? ANSI::Code.ansi(*colors) { msg } : msg 125 | end 126 | 127 | def log msg, *colors 128 | @outstream.print ansi(msg, *colors) 129 | end 130 | 131 | def logln msg, *colors 132 | @outstream.puts ansi(msg, *colors) 133 | end 134 | 135 | def error message, x = Exception 136 | logln message, :red, :bold 137 | raise x.new(message) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/mvmv/command.rb: -------------------------------------------------------------------------------- 1 | class Mvmv 2 | module Command 3 | class Sequencer 4 | def initialize n = 1 5 | @n = n - 1 6 | end 7 | 8 | def convert str 9 | @n += 1 10 | str.gsub(/#+/) { |x| @n.to_s.rjust(x.length, '0') } 11 | end 12 | end 13 | 14 | class << self 15 | def name n, files 16 | seq = Sequencer.new 17 | files.map { |file| 18 | seq.convert(n) + File.extname(file) 19 | } 20 | end 21 | 22 | def prefix p, files 23 | seq = Sequencer.new 24 | files.map { |file| seq.convert(p) + file } 25 | end 26 | 27 | def suffix s, files 28 | seq = Sequencer.new 29 | files.map { |file| file + seq.convert(s) } 30 | end 31 | 32 | def name_suffix s, files 33 | seq = Sequencer.new 34 | files.map { |file| 35 | ext = File.extname(file) 36 | file.chomp(ext) + seq.convert(s) + ext 37 | } 38 | end 39 | 40 | def ext x, files 41 | files.map { |file| 42 | ext = File.extname(file) 43 | file.chomp(ext) + x 44 | } 45 | end 46 | 47 | def upper files 48 | files.map(&:upcase) 49 | end 50 | 51 | def lower files 52 | files.map(&:downcase) 53 | end 54 | 55 | def regexpi f, t, files 56 | seq = Sequencer.new 57 | files.map { |file| 58 | seq.convert file.gsub(Regexp.compile(f, Regexp::IGNORECASE), t) 59 | } 60 | end 61 | 62 | def regexp f, t, files 63 | seq = Sequencer.new 64 | files.map { |file| 65 | seq.convert file.gsub(Regexp.compile(f), t) 66 | } 67 | end 68 | 69 | def name_regexpi f, t, files 70 | regexpi(f, t, files.map { |file| 71 | file.chomp(File.extname file) 72 | }).zip(files.map { |file| File.extname file }).map(&:join) 73 | end 74 | 75 | def name_regexp f, t, files 76 | regexp(f, t, files.map { |file| 77 | file.chomp(File.extname file) 78 | }).zip(files.map { |file| File.extname file }).map(&:join) 79 | end 80 | end 81 | end#Command 82 | end 83 | 84 | -------------------------------------------------------------------------------- /lib/mvmv/version.rb: -------------------------------------------------------------------------------- 1 | class Mvmv 2 | VERSION = "0.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /mvmv.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/mvmv/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Junegunn Choi"] 6 | gem.email = ["junegunn.c@gmail.com"] 7 | gem.description = %q{Simple batch renaming} 8 | gem.summary = %q{Simple batch renaming} 9 | gem.homepage = "https://github.com/junegunn/mvmv" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "mvmv" 15 | gem.require_paths = ["lib"] 16 | gem.version = Mvmv::VERSION 17 | gem.add_runtime_dependency 'ansi', '~> 1.4.2' 18 | gem.add_development_dependency 'test-unit' 19 | end 20 | -------------------------------------------------------------------------------- /test/bin_helper.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | # DISCLAIMER: 5 | # Not a real test! 6 | # Just a helper script for running scripts with local source 7 | 8 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib') 9 | load File.join(File.dirname(__FILE__), '../bin/', File.basename(ARGV.shift)) 10 | -------------------------------------------------------------------------------- /test/test_mvmv.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require 'mvmv' 5 | 6 | class TestMvmv < Test::Unit::TestCase 7 | def test_name_conversion 8 | m = Mvmv.new 9 | input = %w[aaa.jpg bbb.GIF ccc.jpg.png] 10 | assert_equal input.zip(%w[photo001.jpg photo002.GIF photo003.png]), m.convert_filenames(:name, 'photo###', *input) 11 | assert_equal input.zip(%w[old_aaa.jpg old_bbb.GIF old_ccc.jpg.png]), m.convert_filenames(:prefix, 'old_', *input) 12 | assert_equal input.zip(%w[001-aaa.jpg 002-bbb.GIF 003-ccc.jpg.png]), m.convert_filenames(:prefix, '###-', *input) 13 | assert_equal input.zip(%w[aaa.jpg.0001 bbb.GIF.0002 ccc.jpg.png.0003]), m.convert_filenames(:suffix, '.####', *input) 14 | assert_equal input.zip(%w[aaa-0001.jpg bbb-0002.GIF ccc.jpg-0003.png]), m.convert_filenames(:name_suffix, '-####', *input) 15 | assert_equal input.zip(%w[aaa.jpeg bbb.jpeg ccc.jpg.jpeg]), m.convert_filenames(:ext, '.jpeg', *input) 16 | assert_equal input.zip(%w[AAA.JPG BBB.GIF CCC.JPG.PNG]), m.convert_filenames(:upper, *input) 17 | assert_equal input.zip(%w[aaa.jpg bbb.gif ccc.jpg.png]), m.convert_filenames(:lower, *input) 18 | 19 | input = %w[abc.jpg xyz.GIF ijk.jpg.png] 20 | assert_equal input.zip(%w[cba01.jpg zyx02.GIF kji03.jpg.png]), m.convert_filenames(:regexp, '^([a-z])([a-z])([a-z])', '\3\2\1##', *input) 21 | assert_equal input.zip(input), m.convert_filenames(:regexp, '^([A-Z])([A-Z])([A-Z])', '\3\2\1##', *input) 22 | assert_equal input.zip(%w[cba01.jpg zyx02.GIF kji03.jpg.png]), m.convert_filenames(:regexpi, '^([A-Z])([a-z])([A-Z])', '\3\2\1##', *input) 23 | 24 | assert_equal input.zip(%w[cba01.gpj01 zyx02.GIF kji03.gpj03.gnp03]), m.convert_filenames(:regexp, '([a-z])([a-z])([a-z])', '\3\2\1##', *input) 25 | 26 | input = %w[Abc.jpg xyz.GIF ijk.jpg.png] 27 | assert_equal input.zip(%w[Abc.jpg zyx02.GIF kji03.gpj03.png]), m.convert_filenames(:name_regexp, '([a-z])([a-z])([a-z])', '\3\2\1##', *input) 28 | assert_equal input.zip(%w[cbA01.jpg zyx02.GIF kji03.gpj03.png]), m.convert_filenames(:name_regexpi, '([a-z])([a-z])([a-z])', '\3\2\1##', *input) 29 | end 30 | 31 | def test_name_conversion_directory 32 | m = Mvmv.new 33 | input = %w[aaa.jpg bbb.GIF ccc.jpg.png].map { |e| "x/y/z/#{e}" } 34 | assert_equal input.zip( 35 | %w[001-aaa.jpg 002-bbb.GIF 003-ccc.jpg.png].map { |e| "x/y/z/#{e}" } 36 | ), m.convert_filenames(:prefix, '###-', *input) 37 | end 38 | 39 | def test_rename! 40 | m = Mvmv.new 41 | 42 | input = %w[aaa.jpg bbb.gif ccc.jpg.png] 43 | pairs = m.convert_filenames :ext, '.jpeg', *input 44 | 45 | pairs.each do |pair| 46 | system "rm -v #{pair.join ' '}" 47 | end 48 | 49 | input.each do |file| 50 | system "touch #{file}" 51 | end 52 | system "touch #{pairs.last.last}" 53 | 54 | pairs.each do |pair| 55 | assert File.exist?(pair.first), 'File should exist' 56 | assert !File.exist?(pair.last), 'File should not exist' unless pair.last == pairs.last.last 57 | end 58 | 59 | m.rename! :ext, '.jpeg', *input 60 | 61 | pairs.each do |pair| 62 | assert !File.exist?(pair.first), 'File should not exist' 63 | assert File.exist?(pair.last), 'File should exist' 64 | end 65 | 66 | pairs.each do |pair| 67 | system "rm -v #{pair.join ' '}" 68 | end 69 | end 70 | 71 | def test_rename_dir 72 | m = Mvmv.new 73 | 74 | input = %w[aaa bbb ccc] 75 | pairs = m.convert_filenames :name, '##', *input 76 | pairs.each do |pair| 77 | system "rm -rfv #{pair.join ' '}" 78 | system "mkdir -pv #{pair.first}" 79 | end 80 | 81 | m.rename! :name, '##', *input 82 | 83 | pairs.each do |pair| 84 | assert !File.exist?(pair.first), 'Directory should not exist' 85 | assert File.exist?(pair.last), 'Directory should exist' 86 | assert File.directory?(pair.last), 'Should be a directory' 87 | end 88 | 89 | pairs.each do |pair| 90 | system "rm -rfv #{pair.join ' '}" 91 | end 92 | end 93 | end 94 | --------------------------------------------------------------------------------