toys-core-0.21.0/0000755000004100000410000000000015165170677013565 5ustar www-datawww-datatoys-core-0.21.0/lib/0000755000004100000410000000000015165170677014333 5ustar www-datawww-datatoys-core-0.21.0/lib/toys/0000755000004100000410000000000015165170677015331 5ustar www-datawww-datatoys-core-0.21.0/lib/toys/source_info.rb0000644000004100000410000003532715165170677020203 0ustar www-datawww-data# frozen_string_literal: true module Toys ## # Information about the source of a tool, such as the file, git repository, # or block that defined it. # # This object represents a source of tool information and definitions. Such a # source could include: # # * A toys directory # * A single toys file # * A file or directory loaded from git # * A file or directory loaded from a gem # * A config block passed directly to the CLI # * A tool block within a toys file # # The SourceInfo provides information such as the tool's context directory, # and locates data and lib directories appropriate to the tool. It also # locates the tool's source code so it can be reported when an error occurs. # # Each tool has a unique SourceInfo with all the information specific to that # tool. Additionally, SourceInfo objects are arranged in a containment # hierarchy. For example, a SourceInfo object representing a toys files could # have a parent representing a toys directory, and an object representing a # tool block could have a parent representing an enclosing block or a file. # # Child SourceInfo objects generally inherit some attributes of their parent. # For example, the `.toys` directory in a project directory defines the # context directory as that project directory. Then all tools defined under # that directory will share that context directory, so all SourceInfo objects # descending from that root will inherit that value (unless it's changed # explicitly). # # SourceInfo objects can be obtained in the DSL from # {Toys::DSL::Tool#source_info} or at runtime by getting the # {Toys::Context::Key::TOOL_SOURCE} key. However, they are created internally # by the Loader and should not be created manually. # class SourceInfo ## # The parent of this SourceInfo. # # @return [Toys::SourceInfo] The parent. # @return [nil] if this SourceInfo is a root. # attr_reader :parent ## # The root ancestor of this SourceInfo. This generally represents a source # that was added directly to a CLI in code. # # @return [Toys::SourceInfo] The root ancestor. # attr_reader :root ## # The priority of tools defined by this source. Higher values indicate a # higher priority. Lower priority values could be negative. # # @return [Integer] The priority. # attr_reader :priority ## # The context directory path (normally the directory containing the # toplevel toys file or directory). # # This is not affected by setting a custom context directory for a tool. # # @return [String] The context directory path. # @return [nil] if there is no context directory (perhaps because the root # source was a block) # attr_reader :context_directory ## # The source, which may be a path or a proc depending on the {#source_type}. # # @return [String] Path to the source file or directory. # @return [Proc] The block serving as the source. # attr_reader :source ## # The type of source. This could be: # # * `:file`, representing a single toys file. The {#source} will be the # filesystem path to that file. # * `:directory`, representing a toys directory. The {#source} will be the # filesystem path to that directory. # * `:proc`, representing a proc, which could be a toplevel block added # directly to a CLI, a `tool` block within a toys file, or a block within # another block. The {#source} will be the proc itself. # # @return [:file,:directory,:proc] # attr_reader :source_type ## # The path of the current source file or directory. # # This could be set even if {#source_type} is `:proc`, if that proc is # defined within a toys file. The only time this is not set is if the # source is added directly to a CLI in a code block. # # @return [String] The source path # @return [nil] if this source has no file system path. # attr_reader :source_path ## # The source proc. This is set if {#source_type} is `:proc`. # # @return [Proc] The source proc # @return [nil] if this source has no proc. # attr_reader :source_proc ## # The git remote. This is set if the source, or one of its ancestors, comes # from git. # # @return [String] The git remote # @return [nil] if this source is not fron git. # attr_reader :git_remote ## # The git path. This is set if the source, or one of its ancestors, comes # from git. # # @return [String] The git path. This could be the empty string. # @return [nil] if this source is not fron git. # attr_reader :git_path ## # The git commit. This is set if the source, or one of its ancestors, comes # from git. # # @return [String] The git commit. # @return [nil] if this source is not fron git. # attr_reader :git_commit ## # The gem name. This is set if the source, or one of its ancestors, comes # from a gem. # # @return [String] The gem name. # @return [nil] if this source is not from a gem. # attr_reader :gem_name ## # The gem version. This is set if the source, or one of its ancestors, # comes from a gem. # # @return [Gem::Version] The gem version. # @return [nil] if this source is not from a gem. # attr_reader :gem_version ## # The path within the gem, including the toys root directory in the gem. # # @return [String] The path. # @return [nil] if this source is not from a gem. # attr_reader :gem_path ## # A user-visible name of this source. # # @return [String] # attr_reader :source_name alias to_s source_name ## # Locate the given data file or directory and return an absolute path. # # @param path [String] The relative path to find # @param type [nil,:file,:directory] Type of file system object to find, # or nil (the default) to return any type. # @return [String] Absolute path of the resulting data. # @return [nil] if the data was not found. # def find_data(path, type: nil) if @data_dir full_path = ::File.join(@data_dir, path) case type when :file return full_path if ::File.file?(full_path) when :directory return full_path if ::File.directory?(full_path) else return full_path if ::File.readable?(full_path) end end parent&.find_data(path, type: type) end ## # Apply all lib paths in order from high to low priority # # @return [self] # def apply_lib_paths parent&.apply_lib_paths $LOAD_PATH.unshift(@lib_dir) if @lib_dir && !$LOAD_PATH.include?(@lib_dir) self end ## # Create a SourceInfo. # # @private This interface is internal and subject to change without warning. # def initialize(parent, priority, context_directory, source_type, source_path, source_proc, git_remote, git_path, git_commit, gem_name, gem_version, gem_path, source_name, data_dir_name, lib_dir_name) @parent = parent @root = parent&.root || self @priority = priority @context_directory = context_directory @source_type = source_type @source = source_type == :proc ? source_proc : source_path @source_path = source_path @source_proc = source_proc @git_remote = git_remote @git_path = git_path @git_commit = git_commit @gem_name = gem_name @gem_version = gem_version @gem_path = gem_path @source_name = source_name || default_source_name @data_dir_name = data_dir_name @lib_dir_name = lib_dir_name @data_dir = find_special_dir(data_dir_name) @lib_dir = find_special_dir(lib_dir_name) end ## # Create a child SourceInfo relative to the parent path. # # @private This interface is internal and subject to change without warning. # def relative_child(filename, source_name: nil) unless source_type == :directory raise LoaderError, "relative_child is valid only on a directory source" end child_path, type = SourceInfo.check_path(::File.join(source_path, filename), true) return nil unless child_path child_git_path = git_path.empty? ? filename : ::File.join(git_path, filename) if git_path child_gem_path = gem_path.empty? ? filename : ::File.join(gem_path, filename) if gem_path SourceInfo.new(self, priority, context_directory, type, child_path, nil, git_remote, child_git_path, git_commit, gem_name, gem_version, child_gem_path, source_name, @data_dir_name, @lib_dir_name) end ## # Create a child SourceInfo with an absolute path. # # @private This interface is internal and subject to change without warning. # def absolute_child(child_path, source_name: nil) child_path, type = SourceInfo.check_path(child_path, false) SourceInfo.new(self, priority, context_directory, type, child_path, nil, nil, nil, nil, nil, nil, nil, source_name, @data_dir_name, @lib_dir_name) end ## # Create a child SourceInfo with a git source. # # @private This interface is internal and subject to change without warning. # def git_child(child_git_remote, child_git_path, child_git_commit, child_path, source_name: nil) child_path, type = SourceInfo.check_path(child_path, false) SourceInfo.new(self, priority, context_directory, type, child_path, nil, child_git_remote, child_git_path, child_git_commit, nil, nil, nil, source_name, @data_dir_name, @lib_dir_name) end ## # Create a child SourceInfo with a gem source. # # @private This interface is internal and subject to change without warning. # def gem_child(child_gem_name, child_gem_version, child_gem_path, child_path, source_name: nil) child_path, type = SourceInfo.check_path(child_path, false) SourceInfo.new(self, priority, context_directory, type, child_path, nil, nil, nil, nil, child_gem_name, child_gem_version, child_gem_path, source_name, @data_dir_name, @lib_dir_name) end ## # Create a proc child SourceInfo # # @private This interface is internal and subject to change without warning. # def proc_child(child_proc, source_name: nil) source_name ||= self.source_name SourceInfo.new(self, priority, context_directory, :proc, source_path, child_proc, git_remote, git_path, git_commit, gem_name, gem_version, gem_path, source_name, @data_dir_name, @lib_dir_name) end ## # Create a root source info for a file path. # # @private This interface is internal and subject to change without warning. # def self.create_path_root(source_path, priority, context_directory: nil, data_dir_name: nil, lib_dir_name: nil, source_name: nil) source_path, type = check_path(source_path, false) case context_directory when :parent context_directory = ::File.dirname(source_path) when :path context_directory = source_path end new(nil, priority, context_directory, type, source_path, nil, nil, nil, nil, nil, nil, nil, source_name, data_dir_name, lib_dir_name) end ## # Create a root source info for a cached git repo. # # @private This interface is internal and subject to change without warning. # def self.create_git_root(git_remote, git_path, git_commit, source_path, priority, context_directory: nil, data_dir_name: nil, lib_dir_name: nil, source_name: nil) source_path, type = check_path(source_path, false) new(nil, priority, context_directory, type, source_path, nil, git_remote, git_path, git_commit, nil, nil, nil, source_name, data_dir_name, lib_dir_name) end ## # Create a root source info for a loaded gem. # # @private This interface is internal and subject to change without warning. # def self.create_gem_root(gem_name, gem_version, gem_path, source_path, priority, context_directory: nil, data_dir_name: nil, lib_dir_name: nil, source_name: nil) source_path, type = check_path(source_path, false) new(nil, priority, context_directory, type, source_path, nil, nil, nil, nil, gem_name, gem_version, gem_path, source_name, data_dir_name, lib_dir_name) end ## # Create a root source info for a proc. # # @private This interface is internal and subject to change without warning. # def self.create_proc_root(source_proc, priority, context_directory: nil, data_dir_name: nil, lib_dir_name: nil, source_name: nil) new(nil, priority, context_directory, :proc, nil, source_proc, nil, nil, nil, nil, nil, nil, source_name, data_dir_name, lib_dir_name) end ## # Check a path and determine the canonical path and type. # # @private This interface is internal and subject to change without warning. # def self.check_path(path, lenient) path = ::File.expand_path(path) unless ::File.readable?(path) raise LoaderError, "Cannot read: #{path}" unless lenient return [nil, nil] end if ::File.file?(path) unless ::File.extname(path) == ".rb" raise LoaderError, "File is not a ruby file: #{path}" unless lenient return [nil, nil] end [path, :file] elsif ::File.directory?(path) [path, :directory] else raise LoaderError, "Unknown type: #{path}" unless lenient [nil, nil] end end private def default_source_name if @git_remote "git(remote=#{@git_remote} path=#{@git_path} commit=#{@git_commit})" elsif @gem_name "gem(name=#{@gem_name} version=#{@gem_version} path=#{@gem_path})" elsif @source_type == :proc "(code block #{@source_proc.object_id})" else @source_path end end def find_special_dir(dir_name) return nil if @source_type != :directory || dir_name.nil? dir = ::File.join(@source_path, dir_name) dir if ::File.directory?(dir) && ::File.readable?(dir) end end end toys-core-0.21.0/lib/toys/mixin.rb0000644000004100000410000001220715165170677017004 0ustar www-datawww-data# frozen_string_literal: true module Toys ## # A mixin definition. Mixin modules should include this module. # # A mixin is a collection of methods that are available to be called from a # tool implementation (i.e. its run method). The mixin is added to the tool # class, so it has access to the same methods that can be called by the tool, # such as {Toys::Context#get}. # # ### Usage # # To create a mixin, define a module, and include this module. Then define # the methods you want to be available. # # If you want to perform some initialization specific to the mixin, you can # provide an *initializer* block and/or an *inclusion* block. These can be # specified by calling the module methods defined in # {Toys::Mixin::ModuleMethods}. # # The initializer block is called when the tool context is instantiated # in preparation for execution. It has access to context methods such as # {Toys::Context#get}, and can perform setup for the tool execution itself, # such as initializing some persistent state and storing it in the tool using # {Toys::Context#set}. The initializer block is passed any extra arguments # that were provided to the `include` directive. Define the initializer by # calling {Toys::Mixin::ModuleMethods#on_initialize}. # # The inclusion block is called in the context of your tool class when your # mixin is included. It is also passed any extra arguments that were provided # to the `include` directive. It can be used to issue directives to define # tools or other objects in the DSL, or even enhance the DSL by defining DSL # methods specific to the mixin. Define the inclusion block by calling # {Toys::Mixin::ModuleMethods#on_include}. # # ### Example # # This is an example that implements a simple counter. Whenever the counter # is incremented, a log message is emitted. The tool can also retrieve the # final counter value. # # # Define a mixin by creating a module that includes Toys::Mixin # module MyCounterMixin # include Toys::Mixin # # # Initialize the counter. Notice that the initializer is evaluated # # in the context of the runtime context, so has access to the runtime # # context state. # on_initialize do |start = 0| # set(:counter_value, start) # end # # # Mixin methods are evaluated in the runtime context and so have # # access to the runtime context state, just as if you had defined # # them in your tool. # def counter_value # get(:counter_value) # end # # def increment # set(:counter_value, counter_value + 1) # logger.info("Incremented counter") # end # end # # Now we can use it from a tool: # # tool "count-up" do # # Pass 1 as an extra argument to the mixin initializer # include MyCounterMixin, 1 # # def run # # Mixin methods can be called. # 5.times { increment } # puts "Final value is #{counter_value}" # end # end # module Mixin ## # Create a mixin module with the given block. # # @param block [Proc] Defines the mixin module. # @return [Class] # def self.create(&block) mixin_mod = ::Module.new do include ::Toys::Mixin end mixin_mod.module_eval(&block) if block mixin_mod end ## # Methods that will be added to a mixin module object. # module ModuleMethods ## # Set the initializer for this mixin. This block is evaluated in the # runtime context before execution, and is passed any arguments provided # to the `include` directive. It can perform any runtime initialization # needed by the mixin. # # @param block [Proc] Sets the initializer proc. # @return [self] # def on_initialize(&block) self.initializer = block self end ## # The initializer proc for this mixin. This proc is evaluated in the # runtime context before execution, and is passed any arguments provided # to the `include` directive. It can perform any runtime initialization # needed by the mixin. # # @return [Proc] The iniitiliazer for this mixin. # attr_accessor :initializer ## # Set an inclusion proc for this mixin. This block is evaluated in the # tool class immediately after the mixin is included, and is passed any # arguments provided to the `include` directive. # # @param block [Proc] Sets the inclusion proc. # @return [self] # def on_include(&block) self.inclusion = block self end ## # The inclusion proc for this mixin. This block is evaluated in the tool # class immediately after the mixin is included, and is passed any # arguments provided to the `include` directive. # # @return [Proc] The inclusion procedure for this mixin. # attr_accessor :inclusion end ## # @private # def self.included(mod) return if mod.respond_to?(:on_initialize) mod.extend(ModuleMethods) end end end toys-core-0.21.0/lib/toys/positional_arg.rb0000644000004100000410000001241215165170677020670 0ustar www-datawww-data# frozen_string_literal: true module Toys ## # Representation of a formal positional argument # class PositionalArg ## # Create a PositionalArg definition. # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param type [Symbol] The type of arg. Valid values are `:required`, # `:optional`, and `:remaining`. # @param accept [Object] An acceptor that validates and/or converts the # value. See {Toys::Acceptor.create} for recognized formats. Optional. # If not specified, defaults to {Toys::Acceptor::DEFAULT}. # @param complete [Object] A specifier for shell tab completion. See # {Toys::Completion.create} for recognized formats. # @param display_name [String] A name to use for display (in help text and # error reports). Defaults to the key in upper case. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::ToolDefintion#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::ToolDefintion#long_desc} # for a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @return [Toys::PositionalArg] # def self.create(key, type, accept: nil, default: nil, complete: nil, desc: nil, long_desc: nil, display_name: nil) new(key, type, accept, default, complete, desc, long_desc, display_name) end ## # The key for this arg. # @return [Symbol] # attr_reader :key ## # Type of this argument. # @return [:required,:optional,:remaining] # attr_reader :type ## # The effective acceptor. # @return [Toys::Acceptor::Base] # attr_accessor :acceptor ## # The default value, which may be `nil`. # @return [Object] # attr_reader :default ## # The proc that determines shell completions for the value. # @return [Proc,Toys::Completion::Base] # attr_reader :completion ## # The short description string. # # When reading, this is always returned as a {Toys::WrappableString}. # # When setting, the description may be provided as any of the following: # * A {Toys::WrappableString}. # * A normal String, which will be transformed into a # {Toys::WrappableString} using spaces as word delimiters. # * An Array of String, which will be transformed into a # {Toys::WrappableString} where each array element represents an # individual word for wrapping. # # @return [Toys::WrappableString] # attr_reader :desc ## # The long description strings. # # When reading, this is returned as an Array of {Toys::WrappableString} # representing the lines in the description. # # When setting, the description must be provided as an Array where *each # element* may be any of the following: # * A {Toys::WrappableString} representing one line. # * A normal String representing a line. This will be transformed into a # {Toys::WrappableString} using spaces as word delimiters. # * An Array of String representing a line. This will be transformed into # a {Toys::WrappableString} where each array element represents an # individual word for wrapping. # # @return [Array] # attr_reader :long_desc ## # The displayable name. # @return [String] # attr_accessor :display_name ## # Set the short description string. # # See {#desc} for details. # # @param desc [Toys::WrappableString,String,Array] # def desc=(desc) @desc = WrappableString.make(desc) end ## # Set the long description strings. # # See {#long_desc} for details. # # @param long_desc [Array>] # def long_desc=(long_desc) @long_desc = WrappableString.make_array(long_desc) end ## # Append long description strings. # # You must pass an array of lines in the long description. See {#long_desc} # for details on how each line may be represented. # # @param long_desc [Array>] # @return [self] # def append_long_desc(long_desc) @long_desc.concat(WrappableString.make_array(long_desc)) self end ## # Create a PositionalArg definition. # This argument list is subject to change. Use {Toys::PositionalArg.create} # instead for a more stable interface. # # @private # def initialize(key, type, acceptor, default, completion, desc, long_desc, display_name) @key = key @type = type @acceptor = Acceptor.create(acceptor) @default = default @completion = Completion.create(completion, **{}) @desc = WrappableString.make(desc) @long_desc = WrappableString.make_array(long_desc) @display_name = display_name || key.to_s.tr("-", "_").gsub(/\W/, "").upcase end end end toys-core-0.21.0/lib/toys/standard_mixins/0000755000004100000410000000000015165170677020520 5ustar www-datawww-datatoys-core-0.21.0/lib/toys/standard_mixins/highline.rb0000644000004100000410000000721615165170677022642 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A mixin that provides access to the capabilities of the highline gem. # # This mixin requires the highline gem, version 2.0 or later. It will # attempt to install the gem if it is not available. # # You may make these methods available to your tool by including the # following directive in your tool configuration: # # include :highline # # A HighLine object will then be available by calling the {#highline} # method. For information on using this object, see the # [Highline documentation](https://www.rubydoc.info/gems/highline). Some of # the most common HighLine methods, such as `say`, are also mixed into the # tool and can be called directly. # # You can configure the HighLine object by passing options to the `include` # directive. For example: # # include :highline, my_stdin, my_stdout # # The arguments will be passed on to the # [HighLine constructor](https://www.rubydoc.info/gems/highline/HighLine:initialize). # module Highline include Mixin ## # Context key for the highline object. # @return [Object] # KEY = ::Object.new.freeze ## # A tool-wide [HighLine](https://www.rubydoc.info/gems/highline/HighLine) # instance # @return [::HighLine] # def highline self[KEY] end ## # Calls [HighLine#agree](https://www.rubydoc.info/gems/highline/HighLine:agree) # def agree(...) self[KEY].agree(...) end ## # Calls [HighLine#ask](https://www.rubydoc.info/gems/highline/HighLine:ask) # def ask(...) self[KEY].ask(...) end ## # Calls [HighLine#choose](https://www.rubydoc.info/gems/highline/HighLine:choose) # def choose(...) self[KEY].choose(...) end ## # Calls [HighLine#list](https://www.rubydoc.info/gems/highline/HighLine:list) # def list(...) self[KEY].list(...) end ## # Calls [HighLine#say](https://www.rubydoc.info/gems/highline/HighLine:say) # def say(...) self[KEY].say(...) end ## # Calls [HighLine#indent](https://www.rubydoc.info/gems/highline/HighLine:indent) # def indent(...) self[KEY].indent(...) end ## # Calls [HighLine#newline](https://www.rubydoc.info/gems/highline/HighLine:newline) # def newline self[KEY].newline end ## # Calls [HighLine#puts](https://www.rubydoc.info/gems/highline/HighLine:puts) # def puts(*args) self[KEY].puts(*args) end ## # Calls [HighLine#color](https://www.rubydoc.info/gems/highline/HighLine:color) # def color(*args) self[KEY].color(*args) end ## # Calls [HighLine#color_code](https://www.rubydoc.info/gems/highline/HighLine:color_code) # def color_code(*args) self[KEY].color_code(*args) end ## # Calls [HighLine#uncolor](https://www.rubydoc.info/gems/highline/HighLine:uncolor) # def uncolor(*args) self[KEY].uncolor(*args) end ## # Calls [HighLine#new_scope](https://www.rubydoc.info/gems/highline/HighLine:new_scope) # def new_scope self[KEY].new_scope end on_initialize do |*args| require "toys/utils/gems" ::Toys::Utils::Gems.activate("highline", "~> 2.0") require "highline" self[KEY] = ::HighLine.new(*args) self[KEY].use_color = $stdout.tty? end end end end toys-core-0.21.0/lib/toys/standard_mixins/terminal.rb0000644000004100000410000001174115165170677022664 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A mixin that provides a simple terminal. It includes a set of methods # that produce styled output, get user input, and otherwise interact with # the user's terminal. This mixin is not as richly featured as other mixins # such as Highline, but it has no gem dependencies so is ideal for basic # cases. # # You may make these methods available to your tool by including the # following directive in your tool configuration: # # include :terminal # # A Terminal object will then be available by calling the {#terminal} # method. For information on using this object, see the documentation for # {Toys::Utils::Terminal}. Some of the most useful methods are also mixed # into the tool and can be called directly. # # You can configure the Terminal object by passing options to the `include` # directive. For example: # # include :terminal, styled: true # # The arguments will be passed on to {Toys::Utils::Terminal#initialize}. # module Terminal include Mixin ## # Context key for the terminal object. # @return [Object] # KEY = ::Object.new.freeze ## # A tool-wide terminal instance # @return [Toys::Utils::Terminal] # def terminal self[KEY] end ## # Write a line, appending a newline if one is not already present. # # @see Toys::Utils::Terminal#puts # # @param str [String] The line to write # @param styles [Symbol,String,Array...] Styles to apply to the # entire line. # @return [self] # def puts(str = "", *styles) self[KEY].puts(str, *styles) self end alias say puts ## # Write a partial line without appending a newline. # # @see Toys::Utils::Terminal#write # # @param str [String] The line to write # @param styles [Symbol,String,Array...] Styles to apply to the # partial line. # @return [self] # def write(str = "", *styles) self[KEY].write(str, *styles) self end ## # Ask a question and get a response. # # @see Toys::Utils::Terminal#ask # # @param prompt [String] Required prompt string. # @param styles [Symbol,String,Array...] Styles to apply to the # prompt. # @param default [String,nil] Default value, or `nil` for no default. # Uses `nil` if not specified. # @param trailing_text [:default,String,nil] Trailing text appended to # the prompt, `nil` for none, or `:default` to show the default. # @return [String] # def ask(prompt, *styles, default: nil, trailing_text: :default) self[KEY].ask(prompt, *styles, default: default, trailing_text: trailing_text) end ## # Confirm with the user. # # @see Toys::Utils::Terminal#confirm # # @param prompt [String] Prompt string. Defaults to `"Proceed?"`. # @param styles [Symbol,String,Array...] Styles to apply to the # prompt. # @param default [Boolean,nil] Default value, or `nil` for no default. # Uses `nil` if not specified. # @return [Boolean] # def confirm(prompt = "Proceed?", *styles, default: nil) self[KEY].confirm(prompt, *styles, default: default) end ## # Display a spinner during a task. You should provide a block that # performs the long-running task. While the block is executing, a # spinner will be displayed. # # @see Toys::Utils::Terminal#spinner # # @param leading_text [String] Optional leading string to display to the # left of the spinner. Default is the empty string. # @param frame_length [Float] Length of a single frame, in seconds. # Defaults to {Toys::Utils::Terminal::DEFAULT_SPINNER_FRAME_LENGTH}. # @param frames [Array] An array of frames. Defaults to # {Toys::Utils::Terminal::DEFAULT_SPINNER_FRAMES}. # @param style [Symbol,Array] A terminal style or array of styles # to apply to all frames in the spinner. Defaults to empty, # @param final_text [String] Optional final string to display when the # spinner is complete. Default is the empty string. A common practice # is to set this to newline. # @return [Object] The return value of the block. # def spinner(leading_text: "", final_text: "", frame_length: nil, frames: nil, style: nil, &block) self[KEY].spinner(leading_text: leading_text, final_text: final_text, frame_length: frame_length, frames: frames, style: style, &block) end on_initialize do |**opts| require "toys/utils/terminal" self[KEY] = Utils::Terminal.new(**opts) end end end end toys-core-0.21.0/lib/toys/standard_mixins/bundler.rb0000644000004100000410000002322315165170677022502 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # Ensures that a bundle is installed and set up when this tool is run. # # This is the normal recommended way to use [bundler](https://bundler.io) # with Toys. Including this mixin in a tool will cause Toys to ensure that # the bundle is installed and available during tool execution. For example: # # tool "run-rails" do # include :bundler # def run # # Note: no "bundle exec" required because Toys has already # # installed and loaded the bundle. # exec "rails s" # end # end # # ### Customization # # The following parameters can be passed when including this mixin: # # * `:static` (Boolean) Has the same effect as passing `:static` to the # `:setup` parameter. This is present largely for historical # compatibility, but it is supported and _not_ deprecated. # # * `:setup` (:auto,:manual,:static) A symbol indicating when the bundle # should be installed. Possible values are: # # * `:auto` - (Default) Installs the bundle just before the tool runs. # * `:static` - Installs the bundle immediately when defining the # tool. # * `:manual` - Does not install the bundle, but defines the methods # `bundler_setup` and `bundler_setup?` in the tool. The tool can # call `bundler_setup` to install the bundle, optionally passing # any of the remaining keyword arguments below to override the # corresponding mixin parameters. The `bundler_setup?` method can # be queried to determine whether the bundle has been set up yet. # # * `:groups` (Array\) The groups to include in setup. # # * `:gemfile_path` (String) The path to the Gemfile to use. If `nil` or # not given, the `:search_dirs` will be searched for a Gemfile. # # * `:search_dirs` (String,Symbol,Array\) Directories to # search for a Gemfile. # # You can pass full directory paths, and/or any of the following: # * `:context` - the current context directory. # * `:current` - the current working directory. # * `:toys` - the Toys directory containing the tool definition, and # any of its parents within the Toys directory hierarchy. # # The default is to search `[:toys, :context, :current]` in that order. # See {DEFAULT_SEARCH_DIRS}. # # For most directories, the bundler mixin will look for the files # ".gems.rb", "gems.rb", and "Gemfile", in that order. In `:toys` # directories, it will look only for ".gems.rb" and "Gemfile", in that # order. These can be overridden by setting the `:gemfile_names` and/or # `:toys_gemfile_names` arguments. # # * `:gemfile_names` (Array\) File names that are recognized as # Gemfiles when searching in directories other than Toys directories. # Defaults to {Toys::Utils::Gems::DEFAULT_GEMFILE_NAMES}. # # * `:toys_gemfile_names` (Array\) File names that are # recognized as Gemfiles when searching in Toys directories. # Defaults to {DEFAULT_TOYS_GEMFILE_NAMES}. # # * `:on_missing` (Symbol) What to do if a needed gem is not installed. # # Supported values: # * `:confirm` - prompt the user on whether to install (default). # * `:error` - raise an exception. # * `:install` - just install the gem. # # * `:on_conflict` (Symbol) What to do if bundler has already been run # with a different Gemfile. # # Supported values: # * `:error` - raise an exception (default). # * `:ignore` - just silently proceed without bundling again. # * `:warn` - print a warning and proceed without bundling again. # # * `:retries` (Integer) Number of times to retry bundler operations # (optional) # # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional) # # * `:input` (IO) Input IO (optional, defaults to STDIN) # # * `:output` (IO) Output IO (optional, defaults to STDOUT) # module Bundler include Mixin ## # Default search directories for Gemfiles. # @return [Array] # DEFAULT_SEARCH_DIRS = [:toys, :context, :current].freeze ## # The gemfile names that are searched by default in Toys directories. # @return [Array] # DEFAULT_TOYS_GEMFILE_NAMES = [".gems.rb", "Gemfile"].freeze ## # @private # Context key for the mixin parameters when using manual setup. The value # will be a hash of parameters if the bundle has not been set up yet, or # nil if the bundle has already been set up. # SETUP_PARAMS_KEY = ::Object.new.freeze ## # @private # def self.setup_bundle(context_directory, source_info, gemfile_path: nil, search_dirs: nil, gemfile_names: nil, toys_gemfile_names: nil, groups: nil, on_missing: nil, on_conflict: nil, retries: nil, terminal: nil, input: nil, output: nil) require "toys/utils/gems" gemfile_path ||= begin gemfile_finder = GemfileFinder.new(context_directory, source_info, gemfile_names, toys_gemfile_names) gemfile_finder.search(search_dirs || DEFAULT_SEARCH_DIRS) end gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict, terminal: terminal, input: input, output: output) gems.bundle(groups: groups, gemfile_path: gemfile_path, retries: retries) end on_initialize do |static: false, setup: nil, **kwargs| setup ||= (static ? :static : :auto) case setup when :auto context_directory = self[::Toys::Context::Key::CONTEXT_DIRECTORY] source_info = self[::Toys::Context::Key::TOOL_SOURCE] ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs) when :manual self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY] = kwargs when :static # Already set up at include time end end on_include do |static: false, setup: nil, **kwargs| setup ||= (static ? :static : :auto) case setup when :static ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs) when :manual # @private Defined dynamically for the tool but not visible to YARD def bundler_setup(**kwargs) original_kwargs = self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY] raise ::Toys::Utils::Gems::AlreadyBundledError unless original_kwargs context_directory = self[::Toys::Context::Key::CONTEXT_DIRECTORY] source_info = self[::Toys::Context::Key::TOOL_SOURCE] final_kwargs = original_kwargs.merge(kwargs) ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **final_kwargs) self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY] = nil end # @private Defined dynamically for the tool but not visible to YARD def bundler_setup? self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY].nil? end when :auto # Do nothing at this point else raise ::ArgumentError, "Unrecognized setup type: #{setup.inspect}" end end ## # @private # class GemfileFinder ## # @private # def initialize(context_directory, source_info, gemfile_names, toys_gemfile_names) @context_directory = context_directory @source_info = source_info @gemfile_names = gemfile_names || ::Toys::Utils::Gems::DEFAULT_GEMFILE_NAMES @toys_gemfile_names = toys_gemfile_names || DEFAULT_TOYS_GEMFILE_NAMES end ## # @private # def search(search_dir) case search_dir when ::Array search_array(search_dir) when ::String ::Toys::Utils::Gems.find_gemfile(search_dir, gemfile_names: @gemfile_names) when :context search(@context_directory) when :current search(::Dir.getwd) when :toys search_toys else raise ::ArgumentError, "Unrecognized search_dir: #{search_dir.inspect}" end end private def search_array(search_dirs) search_dirs.each do |search_dir| result = search(search_dir) return result if result end nil end def search_toys source_info = @source_info while source_info if source_info.source_type == :directory && source_info.source_path != source_info.context_directory result = ::Toys::Utils::Gems.find_gemfile(source_info.source_path, gemfile_names: @toys_gemfile_names) return result if result end source_info = source_info.parent end nil end end end end end toys-core-0.21.0/lib/toys/standard_mixins/pager.rb0000644000004100000410000000277615165170677022157 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A mixin that provides a pager. # # This mixin provides an instance of {Toys::Utils::Pager}, which invokes # an external pager for output. # # You can also pass additional keyword arguments to the `include` directive # to configure the pager object. These will be passed on to # {Toys::Utils::Pager#initialize}. # # @example # # include :pager # # def run # pager do |io| # io.puts "A long string\n" # end # end # module Pager include Mixin ## # Context key for the Pager object. # @return [Object] # KEY = ::Object.new.freeze ## # Access the Pager. # # If *no* block is given, returns the pager object. # # If a block is given, the pager is executed with the given block, and # the exit code of the pager process is returned. # # @return [Toys::Utils::Pager] if no block is given. # @return [Integer] if a block is given. # def pager(&block) pager = self[KEY] return pager unless block self[KEY].start(&block) end on_initialize do |**opts| require "toys/utils/pager" if !opts.key?(:exec_service) && defined?(::Toys::StandardMixins::Exec) opts[:exec_service] = self[::Toys::StandardMixins::Exec::KEY] end self[KEY] = Utils::Pager.new(**opts) end end end end toys-core-0.21.0/lib/toys/standard_mixins/fileutils.rb0000644000004100000410000000106715165170677023051 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A module that provides all methods in the "fileutils" standard library. # # You may make the methods in the `FileUtils` standard library module # available to your tool by including the following directive in your tool # configuration: # # include :fileutils # module Fileutils include Mixin ## # @private # def self.included(mod) require "fileutils" mod.include(::FileUtils) end end end end toys-core-0.21.0/lib/toys/standard_mixins/git_cache.rb0000644000004100000410000000200015165170677022743 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A mixin that provides a git cache. # # This mixin provides an instance of {Toys::Utils::GitCache}, providing # cached access to files from a remote git repo. # # @example # # include :git_cache # # def run # # Pull and cache the HEAD commit from the Toys repo. # dir = git_cache.get("https://github.com/dazuma/toys.git") # # Display the contents of the readme file. # puts File.read(File.join(dir, "README.md")) # end # module GitCache include Mixin ## # Context key for the GitCache object. # @return [Object] # KEY = ::Object.new.freeze ## # Access the builtin GitCache. # # @return [Toys::Utils::GitCache] # def git_cache self[KEY] end on_initialize do require "toys/utils/git_cache" self[KEY] = Utils::GitCache.new end end end end toys-core-0.21.0/lib/toys/standard_mixins/gems.rb0000644000004100000410000000560615165170677022007 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # Provides methods for installing and activating third-party gems. When # this mixin is included, it provides a `gem` method that has the same # effect as {Toys::Utils::Gems#activate}, so you can ensure a gem is # present when running a tool. A `gem` directive is likewise added to the # tool DSL itself, so you can also ensure a gem is present when defining a # tool. # # ### Usage # # Make these methods available to your tool by including this mixin in your # tool: # # include :gems # # You can then call the mixin method {#gem} to ensure that a gem is # installed and in the load path. For example: # # tool "my_tool" do # include :gems # def run # gem "nokogiri", "~> 1.15" # # Do stuff with Nokogiri # end # end # # If you pass additional options to the include directive, those are used # to initialize settings for the gem install process. For example: # # include :gems, on_missing: :error # # You can also pass options to the {#gem} mixin method itself: # # tool "my_tool" do # include :gems # def run # # If the gem is not installed, error out instead of asking to # # install it. # gem "nokogiri", "~> 1.15", on_missing: :error # # Do stuff with Nokogiri # end # end # # See {Toys::Utils::Gems#initialize} for a list of supported options. # module Gems include Mixin ## # A tool-wide instance of {Toys::Utils::Gems}. # @return [Toys::Utils::Gems] # def gems self.class.gems end ## # Activate the given gem. If it is not present, attempt to install it (or # inform the user to update the bundle). # # @param name [String] Name of the gem # @param requirements [String...] Version requirements # @return [void] # def gem(name, *requirements, **options) self.class.gem(name, *requirements, **options) end on_include do |**opts| @__gems_opts = opts ## # @private # def self.gems # rubocop:disable Naming/MemoizedInstanceVariableName @__gems ||= begin require "toys/utils/gems" Utils::Gems.new(**@__gems_opts) end # rubocop:enable Naming/MemoizedInstanceVariableName end ## # @private # def self.gem(name, *requirements, **options) gems_util = if options.empty? gems else require "toys/utils/gems" Utils::Gems.new(**options) end gems_util.activate(name, *requirements) end end end end end toys-core-0.21.0/lib/toys/standard_mixins/xdg.rb0000644000004100000410000000237015165170677021631 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # A mixin that provides tools for working with the XDG Base Directory # Specification. # # This mixin provides an instance of {Toys::Utils::XDG}, which includes # utility methods that locate base directories and search paths for # application state, configuration, caches, and other data, according to # the [XDG Base Directory Spec version # 0.8](https://specifications.freedesktop.org/basedir/0.8/). # # @example # # include :xdg # # def run # # Get config file paths, in order from most to least inportant # config_files = xdg.lookup_config("my-config.toml") # config_files.each { |path| read_my_config(path) } # end # module XDG include Mixin ## # Context key for the XDG object. # @return [Object] # KEY = ::Object.new.freeze ## # Access XDG utility methods. # # @return [Toys::Utils::XDG] # def xdg self[KEY] end on_initialize do require "toys/utils/xdg" self[KEY] = Utils::XDG.new end end ## # An alternate name for the {XDG} module # Xdg = XDG end end toys-core-0.21.0/lib/toys/standard_mixins/exec.rb0000644000004100000410000011075715165170677022004 0ustar www-datawww-data# frozen_string_literal: true module Toys module StandardMixins ## # The `:exec` mixin provides set of helper methods for executing processes # and subcommands. It provides shortcuts for common cases such as invoking # a Ruby script in a subprocess or capturing output in a string. It also # provides an interface for controlling a spawned process's streams. # # You can make these methods available to your tool by including the # following directive in your tool configuration: # # include :exec # # This is a frontend for {Toys::Utils::Exec}. More information is # available in that class's documentation. # # ### Mixin overview # # The mixin provides a number of methods for spawning processes. The most # basic are {#exec} and {#exec_proc}. The {#exec} method spawns an # operating system process specified by an executable and a set of # arguments. The {#exec_proc} method takes a `Proc` and forks a Ruby # process. Both of these can be heavily configured with stream handling, # result handling, and numerous other options described below. The mixin # also provides convenience methods for common cases such as spawning a # Ruby process, spawning a shell script, or capturing output. # # The mixin also stores default configuration that it applies to processes # it spawns. You can change these defaults by calling {#configure_exec}. # # Underlying the mixin is a service object of type {Toys::Utils::Exec}. # Normally you would use the mixin methods to access this functionality, # but you can also retrieve the service object itself by calling # {Toys::Context#get} with the key {Toys::StandardMixins::Exec::KEY}. # # ### Stream handling # # By default, subprocess streams are connected to the corresponding streams # in the parent process. You can change this behavior, redirecting streams # or providing ways to control them, using the `:in`, `:out`, and `:err` # options. # # Three general strategies are available for custom stream handling. First, # you can redirect to other streams such as files, IO objects, or Ruby # strings. Some of these options map directly to options provided by the # `Process#spawn` method. Second, you can use a controller to manipulate # the streams programmatically. Third, you can capture output stream data # and make it available in the result. # # Following is a full list of the stream handling options, along with how # to specify them using the `:in`, `:out`, and `:err` options. # # * **Inherit parent stream:** You can inherit the corresponding stream # in the parent process by passing `:inherit` as the option value. This # is the default if the subprocess is run in the foreground. # # * **Redirect to null:** You can redirect to a null stream by passing # `:null` as the option value. This connects to a stream that is not # closed but contains no data, i.e. `/dev/null` on unix systems. This # is the default if the subprocess is run in the background. # # * **Close the stream:** You can close the stream by passing `:close` as # the option value. This is the same as passing `:close` to # `Process#spawn`. # # * **Redirect to a file:** You can redirect to a file. This reads from # an existing file when connected to `:in`, and creates or appends to a # file when connected to `:out` or `:err`. To specify a file, use the # setting `[:file, "/path/to/file"]`. You can also, when writing a # file, append an optional mode and permission code to the array. For # example, `[:file, "/path/to/file", "a", 0644]`. # # * **Redirect to an IO object:** You can redirect to an IO object in the # parent process, by passing the IO object as the option value. You can # use any IO object. For example, you could connect the child's output # to the parent's error using `out: $stderr`, or you could connect to # an existing File stream. Unlike `Process#spawn`, this works for IO # objects that do not have a corresponding file descriptor (such as # StringIO objects). In such a case, a thread will be spawned to pipe # the IO data through to the child process. Note that the IO object # will _not_ be closed on completion. # # * **Redirect to a pipe:** You can redirect to a pipe created using # `IO.pipe` (i.e. a two-element array of read and write IO objects) by # passing the array as the option value. This will connect the # appropriate IO (either read or write), and close it in the parent. # Thus, you can connect only one process to each end. If you want more # direct control over IO closing behavior, pass the IO object (i.e. the # element of the pipe array) directly. # # * **Combine with another child stream:** You can redirect one child # output stream to another, to combine them. To merge the child's error # stream into its output stream, use `err: [:child, :out]`. # # * **Read from a string:** You can pass a string to the input stream by # setting `[:string, "the string"]`. This works only for `:in`. # # * **Capture output stream:** You can capture a stream and make it # available on the {Toys::Utils::Exec::Result} object, using the # setting `:capture`. This works only for the `:out` and `:err` # streams. # # * **Use the controller:** You can hook a stream to the controller using # the setting `:controller`. You can then manipulate the stream via the # controller. If you pass a block to {Toys::StandardMixins::Exec#exec}, # it yields the {Toys::Utils::Exec::Controller}, giving you access to # streams. See the section below on controlling processes. # # * **Make copies of an output stream:** You can "tee," or duplicate the # `:out` or `:err` stream and redirect those copies to various # destinations. To specify a tee, use the setting `[:tee, ...]` where # the additional array elements include two or more of the following. # See the corresponding documentation above for more detail. # * `:inherit` to direct to the parent process's stream. # * `:capture` to capture the stream and store it in the result. # * `:controller` to direct the stream to the controller. # * `[:file, "/path/to/file"]` to write to a file. # * An `IO` or `StringIO` object. # * An array of two `IO` objects representing a pipe # # Additionally, the last element of the array can be a hash of options. # Supported options include: # * `:buffer_size` The size of the memory buffer for each element of # the tee. Larger buffers may allow higher throughput. The default # is 65536. # # ### Controlling processes # # A process can be started in the *foreground* or the *background*. If you # start a foreground process, it will inherit your standard input and # output streams by default, and it will keep control until it completes. # If you start a background process, its streams will be redirected to null # by default, and control will be returned to you immediately. # # While a process is running, you can control it using a # {Toys::Utils::Exec::Controller} object. Use a controller to interact with # the process's input and output streams, send it signals, or wait for it # to complete. # # When running a process in the foreground, the controller will be yielded # to an optional block. For example, the following code starts a process in # the foreground and passes its output stream to a controller. # # exec(["git", "init"], out: :controller) do |controller| # loop do # line = controller.out.gets # break if line.nil? # puts "Got line: #{line}" # end # end # # At the end of the block, if the controller is handling the process's # input stream, that stream will automatically be closed. The following # example programmatically sends data to the `wc` unix program, and # captures its output. Because the controller is handling the input stream, # it automatically closes the stream at the end of the block, which causes # `wc` to end. # # result = exec(["wc"], in: :controller, out: :capture) do |controller| # controller.in.puts "Hello, world!" # end # puts "Results: #{result.captured_out}" # # Otherwise, depending on the process's behavior, it may continue to run # after the end of the block. Control will not be returned to the caller # until the process actually terminates. Conversely, it is also possible # the process could terminate by itself while the block is still executing. # You can call controller methods to obtain the process's actual current # state. # # When running a process in the background, the controller is returned # immediately from the method that starts the process. In the following # example, git init is kicked off in the background and the output is # thrown away to /dev/null. # # controller = exec(["git", "init"], background: true) # # In this mode, use the returned controller to query the process's state # and interact with it. Streams directed to the controller are not # automatically closed, so you will need to do so yourself. Following is an # example of running `wc` in the background: # # controller = exec(["wc"], background: true, # in: :controller, out: :controller) # controller.in.puts "Hello, world!" # controller.in.close # Do this explicitly to cause wc to finish # puts "Results: #{controller.out.read}" # Read the entire stream # # ### Result handling # # A subprocess result is represented by a {Toys::Utils::Exec::Result} # object, which includes the exit code, the content of any captured output # streams, and any exeption raised when attempting to run the process. # When you run a process in the foreground, the method will return a result # object. When you run a process in the background, you can obtain the # result from the controller once the process completes. # # The following example demonstrates running a process in the foreground # and getting the exit code: # # result = exec(["git", "init"]) # puts "exit code: #{result.exit_code}" # # The following example demonstrates starting a process in the background, # waiting for it to complete, and getting its exit code: # # controller = exec(["git", "init"], background: true) # result = controller.result(timeout: 1.0) # if result # puts "exit code: #{result.exit_code}" # else # puts "timed out" # end # # You can also provide a callback that is executed once a process # completes. This callback can be specified as a method name or a `Proc` # object, and will be passed the result object. For example: # # def run # exec(["git", "init"], result_callback: :handle_result) # end # def handle_result(result) # puts "exit code: #{result.exit_code}" # end # # In foreground mode, the callback is executed in the calling thread, after # the process terminates (and after any controller block has completed) but # before control is returned to the caller. In background mode, the # callback is executed asynchronously in a separate thread after the # process terminates. # # Finally, you can force your tool to exit if a subprocess fails, similar # to setting the `set -e` option in bash, by setting the # `:exit_on_nonzero_status` option. This is often set as a default # configuration for all subprocesses run in a tool, by passing it as an # argument to the `include` directive: # # include :exec, exit_on_nonzero_status: true # # ### Configuration Options # # A variety of options can be used to control subprocesses. These can be # provided to any method that starts a subprocess. You can also set # defaults by passing them as keyword arguments when you `include` the # mixin. # # Options that affect the behavior of subprocesses: # # * `:env` (Hash) Environment variables to pass to the subprocess. # Keys represent variable names and should be strings. Values should be # either strings or `nil`, which unsets the variable. # # * `:background` (Boolean) Runs the process in the background if `true`. # # * `:unbundle` (Boolean) Disables any existing bundle when running the # subprocess. Has no effect if Bundler isn't active at the call point. # Cannot be used when executing in a fork, e.g. via {#exec_proc}. # # Options related to handling results # # * `:result_callback` (Proc,Symbol) A procedure that is called, and # passed the result object, when the subprocess exits. You can provide # a `Proc` object, or the name of a method as a `Symbol`. # # * `:exit_on_nonzero_status` (Boolean) If set to true, a nonzero exit # code will cause the tool to exit immediately with that same code. # # * `:e` (Boolean) A short name for `:exit_on_nonzero_status`. # # Options for connecting input and output streams. See the section above on # stream handling for info on the values that can be passed. # # * `:in` Connects the input stream of the subprocess. See the section on # stream handling. # # * `:out` Connects the standard output stream of the subprocess. See the # section on stream handling. # # * `:err` Connects the standard error stream of the subprocess. See the # section on stream handling. # # Options related to logging and reporting: # # * `:logger` (Logger) Logger to use for logging the actual command. If # not present, the command is not logged. # # * `:log_level` (Integer,false) Level for logging the actual command. # Defaults to Logger::INFO if not present. You can also pass `false` to # disable logging of the command. # # * `:log_cmd` (String) The string logged for the actual command. # Defaults to the `inspect` representation of the command. # # * `:name` (Object) An optional object that can be used to identify this # subprocess. It is available in the controller and result objects. # # In addition, the following options recognized by # [`Process#spawn`](https://ruby-doc.org/core/Process.html#method-c-spawn) # are supported. # # * `:chdir` (String) Set the working directory for the command. # # * `:close_others` (Boolean) Whether to close non-redirected # non-standard file descriptors. # # * `:new_pgroup` (Boolean) Create new process group (Windows only). # # * `:pgroup` (Integer,true,nil) The process group setting. # # * `:umask` (Integer) Umask setting for the new process. # # * `:unsetenv_others` (Boolean) Clear environment variables except those # explicitly set. # # Any other option key will result in an `ArgumentError`. # module Exec include Mixin ## # Context key for the executor object. # @return [Object] # KEY = ::Object.new.freeze ## # Set default configuration options. # # See the {Toys::StandardMixins::Exec} module documentation for a # description of the options. # # @param opts [keywords] The default options. # @return [self] # def configure_exec(**opts) opts = Exec._setup_exec_opts(opts, self) self[KEY].configure_defaults(**opts) self end ## # Execute a command. The command can be given as a single string to pass # to a shell, or an array of strings indicating a posix command. # # If the process is not set to run in the background, and a block is # provided, a {Toys::Utils::Exec::Controller} will be yielded to it. # # ### Examples # # Run a command without a shell, and print the exit code (0 for success): # # result = exec(["git", "init"]) # puts "exit code: #{result.exit_code}" # # Run a shell command: # # result = exec("cd mydir && git init") # puts "exit code: #{result.exit_code}" # # @param cmd [String,Array] The command to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Toys::Utils::Exec::Controller] The subprocess controller, if # the process is running in the background. # @return [Toys::Utils::Exec::Result] The result, if the process ran in # the foreground. # def exec(cmd, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].exec(cmd, **opts, &block) end ## # Spawn a ruby process and pass the given arguments to it. # # If the process is not set to run in the background, and a block is # provided, a {Toys::Utils::Exec::Controller} will be yielded to it. # # ### Example # # Execute a small script with warnings # # exec_ruby(["-w", "-e", "(1..10).each { |i| puts i }"]) # # @param args [String,Array] The arguments to ruby. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Toys::Utils::Exec::Controller] The subprocess controller, if # the process is running in the background. # @return [Toys::Utils::Exec::Result] The result, if the process ran in # the foreground. # def exec_ruby(args, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].exec_ruby(args, **opts, &block) end alias ruby exec_ruby ## # Execute a proc in a forked subprocess. # # If the process is not set to run in the background, and a block is # provided, a {Toys::Utils::Exec::Controller} will be yielded to it. # # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows) # do not support this method because they do not support fork. # # ### Example # # Run a proc in a forked process. # # code = proc do # puts "Spawned process ID is #{Process.pid}" # end # puts "Main process ID is #{Process.pid}" # exec_proc(code) # # @param func [Proc] The proc to call. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Toys::Utils::Exec::Controller] The subprocess controller, if # the process is running in the background. # @return [Toys::Utils::Exec::Result] The result, if the process ran in # the foreground. # def exec_proc(func, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].exec_proc(func, **opts, &block) end ## # Execute a tool in the current CLI in a forked process. # # The command can be given as a single string or an array of strings, # representing the tool to run and the arguments to pass. # # If the process is not set to run in the background, and a block is # provided, a {Toys::Utils::Exec::Controller} will be yielded to it. # # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows) # do not support this method because they do not support fork. # # ### Example # # Run the "system update" tool and pass it an argument. # # exec_tool(["system", "update", "--verbose"]) # # @param cmd [String,Array] The tool to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Toys::Utils::Exec::Controller] The subprocess controller, if # the process is running in the background. # @return [Toys::Utils::Exec::Result] The result, if the process ran in # the foreground. # def exec_tool(cmd, **opts, &block) func = Exec._make_tool_caller(cmd, self[Context::Key::CLI]) opts = Exec._setup_exec_opts(opts, self) opts = {log_cmd: "exec tool: #{cmd.inspect}"}.merge(opts) self[KEY].exec_proc(func, **opts, &block) end ## # Execute a tool in a separately spawned process. # # The command can be given as a single string or an array of strings, # representing the tool to run and the arguments to pass. # # If the process is not set to run in the background, and a block is # provided, a {Toys::Utils::Exec::Controller} will be yielded to it. # # An entirely separate spawned process is run for this tool, using the # setting of {Toys.executable_path}. Thus, this method can be run only if # that setting is present. The normal Toys gem does set it, but if you # are writing your own executable using Toys-Core, you will need to set # it explicitly for this method to work. Furthermore, Bundler, if # present, is reset to its "unbundled" environment. Thus, the tool found, # the behavior of the CLI, and the gem environment, might not be the same # as those of the calling tool. # # This method is often used if you are already in a bundle and need to # run a tool that uses a different bundle. It may also be necessary on # environments without "fork" (such as JRuby or Ruby on Windows). # # ### Example # # Run the "system update" tool and pass it an argument. # # exec_separate_tool(["system", "update", "--verbose"]) # # @param cmd [String,Array] The tool to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Toys::Utils::Exec::Controller] The subprocess controller, if # the process is running in the background. # @return [Toys::Utils::Exec::Result] The result, if the process ran in # the foreground. # def exec_separate_tool(cmd, **opts, &block) Exec._setup_clean_process(cmd) do |clean_cmd| opts = Exec._setup_exec_opts(opts, self) self[KEY].exec(clean_cmd, **opts, &block) end end ## # Execute a command. The command can be given as a single string to pass # to a shell, or an array of strings indicating a posix command. # # Captures standard out and returns it as a string. # Cannot be run in the background. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # ### Example # # Capture the output of an echo command # # str = capture(["echo", "hello"]) # assert_equal("hello\n", str) # # @param cmd [String,Array] The command to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [String] What was written to standard out. # def capture(cmd, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].capture(cmd, **opts, &block) end ## # Spawn a ruby process and pass the given arguments to it. # # Captures standard out and returns it as a string. # Cannot be run in the background. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # ### Example # # Capture the output of a ruby script. # # str = capture_ruby("-e", "(1..3).each { |i| puts i }") # assert_equal "1\n2\n3\n", str # # @param args [String,Array] The arguments to ruby. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [String] What was written to standard out. # def capture_ruby(args, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].capture_ruby(args, **opts, &block) end ## # Execute a proc in a forked subprocess. # # Captures standard out and returns it as a string. # Cannot be run in the background. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows) # do not support this method because they do not support fork. # # ### Example # # Run a proc in a forked process and capture its output: # # code = proc do # puts Process.pid # end # forked_pid = capture_proc(code).chomp # puts "I forked PID #{forked_pid}" # # @param func [Proc] The proc to call. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [String] What was written to standard out. # def capture_proc(func, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].capture_proc(func, **opts, &block) end ## # Execute a tool in the current CLI in a forked process. # # Captures standard out and returns it as a string. # Cannot be run in the background. # # The command can be given as a single string or an array of strings, # representing the tool to run and the arguments to pass. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows) # do not support this method because they do not support fork. # # ### Example # # Run the "system version" tool and capture its output. # # str = capture_tool(["system", "version"]).chomp # puts "Version was #{str}" # # @param cmd [String,Array] The tool to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [String] What was written to standard out. # def capture_tool(cmd, **opts, &block) func = Exec._make_tool_caller(cmd, self[Context::Key::CLI]) opts = Exec._setup_exec_opts(opts, self) self[KEY].capture_proc(func, **opts, &block) end ## # Execute a tool in a separately spawned process. # # Captures standard out and returns it as a string. # Cannot be run in the background. # # The command can be given as a single string or an array of strings, # representing the tool to run and the arguments to pass. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # An entirely separate spawned process is run for this tool, using the # setting of {Toys.executable_path}. Thus, this method can be run only if # that setting is present. The normal Toys gem does set it, but if you # are writing your own executable using Toys-Core, you will need to set # it explicitly for this method to work. Furthermore, Bundler, if # present, is reset to its "unbundled" environment. Thus, the tool found, # the behavior of the CLI, and the gem environment, might not be the same # as those of the calling tool. # # This method is often used if you are already in a bundle and need to # run a tool that uses a different bundle. It may also be necessary on # environments without "fork" (such as JRuby or Ruby on Windows). # # ### Example # # Run the "system version" tool and capture its output. # # str = capture_separate_tool(["system", "version"]).chomp # puts "Version was #{str}" # # @param cmd [String,Array] The tool to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [String] What was written to standard out. # def capture_separate_tool(cmd, **opts, &block) Exec._setup_clean_process(cmd) do |clean_cmd| opts = Exec._setup_exec_opts(opts, self) self[KEY].capture(clean_cmd, **opts, &block) end end ## # Execute the given string in a shell. Returns the exit code. # Cannot be run in the background. # # If a block is provided, a {Toys::Utils::Exec::Controller} will be # yielded to it. # # ### Example # # Run a shell script # # exit_code = sh("cd mydir && git init") # puts exit_code == 0 ? "Success!" : "Failed!" # # @param cmd [String] The shell command to execute. # @param opts [keywords] The command options. See the section on # Configuration Options in the {Toys::StandardMixins::Exec} module # documentation. # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for # the subprocess. See the section on Controlling Processes in the # {Toys::StandardMixins::Exec} module documentation. # # @return [Integer] The exit code # def sh(cmd, **opts, &block) opts = Exec._setup_exec_opts(opts, self) self[KEY].sh(cmd, **opts, &block) end ## # Exit if the given status code is nonzero. Otherwise, returns 0. # # @param status [Integer,Process::Status,Toys::Utils::Exec::Result] # @return [Integer] # def exit_on_nonzero_status(status) status = status.exit_code if status.respond_to?(:exit_code) status = status.exitstatus if status.respond_to?(:exitstatus) Context.exit(status) unless status.zero? 0 end ## # Returns an array of standard verbosity flags needed to replicate the # current verbosity level. This is useful when you want to spawn tools # with the same verbosity level as the current tool. # # @param short [Boolean] Whether to emit short rather than long flags. # Default is false. # @return [Array] # def verbosity_flags(short: false) verbosity = self[Context::Key::VERBOSITY] if verbosity.positive? if short flag = "v" * verbosity ["-#{flag}"] else ::Array.new(verbosity, "--verbose") end elsif verbosity.negative? if short flag = "q" * -verbosity ["-#{flag}"] else ::Array.new(-verbosity, "--quiet") end else [] end end ## # @private # def self._make_tool_caller(cmd, cli) cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String) proc { ::Kernel.exit(cli.run(*cmd)) } end ## # @private # def self._setup_exec_opts(opts, context) count = 0 result_callback = nil if opts.key?(:result_callback) result_callback = _interpret_result_callback(opts[:result_callback], context) count += 1 end [:exit_on_nonzero_status, :e].each do |sym| if opts.key?(sym) result_callback = _interpret_e(opts[sym], context) count += 1 opts = opts.reject { |k, _v| k == sym } end end if count > 1 raise ::ArgumentError, "You can provide at most one of: result_callback, exit_on_nonzero_status, e" end opts = opts.merge(result_callback: result_callback) if count == 1 opts end ## # @private # def self._interpret_e(value, context) return nil unless value proc do |result| if result.failed? context.exit(127) elsif result.signaled? context.exit(result.signal_code + 128) elsif result.error? context.exit(result.exit_code) end end end ## # @private # def self._interpret_result_callback(value, context) if value.is_a?(::Symbol) context.method(value) elsif value.respond_to?(:call) proc { |r| context.instance_eval { value.call(r, context) } } elsif value.nil? nil else raise ::ArgumentError, "Bad value for result_callback" end end ## # @private # def self._setup_clean_process(cmd) raise ::ArgumentError, "Toys process is unknown" unless ::Toys.executable_path cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String) cmd = [::RbConfig.ruby, ::Toys.executable_path] + cmd if defined?(::Bundler) if ::Bundler.respond_to?(:with_unbundled_env) ::Bundler.with_unbundled_env { yield(cmd) } else ::Bundler.with_clean_env { yield(cmd) } end else yield(cmd) end end on_initialize do |**opts| require "rbconfig" require "shellwords" require "toys/utils/exec" context = self opts = Exec._setup_exec_opts(opts, context) context[KEY] = Utils::Exec.new(**opts) do |k| k == :logger ? context[Context::Key::LOGGER] : nil end end end end end toys-core-0.21.0/lib/toys/dsl/0000755000004100000410000000000015165170677016113 5ustar www-datawww-datatoys-core-0.21.0/lib/toys/dsl/positional_arg.rb0000644000004100000410000001551415165170677021460 0ustar www-datawww-data# frozen_string_literal: true module Toys module DSL ## # DSL for an arg definition block. Lets you set arg attributes in a block # instead of a long series of keyword arguments. # # These directives are available inside a block passed to # {Toys::DSL::Tool#required_arg}, {Toys::DSL::Tool#optional_arg}, or # {Toys::DSL::Tool#remaining_args}. # # ### Example # # tool "mytool" do # optional_arg :value do # # The directives in here are defined by this class # accept Integer # desc "An integer value" # end # # ... # end # class PositionalArg ## # Set the acceptor for this argument's values. # You can pass either the string name of an acceptor defined in this tool # or any of its ancestors, or any other specification recognized by # {Toys::Acceptor.create}. # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def accept(spec = nil, **options, &block) @acceptor = Acceptor.scalarize_spec(spec, options, block) self end ## # Set the default value. # # @param default [Object] # @return [self] # def default(default) @default = default self end ## # Set the shell completion strategy for arg values. # You can pass either the string name of a completion defined in this # tool or any of its ancestors, or any other specification recognized by # {Toys::Completion.create}. # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def complete(spec = nil, **options, &block) @completion = Completion.scalarize_spec(spec, options, block) self end ## # Set the name of this arg as it appears in help screens. # # @param display_name [String] # @return [self] # def display_name(display_name) @display_name = display_name self end ## # Set the short description for the current positional argument. The # short description is displayed with the argument in online help. # # The description is a {Toys::WrappableString}, which may be word-wrapped # when displayed in a help screen. You may pass a {Toys::WrappableString} # directly to this method, or you may pass any input that can be used to # construct a wrappable string: # # * If you pass a String, its whitespace will be compacted (i.e. tabs, # newlines, and multiple consecutive whitespace will be turned into a # single space), and it will be word-wrapped on whitespace. # * If you pass an Array of Strings, each string will be considered a # literal word that cannot be broken, and wrapping will be done # across the strings in the array. In this case, whitespace is not # compacted. # # ### Examples # # If you pass in a sentence as a simple string, it may be word wrapped # when displayed: # # desc "This sentence may be wrapped." # # To specify a sentence that should never be word-wrapped, pass it as the # sole element of a string array: # # desc ["This sentence will not be wrapped."] # # @param desc [String,Array,Toys::WrappableString] # @return [self] # def desc(desc) @desc = desc self end ## # Add to the long description for the current positional argument. The # long description is displayed with the argument in online help. This # directive may be given multiple times, and the results are cumulative. # # A long description is a series of descriptions, which are generally # displayed in a series of lines/paragraphs. Each individual description # uses the form described in the {#desc} documentation, and may be # word-wrapped when displayed. To insert a blank line, include an empty # string as one of the descriptions. # # ### Example # # long_desc "This initial paragraph might get word wrapped.", # "This next paragraph is followed by a blank line.", # "", # ["This line will not be wrapped."], # [" This indent is preserved."] # long_desc "This line is appended to the description." # # @param long_desc [String,Array,Toys::WrappableString...] # @return [self] # def long_desc(*long_desc) @long_desc += long_desc self end ## # Specify whether to add a method for this argument. # # Recognized values are true to force creation of a method, false to # disable method creation, and nil for the default behavior. The default # checks the name and adds a method if the name is a symbol representing # a legal method name that starts with a letter and does not override any # public method in the Ruby Object class or collide with any method # directly defined in the tool class. # # @param value [true,false,nil] # def add_method(value) @add_method = if value.nil? nil elsif value true else false end end ## # Called only from DSL::Tool # # @private # def initialize(acceptor, default, completion, display_name, desc, long_desc, method_flag) @default = default @display_name = display_name @desc = desc @long_desc = long_desc || [] accept(acceptor, **{}) complete(completion, **{}) add_method(method_flag) end ## # @private # def _add_required_to(tool, key) tool.add_required_arg(key, accept: @acceptor, complete: @completion, display_name: @display_name, desc: @desc, long_desc: @long_desc) end ## # @private # def _add_optional_to(tool, key) tool.add_optional_arg(key, accept: @acceptor, default: @default, complete: @completion, display_name: @display_name, desc: @desc, long_desc: @long_desc) end ## # @private # def _set_remaining_on(tool, key) tool.set_remaining_args(key, accept: @acceptor, default: @default, complete: @completion, display_name: @display_name, desc: @desc, long_desc: @long_desc) end ## # @private # def _get_add_method @add_method end end end end toys-core-0.21.0/lib/toys/dsl/internal.rb0000644000004100000410000002042315165170677020255 0ustar www-datawww-data# frozen_string_literal: true module Toys module DSL ## # Internal utility calls used by the DSL. # # @private # module Internal ## # @private A list of method names to avoid using as getters # AVOID_GETTERS = (::Object.instance_methods + [:run, :initialize]) .grep(/^[a-z]\w*$/) .to_h { |name| [name, true] } .freeze class << self ## # Called by the Loader and InputFile to prepare a tool class for running # the DSL. # # @private # def prepare(tool_class, words, priority, remaining_words, source, loader) unless tool_class.is_a?(DSL::Tool) class << tool_class alias_method :super_include, :include end tool_class.extend(DSL::Tool) end unless tool_class.instance_variable_defined?(:@__words) tool_class.instance_variable_set(:@__words, words) tool_class.instance_variable_set(:@__priority, priority) tool_class.instance_variable_set(:@__loader, loader) tool_class.instance_variable_set(:@__source, []) end tool_class.instance_variable_set(:@__remaining_words, remaining_words) tool_class.instance_variable_get(:@__source).push(source) old_source = ::Thread.current[:__toys_current_source] begin ::Thread.current[:__toys_current_source] = source yield ensure tool_class.instance_variable_get(:@__source).pop ::Thread.current[:__toys_current_source] = old_source end end ## # Called by the DSL implementation to get, and optionally activate, the # current tool. # # @private # def current_tool(tool_class, activate) memoize_var = activate ? :@__active_tool : :@__cur_tool if tool_class.instance_variable_defined?(memoize_var) tool_class.instance_variable_get(memoize_var) else loader = tool_class.instance_variable_get(:@__loader) words = tool_class.instance_variable_get(:@__words) priority = tool_class.instance_variable_get(:@__priority) cur_tool = if activate loader.activate_tool(words, priority) else loader.get_tool(words, priority) end if cur_tool && activate source = tool_class.instance_variable_get(:@__source).last cur_tool.lock_source(source) end tool_class.instance_variable_set(memoize_var, cur_tool) end end ## # Called by the DSL implementation to analyze the name of a new tool # definition in context. # # @private # def analyze_name(tool_class, words) loader = tool_class.instance_variable_get(:@__loader) subtool_words = tool_class.instance_variable_get(:@__words).dup next_remaining = tool_class.instance_variable_get(:@__remaining_words) loader.split_path(words).each do |word| word = word.to_s subtool_words << word next_remaining = Loader.next_remaining_words(next_remaining, word) end [subtool_words, next_remaining] end ## # Called by the DSL implementation to add a getter to the tool class. # # @private # def maybe_add_getter(tool_class, key, force) return unless key.is_a?(::Symbol) case force when false return when true return unless /^[_a-zA-Z]\w*[!?]?$/.match(key.to_s) when nil return if !/^[a-zA-Z]\w*[!?]?$/.match?(key.to_s) || AVOID_GETTERS.key?(key) || tool_class.method_defined?(key, false) || tool_class.private_method_defined?(key, false) end tool_class.class_eval do define_method(key) do self[key] end end end ## # Called by the DSL implementation to find a named mixin. # # @private # def resolve_mixin(mixin, cur_tool, loader) mod = case mixin when ::String cur_tool.lookup_mixin(mixin) when ::Symbol loader.resolve_standard_mixin(mixin.to_s) when ::Module mixin end raise ToolDefinitionError, "Mixin not found: #{mixin.inspect}" unless mod mod end ## # Called by the DSL implementation to load a long description from a # file. # # @private # def load_long_desc_file(path) if ::File.extname(path) == ".txt" begin ::File.readlines(path).map do |line| line = line.chomp line =~ /^\s/ ? [line] : line end rescue ::SystemCallError => e raise Toys::ToolDefinitionError, e.to_s end else raise Toys::ToolDefinitionError, "Cannot load long desc from file type: #{path}" end end ## # Called by the Tool base class to set config values for a subclass. # # @private # def configure_class(tool_class, given_name = nil) return if tool_class.name.nil? || tool_class.instance_variable_defined?(:@__loader) mod_names = tool_class.name.split("::") class_name = mod_names.pop parent = parent_from_mod_name_segments(mod_names) loader = parent.instance_variable_get(:@__loader) name = given_name ? loader.split_path(given_name) : class_name_to_tool_name(class_name) priority = parent.instance_variable_get(:@__priority) words = parent.instance_variable_get(:@__words) + name subtool = loader.get_tool(words, priority, tool_class) remaining_words = parent.instance_variable_get(:@__remaining_words) next_remaining = name.reduce(remaining_words) do |running_words, word| Loader.next_remaining_words(running_words, word) end tool_class.instance_variable_set(:@__words, words) tool_class.instance_variable_set(:@__priority, priority) tool_class.instance_variable_set(:@__loader, loader) tool_class.instance_variable_set(:@__source, [current_source_from_context]) tool_class.instance_variable_set(:@__remaining_words, next_remaining) tool_class.instance_variable_set(:@__cur_tool, subtool) end ## # Called by the Tool base class to add the DSL to a subclass. # # @private # def setup_class_dsl(tool_class) return if tool_class.name.nil? || tool_class.is_a?(DSL::Tool) class << tool_class alias_method :super_include, :include end tool_class.extend(DSL::Tool) end private def class_name_to_tool_name(class_name) name = class_name.to_s.sub(/^_+/, "").sub(/_+$/, "").gsub(/_+/, "-") while name.sub!(/([^-])([A-Z])/, "\\1-\\2") do end [name.downcase!] end def parent_from_mod_name_segments(mod_names) parent = mod_names.reduce(::Object) do |running_mod, seg| running_mod.const_get(seg) end if !parent.is_a?(::Toys::Tool) && parent.instance_variable_defined?(:@__tool_class) parent = parent.instance_variable_get(:@__tool_class) end unless parent.ancestors.include?(::Toys::Context) raise ToolDefinitionError, "Toys::Tool can be subclassed only from the Toys DSL" end parent end def current_source_from_context source = ::Thread.current[:__toys_current_source] if source.nil? raise ToolDefinitionError, "Toys::Tool can be subclassed only from a Toys config file" end unless source.source_type == :file raise ToolDefinitionError, "Toys::Tool cannot be subclassed inside a tool block" end source end end end end end toys-core-0.21.0/lib/toys/dsl/tool.rb0000644000004100000410000023357015165170677017427 0ustar www-datawww-data# frozen_string_literal: true module Toys module DSL ## # This module defines the DSL for a Toys configuration file. # # A Toys configuration defines one or more named tools. It provides syntax # for setting the description, defining flags and arguments, specifying # how to execute the tool, and requesting mixin modules and other services. # It also lets you define subtools, nested arbitrarily deep, using blocks. # # ### Simple example # # Create a file called `.toys.rb` in the current directory, with the # following contents: # # tool "greet" do # desc "Prints a simple greeting" # # optional_arg :recipient, default: "world" # # def run # puts "Hello, #{recipient}!" # end # end # # The DSL directives `tool`, `desc`, `optional_arg`, and others are defined # in this module. # # Now you can execute it using: # # toys greet # # or try: # # toys greet rubyists # module Tool ## # Create a named acceptor that can be referenced by name from any flag or # positional argument in this tool or its subtools. # # An acceptor validates the string parameter passed to a flag or # positional argument. It also optionally converts the string to a # different object before storing it in your tool's data. # # Acceptors can be defined in one of four ways. # # * You can provide a **regular expression**. This acceptor validates # only if the regex matches the *entire string parameter*. # # You can also provide an optional conversion function as a block. If # provided, function must take a variable number of arguments, the # first being the matched string and the remainder being the captures # from the regular expression. It should return the converted object # that will be stored in the context data. If you do not provide a # block, the original string will be used. # # * You can provide an **array** of possible values. The acceptor # validates if the string parameter matches the *string form* of one # of the array elements (i.e. the results of calling `to_s` on the # array elements.) # # An array acceptor automatically converts the string parameter to # the actual array element that it matched. For example, if the # symbol `:foo` is in the array, it will match the string `"foo"`, # and then store the symbol `:foo` in the tool data. # # * You can provide a **range** of possible values, along with a # conversion function that converts a string parameter to a type # comparable by the range. (See the "function" spec below for a # detailed description of conversion functions.) If the range has # numeric endpoints, the conversion function is optional because a # default will be provided. # # * You can provide a **function** by passing it as a proc or a block. # This function performs *both* validation and conversion. It should # take the string parameter as its argument, and it must either # return the object that should be stored in the tool data, or raise # an exception (descended from `StandardError`) to indicate that the # string parameter is invalid. # # ### Example # # The following example creates an acceptor named "hex" that is defined # via a regular expression. It uses the acceptor to validate values # passed to a flag. # # tool "example" do # acceptor "hex", /[0-9a-fA-F]+/, type_desc: "hex numbers" # flag :number, accept: "hex" # def run # puts "number was #{number}" # end # end # # @param name [String] The acceptor name. # @param spec [Object] See the description for recognized values. # @param type_desc [String] Type description string, shown in help. # Defaults to the acceptor name. # @param block [Proc] See the description for recognized forms. # @return [self] # def acceptor(name, spec = nil, type_desc: nil, &block) cur_tool = DSL::Internal.current_tool(self, false) cur_tool&.add_acceptor(name, spec, type_desc: type_desc || name.to_s, &block) self end ## # Create a named mixin module that can be included by name from this tool # or its subtools. # # A mixin is a module that defines methods that can be called from a # tool. It is commonly used to provide "utility" methods, implementing # common functionality and allowing tools to share code. # # Normally you provide a block and define the mixin's methods in that # block. Alternatively, you can create a module separately and pass it # directly to this directive. # # ### Example # # The following example creates a named mixin and uses it in a tool. # # mixin "error-reporter" do # def error message # logger.error "An error occurred: #{message}" # exit 1 # end # end # # tool "build" do # include "error-reporter" # def run # puts "Building..." # error "Build failed!" # end # end # # @param name [String] Name of the mixin # @param mixin_module [Module] Module to use as the mixin. Optional. # Either pass a module here, *or* provide a block and define the # mixin within the block. # @param block [Proc] Defines the mixin module. # @return [self] # def mixin(name, mixin_module = nil, &block) cur_tool = DSL::Internal.current_tool(self, false) cur_tool&.add_mixin(name, mixin_module, &block) self end ## # Create a named template that can be expanded by name from this tool # or its subtools. # # A template is an object that generates DSL directives. You can use it # to build "prefabricated" tools, and then instantiate them in your Toys # files. # # A template is an object that defines an `expansion` procedure. This # procedure generates the DSL directives implemented by the template. The # template object typically also includes attributes that are used to # configure the expansion. # # The simplest way to define a template is to pass a block to the # {#template} directive. In the block, define an `initialize` method that # accepts any arguments that may be passed to the template when it is # instantiated and are used to configure the template. Define # `attr_reader`s or other methods to make this configuration accessible # from the object. Then define an `on_expand` block that implements the # template's expansion. The template object is passed as an object to the # `on_expand` block. # # Alternately, you can create a template class separately and pass it # directly. See {Toys::Template} for details on creating a template # class. # # ### Example # # The following example creates and uses a simple template. The template # defines a tool, with a configurable name, that simply prints out a # configurable message. # # template "hello-generator" do # def initialize(name, message) # @name = name # @message = message # end # attr_reader :name, :message # on_expand do |template| # tool template.name do # to_run do # puts template.message # end # end # end # end # # expand "hello-generator", "mytool", "mytool is running!" # # @param name [String] Name of the template # @param template_class [Class] Module to use as the mixin. Optional. # Either pass a module here, *or* provide a block and define the # mixin within the block. # @param block [Proc] Defines the template class. # @return [self] # def template(name, template_class = nil, &block) cur_tool = DSL::Internal.current_tool(self, false) return self if cur_tool.nil? cur_tool.add_template(name, template_class, &block) self end ## # Create a named completion procedure that may be used by name by any # flag or positional arg in this tool or any subtool. # # A completion controls tab completion for the value of a flag or # positional argument. In general, it is a Ruby `Proc` that takes a # context object (of type {Toys::Completion::Context}) and returns an # array of completion candidate strings. # # Completions can be specified in one of three ways. # # * A Proc object itself, either passed directly to this directive or # provided as a block. # * A static array of strings, indicating the completion candidates # independent of context. # * The symbol `:file_system` which indicates that paths in the file # system should serve as completion candidates. # # ### Example # # The following example defines a completion that uses only the immediate # files in the current directory as candidates. (This is different from # the `:file_system` completion which will descend into subdirectories # similar to how bash completes most of its file system commands.) # # completion "local-files" do |_context| # `/bin/ls`.split("\n") # end # tool "example" do # flag :file, complete_values: "local-files" # def run # puts "selected file #{file}" # end # end # # @param name [String] Name of the completion # @param spec [Object] See the description for recognized values. # @param options [Hash] Additional options to pass to the completion. # @param block [Proc] See the description for recognized forms. # @return [self] # def completion(name, spec = nil, **options, &block) cur_tool = DSL::Internal.current_tool(self, false) return self if cur_tool.nil? cur_tool.add_completion(name, spec, **options, &block) self end ## # Create a subtool. You must provide a block defining the subtool. # # ### Example # # The following example defines a tool and two subtools within it. # # tool "build" do # tool "staging" do # def run # puts "Building staging" # end # end # tool "production" do # def run # puts "Building production" # end # end # end # # The following example uses `delegate_to` to define a tool that runs one # of its subtools. # # tool "test", delegate_to: ["test", "unit"] do # tool "unit" do # def run # puts "Running unit tests" # end # end # end # # @param words [String,Array] The name of the subtool # @param if_defined [:combine,:reset,:ignore] What to do if a definition # already exists for this tool. Possible values are `:combine` (the # default) indicating the definition should be combined with the # existing definition, `:reset` indicating the earlier definition # should be reset and the new definition applied instead, or # `:ignore` indicating the new definition should be ignored. # @param delegate_to [String,Array] Optional. This tool should # delegate to another tool, specified by the full path. This path may # be given as an array of strings, or a single string possibly # delimited by path separators. # @param delegate_relative [String,Array] Optional. Similar to # delegate_to, but takes a delegate name relative to the context in # which this tool is being defined. # @param block [Proc] Defines the subtool. # @return [self] # def tool(words, if_defined: :combine, delegate_to: nil, delegate_relative: nil, &block) subtool_words, next_remaining = DSL::Internal.analyze_name(self, words) subtool = @__loader.get_tool(subtool_words, @__priority) if subtool.includes_definition? case if_defined when :ignore return self when :reset subtool.reset_definition end end if delegate_to || delegate_relative delegate_to2 = @__words + @__loader.split_path(delegate_relative) if delegate_relative orig_block = block block = proc do self.delegate_to(delegate_to) if delegate_to self.delegate_to(delegate_to2) if delegate_to2 instance_eval(&orig_block) if orig_block end end if block @__loader.load_block(source_info, block, subtool_words, next_remaining, @__priority) end self end ## # Create an alias, representing an "alternate name" for a tool. # # Note: This is functionally equivalent to creating a tool with the # `:delegate_relative` option. As such, `alias_tool` is considered # deprecated. # # ### Example # # This example defines a tool and an alias pointing to it. Both the tool # name `test` and the alias `t` will then refer to the same tool. # # tool "test" do # def run # puts "Running tests..." # end # end # alias_tool "t", "test" # # Note: the following is preferred over alias_tool: # # tool "t", delegate_relative: "test" # # @param word [String] The name of the alias # @param target [String,Array] Relative path to the target of the # alias. This path may be given as an array of strings, or a single # string possibly delimited by path separators. # @return [self] # @deprecated Use {#tool} and pass `:delegate_relative` instead # def alias_tool(word, target) tool(word, delegate_relative: target) self end ## # Causes the current tool to delegate to another tool, specified by the # full tool name. When run, it simply invokes the target tool with the # same arguments. # # ### Example # # This example defines a tool that runs one of its subtools. Running the # `test` tool will have the same effect (and recognize the same args) as # the subtool `test unit`. # # tool "test" do # tool "unit" do # flag :faster # def run # puts "running tests..." # end # end # delegate_to "test:unit" # end # # @param target [String,Array] The full path to the delegate # tool. This path may be given as an array of strings, or a single # string possibly delimited by path separators. # @return [self] # def delegate_to(target) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? cur_tool.delegate_to(@__loader.split_path(target)) self end ## # Load another config file or directory, as if its contents were inserted # at the current location. # # @param path [String] The file or directory to load. # @param as [String] Load into the given tool/namespace. If omitted, # configuration will be loaded into the current namespace. # # @return [self] # def load(path, as: nil) if as tool(as) do load(path) end return self end @__loader.load_path(source_info, path, @__words, @__remaining_words, @__priority) self end ## # Load configuration from a public git repository, as if its contents # were inserted at the current location. # # @param remote [String] The URL of the git repository. Defaults to the # current repository if already loading from git. # @param path [String] The path within the repo to the file or directory # to load. Defaults to the root of the repo. # @param commit [String] The commit branch, tag, or sha. Defaults to the # current commit if already loading from git, or to `HEAD`. # @param as [String] Load into the given tool/namespace. If omitted, # configuration will be loaded into the current namespace. # @param update [Boolean,Integer] Whether and when to force-fetch from # the remote (unless the commit is a SHA). Force-fetching will ensure # that symbolic commits, such as branch names or HEAD, are up to date. # You can pass `true` or `false` to specify whether to update, or an # integer to update if the last update was done at least that many # seconds ago. Default is false. # # @return [self] # def load_git(remote: nil, path: nil, commit: nil, as: nil, update: false) if as tool(as) do load_git(remote: remote, path: path, commit: commit) end return self end remote ||= source_info.git_remote raise ToolDefinitionError, "Git remote not specified" unless remote path ||= "" commit ||= source_info.git_commit || "HEAD" @__loader.load_git(source_info, remote, path, commit, update, @__words, @__remaining_words, @__priority) self end ## # Load configuration from a gem, as if its contents were inserted at the # current location. # # @param name [String] Name of the gem # @param versions [Array] Version requirements for the gem. # @param version [String,Array] An alternate way to specify # version requirements for the gem. # @param path [String] Optional path within the gem to the file or # directory to load. Defaults to the root of the gem's toys directory. # @param toys_dir [String] Optional override for the gem's toys # directory name. If not specified, the default specified by the gem # will be used. # @param as [String] Load into the given tool/namespace. If omitted, # configuration will be loaded into the current namespace. # def load_gem(name, *versions, version: nil, path: nil, toys_dir: nil, as: nil) version = versions + Array(version) if as tool(as) do load_gem(name, version: version, path: path, toys_dir: toys_dir) end return self end path ||= "" @__loader.load_gem(source_info, name, version, toys_dir, path, @__words, @__remaining_words, @__priority) self end ## # Expand the given template in the current location. # # The template may be specified as a class or a well-known template name. # You may also provide arguments to pass to the template. # # ### Example # # The following example creates and uses a simple template. # # template "hello-generator" do # def initialize(name, message) # @name = name # @message = message # end # attr_reader :name, :message # expansion do |template| # tool template.name do # to_run do # puts template.message # end # end # end # end # # expand "hello-generator", "mytool", "mytool is running!" # # @param template_class [Class,String,Symbol] The template, either as a # class or a well-known name. # @param args [Object...] Template arguments # @return [self] # def expand(template_class, *args, **kwargs) cur_tool = DSL::Internal.current_tool(self, false) return self if cur_tool.nil? name = template_class.to_s case template_class when ::String template_class = cur_tool.lookup_template(template_class) when ::Symbol template_class = @__loader.resolve_standard_template(name) end if template_class.nil? raise ToolDefinitionError, "Template not found: #{name.inspect}" end template = template_class.new(*args, **kwargs) yield template if block_given? class_exec(template, &template_class.expansion) self end ## # Set the short description for the current tool. The short description # is displayed with the tool in a subtool list. You may also use the # equivalent method `short_desc`. # # The description is a {Toys::WrappableString}, which may be word-wrapped # when displayed in a help screen. You may pass a {Toys::WrappableString} # directly to this method, or you may pass any input that can be used to # construct a wrappable string: # # * If you pass a String, its whitespace will be compacted (i.e. tabs, # newlines, and multiple consecutive whitespace will be turned into a # single space), and it will be word-wrapped on whitespace. # * If you pass an Array of Strings, each string will be considered a # literal word that cannot be broken, and wrapping will be done # across the strings in the array. In this case, whitespace is not # compacted. # # ### Examples # # If you pass in a sentence as a simple string, it may be word wrapped # when displayed: # # desc "This sentence may be wrapped." # # To specify a sentence that should never be word-wrapped, pass it as the # sole element of a string array: # # desc ["This sentence will not be wrapped."] # # @param str [Toys::WrappableString,String,Array] # @return [self] # def desc(str) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? cur_tool.desc = str self end alias short_desc desc ## # Add to the long description for the current tool. The long description # is displayed in the usage documentation for the tool itself. This # directive may be given multiple times, and the results are cumulative. # # A long description is a series of descriptions, which are generally # displayed in a series of lines/paragraphs. Each individual description # uses the form described in the {#desc} documentation, and may be # word-wrapped when displayed. To insert a blank line, include an empty # string as one of the descriptions. # # ### Example # # long_desc "This initial paragraph might get word wrapped.", # "This next paragraph is followed by a blank line.", # "", # ["This line will not be wrapped."], # [" This indent is preserved."] # long_desc "This line is appended to the description." # # @param strs [Toys::WrappableString,String,Array...] # @param file [String] Optional. Read the description from the given file # provided relative to the current toys file. The file must be a # plain text file whose suffix is `.txt`. # @param data [String] Optional. Read the description from the given data # file. The file must be a plain text file whose suffix is `.txt`. # @return [self] # def long_desc(*strs, file: nil, data: nil) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? if file unless source_info.source_path raise ::Toys::ToolDefinitionError, "Cannot set long_desc from a file because the tool is not defined in a file" end file = ::File.join(::File.dirname(source_info.source_path), file) elsif data file = source_info.find_data(data, type: :file) end strs += DSL::Internal.load_long_desc_file(file) if file cur_tool.append_long_desc(strs) self end ## # Create a flag group. If a block is given, flags defined in the block # belong to the group. The flags in the group are listed together in # help screens. # # ### Example # # The following example creates a flag group in which all flags are # optional. # # tool "execute" do # flag_group desc: "Debug Flags" do # flag :debug, "-D", desc: "Enable debugger" # flag :warnings, "-W[VAL]", desc: "Enable warnings" # end # # ... # end # # @param type [Symbol] The type of group. Allowed values: `:required`, # `:optional`, `:exactly_one`, `:at_most_one`, `:at_least_one`. # Default is `:optional`. # @param desc [String,Array,Toys::WrappableString] Short # description for the group. See {Toys::DSL::Tool#desc} for a # description of allowed formats. Defaults to `"Flags"`. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag group. See # {Toys::DSL::Tool#long_desc} for a description of allowed formats. # Defaults to the empty array. # @param name [String,Symbol,nil] The name of the group, or nil for no # name. # @param report_collisions [Boolean] If `true`, raise an exception if a # the given name is already taken. If `false`, ignore. Default is # `true`. # @param prepend [Boolean] If `true`, prepend rather than append the # group to the list. Default is `false`. # @param block [Proc] Adds flags to the group. See {Toys::DSL::FlagGroup} # for the directives that can be called in this block. # @return [self] # def flag_group(type: :optional, desc: nil, long_desc: nil, name: nil, report_collisions: true, prepend: false, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? cur_tool.add_flag_group(type: type, desc: desc, long_desc: long_desc, name: name, report_collisions: report_collisions, prepend: prepend) group = prepend ? cur_tool.flag_groups.first : cur_tool.flag_groups.last flag_group_dsl = DSL::FlagGroup.new(self, cur_tool, group) flag_group_dsl.instance_exec(flag_group_dsl, &block) if block self end ## # Create a flag group of type `:required`. If a block is given, flags # defined in the block belong to the group. All flags in this group are # required. # # ### Example # # The following example creates a group of required flags. # # tool "login" do # all_required do # flag :username, "--username=VAL", desc: "Set username (required)" # flag :password, "--password=VAL", desc: "Set password (required)" # end # # ... # end # # @param desc [String,Array,Toys::WrappableString] Short # description for the group. See {Toys::DSL::Tool#desc} for a # description of allowed formats. Defaults to `"Flags"`. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag group. See # {Toys::DSL::Tool#long_desc} for a description of allowed formats. # Defaults to the empty array. # @param name [String,Symbol,nil] The name of the group, or nil for no # name. # @param report_collisions [Boolean] If `true`, raise an exception if a # the given name is already taken. If `false`, ignore. Default is # `true`. # @param prepend [Boolean] If `true`, prepend rather than append the # group to the list. Default is `false`. # @param block [Proc] Adds flags to the group. See {Toys::DSL::FlagGroup} # for the directives that can be called in this block. # @return [self] # def all_required(desc: nil, long_desc: nil, name: nil, report_collisions: true, prepend: false, &block) flag_group(type: :required, desc: desc, long_desc: long_desc, name: name, report_collisions: report_collisions, prepend: prepend, &block) end ## # Create a flag group of type `:at_most_one`. If a block is given, flags # defined in the block belong to the group. At most one flag in this # group must be provided on the command line. # # ### Example # # The following example creates a group of flags in which either one or # none may be set, but not more than one. # # tool "provision-server" do # at_most_one do # flag :restore_from_backup, "--restore-from-backup=VAL" # flag :restore_from_image, "--restore-from-image=VAL" # flag :clone_existing, "--clone-existing=VAL" # end # # ... # end # # @param desc [String,Array,Toys::WrappableString] Short # description for the group. See {Toys::DSL::Tool#desc} for a # description of allowed formats. Defaults to `"Flags"`. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag group. See # {Toys::DSL::Tool#long_desc} for a description of allowed formats. # Defaults to the empty array. # @param name [String,Symbol,nil] The name of the group, or nil for no # name. # @param report_collisions [Boolean] If `true`, raise an exception if a # the given name is already taken. If `false`, ignore. Default is # `true`. # @param prepend [Boolean] If `true`, prepend rather than append the # group to the list. Default is `false`. # @param block [Proc] Adds flags to the group. See {Toys::DSL::FlagGroup} # for the directives that can be called in this block. # @return [self] # def at_most_one(desc: nil, long_desc: nil, name: nil, report_collisions: true, prepend: false, &block) flag_group(type: :at_most_one, desc: desc, long_desc: long_desc, name: name, report_collisions: report_collisions, prepend: prepend, &block) end alias at_most_one_required at_most_one ## # Create a flag group of type `:at_least_one`. If a block is given, flags # defined in the block belong to the group. At least one flag in this # group must be provided on the command line. # # ### Example # # The following example creates a group of flags in which one or more # may be set. # # tool "run-tests" do # at_least_one do # flag :unit, desc: "Run unit tests" # flag :integration, desc: "Run integration tests" # flag :performance, desc: "Run performance tests" # end # # ... # end # # @param desc [String,Array,Toys::WrappableString] Short # description for the group. See {Toys::DSL::Tool#desc} for a # description of allowed formats. Defaults to `"Flags"`. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag group. See # {Toys::DSL::Tool#long_desc} for a description of allowed formats. # Defaults to the empty array. # @param name [String,Symbol,nil] The name of the group, or nil for no # name. # @param report_collisions [Boolean] If `true`, raise an exception if a # the given name is already taken. If `false`, ignore. Default is # `true`. # @param prepend [Boolean] If `true`, prepend rather than append the # group to the list. Default is `false`. # @param block [Proc] Adds flags to the group. See {Toys::DSL::FlagGroup} # for the directives that can be called in this block. # @return [self] # def at_least_one(desc: nil, long_desc: nil, name: nil, report_collisions: true, prepend: false, &block) flag_group(type: :at_least_one, desc: desc, long_desc: long_desc, name: name, report_collisions: report_collisions, prepend: prepend, &block) end alias at_least_one_required at_least_one ## # Create a flag group of type `:exactly_one`. If a block is given, flags # defined in the block belong to the group. Exactly one flag in this # group must be provided on the command line. # # ### Example # # The following example creates a group of flags in which exactly one # must be set. # # tool "deploy" do # exactly_one do # flag :server, "--server=IP_ADDR", desc: "Deploy to server" # flag :vm, "--vm=ID", desc: "Deploy to a VM" # flag :container, "--container=ID", desc: "Deploy to a container" # end # # ... # end # # @param desc [String,Array,Toys::WrappableString] Short # description for the group. See {Toys::DSL::Tool#desc} for a # description of allowed formats. Defaults to `"Flags"`. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag group. See # {Toys::DSL::Tool#long_desc} for a description of allowed formats. # Defaults to the empty array. # @param name [String,Symbol,nil] The name of the group, or nil for no # name. # @param report_collisions [Boolean] If `true`, raise an exception if a # the given name is already taken. If `false`, ignore. Default is # `true`. # @param prepend [Boolean] If `true`, prepend rather than append the # group to the list. Default is `false`. # @param block [Proc] Adds flags to the group. See {Toys::DSL::FlagGroup} # for the directives that can be called in this block. # @return [self] # def exactly_one(desc: nil, long_desc: nil, name: nil, report_collisions: true, prepend: false, &block) flag_group(type: :exactly_one, desc: desc, long_desc: long_desc, name: name, report_collisions: report_collisions, prepend: prepend, &block) end alias exactly_one_required exactly_one ## # Add a flag to the current tool. Each flag must specify a key which # the script may use to obtain the flag value from the context. # You may then provide the flags themselves in OptionParser form. # # If the given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # Attributes of the flag may be passed in as arguments to this method, or # set in a block passed to this method. If you provide a block, you can # use directives in {Toys::DSL::Flag} within the block. # # ### Flag syntax # # The flags themselves should be provided in OptionParser form. Following # are examples of valid syntax. # # * `-a` : A short boolean switch. When this appears as an argument, # the value is set to `true`. # * `--abc` : A long boolean switch. When this appears as an argument, # the value is set to `true`. # * `-aVAL` or `-a VAL` : A short flag that takes a required value. # These two forms are treated identically. If this argument appears # with a value attached (e.g. `-afoo`), the attached string (e.g. # `"foo"`) is taken as the value. Otherwise, the following argument # is taken as the value (e.g. for `-a foo`, the value is set to # `"foo"`.) The following argument is treated as the value even if it # looks like a flag (e.g. `-a -a` causes the string `"-a"` to be # taken as the value.) # * `-a[VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the value # is set to `true`. The following argument is never interpreted as # the value. (Compare with `-a [VAL]`.) # * `-a [VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, if the # following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `-a foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `-a[VAL]`.) # * `--abc=VAL` or `--abc VAL` : A long flag that takes a required # value. These two forms are treated identically. If this argument # appears with a value attached (e.g. `--abc=foo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the # following argument is taken as the value (e.g. for `--abc foo`, the # value is set to `"foo"`.) The following argument is treated as the # value even if it looks like a flag (e.g. `--abc --def` causes the # string `"--def"` to be taken as the value.) # * `--abc[=VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, # the value is set to `true`. The following argument is never # interpreted as the value. (Compare with `--abc [VAL]`.) # * `--abc [VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, if # the following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `--abc foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `--abc=[VAL]`.) # * `--[no-]abc` : A long boolean switch that can be turned either on # or off. This effectively creates two flags, `--abc` which sets the # value to `true`, and `--no-abc` which sets the falue to `false`. # # ### Default flag syntax # # If no flag syntax strings are provided, a default syntax will be # inferred based on the key and other options. # # Specifically, if the key has one character, then that character will be # chosen as a short flag. If the key has multiple characters, a long flag # will be generated. # # Furthermore, if a custom completion, a non-boolean acceptor, or a # non-boolean default value is provided in the options, then the flag # will be considered to take a value. Otherwise, it will be considered to # be a boolean switch. # # For example, the following pairs of flags are identical: # # flag :a # flag :a, "-a" # # flag :abc_def # flag :abc_def, "--abc-def" # # flag :number, accept: Integer # flag :number, "--number=VAL", accept: Integer # # ### More examples # # A flag that sets its value to the number of times it appears on the # command line: # # flag :verbose, "-v", "--verbose", # default: 0, handler: ->(_val, count) { count + 1 } # # An example using block form: # # flag :shout do # flags "-s", "--shout" # default false # desc "Say it louder" # long_desc "This flag says it lowder.", # "You might use this when people can't hear you.", # "", # "Example:", # [" toys say --shout hello"] # end # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param flags [String...] The flags in OptionParser format. # @param accept [Object] An acceptor that validates and/or converts the # value. You may provide either the name of an acceptor you have # defined, one of the default acceptors provided by OptionParser, or # any other specification recognized by {Toys::Acceptor.create}. # Optional. If not specified, accepts any value as a string. # @param default [Object] The default value. This is the value that will # be set in the context if this flag is not provided on the command # line. Defaults to `nil`. # @param handler [Proc,nil,:set,:push] An optional handler that # customizes how a value is set or updated when the flag is parsed. # A handler is a proc that takes up to three arguments: the given # value, the previous value, and a hash containing all the data # collected so far during argument parsing. The proc must return the # new value for the flag. # You may also specify a predefined named handler. The `:set` handler # (the default) replaces the previous value (effectively # `-> (val) { val }`). The `:push` handler expects the previous value # to be an array and pushes the given value onto it; it should be # combined with setting the default value to `[]` and is intended for # "multi-valued" flags. # @param complete_flags [Object] A specifier for shell tab completion # for flag names associated with this flag. By default, a # {Toys::Flag::DefaultCompletion} is used, which provides the flag's # names as completion candidates. To customize completion, set this # to the name of a previously defined completion, a hash of options # to pass to the constructor for {Toys::Flag::DefaultCompletion}, or # any other spec recognized by {Toys::Completion.create}. # @param complete_values [Object] A specifier for shell tab completion # for flag values associated with this flag. This is the empty # completion by default. To customize completion, set this to the # name of a previously defined completion, or any spec recognized by # {Toys::Completion.create}. # @param report_collisions [Boolean] Raise an exception if a flag is # requested that is already in use or marked as unusable. Default is # true. # @param group [Toys::FlagGroup,String,Symbol,nil] Group for this flag. # You may provide a group name, a FlagGroup object, or `nil` which # denotes the default group. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::DSL::Tool#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::DSL::Tool#long_desc} for # a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @param display_name [String] A display name for this flag, used in help # text and error messages. # @param add_method [true,false,nil] Whether to add a method for this # flag. If omitted or set to nil, uses the default behavior, which # adds the method if the key is a symbol representing a legal method # name that starts with a letter and does not override any public # method in the Ruby Object class or collide with any method directly # defined in the tool class. # @param block [Proc] Configures the flag. See {Toys::DSL::Flag} for the # directives that can be called in this block. # @return [self] # def flag(key, *flags, accept: nil, default: nil, handler: nil, complete_flags: nil, complete_values: nil, report_collisions: true, group: nil, desc: nil, long_desc: nil, display_name: nil, add_method: nil, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? flag_dsl = DSL::Flag.new( flags.flatten, accept, default, handler, complete_flags, complete_values, report_collisions, group, desc, long_desc, display_name, add_method ) flag_dsl.instance_exec(flag_dsl, &block) if block flag_dsl._add_to(cur_tool, key) DSL::Internal.maybe_add_getter(self, key, flag_dsl._get_add_method) self end ## # Add a required positional argument to the current tool. You must # specify a key which the script may use to obtain the argument value # from the context. # # If the given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # Attributes of the arg may be passed in as arguments to this method, or # set in a block passed to this method. If you provide a block, you can # use directives in {Toys::DSL::PositionalArg} within the block. # # ### Example # # This tool "moves" something from a source to destination, and takes two # required arguments: # # tool "mv" do # required_arg :source # required_arg :dest # def run # puts "moving from #{source} to #{dest}..." # end # end # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param accept [Object] An acceptor that validates and/or converts the # value. You may provide either the name of an acceptor you have # defined, one of the default acceptors provided by OptionParser, or # any other specification recognized by {Toys::Acceptor.create}. # Optional. If not specified, accepts any value as a string. # @param complete [Object] A specifier for shell tab completion for # values of this arg. This is the empty completion by default. To # customize completion, set this to the name of a previously defined # completion, or any spec recognized by {Toys::Completion.create}. # @param display_name [String] A name to use for display (in help text # and error reports). Defaults to the key in upper case. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::DSL::Tool#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::DSL::Tool#long_desc} for # a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @param add_method [true,false,nil] Whether to add a method for this # argument. If omitted or set to nil, uses the default behavior, # which adds the method if the key is a symbol representing a legal # method name that starts with a letter and does not override any # public method in the Ruby Object class or collide with any method # directly defined in the tool class. # @param block [Proc] Configures the positional argument. See # {Toys::DSL::PositionalArg} for the directives that can be called in # this block. # @return [self] # def required_arg(key, accept: nil, complete: nil, display_name: nil, desc: nil, long_desc: nil, add_method: nil, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? arg_dsl = DSL::PositionalArg.new(accept, nil, complete, display_name, desc, long_desc, add_method) arg_dsl.instance_exec(arg_dsl, &block) if block arg_dsl._add_required_to(cur_tool, key) DSL::Internal.maybe_add_getter(self, key, arg_dsl._get_add_method) self end alias required required_arg ## # Add an optional positional argument to the current tool. You must # specify a key which the script may use to obtain the argument value # from the context. If an optional argument is not given on the command # line, the value is set to the given default. # # If the given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # Attributes of the arg may be passed in as arguments to this method, or # set in a block passed to this method. If you provide a block, you can # use directives in {Toys::DSL::PositionalArg} within the block. # # ### Example # # This tool creates a "link" to a given target. The link location is # optional; if it is not given, it is inferred from the target. # # tool "ln" do # required_arg :target # optional_arg :location # def run # loc = location || File.basename(target) # puts "linking to #{target} from #{loc}..." # end # end # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param default [Object] The default value. This is the value that will # be set in the context if this argument is not provided on the # command line. Defaults to `nil`. # @param accept [Object] An acceptor that validates and/or converts the # value. You may provide either the name of an acceptor you have # defined, one of the default acceptors provided by OptionParser, or # any other specification recognized by {Toys::Acceptor.create}. # Optional. If not specified, accepts any value as a string. # @param complete [Object] A specifier for shell tab completion for # values of this arg. This is the empty completion by default. To # customize completion, set this to the name of a previously defined # completion, or any spec recognized by {Toys::Completion.create}. # @param display_name [String] A name to use for display (in help text # and error reports). Defaults to the key in upper case. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::DSL::Tool#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::DSL::Tool#long_desc} for # a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @param add_method [true,false,nil] Whether to add a method for this # argument. If omitted or set to nil, uses the default behavior, # which adds the method if the key is a symbol representing a legal # method name that starts with a letter and does not override any # public method in the Ruby Object class or collide with any method # directly defined in the tool class. # @param block [Proc] Configures the positional argument. See # {Toys::DSL::PositionalArg} for the directives that can be called in # this block. # @return [self] # def optional_arg(key, default: nil, accept: nil, complete: nil, display_name: nil, desc: nil, long_desc: nil, add_method: nil, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? arg_dsl = DSL::PositionalArg.new(accept, default, complete, display_name, desc, long_desc, add_method) arg_dsl.instance_exec(arg_dsl, &block) if block arg_dsl._add_optional_to(cur_tool, key) DSL::Internal.maybe_add_getter(self, key, arg_dsl._get_add_method) self end alias optional optional_arg ## # Specify what should be done with unmatched positional arguments. You # must specify a key which the script may use to obtain the remaining # args from the context. # # If the given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # Attributes of the arg may be passed in as arguments to this method, or # set in a block passed to this method. If you provide a block, you can # use directives in {Toys::DSL::PositionalArg} within the block. # # ### Example # # This tool displays a "list" of the given directories. If no directories # ar given, lists the current directory. # # tool "ln" do # remaining_args :directories # def run # dirs = directories.empty? ? [Dir.pwd] : directories # dirs.each do |dir| # puts "Listing directory #{dir}..." # end # end # end # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param default [Object] The default value. This is the value that will # be set in the context if no unmatched arguments are provided on the # command line. Defaults to the empty array `[]`. # @param accept [Object] An acceptor that validates and/or converts the # value. You may provide either the name of an acceptor you have # defined, one of the default acceptors provided by OptionParser, or # any other specification recognized by {Toys::Acceptor.create}. # Optional. If not specified, accepts any value as a string. # @param complete [Object] A specifier for shell tab completion for # values of this arg. This is the empty completion by default. To # customize completion, set this to the name of a previously defined # completion, or any spec recognized by {Toys::Completion.create}. # @param display_name [String] A name to use for display (in help text # and error reports). Defaults to the key in upper case. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::DSL::Tool#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::DSL::Tool#long_desc} for # a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @param add_method [true,false,nil] Whether to add a method for these # arguments. If omitted or set to nil, uses the default behavior, # which adds the method if the key is a symbol representing a legal # method name that starts with a letter and does not override any # public method in the Ruby Object class or collide with any method # directly defined in the tool class. # @param block [Proc] Configures the positional argument. See # {Toys::DSL::PositionalArg} for the directives that can be called in # this block. # @return [self] # def remaining_args(key, default: [], accept: nil, complete: nil, display_name: nil, desc: nil, long_desc: nil, add_method: nil, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? arg_dsl = DSL::PositionalArg.new(accept, default, complete, display_name, desc, long_desc, add_method) arg_dsl.instance_exec(arg_dsl, &block) if block arg_dsl._set_remaining_on(cur_tool, key) DSL::Internal.maybe_add_getter(self, key, arg_dsl._get_add_method) self end alias remaining remaining_args ## # Set option values statically and create helper methods. # # If any given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # ### Example # # tool "hello" do # static :greeting, "Hi there" # def run # puts "#{greeting}, world!" # end # end # # @overload static(key, value) # Set a single value by key. # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param value [Object] The value to set. # @return [self] # # @overload static(hash) # Set multiple keys and values # @param hash [Hash] The keys and values to set # @return [self] # def static(key, value = nil) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? if key.is_a?(::Hash) cur_tool.default_data.merge!(key) key.each_key do |k| DSL::Internal.maybe_add_getter(self, k, true) end else cur_tool.default_data[key] = value DSL::Internal.maybe_add_getter(self, key, true) end self end ## # Set option values statically without creating helper methods. # # ### Example # # tool "hello" do # set :greeting, "Hi there" # def run # puts "#{get(:greeting)}, world!" # end # end # # @overload set(key, value) # Set a single value by key. # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param value [Object] The value to set. # @return [self] # # @overload set(hash) # Set multiple keys and values # @param hash [Hash] The keys and values to set # @return [self] # def set(key, value = nil) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? if key.is_a?(::Hash) cur_tool.default_data.merge!(key) else cur_tool.default_data[key] = value end self end ## # Enforce that all flags must be provided before any positional args. # That is, as soon as the first positional arg appears in the command # line arguments, flag parsing is disabled as if `--` had appeared. # # Issuing this directive by itself turns on enforcement. You may turn it # off by passsing `false` as the parameter. # # @param state [Boolean] # @return [self] # def enforce_flags_before_args(state = true) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.enforce_flags_before_args(state) self end ## # Require that flags must match exactly. That is, flags must appear in # their entirety on the command line. (If false, substrings of flags are # accepted as long as they are unambiguous.) # # Issuing this directive by itself turns on exact match. You may turn it # off by passsing `false` as the parameter. # # @param state [Boolean] # @return [self] # def require_exact_flag_match(state = true) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.require_exact_flag_match(state) self end ## # Disable argument parsing for this tool. Arguments will not be parsed # and the options will not be populated. Instead, tools can retrieve the # full unparsed argument list by calling {Toys::Context#args}. # # This directive is mutually exclusive with any of the directives that # declare arguments or flags. # # ### Example # # tool "mytool" do # disable_argument_parsing # def run # puts "Arguments passed: #{args}" # end # end # # @return [self] # def disable_argument_parsing cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.disable_argument_parsing self end ## # Mark one or more flags as disabled, preventing their use by any # subsequent flag definition. This can be used to prevent middleware from # defining a particular flag. # # ### Example # # This tool does not support the `-v` and `-q` short forms for the two # verbosity flags (although it still supports the long forms `--verbose` # and `--quiet`.) # # tool "mytool" do # disable_flag "-v", "-q" # def run # # ... # end # end # # @param flags [String...] The flags to disable # @return [self] # def disable_flag(*flags) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.disable_flag(*flags) self end ## # Set the shell completion strategy for this tool's arguments. # You can pass one of the following: # # * The string name of a completion defined in this tool or any of its # its ancestors. # * A hash of options to pass to the constructor of # {Toys::ToolDefinition::DefaultCompletion}. # * `nil` or `:default` to select the standard completion strategy # (which is {Toys::ToolDefinition::DefaultCompletion} with no extra # options). # * Any other specification recognized by {Toys::Completion.create}. # # ### Example # # The namespace "foo" supports completion only of subtool names. It does # not complete the standard flags (like --help). # # tool "foo" do # complete_tool_args complete_args: false, complete_flags: false, # complete_flag_values: false # tool "bar" do # def run # puts "in foo bar" # end # end # end # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def complete_tool_args(spec = nil, **options, &block) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? cur_tool.completion = Completion.scalarize_spec(spec, options, block) self end ## # Specify how to run this tool. # # Typically the entrypoint for a tool is a method named `run`. However, # you can change this by passing a different method name, as a symbol, to # {#to_run}. # # You can also alternatively pass a block to {#to_run}. You might do this # if your method needs access to local variables in the lexical scope. # However, it is often more convenient to use {#static} to set those # values in the context. # # ### Examples # # # Set a different method name as the entrypoint: # # tool "foo" do # to_run :foo # def foo # puts "The foo tool ran!" # end # end # # # Use a block to retain access to the enclosing lexical scope from # # the run method: # # tool "foo" do # cur_time = Time.now # to_run do # puts "The time at tool definition was #{cur_time}" # end # end # # # But the following is approximately equivalent: # # tool "foo" do # static :cur_time, Time.now # def run # puts "The time at tool definition was #{cur_time}" # end # end # # @param handler [Proc,Symbol,nil] The run handler as a method name # symbol or a proc, or nil to explicitly set as non-runnable. # @param block [Proc] The run handler as a block. # @return [self] # def to_run(handler = nil, &block) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.run_handler = handler || block self end alias on_run to_run ## # Specify how to handle interrupts. # # You can provide either a block to be called, a Proc to be called, or # the name of a method to be called. In each case, the block, Proc, or # method can optionally take one argument, the Interrupt exception that # was raised. # # Note: this is equivalent to `on_signal("SIGINT")`. # # ### Example # # tool "foo" do # def run # sleep 10 # end # on_interrupt do # puts "I was interrupted." # end # end # # @param handler [Proc,Symbol,nil] The interrupt callback proc or method # name. Pass nil to disable interrupt handling. # @param block [Proc] The interrupt callback as a block. # @return [self] # def on_interrupt(handler = nil, &block) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.interrupt_handler = handler || block self end ## # Specify how to handle the given signal. # # You can provide either a block to be called, a Proc to be called, or # the name of a method to be called. In each case, the block, Proc, or # method can optionally take one argument, the SignalException that was # raised. # # ### Example # # tool "foo" do # def run # sleep 10 # end # on_signal("QUIT") do |e| # puts "Signal caught: #{e.signm}." # end # end # # @param signal [Integer,String,Symbol] The signal name or number # @param handler [Proc,Symbol,nil] The signal callback proc or method # name. Pass nil to disable signal handling. # @param block [Proc] The signal callback as a block. # @return [self] # def on_signal(signal, handler = nil, &block) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.set_signal_handler(signal, handler || block) self end ## # Specify how to handle usage errors. # # You can provide either a block to be called, a Proc to be called, or # the name of a method to be called. In each case, the block, Proc, or # method can optionally take one argument, the array of usage errors # reported. # # ### Example # # This tool runs even if a usage error is encountered, by setting the # `run` method as the usage error handler. # # tool "foo" do # def run # puts "Errors: #{usage_errors.join("\n")}" # end # on_usage_error :run # end # # @param handler [Proc,Symbol,nil] The interrupt callback proc or method # name. Pass nil to disable interrupt handling. # @param block [Proc] The interrupt callback as a block. # @return [self] # def on_usage_error(handler = nil, &block) cur_tool = DSL::Internal.current_tool(self, true) cur_tool&.usage_error_handler = handler || block self end ## # Specify that the given module should be mixed into this tool, and its # methods made available when running the tool. # # You can provide either a module, the string name of a mixin that you # have defined in this tool or one of its ancestors, or the symbol name # of a well-known mixin. # # ### Example # # Include the well-known mixin `:terminal` and perform some terminal # magic. # # tool "spin" do # include :terminal # def run # # The spinner method is defined by the :terminal mixin. # spinner(leading_text: "Waiting...", final_text: "\n") do # sleep 5 # end # end # end # # @param mixin [Module,Symbol,String] Module or module name. # @param args [Object...] Arguments to pass to the initializer # @param kwargs [keywords] Keyword arguments to pass to the initializer # @return [self] # def include(mixin, *args, **kwargs) cur_tool = DSL::Internal.current_tool(self, true) return self if cur_tool.nil? mod = DSL::Internal.resolve_mixin(mixin, cur_tool, @__loader) cur_tool.include_mixin(mod, *args, **kwargs) self end ## # Determine if the given module/mixin has already been included. # # You can provide either a module, the string name of a mixin that you # have defined in this tool or one of its ancestors, or the symbol name # of a well-known mixin. # # @param mod [Module,Symbol,String] Module or module name. # # @return [Boolean] Whether the mixin is included # @return [nil] if the current tool is not active. # def include?(mod) cur_tool = DSL::Internal.current_tool(self, false) return if cur_tool.nil? super(DSL::Internal.resolve_mixin(mod, cur_tool, @__loader)) end ## # Return the current source info object. # # @return [Toys::SourceInfo] Source info. # def source_info @__source.last end ## # Find the given data path (file or directory). # # Data directories are a convenient place to put images, archives, keys, # or other such static data needed by your tools. Data files are located # in a directory called `.data` inside a Toys directory. This directive # locates a data file during tool definition. # # ### Example # # This tool reads its description from a text file in the `.data` # directory. # # tool "mytool" do # path = find_data("mytool-desc.txt", type: :file) # desc IO.read(path) if path # def run # # ... # end # end # # @param path [String] The path to find # @param type [nil,:file,:directory] Type of file system object to find. # Default is `nil`, indicating any type. # # @return [String] Absolute path of the data. # @return [nil] if the given data path is not found. # def find_data(path, type: nil) source_info.find_data(path, type: type) end ## # Return the context directory for this tool. Generally, this defaults # to the directory containing the toys config directory structure being # read, but it may be changed by setting a different context directory # for the tool. # # @return [String] Context directory path # @return [nil] if there is no context. # def context_directory cur_tool = DSL::Internal.current_tool(self, false) cur_tool&.context_directory || source_info.context_directory end ## # Return the current tool config. This object can be queried to determine # such information as the name, but it should not be altered. # # @return [Toys::ToolDefinition] # def current_tool DSL::Internal.current_tool(self, false) end ## # Set a custom context directory for this tool. # # @param dir [String] Context directory # @return [self] # def set_context_directory(dir) # rubocop:disable Naming/AccessorMethodName cur_tool = DSL::Internal.current_tool(self, false) cur_tool&.custom_context_directory = dir self end ## # Applies the given block to all subtools, recursively. Effectively, the # given block is run at the *end* of every tool block. This can be used, # for example, to provide some shared configuration for all tools. # # The block is applied only to subtools defined *after* the block # appears. Subtools defined before the block appears are not affected. # # ### Example # # It is common for tools to use the `:exec` mixin to invoke external # programs. This example automatically includes the exec mixin in all # subtools, recursively, so you do not have to repeat the `include` # directive in every tool. # # # .toys.rb # # subtool_apply do # # Include the mixin only if the tool hasn't already done so # unless include?(:exec) # include :exec, exit_on_nonzero_status: true # end # end # # tool "foo" do # def run # # This tool has access to methods defined by the :exec mixin # # because the above block is applied to the tool. # sh "echo hello" # end # end # def subtool_apply(&block) cur_tool = DSL::Internal.current_tool(self, false) return self if cur_tool.nil? cur_tool.subtool_middleware_stack.add(:apply_config, parent_source: source_info, &block) self end ## # Remove lower-priority sources from the load path. This prevents lower- # priority sources (such as Toys files from parent or global directories) # from executing or defining tools. # # This works only if no such sources have already loaded yet. # # @raise [Toys::ToolDefinitionError] if any lower-priority tools have # already been loaded. # def truncate_load_path! unless @__loader.stop_loading_at_priority(@__priority) raise ToolDefinitionError, "Cannot truncate load path because tools have already been loaded" end end ## # Get the settings for this tool. # # @return [Toys::ToolDefinition::Settings] Tool-specific settings. # def settings DSL::Internal.current_tool(self, false)&.settings end ## # Determines whether the current Toys version satisfies the given # requirements. # # @return [Boolean] whether or not the requirements are satisfied # def toys_version?(*requirements) require "rubygems" version = ::Gem::Version.new(Core::VERSION) requirement = ::Gem::Requirement.new(*requirements) requirement.satisfied_by?(version) end ## # Asserts that the current Toys version against the given requirements, # raising an exception if not. # # @return [self] # # @raise [Toys::ToolDefinitionError] if the current Toys version does not # satisfy the requirements. # def toys_version!(*requirements) require "rubygems" version = ::Gem::Version.new(Core::VERSION) requirement = ::Gem::Requirement.new(*requirements) unless requirement.satisfied_by?(version) raise Toys::ToolDefinitionError, "Toys version requirements #{requirement} not satisfied by #{version}" end self end ## # Notify the tool definition when a method is defined in this tool class. # # @private # def method_added(_meth) super DSL::Internal.current_tool(self, true)&.check_definition_state(is_method: true) end ## # Include the tool name in the class inspection dump. # # @private # def inspect return super unless defined? @__words name = @__words.empty? ? "(root)" : @__words.join(" ").inspect id = object_id.to_s(16) "#" end end end end toys-core-0.21.0/lib/toys/dsl/base.rb0000644000004100000410000000527615165170677017364 0ustar www-datawww-data# frozen_string_literal: true ## # Create a base class for defining a tool with a given name. # # This method returns a base class for defining a tool with a given name. # This is useful if the naming behavior of {Toys::Tool} is not adequate for # your tool. # # ### Example # # class FooBar < Toys.Tool("Foo_Bar") # desc "This is a tool called Foo_Bar" # # def run # puts "Foo_Bar called" # end # end # # @param name [String] Name of the tool. Defaults to a name inferred from the # class name. (See {Toys::Tool}.) # @param base [Class] Use this tool class as the base class, and inherit helper # methods from it. # @param args [String,Class] Any string-valued positional argument is # interpreted as the name. Any class-valued positional argument is # interpreted as the base class. # def Toys.Tool(*args, name: nil, base: nil) # rubocop:disable Naming/MethodName args.each do |arg| case arg when ::Class raise ::ArgumentError, "Both base keyword argument and class-valud argument received" if base base = arg when ::String, ::Symbol raise ::ArgumentError, "Both name keyword argument and string-valud argument received" if name name = arg else raise ::ArgumentError, "Unrecognized argument: #{arg}" end end if base && !base.ancestors.include?(::Toys::Context) raise ::ArgumentError, "Base class must itself be a tool" end return base || ::Toys::Tool if name.nil? ::Class.new(base || ::Toys::Context) do base_class = self define_singleton_method(:inherited) do |tool_class| ::Toys::DSL::Internal.configure_class(tool_class, base_class == self ? name.to_s : nil) super(tool_class) ::Toys::DSL::Internal.setup_class_dsl(tool_class) end end end module Toys ## # Base class for defining tools # # This base class provides an alternative to the {Toys::DSL::Tool#tool} # directive for defining tools in the Toys DSL. Creating a subclass of # `Toys::Tool` will create a tool whose name is the "kebab-case" of the class # name. Subclasses can be created only in the context of a tool configuration # DSL. Furthermore, a class-defined tool can be created only at the top level # of a configuration file, or within another class-defined tool. It cannot # be a subtool of a tool block. # # ### Example # # class FooBar < Toys::Tool # desc "This is a tool called foo-bar" # # def run # puts "foo-bar called" # end # end # class Tool < Context ## # @private # def self.inherited(tool_class) DSL::Internal.configure_class(tool_class) super DSL::Internal.setup_class_dsl(tool_class) end end end toys-core-0.21.0/lib/toys/dsl/flag.rb0000644000004100000410000003160315165170677017354 0ustar www-datawww-data# frozen_string_literal: true module Toys module DSL ## # DSL for a flag definition block. Lets you set flag attributes in a block # instead of a long series of keyword arguments. # # These directives are available inside a block passed to # {Toys::DSL::Tool#flag}. # # ### Example # # tool "mytool" do # flag :value do # # The directives in here are defined by this class # flags "--value=VAL" # accept Integer # desc "An integer value" # end # # ... # end # class Flag ## # Add flags in OptionParser format. This may be called multiple times, # and the results are cumulative. # # Following are examples of valid syntax. # # * `-a` : A short boolean switch. When this appears as an argument, # the value is set to `true`. # * `--abc` : A long boolean switch. When this appears as an argument, # the value is set to `true`. # * `-aVAL` or `-a VAL` : A short flag that takes a required value. # These two forms are treated identically. If this argument appears # with a value attached (e.g. `-afoo`), the attached string (e.g. # `"foo"`) is taken as the value. Otherwise, the following argument # is taken as the value (e.g. for `-a foo`, the value is set to # `"foo"`.) The following argument is treated as the value even if it # looks like a flag (e.g. `-a -a` causes the string `"-a"` to be # taken as the value.) # * `-a[VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the value # is set to `true`. The following argument is never interpreted as # the value. (Compare with `-a [VAL]`.) # * `-a [VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, if the # following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `-a foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `-a[VAL]`.) # * `--abc=VAL` or `--abc VAL` : A long flag that takes a required # value. These two forms are treated identically. If this argument # appears with a value attached (e.g. `--abc=foo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the # following argument is taken as the value (e.g. for `--abc foo`, the # value is set to `"foo"`.) The following argument is treated as the # value even if it looks like a flag (e.g. `--abc --def` causes the # string `"--def"` to be taken as the value.) # * `--abc[=VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, # the value is set to `true`. The following argument is never # interpreted as the value. (Compare with `--abc [VAL]`.) # * `--abc [VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, if # the following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `--abc foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `--abc=[VAL]`.) # * `--[no-]abc` : A long boolean switch that can be turned either on # or off. This effectively creates two flags, `--abc` which sets the # value to `true`, and `--no-abc` which sets the falue to `false`. # # @param flags [String...] # @return [self] # def flags(*flags) @flags += flags.flatten self end ## # Set the acceptor for this flag's values. # You can pass either the string name of an acceptor defined in this tool # or any of its ancestors, or any other specification recognized by # {Toys::Acceptor.create}. # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def accept(spec = nil, **options, &block) @acceptor = Acceptor.scalarize_spec(spec, options, block) self end ## # Set the default value. # # @param default [Object] # @return [self] # def default(default) @default = default self end ## # Set the optional handler that customizes how a value is set or updated # when the flag is parsed. # # A handler is a proc that takes up to three arguments: the given value, # the previous value, and a hash containing all the data collected so far # during argument parsing. It must return the new value for the flag. You # You may pass the handler as a Proc (or an object responding to the # `call` method) or you may provide a block. # # You may also specify a predefined named handler. The `:set` handler # (the default) replaces the previous value (effectively # `-> (val) { val }`). The `:push` handler expects the previous value to # be an array and pushes the given value onto it; it should be combined # with setting the default value to `[]` and is intended for # "multi-valued" flags. # # @param handler [Proc,:set,:push] # @param block [Proc] # @return [self] # def handler(handler = nil, &block) @handler = handler || block self end ## # Set the shell completion strategy for flag names. # You can pass one of the following: # # * The string name of a completion defined in this tool or any of its # ancestors. # * A hash of options to pass to the constructor of # {Toys::Flag::DefaultCompletion}. # * `nil` or `:default` to select the standard completion strategy # (which is {Toys::Flag::DefaultCompletion} with no extra options). # * Any other specification recognized by {Toys::Completion.create}. # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def complete_flags(spec = nil, **options, &block) @flag_completion = Completion.scalarize_spec(spec, options, block) self end ## # Set the shell completion strategy for flag values. # You can pass either the string name of a completion defined in this # tool or any of its ancestors, or any other specification recognized by # {Toys::Completion.create}. # # @param spec [Object] # @param options [Hash] # @param block [Proc] # @return [self] # def complete_values(spec = nil, **options, &block) @value_completion = Completion.scalarize_spec(spec, options, block) self end ## # Set whether to raise an exception if a flag is requested that is # already in use or marked as disabled. # # @param setting [Boolean] # @return [self] # def report_collisions(setting) @report_collisions = setting self end ## # Set the short description for the current flag. The short description # is displayed with the flag in online help. # # The description is a {Toys::WrappableString}, which may be word-wrapped # when displayed in a help screen. You may pass a {Toys::WrappableString} # directly to this method, or you may pass any input that can be used to # construct a wrappable string: # # * If you pass a String, its whitespace will be compacted (i.e. tabs, # newlines, and multiple consecutive whitespace will be turned into a # single space), and it will be word-wrapped on whitespace. # * If you pass an Array of Strings, each string will be considered a # literal word that cannot be broken, and wrapping will be done # across the strings in the array. In this case, whitespace is not # compacted. # # ### Examples # # If you pass in a sentence as a simple string, it may be word wrapped # when displayed: # # desc "This sentence may be wrapped." # # To specify a sentence that should never be word-wrapped, pass it as the # sole element of a string array: # # desc ["This sentence will not be wrapped."] # # @param desc [String,Array,Toys::WrappableString] # @return [self] # def desc(desc) @desc = desc self end ## # Add to the long description for the current flag. The long description # is displayed with the flag in online help. This directive may be given # multiple times, and the results are cumulative. # # A long description is a series of descriptions, which are generally # displayed in a series of lines/paragraphs. Each individual description # uses the form described in the {#desc} documentation, and may be # word-wrapped when displayed. To insert a blank line, include an empty # string as one of the descriptions. # # ### Example # # long_desc "This initial paragraph might get word wrapped.", # "This next paragraph is followed by a blank line.", # "", # ["This line will not be wrapped."], # [" This indent is preserved."] # long_desc "This line is appended to the description." # # @param long_desc [String,Array,Toys::WrappableString...] # @return [self] # def long_desc(*long_desc) @long_desc += long_desc self end ## # Set the group. A group may be set by name or group object. Setting # `nil` selects the default group. # # @param group [String,Symbol,Toys::FlagGroup,nil] # @return [self] # def group(group) @group = group self end ## # Set the display name for this flag. This may be used in help text and # error messages. # # @param display_name [String] # @return [self] # def display_name(display_name) @display_name = display_name self end ## # Specify whether to add a method for this flag. # # Recognized values are true to force creation of a method, false to # disable method creation, and nil for the default behavior. The default # checks the name and adds a method if the name is a symbol representing # a legal method name that starts with a letter and does not override any # public method in the Ruby Object class or collide with any method # directly defined in the tool class. # # @param value [true,false,nil] # def add_method(value) @add_method = if value.nil? nil elsif value true else false end end ## # Called only from DSL::Tool # # @private # def initialize(flags, acceptor, default, handler, flag_completion, value_completion, report_collisions, group, desc, long_desc, display_name, method_flag) @flags = flags @default = default @handler = handler @report_collisions = report_collisions @group = group @desc = desc @long_desc = long_desc || [] @display_name = display_name accept(acceptor) complete_flags(flag_completion, **{}) complete_values(value_completion, **{}) add_method(method_flag) end ## # @private # def _add_to(tool, key) tool.add_flag(key, @flags, accept: @acceptor, default: @default, handler: @handler, complete_flags: @flag_completion, complete_values: @value_completion, report_collisions: @report_collisions, group: @group, desc: @desc, long_desc: @long_desc, display_name: @display_name) end ## # @private # def _get_add_method @add_method end end end end toys-core-0.21.0/lib/toys/dsl/flag_group.rb0000644000004100000410000003417415165170677020576 0ustar www-datawww-data# frozen_string_literal: true module Toys module DSL ## # DSL for a flag group definition block. Lets you create flags in a group. # # These directives are available inside a block passed to # {Toys::DSL::Tool#flag_group}, {Toys::DSL::Tool#all_required}, # {Toys::DSL::Tool#at_most_one}, {Toys::DSL::Tool#at_least_one}, or # {Toys::DSL::Tool#exactly_one}. # # ### Example # # tool "login" do # all_required do # # The directives in here are defined by this class # flag :username, "--username=VAL", desc: "Set username (required)" # flag :password, "--password=VAL", desc: "Set password (required)" # end # # ... # end # class FlagGroup ## # Add a flag to the current group. Each flag must specify a key which # the script may use to obtain the flag value from the context. # You may then provide the flags themselves in OptionParser form. # # If the given key is a symbol representing a valid method name, then a # helper method is automatically added to retrieve the value. Otherwise, # if the key is a string or does not represent a valid method name, the # tool can retrieve the value by calling {Toys::Context#get}. # # Attributes of the flag may be passed in as arguments to this method, or # set in a block passed to this method. If you provide a block, you can # use directives in {Toys::DSL::Flag} within the block. # # ### Flag syntax # # The flags themselves should be provided in OptionParser form. Following # are examples of valid syntax. # # * `-a` : A short boolean switch. When this appears as an argument, # the value is set to `true`. # * `--abc` : A long boolean switch. When this appears as an argument, # the value is set to `true`. # * `-aVAL` or `-a VAL` : A short flag that takes a required value. # These two forms are treated identically. If this argument appears # with a value attached (e.g. `-afoo`), the attached string (e.g. # `"foo"`) is taken as the value. Otherwise, the following argument # is taken as the value (e.g. for `-a foo`, the value is set to # `"foo"`.) The following argument is treated as the value even if it # looks like a flag (e.g. `-a -a` causes the string `"-a"` to be # taken as the value.) # * `-a[VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the value # is set to `true`. The following argument is never interpreted as # the value. (Compare with `-a [VAL]`.) # * `-a [VAL]` : A short flag that takes an optional value. If this # argument appears with a value attached (e.g. `-afoo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, if the # following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `-a foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `-a[VAL]`.) # * `--abc=VAL` or `--abc VAL` : A long flag that takes a required # value. These two forms are treated identically. If this argument # appears with a value attached (e.g. `--abc=foo`), the attached # string (e.g. `"foo"`) is taken as the value. Otherwise, the # following argument is taken as the value (e.g. for `--abc foo`, the # value is set to `"foo"`.) The following argument is treated as the # value even if it looks like a flag (e.g. `--abc --def` causes the # string `"--def"` to be taken as the value.) # * `--abc[=VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, # the value is set to `true`. The following argument is never # interpreted as the value. (Compare with `--abc [VAL]`.) # * `--abc [VAL]` : A long flag that takes an optional value. If this # argument appears with a value attached (e.g. `--abc=foo`), the # attached string (e.g. `"foo"`) is taken as the value. Otherwise, if # the following argument does not look like a flag (i.e. it does not # begin with a hyphen), it is taken as the value. (e.g. `--abc foo` # causes the string `"foo"` to be taken as the value.). If there is # no following argument, or the following argument looks like a flag, # the value is set to `true`. (Compare with `--abc=[VAL]`.) # * `--[no-]abc` : A long boolean switch that can be turned either on # or off. This effectively creates two flags, `--abc` which sets the # value to `true`, and `--no-abc` which sets the falue to `false`. # # ### Default flag syntax # # If no flag syntax strings are provided, a default syntax will be # inferred based on the key and other options. # # Specifically, if the key has one character, then that character will be # chosen as a short flag. If the key has multiple characters, a long flag # will be generated. # # Furthermore, if a custom completion, a non-boolean acceptor, or a # non-boolean default value is provided in the options, then the flag # will be considered to take a value. Otherwise, it will be considered to # be a boolean switch. # # For example, the following pairs of flags are identical: # # flag :a # flag :a, "-a" # # flag :abc_def # flag :abc_def, "--abc-def" # # flag :number, accept: Integer # flag :number, "--number=VAL", accept: Integer # # ### More examples # # A flag that sets its value to the number of times it appears on the # command line: # # flag :verbose, "-v", "--verbose", # default: 0, handler: ->(_val, count) { count + 1 } # # An example using block form: # # flag :shout do # flags "-s", "--shout" # default false # desc "Say it louder" # long_desc "This flag says it lowder.", # "You might use this when people can't hear you.", # "", # "Example:", # [" toys say --shout hello"] # end # # @param key [String,Symbol] The key to use to retrieve the value from # the execution context. # @param flags [String...] The flags in OptionParser format. # @param accept [Object] An acceptor that validates and/or converts the # value. You may provide either the name of an acceptor you have # defined, or one of the default acceptors provided by OptionParser. # Optional. If not specified, accepts any value as a string. # @param default [Object] The default value. This is the value that will # be set in the context if this flag is not provided on the command # line. Defaults to `nil`. # @param handler [Proc,nil,:set,:push] An optional handler that # customizes how a value is set or updated when the flag is parsed. # A handler is a proc that takes up to three arguments: the given # value, the previous value, and a hash containing all the data # collected so far during argument parsing. The proc must return the # new value for the flag. # You may also specify a predefined named handler. The `:set` handler # (the default) replaces the previous value (effectively # `-> (val) { val }`). The `:push` handler expects the previous value # to be an array and pushes the given value onto it; it should be # combined with setting the default value to `[]` and is intended for # "multi-valued" flags. # @param complete_flags [Object] A specifier for shell tab completion # for flag names associated with this flag. By default, a # {Toys::Flag::DefaultCompletion} is used, which provides the flag's # names as completion candidates. To customize completion, set this # to the name of a previously defined completion, a hash of options # to pass to the constructor for {Toys::Flag::DefaultCompletion}, or # any other spec recognized by {Toys::Completion.create}. # @param complete_values [Object] A specifier for shell tab completion # for flag values associated with this flag. This is the empty # completion by default. To customize completion, set this to the # name of a previously defined completion, or any spec recognized by # {Toys::Completion.create}. # @param report_collisions [Boolean] Raise an exception if a flag is # requested that is already in use or marked as unusable. Default is # true. # @param desc [String,Array,Toys::WrappableString] Short # description for the flag. See {Toys::DSL::Tool#desc} for a # description of the allowed formats. Defaults to the empty string. # @param long_desc [Array,Toys::WrappableString>] # Long description for the flag. See {Toys::DSL::Tool#long_desc} for # a description of the allowed formats. (But note that this param # takes an Array of description lines, rather than a series of # arguments.) Defaults to the empty array. # @param display_name [String] A display name for this flag, used in help # text and error messages. # @param add_method [true,false,nil] Whether to add a method for this # flag. If omitted or set to nil, uses the default behavior, which # adds the method if the key is a symbol representing a legal method # name that starts with a letter and does not override any public # method in the Ruby Object class or collide with any method directly # defined in the tool class. # @param block [Proc] Configures the flag. See {Toys::DSL::Flag} for the # directives that can be called in this block. # @return [self] # def flag(key, *flags, accept: nil, default: nil, handler: nil, complete_flags: nil, complete_values: nil, report_collisions: true, desc: nil, long_desc: nil, display_name: nil, add_method: nil, &block) flag_dsl = DSL::Flag.new(flags, accept, default, handler, complete_flags, complete_values, report_collisions, @flag_group, desc, long_desc, display_name, add_method) flag_dsl.instance_exec(flag_dsl, &block) if block flag_dsl._add_to(@tool, key) DSL::Internal.maybe_add_getter(@tool_dsl, key, flag_dsl._get_add_method) self end ## # Set the short description for the current flag group. The short # description is displayed as the group title in online help. # # The description is a {Toys::WrappableString}, which may be word-wrapped # when displayed in a help screen. You may pass a {Toys::WrappableString} # directly to this method, or you may pass any input that can be used to # construct a wrappable string: # # * If you pass a String, its whitespace will be compacted (i.e. tabs, # newlines, and multiple consecutive whitespace will be turned into a # single space), and it will be word-wrapped on whitespace. # * If you pass an Array of Strings, each string will be considered a # literal word that cannot be broken, and wrapping will be done # across the strings in the array. In this case, whitespace is not # compacted. # # ### Examples # # If you pass in a sentence as a simple string, it may be word wrapped # when displayed: # # desc "This sentence may be wrapped." # # To specify a sentence that should never be word-wrapped, pass it as the # sole element of a string array: # # desc ["This sentence will not be wrapped."] # # @param desc [String,Array,Toys::WrappableString] # @return [self] # def desc(desc) @flag_group.desc = desc self end ## # Add to the long description for the current flag group. The long # description is displayed with the flag group in online help. This # directive may be given multiple times, and the results are cumulative. # # A long description is a series of descriptions, which are generally # displayed in a series of lines/paragraphs. Each individual description # uses the form described in the {#desc} documentation, and may be # word-wrapped when displayed. To insert a blank line, include an empty # string as one of the descriptions. # # ### Example # # long_desc "This initial paragraph might get word wrapped.", # "This next paragraph is followed by a blank line.", # "", # ["This line will not be wrapped."], # [" This indent is preserved."] # long_desc "This line is appended to the description." # # @param long_desc [String,Array,Toys::WrappableString...] # @return [self] # def long_desc(*long_desc) @flag_group.append_long_desc(long_desc) self end ## # Called only from DSL::Tool. # # @private # def initialize(tool_dsl, tool, flag_group) @tool_dsl = tool_dsl @tool = tool @flag_group = flag_group end end end end toys-core-0.21.0/lib/toys/utils/0000755000004100000410000000000015165170677016471 5ustar www-datawww-datatoys-core-0.21.0/lib/toys/utils/terminal.rb0000644000004100000410000003254215165170677020637 0ustar www-datawww-data# frozen_string_literal: true begin require "io/console" rescue ::LoadError # TODO: alternate methods of getting terminal size end module Toys module Utils ## # A simple terminal class. # # ### Styles # # This class supports ANSI styled output where supported. # # Styles may be specified in any of the following forms: # * A symbol indicating the name of a well-known style, or the name of # a defined style. # * An rgb string in hex "rgb" or "rrggbb" form. # * An ANSI code string in `\e[XXm` form. # * An array of ANSI codes as integers. # class Terminal ## # Fatal terminal error. # class TerminalError < ::StandardError end ## # ANSI style code to clear styles # @return [String] # CLEAR_CODE = "\e[0m" ## # Standard ANSI style codes by name. # @return [Hash{Symbol => Array}] # BUILTIN_STYLE_NAMES = { clear: [0], reset: [0], bold: [1], faint: [2], italic: [3], underline: [4], blink: [5], reverse: [7], black: [30], red: [31], green: [32], yellow: [33], blue: [34], magenta: [35], cyan: [36], white: [37], on_black: [30], on_red: [31], on_green: [32], on_yellow: [33], on_blue: [34], on_magenta: [35], on_cyan: [36], on_white: [37], bright_black: [90], bright_red: [91], bright_green: [92], bright_yellow: [93], bright_blue: [94], bright_magenta: [95], bright_cyan: [96], bright_white: [97], on_bright_black: [100], on_bright_red: [101], on_bright_green: [102], on_bright_yellow: [103], on_bright_blue: [104], on_bright_magenta: [105], on_bright_cyan: [106], on_bright_white: [107], }.freeze ## # Default length of a single spinner frame, in seconds. # @return [Float] # DEFAULT_SPINNER_FRAME_LENGTH = 0.1 ## # Default set of spinner frames. # @return [Array] # DEFAULT_SPINNER_FRAMES = ["-", "\\", "|", "/"].freeze ## # Returns a copy of the given string with all ANSI style codes removed. # # @param str [String] Input string # @return [String] String with styles removed # def self.remove_style_escapes(str) str.to_s.gsub(/\e\[\d+(;\d+)*m/, "") end ## # Create a terminal. # # @param input [IO,nil] Input stream. # @param output [IO,Logger,nil] Output stream or logger. # @param styled [Boolean,nil] Whether to output ansi styles. If `nil`, the # setting is inferred from whether the output has a tty. # def initialize(input: $stdin, output: $stdout, styled: nil) require "monitor" @input = input @output = output @styled = if styled.nil? output.respond_to?(:tty?) && output.tty? && !::ENV["NO_COLOR"] else styled ? true : false end @named_styles = BUILTIN_STYLE_NAMES.dup @output_mutex = ::Monitor.new @input_mutex = ::Monitor.new end ## # Output stream or logger # @return [IO,Logger,nil] # attr_reader :output ## # Input stream # @return [IO,nil] # attr_reader :input ## # Whether output is styled # @return [Boolean] # attr_reader :styled ## # Write a partial line without appending a newline. # # @param str [String] The line to write # @param styles [Symbol,String,Array...] Styles to apply to the # partial line. # @return [self] # def write(str = "", *styles) @output_mutex.synchronize do output&.write(apply_styles(str.to_s, *styles)) output&.flush rescue ::IOError nil end self end ## # Read a line, blocking until one is available. # # @return [String] the entire string including the temrinating newline # @return [nil] if the input is closed or at eof, or there is no input # def readline @input_mutex.synchronize do input&.gets rescue ::IOError nil end end ## # This method is defined so that `::Logger` will recognize a terminal as # a log device target, but it does not actually close anything. # def close nil end ## # Write a line, appending a newline if one is not already present. # # @param str [String] The line to write # @param styles [Symbol,String,Array...] Styles to apply to the # entire line. # @return [self] # def puts(str = "", *styles) str = "#{str}\n" unless str.to_s.end_with?("\n") write(str, *styles) end alias say puts ## # Write a line, appending a newline if one is not already present. # # @param str [String] The line to write # @return [self] # def <<(str) puts(str) end ## # Write a newline and flush the current line. # @return [self] # def newline puts end ## # Ask a question and get a response. # # @param prompt [String] Required prompt string. # @param styles [Symbol,String,Array...] Styles to apply to the # prompt. # @param default [String,nil] Default value, or `nil` for no default. # Uses `nil` if not specified. # @param trailing_text [:default,String,nil] Trailing text appended to # the prompt, `nil` for none, or `:default` to show the default. # @return [String] # def ask(prompt, *styles, default: nil, trailing_text: :default) if trailing_text == :default trailing_text = default.nil? ? nil : "[#{default}]" end if trailing_text ptext, pspaces, = prompt.to_s.partition(/\s+$/) prompt = "#{ptext} #{trailing_text}#{pspaces}" end write(prompt, *styles) resp = readline.to_s.chomp resp.empty? ? default.to_s : resp end ## # Confirm with the user. # # @param prompt [String] Prompt string. Defaults to `"Proceed?"`. # @param styles [Symbol,String,Array...] Styles to apply to the # prompt. # @param default [Boolean,nil] Default value, or `nil` for no default. # Uses `nil` if not specified. # @return [Boolean] # def confirm(prompt = "Proceed? ", *styles, default: nil) default_val, trailing_text = case default when true ["y", "(Y/n)"] when false ["n", "(y/N)"] else [nil, "(y/n)"] end resp = ask(prompt, *styles, default: default_val, trailing_text: trailing_text) return true if resp =~ /^y/i return false if resp =~ /^n/i if resp.nil? && default.nil? raise TerminalError, "Cannot confirm because the input stream is at eof." end if !resp.strip.empty? || default.nil? if input.nil? raise TerminalError, "Cannot confirm because there is no input stream." end confirm('Please answer "y" or "n"', default: default) else default end end ## # Display a spinner during a task. You should provide a block that # performs the long-running task. While the block is executing, a # spinner will be displayed. # # @param leading_text [String] Optional leading string to display to the # left of the spinner. Default is the empty string. # @param frame_length [Float] Length of a single frame, in seconds. # Defaults to {DEFAULT_SPINNER_FRAME_LENGTH}. # @param frames [Array] An array of frames. Defaults to # {DEFAULT_SPINNER_FRAMES}. # @param style [Symbol,Array] A terminal style or array of styles # to apply to all frames in the spinner. Defaults to empty, # @param final_text [String] Optional final string to display when the # spinner is complete. Default is the empty string. A common practice # is to set this to newline. # @return [Object] The return value of the block. # def spinner(leading_text: "", final_text: "", frame_length: nil, frames: nil, style: nil) return nil unless block_given? frame_length ||= DEFAULT_SPINNER_FRAME_LENGTH frames ||= DEFAULT_SPINNER_FRAMES write(leading_text) unless leading_text.to_s.empty? spin = SpinDriver.new(self, frames, Array(style), frame_length) begin yield ensure spin.stop write(final_text) unless final_text.to_s.empty? end end ## # Return the terminal size as an array of width, height. # # @return [Array(Integer,Integer)] # def size if output.respond_to?(:tty?) && output.tty? && output.respond_to?(:winsize) output.winsize.reverse else [80, 25] end end ## # Return the terminal width # # @return [Integer] # def width size[0] end ## # Return the terminal height # # @return [Integer] # def height size[1] end ## # Define a named style. # # Style names must be symbols. # The definition of a style may include any valid style specification, # including the symbol names of existing defined styles. # # @param name [Symbol] The name for the style # @param styles [Symbol,String,Array...] # @return [self] # def define_style(name, *styles) @named_styles[name] = resolve_styles(*styles) self end ## # Apply the given styles to the given string, returning the styled # string. Honors the styled setting; if styling is disabled, does not # add any ANSI style codes and in fact removes any existing codes. If # styles were added, ensures that the string ends with a clear code. # # @param str [String] String to style # @param styles [Symbol,String,Array...] Styles to apply # @return [String] The styled string # def apply_styles(str, *styles) if styled prefix = escape_styles(*styles) suffix = prefix.empty? || str.to_s.end_with?(CLEAR_CODE) ? "" : CLEAR_CODE "#{prefix}#{str}#{suffix}" else Terminal.remove_style_escapes(str) end end private ## # Resolve a style to an ANSI style escape sequence. # def escape_styles(*styles) codes = resolve_styles(*styles) codes.empty? ? "" : "\e[#{codes.join(';')}m" end ## # Resolve a style to an array of ANSI style codes (integers). # def resolve_styles(*styles) result = [] styles.each do |style| codes = case style when ::Array style when ::String interpret_style_string(style) when ::Symbol @named_styles[style] end raise ::ArgumentError, "Unknown style code: #{s.inspect}" unless codes result.concat(codes) end result end ## # Transform various style string formats into a list of style codes. # def interpret_style_string(style) case style when /^[0-9a-fA-F]{6}$/ rgb = style.to_i(16) [38, 2, rgb >> 16, (rgb & 0xff00) >> 8, rgb & 0xff] when /^[0-9a-fA-F]{3}$/ rgb = style.to_i(16) [38, 2, (rgb >> 8) * 0x11, ((rgb & 0xf0) >> 4) * 0x11, (rgb & 0xf) * 0x11] when /^\e\[([\d;]+)m$/ ::Regexp.last_match(1).split(";").map(&:to_i) end end ## # @private # class SpinDriver ## # @private # def initialize(terminal, frames, style, frame_length) @mutex = ::Monitor.new @terminal = terminal @frames = frames.map do |f| [@terminal.apply_styles(f, *style), Terminal.remove_style_escapes(f).size] end @frame_length = frame_length @cur_frame = 0 @stopping = false @cond = @mutex.new_cond @thread = @terminal.output.tty? ? start_thread : nil end ## # @private # def stop @mutex.synchronize do @stopping = true @cond.broadcast end @thread&.join self end private def start_thread ::Thread.new do @mutex.synchronize do until @stopping @terminal.write(@frames[@cur_frame][0]) @cond.wait(@frame_length) size = @frames[@cur_frame][1] @terminal.write(("\b" * size) + (" " * size) + ("\b" * size)) @cur_frame += 1 @cur_frame = 0 if @cur_frame >= @frames.size end end end end end end end end toys-core-0.21.0/lib/toys/utils/standard_ui.rb0000644000004100000410000002132715165170677021320 0ustar www-datawww-data# frozen_string_literal: true require "logger" module Toys module Utils ## # An object that implements standard UI elements, such as error reports and # logging, as provided by the `toys` command line. Specifically, it # implements pretty formatting of log entries and stack traces, and renders # using ANSI coloring where available via {Toys::Utils::Terminal}. # # This object can be used to implement `toys`-style behavior when creating # a CLI object. For example: # # require "toys/utils/standard_ui" # ui = Toys::Utils::StandardUI.new # cli = Toys::CLI.new(**ui.cli_args) # class StandardUI ## # Create a Standard UI. # # By default, all output is written to `$stderr`, and will share a single # {Toys::Utils::Terminal} object, allowing multiple tools and/or threads # to interleave messages without interrupting one another. # # @param output [IO,Toys::Utils::Terminal] Where to write output. You can # pass a terminal object, or an IO stream that will be wrapped in a # terminal output. Default is `$stderr`. # def initialize(output: nil) require "toys/utils/terminal" @terminal = output || $stderr @terminal = Terminal.new(output: @terminal) unless @terminal.is_a?(Terminal) @log_header_severity_styles = { "FATAL" => [:bright_magenta, :bold, :underline], "ERROR" => [:bright_red, :bold], "WARN" => [:bright_yellow], "INFO" => [:bright_cyan], "DEBUG" => [:white], } end ## # The terminal underlying this UI # # @return [Toys::Utils::Terminal] # attr_reader :terminal ## # A hash that maps severities to styles recognized by # {Toys::Utils::Terminal}. Used to style the header for each log entry. # This hash can be modified in place to adjust the behavior of loggers # created by this UI. # # @return [Hash{String => Array}] # attr_reader :log_header_severity_styles ## # Convenience method that returns a hash of arguments that can be passed # to the {Toys::CLI} constructor. Includes the `:error_handler` and # `:logger_factory` arguments. # # @return [Hash] # def cli_args { error_handler: error_handler, logger_factory: logger_factory, } end ## # Returns an error handler conforming to the `:error_handler` argument to # the {Toys::CLI} constructor. Specifically, it returns the # {#error_handler_impl} method as a proc. # # @return [Proc] # def error_handler @error_handler ||= method(:error_handler_impl).to_proc end ## # Returns a logger factory conforming to the `:logger_factory` argument # to the {Toys::CLI} constructor. Specifically, it returns the # {#logger_factory_impl} method as a proc. # # @return [Proc] # def logger_factory @logger_factory ||= method(:logger_factory_impl).to_proc end ## # Implementation of the error handler. As dictated by the error handler # specification in {Toys::CLI}, this must take a {Toys::ContextualError} # as an argument, and return an exit code. # # The base implementation uses {#display_error_notice} and # {#display_signal_notice} to print an appropriate message to the UI's # terminal, and uses {#exit_code_for} to determine the correct exit code. # Any of those methods can be overridden by a subclass to alter their # behavior, or this main implementation method can be overridden to # change the overall behavior. # # @param error [Toys::ContextualError] The error received # @return [Integer] The exit code # def error_handler_impl(error) cause = error.cause if cause.is_a?(::SignalException) display_signal_notice(cause) else display_error_notice(error) end exit_code_for(cause) end ## # Implementation of the logger factory. As dictated by the logger factory # specification in {Toys::CLI}, this must take a {Toys::ToolDefinition} # as an argument, and return a `Logger`. # # The base implementation returns a logger that writes to the UI's # terminal, using {#logger_formatter_impl} as the formatter. It sets the # level to `Logger::WARN` by default. Either this method or the helper # methods can be overridden to change this behavior. # # @param _tool {Toys::ToolDefinition} The tool definition of the tool to # be executed # @return [Logger] # def logger_factory_impl(_tool) logger = ::Logger.new(@terminal) logger.formatter = method(:logger_formatter_impl).to_proc logger.level = ::Logger::WARN logger end ## # Returns an exit code appropriate for the given exception. Currently, # the logic interprets signals (returning the convention of 128 + signo), # usage errors (returning the conventional value of 2), and tool not # runnable errors (returning the conventional value of 126), and defaults # to 1 for all other error types. # # This method is used by {#error_handler_impl} and can be overridden to # change its behavior. # # @param error [Exception] The exception raised. This method expects the # original exception, rather than a ContextualError. # @return [Integer] The appropriate exit code # def exit_code_for(error) case error when ArgParsingError 2 when NotRunnableError 126 when ::SignalException error.signo + 128 else 1 end end ## # Displays a default output for a signal received. # # This method is used by {#error_handler_impl} and can be overridden to # change its behavior. # # @param error [SignalException] # def display_signal_notice(error) @terminal.puts if error.is_a?(::Interrupt) @terminal.puts("INTERRUPTED", :bold) else @terminal.puts("SIGNAL RECEIVED: #{error.signm || error.signo}", :bold) end end ## # Displays a default output for an error. Displays the error, the # backtrace, and contextual information regarding what tool was run and # where in its code the error occurred. # # This method is used by {#error_handler_impl} and can be overridden to # change its behavior. # # @param error [Toys::ContextualError] # def display_error_notice(error) @terminal.puts @terminal.puts(cause_string(error.cause)) @terminal.puts(context_string(error), :bold) end ## # Implementation of the formatter used by loggers created by this UI's # logger factory. This interface is defined by the standard `Logger` # class. # # This method can be overridden to change the behavior of loggers created # by this UI. # # @param severity [String] # @param time [Time] # @param _progname [String] # @param msg [Object] # @return [String] # def logger_formatter_impl(severity, time, _progname, msg) msg_str = case msg when ::String msg when ::Exception "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n") else msg.inspect end timestr = time.strftime("%Y-%m-%d %H:%M:%S") header = format("[%