gr8 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +559 -0
- data/Rakefile +87 -0
- data/bin/gr8 +657 -0
- data/gr8.gemspec +50 -0
- data/setup.rb +1585 -0
- data/test/app_test.rb +277 -0
- data/test/common.rb +31 -0
- data/test/enum_test.rb +834 -0
- data/test/test_all.rb +12 -0
- data/test/util_test.rb +114 -0
- metadata +93 -0
data/Rakefile
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
|
6
|
+
desc "run test scripts"
|
7
|
+
task :test do
|
8
|
+
sh "ruby -Itest test/oktest.rb -ss test/*_test.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
desc "build gem file"
|
13
|
+
task :build do
|
14
|
+
ver = _get_1st_argument(:build, "version")
|
15
|
+
dir = "build/gr8-#{ver}"
|
16
|
+
rm_rf dir
|
17
|
+
#
|
18
|
+
spec = _load_gemspec_file("gr8.gemspec")
|
19
|
+
spec.files.each do |fpath|
|
20
|
+
new_fpath = "#{dir}/#{fpath}"
|
21
|
+
dirpath = File.dirname(new_fpath)
|
22
|
+
mkdir_p dirpath unless File.exist?(dirpath)
|
23
|
+
cp fpath, new_fpath
|
24
|
+
end
|
25
|
+
#
|
26
|
+
Dir.chdir(dir) do
|
27
|
+
spec.files.each do |fpath|
|
28
|
+
_edit_file(fpath, ver) {|s|
|
29
|
+
s.gsub!(/\$[R]elease:.*?\$/, "$\Release: #{ver} $")
|
30
|
+
s.gsub!(/\$[R]elease\$/, "$\Release: #{ver} $")
|
31
|
+
s
|
32
|
+
}
|
33
|
+
end
|
34
|
+
sh "gem build gr8.gemspec"
|
35
|
+
mv "gr8-#{ver}.gem", ".."
|
36
|
+
end
|
37
|
+
#
|
38
|
+
puts "** created: #{dir}.gem"
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
desc "release preparation"
|
43
|
+
task :release => [:test, :build] do
|
44
|
+
ver = _get_1st_argument(:build, "version")
|
45
|
+
dir = "build/gr8-#{ver}"
|
46
|
+
gemfile = "gr8-#{ver}.gem"
|
47
|
+
puts ""
|
48
|
+
print "** Are you sure to release #{gemfile}? [y/N] "
|
49
|
+
answer = $stdin.gets()
|
50
|
+
if answer =~ /\A[yY]/
|
51
|
+
sh "git tag #{ver}"
|
52
|
+
sh "git push --tags"
|
53
|
+
Dir.chdir "build" do
|
54
|
+
sh "gem push #{gemfile}"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
$stderr.puts "** Canceled."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
## helper functions
|
63
|
+
|
64
|
+
def _get_1st_argument(task, argname="argument")
|
65
|
+
if ARGV.length == 1
|
66
|
+
raise ArgumentError.new("rake #{task}: #{argname} required.")
|
67
|
+
end
|
68
|
+
arg = ARGV[1]
|
69
|
+
ARGV.slice(1, ARGV.length).each{|v| task(v.intern) { } }
|
70
|
+
return arg
|
71
|
+
end
|
72
|
+
|
73
|
+
def _load_gemspec_file(filename)
|
74
|
+
require "rubygems"
|
75
|
+
spec = Gem::Specification::load(filename)
|
76
|
+
return spec
|
77
|
+
end
|
78
|
+
|
79
|
+
def _edit_file(fpath, ver)
|
80
|
+
File.open(fpath, 'r+') do |f|
|
81
|
+
s = f.read()
|
82
|
+
s2 = yield s
|
83
|
+
f.rewind()
|
84
|
+
f.truncate(0)
|
85
|
+
f.write(s2)
|
86
|
+
end
|
87
|
+
end
|
data/bin/gr8
ADDED
@@ -0,0 +1,657 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
###
|
5
|
+
### gr8 -- a great command-line utility powered by Ruby
|
6
|
+
###
|
7
|
+
### $Release: 0.1.0 $
|
8
|
+
### $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
|
9
|
+
### $License: MIT License $
|
10
|
+
###
|
11
|
+
|
12
|
+
require "optparse"
|
13
|
+
|
14
|
+
|
15
|
+
def fu
|
16
|
+
#; [!ktccp] returns FileUtils class object.
|
17
|
+
require "fileutils" unless defined?(FileUtils)
|
18
|
+
FileUtils
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
class String
|
23
|
+
|
24
|
+
def q
|
25
|
+
#; [!ejo5y] quotes string with single-quoation.
|
26
|
+
#; [!ageyj] escapes single-quotation characters.
|
27
|
+
"'%s'" % self.gsub(/'/) { "\\'" }
|
28
|
+
end
|
29
|
+
|
30
|
+
def qq
|
31
|
+
#; [!wwvll] quotes string with double-quotation.
|
32
|
+
#; [!rc66j] escapes double-quotation characters.
|
33
|
+
'"%s"' % self.gsub(/"/) { '\\"' }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
class Object
|
40
|
+
|
41
|
+
## [Experimental]
|
42
|
+
## With Object#_, you can use '_' character as each value
|
43
|
+
## in block argument of map() or select().
|
44
|
+
## For example, 'map{|s|s+".bkp"}' can be 'map{_+".bkp"}'.
|
45
|
+
## See 'transform()', 'map()' or 'select()' for details.
|
46
|
+
def _
|
47
|
+
#; [!wvemx] returns self object.
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
module Enumerable
|
55
|
+
|
56
|
+
def transform(&block)
|
57
|
+
#; [!peitw] similar to map() or collect(), make each item as self in block.
|
58
|
+
collect {|x| x.instance_exec(x, &block) }
|
59
|
+
end
|
60
|
+
alias xf transform
|
61
|
+
|
62
|
+
alias __map map
|
63
|
+
def map(&block)
|
64
|
+
#; [!zfmcx] each item is available as self in block of map().
|
65
|
+
__map {|x| x.instance_exec(x, &block) }
|
66
|
+
end
|
67
|
+
|
68
|
+
alias __select select
|
69
|
+
def select(&block)
|
70
|
+
#; [!41hap] each item is available as self in block of select().
|
71
|
+
__select {|x| x.instance_exec(x, &block) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def sum
|
75
|
+
#; [!9izc1] returns sum of numbers.
|
76
|
+
inject(0, :+)
|
77
|
+
end
|
78
|
+
|
79
|
+
def sum_i
|
80
|
+
#; [!01ehd] returns sum of integers, converting values into integer.
|
81
|
+
inject(0) {|t, x| t + x.to_i }
|
82
|
+
end
|
83
|
+
|
84
|
+
def sum_f
|
85
|
+
#; [!kplnt] returns sum of floats, converting values into float.
|
86
|
+
inject(0.0) {|t, x| t + x.to_f }
|
87
|
+
end
|
88
|
+
|
89
|
+
def avg
|
90
|
+
#; [!pvi8h] returnns average of numbers.
|
91
|
+
#; [!poidi] returns nil when no numbers.
|
92
|
+
i = 0
|
93
|
+
sum = inject(0) {|t, n| i += 1; t + n }
|
94
|
+
i == 0 ? nil : sum.to_f / i
|
95
|
+
end
|
96
|
+
|
97
|
+
def avg_i
|
98
|
+
#; [!btiat] returns average of numbers, converting values into integer.
|
99
|
+
#; [!892q9] returns nil when no numbers.
|
100
|
+
i = 0
|
101
|
+
sum = inject(0) {|t, x| i += 1; t + x.to_i }
|
102
|
+
i == 0 ? nil : sum.to_f / i
|
103
|
+
end
|
104
|
+
|
105
|
+
def avg_f
|
106
|
+
#; [!oqpmc] returns average of numbers, converting values into float.
|
107
|
+
#; [!9bckq] returns nil when no numbers.
|
108
|
+
i = 0
|
109
|
+
sum = inject(0) {|t, x| i += 1; t + x.to_f }
|
110
|
+
i == 0 ? nil : sum.to_f / i
|
111
|
+
end
|
112
|
+
|
113
|
+
def xsplit(pat=nil)
|
114
|
+
#; [!1pz77] splits each lines with pattern.
|
115
|
+
#; [!wte7b] if block given, use its result as index.
|
116
|
+
if block_given?
|
117
|
+
idx = yield
|
118
|
+
collect {|s| s.split(pat)[idx] }
|
119
|
+
else
|
120
|
+
collect {|s| s.split(pat) }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def sed(pat, str=nil, &block)
|
125
|
+
#; [!c7m34] replaces all patterns found in each line with str or block.
|
126
|
+
if block_given?
|
127
|
+
collect {|s| s.sub(pat, &block) }
|
128
|
+
else
|
129
|
+
collect {|s| s.sub(pat, str) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def gsed(pat, str=nil, &block)
|
134
|
+
#; [!9lzjv] replaces first pattern found in each line with str or block.
|
135
|
+
if block_given?
|
136
|
+
collect {|s| s.gsub(pat, &block) }
|
137
|
+
else
|
138
|
+
collect {|s| s.gsub(pat, str) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def paths(&block)
|
143
|
+
#; [!t55ce] collects Pathname objects when block argument is not passed.
|
144
|
+
#; [!yjkm5] yields Pathname objects when block argument is passed.
|
145
|
+
#; [!4kppy] self is Patname object in block argument.
|
146
|
+
require "pathname" unless defined?(Pathname)
|
147
|
+
if block_given?
|
148
|
+
collect {|s| x = Pathname(s); x.instance_exec(x, &block) }
|
149
|
+
else
|
150
|
+
collect {|s| Pathname(s) }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def edit(verbose=true, encoding='utf-8', &block)
|
155
|
+
edit_i(nil, verbose, encoding, &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
def edit_i(suffix, verbose=true, encoding='utf-8', &block)
|
159
|
+
require "fileutils" unless defined?(FileUtils)
|
160
|
+
arity = block.arity
|
161
|
+
collect {|fpath|
|
162
|
+
fpath.strip!
|
163
|
+
msg = nil
|
164
|
+
if File.file?(fpath)
|
165
|
+
#; [!lpncu] creates backup file with suffix spedified.
|
166
|
+
if suffix && ! suffix.empty?
|
167
|
+
bkup_fpath = "#{fpath}#{suffix}"
|
168
|
+
FileUtils.mv(fpath, bkup_fpath)
|
169
|
+
FileUtils.cp(bkup_fpath, fpath)
|
170
|
+
end
|
171
|
+
#; [!ur9mj] opens file with utf-8 encoding.
|
172
|
+
File.open(fpath, 'r+', encoding: encoding) do |f|
|
173
|
+
s = f.read()
|
174
|
+
s1 = s + ""
|
175
|
+
s1.object_id != s.object_id or raise "** assertion failed"
|
176
|
+
#; [!qqegl] file content and file path are passed to block argument.
|
177
|
+
#; [!d8dxv] make content as self in block argument.
|
178
|
+
s2 = s.instance_exec(s, fpath, &block)
|
179
|
+
#; [!9g7re] edit file when content changed.
|
180
|
+
if s1 != s2
|
181
|
+
f.rewind()
|
182
|
+
f.truncate(0)
|
183
|
+
f.write(s2.to_s)
|
184
|
+
msg = "Edit: '#{fpath}'" if verbose
|
185
|
+
#; [!exzkz] don't edit file when content not changed.
|
186
|
+
else
|
187
|
+
msg = "NotChanged: '#{fpath}'" if verbose
|
188
|
+
end
|
189
|
+
end
|
190
|
+
else
|
191
|
+
#; [!k9d31] skips if file not exist.
|
192
|
+
#; [!6m49n] skips if file is not a file.
|
193
|
+
if ! File.exist?(fpath)
|
194
|
+
msg = "Skip: '#{fpath}' does not exist."
|
195
|
+
else
|
196
|
+
msg = "Skip: '#{fpath}' is not a file."
|
197
|
+
end
|
198
|
+
end
|
199
|
+
msg
|
200
|
+
}.reject {|x| x.nil? }
|
201
|
+
end
|
202
|
+
|
203
|
+
## experimentals
|
204
|
+
|
205
|
+
def move_to(verbose=true, &block) #:experimental:
|
206
|
+
__copy_or_move_to("Move", verbose, false, false, "move_to", &block)
|
207
|
+
end
|
208
|
+
|
209
|
+
def move_to!(verbose=true, &block) #:experimental:
|
210
|
+
__copy_or_move_to("Move", verbose, true, false, "move_to!", &block)
|
211
|
+
end
|
212
|
+
|
213
|
+
def mkdir_and_move_to(verbose=true, &block) #:experimental:
|
214
|
+
__copy_or_move_to("Move", verbose, false, true, "mkdir_and_move_to", &block)
|
215
|
+
end
|
216
|
+
|
217
|
+
def mkdir_and_move_to!(verbose=true, &block) #:experimental:
|
218
|
+
__copy_or_move_to("Move", verbose, true, true, "mkdir_and_move_to!", &block)
|
219
|
+
end
|
220
|
+
|
221
|
+
def copy_to(verbose=true, &block) #:experimental:
|
222
|
+
__copy_or_move_to("Copy", verbose, false, false, "copy_to", &block)
|
223
|
+
end
|
224
|
+
|
225
|
+
def copy_to!(verbose=true, &block) #:experimental:
|
226
|
+
__copy_or_move_to("Copy", verbose, true, false, "copy_to!", &block)
|
227
|
+
end
|
228
|
+
|
229
|
+
def mkdir_and_copy_to(verbose=true, &block) #:experimental:
|
230
|
+
__copy_or_move_to("Copy", verbose, false, true, "mkdir_and_copy_to", &block)
|
231
|
+
end
|
232
|
+
|
233
|
+
def mkdir_and_copy_to!(verbose=true, &block) #:experimental:
|
234
|
+
__copy_or_move_to("Copy", verbose, true, true, "mkdir_and_copy_to!", &block)
|
235
|
+
end
|
236
|
+
|
237
|
+
def rename_as(verbose=true, &block) #:experimental:
|
238
|
+
__copy_or_rename_as("Rename", verbose, false, false, "rename_as", &block)
|
239
|
+
end
|
240
|
+
|
241
|
+
def rename_as!(verbose=true, &block) #:experimental:
|
242
|
+
__copy_or_rename_as("Rename", verbose, true, false, "rename_as!", &block)
|
243
|
+
end
|
244
|
+
|
245
|
+
def mkdir_and_rename_as(verbose=true, &block) #:experimental:
|
246
|
+
__copy_or_rename_as("Rename", verbose, false, true, "mkdir_and_rename_as", &block)
|
247
|
+
end
|
248
|
+
|
249
|
+
def mkdir_and_rename_as!(verbose=true, &block) #:experimental:
|
250
|
+
__copy_or_rename_as("Rename", verbose, true, true, "mkdir_and_rename_as!", &block)
|
251
|
+
end
|
252
|
+
|
253
|
+
def copy_as(verbose=true, &block) #:experimental:
|
254
|
+
__copy_or_rename_as("Copy", verbose, false, false, "copy_as", &block)
|
255
|
+
end
|
256
|
+
|
257
|
+
def copy_as!(verbose=true, &block) #:experimental:
|
258
|
+
__copy_or_rename_as("Copy", verbose, true, false, "copy_as!", &block)
|
259
|
+
end
|
260
|
+
|
261
|
+
def mkdir_and_copy_as(verbose=true, &block) #:experimental:
|
262
|
+
__copy_or_rename_as("Copy", verbose, false, true, "mkdir_and_copy_as", &block)
|
263
|
+
end
|
264
|
+
|
265
|
+
def mkdir_and_copy_as!(verbose=true, &block) #:experimental:
|
266
|
+
__copy_or_rename_as("Copy", verbose, true, true, "mkdir_and_copy_as!", &block)
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def __copy_or_move_to(action, verbose_p, overwrite_p, mkdir_p, meth, &block)
|
272
|
+
#; [!n0ubo] block argument is required.
|
273
|
+
#; [!40se5] block argument is required.
|
274
|
+
#; [!k74dw] block argument is required.
|
275
|
+
#; [!z9yus] block argument is required.
|
276
|
+
block or
|
277
|
+
raise ArgumentError.new("#{meth}(): block argument required.")
|
278
|
+
require "fileutils" unless defined?(FileUtils)
|
279
|
+
existing = nil
|
280
|
+
collect {|fpath|
|
281
|
+
#; [!qqzqz] trims target file name.
|
282
|
+
fpath.strip!
|
283
|
+
#; [!nnud9] destination directory name is derived from target file name.
|
284
|
+
dirpath = fpath.instance_exec(fpath, &block)
|
285
|
+
#; [!ey3e4] if target directory name is nil or empty, skip moving file.
|
286
|
+
if ! dirpath || dirpath.empty?
|
287
|
+
msg = "Skip: target directory name is nil or empty (file: '#{fpath}')"
|
288
|
+
#; [!i5jt6] if destination directory exists, move file to it.
|
289
|
+
elsif dirpath == existing || File.directory?(dirpath)
|
290
|
+
msg = nil
|
291
|
+
#; [!azqgk] if there is a file that name is same as desination directory, skip.
|
292
|
+
elsif File.exist?(dirpath)
|
293
|
+
msg = "Skip: directory '#{dirpath}' not a directory"
|
294
|
+
#; [!b9d4m] if destination directory doesn't exist, creates it.
|
295
|
+
elsif mkdir_p
|
296
|
+
FileUtils.mkdir_p(dirpath)
|
297
|
+
msg = nil
|
298
|
+
#; [!rqu5q] if destinatio directory doesn't exist, skip.
|
299
|
+
else
|
300
|
+
msg = "Skip: directory '#{dirpath}' not exist"
|
301
|
+
end
|
302
|
+
#
|
303
|
+
if msg.nil?
|
304
|
+
new_fpath = File.join(dirpath, File.basename(fpath))
|
305
|
+
#; [!0gq9h] if destination file already exist, skip.
|
306
|
+
exist_p = File.exist?(new_fpath)
|
307
|
+
if exist_p && ! overwrite_p
|
308
|
+
msg = "Skip: destination file '#{new_fpath}' already exist."
|
309
|
+
#; [!ebdqh] overwrite destination file even if it exists.
|
310
|
+
else
|
311
|
+
#; [!fa5y0] copy files or directories into destination directory.
|
312
|
+
if action == "Copy"
|
313
|
+
FileUtils.cp_r(fpath, dirpath)
|
314
|
+
#; [!d9vxl] move files or directories into destination directory.
|
315
|
+
elsif action == "Move"
|
316
|
+
FileUtils.mv(fpath, dirpath)
|
317
|
+
else
|
318
|
+
raise "** unreachable"
|
319
|
+
end
|
320
|
+
existing = dirpath
|
321
|
+
#; [!n7a1q] prints target file and destination directory when verbose mode.
|
322
|
+
#; [!itsh0] use 'Move!' instead of 'Move' when overwriting existing file.
|
323
|
+
msg = "#{action}#{exist_p ? '!' : ''}: '#{fpath}' => '#{dirpath}'" if verbose_p
|
324
|
+
end
|
325
|
+
end
|
326
|
+
msg
|
327
|
+
}.reject {|s| s.nil? }
|
328
|
+
end
|
329
|
+
|
330
|
+
def __copy_or_rename_as(action, verbose_p, overwrite_p, mkdir_p, meth, &block)
|
331
|
+
#; [!ignfm] block argument is required.
|
332
|
+
block or
|
333
|
+
raise ArgumentError.new("#{meth}(): block argument required.")
|
334
|
+
require "fileutils" unless defined?(FileUtils)
|
335
|
+
existing = nil
|
336
|
+
collect {|fpath|
|
337
|
+
#; [!qqzqz] trims target file name.
|
338
|
+
fpath.strip!
|
339
|
+
#; [!nnud9] destination file name is derived from source file name.
|
340
|
+
new_fpath = fpath.instance_exec(fpath, &block)
|
341
|
+
#
|
342
|
+
overwritten = false
|
343
|
+
#; [!dkejf] if target directory name is nil or empty, skips renaming file.
|
344
|
+
if ! new_fpath || new_fpath.empty?
|
345
|
+
msg = "Skip: target file name is nil or empty (file: '#{fpath}')"
|
346
|
+
#
|
347
|
+
elsif File.exist?(new_fpath)
|
348
|
+
#; [!1yzjd] if target file or directory already exists, removes it before renaming file.
|
349
|
+
if overwrite_p
|
350
|
+
FileUtils.rm_rf(new_fpath)
|
351
|
+
overwritten = true
|
352
|
+
msg = nil
|
353
|
+
#; [!8ap57] if target file or directory already exists, skips renaming files.
|
354
|
+
else
|
355
|
+
msg = "Skip: target file '#{new_fpath}' already exists."
|
356
|
+
end
|
357
|
+
#
|
358
|
+
else
|
359
|
+
#; [!qhlc8] if directory of target file already exists, renames file.
|
360
|
+
dirpath = File.dirname(new_fpath)
|
361
|
+
if existing == dirpath || File.directory?(dirpath)
|
362
|
+
existing = dirpath
|
363
|
+
msg = nil
|
364
|
+
#; [!sh2ti] if directory of target file not exist, creates it.
|
365
|
+
elsif mkdir_p
|
366
|
+
FileUtils.mkdir_p(dirpath)
|
367
|
+
existing = dirpath
|
368
|
+
msg = nil
|
369
|
+
#; [!gg9w1] if directory of target file not exist, skips renaming files.
|
370
|
+
else
|
371
|
+
msg = "Skip: directory of target file '#{new_fpath}' not exist."
|
372
|
+
end
|
373
|
+
end
|
374
|
+
if msg.nil?
|
375
|
+
#; [!0txp4] copy files or directories.
|
376
|
+
if action == "Copy"
|
377
|
+
FileUtils.cp_r(fpath, new_fpath)
|
378
|
+
#; [!xi8u5] rename files or directories.
|
379
|
+
elsif action == "Rename"
|
380
|
+
FileUtils.move(fpath, new_fpath)
|
381
|
+
else
|
382
|
+
raise "** unreachable"
|
383
|
+
end
|
384
|
+
#; [!vt24y] prints source and destination file path when verbose mode.
|
385
|
+
#; [!gd9j9] use 'Rename!' instead of 'Rename' when overwriting existing file.
|
386
|
+
#; [!8warb] use 'Copy!' instead of 'Copy' when overwriting exsiting file.
|
387
|
+
msg = "#{action}#{overwritten ? '!': ''}: '#{fpath}' => '#{new_fpath}'" if verbose_p
|
388
|
+
end
|
389
|
+
msg
|
390
|
+
}.reject {|s| s.nil? }
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
|
396
|
+
class Enumerator::Lazy
|
397
|
+
|
398
|
+
alias __map map
|
399
|
+
def map(&block)
|
400
|
+
#; [!drgky] each item is available as self in block of map().
|
401
|
+
__map {|x| x.instance_exec(x, &block) }
|
402
|
+
end
|
403
|
+
|
404
|
+
alias __select select
|
405
|
+
def select(&block)
|
406
|
+
#; [!uhqz2] each item is available as self in block of map().
|
407
|
+
__select {|x| x.instance_exec(x, &block) }
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
module Gr8
|
414
|
+
|
415
|
+
VERSION = "$Release: 0.1.0 $".split()[1]
|
416
|
+
|
417
|
+
WEBSITE_URL = "https://kwatch.github.io/gr8/"
|
418
|
+
|
419
|
+
HELP = <<"END"
|
420
|
+
%{script} -- great command-line utility powered by Ruby
|
421
|
+
|
422
|
+
Usage:
|
423
|
+
%{script} [options] ruby-code
|
424
|
+
|
425
|
+
Options:
|
426
|
+
-h, --help : print help
|
427
|
+
--doc : open document with browser
|
428
|
+
-v, --version : print version
|
429
|
+
-r lib[,lib2,...] : require libraries
|
430
|
+
-F[regexp] : separate each line into fields
|
431
|
+
-C N : select column (1-origin)
|
432
|
+
|
433
|
+
Example:
|
434
|
+
$ cat data
|
435
|
+
Haruhi 100
|
436
|
+
Mikuru 80
|
437
|
+
Yuki 120
|
438
|
+
$ cat data | %{script}s 'map{|s| s.split()[1]}' ## self == $stdin.lazy
|
439
|
+
100
|
440
|
+
80
|
441
|
+
120
|
442
|
+
$ cat data | %{script}s 'map{split()[1].to_i}.sum' ## map{self} == map{|s| s}
|
443
|
+
300
|
444
|
+
$ cat data | %{script}s 'map{split[1]}.sum_i' ## .sum_i == map(&:to_i).sum
|
445
|
+
300
|
446
|
+
$ cat data | %{script}s -C2 'sum_i' ## -C2 == map{split[1]}
|
447
|
+
300
|
448
|
+
|
449
|
+
See #{WEBSITE_URL} for details and examples.
|
450
|
+
|
451
|
+
END
|
452
|
+
|
453
|
+
|
454
|
+
class EnumWrapper
|
455
|
+
include Enumerable
|
456
|
+
|
457
|
+
def initialize(enum, separator=nil, column=nil)
|
458
|
+
@_base = enum
|
459
|
+
@_separator = separator
|
460
|
+
@_column = column
|
461
|
+
end
|
462
|
+
|
463
|
+
def each
|
464
|
+
#; [!hloy1] splits each line into array.
|
465
|
+
#; [!c22km] chomps each lin before splitting.
|
466
|
+
#; [!m411f] selects column when column number specified.
|
467
|
+
sep = @_separator
|
468
|
+
col = @_column
|
469
|
+
if @_column
|
470
|
+
index = @_column - 1
|
471
|
+
@_base.each {|s| s.chomp!; yield s.split(sep)[index] }
|
472
|
+
else
|
473
|
+
@_base.each {|s| s.chomp!; yield s.split(sep) }
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
end
|
478
|
+
|
479
|
+
|
480
|
+
class App
|
481
|
+
|
482
|
+
def run(*args)
|
483
|
+
#
|
484
|
+
begin
|
485
|
+
opts = parse_options(args)
|
486
|
+
rescue ::OptionParser::ParseError => ex
|
487
|
+
#$stderr.puts "ERROR (#{script_name()}): #{ex.args.is_a?(Array) ? ex.args.join(' ') : ex.args}: #{ex.reason}"
|
488
|
+
$stderr.puts "ERROR (#{script_name()}): #{ex.message}"
|
489
|
+
return 1
|
490
|
+
end
|
491
|
+
#
|
492
|
+
output = handle_opts(opts)
|
493
|
+
if output
|
494
|
+
puts output
|
495
|
+
return 0
|
496
|
+
end
|
497
|
+
#
|
498
|
+
errmsg = validate_args(args)
|
499
|
+
if errmsg
|
500
|
+
$stderr.puts "ERROR (#{script_name()}): #{errmsg}"
|
501
|
+
return 1
|
502
|
+
end
|
503
|
+
#; [!8hk3g] option '-F': separates each line into array.
|
504
|
+
#; [!vnwu6] option '-C': select colum.
|
505
|
+
if opts[:separator] || opts[:column]
|
506
|
+
sep = opts[:separator]
|
507
|
+
stdin = EnumWrapper.new($stdin, sep == true ? nil : sep, opts[:column]).lazy
|
508
|
+
else
|
509
|
+
stdin = $stdin.lazy
|
510
|
+
define_singleton_methods_on(stdin)
|
511
|
+
end
|
512
|
+
#; [!r69d6] executes ruby code with $stdin.lazy as self.
|
513
|
+
code = args[0]
|
514
|
+
filename = "<#{script_name()}>"
|
515
|
+
val = stdin.instance_eval(code, filename)
|
516
|
+
#; [!hsvnd] prints nothing when result is nil.
|
517
|
+
#; [!eiaa6] prints each item when result is Enumerable.
|
518
|
+
#; [!6pfay] prints value when result is not nil nor Enumerable.
|
519
|
+
case val
|
520
|
+
when nil ; nil
|
521
|
+
when Enumerable ; val.each {|x| puts x }
|
522
|
+
else ; puts val
|
523
|
+
end
|
524
|
+
#; [!h5wln] returns 0 as status code when executed successfully.
|
525
|
+
return 0
|
526
|
+
end
|
527
|
+
|
528
|
+
def main(argv=ARGV)
|
529
|
+
#; [!w9kb8] exit with status code 0 when executed successfully.
|
530
|
+
#; [!nbag1] exit with status code 1 when execution failed.
|
531
|
+
args = argv.dup
|
532
|
+
status = run(*args)
|
533
|
+
exit status
|
534
|
+
end
|
535
|
+
|
536
|
+
private
|
537
|
+
|
538
|
+
def script_name
|
539
|
+
@script_name ||= File.basename($0)
|
540
|
+
end
|
541
|
+
|
542
|
+
def parse_options(args)
|
543
|
+
#; [!5efp5] returns Hash object containing command-line options.
|
544
|
+
opts = {}
|
545
|
+
parser = OptionParser.new
|
546
|
+
parser.on("-h", "--help") {|v| opts[:help] = true }
|
547
|
+
parser.on( "--doc") {|v| opts[:doc] = true }
|
548
|
+
parser.on("-v", "--version") {|v| opts[:version] = true }
|
549
|
+
parser.on("-r lib[,lib2,..]") {|v| opts[:require] = v }
|
550
|
+
parser.on("-F[sep]") do |v|
|
551
|
+
#; [!jt4y5] option '-F': separator is omissible.
|
552
|
+
#; [!jo4gm] option '-F': error when invalid regular expression.
|
553
|
+
begin
|
554
|
+
opts[:separator] = v.nil? ? true : Regexp.new(v)
|
555
|
+
rescue RegexpError
|
556
|
+
raise invalid_argument_error(v, "invalid regular expression")
|
557
|
+
end
|
558
|
+
end
|
559
|
+
#parser.on("-C N", Integer) {|v| opts[:column] = v }
|
560
|
+
parser.on("-C N") do |v|
|
561
|
+
#; [!7ruq0] option '-C': argument should be an integer.
|
562
|
+
#; [!6x3dp] option '-C': argument should be >= 1.
|
563
|
+
begin
|
564
|
+
opts[:column] = Integer(v)
|
565
|
+
rescue ArgumentError => ex
|
566
|
+
raise invalid_argument_error(v, "integer expected")
|
567
|
+
end
|
568
|
+
if opts[:column] <= 0
|
569
|
+
raise invalid_argument_error(v, "column number should be >= 1")
|
570
|
+
end
|
571
|
+
end
|
572
|
+
#; [!wdzss] modifies args.
|
573
|
+
parser.parse!(args)
|
574
|
+
return opts
|
575
|
+
end
|
576
|
+
|
577
|
+
def invalid_argument_error(optarg, reason)
|
578
|
+
err = OptionParser::InvalidArgument.new(optarg)
|
579
|
+
err.reason = reason
|
580
|
+
err
|
581
|
+
end
|
582
|
+
|
583
|
+
def handle_opts(opts)
|
584
|
+
#; [!33bj3] option '-h', '--help': prints help message.
|
585
|
+
if opts[:help]
|
586
|
+
return HELP % {script: script_name()}
|
587
|
+
end
|
588
|
+
#; [!7dvjg] option '--doc': opens website with browser.
|
589
|
+
if opts[:doc]
|
590
|
+
cmd = command_to_open_website()
|
591
|
+
system cmd
|
592
|
+
return cmd
|
593
|
+
end
|
594
|
+
#; [!2tfh5] option '-v', '--version': prints version string.
|
595
|
+
if opts[:version]
|
596
|
+
return VERSION
|
597
|
+
end
|
598
|
+
#; [!1s7wm] option '-r': requires libraries.
|
599
|
+
if opts[:require]
|
600
|
+
opts[:require].split(/,/).each do |libname|
|
601
|
+
libname.strip!
|
602
|
+
require libname
|
603
|
+
end
|
604
|
+
end
|
605
|
+
nil
|
606
|
+
end
|
607
|
+
|
608
|
+
def command_to_open_website()
|
609
|
+
url = WEBSITE_URL
|
610
|
+
case RUBY_PLATFORM
|
611
|
+
when /darwin/ ; "open #{url}"
|
612
|
+
when /linux/ ; "xdg-open #{url}"
|
613
|
+
when /bsd/ ; "xdg-open #{url}" # really?
|
614
|
+
when /win|mingw/i ; "start #{url}"
|
615
|
+
else ; "open #{url}" # or error?
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
def validate_args(args)
|
620
|
+
#; [!7wqyh] prints error when no argument.
|
621
|
+
#; [!bwiqv] prints error when too many argument.
|
622
|
+
if args.length == 0
|
623
|
+
return "argument required."
|
624
|
+
elsif args.length > 1
|
625
|
+
return "too many arguments."
|
626
|
+
end
|
627
|
+
nil
|
628
|
+
end
|
629
|
+
|
630
|
+
def define_singleton_methods_on(stdin)
|
631
|
+
(class << stdin; self; end).class_eval do
|
632
|
+
#; [!zcxh1] removes '\n' from each line automatically.
|
633
|
+
alias __each_orig each
|
634
|
+
def each
|
635
|
+
__each_orig {|s| s.chomp!; yield s }
|
636
|
+
end
|
637
|
+
alias __each_new each
|
638
|
+
#; [!i7npb] $1, $2, ... are available in grep() block argument.
|
639
|
+
#; [!vkt64] lines are chomped automatically in grep() if block is not given.
|
640
|
+
def grep(pattern, &block)
|
641
|
+
if pattern.is_a?(Regexp) && block
|
642
|
+
(class << self; self; end).class_eval { alias each __each_orig }
|
643
|
+
end
|
644
|
+
super(pattern, &block)
|
645
|
+
end
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
end
|
650
|
+
|
651
|
+
|
652
|
+
end
|
653
|
+
|
654
|
+
|
655
|
+
#if __FILE__ == $0
|
656
|
+
Gr8::App.new.main() unless $DONT_RUN_GR8_APP
|
657
|
+
#end
|