textrepo 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +53 -4
- data/CHANGELOG.md +5 -0
- data/Rakefile +8 -0
- data/lib/textrepo/error.rb +57 -14
- data/lib/textrepo/file_system_repository.rb +289 -20
- data/lib/textrepo/repository.rb +80 -6
- data/lib/textrepo/timestamp.rb +70 -26
- data/lib/textrepo/version.rb +1 -1
- data/textrepo.gemspec +1 -1
- metadata +3 -4
- 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: dc5cf6089b4883c93dc228e19aa0a149d71a8d9cc26a92e6c75fdc4ee0b2d694
|
4
|
+
data.tar.gz: 8ebecace02d486b6b6c12256d52adfd44963a3a14f6d39729b83d426bce3a26e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aef27bf0363a66eeb1676ccda0682c253155875a8d0c16b27189674836edf2563b8df87b7f3b56cdb5d02c895f724f47a956588a5d97d24d83cdddce76fc083e
|
7
|
+
data.tar.gz: 81a4f8a76eb0ec8545e29ddbd537d49ec00f6009f5617dad61c61d8a5552c6df090d6d37fd507a7976c5f5f60b7d0c8b43f9a99d98d7efa1e933828270711d87
|
data/.gitignore
CHANGED
@@ -1,8 +1,57 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
Gemfile.lock
|
2
|
+
*.gem
|
3
|
+
*.rbc
|
4
|
+
/.config
|
4
5
|
/coverage/
|
5
|
-
/
|
6
|
+
/InstalledFiles
|
6
7
|
/pkg/
|
7
8
|
/spec/reports/
|
9
|
+
/spec/examples.txt
|
10
|
+
/test/tmp/
|
11
|
+
/test/version_tmp/
|
8
12
|
/tmp/
|
13
|
+
|
14
|
+
# Used by dotenv library to load environment variables.
|
15
|
+
# .env
|
16
|
+
|
17
|
+
# Ignore Byebug command history file.
|
18
|
+
.byebug_history
|
19
|
+
|
20
|
+
## Specific to RubyMotion:
|
21
|
+
.dat*
|
22
|
+
.repl_history
|
23
|
+
build/
|
24
|
+
*.bridgesupport
|
25
|
+
build-iPhoneOS/
|
26
|
+
build-iPhoneSimulator/
|
27
|
+
|
28
|
+
## Specific to RubyMotion (use of CocoaPods):
|
29
|
+
#
|
30
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
31
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
32
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
33
|
+
#
|
34
|
+
# vendor/Pods/
|
35
|
+
|
36
|
+
## Documentation cache and generated files:
|
37
|
+
/.yardoc/
|
38
|
+
/_yardoc/
|
39
|
+
/doc/
|
40
|
+
/rdoc/
|
41
|
+
|
42
|
+
## Environment normalization:
|
43
|
+
/.bundle/
|
44
|
+
/vendor/bundle
|
45
|
+
/lib/bundler/man/
|
46
|
+
|
47
|
+
# for a library or gem, you might want to ignore these files since the code is
|
48
|
+
# intended to run in multiple environments; otherwise, check them in:
|
49
|
+
# Gemfile.lock
|
50
|
+
# .ruby-version
|
51
|
+
# .ruby-gemset
|
52
|
+
|
53
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
54
|
+
.rvmrc
|
55
|
+
|
56
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
57
|
+
# .rubocop-https?--*
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
## [Unreleased]
|
8
8
|
Nothing to record here.
|
9
9
|
|
10
|
+
## [0.5.0] - 2020-11-01
|
11
|
+
### Added
|
12
|
+
- Add a new API `Repository#search`.
|
13
|
+
- Add a new API `Repository#exist?`. (0.4.3)
|
14
|
+
|
10
15
|
## [0.4.0] - 2020-10-14
|
11
16
|
### Added
|
12
17
|
- Released to rubygems.org.
|
data/Rakefile
CHANGED
@@ -35,3 +35,11 @@ end
|
|
35
35
|
task :clobber => :clean_sandbox
|
36
36
|
CLOBBER << 'test/fixtures/notes'
|
37
37
|
CLOBBER << 'test/fixtures/test_repo'
|
38
|
+
|
39
|
+
require "rdoc/task"
|
40
|
+
|
41
|
+
RDoc::Task.new do |rdoc|
|
42
|
+
rdoc.generator = "ri"
|
43
|
+
rdoc.rdoc_dir = "doc"
|
44
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
45
|
+
end
|
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,26 +1,89 @@
|
|
1
1
|
require 'fileutils'
|
2
|
+
require "open3"
|
2
3
|
|
3
4
|
module Textrepo
|
4
|
-
|
5
|
+
|
6
|
+
##
|
7
|
+
# A concrete class which implements Repository interfaces. This
|
8
|
+
# repository uses the default file system of the operating system as
|
9
|
+
# a text storage.
|
10
|
+
|
5
11
|
class FileSystemRepository < Repository
|
6
|
-
|
12
|
+
|
13
|
+
##
|
14
|
+
# Repository root.
|
15
|
+
|
16
|
+
attr_reader :path
|
17
|
+
|
18
|
+
##
|
19
|
+
# Extension of notes sotres in the repository.
|
20
|
+
|
21
|
+
attr_reader :extname
|
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
|
+
|
33
|
+
##
|
34
|
+
# Default name for the repository which uses when no name is
|
35
|
+
# specified in the configuration settings.
|
7
36
|
|
8
37
|
FAVORITE_REPOSITORY_NAME = 'notes'
|
38
|
+
|
39
|
+
##
|
40
|
+
# Default extension of notes which uses when no extname is
|
41
|
+
# specified in the configuration settings.
|
42
|
+
|
9
43
|
FAVORITE_EXTNAME = 'md'
|
10
44
|
|
11
|
-
|
12
|
-
#
|
45
|
+
##
|
46
|
+
# Default searcher program to search text in the repository.
|
47
|
+
|
48
|
+
FAVORITE_SEARCHER = 'grep'
|
49
|
+
|
50
|
+
##
|
51
|
+
# Creates a new repository object. The argument, `conf` must be a
|
52
|
+
# Hash object. It should hold the follwoing values:
|
13
53
|
#
|
14
|
-
# - :
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# - :
|
54
|
+
# - MANDATORY:
|
55
|
+
# - :repository_type => `:file_system`
|
56
|
+
# - :repository_base => the parent directory path for the repository
|
57
|
+
# - OPTIONAL: (if not specified, default values are used)
|
58
|
+
# - :repository_name => basename of the root path for the repository
|
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
|
18
62
|
#
|
19
63
|
# The root path of the repository looks like the following:
|
20
64
|
# - conf[:repository_base]/conf[:repository_name]
|
21
65
|
#
|
22
|
-
# Default values are set when
|
66
|
+
# Default values are set when `:repository_name` and `:default_extname`
|
23
67
|
# were not defined in `conf`.
|
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
|
+
|
24
87
|
def initialize(conf)
|
25
88
|
super
|
26
89
|
base = conf[:repository_base]
|
@@ -28,14 +91,17 @@ module Textrepo
|
|
28
91
|
@path = File.expand_path("#{name}", base)
|
29
92
|
FileUtils.mkdir_p(@path)
|
30
93
|
@extname = conf[:default_extname] || FAVORITE_EXTNAME
|
94
|
+
@searcher = find_searcher(conf[:searcher])
|
95
|
+
@searcher_options = conf[:searcher_options]
|
31
96
|
end
|
32
97
|
|
33
|
-
|
34
|
-
# repository operations
|
35
|
-
#
|
36
|
-
|
98
|
+
##
|
37
99
|
# Creates a file into the repository, which contains the specified
|
38
100
|
# text and is associated to the timestamp.
|
101
|
+
#
|
102
|
+
# :call-seq:
|
103
|
+
# create(Timestamp, Array) -> Timestamp
|
104
|
+
|
39
105
|
def create(timestamp, text)
|
40
106
|
abs = abspath(timestamp)
|
41
107
|
raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
|
@@ -45,8 +111,13 @@ module Textrepo
|
|
45
111
|
timestamp
|
46
112
|
end
|
47
113
|
|
114
|
+
##
|
48
115
|
# Reads the file content in the repository. Then, returns its
|
49
116
|
# content.
|
117
|
+
#
|
118
|
+
# :call-seq:
|
119
|
+
# read(Timestamp) -> Array
|
120
|
+
|
50
121
|
def read(timestamp)
|
51
122
|
abs = abspath(timestamp)
|
52
123
|
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
@@ -57,8 +128,13 @@ module Textrepo
|
|
57
128
|
content
|
58
129
|
end
|
59
130
|
|
131
|
+
##
|
60
132
|
# Updates the file content in the repository. A new timestamp
|
61
133
|
# will be attached to the text.
|
134
|
+
#
|
135
|
+
# :call-seq:
|
136
|
+
# update(Timestamp, Array) -> Timestamp
|
137
|
+
|
62
138
|
def update(timestamp, text)
|
63
139
|
raise EmptyTextError if text.empty?
|
64
140
|
org_abs = abspath(timestamp)
|
@@ -75,7 +151,12 @@ module Textrepo
|
|
75
151
|
new_stamp
|
76
152
|
end
|
77
153
|
|
154
|
+
##
|
78
155
|
# Deletes the file in the repository.
|
156
|
+
#
|
157
|
+
# :call-seq:
|
158
|
+
# delete(Timestamp) -> Array
|
159
|
+
|
79
160
|
def delete(timestamp)
|
80
161
|
abs = abspath(timestamp)
|
81
162
|
raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
|
@@ -86,7 +167,12 @@ module Textrepo
|
|
86
167
|
content
|
87
168
|
end
|
88
169
|
|
170
|
+
##
|
89
171
|
# Finds entries of text those timestamp matches the specified pattern.
|
172
|
+
#
|
173
|
+
# :call-seq:
|
174
|
+
# entries(String = nil) -> Array of Timestamp instances
|
175
|
+
|
90
176
|
def entries(stamp_pattern = nil)
|
91
177
|
results = []
|
92
178
|
|
@@ -94,7 +180,7 @@ module Textrepo
|
|
94
180
|
when "yyyymoddhhmiss_lll".size
|
95
181
|
stamp = Timestamp.parse_s(stamp_pattern)
|
96
182
|
if exist?(stamp)
|
97
|
-
results << stamp
|
183
|
+
results << stamp
|
98
184
|
end
|
99
185
|
when 0, "yyyymoddhhmiss".size, "yyyymodd".size
|
100
186
|
results += find_entries(stamp_pattern)
|
@@ -117,12 +203,54 @@ module Textrepo
|
|
117
203
|
results
|
118
204
|
end
|
119
205
|
|
206
|
+
##
|
207
|
+
# Check the existence of text which is associated with the given
|
208
|
+
# timestamp.
|
209
|
+
#
|
210
|
+
# :call-seq:
|
211
|
+
# exist?(Timestamp) -> true or false
|
212
|
+
|
213
|
+
def exist?(timestamp)
|
214
|
+
FileTest.exist?(abspath(timestamp))
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Searches a pattern in all text. The given pattern is a word to
|
219
|
+
# search or a regular expression. The pattern would be passed to
|
220
|
+
# a searcher program as it passed.
|
221
|
+
#
|
222
|
+
# See the document for Textrepo::Repository#search to know about
|
223
|
+
# the search result.
|
224
|
+
#
|
225
|
+
# :call-seq:
|
226
|
+
# search(String for pattern, String for Timestamp pattern) -> Array
|
227
|
+
|
228
|
+
def search(pattern, stamp_pattern = nil)
|
229
|
+
result = nil
|
230
|
+
if stamp_pattern.nil?
|
231
|
+
result = invoke_searcher_at_repo_root(@searcher, pattern)
|
232
|
+
else
|
233
|
+
result = invoke_searcher_for_entries(@searcher, pattern, entries(stamp_pattern))
|
234
|
+
end
|
235
|
+
construct_search_result(result)
|
236
|
+
end
|
237
|
+
|
238
|
+
# :stopdoc:
|
239
|
+
|
120
240
|
private
|
121
241
|
def abspath(timestamp)
|
122
|
-
filename = timestamp
|
242
|
+
filename = timestamp_to_pathname(timestamp) + ".#{@extname}"
|
123
243
|
File.expand_path(filename, @path)
|
124
244
|
end
|
125
245
|
|
246
|
+
# %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
|
247
|
+
# "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
|
248
|
+
# "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
|
249
|
+
def timestamp_to_pathname(timestamp)
|
250
|
+
yyyy, mo = Timestamp.split_stamp(timestamp.to_s)[0..1]
|
251
|
+
File.join(yyyy, mo, timestamp.to_s)
|
252
|
+
end
|
253
|
+
|
126
254
|
def write_text(abs, text)
|
127
255
|
FileUtils.mkdir_p(File.dirname(abs))
|
128
256
|
File.open(abs, 'w') { |f|
|
@@ -134,15 +262,156 @@ module Textrepo
|
|
134
262
|
File.basename(text_path).delete_suffix(".#{@extname}")
|
135
263
|
end
|
136
264
|
|
137
|
-
def exist?(timestamp)
|
138
|
-
FileTest.exist?(abspath(timestamp))
|
139
|
-
end
|
140
|
-
|
141
265
|
def find_entries(stamp_pattern)
|
142
266
|
Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
|
143
|
-
|
267
|
+
begin
|
268
|
+
Timestamp.parse_s(timestamp_str(e))
|
269
|
+
rescue InvalidTimestampStringError => _
|
270
|
+
# Just ignore the erroneous entry, since it is not a text in
|
271
|
+
# the repository. It may be a garbage, or some kind of
|
272
|
+
# hidden stuff of the repository, ... etc.
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
}.compact
|
276
|
+
end
|
277
|
+
|
278
|
+
##
|
279
|
+
# The upper limit of files to search at one time. The value has
|
280
|
+
# no reason to select. It seems to me that not too much, not too
|
281
|
+
# little to handle in one process to search.
|
282
|
+
|
283
|
+
LIMIT_OF_FILES = 20
|
284
|
+
|
285
|
+
##
|
286
|
+
# When no timestamp pattern was given, invoke the searcher with
|
287
|
+
# the repository root path as its argument and the recursive
|
288
|
+
# searching option. The search could be done in only one process.
|
289
|
+
|
290
|
+
def invoke_searcher_at_repo_root(searcher, pattern)
|
291
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
292
|
+
pattern, @path)
|
293
|
+
output = []
|
294
|
+
output += o.lines.map(&:chomp) if s.success? && (! o.empty?)
|
295
|
+
output
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# When a timestamp pattern was given, at first, list target files,
|
300
|
+
# then invoke the searcher for those files. Since the number of
|
301
|
+
# target files may be so much, it seems to be dangerous to pass
|
302
|
+
# all of them to a single search process at one time.
|
303
|
+
#
|
304
|
+
# One more thing to mention, the searcher, like `grep`, does not
|
305
|
+
# add the filename at the beginning of the search result line, if
|
306
|
+
# the target is one file. This behavior is not suitable in this
|
307
|
+
# purpose. The code below adds the filename when the target is
|
308
|
+
# one file.
|
309
|
+
|
310
|
+
def invoke_searcher_for_entries(searcher, pattern, entries)
|
311
|
+
output = []
|
312
|
+
|
313
|
+
num_of_entries = entries.size
|
314
|
+
if num_of_entries == 1
|
315
|
+
# If the search taget is one file, the output needs special
|
316
|
+
# treatment.
|
317
|
+
file = abspath(entries[0])
|
318
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
319
|
+
pattern, file)
|
320
|
+
if s.success? && (! o.empty)
|
321
|
+
output += o.lines.map { |line|
|
322
|
+
# add filename at the beginning of the search result line
|
323
|
+
[file, line.chomp].join(":")
|
324
|
+
}
|
325
|
+
end
|
326
|
+
elsif num_of_entries > LIMIT_OF_FILES
|
327
|
+
output += invoke_searcher_for_entries(searcher, pattern, entries[0..(LIMIT_OF_FILES - 1)])
|
328
|
+
output += invoke_searcher_for_entries(searcher, pattern, entries[LIMIT_OF_FILES..-1])
|
329
|
+
else
|
330
|
+
# When the number of target is less than the upper limit,
|
331
|
+
# invoke the searcher with all of target files as its
|
332
|
+
# arguments.
|
333
|
+
files = find_files(entries)
|
334
|
+
o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
|
335
|
+
pattern, *files)
|
336
|
+
if s.success? && (! o.empty)
|
337
|
+
output += o.lines.map(&:chomp)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
output
|
342
|
+
end
|
343
|
+
|
344
|
+
SEARCHER_OPTS = {
|
345
|
+
# case insensitive, print line number, recursive search, work as egrep
|
346
|
+
"grep" => ["-i", "-n", "-R", "-E"],
|
347
|
+
# case insensitive, print line number, recursive search
|
348
|
+
"egrep" => ["-i", "-n", "-R"],
|
349
|
+
# case insensitive, print line number, recursive search, work as gegrep
|
350
|
+
"ggrep" => ["-i", "-n", "-R", "-E"],
|
351
|
+
# case insensitive, print line number, recursive search
|
352
|
+
"gegrep" => ["-i", "-n", "-R"],
|
353
|
+
# smart case, print line number, no color
|
354
|
+
"rg" => ["-S", "-n", "--no-heading", "--color", "never"],
|
355
|
+
}
|
356
|
+
|
357
|
+
def find_searcher_options(searcher)
|
358
|
+
@searcher_options || SEARCHER_OPTS[File.basename(searcher)] || ""
|
359
|
+
end
|
360
|
+
|
361
|
+
def find_files(timestamps)
|
362
|
+
timestamps.map{|stamp| abspath(stamp)}
|
363
|
+
end
|
364
|
+
|
365
|
+
##
|
366
|
+
# The argument must be an Array contains the searcher output.
|
367
|
+
# Each item is constructed from 3 parts:
|
368
|
+
# "<pathname>:<integer>:<text>"
|
369
|
+
#
|
370
|
+
# For example, it may looks like:
|
371
|
+
#
|
372
|
+
# "/somewhere/2020/11/20201101044300.md:18:foo is foo"
|
373
|
+
#
|
374
|
+
# Or it may contains more ":" in the text part as:
|
375
|
+
#
|
376
|
+
# "/somewhere/2020/11/20201101044500.md:119:apple:orange:grape"
|
377
|
+
#
|
378
|
+
# In the latter case, `split(":")` will split it too much. That is,
|
379
|
+
# the result will be:
|
380
|
+
#
|
381
|
+
# ["/somewhere/2020/11/20201101044500.md", "119", "apple", "orange", "grape"]
|
382
|
+
#
|
383
|
+
# Text part must be joined with ":".
|
384
|
+
|
385
|
+
def construct_search_result(output)
|
386
|
+
output.map { |line|
|
387
|
+
begin
|
388
|
+
pathname, num, *match_text = line.split(":")
|
389
|
+
[Timestamp.parse_s(timestamp_str(pathname)),
|
390
|
+
num.to_i,
|
391
|
+
match_text.join(":")]
|
392
|
+
rescue InvalidTimestampStringError, TypeError => _
|
393
|
+
raise InvalidSearchResultError, [@searcher, @searcher_options.join(" ")].join(" ")
|
394
|
+
end
|
395
|
+
}.compact
|
396
|
+
end
|
397
|
+
|
398
|
+
def find_searcher(program = nil)
|
399
|
+
candidates = [FAVORITE_SEARCHER]
|
400
|
+
candidates.unshift(program) unless program.nil? || candidates.include?(program)
|
401
|
+
search_paths = ENV["PATH"].split(":")
|
402
|
+
candidates.map { |prog|
|
403
|
+
find_in_paths(prog, search_paths)
|
404
|
+
}[0]
|
405
|
+
end
|
406
|
+
|
407
|
+
def find_in_paths(prog, paths)
|
408
|
+
paths.each { |p|
|
409
|
+
abspath = File.expand_path(prog, p)
|
410
|
+
return abspath if FileTest.exist?(abspath) && FileTest.executable?(abspath)
|
144
411
|
}
|
412
|
+
nil
|
145
413
|
end
|
414
|
+
# :startdoc:
|
146
415
|
|
147
416
|
end
|
148
417
|
end
|
data/lib/textrepo/repository.rb
CHANGED
@@ -1,31 +1,72 @@
|
|
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
|
|
45
|
+
##
|
18
46
|
# Updates the content with text in the repository, which is
|
19
47
|
# associated to the timestamp. Returns the timestamp.
|
48
|
+
#
|
49
|
+
# :call-seq:
|
50
|
+
# update(Timestamp, Array) -> Timestamp
|
51
|
+
|
20
52
|
def update(timestamp, text); timestamp; end
|
21
53
|
|
54
|
+
##
|
22
55
|
# Deletes the content in the repository, which is associated to
|
23
56
|
# the timestamp. Returns an array which contains the deleted text.
|
57
|
+
#
|
58
|
+
# :call-seq:
|
59
|
+
# delete(Timestamp) -> Array
|
60
|
+
|
24
61
|
def delete(timestamp); []; end
|
25
62
|
|
63
|
+
##
|
26
64
|
# Finds all entries of text those have timestamps which mathes the
|
27
65
|
# specified pattern of timestamp. Returns an array which contains
|
28
|
-
#
|
66
|
+
# instances of Timestamp. If none of text was found, an empty
|
67
|
+
# array would be returned.
|
68
|
+
#
|
69
|
+
# A pattern must be one of the following:
|
29
70
|
#
|
30
71
|
# - yyyymoddhhmiss_lll : whole stamp
|
31
72
|
# - yyyymoddhhmiss : omit millisecond part
|
@@ -37,17 +78,50 @@ module Textrepo
|
|
37
78
|
# If `stamp_pattern` is omitted, the recent entries will be listed.
|
38
79
|
# Then, how many entries are listed depends on the implementaiton
|
39
80
|
# of the concrete repository class.
|
81
|
+
#
|
82
|
+
# :call-seq:
|
83
|
+
# entries(String) -> Array of Timestamp instances
|
84
|
+
|
40
85
|
def entries(stamp_pattern = nil); []; end
|
41
86
|
|
87
|
+
##
|
88
|
+
# Check the existence of text which is associated with the given
|
89
|
+
# timestamp.
|
90
|
+
#
|
91
|
+
# :call-seq:
|
92
|
+
# exist?(Timestamp) -> true or false
|
93
|
+
|
94
|
+
def exist?(timestamp); false; end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Searches a pattern (word or regular expression) in text those
|
98
|
+
# matches to a given timestamp pattern. Returns an Array of
|
99
|
+
# search results. If no match, returns an empty Array.
|
100
|
+
#
|
101
|
+
# See the document for Repository#entries about a timestamp
|
102
|
+
# pattern. When nil is passed as a timestamp pattern, searching
|
103
|
+
# applies to all text in the repository.
|
104
|
+
#
|
105
|
+
# Each entry of the result Array is constructed from 3 items, (1)
|
106
|
+
# timestamp (Timestamp), (2) line number (Integer), (3) matched
|
107
|
+
# line (String).
|
108
|
+
#
|
109
|
+
# :call-seq:
|
110
|
+
# search(String for pattern, String for Timestamp pattern) -> Array
|
111
|
+
|
112
|
+
def search(pattern, stamp_pattern = nil); []; end
|
113
|
+
|
42
114
|
end
|
43
115
|
|
44
116
|
require_relative 'file_system_repository'
|
45
117
|
|
118
|
+
##
|
46
119
|
# Returns an instance which derived from Textrepo::Repository class.
|
47
|
-
# `conf` must be
|
48
|
-
# `:repository_type` and
|
49
|
-
#
|
50
|
-
# pairs in `conf`.
|
120
|
+
# `conf` must be an object which can be accessed like a Hash object.
|
121
|
+
# And it must also has a value of `:repository_type` and
|
122
|
+
# `:repository_name` at least. Some concrete class derived from
|
123
|
+
# Textrepo::Repository may require more key-value pairs in `conf`.
|
124
|
+
|
51
125
|
def init(conf)
|
52
126
|
type = conf[:repository_type]
|
53
127
|
klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
|
data/lib/textrepo/timestamp.rb
CHANGED
@@ -1,17 +1,47 @@
|
|
1
1
|
module Textrepo
|
2
|
+
##
|
3
|
+
# Timestamp is generated from a Time object. It converts a time to
|
4
|
+
# string in the obvious format, such "20201023122400".
|
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
|
+
|
2
19
|
class Timestamp
|
3
20
|
include Comparable
|
4
21
|
|
5
|
-
|
22
|
+
##
|
23
|
+
# Time object which generates the Timestamp object.
|
24
|
+
|
25
|
+
attr_reader :time
|
26
|
+
|
27
|
+
##
|
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.
|
35
|
+
#
|
36
|
+
# :call-seq:
|
37
|
+
# new(Time, Integer = nil) -> Timestamp
|
6
38
|
|
7
|
-
# time: a Time instance
|
8
|
-
# suffix: an Integer instance
|
9
39
|
def initialize(time, suffix = nil)
|
10
40
|
@time = time
|
11
41
|
@suffix = suffix
|
12
42
|
end
|
13
43
|
|
14
|
-
def <=>(other)
|
44
|
+
def <=>(other) # :nodoc:
|
15
45
|
result = (self.time <=> other.time)
|
16
46
|
|
17
47
|
sfx = self.suffix || 0
|
@@ -20,44 +50,58 @@ module Textrepo
|
|
20
50
|
result == 0 ? (sfx <=> osfx) : result
|
21
51
|
end
|
22
52
|
|
23
|
-
|
24
|
-
#
|
25
|
-
#
|
53
|
+
##
|
54
|
+
# Generate an obvious time string.
|
55
|
+
#
|
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
|
+
|
26
60
|
def to_s
|
27
61
|
s = @time.strftime("%Y%m%d%H%M%S")
|
28
62
|
s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
|
29
63
|
s
|
30
64
|
end
|
31
65
|
|
32
|
-
# %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
|
33
|
-
# "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
|
34
|
-
# "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
|
35
|
-
def to_pathname
|
36
|
-
@time.strftime("%Y/%m/") + self.to_s
|
37
|
-
end
|
38
|
-
|
39
66
|
class << self
|
40
|
-
|
41
|
-
#
|
42
|
-
#
|
67
|
+
##
|
68
|
+
# Splits a string which represents a timestamp into components.
|
69
|
+
# Each component represents a part of constructs to instantiate
|
70
|
+
# a Time object.
|
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
|
+
|
43
78
|
def split_stamp(stamp_str)
|
79
|
+
raise InvalidTimestampStringError, stamp_str if stamp_str.nil?
|
44
80
|
# yyyy mo dd hh mi ss sfx
|
45
81
|
a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
|
46
82
|
a[-1].nil? ? a[0..-2] : a
|
47
83
|
end
|
48
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
|
+
|
49
96
|
def parse_s(stamp_str)
|
50
|
-
|
51
|
-
|
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
|
52
103
|
end
|
53
104
|
|
54
|
-
# (-2)
|
55
|
-
# 0 8 |(-1)
|
56
|
-
# V V VV
|
57
|
-
# "2020/12/20201230123456" => "2020-12-30 12:34:56"
|
58
|
-
def parse_pathname(pathname)
|
59
|
-
parse_s(pathname[8..-1])
|
60
|
-
end
|
61
105
|
end
|
62
106
|
end
|
63
107
|
end
|
data/lib/textrepo/version.rb
CHANGED
data/textrepo.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.email = ["mnbi@users.noreply.github.com"]
|
8
8
|
|
9
9
|
spec.summary = %q{A repository to store text with timestamp.}
|
10
|
-
spec.description = %q{Textrepo is a repository to store text with timestamp. It can manage text with the attached timestamp (read/update/
|
10
|
+
spec.description = %q{Textrepo is a repository to store text with timestamp. It can manage text with the attached timestamp (create/read/update/delete).}
|
11
11
|
spec.homepage = "https://github.com/mnbi/textrepo"
|
12
12
|
spec.license = "MIT"
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
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.0
|
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-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.1'
|
27
27
|
description: Textrepo is a repository to store text with timestamp. It can manage
|
28
|
-
text with the attached timestamp (read/update/
|
28
|
+
text with the attached timestamp (create/read/update/delete).
|
29
29
|
email:
|
30
30
|
- mnbi@users.noreply.github.com
|
31
31
|
executables: []
|
@@ -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.1)
|
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
|