textrepo 0.4.1 → 0.5.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 +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
|