textrepo 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +11 -0
- data/Rakefile +0 -1
- data/lib/textrepo/error.rb +57 -14
- data/lib/textrepo/file_system_repository.rb +246 -26
- data/lib/textrepo/repository.rb +91 -8
- data/lib/textrepo/timestamp.rb +60 -18
- data/lib/textrepo/version.rb +1 -1
- metadata +2 -3
- data/Gemfile.lock +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b504229bd3ea2416e0a85cb8903fe0903b6f045a8a2549857a2b1b296f35e32a
|
4
|
+
data.tar.gz: baaf7776505e0c430d9c540d88e78847eebc406612b5f9c2cb11ba20c5f3371d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb90a5d324536e0c2ba8f87c93923ba19bae2c835f31445f9d0f3701110f37598f44a638b496ac992dab30ea03f82643591ea807623a93bd0afdfa52505595ac
|
7
|
+
data.tar.gz: 1affcd1ff8cce4b3374610791ba733d5d5c8fd1d8b90adb5030725752c84c0f671d4ee9b6ca7f4539ebc2171baf1d2c92b9eb530dc84eed14d477df98729154e
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
## [Unreleased]
|
8
8
|
Nothing to record here.
|
9
9
|
|
10
|
+
## [0.5.1] - 2020-11-02
|
11
|
+
### Changed
|
12
|
+
- Fix issue #28.
|
13
|
+
- Modify `Repository#update` to do nothing when the given text is
|
14
|
+
identical to the one in the repository.
|
15
|
+
|
16
|
+
## [0.5.0] - 2020-11-01
|
17
|
+
### Added
|
18
|
+
- Add a new API `Repository#search`.
|
19
|
+
- Add a new API `Repository#exist?`. (0.4.3)
|
20
|
+
|
10
21
|
## [0.4.0] - 2020-10-14
|
11
22
|
### Added
|
12
23
|
- Released to rubygems.org.
|
data/Rakefile
CHANGED
data/lib/textrepo/error.rb
CHANGED
@@ -1,12 +1,39 @@
|
|
1
1
|
module Textrepo
|
2
|
+
|
3
|
+
##
|
4
|
+
# Following errors might occur in repository operations:
|
5
|
+
# +---------------------------------+-----------------------+
|
6
|
+
# | operation (args) | error type |
|
7
|
+
# +---------------------------------+-----------------------+
|
8
|
+
# | create (timestamp, text) | Duplicate timestamp |
|
9
|
+
# | | Empty text |
|
10
|
+
# +---------------------------------+-----------------------+
|
11
|
+
# | read (timestamp) | Missing timestamp |
|
12
|
+
# +---------------------------------+-----------------------+
|
13
|
+
# | update (timestamp, text) | Mssing timestamp |
|
14
|
+
# | | Empty text |
|
15
|
+
# +---------------------------------+-----------------------+
|
16
|
+
# | delete (timestamp) | Missing timestamp |
|
17
|
+
# +---------------------------------+-----------------------+
|
18
|
+
# | search (pattern, stamp_pattern) | Invalid search result |
|
19
|
+
# +---------------------------------+-----------------------+
|
20
|
+
|
2
21
|
class Error < StandardError; end
|
3
22
|
|
23
|
+
# :stopdoc:
|
4
24
|
module ErrMsg
|
5
25
|
UNKNOWN_REPO_TYPE = 'unknown type for repository: %s'
|
6
26
|
DUPLICATE_TIMESTAMP = 'duplicate timestamp: %s'
|
7
27
|
EMPTY_TEXT = 'empty text'
|
8
28
|
MISSING_TIMESTAMP = 'missing timestamp: %s'
|
29
|
+
INVALID_TIMESTAMP_STRING = "invalid string as timestamp: %s"
|
30
|
+
INVALID_SEARCH_RESULT = "invalid result by searcher: %s"
|
9
31
|
end
|
32
|
+
# :startdoc:
|
33
|
+
|
34
|
+
##
|
35
|
+
# An error raised if unknown type was specified as the repository
|
36
|
+
# type.
|
10
37
|
|
11
38
|
class UnknownRepoTypeError < Error
|
12
39
|
def initialize(type)
|
@@ -14,20 +41,9 @@ module Textrepo
|
|
14
41
|
end
|
15
42
|
end
|
16
43
|
|
17
|
-
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# +--------------------------+---------------------+
|
21
|
-
# | create (timestamp, text) | Duplicate timestamp |
|
22
|
-
# | | Empty text |
|
23
|
-
# +--------------------------+---------------------+
|
24
|
-
# | read (timestamp) | Missing timestamp |
|
25
|
-
# +--------------------------+---------------------+
|
26
|
-
# | update (timestamp, text) | Mssing timestamp |
|
27
|
-
# | | Empty text |
|
28
|
-
# +--------------------------+---------------------+
|
29
|
-
# | delete (timestamp) | Missing timestamp |
|
30
|
-
# +--------------------------+---------------------+
|
44
|
+
##
|
45
|
+
# An error raised if the specified timestamp has already exist in
|
46
|
+
# the repository.
|
31
47
|
|
32
48
|
class DuplicateTimestampError < Error
|
33
49
|
def initialize(timestamp)
|
@@ -35,16 +51,43 @@ module Textrepo
|
|
35
51
|
end
|
36
52
|
end
|
37
53
|
|
54
|
+
##
|
55
|
+
# An error raised if the given text is empty.
|
56
|
+
|
38
57
|
class EmptyTextError < Error
|
39
58
|
def initialize
|
40
59
|
super(ErrMsg::EMPTY_TEXT)
|
41
60
|
end
|
42
61
|
end
|
43
62
|
|
63
|
+
##
|
64
|
+
# An error raised if the given timestamp has not exist in the
|
65
|
+
# repository.
|
66
|
+
|
44
67
|
class MissingTimestampError < Error
|
45
68
|
def initialize(timestamp)
|
46
69
|
super(ErrMsg::MISSING_TIMESTAMP % timestamp)
|
47
70
|
end
|
48
71
|
end
|
49
72
|
|
73
|
+
##
|
74
|
+
# An error raised if an argument is invalid to convert a
|
75
|
+
# Textrepo::Timestamp object.
|
76
|
+
|
77
|
+
class InvalidTimestampStringError < Error
|
78
|
+
def initialize(str)
|
79
|
+
super(ErrMsg::INVALID_TIMESTAMP_STRING % str)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# An error raise if the search result is not suitable to use.
|
85
|
+
#
|
86
|
+
|
87
|
+
class InvalidSearchResultError < Error
|
88
|
+
def initialize(str)
|
89
|
+
super(ErrMsg::INVALID_SEARCH_RESULT % str)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
50
93
|
end
|
@@ -1,29 +1,52 @@
|
|
1
1
|
require 'fileutils'
|
2
|
+
require "open3"
|
2
3
|
|
3
4
|
module Textrepo
|
5
|
+
|
4
6
|
##
|
5
7
|
# A concrete class which implements Repository interfaces. This
|
6
8
|
# repository uses the default file system of the operating system as
|
7
9
|
# a text storage.
|
10
|
+
|
8
11
|
class FileSystemRepository < Repository
|
12
|
+
|
9
13
|
##
|
10
14
|
# Repository root.
|
15
|
+
|
11
16
|
attr_reader :path
|
12
17
|
|
13
18
|
##
|
14
19
|
# Extension of notes sotres in the repository.
|
20
|
+
|
15
21
|
attr_reader :extname
|
16
22
|
|
23
|
+
##
|
24
|
+
# Searcher program name.
|
25
|
+
|
26
|
+
attr_reader :searcher
|
27
|
+
|
28
|
+
##
|
29
|
+
# An array of options to pass to the searcher program.
|
30
|
+
|
31
|
+
attr_reader :searcher_options
|
32
|
+
|
17
33
|
##
|
18
34
|
# Default name for the repository which uses when no name is
|
19
35
|
# specified in the configuration settings.
|
36
|
+
|
20
37
|
FAVORITE_REPOSITORY_NAME = 'notes'
|
21
38
|
|
22
39
|
##
|
23
40
|
# Default extension of notes which uses when no extname is
|
24
41
|
# specified in the configuration settings.
|
42
|
+
|
25
43
|
FAVORITE_EXTNAME = 'md'
|
26
44
|
|
45
|
+
##
|
46
|
+
# Default searcher program to search text in the repository.
|
47
|
+
|
48
|
+
FAVORITE_SEARCHER = 'grep'
|
49
|
+
|
27
50
|
##
|
28
51
|
# Creates a new repository object. The argument, `conf` must be a
|
29
52
|
# Hash object. It should hold the follwoing values:
|
@@ -34,13 +57,33 @@ module Textrepo
|
|
34
57
|
# - OPTIONAL: (if not specified, default values are used)
|
35
58
|
# - :repository_name => basename of the root path for the repository
|
36
59
|
# - :default_extname => extname for a file stored into in the repository
|
60
|
+
# - :searcher => a program to search like `grep`
|
61
|
+
# - :searcher_options => an Array of option to pass to the searcher
|
37
62
|
#
|
38
63
|
# The root path of the repository looks like the following:
|
39
64
|
# - conf[:repository_base]/conf[:repository_name]
|
40
65
|
#
|
41
|
-
# Default values are set when
|
66
|
+
# Default values are set when `:repository_name` and `:default_extname`
|
42
67
|
# were not defined in `conf`.
|
43
68
|
#
|
69
|
+
# Be careful to set `:searcher_options`, it must be to specify the
|
70
|
+
# searcher behavior equivalent to `grep` with "-inR". The default
|
71
|
+
# value for the searcher options is defined for BSD grep (default
|
72
|
+
# grep on macOS), GNU grep, and ripgrep (aka rg). They are:
|
73
|
+
#
|
74
|
+
# "grep" => ["-i", "-n", "-R", "-E"]
|
75
|
+
# "egrep" => ["-i", "-n", "-R"]
|
76
|
+
# "ggrep" => ["-i", "-n", "-R", "-E"]
|
77
|
+
# "gegrep" => ["-i", "-n", "-R"]
|
78
|
+
# "rg" => ["-S", "-n", "--no-heading", "--color", "never"]
|
79
|
+
#
|
80
|
+
# If use those 3 searchers, it is not recommended to set
|
81
|
+
# `:searcher_options`. The default value works well in
|
82
|
+
# `textrepo`.
|
83
|
+
#
|
84
|
+
# :call-seq:
|
85
|
+
# new(Hash or Hash like object) -> FileSystemRepository
|
86
|
+
|
44
87
|
def initialize(conf)
|
45
88
|
super
|
46
89
|
base = conf[:repository_base]
|
@@ -48,6 +91,8 @@ module Textrepo
|
|
48
91
|
@path = File.expand_path("#{name}", base)
|
49
92
|
FileUtils.mkdir_p(@path)
|
50
93
|
@extname = conf[:default_extname] || FAVORITE_EXTNAME
|
94
|
+
@searcher = find_searcher(conf[:searcher])
|
95
|
+
@searcher_options = conf[:searcher_options]
|
51
96
|
end
|
52
97
|
|
53
98
|
##
|
@@ -55,8 +100,8 @@ module Textrepo
|
|
55
100
|
# text and is associated to the timestamp.
|
56
101
|
#
|
57
102
|
# :call-seq:
|
58
|
-
# create(Timestamp, Array)
|
59
|
-
|
103
|
+
# create(Timestamp, Array) -> Timestamp
|
104
|
+
|
60
105
|
def create(timestamp, text)
|
61
106
|
abs = abspath(timestamp)
|
62
107
|
raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
|
@@ -71,8 +116,8 @@ module Textrepo
|
|
71
116
|
# content.
|
72
117
|
#
|
73
118
|
# :call-seq:
|
74
|
-
# read(Timestamp)
|
75
|
-
|
119
|
+
# read(Timestamp) -> Array
|
120
|
+
|
76
121
|
def read(timestamp)
|
77
122
|
abs = abspath(timestamp)
|
78
123
|
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
@@ -87,21 +132,25 @@ module Textrepo
|
|
87
132
|
# Updates the file content in the repository. A new timestamp
|
88
133
|
# will be attached to the text.
|
89
134
|
#
|
90
|
-
#
|
91
|
-
#
|
135
|
+
# See the documentation of Repository#update to know about errors
|
136
|
+
# and constraints of this method.
|
92
137
|
#
|
138
|
+
# :call-seq:
|
139
|
+
# update(Timestamp, Array) -> Timestamp
|
140
|
+
|
93
141
|
def update(timestamp, text)
|
94
142
|
raise EmptyTextError if text.empty?
|
95
|
-
|
96
|
-
|
143
|
+
raise MissingTimestampError, timestamp unless exist?(timestamp)
|
144
|
+
|
145
|
+
# does nothing if given text is the same in the repository one
|
146
|
+
return timestamp if read(timestamp) == text
|
97
147
|
|
98
148
|
# the text must be stored with the new timestamp
|
99
149
|
new_stamp = Timestamp.new(Time.now)
|
100
|
-
|
101
|
-
write_text(new_abs, text)
|
150
|
+
write_text(abspath(new_stamp), text)
|
102
151
|
|
103
|
-
# delete the original file in the repository
|
104
|
-
FileUtils.remove_file(
|
152
|
+
# delete the original text file in the repository
|
153
|
+
FileUtils.remove_file(abspath(timestamp))
|
105
154
|
|
106
155
|
new_stamp
|
107
156
|
end
|
@@ -110,8 +159,8 @@ module Textrepo
|
|
110
159
|
# Deletes the file in the repository.
|
111
160
|
#
|
112
161
|
# :call-seq:
|
113
|
-
# delete(Timestamp)
|
114
|
-
|
162
|
+
# delete(Timestamp) -> Array
|
163
|
+
|
115
164
|
def delete(timestamp)
|
116
165
|
abs = abspath(timestamp)
|
117
166
|
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
@@ -126,8 +175,8 @@ module Textrepo
|
|
126
175
|
# Finds entries of text those timestamp matches the specified pattern.
|
127
176
|
#
|
128
177
|
# :call-seq:
|
129
|
-
# entries(String = nil)
|
130
|
-
|
178
|
+
# entries(String = nil) -> Array of Timestamp instances
|
179
|
+
|
131
180
|
def entries(stamp_pattern = nil)
|
132
181
|
results = []
|
133
182
|
|
@@ -135,7 +184,7 @@ module Textrepo
|
|
135
184
|
when "yyyymoddhhmiss_lll".size
|
136
185
|
stamp = Timestamp.parse_s(stamp_pattern)
|
137
186
|
if exist?(stamp)
|
138
|
-
results << stamp
|
187
|
+
results << stamp
|
139
188
|
end
|
140
189
|
when 0, "yyyymoddhhmiss".size, "yyyymodd".size
|
141
190
|
results += find_entries(stamp_pattern)
|
@@ -158,19 +207,49 @@ module Textrepo
|
|
158
207
|
results
|
159
208
|
end
|
160
209
|
|
210
|
+
##
|
211
|
+
# Check the existence of text which is associated with the given
|
212
|
+
# timestamp.
|
213
|
+
#
|
214
|
+
# :call-seq:
|
215
|
+
# exist?(Timestamp) -> true or false
|
216
|
+
|
217
|
+
def exist?(timestamp)
|
218
|
+
FileTest.exist?(abspath(timestamp))
|
219
|
+
end
|
220
|
+
|
221
|
+
##
|
222
|
+
# Searches a pattern in all text. The given pattern is a word to
|
223
|
+
# search or a regular expression. The pattern would be passed to
|
224
|
+
# a searcher program as it passed.
|
225
|
+
#
|
226
|
+
# See the document for Textrepo::Repository#search to know about
|
227
|
+
# the search result.
|
228
|
+
#
|
229
|
+
# :call-seq:
|
230
|
+
# search(String for pattern, String for Timestamp pattern) -> Array
|
231
|
+
|
232
|
+
def search(pattern, stamp_pattern = nil)
|
233
|
+
result = nil
|
234
|
+
if stamp_pattern.nil?
|
235
|
+
result = invoke_searcher_at_repo_root(@searcher, pattern)
|
236
|
+
else
|
237
|
+
result = invoke_searcher_for_entries(@searcher, pattern, entries(stamp_pattern))
|
238
|
+
end
|
239
|
+
construct_search_result(result)
|
240
|
+
end
|
241
|
+
|
161
242
|
# :stopdoc:
|
243
|
+
|
162
244
|
private
|
163
245
|
def abspath(timestamp)
|
164
246
|
filename = timestamp_to_pathname(timestamp) + ".#{@extname}"
|
165
247
|
File.expand_path(filename, @path)
|
166
248
|
end
|
167
249
|
|
168
|
-
##
|
169
|
-
# ```
|
170
250
|
# %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
|
171
251
|
# "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
|
172
252
|
# "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
|
173
|
-
# ```
|
174
253
|
def timestamp_to_pathname(timestamp)
|
175
254
|
yyyy, mo = Timestamp.split_stamp(timestamp.to_s)[0..1]
|
176
255
|
File.join(yyyy, mo, timestamp.to_s)
|
@@ -187,15 +266,156 @@ module Textrepo
|
|
187
266
|
File.basename(text_path).delete_suffix(".#{@extname}")
|
188
267
|
end
|
189
268
|
|
190
|
-
def exist?(timestamp)
|
191
|
-
FileTest.exist?(abspath(timestamp))
|
192
|
-
end
|
193
|
-
|
194
269
|
def find_entries(stamp_pattern)
|
195
270
|
Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
|
196
|
-
|
271
|
+
begin
|
272
|
+
Timestamp.parse_s(timestamp_str(e))
|
273
|
+
rescue InvalidTimestampStringError => _
|
274
|
+
# Just ignore the erroneous entry, since it is not a text in
|
275
|
+
# the repository. It may be a garbage, or some kind of
|
276
|
+
# hidden stuff of the repository, ... etc.
|
277
|
+
nil
|
278
|
+
end
|
279
|
+
}.compact
|
280
|
+
end
|
281
|
+
|
282
|
+
##
|
283
|
+
# The upper limit of files to search at one time. The value has
|
284
|
+
# no reason to select. It seems to me that not too much, not too
|
285
|
+
# little to handle in one process to search.
|
286
|
+
|
287
|
+
LIMIT_OF_FILES = 20
|
288
|
+
|
289
|
+
##
|
290
|
+
# When no timestamp pattern was given, invoke the searcher with
|
291
|
+
# the repository root path as its argument and the recursive
|
292
|
+
# searching option. The search could be done in only one process.
|
293
|
+
|
294
|
+
def invoke_searcher_at_repo_root(searcher, pattern)
|
295
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
296
|
+
pattern, @path)
|
297
|
+
output = []
|
298
|
+
output += o.lines.map(&:chomp) if s.success? && (! o.empty?)
|
299
|
+
output
|
300
|
+
end
|
301
|
+
|
302
|
+
##
|
303
|
+
# When a timestamp pattern was given, at first, list target files,
|
304
|
+
# then invoke the searcher for those files. Since the number of
|
305
|
+
# target files may be so much, it seems to be dangerous to pass
|
306
|
+
# all of them to a single search process at one time.
|
307
|
+
#
|
308
|
+
# One more thing to mention, the searcher, like `grep`, does not
|
309
|
+
# add the filename at the beginning of the search result line, if
|
310
|
+
# the target is one file. This behavior is not suitable in this
|
311
|
+
# purpose. The code below adds the filename when the target is
|
312
|
+
# one file.
|
313
|
+
|
314
|
+
def invoke_searcher_for_entries(searcher, pattern, entries)
|
315
|
+
output = []
|
316
|
+
|
317
|
+
num_of_entries = entries.size
|
318
|
+
if num_of_entries == 1
|
319
|
+
# If the search taget is one file, the output needs special
|
320
|
+
# treatment.
|
321
|
+
file = abspath(entries[0])
|
322
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
323
|
+
pattern, file)
|
324
|
+
if s.success? && (! o.empty)
|
325
|
+
output += o.lines.map { |line|
|
326
|
+
# add filename at the beginning of the search result line
|
327
|
+
[file, line.chomp].join(":")
|
328
|
+
}
|
329
|
+
end
|
330
|
+
elsif num_of_entries > LIMIT_OF_FILES
|
331
|
+
output += invoke_searcher_for_entries(searcher, pattern, entries[0..(LIMIT_OF_FILES - 1)])
|
332
|
+
output += invoke_searcher_for_entries(searcher, pattern, entries[LIMIT_OF_FILES..-1])
|
333
|
+
else
|
334
|
+
# When the number of target is less than the upper limit,
|
335
|
+
# invoke the searcher with all of target files as its
|
336
|
+
# arguments.
|
337
|
+
files = find_files(entries)
|
338
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
339
|
+
pattern, *files)
|
340
|
+
if s.success? && (! o.empty)
|
341
|
+
output += o.lines.map(&:chomp)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
output
|
346
|
+
end
|
347
|
+
|
348
|
+
SEARCHER_OPTS = {
|
349
|
+
# case insensitive, print line number, recursive search, work as egrep
|
350
|
+
"grep" => ["-i", "-n", "-R", "-E"],
|
351
|
+
# case insensitive, print line number, recursive search
|
352
|
+
"egrep" => ["-i", "-n", "-R"],
|
353
|
+
# case insensitive, print line number, recursive search, work as gegrep
|
354
|
+
"ggrep" => ["-i", "-n", "-R", "-E"],
|
355
|
+
# case insensitive, print line number, recursive search
|
356
|
+
"gegrep" => ["-i", "-n", "-R"],
|
357
|
+
# smart case, print line number, no color
|
358
|
+
"rg" => ["-S", "-n", "--no-heading", "--color", "never"],
|
359
|
+
}
|
360
|
+
|
361
|
+
def find_searcher_options(searcher)
|
362
|
+
@searcher_options || SEARCHER_OPTS[File.basename(searcher)] || ""
|
363
|
+
end
|
364
|
+
|
365
|
+
def find_files(timestamps)
|
366
|
+
timestamps.map{|stamp| abspath(stamp)}
|
367
|
+
end
|
368
|
+
|
369
|
+
##
|
370
|
+
# The argument must be an Array contains the searcher output.
|
371
|
+
# Each item is constructed from 3 parts:
|
372
|
+
# "<pathname>:<integer>:<text>"
|
373
|
+
#
|
374
|
+
# For example, it may looks like:
|
375
|
+
#
|
376
|
+
# "/somewhere/2020/11/20201101044300.md:18:foo is foo"
|
377
|
+
#
|
378
|
+
# Or it may contains more ":" in the text part as:
|
379
|
+
#
|
380
|
+
# "/somewhere/2020/11/20201101044500.md:119:apple:orange:grape"
|
381
|
+
#
|
382
|
+
# In the latter case, `split(":")` will split it too much. That is,
|
383
|
+
# the result will be:
|
384
|
+
#
|
385
|
+
# ["/somewhere/2020/11/20201101044500.md", "119", "apple", "orange", "grape"]
|
386
|
+
#
|
387
|
+
# Text part must be joined with ":".
|
388
|
+
|
389
|
+
def construct_search_result(output)
|
390
|
+
output.map { |line|
|
391
|
+
begin
|
392
|
+
pathname, num, *match_text = line.split(":")
|
393
|
+
[Timestamp.parse_s(timestamp_str(pathname)),
|
394
|
+
num.to_i,
|
395
|
+
match_text.join(":")]
|
396
|
+
rescue InvalidTimestampStringError, TypeError => _
|
397
|
+
raise InvalidSearchResultError, [@searcher, @searcher_options.join(" ")].join(" ")
|
398
|
+
end
|
399
|
+
}.compact
|
400
|
+
end
|
401
|
+
|
402
|
+
def find_searcher(program = nil)
|
403
|
+
candidates = [FAVORITE_SEARCHER]
|
404
|
+
candidates.unshift(program) unless program.nil? || candidates.include?(program)
|
405
|
+
search_paths = ENV["PATH"].split(":")
|
406
|
+
candidates.map { |prog|
|
407
|
+
find_in_paths(prog, search_paths)
|
408
|
+
}[0]
|
409
|
+
end
|
410
|
+
|
411
|
+
def find_in_paths(prog, paths)
|
412
|
+
paths.each { |p|
|
413
|
+
abspath = File.expand_path(prog, p)
|
414
|
+
return abspath if FileTest.exist?(abspath) && FileTest.executable?(abspath)
|
197
415
|
}
|
416
|
+
nil
|
198
417
|
end
|
418
|
+
# :startdoc:
|
199
419
|
|
200
420
|
end
|
201
421
|
end
|
data/lib/textrepo/repository.rb
CHANGED
@@ -1,31 +1,81 @@
|
|
1
1
|
module Textrepo
|
2
2
|
class Repository
|
3
|
-
|
3
|
+
|
4
|
+
##
|
5
|
+
# Repository type. It specifies which concrete repository class
|
6
|
+
# will instantiated. For example, the type `:file_system` specifies
|
7
|
+
# `FileSystemRepository`.
|
8
|
+
|
9
|
+
attr_reader :type
|
10
|
+
|
11
|
+
##
|
12
|
+
# Repository name. The usage of the value of `name` depends on a
|
13
|
+
# concrete repository class. For example, `FileSystemRepository`
|
14
|
+
# uses it as a part of the repository path.
|
15
|
+
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
##
|
19
|
+
# Create a new repository. The argument must be an object which
|
20
|
+
# can be accessed like a Hash object.
|
4
21
|
|
5
22
|
def initialize(conf)
|
6
23
|
@type = conf[:repository_type]
|
7
24
|
@name = conf[:repository_name]
|
8
25
|
end
|
9
26
|
|
27
|
+
##
|
10
28
|
# Stores text data into the repository with the specified timestamp.
|
11
29
|
# Returns the timestamp.
|
30
|
+
#
|
31
|
+
# :call-seq:
|
32
|
+
# create(Timestamp, Array) -> Timestamp
|
33
|
+
|
12
34
|
def create(timestamp, text); timestamp; end
|
13
35
|
|
36
|
+
##
|
14
37
|
# Reads text data from the repository, which is associated to the
|
15
38
|
# timestamp. Returns an array which contains the text.
|
39
|
+
#
|
40
|
+
# :call-seq:
|
41
|
+
# read(Timestamp) -> Array
|
42
|
+
|
16
43
|
def read(timestamp); []; end
|
17
44
|
|
18
|
-
|
19
|
-
#
|
45
|
+
##
|
46
|
+
# Updates the content with given text in the repository, which is
|
47
|
+
# associated to the given timestamp. Returns the timestamp newly
|
48
|
+
# generated during the execution.
|
49
|
+
#
|
50
|
+
# If the given Timestamp is not existed as a Timestamp attached to
|
51
|
+
# text in the repository, raises MissingTimestampError.
|
52
|
+
#
|
53
|
+
# If the given text is empty, raises EmptyTextError.
|
54
|
+
#
|
55
|
+
# If the given text is identical to the text in the repository,
|
56
|
+
# does nothing. Returns the given timestamp itself.
|
57
|
+
#
|
58
|
+
# :call-seq:
|
59
|
+
# update(Timestamp, Array) -> Timestamp
|
60
|
+
|
20
61
|
def update(timestamp, text); timestamp; end
|
21
62
|
|
63
|
+
##
|
22
64
|
# Deletes the content in the repository, which is associated to
|
23
65
|
# the timestamp. Returns an array which contains the deleted text.
|
66
|
+
#
|
67
|
+
# :call-seq:
|
68
|
+
# delete(Timestamp) -> Array
|
69
|
+
|
24
70
|
def delete(timestamp); []; end
|
25
71
|
|
72
|
+
##
|
26
73
|
# Finds all entries of text those have timestamps which mathes the
|
27
74
|
# specified pattern of timestamp. Returns an array which contains
|
28
|
-
#
|
75
|
+
# instances of Timestamp. If none of text was found, an empty
|
76
|
+
# array would be returned.
|
77
|
+
#
|
78
|
+
# A pattern must be one of the following:
|
29
79
|
#
|
30
80
|
# - yyyymoddhhmiss_lll : whole stamp
|
31
81
|
# - yyyymoddhhmiss : omit millisecond part
|
@@ -37,17 +87,50 @@ module Textrepo
|
|
37
87
|
# If `stamp_pattern` is omitted, the recent entries will be listed.
|
38
88
|
# Then, how many entries are listed depends on the implementaiton
|
39
89
|
# of the concrete repository class.
|
90
|
+
#
|
91
|
+
# :call-seq:
|
92
|
+
# entries(String) -> Array of Timestamp instances
|
93
|
+
|
40
94
|
def entries(stamp_pattern = nil); []; end
|
41
95
|
|
96
|
+
##
|
97
|
+
# Check the existence of text which is associated with the given
|
98
|
+
# timestamp.
|
99
|
+
#
|
100
|
+
# :call-seq:
|
101
|
+
# exist?(Timestamp) -> true or false
|
102
|
+
|
103
|
+
def exist?(timestamp); false; end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Searches a pattern (word or regular expression) in text those
|
107
|
+
# matches to a given timestamp pattern. Returns an Array of
|
108
|
+
# search results. If no match, returns an empty Array.
|
109
|
+
#
|
110
|
+
# See the document for Repository#entries about a timestamp
|
111
|
+
# pattern. When nil is passed as a timestamp pattern, searching
|
112
|
+
# applies to all text in the repository.
|
113
|
+
#
|
114
|
+
# Each entry of the result Array is constructed from 3 items, (1)
|
115
|
+
# timestamp (Timestamp), (2) line number (Integer), (3) matched
|
116
|
+
# line (String).
|
117
|
+
#
|
118
|
+
# :call-seq:
|
119
|
+
# search(String for pattern, String for Timestamp pattern) -> Array
|
120
|
+
|
121
|
+
def search(pattern, stamp_pattern = nil); []; end
|
122
|
+
|
42
123
|
end
|
43
124
|
|
44
125
|
require_relative 'file_system_repository'
|
45
126
|
|
127
|
+
##
|
46
128
|
# Returns an instance which derived from Textrepo::Repository class.
|
47
|
-
# `conf` must be
|
48
|
-
# `:repository_type` and
|
49
|
-
#
|
50
|
-
# pairs in `conf`.
|
129
|
+
# `conf` must be an object which can be accessed like a Hash object.
|
130
|
+
# And it must also has a value of `:repository_type` and
|
131
|
+
# `:repository_name` at least. Some concrete class derived from
|
132
|
+
# Textrepo::Repository may require more key-value pairs in `conf`.
|
133
|
+
|
51
134
|
def init(conf)
|
52
135
|
type = conf[:repository_type]
|
53
136
|
klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
|
data/lib/textrepo/timestamp.rb
CHANGED
@@ -1,23 +1,47 @@
|
|
1
1
|
module Textrepo
|
2
2
|
##
|
3
|
-
#
|
3
|
+
# Timestamp is generated from a Time object. It converts a time to
|
4
4
|
# string in the obvious format, such "20201023122400".
|
5
5
|
#
|
6
|
+
# Since the obvious format contains only year, month, day, hour,
|
7
|
+
# minute, and second, the resolution of time is a second. That is,
|
8
|
+
# two Time object those are different only in second will generates
|
9
|
+
# equal Timestamp objects.
|
10
|
+
#
|
11
|
+
# If a client program of Textrepo::Timestamp wants to distinguish
|
12
|
+
# those Time objects, an attribute `suffix` could be used.
|
13
|
+
#
|
14
|
+
# For example, the `suffix` will be converted into a 3 character
|
15
|
+
# string, such "012", "345", "678", ... etc. So, millisecond part
|
16
|
+
# of a Time object will be suitable to pass as `suffix` when
|
17
|
+
# creating a Timestamp object.
|
18
|
+
|
6
19
|
class Timestamp
|
7
20
|
include Comparable
|
8
21
|
|
9
|
-
|
22
|
+
##
|
23
|
+
# Time object which generates the Timestamp object.
|
24
|
+
|
25
|
+
attr_reader :time
|
10
26
|
|
11
27
|
##
|
12
|
-
#
|
13
|
-
|
28
|
+
# An integer specified in `new` method to create the Timestamp object.
|
29
|
+
|
30
|
+
attr_reader :suffix
|
31
|
+
|
32
|
+
##
|
33
|
+
# Creates a Timestamp object from a Time object. In addition, an
|
34
|
+
# Integer can be passed as a suffix use.
|
14
35
|
#
|
36
|
+
# :call-seq:
|
37
|
+
# new(Time, Integer = nil) -> Timestamp
|
38
|
+
|
15
39
|
def initialize(time, suffix = nil)
|
16
40
|
@time = time
|
17
41
|
@suffix = suffix
|
18
42
|
end
|
19
43
|
|
20
|
-
def <=>(other)
|
44
|
+
def <=>(other) # :nodoc:
|
21
45
|
result = (self.time <=> other.time)
|
22
46
|
|
23
47
|
sfx = self.suffix || 0
|
@@ -29,12 +53,10 @@ module Textrepo
|
|
29
53
|
##
|
30
54
|
# Generate an obvious time string.
|
31
55
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
# ```
|
37
|
-
#
|
56
|
+
# %Y %m %d %H %M %S suffix
|
57
|
+
# "2020-12-30 12:34:56 (0 | nil)" -> "20201230123456"
|
58
|
+
# "2020-12-30 12:34:56 (7)" -> "20201230123456_007"
|
59
|
+
|
38
60
|
def to_s
|
39
61
|
s = @time.strftime("%Y%m%d%H%M%S")
|
40
62
|
s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
|
@@ -43,21 +65,41 @@ module Textrepo
|
|
43
65
|
|
44
66
|
class << self
|
45
67
|
##
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
# "20201230123456_789" => "2020", "12", "30", "12", "34", "56", "789"
|
50
|
-
# ```
|
68
|
+
# Splits a string which represents a timestamp into components.
|
69
|
+
# Each component represents a part of constructs to instantiate
|
70
|
+
# a Time object.
|
51
71
|
#
|
72
|
+
# yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
|
73
|
+
# "20201230123456" -> "2020", "12", "30", "12", "34", "56"
|
74
|
+
# "20201230123456_789" -> "2020", "12", "30", "12", "34", "56", "789"
|
75
|
+
#
|
76
|
+
# Raises InvalidTimestampStringError if nil was passed as an arguemnt.
|
77
|
+
|
52
78
|
def split_stamp(stamp_str)
|
79
|
+
raise InvalidTimestampStringError, stamp_str if stamp_str.nil?
|
53
80
|
# yyyy mo dd hh mi ss sfx
|
54
81
|
a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
|
55
82
|
a[-1].nil? ? a[0..-2] : a
|
56
83
|
end
|
57
84
|
|
85
|
+
##
|
86
|
+
# Generate a Timestamp object from a string which represents a
|
87
|
+
# timestamp, such "20201028163400".
|
88
|
+
#
|
89
|
+
# Raises InvalidTimestampStringError if cannot convert the
|
90
|
+
# argument into a Timestamp object.
|
91
|
+
#
|
92
|
+
# :call-seq:
|
93
|
+
# parse_s("20201028163400") -> Timestamp
|
94
|
+
# parse_s("20201028163529_034") -> Timestamp
|
95
|
+
|
58
96
|
def parse_s(stamp_str)
|
59
|
-
|
60
|
-
|
97
|
+
begin
|
98
|
+
ye, mo, da, ho, mi, se, sfx = split_stamp(stamp_str).map(&:to_i)
|
99
|
+
Timestamp.new(Time.new(ye, mo, da, ho, mi, se), sfx)
|
100
|
+
rescue InvalidTimestampStringError, ArgumentError => _
|
101
|
+
raise InvalidTimestampStringError, stamp_str
|
102
|
+
end
|
61
103
|
end
|
62
104
|
|
63
105
|
end
|
data/lib/textrepo/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: textrepo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mnbi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -36,7 +36,6 @@ files:
|
|
36
36
|
- ".travis.yml"
|
37
37
|
- CHANGELOG.md
|
38
38
|
- Gemfile
|
39
|
-
- Gemfile.lock
|
40
39
|
- LICENSE.txt
|
41
40
|
- README.md
|
42
41
|
- Rakefile
|
data/Gemfile.lock
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
textrepo (0.4.2)
|
5
|
-
|
6
|
-
GEM
|
7
|
-
remote: https://rubygems.org/
|
8
|
-
specs:
|
9
|
-
minitest (5.14.2)
|
10
|
-
rake (13.0.1)
|
11
|
-
|
12
|
-
PLATFORMS
|
13
|
-
ruby
|
14
|
-
|
15
|
-
DEPENDENCIES
|
16
|
-
bundler (~> 2.1)
|
17
|
-
minitest (~> 5.0)
|
18
|
-
rake (~> 13.0)
|
19
|
-
textrepo!
|
20
|
-
|
21
|
-
BUNDLED WITH
|
22
|
-
2.1.4
|