toys 0.21.0 → 0.23.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +101 -67
  5. data/builtins/do.rb +208 -2
  6. data/builtins/system/bash-completion.rb +1 -1
  7. data/builtins/system/test.rb +2 -2
  8. data/builtins/system/tools.rb +2 -2
  9. data/builtins/system/zsh-completion.rb +22 -22
  10. data/core-docs/toys/acceptor.rb +20 -29
  11. data/core-docs/toys/arg_parser.rb +61 -48
  12. data/core-docs/toys/cli.rb +314 -163
  13. data/core-docs/toys/completion.rb +64 -67
  14. data/core-docs/toys/context.rb +88 -40
  15. data/core-docs/toys/core.rb +3 -3
  16. data/core-docs/toys/dsl/base.rb +6 -6
  17. data/core-docs/toys/dsl/flag.rb +4 -3
  18. data/core-docs/toys/dsl/flag_group.rb +3 -3
  19. data/core-docs/toys/dsl/positional_arg.rb +2 -2
  20. data/core-docs/toys/dsl/tool.rb +227 -88
  21. data/core-docs/toys/errors.rb +72 -61
  22. data/core-docs/toys/flag.rb +24 -34
  23. data/core-docs/toys/flag_group.rb +15 -19
  24. data/core-docs/toys/input_file.rb +8 -12
  25. data/core-docs/toys/loader/load_state.rb +4 -0
  26. data/core-docs/toys/loader/tool_registry.rb +4 -0
  27. data/core-docs/toys/loader.rb +88 -326
  28. data/core-docs/toys/middleware.rb +13 -40
  29. data/core-docs/toys/mixin.rb +12 -7
  30. data/core-docs/toys/module_lookup.rb +3 -3
  31. data/core-docs/toys/positional_arg.rb +13 -10
  32. data/core-docs/toys/runner.rb +159 -0
  33. data/core-docs/toys/source_info/origin.rb +131 -0
  34. data/core-docs/toys/source_info.rb +67 -160
  35. data/core-docs/toys/source_list.rb +72 -0
  36. data/core-docs/toys/source_spec.rb +317 -0
  37. data/core-docs/toys/standard_middleware/add_verbosity_flags.rb +4 -4
  38. data/core-docs/toys/standard_middleware/apply_config.rb +36 -6
  39. data/core-docs/toys/standard_middleware/handle_usage_errors.rb +3 -3
  40. data/core-docs/toys/standard_middleware/set_default_descriptions.rb +2 -2
  41. data/core-docs/toys/standard_middleware/show_help.rb +54 -54
  42. data/core-docs/toys/standard_middleware/show_root_version.rb +3 -3
  43. data/core-docs/toys/standard_mixins/bundler.rb +7 -4
  44. data/core-docs/toys/standard_mixins/exec.rb +14 -14
  45. data/core-docs/toys/standard_mixins/fileutils.rb +3 -3
  46. data/core-docs/toys/standard_mixins/gems.rb +55 -4
  47. data/core-docs/toys/standard_mixins/git_cache.rb +3 -3
  48. data/core-docs/toys/standard_mixins/highline.rb +4 -4
  49. data/core-docs/toys/standard_mixins/pager.rb +3 -3
  50. data/core-docs/toys/standard_mixins/terminal.rb +6 -6
  51. data/core-docs/toys/standard_mixins/xdg.rb +3 -3
  52. data/core-docs/toys/template.rb +28 -16
  53. data/core-docs/toys/tool_definition.rb +112 -111
  54. data/core-docs/toys/tool_name_splitter.rb +82 -0
  55. data/core-docs/toys/unique_key.rb +35 -0
  56. data/core-docs/toys/utils/completion_engine.rb +24 -13
  57. data/core-docs/toys/utils/exec.rb +30 -12
  58. data/core-docs/toys/utils/gems.rb +52 -45
  59. data/core-docs/toys/utils/git_cache.rb +163 -163
  60. data/core-docs/toys/utils/help_text.rb +24 -15
  61. data/core-docs/toys/utils/pager.rb +2 -2
  62. data/core-docs/toys/utils/standard_ui.rb +47 -34
  63. data/core-docs/toys/utils/terminal.rb +8 -8
  64. data/core-docs/toys/utils/xdg.rb +25 -16
  65. data/core-docs/toys/wrappable_string.rb +4 -4
  66. data/core-docs/toys-core.rb +66 -20
  67. data/docs/guide.md +1964 -1342
  68. data/lib/toys/standard_cli.rb +28 -49
  69. data/lib/toys/templates/clean.rb +1 -1
  70. data/lib/toys/templates/gem_build.rb +46 -31
  71. data/lib/toys/templates/minitest.rb +10 -10
  72. data/lib/toys/templates/rake.rb +9 -9
  73. data/lib/toys/templates/rdoc.rb +5 -5
  74. data/lib/toys/templates/rspec.rb +10 -10
  75. data/lib/toys/templates/rubocop.rb +8 -8
  76. data/lib/toys/templates/yardoc.rb +28 -28
  77. data/lib/toys/testing.rb +25 -6
  78. data/lib/toys/version.rb +1 -1
  79. data/lib/toys.rb +2 -1
  80. metadata +15 -8
  81. data/core-docs/toys/settings.rb +0 -543
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87582d55a2f60738d19c3c4d717f93910c5cc677f5e210c8418366baf2e91921
4
- data.tar.gz: f82f165fa06113b1ad398c5cb2d5e62217d860924f00e5b0d3b6172824d4a116
3
+ metadata.gz: ee5fe663d934949887ff7ce01289453e7b59efcc553331e2efb8a8ae949b626b
4
+ data.tar.gz: c34a38d893789bdb14fc159f211ac83b09a39b2d897601c961755876be2b6d28
5
5
  SHA512:
6
- metadata.gz: 5a9dad0dcdd8c83d678dfe23522faaa38b62bb765e16f7b6aafce2ce014920d13addce0d32a619f4c5d80e21f8e79b40ff8c0936381169df17d080d3d2500aec
7
- data.tar.gz: 0f2b4c0cc5cf87ed070d7f21d57f8a9b2051277c5651f5e4d210b9ebb2a9498df93dde3203cb54437c19f8991ed348ab3b9d59c5a944c3070aa22e09a7cdd14e
6
+ metadata.gz: 0fd0c5c2edfe80ede91db110a513bfe176766b4d3c8a75e378721296b8fe4c7c5681e07695fa249f9efede159ffb9feff43524d3ad085f98ca7498af9433c3d1
7
+ data.tar.gz: 5345ecacee610f36dd439f7adf8b1118a1cc0ecb858f19a5f3c938a5e86a33fb19be94fff5173e3170d1bac6b8ed4309fc54192f5542573e15a7109ddeaba9e2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,129 @@
1
1
  # Release History
2
2
 
3
+ ### v0.23.0 / 2026-09-09
4
+
5
+ This is a major release, with several new features. It also includes a significant refactor of some of the underlying layers, which should be mostly invisible. It is a release candidate for the upcoming version 1.0.
6
+
7
+ Highlights include:
8
+
9
+ * Added the ability to treat unknown flags as positional args via the `treat_unknown_flags_as_args` DSL directive. Useful for wrapping and delegating to other commands.
10
+ * The `do` builtin tool supports `--gem=`, `--git=`, and `--path=` flags that make additional tool sources available.
11
+ * Running tools from within a tool should generally now be done via the new `Toys::Runner` object, now available in the runtime context, instead of via the `Toys::CLI`. The new object provides finer-grained control over error handling and verbosity.
12
+ * Updates to error handling to improve the output and fix some long-standing issues related to tool delegation.
13
+ * Several fixes to bundler integration, especially with Bundler 4.
14
+
15
+ Details follow.
16
+
17
+ * Changes to tool definition:
18
+ * Added the `treat_unknown_flags_as_args` DSL directive (and the corresponding `ToolDefinition#treat_unknown_flags_as_args` setter and `ToolDefinition#unknown_flags_are_args?` query), which redirects unrecognized flags to the tool's positional arguments instead of reporting a usage error. Useful for wrapping and delegating to other commands.
19
+ * The `set_context_directory` DSL directive can now take Pathname arguments and handles relative paths (by expanding to absolute paths).
20
+ * When two or more flags are declared using the same context key, within a flag group, the flag group's requirements now evaluate correctly.
21
+ * A flag or optional/remaining positional argument declared with a `nil` default no longer clears default data already set for the same key by an earlier declaration.
22
+ * Toys now raises `Toys::ToolDefinitionError` if you attempt to "reset" a tool previously defined using a `Toys::Tool` subclass.
23
+ * SECURITY FIX: Toys now raises `Toys::ToolSourceError` if a tool loaded from a RubyGem attempts to load tools from the local file system.
24
+ * BREAKING CHANGE: Toys now raises `Toys::ToolDefintionError` if you attempt to create or descend into a subtool from a `subtool_apply` block or a middleware-based config.
25
+ * Caught additional cases of incorrectly located `Toys::Tool` subclasses and raised `Toys::ToolDefinitionError`
26
+ * Toys directory contents are loaded in deterministic (sorted) order.
27
+
28
+ * Changes to tool execution:
29
+ * Added `Toys::Context#runner` for access to the `Toys::Runner` object. This is now the preferred way to run tools from within other tools (instead of `Toys::CLI#run`), unless you need to change the configuration.
30
+ * Added `Toys::Context#loader` for access to the `Toys::Loader` object.
31
+ * BREAKING CHANGE: `Toys::Context#context_directory`, and `Toys::Context#get` with the associated context key, now never return nil. If no context directory is set for the tool, these now return the current working directory.
32
+ * BREAKING CHANGE: Removed git- and gem- specific attributes from `Toys::SourceInfo` and replaced with `Toys::SourceInfo#origin`.
33
+ * Standard context keys and sentinels use the new class `Toys::UniqueKey`, which displays useful names in diagnostic output.
34
+ * Delegation now propagates the caller's verbosity to the delegate target, rather than resetting it to zero.
35
+ * When a tool delegates, a `SignalException` unhandled by the inner tool now propagates outward so each tool in the delegation chain gets a chance at its own `on_interrupt` / `on_signal` handler.
36
+ * A nested run that shares a logger with the run that called it now uses the base level already in effect for that logger, so verbosity no longer compounds across nested runs.
37
+ * Revamped the output and stack trace printed when an exception occurs.
38
+
39
+ * Changes to shell completion:
40
+ * Fixed shell completion for non-flag words containing `=` or `:`: completions are now computed against the whole word, and the engine trims candidates to the span the shell will actually replace.
41
+ * Zsh completion now replaces the entire word rather than only the text after an `=` or `:`, matching how zsh actually handles word breaks; bash continues to break at `=` and `:`.
42
+ * Word-break trimming for bash now ignores `=` and `:` characters that were quoted or backslash-escaped, since the shell does not break a word at a quoted character.
43
+ * Subtool completion now uses the loader's configured delimiters to decide where a tool path ends, rather than a separate regex over the fragment prefix, so completing a partially typed tool path works consistently with any configured extra delimiters.
44
+
45
+ * Changes to builtin tools:
46
+ * The `do` builtin tool supports `--gem=`, `--git=`, and `--path=` flags that make additional tool sources available.
47
+
48
+ * Changes to CLI interfaces:
49
+ * `Toys::CLI#child` accepts `copy_sources: true`, which populates the new CLI's loader with the same sources as the original. This makes it easy to create a new CLI with additional sources on top of the current.
50
+ * You can get the `Toys::Runner` for a CLI using `Toys::CLI#runner`, providing a way to customize the run process more closely than using `Toys::CLI#run`.
51
+ * Added `add_source` method that takes an instance of the new `Toys::SourceSpec` types that describe a tool source. This method replaces the now deprecated `add_config_*` methods (although those methods will continue to be supported for a time). In general, the "config" terminology is being retired in favor of "source" which I think better describes what is going on.
52
+ * BREAKING CHANGE: The `config_file_name` and `config_dir_name` arguments to CLI have been renamed to `toplevel_tool_file_name` and `toplevel_tool_dir_name`, respectively, as part of a general removal of the "config" term. The old names are not aliased.
53
+ * BREAKING CHANGE: `Toys::CLI#run` no longer accepts the `delegated_from:` keyword argument. Tool delegation is now handled internally and its runtime is not exposed in the public interface.
54
+ * BREAKING CHANGE: Renamed the `StandardCLI::CONFIG_DIR_NAME` to `StandardCLI::TOPLEVEL_TOOL_DIR_NAME` and `StandardCLI::CONFIG_FILE_NAME` to `StandardCLI::TOPLEVEL_TOOL_FILE_NAME`.
55
+ * BREAKING CHANGE: Removed the constants `INDEX_FILE_NAME`, `PRELOAD_DIR_NAME`, `PRELOAD_FILE_NAME`, `DATA_DIR_NAME`, and `LIB_DIR_NAME` from `StandardCLI`.
56
+ * The `Toys::StandardCLI` constructor now takes `git_cache` and `gems_util` keyword arguments letting you customize the objects used to resolve git and gem sources.
57
+ * Path parameters for the methods that add sources, such as `path` and `context_directory`, can now take Pathname objects and can handle relative paths properly (by expanding them into absolute paths).
58
+
59
+ * Rubygems/Bundler integration changes:
60
+ * Bundler integration requires Bundler 2.4 or later. This is the default on Ruby 3.2, but requires that Bundler is updated on earlier Rubies.
61
+ * Bundler integration properly detects provenance attributes such as `path:` and `git:`.
62
+ * Calling bundler integration with a nonempty group list no longer loses gems that were already loaded.
63
+ * Bundler integration now honors `BUNDLE_LOCKFILE` (new in Bundler 4) and stops leaking it from the modified bundle.
64
+
65
+ * Other fixes
66
+ * Terminal mixin now raises the correct ArgumentError (instead of NameError) if given an unknown style code.
67
+ * `Toys::Loader#list_subtools` now ensures returned tools have been finished by their middleware. This affects listing tools not underneath the currently running tool.
68
+
69
+ A number of additional minor breaking changes were made to internal interfaces in the toys-core gem classes. It is unlikely you will encounter these unless you are extending the framework, but you can see the toys-core changelog for details.
70
+
71
+ ### v0.22.0 / 2026-05-05
72
+
73
+ Toys 0.22 is a major release focused on polish and cleanup in preparation for version 1.0. It includes a number of small breaking changes where needed to clean up the interfaces. (Note that many of the changes listed below are actually in the `toys-core` gem.)
74
+
75
+ Breaking interface changes:
76
+
77
+ * The `static` directive no longer "forces" creating of helper methods, instead using the same logic as the flag and argument directives. In particular, by default it will not create a method starting with underscore.
78
+ * Removed `Toys::Settings` and moved inheritable_helper_methods to a dedicated tool attribute. Use the `basic_settings` gem if you need the old settings class.
79
+ * `Toys::Acceptor#create` no longer takes a hash as the spec. (This was undocumented but worked previously.) To construct an object, pass the class as the main spec object, and the keyword arguments in the options.
80
+ * `Toys::Acceptor::Enum#values` now returns the values themselves instead of a zipped array that includes strings.
81
+ * `Toys::FlagGroup::Base` is no longer instantiable; use `Toys::FlagGroup::Optional` instead.
82
+ * The `acceptor` and `display_name` attributes of `Toys::PositionalArg` are no longer writable and must be set in the constructor.
83
+ * `Toys::Completion#create` no longer takes a hash as the spec. (This was undocumented but worked previously.) To construct an object, pass the class as the main spec object, and the keyword arguments in the options.
84
+ * The `delegation_target` attribute of `Toys::ToolDefinition::DefaultCompletion` is no longer writable and must be set in the constructor.
85
+ * You can no longer override a completion class using the `:""` key. (This was undocumented but worked previously.) Just pass a fully constructed completion object if you need a custom object.
86
+ * The original `Module#include` method is now available in the DSL as `include_module` instead of `super_include`.
87
+ * Template classes no longer automatically include `Toys::Context::Key`. This behavior was undocumented and inconsistent between different ways of defining templates.
88
+ * Made `Toys::CLI#load_tool` return the block value rather than the exit_code.
89
+ * Made `Toys::Testing#toys_load_tool` return the block value rather than the exit_code
90
+ * Dropped undocumented `Loader#add_path_set` option to include individual source names in the relative_paths argument.
91
+ * Renamed `use_less` in the `Toys::StandardMiddleware::ShowHelp` constructor to `use_pager`, and added support for custom pagers.
92
+ * Renamed many of the methods in `StandardUI` to reduce confusion.
93
+ * Removed `LoaderError` and used `ToolDefinitionError` for those cases, as it was decided that the distinction was ambiguous and dependent on internal details.
94
+ * Removed `ContextualError#underlying_error` as it was synonymous with `cause`.
95
+ * Consolidated `ContextualError` public interface into a single `capture` method.
96
+ * `ArgParser::ExtraArgumentsError` now takes `:arguments` instead of `:values`, and provides an accessor for the same.
97
+ * `ArgParser::ToolUnrecognizedError` now takes `:full_name` instead of `:values`, and provides an accessor for the same.
98
+ * Renamed `ArgParser::UsageError#full_message` to `message_with_suggestions` to avoid overriding `Exception#full_message`.
99
+
100
+ New features:
101
+
102
+ * The range acceptor supports beginless and endless ranges.
103
+ * A custom source_name can be provided to `Loader#add_path_set`, `Loader#add_git`, and `Loader#add_gem`.
104
+ * The `:gems` mixin provides a context key for retrieving the underlying `Toys::Utils::Gems` service object.
105
+ * The `:gems` mixin provides an explicit `Toys::Utils::Gems::ClassMethods` module defining the directives added to the tool class.
106
+ * The `activate` and `bundle` methods in the Gems utility now return useful results.
107
+
108
+ Other fixes:
109
+
110
+ * The `:update` argument to the `load_git` directive had no effect if the `:as` argument was provided.
111
+ * `FlagValueUnacceptableError` and `ArgValueUnacceptableError` have their value set correctly.
112
+ * If a CLI has a static (shared) logger, creating a child with `logger: nil` actually clears it.
113
+ * Removed unused `Toys::StandardMiddleware::ShowHelp::TOOL_NAME_KEY`.
114
+ * ShowHelp middleware displays an error message instead of raising an exception when help cannot be generated due to a bad search regex.
115
+ * `ArgParser::UsageError` is now a proper exception.
116
+ * `ContextualError` does a better job of capturing relevant syntax errors.
117
+ * Various minor completion system fixes.
118
+
119
+ Several libraries under `toys/utils` were extracted into their own gems. The current code remains under `Toys::Utils` as a vendored copy of the external gem, so the extracted gems are not actually dependencies of Toys. The affected libraries are:
120
+
121
+ * `Toys::Utils::Exec` which was extracted to the gem `exec_service`.
122
+ * `Toys::Utils::XDG` which was extracted to the gem `simple_xdg`.
123
+ * `Toys::Utils::GitCache` which was extracted to the gem `git_cache`.
124
+
125
+ Finally, a variety of clarifications and fixes were made to the reference documentation, readme, and user's guide.
126
+
3
127
  ### v0.21.0 / 2026-03-23
4
128
 
5
129
  This release includes a variety of small fixes and updates toward improving product polish in preparation for a 1.0 release. It focuses on the following areas:
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # License
2
2
 
3
- Copyright 2019-2023 Daniel Azuma and the Toys contributors
3
+ Copyright 2019-2026 Daniel Azuma and the Toys contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -23,12 +23,16 @@ Here's a tutorial to help you get a feel of what Toys can do.
23
23
 
24
24
  Install the **toys** gem using:
25
25
 
26
- $ gem install toys
26
+ ```
27
+ $ gem install toys
28
+ ```
27
29
 
28
30
  This installs the `toys` executable, along with some built-in tools and
29
31
  libraries. You can run the executable immediately:
30
32
 
31
- $ toys
33
+ ```
34
+ $ toys
35
+ ```
32
36
 
33
37
  This displays overall help for Toys. If you have `less` installed, Toys will
34
38
  use it to display the help screen. Press `q` to exit.
@@ -36,19 +40,25 @@ use it to display the help screen. Press `q` to exit.
36
40
  You may notice that the help screen lists some tools that are pre-installed.
37
41
  Let's run one of them:
38
42
 
39
- $ toys system version
43
+ ```
44
+ $ toys system version
45
+ ```
40
46
 
41
47
  The `system version` tool displays the current version of the toys gem.
42
48
 
43
- Toys also provides optional tab completion for bash. To install it, execute the
44
- following command in your shell, or add it to your bash configuration file
45
- (e.g. `~/.bashrc`).
49
+ Toys also provides optional tab completion for bash and zsh. To install for
50
+ bash, execute the following command in your shell, or add it to your bash
51
+ configuration file (e.g. `~/.bashrc`).
46
52
 
47
- $(toys system bash-completion install)
53
+ ```
54
+ $(toys system bash-completion install)
55
+ ```
48
56
 
49
- Toys does not yet specially implement tab completion for zsh or other shells.
50
- However, if you are using zsh, installing bash completion using `bashcompinit`
51
- *mostly* works.
57
+ To install for zsh, execute this or add to your `~/.zshrc`.
58
+
59
+ ```
60
+ $(toys system zsh-completion install)
61
+ ```
52
62
 
53
63
  ### Write your first tool
54
64
 
@@ -56,32 +66,42 @@ You can define tools by creating a *Toys file*. Go into any directory, and,
56
66
  using your favorite editor, create a new file called `.toys.rb` (note the
57
67
  leading period). Copy the following text into the file, and save it:
58
68
 
59
- tool "greet" do
60
- desc "My first tool!"
61
- flag :whom, default: "world"
62
- def run
63
- puts "Hello, #{whom}!"
64
- end
65
- end
69
+ ```ruby
70
+ tool "greet" do
71
+ desc "My first tool!"
72
+ flag :whom, default: "world"
73
+ def run
74
+ puts "Hello, #{whom}!"
75
+ end
76
+ end
77
+ ```
66
78
 
67
79
  This defines a tool named "greet". Try running it:
68
80
 
69
- $ toys greet
81
+ ```
82
+ $ toys greet
83
+ ```
70
84
 
71
85
  The tool also recognizes a flag on the command line. Try this:
72
86
 
73
- $ toys greet --whom=ruby
87
+ ```
88
+ $ toys greet --whom=ruby
89
+ ```
74
90
 
75
91
  Toys provides a rich set of features for defining command line arguments and
76
92
  flags. It can also validate arguments. Try this:
77
93
 
78
- $ toys greet --bye
94
+ ```
95
+ $ toys greet --bye
96
+ ```
79
97
 
80
98
  Notice that Toys automatically generated a usage summary for your tool. It also
81
99
  automatically generates a full help screen, which you can view using the
82
100
  `--help` flag:
83
101
 
84
- $ toys greet --help
102
+ ```
103
+ $ toys greet --help
104
+ ```
85
105
 
86
106
  Toys searches up the directory hierarchy for Toys files. So it will find this
87
107
  `.toys.rb` if you are located in this directory or any subdirectory. It will
@@ -99,53 +119,59 @@ likely to see in real-world usage. Add the following to your `.toys.rb` file.
99
119
  (You don't need to replace the greet tool you just wrote; just add this new
100
120
  tool to the end of the file.)
101
121
 
102
- tool "new-repo" do
103
- desc "Create a new git repo"
104
-
105
- optional_arg :name, desc: "Name of the directory to create"
106
-
107
- include :exec, exit_on_nonzero_status: true
108
- include :fileutils
109
- include :terminal
110
-
111
- def run
112
- if name.nil?
113
- response = ask "Please enter a directory name: "
114
- set :name, response
115
- end
116
- if File.exist? name
117
- puts "Aborting because #{name} already exists", :red, :bold
118
- exit 1
119
- end
120
- logger.info "Creating new repo in directory #{name}..."
121
- mkdir name
122
- cd name do
123
- create_repo
124
- end
125
- puts "Created repo in #{name}", :green, :bold
126
- end
127
-
128
- def create_repo
129
- exec "git init"
130
- File.write ".gitignore", <<~CONTENT
131
- tmp
132
- .DS_Store
133
- CONTENT
134
- # You can add additional files here.
135
- exec "git add ."
136
- exec "git commit -m 'Initial commit'"
137
- end
122
+ ```ruby
123
+ tool "new-repo" do
124
+ desc "Create a new git repo"
125
+
126
+ optional_arg :name, desc: "Name of the directory to create"
127
+
128
+ include :exec, exit_on_nonzero_status: true
129
+ include :fileutils
130
+ include :terminal
131
+
132
+ def run
133
+ if name.nil?
134
+ response = ask "Please enter a directory name: "
135
+ set :name, response
136
+ end
137
+ if File.exist? name
138
+ puts "Aborting because #{name} already exists", :red, :bold
139
+ exit 1
140
+ end
141
+ logger.info "Creating new repo in directory #{name}..."
142
+ mkdir name
143
+ cd name do
144
+ create_repo
138
145
  end
146
+ puts "Created repo in #{name}", :green, :bold
147
+ end
148
+
149
+ def create_repo
150
+ exec "git init"
151
+ File.write ".gitignore", <<~CONTENT
152
+ tmp
153
+ .DS_Store
154
+ CONTENT
155
+ # You can add additional files here.
156
+ exec "git add ."
157
+ exec "git commit -m 'Initial commit'"
158
+ end
159
+ end
160
+ ```
139
161
 
140
162
  Now you should have an additional tool called `new-repo` available. Type:
141
163
 
142
- $ toys
164
+ ```
165
+ $ toys
166
+ ```
143
167
 
144
168
  The help screen lists both the `greet` tool we started with, and the new
145
169
  `new-repo` tool. This new tool creates a directory containing a newly created
146
170
  git repo. (It assumes you have `git` available on your path.) Try running it:
147
171
 
148
- $ toys new-repo foo
172
+ ```
173
+ $ toys new-repo foo
174
+ ```
149
175
 
150
176
  That should create a directory `foo`, initialize a git repository within it,
151
177
  and make a commit.
@@ -155,7 +181,9 @@ any combination of flags and required and optional arguments. This tool's
155
181
  argument is declared with a description string, which you can see if you view
156
182
  the tool's help:
157
183
 
158
- $ toys new-repo --help
184
+ ```
185
+ $ toys new-repo --help
186
+ ```
159
187
 
160
188
  The argument is marked as "optional" which means you can omit it. Notice that
161
189
  the tool's code detects that it has been omitted and responds by prompting you
@@ -172,7 +200,7 @@ than modules. These symbols are the names of some of Toys's built-in helper
172
200
  *mixins*, which are configurable modules that enhance your tool. They may
173
201
  provide methods your tool can call, or invoke other behavior. In our example:
174
202
 
175
- * The `:exec` mixin provides a variety of methods for running external
203
+ * The `:exec` mixin provides a variety of methods for running external
176
204
  commands. In this example, we use the `exec` method to run shell
177
205
  commands, but you can also signal and control these commands, capture
178
206
  and redirect streams, and so forth. Note that we pass the
@@ -181,10 +209,10 @@ provide methods your tool can call, or invoke other behavior. In our example:
181
209
  to `set -e` in bash). This is a common pattern when writing tools that
182
210
  invoke external commands. (If you want more control, the `:exec` mixin also
183
211
  provides ways to respond to result codes individually.)
184
- * The `:fileutils` mixin provides the methods of the Ruby `FileUtils`
212
+ * The `:fileutils` mixin provides the methods of the Ruby `FileUtils`
185
213
  library, such as `mkdir` and `cd` used in this example. It's effectively
186
214
  shorthand for `require "fileutils"; include ::FileUtils`.
187
- * The `:terminal` mixin provides styled output, as you can see with the style
215
+ * The `:terminal` mixin provides styled output, as you can see with the style
188
216
  codes being passed to `puts`. It also provides some user interaction
189
217
  commands such as `ask`, as well as spinners and other controls. You can see
190
218
  operation of the `:terminal` mixin in the tool's output, which is styled
@@ -193,7 +221,9 @@ provide methods your tool can call, or invoke other behavior. In our example:
193
221
 
194
222
  Now try running this:
195
223
 
196
- $ toys new-repo bar --verbose
224
+ ```
225
+ $ toys new-repo bar --verbose
226
+ ```
197
227
 
198
228
  You'll notice some diagnostic log output. Toys provides a standard Ruby Logger
199
229
  for each tool, and you can use it to emit diagnostic logs directly as
@@ -219,14 +249,18 @@ If you have a project with a Rakefile, move into that directory and create a
219
249
  new file called `.toys.rb` in that same directory (next to the Rakefile). Add
220
250
  the following line to your `.toys.rb` file:
221
251
 
222
- expand :rake
252
+ ```ruby
253
+ expand :rake
254
+ ```
223
255
 
224
256
  This syntax is called a "template expansion." It's a way to generate tools
225
257
  programmatically. In this case, Toys provides the `:rake` template, which reads
226
258
  your Rakefile and generates Toys tools corresponding to all your Rake tasks!
227
259
  Now if you run:
228
260
 
229
- $ toys
261
+ ```
262
+ $ toys
263
+ ```
230
264
 
231
265
  You'll see that you now have tools associated with each of your Rake tasks. So
232
266
  if you have a `rake test` task, you can run it using `toys test`.
@@ -326,7 +360,7 @@ because it has a few known bugs that affect Toys.
326
360
 
327
361
  ## License
328
362
 
329
- Copyright 2019-2025 Daniel Azuma and the Toys contributors
363
+ Copyright 2019-2026 Daniel Azuma and the Toys contributors
330
364
 
331
365
  Permission is hereby granted, free of charge, to any person obtaining a copy
332
366
  of this software and associated documentation files (the "Software"), to deal
data/builtins/do.rb CHANGED
@@ -15,7 +15,18 @@ long_desc \
15
15
  "You may change the delimiter using the --delim flag. For example:",
16
16
  [" toys do --delim=/ rails build --staging / deploy --migrate"],
17
17
  "The --delim flag must appear first before the tools to run. Any flags that appear later in" \
18
- " the command line will be passed to the tools themselves."
18
+ " the command line will be passed to the tools themselves.",
19
+ "",
20
+ "You may also make additional tools available to the tools you run, by passing the --gem," \
21
+ " --git, and --path flags. Each adds a source of tools, which takes priority over the" \
22
+ " tools that would otherwise be found. The three flags may be repeated and interleaved," \
23
+ " and the source added by the leftmost flag takes priority over the sources added by the" \
24
+ " flags to its right. For example:",
25
+ [" toys do --gem=my-tools --git=https://github.com/dazuma/example deploy --migrate"],
26
+ "Here, a tool defined by both sources is taken from the gem. See the descriptions of the" \
27
+ " individual flags below for the syntax of their values. Note that the commas within those" \
28
+ " values are part of the flag value, and are unrelated to the delimiter that separates the" \
29
+ " tools to run."
19
30
 
20
31
  flag :delim do
21
32
  flags "-d", "--delim=VALUE"
@@ -24,6 +35,67 @@ flag :delim do
24
35
  long_desc "Sets the delimiter that separates tool invocations. The default value is \",\"."
25
36
  end
26
37
 
38
+ # The source flags all share the :sources key, and their handlers append to
39
+ # the same array, tagging each value with the kind of source requested. That
40
+ # way the array records the order in which the flags appeared on the command
41
+ # line, across all of the flags, which is the order that determines priority.
42
+ # The handlers store the raw value; parsing happens later, in build_cli,
43
+ # because an error raised from a handler would come out of the argument parser
44
+ # as a stack trace rather than as a simple message.
45
+
46
+ flag :sources do
47
+ flags "--gem=GEM"
48
+ handler { |val, prev| prev + [[:gem, val]] }
49
+ default []
50
+ desc "Make the tools from the given gem available"
51
+ long_desc \
52
+ "Adds the tools from the given gem, prompting to install the gem if it is not present.",
53
+ "",
54
+ "The value is the gem name, optionally followed by any number of version requirements," \
55
+ " all separated by commas. Whitespace surrounding each element is ignored. The version" \
56
+ " requirements use the same syntax as Rubygems and Bundler. For example:",
57
+ [" --gem=\"my-tools, ~> 1.5, >= 1.5.2\""]
58
+ end
59
+
60
+ flag :sources do
61
+ flags "--git=SPEC"
62
+ handler { |val, prev| prev + [[:git, val]] }
63
+ default []
64
+ desc "Make the tools from the given git repository available"
65
+ long_desc \
66
+ "Adds the tools from the given git repository, fetching the repository into a local cache" \
67
+ " if it is not already there.",
68
+ "",
69
+ "The value is the git remote (i.e. the repository URL or path), optionally followed by any" \
70
+ " number of \"key=value\" elements, all separated by commas. Whitespace surrounding each" \
71
+ " element, and surrounding each equals sign, is ignored. For example:",
72
+ [" --git=\"https://github.com/dazuma/example, path=toys, commit=main\""],
73
+ "The recognized keys are:",
74
+ [" path The file or directory within the repository to load. By default, the"],
75
+ [" entire repository is loaded."],
76
+ [" commit The SHA, tag, or branch to load. By default, the repository head is used."],
77
+ [" update Whether to refresh a previously cached repository. Pass \"true\" or \"false\","],
78
+ [" or a number of seconds, to refresh only if the cache is at least that old."],
79
+ [" The default is \"false\"."],
80
+ "Unlike --gem, which takes its elements positionally, the elements here are named, because" \
81
+ " a git source has three independent optional fields and a positional syntax could not" \
82
+ " express, for example, a path with no commit.",
83
+ "",
84
+ "There is no way to escape a comma appearing within the value."
85
+ end
86
+
87
+ flag :sources do
88
+ flags "--path=PATH"
89
+ handler { |val, prev| prev + [[:path, val]] }
90
+ default []
91
+ complete_values :file_system
92
+ desc "Make the tools from the given path available"
93
+ long_desc \
94
+ "Adds the tools from the given file system path. The path must name either a directory" \
95
+ " of tools, or a single Ruby file defining tools. For example:",
96
+ [" --path=/path/to/my-tools"]
97
+ end
98
+
27
99
  remaining_args :commands do
28
100
  complete do |context|
29
101
  commands = context.arg_parser.data[:commands]
@@ -37,10 +109,144 @@ end
37
109
  enforce_flags_before_args
38
110
 
39
111
  def run
112
+ tool_cli = build_cli
40
113
  commands
41
114
  .chunk { |arg| arg == delim ? :_separator : true }
42
115
  .each do |_, action|
43
- code = cli.run(action)
116
+ code = tool_cli.run(action)
44
117
  exit(code) unless code.zero?
45
118
  end
46
119
  end
120
+
121
+ # Returns the CLI used to run the requested tools. Normally this is simply the
122
+ # current CLI, but if any sources were requested, we need a new CLI because
123
+ # sources cannot be added to a CLI that has already started loading tools. The
124
+ # new CLI copies the current sources and adds the requested ones on top of
125
+ # them. The requested sources are added in reverse order so that the first flag
126
+ # on the command line ends up with the highest priority.
127
+ #
128
+ # All the flag values are parsed up front, in command line order, so that a
129
+ # malformed value is reported before any source is added, and is reported
130
+ # against the first offending flag rather than the last.
131
+ #
132
+ # All added sources are then proactively resolved and failures reported.
133
+ def build_cli
134
+ specs = sources.map { |kind, value| parse_source_request(kind, value) }
135
+ return cli if specs.empty?
136
+ require "toys/utils/gems" if specs.any? { |spec| spec.is_a?(::Toys::SourceSpec::Gem) }
137
+ tool_cli = cli.child(copy_sources: true) do |child_cli|
138
+ specs.reverse_each { |spec| child_cli.add_source(spec, high_priority: true) }
139
+ end
140
+ begin
141
+ tool_cli.loader.resolve_sources
142
+ rescue ::Toys::ToolSourceError => e
143
+ logger.error("Unable to resolve the given tool sources: #{e.message}")
144
+ exit(1)
145
+ end
146
+ tool_cli
147
+ end
148
+
149
+ # Parses and checks a single source request, returning the source spec to add
150
+ # to the CLI later.
151
+ def parse_source_request(kind, value)
152
+ case kind
153
+ when :gem
154
+ parse_gem_request(value)
155
+ when :git
156
+ parse_git_request(value)
157
+ when :path
158
+ parse_path_request(value)
159
+ end
160
+ end
161
+
162
+ # Splits a --gem flag value into the gem name and its version requirements,
163
+ # and checks that both are valid. The requirements are validated by Rubygems
164
+ # itself, but are passed along as the original strings.
165
+ def parse_gem_request(gem_request)
166
+ gem_name, *gem_version = gem_request.split(",", -1).map(&:strip)
167
+ if gem_name.nil? || gem_name.empty? || gem_version.any?(&:empty?)
168
+ logger.fatal("Invalid --gem value: #{gem_request.inspect}")
169
+ exit(1)
170
+ end
171
+ begin
172
+ ::Gem::Requirement.create(*gem_version)
173
+ rescue ::Gem::Requirement::BadRequirementError => e
174
+ logger.fatal("Invalid version requirement for gem #{gem_name.inspect}: #{e.message}")
175
+ exit(1)
176
+ end
177
+ ::Toys::SourceSpec.gem(gem_name, version: gem_version)
178
+ end
179
+
180
+ # Splits a --git flag value into the git remote and the options that follow it,
181
+ # and checks that all of them are valid. Unrecognized and duplicate keys are
182
+ # errors rather than being ignored or resolved as last-wins, because a mistyped
183
+ # key would otherwise silently load something other than what was asked for.
184
+ def parse_git_request(git_request)
185
+ git_remote, *elements = git_request.split(",", -1).map(&:strip)
186
+ git_error(git_request, "the git remote is required") if git_remote.nil? || git_remote.empty?
187
+ opts = { path: nil, commit: nil, update: false }
188
+ seen_keys = []
189
+ elements.each do |element|
190
+ key, value = parse_git_element(git_request, element, seen_keys)
191
+ case key
192
+ when "path"
193
+ opts[:path] = value
194
+ when "commit"
195
+ opts[:commit] = value
196
+ when "update"
197
+ opts[:update] = parse_git_update(git_request, value)
198
+ else
199
+ git_error(git_request, "unrecognized key #{key.inspect}")
200
+ end
201
+ end
202
+ ::Toys::SourceSpec.git(git_remote, **opts)
203
+ end
204
+
205
+ # Splits one element following the git remote into its key and value, and
206
+ # checks that it is well-formed and does not repeat an earlier key.
207
+ def parse_git_element(git_request, element, seen_keys)
208
+ key, value = element.split("=", 2).map(&:strip)
209
+ if value.nil? || key.empty?
210
+ git_error(git_request, "expected \"key=value\" but got #{element.inspect}")
211
+ end
212
+ git_error(git_request, "empty value for key #{key.inspect}") if value.empty?
213
+ git_error(git_request, "duplicate key #{key.inspect}") if seen_keys.include?(key)
214
+ seen_keys << key
215
+ [key, value]
216
+ end
217
+
218
+ # Interprets the value of the "update" key in a --git flag value, which is
219
+ # either a boolean or a number of seconds.
220
+ def parse_git_update(git_request, value)
221
+ case value
222
+ when "true"
223
+ true
224
+ when "false"
225
+ false
226
+ when /\A\d+\z/
227
+ value.to_i
228
+ else
229
+ git_error(git_request, "invalid update value #{value.inspect}")
230
+ end
231
+ end
232
+
233
+ # Reports a malformed --git flag value, naming the part of the value that was
234
+ # not understood.
235
+ def git_error(git_request, message)
236
+ logger.fatal("Invalid --git value: #{git_request.inspect}: #{message}")
237
+ exit(1)
238
+ end
239
+
240
+ # Checks that a --path flag value is present. Whether the path actually names
241
+ # tools is left to the loader to determine, so that this tool does not have to
242
+ # duplicate that rule. The spec carries no context directory, like the gem and
243
+ # git sources, because it is injected from the command line rather than found
244
+ # in a project.
245
+ def parse_path_request(path_request)
246
+ path = path_request.strip
247
+ if path.empty?
248
+ logger.fatal("Invalid --path value: #{path_request.inspect}")
249
+ exit(1)
250
+ end
251
+ ::Toys::SourceSpec.path(path)
252
+ end