whale 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []