inplace 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/BSDmakefile +18 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.md +247 -0
- data/Rakefile +8 -0
- data/bin/inplace +10 -0
- data/inplace.gemspec +20 -0
- data/lib/inplace.rb +555 -0
- data/lib/inplace/version.rb +1 -0
- data/man/inplace.1 +254 -0
- data/test/test.sh +292 -0
- metadata +77 -0
data/.gitignore
ADDED
data/BSDmakefile
ADDED
@@ -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
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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/bin/inplace
ADDED
data/inplace.gemspec
ADDED
@@ -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
|
data/lib/inplace.rb
ADDED
@@ -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
|