cr.rb 3.18.2 → 3.20.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 +4 -4
  2. data/bin/cr +725 -716
  3. data/lib/cr.rb +13 -13
  4. metadata +6 -6
data/bin/cr CHANGED
@@ -1,716 +1,725 @@
1
- #!/usr/bin/env ruby
2
- # coding: utf-8
3
- # ------------------------------------------------------
4
- # File : cr.rb
5
- # Authors : ccmywish <ccmywish@qq.com>
6
- # Created on : <2021-07-08>
7
- # Last modified : <2022-04-15>
8
- #
9
- # cr:
10
- #
11
- # This file is used to explain a CRyptic command
12
- # or an acronym's real meaning in computer world or
13
- # other fields.
14
- #
15
- # ------------------------------------------------------
16
-
17
- require 'cr'
18
- require 'tomlrb'
19
- require 'fileutils'
20
-
21
- CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
- CRYPTIC_DEFAULT_DICTS = {
23
- computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
24
- common: "https://github.com/cryptic-resolver/cryptic_common.git",
25
- science: "https://github.com/cryptic-resolver/cryptic_science.git",
26
- economy: "https://github.com/cryptic-resolver/cryptic_economy.git",
27
- medicine: "https://github.com/cryptic-resolver/cryptic_medicine.git"
28
- }
29
-
30
-
31
- ####################
32
- # helper: for color
33
- ####################
34
-
35
- def bold(str) "\e[1m#{str}\e[0m" end
36
- def underline(str) "\e[4m#{str}\e[0m" end
37
- def red(str) "\e[31m#{str}\e[0m" end
38
- def green(str) "\e[32m#{str}\e[0m" end
39
- def yellow(str) "\e[33m#{str}\e[0m" end
40
- def blue(str) "\e[34m#{str}\e[0m" end
41
- def purple(str) "\e[35m#{str}\e[0m" end
42
- def cyan(str) "\e[36m#{str}\e[0m" end
43
-
44
-
45
- ####################
46
- # core: logic
47
- ####################
48
-
49
- def is_there_any_dict?
50
- unless Dir.exist? CRYPTIC_RESOLVER_HOME
51
- Dir.mkdir CRYPTIC_RESOLVER_HOME
52
- end
53
-
54
- !Dir.empty? CRYPTIC_RESOLVER_HOME
55
- end
56
-
57
-
58
- def add_default_dicts_if_none_exists
59
- unless is_there_any_dict?
60
- puts "cr: Adding default dictionaries..."
61
-
62
- begin
63
- if RUBY_PLATFORM.include? "mingw"
64
- # Windows doesn't have fork
65
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
66
- puts "cr: Pulling cryptic_#{key}..."
67
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
68
- end
69
- else
70
- # *nix
71
- CRYPTIC_DEFAULT_DICTS.each do |key, dict|
72
- fork do
73
- puts "cr: Pulling cryptic_#{key}..."
74
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
75
- end
76
- end
77
- Process.waitall
78
- end
79
-
80
- rescue Interrupt
81
- puts "cr: Cancel add default dicts"
82
- exit 1
83
- end
84
-
85
- puts "cr: Add done"
86
- word_count(p: false)
87
- puts
88
- puts "#{$WordCount} words added"
89
-
90
- # Really added
91
- return true
92
- end
93
- # Not added
94
- return false
95
- end
96
-
97
-
98
- def update_dicts()
99
- return if add_default_dicts_if_none_exists
100
-
101
- word_count(p: false)
102
- old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
103
-
104
- puts "cr: Updating all dictionaries..."
105
-
106
- begin
107
- Dir.chdir CRYPTIC_RESOLVER_HOME do
108
-
109
- if RUBY_PLATFORM.include? "mingw"
110
- # Windows doesn't have fork
111
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
112
- puts "cr: Wait to update #{dict}..."
113
- `git -C ./#{dict} pull -q`
114
- end
115
- else
116
- # *nix
117
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
118
- fork do
119
- puts "cr: Wait to update #{dict}..."
120
- `git -C ./#{dict} pull -q`
121
- end
122
- end
123
- Process.waitall
124
-
125
- end # end if/else
126
- end
127
-
128
- rescue Interrupt
129
- puts "cr: Cancel update"
130
- exit 1
131
- end
132
-
133
-
134
- puts "cr: Update done"
135
-
136
- # clear
137
- $DefaultWordCount, $WordCount = 0, 0
138
- # recount
139
- word_count(p: false)
140
- new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
141
- diff = []
142
- new_wc.each_with_index do
143
- diff[_2] = _1 - old_wc[_2]
144
- end
145
-
146
- puts
147
- puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
148
-
149
- end
150
-
151
-
152
- def add_dict(repo)
153
- if repo.nil?
154
- puts bold(red("cr: Need an argument!"))
155
- exit -1
156
- end
157
-
158
- # Ensure the cr home dir exists
159
- FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
160
-
161
- begin
162
- puts "cr: Adding new dictionary..."
163
- `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
164
- rescue Interrupt
165
- puts "cr: Cancel add dict"
166
- exit 1
167
- end
168
-
169
- puts "cr: Add new dictionary done"
170
-
171
- # github/com/ccmywish/ruby_knowledge(.git)
172
- dict = repo.split('/')[-1].delete_suffix('.git')
173
- count_dict_words(dict)
174
- puts
175
- puts "#$WordCount words added"
176
-
177
- end
178
-
179
-
180
- def del_dict(repo)
181
- if repo.nil?
182
- puts bold(red("cr: Need an argument!"))
183
- exit -1
184
- end
185
- Dir.chdir CRYPTIC_RESOLVER_HOME do
186
- begin
187
- # Dir.rmdir repo # Can't rm a filled dir
188
- # FileUtils.rmdir repo # Can't rm a filled dir
189
- FileUtils.rm_rf repo
190
- puts "cr: Delete dictionary #{bold(green(repo))} done"
191
- rescue Exception => e
192
- puts bold(red("cr: #{e}"))
193
- list_dictionaries
194
- end
195
- end
196
- end
197
-
198
-
199
- def load_sheet(dict, sheet_name)
200
- file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
201
-
202
- if File.exist? file
203
- return Tomlrb.load_file file # gem 'tomlrb'
204
- # return TOML.load_file file # gem 'toml'
205
- else
206
- nil
207
- end
208
- end
209
-
210
-
211
- #
212
- # Pretty print the info of the given word
213
- #
214
- # A info looks like this
215
- # emacs = {
216
- # name = "Emacs"
217
- # desc = "edit macros"
218
- # more = "a feature-rich editor"
219
- # see = ["Vim"]
220
- # }
221
- #
222
- # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
223
- #
224
- def pp_info(info)
225
- name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
226
-
227
- desc = info['desc']
228
- more = info['more']
229
-
230
- if desc
231
- puts "\n #{name}: #{desc}"
232
- print "\n ",more,"\n" if more
233
- else
234
- puts "\n #{name}"
235
- print "\n ",more,"\n" if more
236
- end
237
-
238
- if see_also = info['see']
239
- print "\n", purple("SEE ALSO ")
240
- see_also.each {|x| print underline(x),' '}
241
- puts
242
- end
243
- puts
244
- end
245
-
246
- # Print default cryptic_ dictionaries
247
- def pp_dict(dict)
248
- puts green("From: #{dict}")
249
- end
250
-
251
-
252
- #
253
- # Used for synonym jump
254
- # Because we absolutely jump to a must-have word
255
- # So we can directly lookup to it
256
- #
257
- # Notice that, we must jump to a specific word definition
258
- # So in the toml file, you must specify the precise word.
259
- # If it has multiple meanings, for example
260
- #
261
- # [blah]
262
- # same = "xdg" # this is wrong, because xdg has multiple
263
- # # definitions, and all of them specify a
264
- # # category
265
- #
266
- # [blah]
267
- # same = "XDG downloader <=>xdg.Download" # this is correct
268
- #
269
- # [blah]
270
- # name = "BlaH" # You may want to display a better name first
271
- # same = "XDG downloader <=>xdg.Download" # this is correct
272
- #
273
- #
274
- def pp_same_info(dict, word, cache_content, same_key, own_name)
275
-
276
- # If it's a synonym for anther word,
277
- # we should lookup into this dict again, but maybe with a different file
278
-
279
- # file name
280
- x = word.chr.downcase
281
-
282
- #
283
- # dictionary maintainer must obey the rule for xxx.yyy word:
284
- # xxx should be lower case
285
- # yyy can be any case
286
- #
287
- # Because yyy should clearly explain the category info, IBM is better than ibm
288
- # Implementation should not be too simple if we want to stress the function we
289
- # expect.
290
- #
291
- # 'jump to' will output to user, so this is important not only inside our sheet.
292
- #
293
- # same = "XDM downloader<=>xdm.Download"
294
- #
295
- # We split 'same' key into two parts via spaceship symbol <=>, first part will
296
- # output to user, the second part is for internal jump.
297
- #
298
-
299
- jump_to, same = same_key.split("<=>")
300
- same = jump_to if same.nil?
301
-
302
- unless own_name
303
- own_name = word
304
- end
305
- puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
306
-
307
- #
308
- # As '.' is used to delimit a word and a category, what if
309
- # we jump to a dotted word?
310
- #
311
- # [eg]
312
- # same = "e.g." # this must lead to a wrong resolution to
313
- # # word 'e', category 'g'
314
- #
315
- # All you need is to be like this:
316
- #
317
- # [eg]
318
- # same = "'e.g.'" # cr will notice the single quote
319
- #
320
-
321
- if same =~ /^'(.*)'$/
322
- same, category = $1, nil
323
- else
324
- same, category = same.split('.')
325
- end
326
-
327
- if same.chr == x
328
- # No need to load another dictionary if match
329
- sheet_content = cache_content
330
- else
331
- sheet_content = load_sheet(dict, same.chr.downcase)
332
- end
333
-
334
- if category.nil?
335
- info = sheet_content[same]
336
- else
337
- info = sheet_content[same][category]
338
- end
339
-
340
- if info.nil?
341
- puts red("Warn: Synonym jumps to the wrong place `#{same}`,
342
- Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
343
- # exit
344
- return false
345
- # double or more jumps
346
- elsif same_key = info['same']
347
- own_name = info['name']
348
- return pp_same_info(dict, same, cache_content, same_key, own_name)
349
- else
350
- pp_info(info)
351
- return true
352
- end
353
- end
354
-
355
-
356
-
357
- #
358
- # Lookup the given word in a sheet (a toml file) and also print.
359
- # The core idea is that:
360
- #
361
- # 1. if the word is `same` with another synonym, it will directly jump to
362
- # a word in this dictionary, but maybe a different sheet.
363
- #
364
- # 2. load the toml file and check the given word
365
- # 2.1 with no category specifier
366
- # [abcd]
367
- # 2.2 with category specifier
368
- # [abcd.tYPe]
369
- #
370
- def lookup(dict, file, word)
371
- sheet_content = load_sheet(dict, file)
372
- return false if sheet_content.nil?
373
-
374
- info = sheet_content[word]
375
- return false if info.nil?
376
-
377
- # Warn if the info is empty. For example:
378
- # emacs = { }
379
- if info.size == 0
380
- puts red("WARN: Lack of everything of the given word
381
- Please consider fixing this in the dict `#{dict}`")
382
- exit
383
- end
384
-
385
-
386
-
387
- # Word with no category specifier
388
- # We call this meaning as type 1
389
- type_1_exist_flag = false
390
-
391
- # if already jump, don't check the word itself
392
- is_jump = false
393
-
394
- # synonym info print
395
- if same_key = info['same']
396
- own_name = info['name']
397
- pp_dict(dict)
398
- pp_same_info(dict, word, sheet_content, same_key, own_name)
399
- # It's also a type 1
400
- type_1_exist_flag = true
401
- is_jump = true
402
- end
403
-
404
- # normal info print
405
- # To developer:
406
- # The word should at least has one of `desc` and `more`
407
- # But when none exists, this may not be considered wrong,
408
- # Because the type2 make the case too.
409
- #
410
- # So, just ignore it, even if it's really a mistake(insignificant)
411
- # by dictionary maintainers.
412
- #
413
- if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
414
- pp_dict(dict)
415
- pp_info(info)
416
- type_1_exist_flag = true
417
- end
418
-
419
- # Meanings with category specifier
420
- # We call this meaning as type 2
421
- categories = info.keys - ["name", "desc", "more", "same", "see"]
422
-
423
- if !categories.empty?
424
-
425
- if type_1_exist_flag
426
- print blue(bold("OR")),"\n"
427
- else
428
- pp_dict(dict)
429
- end
430
-
431
- categories.each do |meaning|
432
- info0 = sheet_content[word][meaning]
433
- if same_key = info0['same']
434
- own_name = info0['name']
435
- pp_same_info(dict, word, sheet_content, same_key, own_name)
436
- else
437
- pp_info(info0)
438
- end
439
-
440
- # last meaning doesn't show this separate line
441
- print blue(bold("OR")),"\n" unless categories.last == meaning
442
- end
443
- return true
444
- elsif type_1_exist_flag
445
- return true
446
- else
447
- return false
448
- end
449
- end
450
-
451
-
452
- #
453
- # The main procedure of `cr`
454
- # 1. Search the default's first dict first
455
- # 2. Search the rest dictionaries in the cryptic dictionaries default dir
456
- #
457
- # The `search` is done via the `lookup` function. It will print
458
- # the info while finding. If `lookup` always return false then
459
- # means lacking of this word in our dictionaries. So a welcomed
460
- # contribution is printed on the screen.
461
- #
462
- def solve_word(word)
463
-
464
- add_default_dicts_if_none_exists
465
-
466
- word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
467
- # The index is the toml file we'll look into
468
- index = word.chr
469
- case index
470
- when '0'..'9'
471
- index = '0123456789'
472
- end
473
-
474
- # Default's first should be 1st to consider
475
- first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
476
-
477
- # cache lookup results
478
- results = []
479
- results << lookup(first_dict,index,word)
480
- # return if result == true # We should consider all dicts
481
-
482
- # Then else
483
- rest = Dir.children(CRYPTIC_RESOLVER_HOME)
484
- rest.delete first_dict
485
- rest.each do |dict|
486
- results << lookup(dict,index,word)
487
- # continue if result == false # We should consider all dicts
488
- end
489
-
490
- unless results.include? true
491
- puts <<-NotFound
492
- cr: Not found anything.
493
-
494
- You may use `cr -u` to update all dictionaries.
495
- Or you could contribute to:
496
-
497
- 1. computer: #{CRYPTIC_DEFAULT_DICTS[:computer]}
498
- 2. common: #{CRYPTIC_DEFAULT_DICTS[:common]}
499
- 3. science: #{CRYPTIC_DEFAULT_DICTS[:science]}
500
- 4. economy: #{CRYPTIC_DEFAULT_DICTS[:economy]}
501
- 5. medicine: #{CRYPTIC_DEFAULT_DICTS[:medicine]}
502
-
503
- NotFound
504
-
505
- else
506
- return
507
- end
508
-
509
- end
510
-
511
-
512
-
513
- #
514
- # The search word process is quite like `solve_word``
515
- # Notice:
516
- # We handle two cases
517
- #
518
- # 1. the 'pattern' is the regexp itself
519
- # 2. the 'pattern' is like '/blahblah/'
520
- #
521
- # The second is what Ruby and Perl users like to do, handle it!
522
- #
523
- def search_word(pattern)
524
-
525
- if pattern.nil?
526
- puts bold(red("cr: Need an argument!"))
527
- exit -1
528
- end
529
-
530
- add_default_dicts_if_none_exists
531
-
532
- if pattern =~ /^\/(.*)\/$/
533
- regexp = %r[#$1]
534
- else
535
- regexp = %r[#{pattern}]
536
- end
537
-
538
- found = false
539
-
540
- #
541
- # Try to match every word in all dictionaries
542
- #
543
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
544
- sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
545
- _1.end_with?('.toml')
546
- end
547
-
548
- similar_words_in_a_dict = []
549
-
550
- sheets.each do |sheet|
551
- sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
552
-
553
- sheet_content.keys.each do
554
- if _1 =~ regexp
555
- found = true
556
- similar_words_in_a_dict << _1
557
- end
558
- end
559
- # end of each sheet in a dict
560
- end
561
-
562
- unless similar_words_in_a_dict.empty?
563
- pp_dict(dict)
564
- require 'ls_table'
565
- LsTable.ls(similar_words_in_a_dict) do |e|
566
- puts blue(e)
567
- end
568
- puts
569
- end
570
- end
571
- if !found
572
- puts red("cr: No words match with #{regexp.inspect}")
573
- puts
574
- end
575
- end
576
-
577
-
578
- def help
579
- word_count(p: false)
580
- user_words = $WordCount - $DefaultWordCount
581
- puts <<-HELP
582
- cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
583
-
584
- usage:
585
- cr -v => Print version
586
- cr -h => Print this help
587
- cr -c => Print word count
588
- cr -l => List local dictionaries
589
- cr -u => Update all dictionaries
590
- cr -a repo.git => Add a new dictionary
591
- cr -d cryptic_xx => Delete a dictionary
592
- cr -s pattern => Search words matched with pattern
593
- cr emacs => Edit macros: a feature-rich editor
594
-
595
- HELP
596
-
597
- add_default_dicts_if_none_exists
598
-
599
- end
600
-
601
-
602
- def print_version
603
- puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
604
- end
605
-
606
-
607
- def list_dictionaries
608
- Dir.chdir CRYPTIC_RESOLVER_HOME do
609
- Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
610
- puts "#{blue(i+1)}. #{bold(green(dict))}"
611
- end
612
- end
613
- end
614
-
615
-
616
- # All dictionaries word count
617
- $WordCount = 0
618
- # Default dictionaries word count
619
- $DefaultWordCount = 0
620
-
621
- def count_dict_words(dict)
622
-
623
- dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
624
-
625
- wc = 0
626
-
627
- Dir.children(dict_dir).each do |entry|
628
- next unless entry.end_with?('.toml')
629
- sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
630
- count = sheet_content.keys.count
631
-
632
- # puts "#{entry}: #{count}"
633
- wc = wc + count
634
- end
635
-
636
- $WordCount += wc
637
-
638
- return wc
639
-
640
- end
641
-
642
-
643
- def word_count(p:)
644
-
645
- # Always check before Dir.children (this method creates dir if not exists)
646
- is_there_any_dict?
647
-
648
- # real dicts in user's directory
649
- locals = []
650
- Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
651
- locals << dict
652
- end
653
-
654
- # pre-defined default
655
- defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
656
- "cryptic_#{s}"
657
- end
658
-
659
- # user may delete some default dicts
660
- defaults &= locals
661
-
662
- unless defaults.empty?
663
- puts(bold(green("Default dict: "))) if p
664
- defaults.each do |s|
665
- wc = count_dict_words(s)
666
- $DefaultWordCount += wc
667
- # With color, ljust not works, so we disable color
668
- puts(" #{s.ljust(17)}: #{wc}") if p
669
- end
670
- end
671
-
672
- users = locals - defaults
673
- user_words = 0
674
- unless users.empty?
675
- wc = 0
676
- puts(bold(blue("\nUser's dict:"))) if p
677
- users.each do |s|
678
- wc = count_dict_words(s)
679
- # no need to add to $WordCount,
680
- # because it's done in `count_dict_words` func
681
- puts(" #{s.ljust(17)}: #{wc}") if p
682
- end
683
-
684
- user_words = $WordCount - $DefaultWordCount
685
- end
686
-
687
- if p
688
- puts
689
- puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
690
- puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
691
- puts "#{$WordCount.to_s.rjust(4)} words altogether"
692
- end
693
- end
694
-
695
-
696
- ####################
697
- # main: CLI Handling
698
- ####################
699
- arg = ARGV.shift
700
-
701
- case arg
702
- when nil then help
703
- when '-v' then print_version
704
- when '-h' then help
705
- when '-l' then list_dictionaries
706
- when '-c' then word_count(p: true)
707
- when '-u' then update_dicts
708
- when '-s' then search_word ARGV.shift
709
- when '-a' then add_dict ARGV.shift
710
- when '-d' then del_dict ARGV.shift
711
- when '--help'
712
- help
713
- else
714
- solve_word arg
715
- end
716
-
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # ------------------------------------------------------
4
+ # File : cr.rb
5
+ # Authors : ccmywish <ccmywish@qq.com>
6
+ # Created on : <2021-07-08>
7
+ # Last modified : <2022-10-20>
8
+ #
9
+ # cr:
10
+ #
11
+ # This file is used to explain a CRyptic command
12
+ # or an acronym's real meaning in computer world or
13
+ # other fields.
14
+ #
15
+ # ------------------------------------------------------
16
+
17
+ require 'cr'
18
+ require 'tomlrb'
19
+ require 'fileutils'
20
+
21
+ CRYPTIC_RESOLVER_HOME = File.expand_path("~/.cryptic-resolver")
22
+ CRYPTIC_DEFAULT_DICTS = {
23
+ common: "https://github.com/cryptic-resolver/cryptic_common.git",
24
+ computer: "https://github.com/cryptic-resolver/cryptic_computer.git",
25
+ windows: "https://github.com/cryptic-resolver/cryptic_windows.git",
26
+ electronics: "https://github.com/cryptic-resolver/cryptic_electronics.git"
27
+ }
28
+
29
+
30
+ ####################
31
+ # helper: for color
32
+ ####################
33
+
34
+ def bold(str) "\e[1m#{str}\e[0m" end
35
+ def underline(str) "\e[4m#{str}\e[0m" end
36
+ def red(str) "\e[31m#{str}\e[0m" end
37
+ def green(str) "\e[32m#{str}\e[0m" end
38
+ def yellow(str) "\e[33m#{str}\e[0m" end
39
+ def blue(str) "\e[34m#{str}\e[0m" end
40
+ def purple(str) "\e[35m#{str}\e[0m" end
41
+ def cyan(str) "\e[36m#{str}\e[0m" end
42
+
43
+
44
+ ####################
45
+ # core: logic
46
+ ####################
47
+
48
+ def is_there_any_dict?
49
+ unless Dir.exist? CRYPTIC_RESOLVER_HOME
50
+ Dir.mkdir CRYPTIC_RESOLVER_HOME
51
+ end
52
+
53
+ !Dir.empty? CRYPTIC_RESOLVER_HOME
54
+ end
55
+
56
+
57
+ def add_default_dicts_if_none_exists
58
+ unless is_there_any_dict?
59
+ puts "cr: Adding default dictionaries..."
60
+
61
+ begin
62
+ if RUBY_PLATFORM.include? "mingw"
63
+ # Windows doesn't have fork
64
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
65
+ puts "cr: Pulling cryptic_#{key}..."
66
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
67
+ end
68
+ else
69
+ # *nix
70
+ CRYPTIC_DEFAULT_DICTS.each do |key, dict|
71
+ fork do
72
+ puts "cr: Pulling cryptic_#{key}..."
73
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{dict} -q`
74
+ end
75
+ end
76
+ Process.waitall
77
+ end
78
+
79
+ rescue Interrupt
80
+ puts "cr: Cancel add default dicts"
81
+ exit 1
82
+ end
83
+
84
+ puts "cr: Add done"
85
+ word_count(p: false)
86
+ puts
87
+ puts "#{$WordCount} words added"
88
+
89
+ # Really added
90
+ return true
91
+ end
92
+ # Not added
93
+ return false
94
+ end
95
+
96
+
97
+ def update_dicts()
98
+ return if add_default_dicts_if_none_exists
99
+
100
+ word_count(p: false)
101
+ old_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
102
+
103
+ puts "cr: Updating all dictionaries..."
104
+
105
+ begin
106
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
107
+
108
+ if RUBY_PLATFORM.include? "mingw"
109
+ # Windows doesn't have fork
110
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
111
+ puts "cr: Wait to update #{dict}..."
112
+ `git -C ./#{dict} pull -q`
113
+ end
114
+ else
115
+ # *nix
116
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
117
+ fork do
118
+ puts "cr: Wait to update #{dict}..."
119
+ `git -C ./#{dict} pull -q`
120
+ end
121
+ end
122
+ Process.waitall
123
+
124
+ end # end if/else
125
+ end
126
+
127
+ rescue Interrupt
128
+ puts "cr: Cancel update"
129
+ exit 1
130
+ end
131
+
132
+
133
+ puts "cr: Update done"
134
+
135
+ # clear
136
+ $DefaultWordCount, $WordCount = 0, 0
137
+ # recount
138
+ word_count(p: false)
139
+ new_wc = [$DefaultWordCount, $WordCount-$DefaultWordCount, $WordCount]
140
+ diff = []
141
+ new_wc.each_with_index do
142
+ diff[_2] = _1 - old_wc[_2]
143
+ end
144
+
145
+ puts
146
+ puts "#{diff.[]2} words added: default/#{diff.[]0} user/#{diff.[]1}"
147
+
148
+ end
149
+
150
+
151
+ def add_dict(repo)
152
+ if repo.nil?
153
+ puts bold(red("cr: Need an argument!"))
154
+ exit -1
155
+ end
156
+
157
+ # Ensure the cr home dir exists
158
+ FileUtils.mkdir_p(CRYPTIC_RESOLVER_HOME)
159
+
160
+ # Simplify adding dictionary
161
+ if !repo.start_with?("https://") and !repo.start_with?("git@")
162
+ repo = "https://github.com/#{repo}.git"
163
+ end
164
+
165
+ begin
166
+ puts "cr: Adding new dictionary..."
167
+ `git -C #{CRYPTIC_RESOLVER_HOME} clone #{repo} -q`
168
+ rescue Interrupt
169
+ puts "cr: Cancel add dict"
170
+ exit 1
171
+ end
172
+
173
+ puts "cr: Add new dictionary done"
174
+
175
+ # github/com/ccmywish/ruby_knowledge(.git)
176
+ dict = repo.split('/')[-1].delete_suffix('.git')
177
+ count_dict_words(dict)
178
+ puts
179
+ puts "#$WordCount words added"
180
+
181
+ end
182
+
183
+
184
+ def del_dict(repo)
185
+ if repo.nil?
186
+ puts bold(red("cr: Need an argument!"))
187
+ exit -1
188
+ end
189
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
190
+ begin
191
+ # Dir.rmdir repo # Can't rm a filled dir
192
+ # FileUtils.rmdir repo # Can't rm a filled dir
193
+ FileUtils.rm_rf repo
194
+ puts "cr: Delete dictionary #{bold(green(repo))} done"
195
+ rescue Exception => e
196
+ puts bold(red("cr: #{e}"))
197
+ list_dictionaries
198
+ end
199
+ end
200
+ end
201
+
202
+
203
+ def load_sheet(dict, sheet_name)
204
+ file = CRYPTIC_RESOLVER_HOME + "/#{dict}/#{sheet_name}.toml"
205
+
206
+ if File.exist? file
207
+ return Tomlrb.load_file file # gem 'tomlrb'
208
+ # return TOML.load_file file # gem 'toml'
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+
215
+ #
216
+ # Pretty print the info of the given word
217
+ #
218
+ # A info looks like this
219
+ # emacs = {
220
+ # name = "Emacs"
221
+ # desc = "edit macros"
222
+ # more = "a feature-rich editor"
223
+ # see = ["Vim"]
224
+ # }
225
+ #
226
+ # @param info [Hash] the information of the given word (mapped to a keyword in TOML)
227
+ #
228
+ def pp_info(info)
229
+ name = info['name'] || red("No name!") # keyword `or` is invalid here in Ruby
230
+
231
+ desc = info['desc']
232
+ more = info['more']
233
+
234
+ if desc
235
+ puts "\n #{name}: #{desc}"
236
+ print "\n ",more,"\n" if more
237
+ else
238
+ puts "\n #{name}"
239
+ print "\n ",more,"\n" if more
240
+ end
241
+
242
+ if see_also = info['see']
243
+ print "\n", purple("SEE ALSO ")
244
+ if see_also.is_a?(Array)
245
+ see_also.each {|x| print underline(x),' '}
246
+ else
247
+ print underline(see_also),' '
248
+ end
249
+ puts
250
+ end
251
+ puts
252
+ end
253
+
254
+ # Print default cryptic_ dictionaries
255
+ def pp_dict(dict)
256
+ puts green("From: #{dict}")
257
+ end
258
+
259
+
260
+ #
261
+ # Used for synonym jump
262
+ # Because we absolutely jump to a must-have word
263
+ # So we can directly lookup to it
264
+ #
265
+ # Notice that, we must jump to a specific word definition
266
+ # So in the toml file, you must specify the precise word.
267
+ # If it has multiple meanings, for example
268
+ #
269
+ # [blah]
270
+ # same = "xdg" # this is wrong, because xdg has multiple
271
+ # # definitions, and all of them specify a
272
+ # # category
273
+ #
274
+ # [blah]
275
+ # same = "XDG downloader <=>xdg.Download" # this is correct
276
+ #
277
+ # [blah]
278
+ # name = "BlaH" # You may want to display a better name first
279
+ # same = "XDG downloader <=>xdg.Download" # this is correct
280
+ #
281
+ #
282
+ def pp_same_info(dict, word, cache_content, same_key, own_name)
283
+
284
+ # If it's a synonym for anther word,
285
+ # we should lookup into this dict again, but maybe with a different file
286
+
287
+ # file name
288
+ x = word.chr.downcase
289
+
290
+ #
291
+ # dictionary maintainer must obey the rule for xxx.yyy word:
292
+ # xxx should be lower case
293
+ # yyy can be any case
294
+ #
295
+ # Because yyy should clearly explain the category info, IBM is better than ibm
296
+ # Implementation should not be too simple if we want to stress the function we
297
+ # expect.
298
+ #
299
+ # 'jump to' will output to user, so this is important not only inside our sheet.
300
+ #
301
+ # same = "XDM downloader<=>xdm.Download"
302
+ #
303
+ # We split 'same' key into two parts via spaceship symbol <=>, first part will
304
+ # output to user, the second part is for internal jump.
305
+ #
306
+
307
+ jump_to, same = same_key.split("<=>")
308
+ same = jump_to if same.nil?
309
+
310
+ unless own_name
311
+ own_name = word
312
+ end
313
+ puts blue(bold(own_name)) + ' redirects to ' + blue(bold(jump_to))
314
+
315
+ #
316
+ # As '.' is used to delimit a word and a category, what if
317
+ # we jump to a dotted word?
318
+ #
319
+ # [eg]
320
+ # same = "e.g." # this must lead to a wrong resolution to
321
+ # # word 'e', category 'g'
322
+ #
323
+ # All you need is to be like this:
324
+ #
325
+ # [eg]
326
+ # same = "'e.g.'" # cr will notice the single quote
327
+ #
328
+
329
+ if same =~ /^'(.*)'$/
330
+ same, category = $1, nil
331
+ else
332
+ same, category = same.split('.')
333
+ end
334
+
335
+ if same.chr == x
336
+ # No need to load another dictionary if match
337
+ sheet_content = cache_content
338
+ else
339
+ sheet_content = load_sheet(dict, same.chr.downcase)
340
+ end
341
+
342
+ if category.nil?
343
+ info = sheet_content[same]
344
+ else
345
+ info = sheet_content[same][category]
346
+ end
347
+
348
+ if info.nil?
349
+ puts red("Warn: Synonym jumps to the wrong place `#{same}`,
350
+ Please consider fixing this in `#{x}.toml` of the dictionary `#{dict}`")
351
+ # exit
352
+ return false
353
+ # double or more jumps
354
+ elsif same_key = info['same']
355
+ own_name = info['name']
356
+ return pp_same_info(dict, same, cache_content, same_key, own_name)
357
+ else
358
+ pp_info(info)
359
+ return true
360
+ end
361
+ end
362
+
363
+
364
+
365
+ #
366
+ # Lookup the given word in a sheet (a toml file) and also print.
367
+ # The core idea is that:
368
+ #
369
+ # 1. if the word is `same` with another synonym, it will directly jump to
370
+ # a word in this dictionary, but maybe a different sheet.
371
+ #
372
+ # 2. load the toml file and check the given word
373
+ # 2.1 with no category specifier
374
+ # [abcd]
375
+ # 2.2 with category specifier
376
+ # [abcd.tYPe]
377
+ #
378
+ def lookup(dict, file, word)
379
+ sheet_content = load_sheet(dict, file)
380
+ return false if sheet_content.nil?
381
+
382
+ info = sheet_content[word]
383
+ return false if info.nil?
384
+
385
+ # Warn if the info is empty. For example:
386
+ # emacs = { }
387
+ if info.size == 0
388
+ puts red("WARN: Lack of everything of the given word
389
+ Please consider fixing this in the dict `#{dict}`")
390
+ exit
391
+ end
392
+
393
+
394
+
395
+ # Word with no category specifier
396
+ # We call this meaning as type 1
397
+ type_1_exist_flag = false
398
+
399
+ # if already jump, don't check the word itself
400
+ is_jump = false
401
+
402
+ # synonym info print
403
+ if same_key = info['same']
404
+ own_name = info['name']
405
+ pp_dict(dict)
406
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
407
+ # It's also a type 1
408
+ type_1_exist_flag = true
409
+ is_jump = true
410
+ end
411
+
412
+ # normal info print
413
+ # To developer:
414
+ # The word should at least has one of `desc` and `more`
415
+ # But when none exists, this may not be considered wrong,
416
+ # Because the type2 make the case too.
417
+ #
418
+ # So, just ignore it, even if it's really a mistake(insignificant)
419
+ # by dictionary maintainers.
420
+ #
421
+ if !is_jump && (info.has_key?('desc') || info.has_key?('more'))
422
+ pp_dict(dict)
423
+ pp_info(info)
424
+ type_1_exist_flag = true
425
+ end
426
+
427
+ # Meanings with category specifier
428
+ # We call this meaning as type 2
429
+ categories = info.keys - ["name", "desc", "more", "same", "see"]
430
+
431
+ if !categories.empty?
432
+
433
+ if type_1_exist_flag
434
+ print blue(bold("OR")),"\n"
435
+ else
436
+ pp_dict(dict)
437
+ end
438
+
439
+ categories.each do |meaning|
440
+ info0 = sheet_content[word][meaning]
441
+ if same_key = info0['same']
442
+ own_name = info0['name']
443
+ pp_same_info(dict, word, sheet_content, same_key, own_name)
444
+ else
445
+ pp_info(info0)
446
+ end
447
+
448
+ # last meaning doesn't show this separate line
449
+ print blue(bold("OR")),"\n" unless categories.last == meaning
450
+ end
451
+ return true
452
+ elsif type_1_exist_flag
453
+ return true
454
+ else
455
+ return false
456
+ end
457
+ end
458
+
459
+
460
+ #
461
+ # The main procedure of `cr`
462
+ # 1. Search the default's first dict first
463
+ # 2. Search the rest dictionaries in the cryptic dictionaries default dir
464
+ #
465
+ # The `search` is done via the `lookup` function. It will print
466
+ # the info while finding. If `lookup` always return false then
467
+ # means lacking of this word in our dictionaries. So a welcomed
468
+ # contribution is printed on the screen.
469
+ #
470
+ def solve_word(word)
471
+
472
+ add_default_dicts_if_none_exists
473
+
474
+ word = word.downcase # downcase! would lead to frozen error in Ruby 2.7.2
475
+ # The index is the toml file we'll look into
476
+ index = word.chr
477
+ case index
478
+ when '0'..'9'
479
+ index = '0123456789'
480
+ end
481
+
482
+ # Default's first should be 1st to consider
483
+ first_dict = "cryptic_" + CRYPTIC_DEFAULT_DICTS.keys[0].to_s # When Ruby3, We can use DICTS.key(0)
484
+
485
+ # cache lookup results
486
+ results = []
487
+ results << lookup(first_dict,index,word)
488
+ # return if result == true # We should consider all dicts
489
+
490
+ # Then else
491
+ rest = Dir.children(CRYPTIC_RESOLVER_HOME)
492
+ rest.delete first_dict
493
+ rest.each do |dict|
494
+ results << lookup(dict,index,word)
495
+ # continue if result == false # We should consider all dicts
496
+ end
497
+
498
+ unless results.include? true
499
+ puts <<-NotFound
500
+ cr: Not found anything.
501
+
502
+ You may
503
+ 1. Use `cr -u` to update all dicts
504
+ 2. Use `cr -a user/repo` to add more available dicts
505
+
506
+ See: https://github.com/cryptic-resolver
507
+
508
+ 3. Contribute to theses dicts
509
+
510
+ NotFound
511
+
512
+ else
513
+ return
514
+ end
515
+
516
+ end
517
+
518
+
519
+
520
+ #
521
+ # The search word process is quite like `solve_word``
522
+ # Notice:
523
+ # We handle two cases
524
+ #
525
+ # 1. the 'pattern' is the regexp itself
526
+ # 2. the 'pattern' is like '/blahblah/'
527
+ #
528
+ # The second is what Ruby and Perl users like to do, handle it!
529
+ #
530
+ def search_word(pattern)
531
+
532
+ if pattern.nil?
533
+ puts bold(red("cr: Need an argument!"))
534
+ exit -1
535
+ end
536
+
537
+ add_default_dicts_if_none_exists
538
+
539
+ if pattern =~ /^\/(.*)\/$/
540
+ regexp = %r[#$1]
541
+ else
542
+ regexp = %r[#{pattern}]
543
+ end
544
+
545
+ found = false
546
+
547
+ #
548
+ # Try to match every word in all dictionaries
549
+ #
550
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
551
+ sheets = Dir.children(File.join(CRYPTIC_RESOLVER_HOME, dict)).select do
552
+ _1.end_with?('.toml')
553
+ end
554
+
555
+ similar_words_in_a_dict = []
556
+
557
+ sheets.each do |sheet|
558
+ sheet_content = load_sheet(dict, File.basename(sheet,'.toml'))
559
+
560
+ sheet_content.keys.each do
561
+ if _1 =~ regexp
562
+ found = true
563
+ similar_words_in_a_dict << _1
564
+ end
565
+ end
566
+ # end of each sheet in a dict
567
+ end
568
+
569
+ unless similar_words_in_a_dict.empty?
570
+ pp_dict(dict)
571
+ require 'ls_table'
572
+ LsTable.ls(similar_words_in_a_dict) do |e|
573
+ puts blue(e)
574
+ end
575
+ puts
576
+ end
577
+ end
578
+ if !found
579
+ puts red("cr: No words match with #{regexp.inspect}")
580
+ puts
581
+ end
582
+ end
583
+
584
+
585
+ def help
586
+ word_count(p: false)
587
+ user_words = $WordCount - $DefaultWordCount
588
+ puts <<-HELP
589
+ cr: Cryptic Resolver v#{CR_GEM_VERSION} (#{$WordCount} words: default/#{$DefaultWordCount} user/#{user_words})
590
+
591
+ usage:
592
+
593
+ cr emacs => Edit macros: a feature-rich editor
594
+ cr -c => Print word count
595
+ cr -l => List local dictionaries
596
+ cr -u => Update all dictionaries
597
+ cr -a repo.git => Add a new dictionary
598
+ cr -a user/repo => Add a new dictionary on Github
599
+ cr -d cryptic_xx => Delete a dictionary
600
+ cr -s pattern => Search words matched with pattern
601
+ cr -v => Print version
602
+ cr -h => Print this help
603
+
604
+ HELP
605
+
606
+ add_default_dicts_if_none_exists
607
+
608
+ end
609
+
610
+
611
+ def print_version
612
+ puts "cr: Cryptic Resolver version #{CR_GEM_VERSION} in Ruby "
613
+ end
614
+
615
+
616
+ def list_dictionaries
617
+ Dir.chdir CRYPTIC_RESOLVER_HOME do
618
+ Dir.children(CRYPTIC_RESOLVER_HOME).each_with_index do |dict,i|
619
+ puts "#{blue(i+1)}. #{bold(green(dict))}"
620
+ end
621
+ end
622
+ end
623
+
624
+
625
+ # All dictionaries word count
626
+ $WordCount = 0
627
+ # Default dictionaries word count
628
+ $DefaultWordCount = 0
629
+
630
+ def count_dict_words(dict)
631
+
632
+ dict_dir = CRYPTIC_RESOLVER_HOME + "/#{dict}"
633
+
634
+ wc = 0
635
+
636
+ Dir.children(dict_dir).each do |entry|
637
+ next unless entry.end_with?('.toml')
638
+ sheet_content = load_sheet(dict, entry.delete_suffix('.toml'))
639
+ count = sheet_content.keys.count
640
+
641
+ # puts "#{entry}: #{count}"
642
+ wc = wc + count
643
+ end
644
+
645
+ $WordCount += wc
646
+
647
+ return wc
648
+
649
+ end
650
+
651
+
652
+ def word_count(p:)
653
+
654
+ # Always check before Dir.children (this method creates dir if not exists)
655
+ is_there_any_dict?
656
+
657
+ # real dicts in user's directory
658
+ locals = []
659
+ Dir.children(CRYPTIC_RESOLVER_HOME).each do |dict|
660
+ locals << dict
661
+ end
662
+
663
+ # pre-defined default
664
+ defaults = CRYPTIC_DEFAULT_DICTS.keys.map do |s|
665
+ "cryptic_#{s}"
666
+ end
667
+
668
+ # user may delete some default dicts
669
+ defaults &= locals
670
+
671
+ unless defaults.empty?
672
+ puts(bold(green("Default dicts: "))) if p
673
+ defaults.each do |s|
674
+ wc = count_dict_words(s)
675
+ $DefaultWordCount += wc
676
+ # With color, ljust not works, so we disable color
677
+ puts(" #{s.ljust(17)}: #{wc}") if p
678
+ end
679
+ end
680
+
681
+ users = locals - defaults
682
+ user_words = 0
683
+ unless users.empty?
684
+ wc = 0
685
+ puts(bold(blue("\nUser's dict:"))) if p
686
+ users.each do |s|
687
+ wc = count_dict_words(s)
688
+ # no need to add to $WordCount,
689
+ # because it's done in `count_dict_words` func
690
+ puts(" #{s.ljust(17)}: #{wc}") if p
691
+ end
692
+
693
+ user_words = $WordCount - $DefaultWordCount
694
+ end
695
+
696
+ if p
697
+ puts
698
+ puts "#{$DefaultWordCount.to_s.rjust(4)} words in default dictionaries"
699
+ puts "#{user_words.to_s.rjust(4)} words in user's dictionaries"
700
+ puts "#{$WordCount.to_s.rjust(4)} words altogether"
701
+ end
702
+ end
703
+
704
+
705
+ ####################
706
+ # main: CLI Handling
707
+ ####################
708
+ arg = ARGV.shift
709
+
710
+ case arg
711
+ when nil then help
712
+ when '-v' then print_version
713
+ when '-h' then help
714
+ when '-l' then list_dictionaries
715
+ when '-c' then word_count(p: true)
716
+ when '-u' then update_dicts
717
+ when '-s' then search_word ARGV.shift
718
+ when '-a' then add_dict ARGV.shift
719
+ when '-d' then del_dict ARGV.shift
720
+ when '--help'
721
+ help
722
+ else
723
+ solve_word arg
724
+ end
725
+