modname 0.2.4 → 1.0.1

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: ba0fa2d1f63b3777585a4202c0560d15b47c0c345b5c614015baed843d78f099
4
- data.tar.gz: eb9bbbc814a25107ff88e4dae35ba29a1faebf9701c2af3d70a07a350d2e5eb1
3
+ metadata.gz: '08c89eeb3f25ef386045b8ee7ac0f52695b24b7491362f18ad0e71dec728a35c'
4
+ data.tar.gz: 442c1038cd4ea91b61e82612468506789fab3d5d9702d97135ec45673be99138
5
5
  SHA512:
6
- metadata.gz: 909a24c120475377a6b323d56b4cf0ab1cb1a8b0d6b41a81eb1b241c8a13cbcf76bbbdaf028101456216d674dc2417e61a478113055838a6438cb646927f4ac1
7
- data.tar.gz: b6686c14fad14cbd7025d39ef985e8ed6dfcb06f0bed43079ca0a01c53724498a4c124a39a592ea0f4906c2ab581779383bf0099c3184142c0d2fb3f4403294e
6
+ metadata.gz: 3e0e66eeb5c5c7bdde75c518f57ec348cbf686af0514e764fd2aeb508d157984b1faf739f3dfb5db55aa72905063c0602ae83557e6dbc35a929dce29bc2524a7
7
+ data.tar.gz: bee28f157f2963a03495a8b44271dc389d4b5923e5ada5be317bd78778e299cd8b66717425d7dbb5623280362bee385d81e3dd018567dfb615b514dc34ec2e96
@@ -11,6 +11,7 @@ Modname::HELP_BANNER = <<~HELP.freeze
11
11
  -e #{'|'.cyan} change file extensions
12
12
  -f #{'|'.cyan} force run; don't pre-check
13
13
  -r #{'|'.cyan} run modname recursively
14
+ --dirs #{'|'.cyan} also rename directory names
14
15
  -h #{'|'.cyan} show more help, examples
15
16
  HELP
16
17
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  # any module including modder should implement:
7
7
  # @transfer => hash of file transfers to occur
8
- # @options => hash with :recurse and :force
8
+ # @options => hash with :recurse, :force, and :dirs
9
9
 
10
10
  # extensions
11
11
  module Modder
@@ -13,10 +13,13 @@ module Modder
13
13
  def regex(args = [])
14
14
  match, trans = Modder.parse args
15
15
 
16
- Modder.files(@options[:recurse]).each do |file|
17
- new = file.sub Regexp.new(match), trans
16
+ Modder.files(@options[:recurse], dirs: @options[:dirs]).each do |file|
17
+ new = Modder.rename_base(file) do |base|
18
+ result = base.sub Regexp.new(match), trans
19
+ result.empty? ? base : result # no changes
20
+ end
18
21
 
19
- next if new == file || new == '' # no changes
22
+ next if new.nil?
20
23
 
21
24
  @transfer[file] = new
22
25
  end
@@ -35,10 +38,10 @@ module Modder
35
38
  undercase_ext match
36
39
 
37
40
  else # move match extension to targeted
38
- Modder.files(@options[:recurse]).each do |file|
39
- new = file.sub(/#{match}$/, trans)
41
+ Modder.files(@options[:recurse], dirs: @options[:dirs]).each do |file|
42
+ new = Modder.rename_base(file) { |base| base.sub(/#{match}$/, trans) }
40
43
 
41
- next if new == file # no changes
44
+ next if new.nil?
42
45
 
43
46
  @transfer[file] = new
44
47
  end
@@ -49,7 +52,7 @@ module Modder
49
52
 
50
53
  # top level wrapper for exts
51
54
  def undercase_ext(ext = '')
52
- transfer = Modder.undercase_ext_get ext, @options[:recurse]
55
+ transfer = Modder.undercase_ext_get ext, @options[:recurse], dirs: @options[:dirs]
53
56
  Modder.undercase_ext_set ext, transfer, @options[:force]
54
57
  end
55
58
  end
@@ -71,15 +74,52 @@ class << Modder
71
74
  $stdin.gets.chomp.downcase[0] == 'y'
72
75
  end
73
76
 
74
- # return a list of files to examine
75
- def files(recurse)
76
- if recurse
77
- Dir['**/*'].select { |f| File.file?(f) }
78
- else
79
- Dir.entries(Dir.pwd).select { |f| File.file? f }
77
+ # return a list of files (and, optionally, directories) to examine.
78
+ # files always come first; directories are sorted deepest-first so a
79
+ # parent is only renamed after everything nested inside it.
80
+ # dotfiles/dotdirs (.git, etc.) are always excluded, matching Dir['**/*']'s
81
+ # default behavior in the recursive case
82
+ def files(recurse, dirs: false)
83
+ paths = recurse ? Dir['**/*'] : Dir.entries(Dir.pwd).reject { |f| f.start_with?('.') }
84
+ found, folders = Modder.classify(paths)
85
+
86
+ return found unless dirs
87
+
88
+ found + folders.sort_by { |f| -f.count('/') }
89
+ end
90
+
91
+ # split paths into [files, directories]; a single stat covers both checks
92
+ # per path, skipping entries that vanished mid-scan or are broken symlinks
93
+ def classify(paths)
94
+ paths.each_with_object([[], []]) do |f, (fs, ds)|
95
+ stat = Modder.safe_stat(f)
96
+ next unless stat
97
+
98
+ fs << f if stat.file?
99
+ ds << f if stat.directory?
80
100
  end
81
101
  end
82
102
 
103
+ # stat a path, returning nil (instead of raising) for entries that
104
+ # disappeared mid-scan or are broken symlinks
105
+ def safe_stat(file)
106
+ File.stat(file)
107
+ rescue Errno::ENOENT
108
+ nil
109
+ end
110
+
111
+ # apply a transformation to a file's basename only, preserving its directory.
112
+ # the block receives the basename and returns a new basename; returning it
113
+ # unchanged means no rename should happen, in which case nil is returned
114
+ def rename_base(file)
115
+ dir, base = File.split(file)
116
+ new_base = yield base
117
+
118
+ return nil if new_base == base
119
+
120
+ dir == '.' ? new_base : File.join(dir, new_base)
121
+ end
122
+
83
123
  # show the status of current files
84
124
  def status(transfer)
85
125
  if transfer.empty?
@@ -90,7 +130,9 @@ class << Modder
90
130
  end
91
131
  end
92
132
 
93
- # rename all files
133
+ # rename all files. must run in insertion order: when directories are
134
+ # included, Modder.files places them deepest-first so a parent is never
135
+ # renamed before what's nested inside it
94
136
  def execute(transfer, force: false)
95
137
  transfer.each { |o, n| Modder.rename o, n, force }
96
138
  end
@@ -123,19 +165,40 @@ class << Modder
123
165
  end
124
166
 
125
167
  # get all extensions to change
126
- def undercase_ext_get(ext, recurse)
168
+ def undercase_ext_get(ext, recurse, dirs: false)
127
169
  transfer = {}
128
170
  allexts = ext.empty?
129
- Modder.files(recurse).each do |file|
130
- ext = file.split('.').last if allexts
171
+ Modder.files(recurse, dirs: dirs).each do |file|
172
+ new = Modder.rename_base(file) do |base|
173
+ cur_ext = allexts ? base.split('.').last : ext
174
+ next base if cur_ext == base # no extension to change
175
+
176
+ base.sub(/#{cur_ext}$/i, cur_ext.downcase)
177
+ end
131
178
 
132
- new = file.sub(/#{ext}$/i, ext.downcase)
133
- next if new == file || ext == file # no changes or extension
179
+ next if new.nil?
134
180
 
135
181
  transfer[file] = new
136
182
  end
137
183
 
138
- transfer
184
+ Modder.resolve_dir_renames(transfer)
185
+ end
186
+
187
+ # rewrite each target path so it reflects any ancestor directory renames
188
+ # also present in this transfer. undercase_ext_set flattens every entry to
189
+ # a bare temp name before restoring it, which loses the "renaming a
190
+ # directory carries its contents along" guarantee that Modder.execute
191
+ # otherwise gets for free from File.rename — so a nested file's precomputed
192
+ # target must have its renamed ancestor directory's NEW name spliced in
193
+ def resolve_dir_renames(transfer)
194
+ dir_renames = transfer.select { |k, _| File.directory?(k) }
195
+ return transfer if dir_renames.empty?
196
+
197
+ transfer.transform_values do |target|
198
+ dir_renames.reduce(target) do |path, (old_dir, new_dir)|
199
+ path.start_with?("#{old_dir}/") ? path.sub("#{old_dir}/", "#{new_dir}/") : path
200
+ end
201
+ end
139
202
  end
140
203
 
141
204
  # set all extensions to change
@@ -153,7 +216,8 @@ class << Modder
153
216
  final = {}
154
217
  temp = {}
155
218
 
156
- # create hash temp map
219
+ # create hash temp map; iterating `transfer` preserves its deepest-first
220
+ # directory ordering, which `temp` needs to safely rename originals away
157
221
  transfer.each do |k, v|
158
222
  tempfile = (v.hash * v.object_id).abs.to_s
159
223
  final[tempfile] = v
@@ -161,7 +225,10 @@ class << Modder
161
225
  end
162
226
 
163
227
  Modder.execute temp
164
- Modder.execute final
228
+ # `transfer`'s order is files-then-dirs-deepest-first (see Modder.files);
229
+ # reversing it gives dirs-shallowest-first-then-files, which is exactly
230
+ # what's needed to restore ancestors before anything nested inside them
231
+ Modder.execute(final.to_a.reverse)
165
232
  puts 'Modifications complete.'
166
233
  else
167
234
  puts 'No modifications done.'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modname
4
- VERSION = '0.2.4'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/modname.rb CHANGED
@@ -3,9 +3,9 @@
3
3
  # parse modname's command line args
4
4
 
5
5
  require 'colored'
6
- require 'modname/banner'
7
- require 'modname/modder'
8
- require 'modname/version'
6
+ require_relative 'modname/banner'
7
+ require_relative 'modname/modder'
8
+ require_relative 'modname/version'
9
9
 
10
10
  # Modname is a versatile file naming tool for renaming groups of files
11
11
  module Modname
@@ -13,10 +13,7 @@ module Modname
13
13
  class << self
14
14
  def run(args) = Driver.new.run(args)
15
15
  end
16
- end
17
16
 
18
- # Modname module containing the CLI driver
19
- module Modname
20
17
  # Driver class handles command-line argument parsing and execution
21
18
  class Driver
22
19
  include Modder
@@ -24,7 +21,7 @@ module Modname
24
21
  attr_reader :options
25
22
 
26
23
  def initialize
27
- @options = { force: false, recurse: false }
24
+ @options = { force: false, recurse: false, dirs: false }
28
25
  @transfer = {}
29
26
  end
30
27
 
@@ -52,24 +49,27 @@ module Modname
52
49
  end
53
50
  end
54
51
 
52
+ # boolean options toggled by a single flag with no argument
53
+ FLAGS = { '-f' => :force, '-r' => :recurse, '--dirs' => :dirs }.freeze
54
+
55
+ # flags that select which command to run
56
+ COMMANDS = {
57
+ '-e' => 'ext', '--ext' => 'ext',
58
+ '-h' => 'help', '--help' => 'help',
59
+ '-v' => 'version', '--version' => 'version'
60
+ }.freeze
61
+
55
62
  # parse out arguments
56
63
  def parse(args)
57
64
  opts = { cmd: 'file', args: [] }
58
65
 
59
66
  args.each do |opt|
60
- case opt
61
- when '-f'
62
- @options[:force] = true
63
- when '-r'
64
- @options[:recurse] = true
65
- when '-e', '--ext'
66
- opts[:cmd] = 'ext'
67
- when '-h', '--help'
68
- opts[:cmd] = 'help'
69
- when '-v', '--version'
70
- opts[:cmd] = 'version'
71
- else # command argument
72
- opts[:args] << opt
67
+ if (flag = FLAGS[opt])
68
+ @options[flag] = true
69
+ elsif (cmd = COMMANDS[opt])
70
+ opts[:cmd] = cmd
71
+ else
72
+ opts[:args] << opt # command argument
73
73
  end
74
74
  end
75
75
 
data/readme.md CHANGED
@@ -28,6 +28,7 @@ modname | rename files, fast
28
28
  -e | change file extensions
29
29
  -f | force run; don't pre-check
30
30
  -r | run modname recursively
31
+ --dirs | also rename directory names
31
32
  -h | show more help, examples
32
33
 
33
34
  commands
@@ -70,16 +71,14 @@ try running `gem install bundler`. Once that is done:
70
71
  Now, we should be able to build the gem locally. This will build the local
71
72
  modname gem and link it in your path, so you can playing around with `modname`.
72
73
 
73
- rake build
74
+ just build
74
75
 
75
- This uses `rspec` and `rake` to run a suite of unit tests. To run the suite:
76
+ This uses `rspec` and `just` to run a suite of unit tests. To run the suite:
76
77
 
77
- rake
78
+ just spec
78
79
 
79
80
 
80
81
  ## todos
81
82
 
82
- - only try to rename filenames, not folders on recursive calls
83
83
  - figure out whats up with the extension globbing
84
- - apply optionally to directories as well
85
84
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modname
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Warner
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.2'
26
- - !ruby/object:Gem::Dependency
27
- name: rake
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: rspec
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -103,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
89
  requirements:
104
90
  - - ">="
105
91
  - !ruby/object:Gem::Version
106
- version: '0'
92
+ version: '3.0'
107
93
  required_rubygems_version: !ruby/object:Gem::Requirement
108
94
  requirements:
109
95
  - - ">="
110
96
  - !ruby/object:Gem::Version
111
97
  version: '0'
112
98
  requirements: []
113
- rubygems_version: 3.7.2
99
+ rubygems_version: 4.0.10
114
100
  specification_version: 4
115
101
  summary: a versatile file naming tool.
116
102
  test_files: []