dorian 2.3.0 → 2.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/ci.yml +38 -0
  4. data/.gitignore +30 -0
  5. data/.node-version +1 -0
  6. data/.prettierignore +2 -0
  7. data/.rubocop.yml +129 -0
  8. data/.ruby-version +1 -0
  9. data/.tool-versions +3 -0
  10. data/Gemfile +20 -0
  11. data/Gemfile.lock +186 -0
  12. data/LICENSE +19 -0
  13. data/README.md +24 -0
  14. data/VERSION +1 -1
  15. data/bin/bundle +123 -0
  16. data/bin/bundle-audit +31 -0
  17. data/bin/bundler-audit +31 -0
  18. data/bin/rspec +31 -0
  19. data/bin/rubocop +31 -0
  20. data/dorian.gemspec +35 -0
  21. data/lib/dorian/bin.rb +921 -14
  22. data/package-lock.json +39 -0
  23. data/package.json +11 -0
  24. data/samples/books.json +33 -0
  25. data/samples/books.jsonl +3 -0
  26. data/samples/config.yml +27 -0
  27. data/samples/config_2.yml +22 -0
  28. data/samples/maths.js +1 -0
  29. data/samples/numbers.raw +10 -0
  30. data/samples/people.csv +6 -0
  31. data/samples/people.yml +28 -0
  32. data/samples/query.sql +7 -0
  33. data/samples/user.json +31 -0
  34. data/spec/dorian_spec.rb +276 -0
  35. data/spec/spec_helper.rb +3 -0
  36. data/vendor/groovy-beautify/LICENSE.MD +21 -0
  37. data/vendor/groovy-beautify/README.md +65 -0
  38. data/vendor/groovy-beautify/dist/__tests__/index.test.d.ts +1 -0
  39. data/vendor/groovy-beautify/dist/cjs/index.js +573 -0
  40. data/vendor/groovy-beautify/dist/cjs/package.json +3 -0
  41. data/vendor/groovy-beautify/dist/es/index.js +682 -0
  42. data/vendor/groovy-beautify/dist/es/package.json +3 -0
  43. data/vendor/groovy-beautify/dist/formatter/FormatRule.d.ts +14 -0
  44. data/vendor/groovy-beautify/dist/formatter/Formatter.d.ts +11 -0
  45. data/vendor/groovy-beautify/dist/formatter/GroovyFormatRules.d.ts +3 -0
  46. data/vendor/groovy-beautify/dist/formatter/__tests__/formatter.test.d.ts +1 -0
  47. data/vendor/groovy-beautify/dist/index.d.ts +5 -0
  48. data/vendor/groovy-beautify/dist/parser/GroovyParseRules.d.ts +3 -0
  49. data/vendor/groovy-beautify/dist/parser/ParseRule.d.ts +23 -0
  50. data/vendor/groovy-beautify/dist/parser/Parser.d.ts +29 -0
  51. data/vendor/groovy-beautify/dist/parser/__tests__/parser.test.d.ts +1 -0
  52. data/vendor/groovy-beautify/dist/parser/types.d.ts +6 -0
  53. data/vendor/groovy-beautify/dist/utils/text.d.ts +4 -0
  54. data/vendor/groovy-beautify/package.json +55 -0
  55. data/vendor/prettier/LICENSE +4379 -0
  56. data/vendor/prettier/README.md +109 -0
  57. data/vendor/prettier/bin/prettier.cjs +71 -0
  58. data/vendor/prettier/doc.d.ts +243 -0
  59. data/vendor/prettier/doc.js +1545 -0
  60. data/vendor/prettier/doc.mjs +1503 -0
  61. data/vendor/prettier/index.cjs +825 -0
  62. data/vendor/prettier/index.d.ts +941 -0
  63. data/vendor/prettier/index.mjs +25899 -0
  64. data/vendor/prettier/internal/cli.mjs +4366 -0
  65. data/vendor/prettier/package.json +198 -0
  66. data/vendor/prettier/plugins/acorn.d.ts +6 -0
  67. data/vendor/prettier/plugins/acorn.js +6618 -0
  68. data/vendor/prettier/plugins/acorn.mjs +6456 -0
  69. data/vendor/prettier/plugins/angular.d.ts +8 -0
  70. data/vendor/prettier/plugins/angular.js +2435 -0
  71. data/vendor/prettier/plugins/angular.mjs +2375 -0
  72. data/vendor/prettier/plugins/babel.d.ts +18 -0
  73. data/vendor/prettier/plugins/babel.js +14271 -0
  74. data/vendor/prettier/plugins/babel.mjs +13986 -0
  75. data/vendor/prettier/plugins/estree.d.ts +1 -0
  76. data/vendor/prettier/plugins/estree.js +9487 -0
  77. data/vendor/prettier/plugins/estree.mjs +9279 -0
  78. data/vendor/prettier/plugins/flow.d.ts +5 -0
  79. data/vendor/prettier/plugins/flow.js +51477 -0
  80. data/vendor/prettier/plugins/flow.mjs +51219 -0
  81. data/vendor/prettier/plugins/glimmer.d.ts +5 -0
  82. data/vendor/prettier/plugins/glimmer.js +8534 -0
  83. data/vendor/prettier/plugins/glimmer.mjs +8421 -0
  84. data/vendor/prettier/plugins/graphql.d.ts +5 -0
  85. data/vendor/prettier/plugins/graphql.js +2421 -0
  86. data/vendor/prettier/plugins/graphql.mjs +2347 -0
  87. data/vendor/prettier/plugins/html.d.ts +8 -0
  88. data/vendor/prettier/plugins/html.js +8182 -0
  89. data/vendor/prettier/plugins/html.mjs +8077 -0
  90. data/vendor/prettier/plugins/markdown.d.ts +7 -0
  91. data/vendor/prettier/plugins/markdown.js +9068 -0
  92. data/vendor/prettier/plugins/markdown.mjs +8977 -0
  93. data/vendor/prettier/plugins/meriyah.d.ts +5 -0
  94. data/vendor/prettier/plugins/meriyah.js +5953 -0
  95. data/vendor/prettier/plugins/meriyah.mjs +5800 -0
  96. data/vendor/prettier/plugins/postcss.d.ts +7 -0
  97. data/vendor/prettier/plugins/postcss.js +9244 -0
  98. data/vendor/prettier/plugins/postcss.mjs +9046 -0
  99. data/vendor/prettier/plugins/typescript.d.ts +5 -0
  100. data/vendor/prettier/plugins/typescript.js +38058 -0
  101. data/vendor/prettier/plugins/typescript.mjs +37732 -0
  102. data/vendor/prettier/plugins/yaml.d.ts +5 -0
  103. data/vendor/prettier/plugins/yaml.js +7425 -0
  104. data/vendor/prettier/plugins/yaml.mjs +7329 -0
  105. data/vendor/prettier/standalone.d.ts +33 -0
  106. data/vendor/prettier/standalone.js +3984 -0
  107. data/vendor/prettier/standalone.mjs +3938 -0
  108. data/vendor/sql-formatter.js +18762 -0
  109. metadata +207 -4
data/lib/dorian/bin.rb CHANGED
@@ -6,14 +6,82 @@ require "dorian/eval"
6
6
  require "dorian/progress"
7
7
  require "dorian/to_struct"
8
8
  require "git"
9
+ require "hexapdf"
9
10
  require "json"
11
+ require "mini_racer"
10
12
  require "net/http"
11
13
  require "parallel"
14
+ require "shellwords"
15
+ require "syntax_tree"
16
+ require "syntax_tree/erb"
17
+ require "syntax_tree/haml"
18
+ require "syntax_tree/xml"
19
+ require "syntax_tree/json"
20
+ require "tempfile"
21
+ require "terminal-table"
12
22
  require "uri"
13
23
  require "yaml"
14
24
 
15
25
  class Dorian
16
26
  class Bin
27
+ RUBY_EXTENSIONS = %w[
28
+ .rb
29
+ .arb
30
+ .axlsx
31
+ .builder
32
+ .fcgi
33
+ .gemfile
34
+ .gemspec
35
+ .god
36
+ .jb
37
+ .jbuilder
38
+ .mspec
39
+ .opal
40
+ .pluginspec
41
+ .podspec
42
+ .rabl
43
+ .rake
44
+ .rbuild
45
+ .rbw
46
+ .rbx
47
+ .ru
48
+ .ruby
49
+ .schema
50
+ .spec
51
+ .thor
52
+ .watchr
53
+ ].freeze
54
+
55
+ RUBY_FILENAMES = %w[
56
+ .irbrc
57
+ .pryrc
58
+ .simplecov
59
+ Appraisals
60
+ Berksfile
61
+ Brewfile
62
+ Buildfile
63
+ Capfile
64
+ Cheffile
65
+ Dangerfile
66
+ Deliverfile
67
+ Fastfile
68
+ Gemfile
69
+ Guardfile
70
+ Jarfile
71
+ Mavenfile
72
+ Podfile
73
+ Puppetfile
74
+ Rakefile
75
+ rakefile
76
+ Schemafile
77
+ Snapfile
78
+ Steepfile
79
+ Thorfile
80
+ Vagabondfile
81
+ Vagrantfile
82
+ buildfile
83
+ ].freeze
84
+
17
85
  VERSION = File.read(File.expand_path("../../VERSION", __dir__))
18
86
 
19
87
  DEFAULT_IO = :raw
@@ -51,8 +119,7 @@ class Dorian
51
119
  alias: :o
52
120
  },
53
121
  parallel: {
54
- alias: :p,
55
- default: true
122
+ alias: :p
56
123
  },
57
124
  parallel_type: {
58
125
  alias: :pt,
@@ -87,7 +154,9 @@ class Dorian
87
154
  alias: :d
88
155
  },
89
156
  progress: :boolean,
90
- headers: { default: true },
157
+ headers: {
158
+ default: true
159
+ },
91
160
  progress_format: {
92
161
  alias: :pf,
93
162
  type: :string
@@ -96,6 +165,7 @@ class Dorian
96
165
  default: true
97
166
  },
98
167
  io: :string,
168
+ self: :boolean,
99
169
  version: {
100
170
  alias: :v
101
171
  },
@@ -179,6 +249,107 @@ class Dorian
179
249
  arguments.delete("commit")
180
250
  @command = :commit
181
251
  command_commit
252
+ when :compare
253
+ arguments.delete("compare")
254
+ @command = :compare
255
+ command_compare
256
+ when :dir
257
+ arguments.delete("dir")
258
+ @command = :dir
259
+ command_dir
260
+ when :submodules
261
+ arguments.delete("submodules")
262
+ @command = :submodules
263
+ command_submodules
264
+ when :dot
265
+ arguments.delete("dot")
266
+ @command = :dot
267
+ command_dot
268
+ when :eval
269
+ arguments.delete("eval")
270
+ @command = :eval
271
+ command_eval
272
+ when :ls
273
+ arguments.delete("ls")
274
+ @command = :ls
275
+ command_ls
276
+ when :strip
277
+ arguments.delete("strip")
278
+ @command = :strip
279
+ command_strip
280
+ when :rstrip
281
+ arguments.delete("rstrip")
282
+ @command = :rstrip
283
+ command_rstrip
284
+ when :lstrip
285
+ arguments.delete("lstrip")
286
+ @command = :lstrip
287
+ command_lstrip
288
+ when :merge
289
+ arguments.delete("merge")
290
+ @command = :merge
291
+ command_merge
292
+ when :pluck
293
+ arguments.delete("pluck")
294
+ @command = :pluck
295
+ command_pluck
296
+ when :shuffle
297
+ arguments.delete("shuffle")
298
+ @command = :shuffle
299
+ command_shuffle
300
+ when :rename
301
+ arguments.delete("rename")
302
+ @command = :rename
303
+ command_rename
304
+ when :replace
305
+ arguments.delete("replace")
306
+ @command = :replace
307
+ command_replace
308
+ when :sort
309
+ arguments.delete("sort")
310
+ @command = :sort
311
+ command_sort
312
+ when :table
313
+ arguments.delete("table")
314
+ @command = :table
315
+ command_table
316
+ when :then
317
+ arguments.delete("then")
318
+ @command = :then
319
+ @ruby = arguments.delete_at(0)
320
+ command_then
321
+ when :times
322
+ arguments.delete("times")
323
+ @command = :times
324
+ command_times
325
+ when :uniq
326
+ arguments.delete("uniq")
327
+ @command = :uniq
328
+ command_uniq
329
+ when :write
330
+ arguments.delete("write")
331
+ @command = :write
332
+ command_write
333
+ when :release
334
+ arguments.delete("release")
335
+ @command = :release
336
+ command_release
337
+ when :top
338
+ arguments.delete("top")
339
+ @command = :top
340
+ command_top
341
+ when :tree
342
+ arguments.delete("tree")
343
+ @command = :tree
344
+ command_tree
345
+ when :format
346
+ arguments.delete("format")
347
+ @command = :format
348
+ command_format
349
+ when :pretty
350
+ arguments.delete("pretty")
351
+ @command = :pretty
352
+ command_pretty
182
353
  else
183
354
  arguments.delete("read")
184
355
  @command = :read
@@ -186,10 +357,219 @@ class Dorian
186
357
  end
187
358
  end
188
359
 
360
+ def command_pretty
361
+ command_format
362
+ end
363
+
364
+ def command_format
365
+ context = MiniRacer::Context.new
366
+ context.attach("puts", proc { |string| puts(string) })
367
+ context.attach("warn", proc { |string| warn(string) })
368
+ context.attach("read", proc { |path| File.read(path) })
369
+ context.attach(
370
+ "write",
371
+ proc { |path, content| File.write(path, content) }
372
+ )
373
+
374
+ root = File.expand_path("../../", __dir__)
375
+
376
+ prettier_path = File.join(root, "vendor/prettier/standalone.js")
377
+ prettier_js = File.read(prettier_path)
378
+ context.eval(prettier_js)
379
+
380
+ sql_path = File.join(root, "vendor/sql-formatter.js")
381
+ sql_js = File.read(sql_path)
382
+ context.eval("self = {};")
383
+ context.eval(sql_js)
384
+ context.eval("sqlFormatter = self.sqlFormatter;")
385
+
386
+ groovy_path = File.join(root, "vendor/groovy-beautify/dist/cjs/index.js")
387
+ groovy_js = File.read(groovy_path)
388
+ context.eval("module = { exports: {} };")
389
+ context.eval("exports = module.exports;")
390
+ context.eval(groovy_js)
391
+ context.eval("groovyBeautify = module.exports;")
392
+
393
+ plugins = %w[babel estree typescript html postcss markdown]
394
+
395
+ plugins.each do |plugin|
396
+ path = File.join(root, "vendor/prettier/plugins/#{plugin}.js")
397
+ js = File.read(path)
398
+ context.eval("module = { exports: {} };")
399
+ context.eval("exports = module.exports;")
400
+ context.eval(js)
401
+ context.eval("#{plugin} = module.exports;")
402
+ end
403
+
404
+ context.eval("plugins = [#{plugins.join(", ")}];")
405
+
406
+ context.eval(<<~JS)
407
+ format = async (path, parser) => {
408
+ try {
409
+ const before = read(path);
410
+ let after;
411
+
412
+ if (parser === "sql") {
413
+ after = sqlFormatter.format(before);
414
+ } else if (parser === "groovy") {
415
+ after = groovyBeautify(before);
416
+ } else {
417
+ after = await prettier.format(before, { parser, plugins });
418
+ }
419
+
420
+ if (before != after) {
421
+ puts(path);
422
+ write(path, after);
423
+ }
424
+ } catch (e) {
425
+ warn(`failed to parse ${path}: ${e.message.split("\\n")[0]}`);
426
+ }
427
+ };
428
+ JS
429
+
430
+ if files.any?
431
+ each(files) { |file| format(file, context:) }
432
+ else
433
+ each(
434
+ Git.open(".").ls_files.map(&:first)
435
+ ) { |file| format(file, context:) }
436
+ end
437
+ end
438
+
439
+ def command_release
440
+ File.delete(*Dir["*.gem"])
441
+ system("gem build")
442
+ system("gem push *.gem")
443
+ File.delete(*Dir["*.gem"])
444
+ end
445
+
446
+ def command_top
447
+ shell = arguments[0] || File.basename(ENV.fetch("SHELL", nil)) || "bash"
448
+ limit = arguments[1] || 10
449
+
450
+ history =
451
+ case shell.to_s.downcase
452
+ when "fish"
453
+ File
454
+ .read("#{Dir.home}/.local/share/fish/fish_history")
455
+ .lines
456
+ .grep(/^- cmd: /)
457
+ .map { |line| line.split("- cmd: ", 2).last.strip }
458
+ when "bash"
459
+ File.read("#{Dir.home}/.bash_history").lines.map(&:strip)
460
+ when "zsh"
461
+ File.read("#{Dir.home}/.zsh_history").lines.map(&:strip)
462
+ else
463
+ raise NotImplementedError, shell
464
+ end
465
+
466
+ table(
467
+ history
468
+ .map { |line| line.split.first }
469
+ .tally
470
+ .to_a
471
+ .sort_by(&:last)
472
+ .reverse
473
+ .map
474
+ .with_index do |(command, command_count), index|
475
+ {
476
+ "#" => index + 1,
477
+ :count => command_count,
478
+ :percent =>
479
+ "#{(command_count * 100 / history.size.to_f).round(3)}%",
480
+ :command => command
481
+ }
482
+ end
483
+ .first(limit)
484
+ )
485
+ end
486
+
487
+ def command_tree
488
+ space = " "
489
+ right = "└── "
490
+ down = "│   "
491
+ down_and_right = "├── "
492
+
493
+ git_ls_files = ->(path) { Git.open(".").ls_files(path).map(&:first) }
494
+
495
+ group =
496
+ lambda do |files|
497
+ files
498
+ .group_by { |file| file.split("/").first }
499
+ .transform_values do |values|
500
+ group.call(
501
+ values
502
+ .map { |value| value.split("/")[1..].join("/") }
503
+ .reject(&:empty?)
504
+ )
505
+ end
506
+ end
507
+
508
+ print =
509
+ lambda do |key:, values:, index: 0, size: 1, prefix: ""|
510
+ key = "#{key}/" if values.any?
511
+ last = index + 1 == size
512
+ right_prefix = last ? right : down_and_right
513
+ puts prefix + right_prefix + key
514
+ values.each.with_index do |(value_key, value_values), value_index|
515
+ print.call(
516
+ key: value_key,
517
+ values: value_values,
518
+ index: value_index,
519
+ size: values.size,
520
+ prefix: prefix + (last ? space : down)
521
+ )
522
+ end
523
+ end
524
+
525
+ keys = (arguments + files)
526
+ keys = ["."] unless keys.any?
527
+
528
+ keys.each do |key|
529
+ files =
530
+ git_ls_files
531
+ .call(key)
532
+ .map { |file| parsed.arguments.any? ? file.sub(key, "") : file }
533
+ values = group.call(files)
534
+ key = "#{key}/" if values.any? && key != "." && key[-1] != "/"
535
+ puts key
536
+ values.each.with_index do |(value_key, value_values), value_index|
537
+ print.call(
538
+ key: value_key,
539
+ values: value_values,
540
+ index: value_index,
541
+ size: values.size
542
+ )
543
+ end
544
+ end
545
+ end
546
+
189
547
  def files
190
548
  parsed.files
191
549
  end
192
550
 
551
+ def command_times
552
+ map(everything, &:to_i).sum.times { |index| puts index + 1 }
553
+ end
554
+
555
+ def command_eval
556
+ each(everything) { |thing| outputs(evaluates(ruby: thing)) }
557
+ end
558
+
559
+ def command_then
560
+ each(stdin_files + files) do |input|
561
+ outputs(evaluates(it: reads(File.read(input))), file: input)
562
+ end
563
+
564
+ each(stdin_arguments + arguments) do |input|
565
+ outputs(evaluates(it: reads(input)))
566
+ end
567
+ end
568
+
569
+ def command_table
570
+ table(map(everything) { |thing| lines(reads(thing)) }.inject(&:+))
571
+ end
572
+
193
573
  def command_chat
194
574
  puts completion(
195
575
  token: token(".chat"),
@@ -227,6 +607,39 @@ class Dorian
227
607
  puts message
228
608
  end
229
609
 
610
+ def command_replace
611
+ from, to = arguments
612
+
613
+ each(stdin_files + stdin_arguments + files) do |file|
614
+ next if File.directory?(file)
615
+
616
+ File.write(file, File.read(file).gsub(from, to))
617
+ end
618
+ end
619
+
620
+ def command_rename
621
+ from, to = arguments
622
+ files = stdin_files + stdin_arguments + self.files
623
+ (files - directories).each { |file| rename(file, file.gsub(from, to)) }
624
+ directories.each { |dir| rename(dir, dir.gsub(from, to)) }
625
+ end
626
+
627
+ def directories
628
+ (stdin_files + files).select { |file| File.directory?(file) }
629
+ end
630
+
631
+ def rename(old, new)
632
+ return if old == new
633
+
634
+ puts "#{old} -> #{new}"
635
+ File.rename(old, new)
636
+ end
637
+
638
+ def command_write
639
+ content = read_stdin.join
640
+ each(files + arguments) { |file| File.write(file, content) }
641
+ end
642
+
230
643
  def command_read
231
644
  each(stdin_files + files) do |input|
232
645
  outputs(reads(File.read(input)), file: input)
@@ -235,16 +648,179 @@ class Dorian
235
648
  each(stdin_arguments + arguments) { |input| outputs(reads(input)) }
236
649
  end
237
650
 
651
+ def command_strip
652
+ each(stdin_files + files) do |input|
653
+ outputs(lines(reads(File.read(input)), strip: :strip), file: input)
654
+ end
655
+
656
+ each(stdin_arguments + arguments) do |input|
657
+ outputs(lines(reads(input), strip: :strip))
658
+ end
659
+ end
660
+
661
+ def command_rstrip
662
+ each(stdin_files + files) do |input|
663
+ outputs(lines(reads(File.read(input)), strip: :rstrip), file: input)
664
+ end
665
+
666
+ each(stdin_arguments + arguments) do |input|
667
+ outputs(lines(reads(input), strip: :rstrip))
668
+ end
669
+ end
670
+
671
+ def command_lstrip
672
+ each(stdin_files + files) do |input|
673
+ outputs(lines(reads(File.read(input)), strip: :lstrip), file: input)
674
+ end
675
+
676
+ each(stdin_arguments + arguments) do |input|
677
+ outputs(lines(reads(input), strip: :lstrip))
678
+ end
679
+ end
680
+
238
681
  def everything
239
682
  read_stdin_files + stdin_arguments + read_files + arguments
240
683
  end
241
684
 
685
+ def command_dir
686
+ puts(
687
+ Git
688
+ .open(".")
689
+ .ls_files
690
+ .map(&:first)
691
+ .map { |path| path.split("/").first }
692
+ .select { |path| Dir.exist?(path) }
693
+ .reject { |path| path.start_with?(".") }
694
+ .sort
695
+ .uniq
696
+ )
697
+
698
+ puts "." if self?
699
+ end
700
+
701
+ def command_ls
702
+ puts(
703
+ Git
704
+ .open(".")
705
+ .ls_files
706
+ .map(&:first)
707
+ .map { |path| path.split("/").first }
708
+ .reject { |path| path.start_with?(".") }
709
+ .select { |path| match_filetypes?(path) }
710
+ .sort
711
+ .uniq
712
+ )
713
+
714
+ puts "." if self?
715
+ end
716
+
717
+ def command_submodules
718
+ puts(
719
+ File
720
+ .read(".gitmodules")
721
+ .lines
722
+ .grep(/path = /)
723
+ .map { |path| path.split("=").last.strip }
724
+ )
725
+
726
+ puts "." if self?
727
+ end
728
+
729
+ def command_dot
730
+ dir = files.first || arguments.first || "."
731
+
732
+ ignore_file = File.expand_path("#{dir.chomp("/")}/.dotignore")
733
+ ignore_content = File.exist?(ignore_file) ? File.read(ignore_file) : ""
734
+ ignore_patterns =
735
+ ignore_content
736
+ .lines
737
+ .map(&:strip)
738
+ .reject { |line| line.empty? || line.start_with?("#") }
739
+ .map { |pattern| Regexp.new("\\A#{pattern}\\z") }
740
+
741
+ Git
742
+ .open(dir)
743
+ .ls_files
744
+ .map(&:first)
745
+ .each do |file|
746
+ next if ignore_patterns.any? { |pattern| pattern.match?(file) }
747
+
748
+ homefile = "#{Dir.home}/#{file}"
749
+ dotfile = File.expand_path("#{dir.chomp("/")}/#{file}")
750
+ if File.exist?(homefile) || File.symlink?(homefile)
751
+ File.delete(homefile)
752
+ end
753
+ FileUtils.mkdir_p(File.dirname(homefile))
754
+ FileUtils.ln_s(dotfile, homefile, verbose: true)
755
+ end
756
+ end
757
+
758
+ def self?
759
+ !!options.self
760
+ end
761
+
762
+ def command_compare
763
+ file_1, file_2 = files
764
+ key_1, key_2 = arguments
765
+ read_1, read_2 =
766
+ files.map.with_index do |file, index|
767
+ read = reads(File.read(file))
768
+
769
+ if arguments[index] && read.from_deep_struct.key?(arguments[index])
770
+ read[arguments[index]]
771
+ elsif arguments[index]
772
+ nil
773
+ else
774
+ read
775
+ end
776
+ end
777
+
778
+ compare(read_1, read_2, file_1:, file_2:)
779
+ end
780
+
242
781
  def command_each
243
782
  each(everything) do |input|
244
783
  each(lines(reads(input)), progress: true) { |line| evaluates(it: line) }
245
784
  end
246
785
  end
247
786
 
787
+ def command_merge
788
+ outputs(map(everything) { |thing| lines(reads(thing)) }.inject(&:+))
789
+ end
790
+
791
+ def command_sort
792
+ outputs(
793
+ map(everything) { |thing| lines(reads(thing)) }
794
+ .inject(&:+)
795
+ .sort_by do |line|
796
+ result = pluck(line).from_deep_struct
797
+ result.is_a?(Hash) ? result.values : result
798
+ end
799
+ )
800
+ end
801
+
802
+ def command_uniq
803
+ outputs(
804
+ map(everything) { |thing| lines(reads(thing)) }
805
+ .inject(&:+)
806
+ .uniq { |line| pluck(line) }
807
+ )
808
+ end
809
+
810
+ def command_pluck
811
+ outputs(
812
+ map(everything) do |thing|
813
+ map(lines(reads(thing))) { |line| pluck(line) }
814
+ end.inject(&:+)
815
+ )
816
+ end
817
+
818
+ def command_shuffle
819
+ outputs(
820
+ map(everything) { |thing| lines(reads(thing)) }.inject(&:+).shuffle
821
+ )
822
+ end
823
+
248
824
  def command_tally
249
825
  each(everything) do |input|
250
826
  outputs(
@@ -253,7 +829,7 @@ class Dorian
253
829
  if ruby.to_s.empty?
254
830
  element
255
831
  else
256
- evaluates(it: element, returns: true, stdout: false).returned
832
+ evaluates(it: element, returns: true, stdout: false)
257
833
  end
258
834
  end.tally
259
835
  )
@@ -268,11 +844,13 @@ class Dorian
268
844
  end
269
845
 
270
846
  def command_append
271
- outputs(everything.sum { |input| lines(reads(input)) })
847
+ outputs(map(everything) { |input| lines(reads(input)) }.inject(&:+))
272
848
  end
273
849
 
274
850
  def command_prepend
275
- outputs(everything.reverse.sum { |input| lines(reads(input)) })
851
+ outputs(
852
+ map(everything.reverse) { |input| lines(reads(input)) }.inject(&:+)
853
+ )
276
854
  end
277
855
 
278
856
  def command_select
@@ -344,7 +922,7 @@ class Dorian
344
922
  def outputs(content, file: nil)
345
923
  if write? && file
346
924
  File.write(file, to_output(content))
347
- else
925
+ elsif !content.nil?
348
926
  puts to_output(content)
349
927
  end
350
928
  end
@@ -362,12 +940,14 @@ class Dorian
362
940
  end.join
363
941
  when :json
364
942
  pretty? ? JSON.pretty_generate(content) : content.to_json
365
- when :jsonl, :yamll
366
- map(content, &:to_json).join("\n")
943
+ when :jsonl
944
+ "#{map(content, &:to_json).join("\n")}\n"
367
945
  when :raw
368
946
  content
369
947
  when :yaml
370
948
  content.to_yaml
949
+ when :yamll
950
+ "#{map(map(content, &:to_yaml), &:to_json).join("\n")}\n"
371
951
  else
372
952
  abort "#{output.inspect} not supported"
373
953
  end
@@ -383,12 +963,16 @@ class Dorian
383
963
  end
384
964
  when :json
385
965
  JSON.parse(content).to_deep_struct
386
- when :jsonl, :yamll
966
+ when :jsonl
387
967
  map(content.lines) { |line| JSON.parse(line) }.to_deep_struct
388
968
  when :raw
389
969
  content
390
970
  when :yaml
391
971
  YAML.safe_load(content).to_deep_struct
972
+ when :yamll
973
+ map(content.lines) do |line|
974
+ YAML.safe_load(JSON.parse(line))
975
+ end.to_deep_struct
392
976
  else
393
977
  abort "#{input.inspect} not supported"
394
978
  end
@@ -597,9 +1181,9 @@ class Dorian
597
1181
  end
598
1182
  end
599
1183
 
600
- def lines(input)
1184
+ def lines(input, strip: :rstrip)
601
1185
  if input.is_a?(String)
602
- input.lines.map(&:rstrip)
1186
+ input.lines.map(&strip)
603
1187
  elsif deep?
604
1188
  deep_lines(input)
605
1189
  else
@@ -702,7 +1286,7 @@ class Dorian
702
1286
  end
703
1287
 
704
1288
  def match?(element, ruby: @ruby)
705
- !!evaluates(ruby:, it: element, stdout: false, returns: true).returned
1289
+ !!evaluates(ruby:, it: element, stdout: false, returns: true)
706
1290
  end
707
1291
 
708
1292
  def token(file)
@@ -750,6 +1334,57 @@ class Dorian
750
1334
  http.request(request).body
751
1335
  end
752
1336
 
1337
+ def compare(content_1, content_2, file_1:, file_2:, path: ".")
1338
+ content_1 = content_1.from_deep_struct
1339
+ content_2 = content_2.from_deep_struct
1340
+
1341
+ if content_1.is_a?(Hash) && content_2.is_a?(Hash)
1342
+ (content_1.keys + content_2.keys).uniq.each do |key|
1343
+ new_path = path == "." ? "#{path}#{key}" : "#{path}.#{key}"
1344
+
1345
+ if content_1[key] && !content_2[key]
1346
+ warn "#{new_path} present in #{file_1} but not in #{file_2}"
1347
+ next
1348
+ elsif !content_1[key] && content_2[key]
1349
+ warn "#{new_path} present in #{file_2} but not in #{file_1}"
1350
+ next
1351
+ end
1352
+
1353
+ compare(
1354
+ content_1[key],
1355
+ content_2[key],
1356
+ path: new_path,
1357
+ file_1:,
1358
+ file_2:
1359
+ )
1360
+ end
1361
+ elsif content_1.is_a?(Array) && content_2.is_a?(Array)
1362
+ (0...([content_1.size, content_2.size].max)).each do |index|
1363
+ new_path = "#{path}[#{index}]"
1364
+ if content_1[index] && !content_2[index]
1365
+ warn "#{new_path} present in #{file_1} but not in #{file_2}"
1366
+ next
1367
+ elsif !content_1[index] && content_2[index]
1368
+ warn "#{new_path} present in #{file_2} but not in #{file_1}"
1369
+ next
1370
+ end
1371
+
1372
+ compare(
1373
+ content_1[index],
1374
+ content_2[index],
1375
+ path: new_path,
1376
+ file_1:,
1377
+ file_2:
1378
+ )
1379
+ end
1380
+ elsif content_1.class != content_2.class
1381
+ warn(
1382
+ "#{path} has #{content_1.class} for #{file_1} " \
1383
+ "and #{content_2.class} for #{file_2}"
1384
+ )
1385
+ end
1386
+ end
1387
+
753
1388
  def short(string)
754
1389
  string[0..5000]
755
1390
  end
@@ -758,6 +1393,67 @@ class Dorian
758
1393
  Tiktoken.encoding_for_model("gpt-4o")
759
1394
  end
760
1395
 
1396
+ def match_filetypes?(path, filetypes: arguments)
1397
+ return true if filetypes.none?
1398
+ return true unless filetypes.intersect?(%w[rb ruby])
1399
+ return false if Dir.exist?(path)
1400
+ return true if RUBY_FILENAMES.include?(path)
1401
+ return true if RUBY_EXTENSIONS.include?(File.extname(path))
1402
+ return false unless File.exist?(path)
1403
+
1404
+ first_line = File.open(path, &:gets).to_s
1405
+ first_line = first_line.encode("UTF-8", invalid: :replace)
1406
+
1407
+ return true if /\A#!.*ruby\z/.match?(first_line)
1408
+
1409
+ false
1410
+ end
1411
+
1412
+ def pluck(element)
1413
+ element = element.from_deep_struct
1414
+
1415
+ keys =
1416
+ if arguments.any?
1417
+ arguments
1418
+ elsif element.is_a?(Hash)
1419
+ element.keys
1420
+ elsif element.is_a?(Array)
1421
+ (0...(element.size)).map(&:to_s)
1422
+ else
1423
+ "it"
1424
+ end
1425
+
1426
+ results =
1427
+ keys.map do |argument|
1428
+ if element.is_a?(Array) && argument.to_i.to_s == argument
1429
+ element[argument.to_i]
1430
+ elsif element.is_a?(Hash) && element.key?(argument)
1431
+ { argument => element[argument] }
1432
+ else
1433
+ evaluates(ruby: argument, it: element.to_deep_struct)
1434
+ end
1435
+ end
1436
+
1437
+ if results.all?(Hash)
1438
+ results.inject(&:merge).to_deep_struct
1439
+ else
1440
+ results
1441
+ .map { |result| result.is_a?(Hash) ? result.values.first : result }
1442
+ .to_deep_struct
1443
+ end
1444
+ end
1445
+
1446
+ def table(data)
1447
+ is_hashes = data.first.from_deep_struct.is_a?(Hash)
1448
+ headings = is_hashes ? data.first.to_h.keys : nil
1449
+ rows = is_hashes ? data.map(&:values) : data.map { |row| wrap(row) }
1450
+ if headings
1451
+ puts Terminal::Table.new(headings:, rows:)
1452
+ else
1453
+ puts Terminal::Table.new(rows:)
1454
+ end
1455
+ end
1456
+
761
1457
  def evaluates(
762
1458
  ruby: @ruby,
763
1459
  it: nil,
@@ -779,7 +1475,218 @@ class Dorian
779
1475
  rails:,
780
1476
  fast:,
781
1477
  returns:
782
- )
1478
+ ).returned
1479
+ end
1480
+
1481
+ def sort(object)
1482
+ object = object.from_deep_struct
1483
+
1484
+ if object.is_a?(Hash)
1485
+ object
1486
+ .to_a
1487
+ .sort_by(&:first)
1488
+ .to_h
1489
+ .transform_values { |value| sort(value) }
1490
+ elsif object.is_a?(Array)
1491
+ object.map { |element| sort(element) }
1492
+ else
1493
+ object
1494
+ end
1495
+ end
1496
+
1497
+ def filetype(path)
1498
+ ext = File.extname(path).to_s.downcase
1499
+ return :directory if Dir.exist?(path)
1500
+ return :symlink if File.symlink?(path)
1501
+ return :ruby if RUBY_FILENAMES.include?(File.basename(path))
1502
+ return :ruby if RUBY_EXTENSIONS.include?(ext)
1503
+ return :json if ext == ".json"
1504
+ return :jsonl if ext == ".jsonl"
1505
+ return :yaml if ext == ".yaml"
1506
+ return :yaml if ext == ".yml"
1507
+ return :yamll if ext == ".yamll"
1508
+ return :yamll if ext == ".ymll"
1509
+ return :csv if ext == ".csv"
1510
+ return :js if ext == ".js"
1511
+ return :js if ext == ".mjs"
1512
+ return :js if ext == ".cjs"
1513
+ return :ts if ext == ".ts"
1514
+ return :css if ext == ".css"
1515
+ return :html if ext == ".html"
1516
+ return :html if ext == ".htm"
1517
+ return :haml if ext == ".haml"
1518
+ return :slim if ext == ".slim"
1519
+ return :erb if ext == ".erb"
1520
+ return :fish if ext == ".fish"
1521
+ return :sql if ext == ".sql"
1522
+ return :tex if ext == ".tex"
1523
+ return :md if ext == ".md"
1524
+ return :md if ext == ".markdown"
1525
+ return :png if ext == ".png"
1526
+ return :jpeg if ext == ".jpg"
1527
+ return :jpeg if ext == ".jpeg"
1528
+ return :ico if ext == ".ico"
1529
+ return :webp if ext == ".webp"
1530
+ return :heic if ext == ".heic"
1531
+ return :pdf if ext == ".pdf"
1532
+ return :raw if ext == ".raw"
1533
+ return :env if path == ".env"
1534
+ return :env if path.start_with?(".env.")
1535
+ return :sh if path == "Dockerfile"
1536
+ return :sh if ext == ".sh"
1537
+ return :enc if ext == ".enc"
1538
+ return :enc if ext == ".keystore"
1539
+ return :pro if ext == ".pro"
1540
+ return :txt if ext == ".txt"
1541
+ return :bat if ext == ".bat"
1542
+ return :xcconfig if ext == ".xcconfig"
1543
+ return :pbxproj if ext == ".pbxproj"
1544
+ return :xml if ext == ".xml"
1545
+ return :xml if ext == ".plist"
1546
+ return :xml if ext == ".storyboard"
1547
+ return :xml if ext == ".xcscheme"
1548
+ return :xml if ext == ".xcworkspacedata"
1549
+ return :xml if ext == ".xcprivacy"
1550
+ return :xml if ext == ".entitlements"
1551
+ return :kotlin if ext == ".kt"
1552
+ return :groovy if ext == ".gradle"
1553
+ return :groovy if ext == ".properties"
1554
+ return :binary if ext == ".jar"
1555
+ return :objectivec if ext == ".h"
1556
+ return :objectivec if ext == ".mm"
1557
+ return :objectivec if ext == ".m"
1558
+ return unless File.exist?(path)
1559
+
1560
+ first_line = File.open(path, &:gets).to_s
1561
+ first_line = first_line.encode("UTF-8", invalid: :replace).strip
1562
+ return :ruby if first_line == "#!/usr/bin/env ruby"
1563
+ return :sh if first_line == "#!/bin/bash"
1564
+ return :sh if first_line == "#!/bin/sh"
1565
+ return :sh if first_line == "#!/bin/bash -e"
1566
+ return :sh if first_line == "#!/usr/bin/env sh"
1567
+
1568
+ nil
1569
+ end
1570
+
1571
+ def format(path, context:)
1572
+ return if File.symlink?(path)
1573
+ return if File.directory?(path)
1574
+ return unless File.exist?(path)
1575
+
1576
+ before = File.read(path)
1577
+
1578
+ case filetype(path)
1579
+ when :directory
1580
+ when :ruby
1581
+ after = SyntaxTree.format(before)
1582
+ when :haml
1583
+ after = SyntaxTree::Haml.format(before)
1584
+ when :erb
1585
+ after = SyntaxTree::ERB.format(before)
1586
+ when :xml
1587
+ after = SyntaxTree::XML.format(before)
1588
+ when :json
1589
+ after = JSON.pretty_generate(JSON.parse(before))
1590
+ when :jsonl
1591
+ after = before.lines.map { |line| JSON.parse(line).to_json }.join("\n")
1592
+ when :csv
1593
+ after =
1594
+ CSV.generate { |csv| CSV.parse(before).each { |row| csv << row } }
1595
+ when :yaml
1596
+ after = sort(YAML.safe_load(before)).to_yaml
1597
+ when :yamll
1598
+ after =
1599
+ before
1600
+ .lines
1601
+ .map do |line|
1602
+ sort(YAML.safe_load(JSON.parse(line))).to_yaml.to_json
1603
+ end
1604
+ .join("\n") + "\n"
1605
+ when :js
1606
+ context.eval("format(#{path.to_json}, 'babel')")
1607
+ when :ts
1608
+ context.eval("format(#{path.to_json}, 'typescript')")
1609
+ when :html
1610
+ context.eval("format(#{path.to_json}, 'html')")
1611
+ when :md
1612
+ context.eval("format(#{path.to_json}, 'markdown')")
1613
+ when :sql
1614
+ context.eval("format(#{path.to_json}, 'sql')")
1615
+ when :groovy
1616
+ context.eval("format(#{path.to_json}, 'groovy')")
1617
+ when :css
1618
+ context.eval("format(#{path.to_json}, 'css')")
1619
+ when :sh
1620
+ if system("command -v shfmt > /dev/null 2>&1")
1621
+ command = ["shfmt", "--indent", "4", path].shelljoin
1622
+ stdout, stderr, status = Open3.capture3(command)
1623
+ raise stderr unless stderr.empty? && status.success?
1624
+
1625
+ after = stdout
1626
+ else
1627
+ warn "run: `brew install shfmt` for #{path}"
1628
+ end
1629
+ when :pdf
1630
+ doc = HexaPDF::Document.open(path)
1631
+ doc.trailer.info.each_key { |key| doc.trailer.info.delete(key) }
1632
+ doc.write(path, update_fields: false)
1633
+ after = File.read(path)
1634
+ when :tex
1635
+ if system("command -v latexindent > /dev/null 2>&1")
1636
+ command = ["latexindent", path, "--logfile", "/dev/null"].shelljoin
1637
+ stdout, stderr, status = Open3.capture3(command)
1638
+ raise stderr unless stderr.empty? && status.success?
1639
+
1640
+ after = stdout.gsub("\t", " ")
1641
+ else
1642
+ warn "run: `brew install latexindent` for #{path}"
1643
+ end
1644
+ when :objectivec
1645
+ if system("command -v clang-format > /dev/null 2>&1")
1646
+ command = ["clang-format", path].shelljoin
1647
+ stdout, stderr, status = Open3.capture3(command)
1648
+ raise stderr unless stderr.empty? && status.success?
1649
+
1650
+ after = stdout.gsub("\t", " ")
1651
+ else
1652
+ warn "run: `brew install clang-format` for #{path}"
1653
+ end
1654
+ when :kotlin
1655
+ if system("command -v ktlint > /dev/null 2>&1")
1656
+ command = ["ktlint", "-F", path].shelljoin
1657
+ stdout, stderr, status = Open3.capture3(command)
1658
+ raise stderr unless stderr.empty? && status.success?
1659
+
1660
+ after = File.read(path)
1661
+ else
1662
+ warn "run: `brew install ktlint` for #{path}"
1663
+ end
1664
+ when :raw, :directory, :symlink, :env, :enc, :txt, :pro, :binary, :slim,
1665
+ :fish, :bat, :xcconfig, :pbxproj, :jpeg, :png, :webp, :heic, :ico
1666
+ # nothing to do
1667
+ else
1668
+ case File.basename(path)
1669
+ when ".gitignore", ".node-version", ".prettierignore", ".ruby-version",
1670
+ ".tool-versions", "Gemfile.lock", "LICENSE", "VERSION", ".rspec",
1671
+ "Procfile", "Procfile.dev", "Podfile.lock", ".xcode.env", "CNAME",
1672
+ "TODO", ".gitmodules", ".asdfrc", "config", ".dotignore", ".gemrc",
1673
+ ".gitconfig", ".gitmessage", ".hushlogin", ".psqlrc", ".vimrc",
1674
+ "DIRECTORIES"
1675
+ # nothing to do
1676
+ when ".keep"
1677
+ File.write(path, "")
1678
+ else
1679
+ puts "unhandled: #{path}"
1680
+ end
1681
+ end
1682
+
1683
+ if after && before != after
1684
+ puts path
1685
+ File.write(path, after)
1686
+ end
1687
+ rescue StandardError => e
1688
+ warn "failed to parse #{path}: #{e.message}"
1689
+ binding.irb
783
1690
  end
784
1691
  end
785
1692
  end