toys-core 0.11.5 → 0.12.0

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