toys-core 0.11.5 → 0.13.0

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