whale 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/whale +93 -0
- data/lib/whale.rb +280 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 84a82a8527b5b787b69deb1dbb9c229db5cb356f
|
4
|
+
data.tar.gz: 4818466b7d1d8ca1d26a250754ec95fa451d188d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b8c2f2f092f0e34d3726267ddfdfb87bba0efb1850495d62c8b145561b95075611b64fe00e400a8d1c5ace6110ada2232cca73599e69113cfa951fd054078eab
|
7
|
+
data.tar.gz: 231ace61fa951ce4b6a705c3eba13a3cda42f253e867bda0eacf4103bbfc1022958b53f886e1a2b3febe63ca78d2713c0ea9e42ec3ee6b6caab42091454f0193
|
data/bin/whale
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'whale'
|
3
|
+
|
4
|
+
USAGE = <<ENDUSAGE
|
5
|
+
Usage:
|
6
|
+
whale [-h] [-f tag] [-s tag] [-e id] files..
|
7
|
+
ENDUSAGE
|
8
|
+
|
9
|
+
HELP = <<ENDHELP
|
10
|
+
-h, --help View this message
|
11
|
+
-f, --filter List entries with the tag
|
12
|
+
-s, --sort Sort entries by the tag value
|
13
|
+
-e, --edit Open the editor to given entry
|
14
|
+
-w, --write Write the entries to file
|
15
|
+
--version Show the version
|
16
|
+
|
17
|
+
-f, --filter filter
|
18
|
+
Show entries with tags satisfying the filter. The filter is a string
|
19
|
+
consisting of Ruby regexes and operators in reverse polish notation.
|
20
|
+
A regex evaluates to true if at least one tag name in an entry matches it.
|
21
|
+
One can further match on the tag value by writing = followed by a regex.
|
22
|
+
Options for regexes are specified by writing /(?option:regex)/.
|
23
|
+
The symbols for AND, OR, and NOT are &, |, and *, respectively.
|
24
|
+
|
25
|
+
-s, --sort tag
|
26
|
+
Sort the entries by tag.
|
27
|
+
|
28
|
+
-e, --edit id
|
29
|
+
Edit the entry with id id using the text editor specified by the environment
|
30
|
+
EDITOR.
|
31
|
+
|
32
|
+
-w, --write
|
33
|
+
Write the entries, including title, body, and tags, to stdout.
|
34
|
+
ENDHELP
|
35
|
+
|
36
|
+
args = { :files => [] }
|
37
|
+
unflagged_args = [:files]
|
38
|
+
next_arg = unflagged_args.first
|
39
|
+
ARGV.each do |arg|
|
40
|
+
case arg
|
41
|
+
when '-h','--help' then args[:help] = true
|
42
|
+
when '-f','--filter' then next_arg = :filter
|
43
|
+
when '-s','--sort' then next_arg = :sort
|
44
|
+
when '-e','--edit' then next_arg = :edit
|
45
|
+
when '-w', '--write' then args[:write] = true
|
46
|
+
when '--version' then args[:version] = true
|
47
|
+
else
|
48
|
+
if next_arg == :files
|
49
|
+
args[:files] << arg
|
50
|
+
else
|
51
|
+
args[next_arg] = arg
|
52
|
+
unflagged_args.delete next_arg
|
53
|
+
next_arg = unflagged_args.first
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
if args[:version]
|
58
|
+
puts "whale.rb version #{MAJOR_VERSION}.#{MINOR_VERSION}.#{REVISION}"
|
59
|
+
exit
|
60
|
+
end
|
61
|
+
if args[:help] or args[:files].empty?
|
62
|
+
puts USAGE
|
63
|
+
puts HELP if args[:help]
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
entries = []
|
67
|
+
args[:files].each { |f| entries += parse_file(f) }
|
68
|
+
puts "Parsed #{args[:files].length} files and #{entries.length} entries"
|
69
|
+
if args[:filter]
|
70
|
+
filter = Filter.new
|
71
|
+
filter.parse_filter args[:filter]
|
72
|
+
filter_entries(entries, filter)
|
73
|
+
end
|
74
|
+
sort_entries_by(entries, args[:sort]) if args[:sort]
|
75
|
+
if args[:edit]
|
76
|
+
i = args[:edit].to_i - 1
|
77
|
+
e = entries[i]
|
78
|
+
if e.nil?
|
79
|
+
puts "Invalid ID"
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
open_editor(ENV['EDITOR'].to_sym, e.tags[:file], e.tags[:line])
|
83
|
+
end
|
84
|
+
|
85
|
+
write_entries(entries) if args[:write]
|
86
|
+
|
87
|
+
if !args[:write]
|
88
|
+
all_tags = get_all_tags entries
|
89
|
+
debug(list_tags(all_tags))
|
90
|
+
tags_to_list = [:title, :date, :tags]
|
91
|
+
tags_format = [45, 10, 25]
|
92
|
+
list_entries(entries, tags_to_list, tags_format)
|
93
|
+
end
|
data/lib/whale.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
# File: whale.rb
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
DEBUG = true
|
6
|
+
|
7
|
+
MAJOR_VERSION = 0
|
8
|
+
MINOR_VERSION = 0
|
9
|
+
REVISION = 0
|
10
|
+
|
11
|
+
|
12
|
+
def debug(msg)
|
13
|
+
puts msg if DEBUG
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(msg)
|
17
|
+
puts "Error: #{msg}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def warning(msg)
|
21
|
+
puts "Warning: #{msg}"
|
22
|
+
end
|
23
|
+
|
24
|
+
$DEFAULT_TAGS = [:title, :body, :line, :file, :tags]
|
25
|
+
|
26
|
+
class Entry
|
27
|
+
attr_accessor :tags
|
28
|
+
|
29
|
+
def initialize()
|
30
|
+
@tags = { title: '', body: '', line: 0, file: '', tags: '' }
|
31
|
+
end
|
32
|
+
|
33
|
+
def print()
|
34
|
+
puts "#{@tags[:line]}, #{@tags[:file]}: #{@tags[:title]}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class Filter
|
40
|
+
attr_reader :stack
|
41
|
+
|
42
|
+
def initialize()
|
43
|
+
@stack = []
|
44
|
+
end
|
45
|
+
|
46
|
+
# For regex options (e.g. ignorecase) use the (?opt:source) notation,
|
47
|
+
# e.g. /(?i-mx:hEllo .*)/
|
48
|
+
def parse_filter(f)
|
49
|
+
debug("parsing #{f}")
|
50
|
+
regex = false
|
51
|
+
quote = nil
|
52
|
+
escape = false
|
53
|
+
# literal = false
|
54
|
+
token = 0
|
55
|
+
stack = []
|
56
|
+
(0...f.length).each do |i|
|
57
|
+
if f[i] == '\\' or escape
|
58
|
+
escape ^= true
|
59
|
+
elsif f[i] == '"' or f[i] == "'"
|
60
|
+
unless regex
|
61
|
+
if quote == f[i]
|
62
|
+
stack << f[token, i - token]
|
63
|
+
quote = nil
|
64
|
+
else
|
65
|
+
token = i + 1
|
66
|
+
quote = f[i]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
elsif f[i] == '/'
|
70
|
+
unless quote
|
71
|
+
if regex
|
72
|
+
stack << Regexp.new(f[token, i - token])
|
73
|
+
regex = false
|
74
|
+
else
|
75
|
+
token = i + 1
|
76
|
+
regex = true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
elsif !regex and !quote
|
80
|
+
stack << :FILTER_AND if f[i] == '&'
|
81
|
+
stack << :FILTER_OR if f[i] == '|'
|
82
|
+
stack << :FILTER_NOT if f[i] == '*'
|
83
|
+
stack << :FILTER_EQ if f[i] == '='
|
84
|
+
end
|
85
|
+
end
|
86
|
+
debug(stack)
|
87
|
+
@stack = stack
|
88
|
+
end
|
89
|
+
|
90
|
+
def match_token(entry, token)
|
91
|
+
tags = []
|
92
|
+
entry.tags.each do |t, v|
|
93
|
+
tags << t if token.match t.to_s
|
94
|
+
end
|
95
|
+
return tags
|
96
|
+
end
|
97
|
+
|
98
|
+
# apply the stack to the entry tags
|
99
|
+
def filter(entry)
|
100
|
+
s = []
|
101
|
+
last_tags = []
|
102
|
+
eq = false
|
103
|
+
@stack.each do |t|
|
104
|
+
case t
|
105
|
+
when :FILTER_AND then s << (s.pop & s.pop)
|
106
|
+
when :FILTER_OR then s << (s.pop | s.pop)
|
107
|
+
when :FILTER_NOT then s << !s.pop
|
108
|
+
when :FILTER_EQ then eq = true
|
109
|
+
else
|
110
|
+
if eq
|
111
|
+
debug("last tags: #{last_tags}")
|
112
|
+
match = false
|
113
|
+
s.pop
|
114
|
+
last_tags.each do |u|
|
115
|
+
if t.match entry.tags[u]
|
116
|
+
match = true
|
117
|
+
debug("#{t} matches #{entry.tags[u]}")
|
118
|
+
break
|
119
|
+
end
|
120
|
+
end
|
121
|
+
s << match
|
122
|
+
eq = false
|
123
|
+
else
|
124
|
+
last_tags = match_token entry, t
|
125
|
+
s << !last_tags.empty?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
debug(s)
|
130
|
+
warning("malformed filter") if s.length != 1
|
131
|
+
return s.first
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def filter_entries(entries, filter)
|
137
|
+
entries.delete_if { |a| !filter.filter(a) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def sort_entries_by(entries, tag)
|
141
|
+
return entries.sort { |a, b| a.tags[tag] <=> b.tags[tag] }
|
142
|
+
end
|
143
|
+
|
144
|
+
$EDITOR_CMDS = {
|
145
|
+
vim: "vim +%<line>d %<file>s",
|
146
|
+
emacs: "emacs +%<line>d %<file>s",
|
147
|
+
nano: "nano +%<line>d,1 %<file>s",
|
148
|
+
}
|
149
|
+
$EDITOR_CMDS.default = "ed %<file>s"
|
150
|
+
|
151
|
+
def open_editor(editor, path, lineno)
|
152
|
+
debug("opening at #{lineno}")
|
153
|
+
args = {line: lineno, file: path}
|
154
|
+
cmd = $EDITOR_CMDS[editor] % args
|
155
|
+
exec(cmd)
|
156
|
+
end
|
157
|
+
|
158
|
+
def write_entries(entries)
|
159
|
+
entries.each do |e|
|
160
|
+
printf("#{e.tags[:title]}\n")
|
161
|
+
printf("#{e.tags[:body]}")
|
162
|
+
# extension: implement wrapping
|
163
|
+
e.tags.each do |t, v|
|
164
|
+
next if $DEFAULT_TAGS.find_index(t)
|
165
|
+
printf(";#{t}")
|
166
|
+
printf("=#{v}") if v != true
|
167
|
+
printf("\n")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Print the entries.
|
173
|
+
def list_entries(entries, tags, tags_format)
|
174
|
+
id = 1
|
175
|
+
header_format = "%6.6s "
|
176
|
+
header = ["ID"]
|
177
|
+
row_format = "%<id>6d "
|
178
|
+
raise "tags and format length mismatch" if tags.length != tags_format.length
|
179
|
+
tags.each_index do |i|
|
180
|
+
w = tags_format[i]
|
181
|
+
header_format += "%-#{w}.#{w}s "
|
182
|
+
row_format += "%<#{tags[i]}>-#{w}.#{w}s "
|
183
|
+
header << tags[i]
|
184
|
+
end
|
185
|
+
puts header_format % header
|
186
|
+
entries.each do |e|
|
187
|
+
h = e.tags.merge({ id: id })
|
188
|
+
h.default = "--"
|
189
|
+
puts row_format % h
|
190
|
+
id += 1
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def parse_tag(entry, tag_str)
|
195
|
+
a = tag_str.split("=", 2)
|
196
|
+
tag = a[0].strip.to_sym
|
197
|
+
if tag.length == 0
|
198
|
+
return
|
199
|
+
end
|
200
|
+
if a.length != 2
|
201
|
+
value = true
|
202
|
+
else
|
203
|
+
value = a[1]
|
204
|
+
end
|
205
|
+
entry.tags[tag] = value
|
206
|
+
entry.tags[:tags] << "," unless entry.tags[:tags].empty?
|
207
|
+
entry.tags[:tags] << "#{tag}"
|
208
|
+
end
|
209
|
+
|
210
|
+
def parse_tags(entry, line)
|
211
|
+
a = line.split(" ")
|
212
|
+
a.each { |tag_str| parse_tag(entry, tag_str) }
|
213
|
+
end
|
214
|
+
|
215
|
+
def get_all_tags(entries)
|
216
|
+
s = Set.new
|
217
|
+
entries.each do |e|
|
218
|
+
e.tags.each do |k, _|
|
219
|
+
s.add(k)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
return s.to_a()
|
223
|
+
end
|
224
|
+
|
225
|
+
def list_tags(tags)
|
226
|
+
s = ""
|
227
|
+
tags.each do |tag|
|
228
|
+
s << "#{tag}, "
|
229
|
+
end
|
230
|
+
puts s.slice(0, s.length - 2)
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
EMPTY_LINE = /\A\s*\Z/
|
235
|
+
LABEL_LINE = /\A;(.*)\Z/
|
236
|
+
|
237
|
+
# Extract entries from file.
|
238
|
+
# param @file String the path of the file to parse
|
239
|
+
# return Array the array of Entry
|
240
|
+
def parse_file(file)
|
241
|
+
entries = []
|
242
|
+
file_entry = Entry.new
|
243
|
+
File.open(file, "r") do |f|
|
244
|
+
entry = nil
|
245
|
+
is_reading_tag = true
|
246
|
+
lineno = 0
|
247
|
+
f.each_line do |line|
|
248
|
+
lineno += 1
|
249
|
+
# skip if the line is whitespace
|
250
|
+
next if EMPTY_LINE.match line
|
251
|
+
if (m = LABEL_LINE.match line)
|
252
|
+
debug("#{f.path}, #{lineno}, reading tag")
|
253
|
+
is_reading_tag = true
|
254
|
+
matched_line = m[1]
|
255
|
+
if entry.nil?
|
256
|
+
debug("adding file entry tags #{matched_line}")
|
257
|
+
parse_tags file_entry, matched_line
|
258
|
+
else
|
259
|
+
parse_tags entry, matched_line
|
260
|
+
end
|
261
|
+
elsif is_reading_tag
|
262
|
+
debug("#{f.path}, #{lineno}, new entry")
|
263
|
+
is_reading_tag = false
|
264
|
+
entries << entry if !entry.nil?
|
265
|
+
entry = Entry.new
|
266
|
+
entry.tags[:title] = line.strip
|
267
|
+
entry.tags[:line] = lineno
|
268
|
+
entry.tags[:file] = f.path
|
269
|
+
else
|
270
|
+
entry.tags[:body] << line
|
271
|
+
end
|
272
|
+
end
|
273
|
+
entries << entry if !entry.nil?
|
274
|
+
puts "Last entry is missing tag" if !is_reading_tag
|
275
|
+
end
|
276
|
+
# add the file level tags to each entry
|
277
|
+
debug(file_entry.tags)
|
278
|
+
entries.each { |e| e.tags = file_entry.tags.merge(e.tags) }
|
279
|
+
return entries
|
280
|
+
end
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: whale
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryutaro Ikeda
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Tag and filter your ideas
|
14
|
+
email: ryutaroikeda94@gmail.com
|
15
|
+
executables:
|
16
|
+
- whale
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/whale
|
21
|
+
- lib/whale.rb
|
22
|
+
homepage: http://rubygems.org/gems/whale
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.5.1
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: An ideas organizer
|
46
|
+
test_files: []
|