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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/whale +93 -0
  3. data/lib/whale.rb +280 -0
  4. 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: []