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.
- 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
|