toys-core 0.11.5 → 0.13.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +5 -2
  5. data/docs/guide.md +1 -1
  6. data/lib/toys/acceptor.rb +13 -4
  7. data/lib/toys/arg_parser.rb +7 -7
  8. data/lib/toys/cli.rb +170 -120
  9. data/lib/toys/compat.rb +71 -23
  10. data/lib/toys/completion.rb +18 -6
  11. data/lib/toys/context.rb +24 -15
  12. data/lib/toys/core.rb +6 -2
  13. data/lib/toys/dsl/base.rb +87 -0
  14. data/lib/toys/dsl/flag.rb +26 -20
  15. data/lib/toys/dsl/flag_group.rb +18 -14
  16. data/lib/toys/dsl/internal.rb +206 -0
  17. data/lib/toys/dsl/positional_arg.rb +26 -16
  18. data/lib/toys/dsl/tool.rb +180 -218
  19. data/lib/toys/errors.rb +64 -8
  20. data/lib/toys/flag.rb +662 -656
  21. data/lib/toys/flag_group.rb +24 -10
  22. data/lib/toys/input_file.rb +13 -7
  23. data/lib/toys/loader.rb +293 -140
  24. data/lib/toys/middleware.rb +46 -22
  25. data/lib/toys/mixin.rb +10 -8
  26. data/lib/toys/positional_arg.rb +21 -20
  27. data/lib/toys/settings.rb +914 -0
  28. data/lib/toys/source_info.rb +147 -35
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
  30. data/lib/toys/standard_middleware/apply_config.rb +6 -4
  31. data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
  32. data/lib/toys/standard_middleware/set_default_descriptions.rb +19 -18
  33. data/lib/toys/standard_middleware/show_help.rb +19 -5
  34. data/lib/toys/standard_middleware/show_root_version.rb +2 -0
  35. data/lib/toys/standard_mixins/bundler.rb +24 -15
  36. data/lib/toys/standard_mixins/exec.rb +43 -34
  37. data/lib/toys/standard_mixins/fileutils.rb +3 -1
  38. data/lib/toys/standard_mixins/gems.rb +21 -17
  39. data/lib/toys/standard_mixins/git_cache.rb +46 -0
  40. data/lib/toys/standard_mixins/highline.rb +8 -8
  41. data/lib/toys/standard_mixins/terminal.rb +5 -5
  42. data/lib/toys/standard_mixins/xdg.rb +56 -0
  43. data/lib/toys/template.rb +11 -9
  44. data/lib/toys/{tool.rb → tool_definition.rb} +292 -226
  45. data/lib/toys/utils/completion_engine.rb +7 -2
  46. data/lib/toys/utils/exec.rb +162 -132
  47. data/lib/toys/utils/gems.rb +85 -60
  48. data/lib/toys/utils/git_cache.rb +813 -0
  49. data/lib/toys/utils/help_text.rb +117 -37
  50. data/lib/toys/utils/terminal.rb +11 -3
  51. data/lib/toys/utils/xdg.rb +293 -0
  52. data/lib/toys/wrappable_string.rb +9 -2
  53. data/lib/toys-core.rb +18 -6
  54. metadata +14 -7
@@ -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,42 @@ 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
- ## @private
203
+ ##
204
+ # @private
205
+ #
174
206
  class UsageStringAssembler
175
- def initialize(tool, executable_name, subtools, indent, left_column_width, wrap_width)
207
+ ##
208
+ # @private
209
+ #
210
+ def initialize(tool, executable_name, subtools, separate_sources,
211
+ indent, left_column_width, wrap_width)
176
212
  @tool = tool
177
213
  @executable_name = executable_name
178
214
  @subtools = subtools
215
+ @separate_sources = separate_sources
179
216
  @indent = indent
180
217
  @left_column_width = left_column_width
181
218
  @wrap_width = wrap_width
@@ -184,6 +221,9 @@ module Toys
184
221
  assemble
185
222
  end
186
223
 
224
+ ##
225
+ # @private
226
+ #
187
227
  attr_reader :result
188
228
 
189
229
  private
@@ -193,7 +233,8 @@ module Toys
193
233
  add_flag_group_sections
194
234
  add_positional_arguments_section if @tool.runnable?
195
235
  add_subtool_list_section
196
- @result = @lines.join("\n") + "\n"
236
+ joined_lines = @lines.join("\n")
237
+ @result = "#{joined_lines}\n"
197
238
  end
198
239
 
199
240
  def add_synopsis_section
@@ -226,7 +267,7 @@ module Toys
226
267
  @lines << ""
227
268
  desc_str = group.desc.to_s
228
269
  desc_str = "Flags" if desc_str.empty?
229
- @lines << desc_str + ":"
270
+ @lines << "#{desc_str}:"
230
271
  group.flags.each do |flag|
231
272
  add_flag(flag)
232
273
  end
@@ -254,11 +295,12 @@ module Toys
254
295
  end
255
296
 
256
297
  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))
298
+ @subtools.each do |source_name, subtool_list|
299
+ @lines << ""
300
+ @lines << (@separate_sources ? "Tools from #{source_name}:" : "Tools:")
301
+ subtool_list.each do |local_name, subtool|
302
+ add_right_column_desc(local_name, wrap_desc(subtool.desc))
303
+ end
262
304
  end
263
305
  end
264
306
 
@@ -296,10 +338,15 @@ module Toys
296
338
  end
297
339
  end
298
340
 
299
- ## @private
341
+ ##
342
+ # @private
343
+ #
300
344
  class HelpStringAssembler
345
+ ##
346
+ # @private
347
+ #
301
348
  def initialize(tool, executable_name, delegates, subtools, search_term,
302
- show_source_path, indent, indent2, wrap_width, styled)
349
+ show_source_path, separate_sources, indent, indent2, wrap_width, styled)
303
350
  require "toys/utils/terminal"
304
351
  @tool = tool
305
352
  @executable_name = executable_name
@@ -307,6 +354,7 @@ module Toys
307
354
  @subtools = subtools
308
355
  @search_term = search_term
309
356
  @show_source_path = show_source_path
357
+ @separate_sources = separate_sources
310
358
  @indent = indent
311
359
  @indent2 = indent2
312
360
  @wrap_width = wrap_width
@@ -314,6 +362,9 @@ module Toys
314
362
  assemble
315
363
  end
316
364
 
365
+ ##
366
+ # @private
367
+ #
317
368
  attr_reader :result
318
369
 
319
370
  private
@@ -532,8 +583,17 @@ module Toys
532
583
  @lines << indent_str("Showing search results for \"#{@search_term}\"")
533
584
  @lines << ""
534
585
  end
535
- @subtools.each do |local_name, subtool|
536
- add_prefix_with_desc(bold(local_name), subtool.desc)
586
+ first_section = true
587
+ @subtools.each do |source_name, subtool_list|
588
+ @lines << "" unless first_section
589
+ if @separate_sources
590
+ @lines << indent_str(underline("From #{source_name}"))
591
+ @lines << ""
592
+ end
593
+ subtool_list.each do |local_name, subtool|
594
+ add_prefix_with_desc(bold(local_name), subtool.desc)
595
+ end
596
+ first_section = false
537
597
  end
538
598
  end
539
599
 
@@ -594,19 +654,29 @@ module Toys
594
654
  end
595
655
  end
596
656
 
597
- ## @private
657
+ ##
658
+ # @private
659
+ #
598
660
  class ListStringAssembler
599
- def initialize(tool, subtools, recursive, search_term, indent, wrap_width, styled)
661
+ ##
662
+ # @private
663
+ #
664
+ def initialize(tool, subtools, recursive, search_term, separate_sources,
665
+ indent, wrap_width, styled)
600
666
  require "toys/utils/terminal"
601
667
  @tool = tool
602
668
  @subtools = subtools
603
669
  @recursive = recursive
604
670
  @search_term = search_term
671
+ @separate_sources = separate_sources
605
672
  @indent = indent
606
673
  @wrap_width = wrap_width
607
674
  assemble(styled)
608
675
  end
609
676
 
677
+ ##
678
+ # @private
679
+ #
610
680
  attr_reader :result
611
681
 
612
682
  private
@@ -626,16 +696,22 @@ module Toys
626
696
  else
627
697
  "#{top_line} under #{bold(@tool.display_name)}:"
628
698
  end
629
- @lines << ""
630
699
  if @search_term
631
- @lines << "Showing search results for \"#{@search_term}\""
632
700
  @lines << ""
701
+ @lines << "Showing search results for \"#{@search_term}\""
633
702
  end
634
703
  end
635
704
 
636
705
  def add_list
637
- @subtools.each do |local_name, subtool|
638
- add_prefix_with_desc(bold(local_name), subtool.desc)
706
+ @subtools.each do |source_name, subtool_list|
707
+ @lines << ""
708
+ if @separate_sources
709
+ @lines << underline("From: #{source_name}")
710
+ @lines << ""
711
+ end
712
+ subtool_list.each do |local_name, subtool|
713
+ add_prefix_with_desc(bold(local_name), subtool.desc)
714
+ end
639
715
  end
640
716
  end
641
717
 
@@ -662,6 +738,10 @@ module Toys
662
738
  @lines.apply_styles(str, :bold)
663
739
  end
664
740
 
741
+ def underline(str)
742
+ @lines.apply_styles(str, :underline)
743
+ end
744
+
665
745
  def indent_str(str)
666
746
  "#{' ' * @indent}#{str}"
667
747
  end
@@ -14,7 +14,7 @@ module Toys
14
14
  ##
15
15
  # A simple terminal class.
16
16
  #
17
- # ## Styles
17
+ # ### Styles
18
18
  #
19
19
  # This class supports ANSI styled output where supported.
20
20
  #
@@ -120,7 +120,7 @@ module Toys
120
120
  @output = output
121
121
  @styled =
122
122
  if styled.nil?
123
- output.respond_to?(:tty?) && output.tty?
123
+ output.respond_to?(:tty?) && output.tty? && !::ENV["NO_COLOR"]
124
124
  else
125
125
  styled ? true : false
126
126
  end
@@ -431,8 +431,13 @@ module Toys
431
431
  end
432
432
  end
433
433
 
434
- ## @private
434
+ ##
435
+ # @private
436
+ #
435
437
  class SpinDriver
438
+ ##
439
+ # @private
440
+ #
436
441
  def initialize(terminal, frames, style, frame_length)
437
442
  @mutex = ::Monitor.new
438
443
  @terminal = terminal
@@ -446,6 +451,9 @@ module Toys
446
451
  @thread = @terminal.output.tty? ? start_thread : nil
447
452
  end
448
453
 
454
+ ##
455
+ # @private
456
+ #
449
457
  def stop
450
458
  @mutex.synchronize do
451
459
  @stopping = true
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toys
4
+ module Utils
5
+ ##
6
+ # A class that provides tools for working with the XDG Base Directory
7
+ # Specification.
8
+ #
9
+ # This class provides utility methods that locate base directories and
10
+ # search paths for application state, configuration, caches, and other
11
+ # data, according to the [XDG Base Directory Spec version
12
+ # 0.8](https://specifications.freedesktop.org/basedir-spec/0.8/).
13
+ #
14
+ # Tools can use the `:xdg` mixin for convenient access to this class.
15
+ #
16
+ # ### Example
17
+ #
18
+ # require "toys/utils/xdg"
19
+ #
20
+ # xdg = Toys::Utils::XDG.new
21
+ #
22
+ # # Get config file paths, in order from most to least inportant
23
+ # config_files = xdg.lookup_config("my-config.toml")
24
+ # config_files.each { |path| read_my_config(path) }
25
+ #
26
+ # ### Windows operation
27
+ #
28
+ # The Spec assumes a unix-like environment, and cannot be applied directly
29
+ # to Windows without modification. In general, this class will function on
30
+ # Windows, but with the following caveats:
31
+ #
32
+ # * All file paths must use Windows-style absolute paths, beginning with
33
+ # the drive letter.
34
+ # * Environment variables that can contain multiple paths (`XDG_*_DIRS`)
35
+ # use the Windows path delimiter (`;`) rather than the unix path
36
+ # delimiter (`:`).
37
+ # * Defaults for home directories (`XDG_*_HOME`) will follow unix
38
+ # conventions, using subdirectories under the user's profile directory
39
+ # rather than the Windows known folder paths.
40
+ # * Defaults for search paths (`XDG_*_DIRS`) will be empty and will not
41
+ # use the Windows known folder paths.
42
+ #
43
+ class XDG
44
+ ##
45
+ # Create an instance of XDG.
46
+ #
47
+ # @param env [Hash{String=>String}] the environment variables. Normally,
48
+ # you can omit this argument, as it will default to `::ENV`.
49
+ #
50
+ def initialize(env: ::ENV)
51
+ @env = env
52
+ end
53
+
54
+ ##
55
+ # Returns the absolute path to the current user's home directory.
56
+ #
57
+ # @return [String]
58
+ #
59
+ def home_dir
60
+ @home_dir ||= validate_dir_env("HOME") || ::Dir.home
61
+ end
62
+
63
+ ##
64
+ # Returns the absolute path to the single base directory relative to
65
+ # which user-specific data files should be written.
66
+ # Corresponds to the value of the `$XDG_DATA_HOME` environment variable
67
+ # and its defaults according to the XDG Base Directory Spec.
68
+ #
69
+ # @return [String]
70
+ #
71
+ def data_home
72
+ @data_home ||=
73
+ validate_dir_env("XDG_DATA_HOME") || ::File.join(home_dir, ".local", "share")
74
+ end
75
+
76
+ ##
77
+ # Returns the absolute path to the single base directory relative to
78
+ # which user-specific configuration files should be written.
79
+ # Corresponds to the value of the `$XDG_CONFIG_HOME` environment variable
80
+ # and its defaults according to the XDG Base Directory Spec.
81
+ #
82
+ # @return [String]
83
+ #
84
+ def config_home
85
+ @config_home ||= validate_dir_env("XDG_CONFIG_HOME") || ::File.join(home_dir, ".config")
86
+ end
87
+
88
+ ##
89
+ # Returns the absolute path to the single base directory relative to
90
+ # which user-specific state files should be written.
91
+ # Corresponds to the value of the `$XDG_STATE_HOME` environment variable
92
+ # and its defaults according to the XDG Base Directory Spec.
93
+ #
94
+ # @return [String]
95
+ #
96
+ def state_home
97
+ @state_home ||=
98
+ validate_dir_env("XDG_STATE_HOME") || ::File.join(home_dir, ".local", "state")
99
+ end
100
+
101
+ ##
102
+ # Returns the absolute path to the single base directory relative to
103
+ # which user-specific non-essential (cached) data should be written.
104
+ # Corresponds to the value of the `$XDG_CACHE_HOME` environment variable
105
+ # and its defaults according to the XDG Base Directory Spec.
106
+ #
107
+ # @return [String]
108
+ #
109
+ def cache_home
110
+ @cache_home ||= validate_dir_env("XDG_CACHE_HOME") || ::File.join(home_dir, ".cache")
111
+ end
112
+
113
+ ##
114
+ # Returns the absolute path to the single base directory relative to
115
+ # which user-specific executable files may be written.
116
+ # Returns the value of `$HOME/.local/bin` as specified by the XDG Base
117
+ # Directory Spec.
118
+ #
119
+ # @return [String]
120
+ #
121
+ def executable_home
122
+ @executable_home ||= ::File.join(home_dir, ".local", "bin")
123
+ end
124
+
125
+ ##
126
+ # Returns the set of preference ordered base directories relative to
127
+ # which data files should be searched, as an array of absolute paths.
128
+ # The array is ordered from most to least important, and does _not_
129
+ # include the data home directory.
130
+ # Corresponds to the value of the `$XDG_DATA_DIRS` environment variable
131
+ # and its defaults according to the XDG Base Directory Spec.
132
+ #
133
+ # @return [Array<String>]
134
+ #
135
+ def data_dirs
136
+ @data_dirs ||= validate_dirs_env("XDG_DATA_DIRS") ||
137
+ validate_dirs(["/usr/local/share", "/usr/share"]) || []
138
+ end
139
+
140
+ ##
141
+ # Returns the set of preference ordered base directories relative to
142
+ # which configuration files should be searched, as an array of absolute
143
+ # paths. The array is ordered from most to least important, and does
144
+ # _not_ include the config home directory.
145
+ # Corresponds to the value of the `$XDG_CONFIG_DIRS` environment variable
146
+ # and its defaults according to the XDG Base Directory Spec.
147
+ #
148
+ # @return [Array<String>]
149
+ #
150
+ def config_dirs
151
+ @config_dirs ||= validate_dirs_env("XDG_CONFIG_DIRS") ||
152
+ validate_dirs(["/etc/xdg"]) || []
153
+ end
154
+
155
+ ##
156
+ # Returns the absolute path to the single base directory relative to
157
+ # which user-specific runtime files and other file objects should be
158
+ # placed. May return `nil` if no such directory could be determined.
159
+ #
160
+ # @return [String,nil]
161
+ #
162
+ def runtime_dir
163
+ @runtime_dir = validate_dir_env("XDG_RUNTIME_DIR") unless defined? @runtime_dir
164
+ @runtime_dir
165
+ end
166
+
167
+ ##
168
+ # Searches the data directories for an object with the given relative
169
+ # path, and returns an array of absolute paths to all objects found in
170
+ # the data directories (i.e. in {#data_dirs} or {#data_home}), in order
171
+ # from most to least important.
172
+ #
173
+ # @param path [String] Relative path of the object to search for
174
+ # @param type [String,Symbol,Array<String,Symbol>] The type(s) of objects
175
+ # to find. You can specify any of the types defined by
176
+ # [File::Stat#ftype](https://ruby-doc.org/core/File/Stat.html#method-i-ftype),
177
+ # such as `file` or `directory`, or the special type `any`. Types can
178
+ # be specified as strings or the corresponding symbols. If this
179
+ # argument is not provided, the default of `file` is used.
180
+ # @return [Array<String>]
181
+ #
182
+ def lookup_data(path, type: :file)
183
+ lookup_internal([data_home] + data_dirs, path, type)
184
+ end
185
+
186
+ ##
187
+ # Searches the config directories for an object with the given relative
188
+ # path, and returns an array of absolute paths to all objects found in
189
+ # the config directories (i.e. in {#config_dirs} or {#config_home}), in
190
+ # order from most to least important.
191
+ #
192
+ # @param path [String] Relative path of the object to search for
193
+ # @param type [String,Symbol,Array<String,Symbol>] The type(s) of objects
194
+ # to find. You can specify any of the types defined by
195
+ # [File::Stat#ftype](https://ruby-doc.org/core/File/Stat.html#method-i-ftype),
196
+ # such as `file` or `directory`, or the special type `any`. Types can
197
+ # be specified as strings or the corresponding symbols. If this
198
+ # argument is not provided, the default of `file` is used.
199
+ # @return [Array<String>]
200
+ #
201
+ def lookup_config(path, type: :file)
202
+ lookup_internal([config_home] + config_dirs, path, type)
203
+ end
204
+
205
+ ##
206
+ # Returns the absolute path to a directory under {#data_home}, creating
207
+ # it if it doesn't already exist.
208
+ #
209
+ # @param path [String] The relative path to the subdir within the base
210
+ # data directory.
211
+ # @return [String] The absolute path to the subdir.
212
+ # @raise [Errno::EEXIST] If a non-directory already exists there
213
+ #
214
+ def ensure_data_subdir(path)
215
+ ensure_subdir_internal(data_home, path)
216
+ end
217
+
218
+ ##
219
+ # Returns the absolute path to a directory under {#config_home}, creating
220
+ # it if it doesn't already exist.
221
+ #
222
+ # @param path [String] The relative path to the subdir within the base
223
+ # config directory.
224
+ # @return [String] The absolute path to the subdir.
225
+ # @raise [Errno::EEXIST] If a non-directory already exists there
226
+ #
227
+ def ensure_config_subdir(path)
228
+ ensure_subdir_internal(config_home, path)
229
+ end
230
+
231
+ ##
232
+ # Returns the absolute path to a directory under {#state_home}, creating
233
+ # it if it doesn't already exist.
234
+ #
235
+ # @param path [String] The relative path to the subdir within the base
236
+ # state directory.
237
+ # @return [String] The absolute path to the subdir.
238
+ # @raise [Errno::EEXIST] If a non-directory already exists there
239
+ #
240
+ def ensure_state_subdir(path)
241
+ ensure_subdir_internal(state_home, path)
242
+ end
243
+
244
+ ##
245
+ # Returns the absolute path to a directory under {#cache_home}, creating
246
+ # it if it doesn't already exist.
247
+ #
248
+ # @param path [String] The relative path to the subdir within the base
249
+ # cache directory.
250
+ # @return [String] The absolute path to the subdir.
251
+ # @raise [Errno::EEXIST] If a non-directory already exists there
252
+ #
253
+ def ensure_cache_subdir(path)
254
+ ensure_subdir_internal(cache_home, path)
255
+ end
256
+
257
+ private
258
+
259
+ def validate_dir_env(name)
260
+ path = @env[name].to_s
261
+ !path.empty? && Compat.absolute_path?(path) ? path : nil
262
+ end
263
+
264
+ def validate_dirs_env(name)
265
+ validate_dirs(@env[name].to_s.split(::File::PATH_SEPARATOR))
266
+ end
267
+
268
+ def validate_dirs(paths)
269
+ paths = paths.find_all { |path| Compat.absolute_path?(path) }
270
+ paths.empty? ? nil : paths
271
+ end
272
+
273
+ def lookup_internal(dirs, path, types)
274
+ results = []
275
+ types = Array(types).map(&:to_s)
276
+ dirs.each do |dir|
277
+ to_check = ::File.join(dir, path)
278
+ stat = ::File.stat(to_check) rescue nil # rubocop:disable Style/RescueModifier
279
+ if stat&.readable? && (types.include?("any") || types.include?(stat.ftype))
280
+ results << to_check
281
+ end
282
+ end
283
+ results
284
+ end
285
+
286
+ def ensure_subdir_internal(base_dir, path)
287
+ path = ::File.join(base_dir, path)
288
+ ::FileUtils.mkdir_p(path, mode: 0o700)
289
+ path
290
+ end
291
+ end
292
+ end
293
+ end
@@ -50,14 +50,21 @@ module Toys
50
50
  end
51
51
  alias to_s string
52
52
 
53
- ## @private
53
+ ##
54
+ # Tests two wrappable strings for equality
55
+ # @param other [Object]
56
+ # @return [Boolean]
57
+ #
54
58
  def ==(other)
55
59
  return false unless other.is_a?(WrappableString)
56
60
  other.fragments == fragments
57
61
  end
58
62
  alias eql? ==
59
63
 
60
- ## @private
64
+ ##
65
+ # Returns a hash code for this object
66
+ # @return [Integer]
67
+ #
61
68
  def hash
62
69
  fragments.hash
63
70
  end