inplace 1.2.2

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.
@@ -0,0 +1,18 @@
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
+ *.gz
@@ -0,0 +1,18 @@
1
+ # $Idaemons: /home/cvs/inplace/BSDmakefile,v 1.1 2004/04/07 09:07:46 knu Exp $
2
+ # $Id$
3
+
4
+ PREFIX?= /usr/local
5
+ BINDIR= ${PREFIX}/bin
6
+ MANPREFIX?= ${PREFIX}
7
+ MANDIR= ${MANPREFIX}/man/man
8
+
9
+ SCRIPTS= lib/inplace.rb
10
+ MAN= man/inplace.1
11
+
12
+ .PATH: ${.CURDIR}/..
13
+ .PHONY: test
14
+
15
+ .include <bsd.prog.mk>
16
+
17
+ test:
18
+ @${.CURDIR}/test/test.sh
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in inplace.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2004, 2005, 2006, 2007, 2008, 2012 Akinori MUSHA
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions
7
+ are met:
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
+ SUCH DAMAGE.
@@ -0,0 +1,247 @@
1
+ INPLACE(1)
2
+ ==========
3
+
4
+ ## NAME
5
+
6
+ inplace -- edits files in-place through given filter commands
7
+
8
+ ## SYNOPSIS
9
+
10
+ ```
11
+ inplace [-DLfinstvz] [-b suffix] -e commandline [[-e commandline] ...]
12
+ [file ...]
13
+ inplace [-DLfinstvz] [-b suffix] commandline [file ...]
14
+ ```
15
+
16
+ ## DESCRIPTION
17
+
18
+ The inplace command is a utility to edit files in-place through given
19
+ filter commands preserving the original file attributes. Mode and
20
+ ownership (user and group) are preserved by default, and time (access
21
+ and modification) by choice.
22
+
23
+ Inode numbers will change by default, but there is a `-i` option with
24
+ which given the inode number of each edited file will be preserved.
25
+
26
+ As for filter commands, a single command may be specified as the first
27
+ argument to inplace. To pass many filter commands, specify each
28
+ followed by the `-e` option.
29
+
30
+ There are some cases where inplace does not replace a file, such as
31
+ when:
32
+
33
+ 1. The original file is not writable (use `-f` to force editing
34
+ against read-only files)
35
+
36
+ 2. A filter command fails and exits with a non-zero return code
37
+
38
+ 3. The resulted output is identical to the original file
39
+
40
+ 4. The resulted output is empty (use `-z` to accept empty output)
41
+
42
+ ## OPTIONS
43
+
44
+ The following command line arguments are supported:
45
+
46
+ * `-h`
47
+ * `--help`
48
+
49
+ Show help and exit.
50
+
51
+ * `-D`
52
+ * `--debug`
53
+
54
+ Turn on debug output.
55
+
56
+ * `-L`
57
+ * `--dereference`
58
+
59
+ By default, inplace ignores non-regular files including symlinks,
60
+ but this switch makes it resolve (dereference) each symlink using
61
+ `realpath(3)` and edit the original file.
62
+
63
+ * `-b SUFFIX`
64
+ * `--backup-suffix SUFFIX`
65
+
66
+ Create a backup file with the given suffix for each file. Note
67
+ that backup files will be written over existing files, if any.
68
+
69
+ * `-e COMMANDLINE`
70
+ * `--execute COMMANDLINE`
71
+
72
+ Specify a filter command line to run for each file in which the
73
+ following placeholders can be used:
74
+
75
+ * `%0`
76
+
77
+ replaced by the original file path, shell escaped with `\`'s
78
+ as necessary
79
+
80
+ * `%1`
81
+
82
+ replaced by the source file path, shell escaped with `\`'s as
83
+ necessary
84
+
85
+ * `%2`
86
+
87
+ replaced by the destination file path, shell escaped with
88
+ `\`'s as necessary
89
+
90
+ * `%%`
91
+
92
+ replaced by `%`
93
+
94
+ Omission of `%2` indicates `%1` should be modified destructively,
95
+ and omission of both `%1` and `%2` implies `(...) < %1 > %2`
96
+ around the command line.
97
+
98
+ When the filter command is run, the destination file is always an
99
+ empty temporary file, and the source file is either the original
100
+ file or a temporary copy file.
101
+
102
+ Every temporary file has the same suffix as the original file, so
103
+ that file name aware programs can play nicely with it.
104
+
105
+ Instead of specifying a whole command line, you can use a command
106
+ alias defined in a configuration file, `~/.inplace`. See the
107
+ FILES section for the file format.
108
+
109
+ This option can be specified many times, and they will be executed
110
+ in sequence. A file is only replaced if all of them succeeds.
111
+
112
+ See the EXAMPLES section below for details.
113
+
114
+ * `-f`
115
+ * `--force`
116
+
117
+ By default, inplace does not perform editing if a file is not
118
+ writable. This switch makes it force editing even if a file to
119
+ process is read-only.
120
+
121
+ * `-i`
122
+ * `--preserve-inode`
123
+
124
+ Make sure to preserve the inode number of each file.
125
+
126
+ * `-n`
127
+ * `--dry-run`
128
+
129
+ Do not perform any destructive operation and just show what would
130
+ have been done. This switch implies `-v`.
131
+
132
+ * `-s`
133
+ * `--same-directory`
134
+
135
+ Create a temporary file in the same directory as each replaced
136
+ file. This may speed up the performance when the directory in
137
+ question is on a partition that is fast enough and the system
138
+ temporary directory is slow.
139
+
140
+ This switch can be effectively used when the temporary directory
141
+ does not have sufficient disk space for a resulted file.
142
+
143
+ If this option is specified, edited files will have newly assigned
144
+ inode numbers. To prevent this, use the `-i` option.
145
+
146
+ * `-t`
147
+ * `--preserve-timestamp`
148
+
149
+ Preserve the access and modification times of each file.
150
+
151
+ * `-v`
152
+ * `--verbose`
153
+
154
+ Turn on verbose mode.
155
+
156
+ * `-z`
157
+ * `--accept-empty`
158
+
159
+ By default, inplace does not replace the original file when a
160
+ resulted file is empty in size because it is likely that there is
161
+ a mistake in the filter command. This switch makes it accept
162
+ empty (zero-sized) output and replace the original file with it.
163
+
164
+ ## EXAMPLES
165
+
166
+ * Sort files in-place using sort(1):
167
+
168
+ inplace sort file1 file2 file3
169
+
170
+ Below works the same as above, passing each input file via the
171
+ command line argument:
172
+
173
+ inplace 'sort %1 > %2' file1 file2 file3
174
+
175
+ * Perform in-place charset conversion and newline code conversion:
176
+
177
+ inplace -e 'iconv -f EUC-JP -t UTF-8' -e 'perl -pe "s/$/\\r/"'
178
+ file1 file2 file3
179
+
180
+ * Process image files taking backup files:
181
+
182
+ inplace -b.orig 'convert -rotate 270 -resize 50%% %1 %2' *.jpg
183
+
184
+ * Perform a mass MP3 tag modification without changing timestamps:
185
+
186
+ find mp3/Some_Artist -name '*.mp3' -print0 | xargs -0 inplace
187
+ -te 'mp3info -a "Some Artist" -g "Progressive Rock" %1'
188
+
189
+ As you see above, inplace makes a nice combo with find(1) and
190
+ `xargs(1)`.
191
+
192
+ ## FILES
193
+
194
+ * `~/.inplace`
195
+
196
+ The configuration file, which syntax is described as follows:
197
+
198
+ * Each alias definition is a name/value pair separated with an
199
+ `=`, one per line.
200
+
201
+ * White spaces at the beginning or the end of a line, and around
202
+ assignment separators (`=`) are stripped off.
203
+
204
+ * Lines starting with a `#` are ignored.
205
+
206
+ ## ENVIRONMENT
207
+
208
+ * `TMPDIR`
209
+ * `TMP`
210
+ * `TEMP`
211
+
212
+ Temporary directory candidates where inplace attempts to create
213
+ intermediate output files, in that order. If none is available
214
+ and writable, `/tmp` is used. If `-s` is specified, they will not
215
+ be used.
216
+
217
+ ## HOW TO INSTALL
218
+
219
+ Just copy `lib/inplace.rb` to `/somewhere/in/your/path/inplace`, or:
220
+
221
+ gem install inplace
222
+
223
+ ## SEE ALSO
224
+
225
+ [`find(1)`](http://www.freebsd.org/cgi/man.cgi?query=find&sektion=1),
226
+ [`xargs(1)`](http://www.freebsd.org/cgi/man.cgi?query=xargs&sektion=1),
227
+ [`realpath(3)`](http://www.freebsd.org/cgi/man.cgi?query=realpath&sektion=3)
228
+
229
+ ## HISTORY
230
+
231
+ The inplace utility was first released on 2 May, 2004.
232
+
233
+ This utility was written when the author did not feel very happy with
234
+ the `-i` option added to `sed(1)` on FreeBSD.
235
+
236
+ ## AUTHORS
237
+
238
+ Akinori MUSHA <knu@iDaemons.org>
239
+
240
+ Licensed under the 2-clause BSD license. See `LICENSE` for details.
241
+
242
+ Visit [the GitHub repository](https://github.com/knu/inplace) for the
243
+ latest information and feedback.
244
+
245
+ ## BUGS
246
+
247
+ There may always be some bugs. Use at your own risk.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+
6
+ task :test do
7
+ sh 'test/test.sh'
8
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'inplace'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'inplace'
8
+ end
9
+
10
+ main(ARGV)
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/inplace/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Akinori MUSHA"]
6
+ gem.email = ["knu@idaemons.org"]
7
+ gem.description = %q{A command line utility that edits files in-place through given filter commands}
8
+ gem.summary = <<-'EOS'
9
+ Inplace(1) is a command line utility that edits files in-place through
10
+ given filter commands. e.g. inplace 'sort' file1 file2 file3
11
+ EOS
12
+ gem.homepage = "https://github.com/knu/inplace"
13
+
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "inplace"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = Inplace::VERSION
20
+ end
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- ruby -*-
3
+ #
4
+ # inplace.rb - edits files in-place through given filter commands
5
+ #
6
+ # Copyright (c) 2004, 2005, 2006, 2007, 2008, 2012 Akinori MUSHA
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions
12
+ # are met:
13
+ # 1. Redistributions of source code must retain the above copyright
14
+ # notice, this list of conditions and the following disclaimer.
15
+ # 2. Redistributions in binary form must reproduce the above copyright
16
+ # notice, this list of conditions and the following disclaimer in the
17
+ # documentation and/or other materials provided with the distribution.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29
+ # SUCH DAMAGE.
30
+ #
31
+
32
+ if RUBY_VERSION < "1.8.2"
33
+ STDERR.puts "Ruby 1.8.2 or later is required."
34
+ exit 255
35
+ end
36
+
37
+ module Inplace
38
+ VERSION = "1.2.2"
39
+ end
40
+
41
+ MYNAME = File.basename($0)
42
+
43
+ require "optparse"
44
+
45
+ def main(argv)
46
+ $uninterruptible = $interrupt = false
47
+
48
+ [:SIGINT, :SIGQUIT, :SIGTERM].each { |sig|
49
+ trap(sig) {
50
+ if $uninterruptible
51
+ $interrupt = true
52
+ else
53
+ interrupt
54
+ end
55
+ }
56
+ }
57
+
58
+ usage = <<-"EOF"
59
+ usage: #{MYNAME} [-Lfinstvz] [-b SUFFIX] COMMANDLINE [file ...]
60
+ #{MYNAME} [-Lfinstvz] [-b SUFFIX] [-e COMMANDLINE] [file ...]
61
+ EOF
62
+
63
+ banner = <<-"EOF"
64
+ #{MYNAME} version #{Inplace::VERSION}
65
+
66
+ Edits files in-place through given filter commands.
67
+
68
+ #{usage}
69
+ EOF
70
+
71
+ filters = []
72
+
73
+ $config = Inplace::Config.new
74
+ file = File.expand_path("~/.inplace")
75
+ $config.load(file) if File.exist?(file)
76
+
77
+ opts = OptionParser.new(banner, 24) { |opts|
78
+ nextline = "\n" << opts.summary_indent << " " * opts.summary_width << " "
79
+
80
+ opts.on("-h", "--help",
81
+ "Show this message.") {
82
+ print opts
83
+ exit 0
84
+ }
85
+
86
+ opts.on("-L", "--dereference",
87
+ "Edit the original file for each symlink.") {
88
+ |b| $dereference = b
89
+ }
90
+
91
+ opts.on("-b", "--backup-suffix=SUFFIX",
92
+ "Create a backup file with the SUFFIX for each file." << nextline <<
93
+ "Backup files will be written over existing files," << nextline <<
94
+ "if any.") {
95
+ |s| $backup_suffix = s
96
+ }
97
+
98
+ opts.on("-D", "--debug",
99
+ "Turn on debug mode.") {
100
+ |b| $debug = b and $verbose = true
101
+ }
102
+
103
+ opts.on("-e", "--execute=COMMANDLINE",
104
+ "Run COMMANDLINE for each file in which the following" << nextline <<
105
+ "placeholders can be used:" << nextline <<
106
+ " %0: replaced by the original file path" << nextline <<
107
+ " %1: replaced by the source file path" << nextline <<
108
+ " %2: replaced by the destination file path" << nextline <<
109
+ " %%: replaced by a %" << nextline <<
110
+ "Missing %2 indicates %1 is modified destructively," << nextline <<
111
+ "and missing both %1 and %2 implies \"(...) < %1 > %2\"" << nextline <<
112
+ "around the command line.") {
113
+ |s| filters << FileFilter.new($config.expand_alias(s))
114
+ }
115
+
116
+ opts.on("-f", "--force",
117
+ "Force editing even if a file is read-only.") {
118
+ |b| $force = b
119
+ }
120
+
121
+ opts.on("-i", "--preserve-inode",
122
+ "Make sure to preserve the inode number of each file.") {
123
+ |b| $preserve_inode = b
124
+ }
125
+
126
+ opts.on("-n", "--dry-run",
127
+ "Just show what would have been done.") {
128
+ |b| $dry_run = b and $verbose = true
129
+ }
130
+
131
+ opts.on("-s", "--same-directory",
132
+ "Create a temporary file in the same directory as" << nextline <<
133
+ "each replaced file.") {
134
+ |b| $same_directory = b
135
+ }
136
+
137
+ opts.on("-t", "--preserve-timestamp",
138
+ "Preserve the modification time of each file.") {
139
+ |b| $preserve_time = b
140
+ }
141
+
142
+ opts.on("-v", "--verbose",
143
+ "Turn on verbose mode.") {
144
+ |b| $verbose = b
145
+ }
146
+
147
+ opts.on("-z", "--accept-empty",
148
+ "Accept empty (zero-sized) output.") {
149
+ |b| $accept_empty = b
150
+ }
151
+ }
152
+
153
+ setup()
154
+
155
+ files = opts.order(*argv)
156
+
157
+ if filters.empty? && !files.empty?
158
+ filters << FileFilter.new($config.expand_alias(files.shift))
159
+ end
160
+
161
+ if files.empty?
162
+ STDERR.puts "No files to process given.", ""
163
+ print opts
164
+ exit 2
165
+ end
166
+
167
+ case filters.size
168
+ when 0
169
+ STDERR.puts "No filter command line to execute given.", ""
170
+ print opts
171
+ exit 1
172
+ when 1
173
+ filter = filters.first
174
+
175
+ files.each { |file|
176
+ begin
177
+ filter.filter!(file, file)
178
+ rescue => e
179
+ STDERR.puts "#{file}: skipped: #{e}"
180
+ end
181
+ }
182
+ else
183
+ files.each { |file|
184
+ tmpfile = FileFilter.make_tmpfile_for(file)
185
+
186
+ first, last = 0, filters.size - 1
187
+
188
+ begin
189
+ filters.each_with_index { |filter, i|
190
+ if i == first
191
+ filter.filter(file, file, tmpfile)
192
+ elsif i == last
193
+ filter.filter(file, tmpfile, file)
194
+ else
195
+ filter.filter!(file, tmpfile)
196
+ end
197
+ }
198
+ rescue => e
199
+ STDERR.puts "#{file}: skipped: #{e}"
200
+ end
201
+ }
202
+ end
203
+ rescue OptionParser::ParseError => e
204
+ STDERR.puts "#{MYNAME}: #{e}", usage
205
+ exit 64
206
+ rescue => e
207
+ STDERR.puts "#{MYNAME}: #{e}"
208
+ exit 1
209
+ end
210
+
211
+ def setup
212
+ $backup_suffix = nil
213
+ $debug = $verbose =
214
+ $dereference = $force = $dry_run = $same_directory =
215
+ $preserve_inode = $preserve_time = $accept_empty = false
216
+ end
217
+
218
+ require 'set'
219
+ require 'tempfile'
220
+ require 'fileutils'
221
+ require 'pathname'
222
+
223
+ class FileFilter
224
+ def initialize(template)
225
+ @formatter = Formatter.new(template)
226
+ end
227
+
228
+ def destructive?
229
+ @formatter.arity == 1
230
+ end
231
+
232
+ def filter!(origfile, file)
233
+ filter(origfile, file, file)
234
+ end
235
+
236
+ def filter(origfile, infile, outfile)
237
+ if !File.exist?(infile)
238
+ flunk origfile, "file not found"
239
+ end
240
+
241
+ outfile_is_original = !tmpfile?(outfile)
242
+ outfile_stat = File.lstat(outfile)
243
+
244
+ if outfile_stat.symlink?
245
+ $dereference or
246
+ flunk origfile, "symlink"
247
+
248
+ begin
249
+ outfile = Pathname.new(outfile).realpath.to_s
250
+ outfile_stat = File.lstat(outfile)
251
+ rescue => e
252
+ flunk origfile, "symlink unresolvable: %s", e
253
+ end
254
+ end
255
+
256
+ outfile_stat.file? or
257
+ flunk origfile, "symlink to a non-regular file"
258
+
259
+ $force || outfile_stat.writable? or
260
+ flunk origfile, "symlink to a read-only file"
261
+
262
+ tmpfile = FileFilter.make_tmpfile_for(outfile)
263
+
264
+ if destructive?
265
+ debug "cp(%s, %s)", infile, tmpfile
266
+ FileUtils.cp(infile, tmpfile)
267
+ command = @formatter.format(origfile, tmpfile)
268
+ else
269
+ command = @formatter.format(origfile, infile, tmpfile)
270
+ end
271
+
272
+ if run(command)
273
+ File.file?(tmpfile) or
274
+ flunk origfile, "output file removed"
275
+
276
+ !$accept_empty && File.zero?(tmpfile) and
277
+ flunk origfile, "empty output"
278
+
279
+ outfile_is_original && FileUtils.identical?(origfile, tmpfile) and
280
+ flunk origfile, "unchanged"
281
+
282
+ stat = File.stat(infile)
283
+ newsize = File.size(tmpfile) if $dry_run
284
+
285
+ uninterruptible {
286
+ replace(tmpfile, outfile, stat)
287
+ }
288
+
289
+ newsize = File.size(outfile) unless $dry_run
290
+
291
+ info "%s: edited (%d bytes -> %d bytes)", origfile, stat.size, newsize
292
+ else
293
+ flunk origfile, "command exited with %d", $?.exitstatus
294
+ end
295
+ end
296
+
297
+ @@tmpfiles = Set.new
298
+
299
+ def tmpfile?(file)
300
+ @@tmpfiles.include?(file)
301
+ end
302
+
303
+ TMPNAME_BASE = MYNAME.tr('.', '-')
304
+
305
+ def self.make_tmpfile_for(outfile)
306
+ if m = File.basename(outfile).match(/(\..+)$/)
307
+ ext = m[1]
308
+ else
309
+ ext = ''
310
+ end
311
+ if $same_directory
312
+ tmpf = Tempfile.new([TMPNAME_BASE, ext], File.dirname(outfile))
313
+ else
314
+ tmpf = Tempfile.new([TMPNAME_BASE, ext])
315
+ end
316
+ tmpf.close
317
+ path = tmpf.path
318
+ @@tmpfiles << path
319
+ return path
320
+ end
321
+
322
+ private
323
+ def debug(fmt, *args)
324
+ puts sprintf(fmt, *args) if $debug || $dry_run
325
+ end
326
+
327
+ def info(fmt, *args)
328
+ puts sprintf(fmt, *args) if $verbose
329
+ end
330
+
331
+ def warn(fmt, *args)
332
+ STDERR.puts "warning: " + sprintf(fmt, *args)
333
+ end
334
+
335
+ def error(fmt, *args)
336
+ STDERR.puts "error: " + sprintf(fmt, *args)
337
+ end
338
+
339
+ def flunk(origfile, fmt, *args)
340
+ raise "#{origfile}: " << sprintf(fmt, *args)
341
+ end
342
+
343
+ def run(command)
344
+ debug "command: %s", command
345
+ system(command)
346
+ end
347
+
348
+ def replace(file1, file2, stat)
349
+ if tmpfile?(file2)
350
+ debug "move: %s -> %s", file1.shellescape, file2.shellescape
351
+ FileUtils.mv(file1, file2)
352
+ else
353
+ if $backup_suffix && !$backup_suffix.empty?
354
+ bakfile = file2 + $backup_suffix
355
+
356
+ if $preserve_inode
357
+ debug "copy: %s -> %s", file2.shellescape, bakfile.shellescape
358
+ FileUtils.cp(file2, bakfile, :preserve => true) unless $dry_run
359
+ else
360
+ debug "move: %s -> %s", file2.shellescape, bakfile.shellescape
361
+ FileUtils.mv(file2, bakfile) unless $dry_run
362
+ end
363
+ end
364
+
365
+ begin
366
+ if $preserve_inode
367
+ debug "copy: %s -> %s", file1.shellescape, file2.shellescape
368
+ FileUtils.cp(file1, file2) unless $dry_run
369
+ debug "remove: %s", file1.shellescape
370
+ FileUtils.rm(file1) unless $dry_run
371
+ else
372
+ debug "move: %s -> %s", file1.shellescape, file2.shellescape
373
+ FileUtils.mv(file1, file2) unless $dry_run
374
+ end
375
+ rescue => e
376
+ error "%s: failed to overwrite: %s", file2, e
377
+ error "%s: result file left: %s", file2, file1
378
+ exit! 1
379
+ end
380
+ end
381
+
382
+ preserve(file2, stat)
383
+ end
384
+
385
+ def preserve(file, stat)
386
+ if $preserve_time
387
+ debug "utime: %s/%s %s",
388
+ stat.atime.strftime("%Y-%m-%dT%T"),
389
+ stat.mtime.strftime("%Y-%m-%dT%T"), file.shellescape
390
+ File.utime stat.atime, stat.mtime, file unless $dry_run
391
+ end
392
+
393
+ mode = stat.mode
394
+
395
+ begin
396
+ debug "chown: %d:%d %s", stat.uid, stat.gid, file.shellescape
397
+ File.chown stat.uid, stat.gid, file unless $dry_run
398
+ rescue Errno::EPERM
399
+ mode &= 01777
400
+ end
401
+
402
+ debug "chmod: %o %s", mode, file.shellescape
403
+ File.chmod mode, file unless $dry_run
404
+ end
405
+
406
+ class Formatter
407
+ def initialize(template)
408
+ @template = template.dup
409
+
410
+ begin
411
+ self.format("0", "1", "2")
412
+ rescue => e
413
+ raise e
414
+ end
415
+
416
+ if @arity == 0
417
+ @template = "(#{@template}) < %1 > %2"
418
+ @arity = 2
419
+ end
420
+ end
421
+
422
+ attr_reader :template, :arity
423
+
424
+ def format(origfile, infile, outfile = nil)
425
+ s = ''
426
+ template = @template.dup
427
+ arity_bits = 0
428
+
429
+ until template.empty?
430
+ template.sub!(/\A([^%]+)/) {
431
+ s << $1
432
+ ''
433
+ }
434
+ template.sub!(/\A%(.)/) {
435
+ case c = $1
436
+ when '%'
437
+ s << c
438
+ when '0'
439
+ s << origfile.shellescape
440
+ when '1'
441
+ s << infile.shellescape
442
+ arity_bits |= 0x1
443
+ when '2'
444
+ s << outfile.shellescape
445
+ arity_bits |= 0x2
446
+ else
447
+ raise ArgumentError, "invalid placeholder specification (%#{c}): #{@template}"
448
+ end
449
+ ''
450
+ }
451
+ end
452
+
453
+ case arity_bits
454
+ when 0x0
455
+ @arity = 0
456
+ when 0x1
457
+ @arity = 1
458
+ when 0x2
459
+ raise ArgumentError, "%1 is missing while %2 is specified: #{@template}"
460
+ when 0x3
461
+ @arity = 2
462
+ end
463
+
464
+ return s
465
+ end
466
+ end
467
+ end
468
+
469
+ if RUBY_VERSION >= "1.8.7"
470
+ require 'shellwords'
471
+ else
472
+ class String
473
+ def shellescape
474
+ # An empty argument will be skipped, so return empty quotes.
475
+ return "''" if empty?
476
+
477
+ str = dup
478
+
479
+ # Process as a single byte sequence because not all shell
480
+ # implementations are multibyte aware.
481
+ str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
482
+
483
+ # A LF cannot be escaped with a backslash because a backslash + LF
484
+ # combo is regarded as line continuation and simply ignored.
485
+ str.gsub!(/\n/, "'\n'")
486
+
487
+ return str
488
+ end
489
+ end
490
+
491
+ class Tempfile
492
+ alias orig_make_tmpname make_tmpname
493
+
494
+ def make_tmpname(basename, n)
495
+ case basename
496
+ when Array
497
+ prefix, suffix = *basename
498
+ make_tmpname(prefix, n) + suffix
499
+ else
500
+ orig_make_tmpname(basename, n).tr('.', '-')
501
+ end
502
+ end
503
+ end
504
+ end
505
+
506
+ class Inplace::Config
507
+ def initialize
508
+ @alias = {}
509
+ end
510
+
511
+ def load(file)
512
+ File.open(file) { |f|
513
+ f.each_line { |line|
514
+ line.strip!
515
+ next if /^#/ =~ line
516
+
517
+ if m = line.match(/^([^\s=]+)\s*=\s*(.+)/)
518
+ @alias[m[1]] = m[2]
519
+ end
520
+ }
521
+ }
522
+ end
523
+
524
+ def expand_alias(command)
525
+ if @alias.key?(command)
526
+ new_command = @alias[command]
527
+
528
+ info "expanding alias: %s: %s\n", command, new_command
529
+
530
+ new_command
531
+ else
532
+ command
533
+ end
534
+ end
535
+ end
536
+
537
+ def interrupt
538
+ STDERR.puts "Interrupted."
539
+ exit 130
540
+ end
541
+
542
+ def uninterruptible
543
+ orig = $uninterruptible
544
+ $uninterruptible = true
545
+
546
+ yield
547
+
548
+ interrupt if $interrupt
549
+ ensure
550
+ $uninterruptible = orig
551
+ end
552
+
553
+ if $0 == __FILE__
554
+ main(ARGV)
555
+ end