inplace 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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