toys-core 0.11.5 → 0.12.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +1 -1
  4. data/lib/toys-core.rb +4 -1
  5. data/lib/toys/acceptor.rb +3 -3
  6. data/lib/toys/arg_parser.rb +6 -7
  7. data/lib/toys/cli.rb +44 -14
  8. data/lib/toys/compat.rb +19 -22
  9. data/lib/toys/completion.rb +3 -1
  10. data/lib/toys/context.rb +2 -2
  11. data/lib/toys/core.rb +1 -1
  12. data/lib/toys/dsl/base.rb +85 -0
  13. data/lib/toys/dsl/flag.rb +3 -3
  14. data/lib/toys/dsl/flag_group.rb +7 -7
  15. data/lib/toys/dsl/internal.rb +206 -0
  16. data/lib/toys/dsl/positional_arg.rb +3 -3
  17. data/lib/toys/dsl/tool.rb +174 -216
  18. data/lib/toys/errors.rb +1 -0
  19. data/lib/toys/flag.rb +15 -18
  20. data/lib/toys/flag_group.rb +5 -4
  21. data/lib/toys/input_file.rb +4 -4
  22. data/lib/toys/loader.rb +189 -50
  23. data/lib/toys/middleware.rb +1 -1
  24. data/lib/toys/mixin.rb +2 -2
  25. data/lib/toys/positional_arg.rb +3 -3
  26. data/lib/toys/settings.rb +900 -0
  27. data/lib/toys/source_info.rb +121 -18
  28. data/lib/toys/standard_middleware/apply_config.rb +5 -4
  29. data/lib/toys/standard_middleware/set_default_descriptions.rb +18 -18
  30. data/lib/toys/standard_middleware/show_help.rb +17 -5
  31. data/lib/toys/standard_mixins/exec.rb +12 -14
  32. data/lib/toys/standard_mixins/git_cache.rb +48 -0
  33. data/lib/toys/standard_mixins/xdg.rb +56 -0
  34. data/lib/toys/template.rb +2 -2
  35. data/lib/toys/{tool.rb → tool_definition.rb} +100 -41
  36. data/lib/toys/utils/exec.rb +4 -5
  37. data/lib/toys/utils/gems.rb +8 -7
  38. data/lib/toys/utils/git_cache.rb +184 -0
  39. data/lib/toys/utils/help_text.rb +90 -34
  40. data/lib/toys/utils/terminal.rb +1 -1
  41. data/lib/toys/utils/xdg.rb +293 -0
  42. metadata +14 -7
@@ -132,8 +132,8 @@ module Toys
132
132
  end
133
133
  @on_conflict = on_conflict || :error
134
134
  @terminal = terminal
135
- @input = input || ::STDIN
136
- @output = output || ::STDOUT
135
+ @input = input || $stdin
136
+ @output = output || $stdout
137
137
  end
138
138
 
139
139
  ##
@@ -185,6 +185,7 @@ module Toys
185
185
  gemfile_path = Gems.find_gemfile(dir, gemfile_names: gemfile_names)
186
186
  end
187
187
  raise GemfileNotFoundError, "Gemfile not found" unless gemfile_path
188
+ gemfile_path = ::File.absolute_path(gemfile_path)
188
189
  Gems.synchronize do
189
190
  if configure_gemfile(gemfile_path)
190
191
  activate("bundler", "~> 2.1")
@@ -296,7 +297,7 @@ module Toys
296
297
  if ::File.basename(gemfile_path) == "gems.rb"
297
298
  ::File.join(::File.dirname(gemfile_path), "gems.locked")
298
299
  else
299
- gemfile_path + ".lock"
300
+ "#{gemfile_path}.lock"
300
301
  end
301
302
  end
302
303
 
@@ -325,14 +326,14 @@ module Toys
325
326
  end
326
327
 
327
328
  def restore_old_lockfile(lockfile_path, contents)
328
- if contents
329
- ::File.open(lockfile_path, "w") do |file|
330
- file.write(contents)
331
- end
329
+ return unless contents
330
+ ::File.open(lockfile_path, "w") do |file|
331
+ file.write(contents)
332
332
  end
333
333
  end
334
334
 
335
335
  def modify_bundle_definition(gemfile_path, lockfile_path)
336
+ ::Bundler.configure
336
337
  builder = ::Bundler::Dsl.new
337
338
  builder.eval_gemfile(gemfile_path)
338
339
  toys_gems = ["toys-core"]
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "toys/utils/exec"
6
+ require "toys/utils/xdg"
7
+
8
+ module Toys
9
+ module Utils
10
+ ##
11
+ # This object provides cached access to remote git data. Given a remote
12
+ # repository, a path, and a commit, it makes the files availble in the
13
+ # local filesystem. Access is cached, so repeated requests do not hit the
14
+ # remote repository again.
15
+ #
16
+ # This class is used by the Loader to load tools from git. Tools can also
17
+ # use the `:git_cache` mixin for direct access to this class.
18
+ #
19
+ class GitCache
20
+ ##
21
+ # GitCache encountered a failure
22
+ #
23
+ class Error < ::StandardError
24
+ ##
25
+ # Create a GitCache::Error.
26
+ #
27
+ # @param message [String] The error message
28
+ # @param result [Toys::Utils::Exec::Result] The result of a git
29
+ # command execution, or `nil` if this error was not due to a git
30
+ # command error.
31
+ #
32
+ def initialize(message, result)
33
+ super(message)
34
+ @exec_result = result
35
+ end
36
+
37
+ ##
38
+ # @return [Toys::Utils::Exec::Result] The result of a git command
39
+ # execution, or `nil` if this error was not due to a git command
40
+ # error.
41
+ #
42
+ attr_reader :exec_result
43
+ end
44
+
45
+ ##
46
+ # Access a git cache.
47
+ #
48
+ # @param cache_dir [String] The path to the cache directory. Defaults to
49
+ # a specific directory in the user's XDG cache.
50
+ #
51
+ def initialize(cache_dir: nil)
52
+ @cache_dir = ::File.expand_path(cache_dir || default_cache_dir)
53
+ @exec = Utils::Exec.new(out: :capture, err: :capture)
54
+ end
55
+
56
+ ##
57
+ # Find the given git-based files from the git cache, loading from the
58
+ # remote repo if necessary.
59
+ #
60
+ # @param remote [String] The URL of the git repo. Required.
61
+ # @param path [String] The path to the file or directory within the repo.
62
+ # Optional. Defaults to the entire repo.
63
+ # @param commit [String] The commit reference, which may be a SHA or any
64
+ # git ref such as a branch or tag. Optional. Defaults to `HEAD`.
65
+ # @param update [Boolean] Force update of non-SHA commit references, even
66
+ # if it has previously been loaded.
67
+ #
68
+ # @return [String] The full path to the cached files.
69
+ #
70
+ def find(remote, path: nil, commit: nil, update: false)
71
+ path ||= ""
72
+ commit ||= "HEAD"
73
+ dir = ensure_dir(remote)
74
+ lock_repo(dir) do
75
+ ensure_repo(dir, remote)
76
+ sha = ensure_commit(dir, commit, update)
77
+ ensure_source(dir, sha, path.to_s)
78
+ end
79
+ end
80
+
81
+ ##
82
+ # The cache directory.
83
+ #
84
+ # @return [String]
85
+ #
86
+ attr_reader :cache_dir
87
+
88
+ # @private Used for testing
89
+ def repo_dir_for(remote)
90
+ ::File.join(@cache_dir, remote_dir_name(remote), "repo")
91
+ end
92
+
93
+ private
94
+
95
+ def remote_dir_name(remote)
96
+ ::Digest::MD5.hexdigest(remote)
97
+ end
98
+
99
+ def source_name(sha, path)
100
+ digest = ::Digest::MD5.hexdigest("#{sha}#{path}")
101
+ "#{digest}#{::File.extname(path)}"
102
+ end
103
+
104
+ def repo_dir_name
105
+ "repo"
106
+ end
107
+
108
+ def default_cache_dir
109
+ ::File.join(XDG.new.cache_home, "toys", "git")
110
+ end
111
+
112
+ def git(dir, cmd, error_message: nil)
113
+ result = @exec.exec(["git"] + cmd, chdir: dir)
114
+ if result.failed?
115
+ raise GitCache::Error.new("Could not run git command line", result)
116
+ end
117
+ if block_given?
118
+ yield result
119
+ elsif result.error? && error_message
120
+ raise GitCache::Error.new(error_message, result)
121
+ else
122
+ result
123
+ end
124
+ end
125
+
126
+ def ensure_dir(remote)
127
+ dir = ::File.join(@cache_dir, remote_dir_name(remote))
128
+ ::FileUtils.mkdir_p(dir)
129
+ dir
130
+ end
131
+
132
+ def lock_repo(dir)
133
+ lock_path = ::File.join(dir, "repo.lock")
134
+ ::File.open(lock_path, ::File::RDWR | ::File::CREAT) do |file|
135
+ file.flock(::File::LOCK_EX)
136
+ yield
137
+ end
138
+ end
139
+
140
+ def ensure_repo(dir, remote)
141
+ repo_dir = ::File.join(dir, repo_dir_name)
142
+ ::FileUtils.mkdir_p(repo_dir)
143
+ result = git(repo_dir, ["remote", "get-url", "origin"])
144
+ unless result.success? && result.captured_out.strip == remote
145
+ ::FileUtils.rm_rf(repo_dir)
146
+ ::FileUtils.mkdir_p(repo_dir)
147
+ git(repo_dir, ["init"],
148
+ error_message: "Unable to initialize git repository")
149
+ git(repo_dir, ["remote", "add", "origin", remote],
150
+ error_message: "Unable to add git remote")
151
+ end
152
+ end
153
+
154
+ def ensure_commit(dir, commit, update = false)
155
+ local_commit = "toys-git-cache/#{commit}"
156
+ repo_dir = ::File.join(dir, repo_dir_name)
157
+ is_sha = commit =~ /^[0-9a-f]{40}$/
158
+ if update && !is_sha || !commit_exists?(repo_dir, local_commit)
159
+ git(repo_dir, ["fetch", "--depth=1", "origin", "#{commit}:#{local_commit}"],
160
+ error_message: "Unable to to fetch commit: #{commit}")
161
+ end
162
+ result = git(repo_dir, ["rev-parse", local_commit],
163
+ error_message: "Unable to retrieve commit: #{local_commit}")
164
+ result.captured_out.strip
165
+ end
166
+
167
+ def commit_exists?(repo_dir, commit)
168
+ result = git(repo_dir, ["cat-file", "-t", commit])
169
+ result.success? && result.captured_out.strip == "commit"
170
+ end
171
+
172
+ def ensure_source(dir, sha, path)
173
+ source_path = ::File.join(dir, source_name(sha, path))
174
+ unless ::File.exist?(source_path)
175
+ repo_dir = ::File.join(dir, repo_dir_name)
176
+ git(repo_dir, ["checkout", sha])
177
+ from_path = ::File.join(repo_dir, path)
178
+ ::FileUtils.cp_r(from_path, source_path)
179
+ end
180
+ source_path
181
+ end
182
+ end
183
+ end
184
+ end
@@ -44,11 +44,12 @@ module Toys
44
44
  ##
45
45
  # Create a usage helper.
46
46
  #
47
- # @param tool [Toys::Tool] The tool to document.
47
+ # @param tool [Toys::ToolDefinition] The tool to document.
48
48
  # @param loader [Toys::Loader] A loader that can provide subcommands.
49
49
  # @param executable_name [String] The name of the executable.
50
50
  # e.g. `"toys"`.
51
- # @param delegates [Array<Toys::Tool>] The delegation path to the tool.
51
+ # @param delegates [Array<Toys::ToolDefinition>] The delegation path to
52
+ # the tool.
52
53
  #
53
54
  # @return [Toys::Utils::HelpText]
54
55
  #
@@ -60,8 +61,8 @@ module Toys
60
61
  end
61
62
 
62
63
  ##
63
- # The Tool being documented.
64
- # @return [Toys::Tool]
64
+ # The ToolDefinition being documented.
65
+ # @return [Toys::ToolDefinition]
65
66
  #
66
67
  attr_reader :tool
67
68
 
@@ -72,6 +73,8 @@ module Toys
72
73
  # display all subtools recursively. Defaults to false.
73
74
  # @param include_hidden [Boolean] Include hidden subtools (i.e. whose
74
75
  # names begin with underscore.) Default is false.
76
+ # @param separate_sources [Boolean] Split up tool list by source root.
77
+ # Defaults to false.
75
78
  # @param left_column_width [Integer] Width of the first column. Default
76
79
  # is {DEFAULT_LEFT_COLUMN_WIDTH}.
77
80
  # @param indent [Integer] Indent width. Default is {DEFAULT_INDENT}.
@@ -80,13 +83,14 @@ module Toys
80
83
  #
81
84
  # @return [String] A usage string.
82
85
  #
83
- def usage_string(recursive: false, include_hidden: false,
86
+ def usage_string(recursive: false, include_hidden: false, separate_sources: false,
84
87
  left_column_width: nil, indent: nil, wrap_width: nil)
85
88
  left_column_width ||= DEFAULT_LEFT_COLUMN_WIDTH
86
89
  indent ||= DEFAULT_INDENT
87
- subtools = find_subtools(recursive, nil, include_hidden)
90
+ subtools = collect_subtool_info(recursive, nil, include_hidden, separate_sources)
88
91
  assembler = UsageStringAssembler.new(
89
- @tool, @executable_name, subtools, indent, left_column_width, wrap_width
92
+ @tool, @executable_name, subtools, separate_sources,
93
+ indent, left_column_width, wrap_width
90
94
  )
91
95
  assembler.result
92
96
  end
@@ -102,6 +106,8 @@ module Toys
102
106
  # names begin with underscore.) Default is false.
103
107
  # @param show_source_path [Boolean] If true, shows the source path
104
108
  # section. Defaults to false.
109
+ # @param separate_sources [Boolean] Split up tool list by source root.
110
+ # Defaults to false.
105
111
  # @param indent [Integer] Indent width. Default is {DEFAULT_INDENT}.
106
112
  # @param indent2 [Integer] Second indent width. Default is
107
113
  # {DEFAULT_INDENT}.
@@ -112,13 +118,14 @@ module Toys
112
118
  # @return [String] A usage string.
113
119
  #
114
120
  def help_string(recursive: false, search: nil, include_hidden: false,
115
- show_source_path: false,
121
+ show_source_path: false, separate_sources: false,
116
122
  indent: nil, indent2: nil, wrap_width: nil, styled: true)
117
123
  indent ||= DEFAULT_INDENT
118
124
  indent2 ||= DEFAULT_INDENT
119
- subtools = find_subtools(recursive, search, include_hidden)
125
+ subtools = collect_subtool_info(recursive, search, include_hidden, separate_sources)
120
126
  assembler = HelpStringAssembler.new(
121
- @tool, @executable_name, @delegates, subtools, search, show_source_path,
127
+ @tool, @executable_name, @delegates, subtools, search,
128
+ show_source_path, separate_sources,
122
129
  indent, indent2, wrap_width, styled
123
130
  )
124
131
  assembler.result
@@ -133,6 +140,8 @@ module Toys
133
140
  # listing subtools. Defaults to `nil` which finds all subtools.
134
141
  # @param include_hidden [Boolean] Include hidden subtools (i.e. whose
135
142
  # names begin with underscore.) Default is false.
143
+ # @param separate_sources [Boolean] Split up tool list by source root.
144
+ # Defaults to false.
136
145
  # @param indent [Integer] Indent width. Default is {DEFAULT_INDENT}.
137
146
  # @param wrap_width [Integer,nil] Wrap width of the column, or `nil` to
138
147
  # disable wrap. Default is `nil`.
@@ -141,17 +150,23 @@ module Toys
141
150
  # @return [String] A usage string.
142
151
  #
143
152
  def list_string(recursive: false, search: nil, include_hidden: false,
144
- indent: nil, wrap_width: nil, styled: true)
153
+ separate_sources: false, indent: nil, wrap_width: nil, styled: true)
145
154
  indent ||= DEFAULT_INDENT
146
- subtools = find_subtools(recursive, search, include_hidden)
147
- assembler = ListStringAssembler.new(@tool, subtools, recursive, search,
155
+ subtools = collect_subtool_info(recursive, search, include_hidden, separate_sources)
156
+ assembler = ListStringAssembler.new(@tool, subtools, recursive, search, separate_sources,
148
157
  indent, wrap_width, styled)
149
158
  assembler.result
150
159
  end
151
160
 
152
161
  private
153
162
 
154
- def find_subtools(recursive, search, include_hidden)
163
+ def collect_subtool_info(recursive, search, include_hidden, separate_sources)
164
+ subtools_by_name = list_subtools(recursive, include_hidden)
165
+ filter_subtools(subtools_by_name, search)
166
+ arrange_subtools(subtools_by_name, separate_sources)
167
+ end
168
+
169
+ def list_subtools(recursive, include_hidden)
155
170
  subtools_by_name = {}
156
171
  ([@tool] + @delegates).each do |tool|
157
172
  name_len = tool.full_name.length
@@ -162,20 +177,37 @@ module Toys
162
177
  subtools_by_name[local_name] = subtool
163
178
  end
164
179
  end
180
+ subtools_by_name
181
+ end
182
+
183
+ def filter_subtools(subtools_by_name, search)
184
+ if !search.nil? && !search.empty?
185
+ regex = ::Regexp.new(search, ::Regexp::IGNORECASE)
186
+ subtools_by_name.delete_if do |local_name, tool|
187
+ !regex.match?(local_name) && !regex.match?(tool.desc.to_s)
188
+ end
189
+ end
190
+ end
191
+
192
+ def arrange_subtools(subtools_by_name, separate_sources)
165
193
  subtool_list = subtools_by_name.sort_by { |(local_name, _tool)| local_name }
166
- return subtool_list if search.nil? || search.empty?
167
- regex = ::Regexp.new(search, ::Regexp::IGNORECASE)
168
- subtool_list.find_all do |local_name, tool|
169
- regex =~ local_name || regex =~ tool.desc.to_s
194
+ result = {}
195
+ subtool_list.each do |(local_name, subtool)|
196
+ key = separate_sources ? subtool.source_root : nil
197
+ (result[key] ||= []) << [local_name, subtool]
170
198
  end
199
+ result.sort_by { |source, _subtools| -(source&.priority || -999_999) }
200
+ .map { |source, subtools| [source&.source_name || "unknown source", subtools] }
171
201
  end
172
202
 
173
203
  ## @private
174
204
  class UsageStringAssembler
175
- def initialize(tool, executable_name, subtools, indent, left_column_width, wrap_width)
205
+ def initialize(tool, executable_name, subtools, separate_sources,
206
+ indent, left_column_width, wrap_width)
176
207
  @tool = tool
177
208
  @executable_name = executable_name
178
209
  @subtools = subtools
210
+ @separate_sources = separate_sources
179
211
  @indent = indent
180
212
  @left_column_width = left_column_width
181
213
  @wrap_width = wrap_width
@@ -193,7 +225,8 @@ module Toys
193
225
  add_flag_group_sections
194
226
  add_positional_arguments_section if @tool.runnable?
195
227
  add_subtool_list_section
196
- @result = @lines.join("\n") + "\n"
228
+ joined_lines = @lines.join("\n")
229
+ @result = "#{joined_lines}\n"
197
230
  end
198
231
 
199
232
  def add_synopsis_section
@@ -226,7 +259,7 @@ module Toys
226
259
  @lines << ""
227
260
  desc_str = group.desc.to_s
228
261
  desc_str = "Flags" if desc_str.empty?
229
- @lines << desc_str + ":"
262
+ @lines << "#{desc_str}:"
230
263
  group.flags.each do |flag|
231
264
  add_flag(flag)
232
265
  end
@@ -254,11 +287,12 @@ module Toys
254
287
  end
255
288
 
256
289
  def add_subtool_list_section
257
- return if @subtools.empty?
258
- @lines << ""
259
- @lines << "Tools:"
260
- @subtools.each do |local_name, subtool|
261
- add_right_column_desc(local_name, wrap_desc(subtool.desc))
290
+ @subtools.each do |source_name, subtool_list|
291
+ @lines << ""
292
+ @lines << (@separate_sources ? "Tools from #{source_name}:" : "Tools:")
293
+ subtool_list.each do |local_name, subtool|
294
+ add_right_column_desc(local_name, wrap_desc(subtool.desc))
295
+ end
262
296
  end
263
297
  end
264
298
 
@@ -299,7 +333,7 @@ module Toys
299
333
  ## @private
300
334
  class HelpStringAssembler
301
335
  def initialize(tool, executable_name, delegates, subtools, search_term,
302
- show_source_path, indent, indent2, wrap_width, styled)
336
+ show_source_path, separate_sources, indent, indent2, wrap_width, styled)
303
337
  require "toys/utils/terminal"
304
338
  @tool = tool
305
339
  @executable_name = executable_name
@@ -307,6 +341,7 @@ module Toys
307
341
  @subtools = subtools
308
342
  @search_term = search_term
309
343
  @show_source_path = show_source_path
344
+ @separate_sources = separate_sources
310
345
  @indent = indent
311
346
  @indent2 = indent2
312
347
  @wrap_width = wrap_width
@@ -532,8 +567,17 @@ module Toys
532
567
  @lines << indent_str("Showing search results for \"#{@search_term}\"")
533
568
  @lines << ""
534
569
  end
535
- @subtools.each do |local_name, subtool|
536
- add_prefix_with_desc(bold(local_name), subtool.desc)
570
+ first_section = true
571
+ @subtools.each do |source_name, subtool_list|
572
+ @lines << "" unless first_section
573
+ if @separate_sources
574
+ @lines << indent_str(underline("From #{source_name}"))
575
+ @lines << ""
576
+ end
577
+ subtool_list.each do |local_name, subtool|
578
+ add_prefix_with_desc(bold(local_name), subtool.desc)
579
+ end
580
+ first_section = false
537
581
  end
538
582
  end
539
583
 
@@ -596,12 +640,14 @@ module Toys
596
640
 
597
641
  ## @private
598
642
  class ListStringAssembler
599
- def initialize(tool, subtools, recursive, search_term, indent, wrap_width, styled)
643
+ def initialize(tool, subtools, recursive, search_term, separate_sources,
644
+ indent, wrap_width, styled)
600
645
  require "toys/utils/terminal"
601
646
  @tool = tool
602
647
  @subtools = subtools
603
648
  @recursive = recursive
604
649
  @search_term = search_term
650
+ @separate_sources = separate_sources
605
651
  @indent = indent
606
652
  @wrap_width = wrap_width
607
653
  assemble(styled)
@@ -626,16 +672,22 @@ module Toys
626
672
  else
627
673
  "#{top_line} under #{bold(@tool.display_name)}:"
628
674
  end
629
- @lines << ""
630
675
  if @search_term
631
- @lines << "Showing search results for \"#{@search_term}\""
632
676
  @lines << ""
677
+ @lines << "Showing search results for \"#{@search_term}\""
633
678
  end
634
679
  end
635
680
 
636
681
  def add_list
637
- @subtools.each do |local_name, subtool|
638
- add_prefix_with_desc(bold(local_name), subtool.desc)
682
+ @subtools.each do |source_name, subtool_list|
683
+ @lines << ""
684
+ if @separate_sources
685
+ @lines << underline("From: #{source_name}")
686
+ @lines << ""
687
+ end
688
+ subtool_list.each do |local_name, subtool|
689
+ add_prefix_with_desc(bold(local_name), subtool.desc)
690
+ end
639
691
  end
640
692
  end
641
693
 
@@ -662,6 +714,10 @@ module Toys
662
714
  @lines.apply_styles(str, :bold)
663
715
  end
664
716
 
717
+ def underline(str)
718
+ @lines.apply_styles(str, :underline)
719
+ end
720
+
665
721
  def indent_str(str)
666
722
  "#{' ' * @indent}#{str}"
667
723
  end