s7 0.0.1
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/ChangeLog +311 -0
- data/LICENCE +24 -0
- data/README +56 -0
- data/Rakefile +128 -0
- data/VERSION +1 -0
- data/bin/s7cli +27 -0
- data/lib/gettext.rb +28 -0
- data/lib/s7.rb +12 -0
- data/lib/s7/action.rb +7 -0
- data/lib/s7/attribute.rb +226 -0
- data/lib/s7/cipher.rb +110 -0
- data/lib/s7/configuration.rb +41 -0
- data/lib/s7/entry.rb +106 -0
- data/lib/s7/entry_collection.rb +44 -0
- data/lib/s7/entry_template.rb +10 -0
- data/lib/s7/exception.rb +116 -0
- data/lib/s7/file.rb +77 -0
- data/lib/s7/gpass_file.rb +202 -0
- data/lib/s7/key.rb +83 -0
- data/lib/s7/message_catalog.rb +5 -0
- data/lib/s7/s7_file.rb +47 -0
- data/lib/s7/s7cli.rb +213 -0
- data/lib/s7/s7cli/attribute_command.rb +69 -0
- data/lib/s7/s7cli/command.rb +210 -0
- data/lib/s7/s7cli/entry_collection_command.rb +63 -0
- data/lib/s7/s7cli/entry_command.rb +728 -0
- data/lib/s7/s7cli/option.rb +101 -0
- data/lib/s7/secret_generator.rb +30 -0
- data/lib/s7/undo_stack.rb +7 -0
- data/lib/s7/unicode_data.rb +29 -0
- data/lib/s7/utils.rb +37 -0
- data/lib/s7/world.rb +150 -0
- data/setup.rb +1585 -0
- data/test/s7/attribute_test.rb +45 -0
- data/test/s7/gpass_file_test.rb +169 -0
- data/test/s7/gpass_file_test/passwords.gps.empty +1 -0
- data/test/s7/gpass_file_test/passwords.gps.one_entry +2 -0
- data/test/s7/gpass_file_test/passwords.gps.three_entries +3 -0
- data/test/s7/gpass_file_test/passwords.gps.with_folders +0 -0
- data/test/s7/secret_generator_test.rb +29 -0
- data/test/s7/unicode_data_test.rb +28 -0
- data/test/s7/world_test.rb +35 -0
- data/test/test_helper.rb +19 -0
- metadata +108 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "s7/s7cli/command"
|
4
|
+
|
5
|
+
module S7
|
6
|
+
class S7Cli
|
7
|
+
# 機密情報をインポートするコマンドを表現する。
|
8
|
+
class EntryCollectionImportCommand < Command
|
9
|
+
command("import",
|
10
|
+
["entry_collection:import",
|
11
|
+
"entry_collection:read",
|
12
|
+
"entry_collection:load",
|
13
|
+
"read",
|
14
|
+
"load"],
|
15
|
+
_("Import the entry colleciton from file."),
|
16
|
+
_("usage: import [PATH] [OPTIONS]"))
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super
|
20
|
+
@config["type"] = "gpass"
|
21
|
+
@config["path"] = "~/.gpass/passwords.gps"
|
22
|
+
@options.push(StringOption.new("type", _("type")),
|
23
|
+
StringOption.new("path", _("path")))
|
24
|
+
end
|
25
|
+
|
26
|
+
# コマンドを実行する。
|
27
|
+
def run(argv)
|
28
|
+
option_parser.parse!(argv)
|
29
|
+
if argv.length > 0
|
30
|
+
config["path"] = argv.shift
|
31
|
+
end
|
32
|
+
case config["type"]
|
33
|
+
when "gpass"
|
34
|
+
path = File.expand_path(config["path"])
|
35
|
+
if !File.file?(path)
|
36
|
+
raise NotExist.new(path)
|
37
|
+
end
|
38
|
+
puts(_("Import the entry collection in the GPass file: %s") % path)
|
39
|
+
gpass_file = GPassFile.new
|
40
|
+
begin
|
41
|
+
msg = _("Enter the master passphrase for GPass(^D is cancel): ")
|
42
|
+
passphrase = S7Cli.input_string_without_echo(msg)
|
43
|
+
if passphrase
|
44
|
+
ec = gpass_file.read(passphrase, path)
|
45
|
+
# TODO: UNDO スタックにタグの追加と機密情報の追加の操作を積む。
|
46
|
+
world.entry_collection.merge(ec)
|
47
|
+
world.changed = true
|
48
|
+
puts(_("Imported %d entries.") % ec.entries.length)
|
49
|
+
else
|
50
|
+
canceled
|
51
|
+
end
|
52
|
+
rescue InvalidPassphrase => e
|
53
|
+
puts(e.message)
|
54
|
+
retry
|
55
|
+
end
|
56
|
+
else
|
57
|
+
raise ArgumentError, _("not supported type: %s") % config["type"]
|
58
|
+
end
|
59
|
+
return true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,728 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require "s7/s7cli/command"
|
4
|
+
require "s7/secret_generator"
|
5
|
+
require "s7/unicode_data"
|
6
|
+
|
7
|
+
module S7
|
8
|
+
class S7Cli
|
9
|
+
# 機密情報の操作をするコマンドで使用するメソッド群。
|
10
|
+
module EntryCommandUtils
|
11
|
+
include GetText
|
12
|
+
|
13
|
+
EDITING_USAGE =
|
14
|
+
["-----",
|
15
|
+
_("<n>,e<n>:edit attribute a:add attribute d<n>:delete attribute"),
|
16
|
+
_("t:edit tags s:save")].join("\n")
|
17
|
+
|
18
|
+
# ユーザに属性名を入力するように促し、入力された値を返す。
|
19
|
+
def input_attribute_name(default = nil)
|
20
|
+
names = (@entry.attributes.collect(&:name) +
|
21
|
+
@attributes.collect(&:name)).uniq
|
22
|
+
if default
|
23
|
+
s = " (#{default})"
|
24
|
+
else
|
25
|
+
s = ""
|
26
|
+
end
|
27
|
+
prompt = _("Attribute name%s: ") % s
|
28
|
+
begin
|
29
|
+
if input = S7Cli.input_string(prompt)
|
30
|
+
input.strip!
|
31
|
+
if input.empty?
|
32
|
+
input = default
|
33
|
+
elsif names.include?(input)
|
34
|
+
raise ApplicationError, _("Already exist: %s") % input
|
35
|
+
end
|
36
|
+
end
|
37
|
+
return input
|
38
|
+
rescue ApplicationError => e
|
39
|
+
puts("")
|
40
|
+
puts(e.message)
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# ユーザに属性の値を入力するように促し、入力された値を返す。
|
46
|
+
# ^D を入力されて、なおかつ protected が false の場合、nil を返す。
|
47
|
+
def input_value(value, secret, protected)
|
48
|
+
if value.length > 0
|
49
|
+
current = _(" (%s)") % value
|
50
|
+
else
|
51
|
+
current = ""
|
52
|
+
end
|
53
|
+
prompt = _("Value%s: ") % current
|
54
|
+
begin
|
55
|
+
if secret
|
56
|
+
if input = S7Cli.input_string_without_echo(prompt)
|
57
|
+
confirmation =
|
58
|
+
S7Cli.input_string_without_echo(_("Comfirm value: "))
|
59
|
+
if input != confirmation
|
60
|
+
raise ApplicationError, _("Mismatched.")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
else
|
64
|
+
input = S7Cli.input_string(prompt)
|
65
|
+
end
|
66
|
+
if input
|
67
|
+
input.strip!
|
68
|
+
if input.empty? && value
|
69
|
+
input = value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
if protected && (input.nil? || input.empty?)
|
73
|
+
raise ApplicationError, _("The value is empty.")
|
74
|
+
end
|
75
|
+
rescue ApplicationError => e
|
76
|
+
puts("")
|
77
|
+
puts(e.message + _("Try again."))
|
78
|
+
retry
|
79
|
+
end
|
80
|
+
return input
|
81
|
+
end
|
82
|
+
|
83
|
+
# ユーザにタグの入力を促す。入力内容を解析し、タグ名の配列を返す。
|
84
|
+
# ^D を入力された場合、nil を返す。
|
85
|
+
def input_tags(tags)
|
86
|
+
if tags.length > 0
|
87
|
+
current = " (" + tags.join(", ") + ")"
|
88
|
+
else
|
89
|
+
current = ""
|
90
|
+
end
|
91
|
+
prompt = _("Edit tags%s: ") % current
|
92
|
+
if input = S7Cli.input_string(prompt)
|
93
|
+
return input.split(",").collect { |t| t.strip }
|
94
|
+
else
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# ユーザに機密情報かどうかの入力を促す。
|
100
|
+
def input_secret(default)
|
101
|
+
prompt = _("Is this a secret? (%s): ") % (default ? "yes" : "no")
|
102
|
+
return S7Cli.input_boolean(prompt, default)
|
103
|
+
end
|
104
|
+
|
105
|
+
# ユーザに属性の型の入力を促す。
|
106
|
+
def input_attribute_type
|
107
|
+
prompt = _("Attribute type: ")
|
108
|
+
types = Attribute.types
|
109
|
+
Readline.completion_proc = Proc.new { |text|
|
110
|
+
begin
|
111
|
+
s = Readline.line_buffer.lstrip
|
112
|
+
rescue NotImplementedError, NoMethodError
|
113
|
+
s = text
|
114
|
+
end
|
115
|
+
types.select { |type|
|
116
|
+
type =~ /\A#{s}/
|
117
|
+
}
|
118
|
+
}
|
119
|
+
begin
|
120
|
+
if input = S7Cli.input_string(prompt)
|
121
|
+
input.strip!
|
122
|
+
if !types.include?(input)
|
123
|
+
raise ApplicationError, _("Unknown type: %s") % input
|
124
|
+
end
|
125
|
+
end
|
126
|
+
return input
|
127
|
+
rescue ApplicationError => e
|
128
|
+
puts(e.message)
|
129
|
+
puts(_("Try again."))
|
130
|
+
retry
|
131
|
+
ensure
|
132
|
+
begin
|
133
|
+
Readline.completion_proc = nil
|
134
|
+
rescue ArgumentError
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# ユーザにコマンドの入力を促す。終了コマンドを入力するまで繰り返す。
|
140
|
+
def command_loop
|
141
|
+
begin
|
142
|
+
loop do
|
143
|
+
begin
|
144
|
+
puts("-----")
|
145
|
+
@attributes.each.with_index do |attr, i|
|
146
|
+
value = attr.display_value
|
147
|
+
printf(" %2d) %s: %s\n", i, _(attr.name), value)
|
148
|
+
end
|
149
|
+
printf(" t) %s: %s\n", _("tags"), @tags.join(", "))
|
150
|
+
puts("-----")
|
151
|
+
prompt = _("Command (h:help, s:save, ^C:cancel): ")
|
152
|
+
input = S7Cli.input_string(prompt)
|
153
|
+
if input.nil?
|
154
|
+
raise Interrupt
|
155
|
+
end
|
156
|
+
begin
|
157
|
+
if !dispatch_command(input)
|
158
|
+
break
|
159
|
+
end
|
160
|
+
rescue Interrupt
|
161
|
+
raise Canceled
|
162
|
+
end
|
163
|
+
rescue ApplicationError => e
|
164
|
+
puts(e.message)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
rescue Interrupt
|
168
|
+
canceled
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# ユーザからの入力 input を解析し、適切なコマンドを処理する。
|
173
|
+
def dispatch_command(input)
|
174
|
+
case input
|
175
|
+
when /\A\s*(\d+)\z/, /\Ae\s*(\d+)\z/
|
176
|
+
index = $1.to_i
|
177
|
+
attr = @attributes[index]
|
178
|
+
if attr.nil?
|
179
|
+
raise ApplicationError, _("Invalid index: %d") % index
|
180
|
+
end
|
181
|
+
puts(_("Edit attribute: %s") % _(attr.name))
|
182
|
+
if attr.protected?
|
183
|
+
name = attr.name
|
184
|
+
secret = attr.secret?
|
185
|
+
else
|
186
|
+
name = input_attribute_name(attr.name)
|
187
|
+
if name.nil?
|
188
|
+
raise Canceled
|
189
|
+
end
|
190
|
+
secret = input_secret(attr.secret?)
|
191
|
+
end
|
192
|
+
if secret
|
193
|
+
prompt = _("Generate value? (no): ")
|
194
|
+
if S7Cli.input_boolean(prompt, false)
|
195
|
+
prompt = _("Length (8): ")
|
196
|
+
length = S7Cli.input_string(prompt).to_i
|
197
|
+
if length <= 0
|
198
|
+
length = 8
|
199
|
+
end
|
200
|
+
value = SecretGenerator.generate(length: length,
|
201
|
+
characters: [:alphabet, :number])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
if value.nil?
|
205
|
+
value =
|
206
|
+
input_value(attr.display_value(secret), secret, attr.protected?)
|
207
|
+
end
|
208
|
+
if value.nil?
|
209
|
+
raise Canceled
|
210
|
+
end
|
211
|
+
attr.name = name
|
212
|
+
attr.secret = secret
|
213
|
+
attr.value = value
|
214
|
+
puts(_("Updated '%s'.") % _(attr.name))
|
215
|
+
when "a"
|
216
|
+
puts(_("Add attribute."))
|
217
|
+
name = input_attribute_name
|
218
|
+
if name.nil?
|
219
|
+
raise Canceled
|
220
|
+
end
|
221
|
+
type = input_attribute_type
|
222
|
+
if type.nil?
|
223
|
+
raise Canceled
|
224
|
+
end
|
225
|
+
secret = input_secret(false)
|
226
|
+
if secret
|
227
|
+
prompt = _("Generate value? (no): ")
|
228
|
+
if S7Cli.input_boolean(prompt, false)
|
229
|
+
# TODO: pwgen コマンドを呼び出す生成機を選択できるようにする。
|
230
|
+
prompt = _("Length (8): ")
|
231
|
+
length = S7Cli.input_string(prompt).to_i
|
232
|
+
if length <= 0
|
233
|
+
length = 8
|
234
|
+
end
|
235
|
+
# TODO: 文字種も選べるようにする。
|
236
|
+
value = SecretGenerator.generate(length: length)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
if value.nil?
|
240
|
+
value = input_value("", secret, false)
|
241
|
+
end
|
242
|
+
if value.nil?
|
243
|
+
raise Canceled
|
244
|
+
end
|
245
|
+
options = {
|
246
|
+
"name" => name,
|
247
|
+
"secret" => secret,
|
248
|
+
"value" => value,
|
249
|
+
}
|
250
|
+
attr = Attribute.create_instance(type, options)
|
251
|
+
@attributes.push(attr)
|
252
|
+
puts(_("Added attribute: '%s'") % _(attr.name))
|
253
|
+
when /\Ad\s*(\d+)\z/
|
254
|
+
index = $1.to_i
|
255
|
+
attr = @attributes[index]
|
256
|
+
if attr.nil?
|
257
|
+
raise ApplicationError, _("Invalid index: %d") % index
|
258
|
+
end
|
259
|
+
if attr.protected?
|
260
|
+
msg = _("Can't delete attribute: %s") % _(attr.name)
|
261
|
+
raise ApplicationError, msg
|
262
|
+
end
|
263
|
+
prompt = _("Delete attribute '%s'? (no): ") % _(attr.name)
|
264
|
+
if !S7Cli.input_boolean(prompt)
|
265
|
+
raise Canceled
|
266
|
+
end
|
267
|
+
@attributes.delete(attr)
|
268
|
+
puts(_("Deleted attribute: '%s'") % _(attr.name))
|
269
|
+
when "t"
|
270
|
+
if input = input_tags(@tags)
|
271
|
+
@tags = input
|
272
|
+
else
|
273
|
+
raise Canceled
|
274
|
+
end
|
275
|
+
puts(_("Updated tags."))
|
276
|
+
when "s"
|
277
|
+
save
|
278
|
+
world.changed = true
|
279
|
+
puts(_("Saved: %d") % @entry.id)
|
280
|
+
return false
|
281
|
+
when "h"
|
282
|
+
puts(EDITING_USAGE)
|
283
|
+
else
|
284
|
+
raise ApplicationError, _("Unknown command: %s") % input
|
285
|
+
end
|
286
|
+
return true
|
287
|
+
end
|
288
|
+
|
289
|
+
# 機密情報に対する属性とタグの変更を反映させる。
|
290
|
+
def apply_entry_changes
|
291
|
+
@entry.tags.replace(@tags)
|
292
|
+
d = @entry.attributes.collect(&:name) - @attributes.collect(&:name)
|
293
|
+
if d.length > 0
|
294
|
+
@entry.attributes.delete_if { |attr|
|
295
|
+
attr.editable? && d.include?(attr.name)
|
296
|
+
}
|
297
|
+
end
|
298
|
+
@entry.add_attributes(@attributes)
|
299
|
+
end
|
300
|
+
|
301
|
+
# entry に指定した機密情報を表示する。
|
302
|
+
def display_entry(entry)
|
303
|
+
attr_names = entry.attributes.collect { |attr|
|
304
|
+
_(attr.name)
|
305
|
+
}
|
306
|
+
n = attr_names.collect(&:length).max
|
307
|
+
entry.attributes.each do |attr|
|
308
|
+
secret = attr.secret? && !config["show_secret"]
|
309
|
+
value = attr.display_value(secret)
|
310
|
+
puts("%-#{n}s: %s" % [_(attr.name), value])
|
311
|
+
end
|
312
|
+
puts("%-#{n}s: %s" % [_("tag"), entry.tags.join(", ")])
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# 機密情報を追加するコマンドを表現する。
|
317
|
+
class EntryAddCommand < Command
|
318
|
+
include EntryCommandUtils
|
319
|
+
|
320
|
+
command("add",
|
321
|
+
["entry:add",
|
322
|
+
"entry:new",
|
323
|
+
"entry:create",
|
324
|
+
"new",
|
325
|
+
"create",
|
326
|
+
"a"],
|
327
|
+
_("Add the entry."),
|
328
|
+
_("usage: add [NAME] [TAGS] [OPTIONS]"))
|
329
|
+
|
330
|
+
def initialize(*args)
|
331
|
+
super
|
332
|
+
@config["tags"] = []
|
333
|
+
@config["attributes"] = []
|
334
|
+
@options.push(StringOption.new("n", "name", _("Name.")),
|
335
|
+
ArrayOption.new("t", "tags", _("Tags.")),
|
336
|
+
IntOption.new("c", "copy", "ID",
|
337
|
+
_("Entry ID that is copied.")))
|
338
|
+
end
|
339
|
+
|
340
|
+
# 機密情報を追加する。
|
341
|
+
def save
|
342
|
+
apply_entry_changes
|
343
|
+
# TODO: undo スタックに操作を追加する。
|
344
|
+
world.entry_collection.add_entries(@entry)
|
345
|
+
end
|
346
|
+
|
347
|
+
# コマンドを実行する。
|
348
|
+
def run(argv)
|
349
|
+
option_parser.parse!(argv)
|
350
|
+
if argv.length > 0
|
351
|
+
config["name"] = argv.shift
|
352
|
+
end
|
353
|
+
if argv.length > 0
|
354
|
+
config["tags"].push(*argv.shift.split(","))
|
355
|
+
end
|
356
|
+
if config["copy"]
|
357
|
+
from_entry = world.entry_collection.find(config["copy"])
|
358
|
+
if !from_entry
|
359
|
+
raise NoSuchEntry.new("id" => config["copy"])
|
360
|
+
end
|
361
|
+
end
|
362
|
+
puts(_("Add the entry."))
|
363
|
+
@entry = Entry.new
|
364
|
+
if from_entry
|
365
|
+
@entry.add_attributes(from_entry.duplicate_attributes)
|
366
|
+
end
|
367
|
+
@attributes = @entry.attributes.select { |attr|
|
368
|
+
attr.editable?
|
369
|
+
}
|
370
|
+
attr = @entry.get_attribute("name")
|
371
|
+
if config["name"]
|
372
|
+
attr.value = config["name"]
|
373
|
+
end
|
374
|
+
if attr.value.to_s.empty?
|
375
|
+
puts(_("Edit attribute: %s") % _(attr.name))
|
376
|
+
attr.value =
|
377
|
+
input_value(attr.display_value, attr.secret?, attr.protected?)
|
378
|
+
end
|
379
|
+
if config["tags"].length > 0
|
380
|
+
@tags = config["tags"]
|
381
|
+
else
|
382
|
+
@tags = @entry.tags
|
383
|
+
end
|
384
|
+
if @tags.empty?
|
385
|
+
@tags = input_tags(@tags) || []
|
386
|
+
end
|
387
|
+
command_loop
|
388
|
+
return true
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# 機密情報を追加するコマンドを表現する。
|
393
|
+
class EntryEditCommand < Command
|
394
|
+
include EntryCommandUtils
|
395
|
+
|
396
|
+
command("edit",
|
397
|
+
["entry:edit",
|
398
|
+
"entry:update",
|
399
|
+
"update",
|
400
|
+
"e"],
|
401
|
+
_("Edit the entry."),
|
402
|
+
_("usage: edit <ID> [OPTIONS]"))
|
403
|
+
|
404
|
+
def initialize(*args)
|
405
|
+
super
|
406
|
+
@options.push(IntOption.new("i", "id", "ID", _("Entry ID.")))
|
407
|
+
end
|
408
|
+
|
409
|
+
# 機密情報を追加する。
|
410
|
+
def save
|
411
|
+
# TODO: undo スタックに操作を追加する。
|
412
|
+
apply_entry_changes
|
413
|
+
end
|
414
|
+
|
415
|
+
# コマンドを実行する。
|
416
|
+
def run(argv)
|
417
|
+
option_parser.parse!(argv)
|
418
|
+
if argv.length > 0
|
419
|
+
config["id"] = argv.shift.to_i
|
420
|
+
end
|
421
|
+
if config["id"].nil?
|
422
|
+
puts(_("Too few arguments."))
|
423
|
+
help
|
424
|
+
return true
|
425
|
+
end
|
426
|
+
@entry = world.entry_collection.find(config["id"])
|
427
|
+
if @entry.nil?
|
428
|
+
raise NoSuchEntry.new("id" => config["id"])
|
429
|
+
end
|
430
|
+
puts(_("Edit the entry."))
|
431
|
+
@attributes = @entry.duplicate_attributes
|
432
|
+
@tags = @entry.tags.dup
|
433
|
+
if @tags.empty?
|
434
|
+
@tags = input_tags(@tags) || []
|
435
|
+
end
|
436
|
+
command_loop
|
437
|
+
return true
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# 機密情報を検索するコマンドを表現する。
|
442
|
+
class EntryShowCommand < Command
|
443
|
+
include EntryCommandUtils
|
444
|
+
|
445
|
+
command("show",
|
446
|
+
["entry:show",
|
447
|
+
"s"],
|
448
|
+
_("Show the entry."),
|
449
|
+
_("usage: show <ID> [OPTIONS]"))
|
450
|
+
|
451
|
+
def initialize(*args)
|
452
|
+
super
|
453
|
+
@config["show_secret"] = false
|
454
|
+
@options.push(IntOption.new("id", _("Entry ID.")),
|
455
|
+
BoolOption.new("s", "show_secret",
|
456
|
+
_("Show value of secret attributes.")))
|
457
|
+
end
|
458
|
+
|
459
|
+
# コマンドを実行する。
|
460
|
+
def run(argv)
|
461
|
+
option_parser.parse!(argv)
|
462
|
+
if argv.length > 0
|
463
|
+
config["id"] = argv.shift.to_i
|
464
|
+
end
|
465
|
+
if config["id"].nil?
|
466
|
+
puts(_("Too few arguments."))
|
467
|
+
help
|
468
|
+
return true
|
469
|
+
end
|
470
|
+
entries = world.entry_collection.entries
|
471
|
+
entry = entries.find { |entry|
|
472
|
+
entry.id == config["id"]
|
473
|
+
}
|
474
|
+
if !entry
|
475
|
+
raise NoSuchEntry.new("id" => config["id"])
|
476
|
+
end
|
477
|
+
display_entry(entry)
|
478
|
+
if config["show_secret"]
|
479
|
+
# TODO: undo スタックに操作を追加する。
|
480
|
+
entry.rate += 1
|
481
|
+
puts(_("Changed rate to %d.") % entry.rate)
|
482
|
+
world.changed = true
|
483
|
+
end
|
484
|
+
return true
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# 機密情報を検索するコマンドを表現する。
|
489
|
+
class EntrySearchCommand < Command
|
490
|
+
include EntryCommandUtils
|
491
|
+
|
492
|
+
command("search",
|
493
|
+
["entry:search",
|
494
|
+
"entry:list",
|
495
|
+
"list",
|
496
|
+
"ls",
|
497
|
+
"l"],
|
498
|
+
_("Search the entry."),
|
499
|
+
_("usage: search [QUERY] [OPTIONS]"))
|
500
|
+
|
501
|
+
def initialize(*args)
|
502
|
+
super
|
503
|
+
@config["query"] = ""
|
504
|
+
@config["tags"] = []
|
505
|
+
@config["num"] = 10
|
506
|
+
@config["ignore-case"] = false
|
507
|
+
@options.push(StringOption.new("query", _("Query.")),
|
508
|
+
ArrayOption.new("tags", _("Tags.")),
|
509
|
+
IntOption.new("n", "num", _("Number of entries.")),
|
510
|
+
BoolOption.new("i", "ignore_case", _("Ignore case.")))
|
511
|
+
end
|
512
|
+
|
513
|
+
def search(entries, words, tags, ignore_case)
|
514
|
+
if words.empty?
|
515
|
+
matched_entries = entries
|
516
|
+
else
|
517
|
+
matched_entries = entries.select { |entry|
|
518
|
+
s = entry.attributes.collect { |attr|
|
519
|
+
attr.value.to_s
|
520
|
+
}.join("\n")
|
521
|
+
res = true
|
522
|
+
if ignore_case
|
523
|
+
s.downcase!
|
524
|
+
words.each(&:downcase!)
|
525
|
+
end
|
526
|
+
words.each do |word|
|
527
|
+
if !s.include?(word)
|
528
|
+
res = false
|
529
|
+
break
|
530
|
+
end
|
531
|
+
end
|
532
|
+
res
|
533
|
+
}
|
534
|
+
end
|
535
|
+
if tags.length > 0
|
536
|
+
matched_entries = matched_entries.select { |entry|
|
537
|
+
tags == (entry.tags & tags)
|
538
|
+
}
|
539
|
+
end
|
540
|
+
return matched_entries
|
541
|
+
end
|
542
|
+
|
543
|
+
# コマンドを実行する。
|
544
|
+
def run(argv)
|
545
|
+
option_parser.parse!(argv)
|
546
|
+
if argv.length > 0
|
547
|
+
config["query"].concat(" " + argv.join(" "))
|
548
|
+
end
|
549
|
+
words = config["query"].split
|
550
|
+
entries = search(world.entry_collection.entries,
|
551
|
+
words, config["tags"], config["ignore_case"])
|
552
|
+
if entries.empty?
|
553
|
+
puts(_("No entry."))
|
554
|
+
else
|
555
|
+
sort_key = :id
|
556
|
+
page_no = 1
|
557
|
+
per_page = ENV["LINES"].to_i
|
558
|
+
if per_page <= 3
|
559
|
+
per_page = 25 - 3
|
560
|
+
else
|
561
|
+
per_page -= 3
|
562
|
+
end
|
563
|
+
current_mode = :next
|
564
|
+
|
565
|
+
begin
|
566
|
+
loop do
|
567
|
+
begin
|
568
|
+
if words.length > 0
|
569
|
+
print(_("Search result of '%s', %d entries.") %
|
570
|
+
[words.join(" "), entries.length])
|
571
|
+
else
|
572
|
+
print(_("List of %d entries.") % entries.length)
|
573
|
+
end
|
574
|
+
num_pages = (entries.length.to_f / per_page).ceil
|
575
|
+
puts(_(" [page %d/%d]") % [page_no, num_pages])
|
576
|
+
|
577
|
+
list_entries(entries, sort_key, page_no, per_page)
|
578
|
+
|
579
|
+
if num_pages == 1
|
580
|
+
raise Interrupt
|
581
|
+
end
|
582
|
+
|
583
|
+
prompt = _("Command (p:prev, n:next, s:sort, q:quit): ")
|
584
|
+
input = S7Cli.input_string(prompt)
|
585
|
+
if input.nil?
|
586
|
+
raise Interrupt
|
587
|
+
end
|
588
|
+
begin
|
589
|
+
case input
|
590
|
+
when /\A(n|next)\z/i
|
591
|
+
page_no += 1
|
592
|
+
current_mode = :next
|
593
|
+
when /\A(p|prev)\z/i
|
594
|
+
page_no -= 1
|
595
|
+
current_mode = :prev
|
596
|
+
when /\A(s|sort)\z/i
|
597
|
+
sort_key = next_sort_key(sort_key)
|
598
|
+
when /\A(q|quit)/i
|
599
|
+
break
|
600
|
+
else
|
601
|
+
if current_mode == :next
|
602
|
+
page_no += 1
|
603
|
+
else
|
604
|
+
page_no -= 1
|
605
|
+
end
|
606
|
+
end
|
607
|
+
if page_no < 1
|
608
|
+
page_no = 1
|
609
|
+
elsif page_no >= num_pages
|
610
|
+
page_no = num_pages
|
611
|
+
end
|
612
|
+
end
|
613
|
+
rescue ApplicationError => e
|
614
|
+
puts(e.message)
|
615
|
+
end
|
616
|
+
end
|
617
|
+
rescue Interrupt
|
618
|
+
puts("\n")
|
619
|
+
end
|
620
|
+
end
|
621
|
+
return true
|
622
|
+
end
|
623
|
+
|
624
|
+
# ソート対象の属性の配列。
|
625
|
+
SORT_KEYS = [:id, :name, :tags, :rate]
|
626
|
+
|
627
|
+
def next_sort_key(current)
|
628
|
+
return SORT_KEYS[(SORT_KEYS.index(current) + 1) % SORT_KEYS.length]
|
629
|
+
end
|
630
|
+
|
631
|
+
# 指定した文字列 str の表示幅が width よりも長い場合は切り詰める。
|
632
|
+
# width よりも短い場合、left が true だと右部分にスペースをつめ、
|
633
|
+
# left が false だと、左部分にスペースをつめる。
|
634
|
+
def truncate_or_add_space(str, width, left = true)
|
635
|
+
len = 0
|
636
|
+
truncate_index = nil
|
637
|
+
truncate_with_space = false
|
638
|
+
str.chars.each_with_index do |chr, i|
|
639
|
+
case UnicodeData.east_asian_width(chr)
|
640
|
+
when :H, :N
|
641
|
+
n = 1
|
642
|
+
else
|
643
|
+
n = 2
|
644
|
+
end
|
645
|
+
len += n
|
646
|
+
if truncate_index.nil? && len >= width
|
647
|
+
truncate_index = i
|
648
|
+
if len > width
|
649
|
+
truncate_with_space = true
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
if len < width
|
654
|
+
space = " " * (width - len)
|
655
|
+
if left
|
656
|
+
res = str + space
|
657
|
+
else
|
658
|
+
res = space + str
|
659
|
+
end
|
660
|
+
elsif len == width
|
661
|
+
res = str
|
662
|
+
else
|
663
|
+
res = str[0..truncate_index]
|
664
|
+
if truncate_with_space
|
665
|
+
res[-1] = " "
|
666
|
+
end
|
667
|
+
end
|
668
|
+
return res
|
669
|
+
end
|
670
|
+
|
671
|
+
def list_entries(entries, sort_key, page_no, per_page)
|
672
|
+
sort_marks = [" ", " ", " ", " "]
|
673
|
+
sort_marks[SORT_KEYS.index(sort_key)] = ">"
|
674
|
+
puts(_("%sID |%sName |%sTags(s) |%sRate") % sort_marks)
|
675
|
+
sorted_entries = entries.sort_by { |e| e.send(sort_key) }
|
676
|
+
sorted_entries[((page_no - 1) * per_page), per_page].each do |entry|
|
677
|
+
puts([truncate_or_add_space(entry.id.to_s, 4, false),
|
678
|
+
truncate_or_add_space(entry.name, 49),
|
679
|
+
truncate_or_add_space(entry.tags.join(","), 20),
|
680
|
+
truncate_or_add_space(entry.rate.to_s, 5, false)].join("|"))
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
# 機密情報を削除するコマンドを表現する。
|
686
|
+
class EntryDeleteCommand < Command
|
687
|
+
include EntryCommandUtils
|
688
|
+
|
689
|
+
command("delete",
|
690
|
+
["entry:delete",
|
691
|
+
"entry:destroy",
|
692
|
+
"entry:remove",
|
693
|
+
"destroy",
|
694
|
+
"remove",
|
695
|
+
"del",
|
696
|
+
"rm",
|
697
|
+
"d"],
|
698
|
+
_("Delete the entry."),
|
699
|
+
_("usage: delete <ID>"))
|
700
|
+
|
701
|
+
# コマンドを実行する。
|
702
|
+
def run(argv)
|
703
|
+
if argv.length > 0
|
704
|
+
id = argv.shift.to_i
|
705
|
+
end
|
706
|
+
if id.nil?
|
707
|
+
puts(_("Too few arguments."))
|
708
|
+
help
|
709
|
+
return true
|
710
|
+
end
|
711
|
+
entry = world.entry_collection.find(id)
|
712
|
+
if entry.nil?
|
713
|
+
raise NoSuchEntry.new("id" => id)
|
714
|
+
end
|
715
|
+
display_entry(entry)
|
716
|
+
puts("-----")
|
717
|
+
if S7Cli.input_boolean(_("Delete? (no): "))
|
718
|
+
# TODO: undo スタックに操作を追加。
|
719
|
+
world.entry_collection.delete_entries(entry)
|
720
|
+
puts(_("Deleted: %d") % entry.id)
|
721
|
+
else
|
722
|
+
canceled
|
723
|
+
end
|
724
|
+
return true
|
725
|
+
end
|
726
|
+
end
|
727
|
+
end
|
728
|
+
end
|