modname 1.0.0 → 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: a75ac155dd3255d8cebd50ec9e01e629a9f64aa0b17090e482db22978baf8832
4
- data.tar.gz: 29414f4677355f27ddda53fbfd857241adf1550c28709c955da4e2165b71d1c6
3
+ metadata.gz: '08c89eeb3f25ef386045b8ee7ac0f52695b24b7491362f18ad0e71dec728a35c'
4
+ data.tar.gz: 442c1038cd4ea91b61e82612468506789fab3d5d9702d97135ec45673be99138
5
5
  SHA512:
6
- metadata.gz: 2ea30c66c31f07b5b5f928cb49c0fd8cbddd6d765042b7b3f1cdd7c08e746d0085f7dd916e32488424b3c9a594a46a9c7ccd1fe769d663b0217b49b60216e6f6
7
- data.tar.gz: 39b3a3d68f4462308713223c71b6fc7a7b307f333f452e1b1c4cf7e9509698e075c7ab2023dd6adcc25581e87dbecceb1dde56cf62b7022b29f1c5abbdb2eed8
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,7 +13,7 @@ module Modder
13
13
  def regex(args = [])
14
14
  match, trans = Modder.parse args
15
15
 
16
- Modder.files(@options[:recurse]).each do |file|
16
+ Modder.files(@options[:recurse], dirs: @options[:dirs]).each do |file|
17
17
  new = Modder.rename_base(file) do |base|
18
18
  result = base.sub Regexp.new(match), trans
19
19
  result.empty? ? base : result # no changes
@@ -38,7 +38,7 @@ module Modder
38
38
  undercase_ext match
39
39
 
40
40
  else # move match extension to targeted
41
- Modder.files(@options[:recurse]).each do |file|
41
+ Modder.files(@options[:recurse], dirs: @options[:dirs]).each do |file|
42
42
  new = Modder.rename_base(file) { |base| base.sub(/#{match}$/, trans) }
43
43
 
44
44
  next if new.nil?
@@ -52,7 +52,7 @@ module Modder
52
52
 
53
53
  # top level wrapper for exts
54
54
  def undercase_ext(ext = '')
55
- transfer = Modder.undercase_ext_get ext, @options[:recurse]
55
+ transfer = Modder.undercase_ext_get ext, @options[:recurse], dirs: @options[:dirs]
56
56
  Modder.undercase_ext_set ext, transfer, @options[:force]
57
57
  end
58
58
  end
@@ -74,15 +74,40 @@ class << Modder
74
74
  $stdin.gets.chomp.downcase[0] == 'y'
75
75
  end
76
76
 
77
- # return a list of files to examine
78
- def files(recurse)
79
- if recurse
80
- Dir['**/*'].select { |f| File.file?(f) }
81
- else
82
- 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?
83
100
  end
84
101
  end
85
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
+
86
111
  # apply a transformation to a file's basename only, preserving its directory.
87
112
  # the block receives the basename and returns a new basename; returning it
88
113
  # unchanged means no rename should happen, in which case nil is returned
@@ -105,7 +130,9 @@ class << Modder
105
130
  end
106
131
  end
107
132
 
108
- # 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
109
136
  def execute(transfer, force: false)
110
137
  transfer.each { |o, n| Modder.rename o, n, force }
111
138
  end
@@ -138,10 +165,10 @@ class << Modder
138
165
  end
139
166
 
140
167
  # get all extensions to change
141
- def undercase_ext_get(ext, recurse)
168
+ def undercase_ext_get(ext, recurse, dirs: false)
142
169
  transfer = {}
143
170
  allexts = ext.empty?
144
- Modder.files(recurse).each do |file|
171
+ Modder.files(recurse, dirs: dirs).each do |file|
145
172
  new = Modder.rename_base(file) do |base|
146
173
  cur_ext = allexts ? base.split('.').last : ext
147
174
  next base if cur_ext == base # no extension to change
@@ -154,7 +181,24 @@ class << Modder
154
181
  transfer[file] = new
155
182
  end
156
183
 
157
- 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
158
202
  end
159
203
 
160
204
  # set all extensions to change
@@ -172,7 +216,8 @@ class << Modder
172
216
  final = {}
173
217
  temp = {}
174
218
 
175
- # 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
176
221
  transfer.each do |k, v|
177
222
  tempfile = (v.hash * v.object_id).abs.to_s
178
223
  final[tempfile] = v
@@ -180,7 +225,10 @@ class << Modder
180
225
  end
181
226
 
182
227
  Modder.execute temp
183
- 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)
184
232
  puts 'Modifications complete.'
185
233
  else
186
234
  puts 'No modifications done.'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Modname
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/modname.rb CHANGED
@@ -21,7 +21,7 @@ module Modname
21
21
  attr_reader :options
22
22
 
23
23
  def initialize
24
- @options = { force: false, recurse: false }
24
+ @options = { force: false, recurse: false, dirs: false }
25
25
  @transfer = {}
26
26
  end
27
27
 
@@ -49,24 +49,27 @@ module Modname
49
49
  end
50
50
  end
51
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
+
52
62
  # parse out arguments
53
63
  def parse(args)
54
64
  opts = { cmd: 'file', args: [] }
55
65
 
56
66
  args.each do |opt|
57
- case opt
58
- when '-f'
59
- @options[:force] = true
60
- when '-r'
61
- @options[:recurse] = true
62
- when '-e', '--ext'
63
- opts[:cmd] = 'ext'
64
- when '-h', '--help'
65
- opts[:cmd] = 'help'
66
- when '-v', '--version'
67
- opts[:cmd] = 'version'
68
- else # command argument
69
- 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
70
73
  end
71
74
  end
72
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
@@ -80,5 +81,4 @@ This uses `rspec` and `just` to run a suite of unit tests. To run the suite:
80
81
  ## todos
81
82
 
82
83
  - figure out whats up with the extension globbing
83
- - apply optionally to directories as well
84
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: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Warner
@@ -89,7 +89,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
89
  requirements:
90
90
  - - ">="
91
91
  - !ruby/object:Gem::Version
92
- version: '0'
92
+ version: '3.0'
93
93
  required_rubygems_version: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - ">="