Class: Ukiryu::Tool
- Inherits:
-
Object
- Object
- Ukiryu::Tool
- Defined in:
- lib/ukiryu/tool.rb,
lib/ukiryu/tool/loader.rb,
lib/ukiryu/tool/version_detection.rb,
lib/ukiryu/tool/command_resolution.rb,
lib/ukiryu/tool/executable_discovery.rb
Overview
Tool wrapper class for external command-line tools
Provides a Ruby interface to external CLI tools defined in YAML profiles.
## Usage
### Traditional API (backward compatible)
tool = Ukiryu::Tool.get(:imagemagick)
tool.execute(:convert, inputs: ["image.png"], resize: "50%")
### New OOP API (recommended)
# Lazy autoload - creates Ukiryu::Tools::Imagemagick class on first access
Ukiryu::Tools::Imagemagick.new.tap do |tool|
= tool.(:convert)
.set(inputs: ["image.png"], resize: "50%")
.output = "output.jpg"
.run
end
Defined Under Namespace
Modules: CommandResolution, ExecutableDiscovery, Loader, VersionDetection
Instance Attribute Summary collapse
-
#definition_source ⇒ Definition::Source?
readonly
Get the definition source if loaded from non-register source.
-
#executable ⇒ String
readonly
Get the executable path.
-
#executable_info ⇒ Models::ExecutableInfo?
readonly
Get the executable discovery information.
-
#profile ⇒ Hash
readonly
Get the raw profile data.
Class Method Summary collapse
-
.bundled_definition_search_paths ⇒ Array<String>
Get bundled definition search paths.
-
.clear_cache ⇒ Object
Clear the tool cache.
-
.clear_definition_cache ⇒ Object
Clear the definition cache only.
-
.configure(options = {}) ⇒ Object
Configure default options.
-
.convert_actions_to_array(actions_data) ⇒ Array<Hash>
Convert actions hash to array format.
-
.convert_hash_to_command_definition(cmd_hash) ⇒ CommandDefinition
Convert hash to CommandDefinition object.
-
.convert_profile_to_hash(profile, actions) ⇒ Hash
Convert ExecutionProfile to hash format for ToolDefinition.
-
.convert_profile_to_platform_profile(profile, actions) ⇒ PlatformProfile
Convert ExecutionProfile to PlatformProfile object for ToolDefinition.
-
.convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version, options = {}) ⇒ ToolDefinition
Convert ImplementationVersion to ToolDefinition for compatibility.
-
.deep_merge_hashes(base, override) ⇒ Hash
Deep merge two hashes (second hash takes precedence).
-
.detect_implementation_and_version(index, tool_name, options = {}) ⇒ Hash?
Detect implementation and version from ImplementationIndex.
-
.extract_definition(tool_name, options = {}) ⇒ Hash
Extract tool definition from an installed CLI tool.
-
.extract_version_from_pattern(output, pattern) ⇒ String?
Extract version from command output using pattern.
-
.find(name, options = {}) ⇒ Tool?
Find a tool by name, returning nil if not found.
-
.find_all(tool_name, options = {}) ⇒ Array<Models::ExecutableInfo>
Find all instances of a tool in PATH and aliases.
-
.find_by(identifier, options = {}) ⇒ Tool?
Find a tool by name, alias, or interface.
-
.find_matching_version_spec(versions, detected_version, scheme) ⇒ Hash?
Find matching version spec using versionian.
-
.from_bundled(tool_name, options = {}) ⇒ Tool?
Load a tool from bundled system locations.
-
.from_definition(yaml_string, options = {}) ⇒ Tool
Alias for load_from_string - load from YAML string.
-
.from_file(file_path, options = {}) ⇒ Tool
Alias for load - load from file path.
-
.get(name, options = {}) ⇒ Tool
Get a tool by name using the new ImplementationIndex architecture.
-
.get_class(tool_name) ⇒ Class
Get the tool-specific class (new OOP API).
-
.load(file_path, options = {}) ⇒ Tool
Load a tool definition from a file path.
-
.load_from_string(yaml_string, options = {}) ⇒ Tool
Load a tool definition from a YAML string.
-
.load_with_implementation_index(name, options = {}) ⇒ Tool?
Try loading a tool using the new ImplementationIndex architecture Delegates to Tool::Loader module.
-
.profile_is_modern?(profile_version, version_detection) ⇒ Boolean?
Check if a profile version is “modern” based on version_detection modern_threshold.
-
.run_detection_command(detection, tool_name, options = {}) ⇒ String?
Run detection command for an implementation.
-
.tools_cache ⇒ Cache
Get the tools cache (bounded LRU cache).
-
.try_execute_command(cmd, options = {}) ⇒ String?
Try executing a command and return stdout on success.
Instance Method Summary collapse
-
#available? ⇒ Boolean
Check if the tool is available.
-
#command?(command_name) ⇒ Boolean
Check if a command is available.
-
#command_definition(command_name) ⇒ CommandDefinition?
Get a command definition by name.
-
#commands ⇒ Hash?
Get the commands defined in the active profile.
-
#definition_mtime ⇒ Time?
Get the definition mtime if loaded from file.
-
#definition_path ⇒ String?
Get the definition path if loaded from file.
-
#execute_simple(command_name, execution_timeout:, **params) ⇒ Executor::Result
Execute a command defined in the profile.
-
#execute_with_config(executable, args, command_def, params, execution_timeout:, stdin:) ⇒ Executor::Result
Execute command with common configuration.
-
#initialize(profile, options = {}) ⇒ Tool
constructor
Create a new Tool instance.
-
#name ⇒ String
Get the tool name.
-
#normalize_params(params) ⇒ Hash
Normalize params to hash.
-
#options_for(command_name) ⇒ Class
Get the options class for a command.
-
#routing ⇒ Models::Routing?
Get the routing table from the active profile.
-
#routing? ⇒ Boolean
Check if this tool has routing defined.
-
#unavailability_reason ⇒ String?
Get the reason why the tool is not available.
-
#version ⇒ String?
Get the tool version.
-
#version_info ⇒ Models::VersionInfo?
Get the tool version info (full metadata).
Methods included from ExecutableDiscovery
#alias?, #discovery_description, #find_executable, #path_found?
Methods included from CommandResolution
#execute, #execute_action, #find_action_with_parent, #find_command_profile, #resolve_action_path
Methods included from VersionDetection
#check_version_compatibility, #detect_version, #detect_version_with_detection_methods, #probe_flag, #profile_version_requirement
Methods included from CommandBuilder
#build_args, #build_env_vars, #detect_delimiter, #format_arg, #format_flag, #format_option
Constructor Details
#initialize(profile, options = {}) ⇒ Tool
Create a new Tool instance
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 |
# File 'lib/ukiryu/tool.rb', line 818 def initialize(profile, = {}) @profile = profile @options = @definition_source = [:definition_source] runtime = Ukiryu::Runtime.instance # Allow override via options for testing @platform = [:platform]&.to_sym || runtime.platform @shell = [:shell]&.to_sym || runtime.shell @version = [:version] # Find compatible profile @command_profile = find_command_profile raise Ukiryu::Errors::ProfileNotFoundError, "No compatible profile for #{name}" unless @command_profile # Find executable @executable = find_executable end |
Instance Attribute Details
#definition_source ⇒ Definition::Source? (readonly)
Get the definition source if loaded from non-register source
873 874 875 |
# File 'lib/ukiryu/tool.rb', line 873 def definition_source @definition_source end |
#executable ⇒ String (readonly)
Get the executable path
892 893 894 |
# File 'lib/ukiryu/tool.rb', line 892 def executable @executable end |
#executable_info ⇒ Models::ExecutableInfo? (readonly)
Get the executable discovery information
897 898 899 |
# File 'lib/ukiryu/tool.rb', line 897 def executable_info @executable_info end |
#profile ⇒ Hash (readonly)
Get the raw profile data
840 841 842 |
# File 'lib/ukiryu/tool.rb', line 840 def profile @profile end |
Class Method Details
.bundled_definition_search_paths ⇒ Array<String>
Get bundled definition search paths
734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 |
# File 'lib/ukiryu/tool.rb', line 734 def bundled_definition_search_paths platform = Ukiryu::Platform.detect paths = case platform when :macos, :linux [ '/usr/share/ukiryu', '/usr/local/share/ukiryu', '/opt/homebrew/share/ukiryu' ] when :windows [ File.('C:/Program Files/Ukiryu'), File.('C:/Program Files (x86)/Ukiryu') ] else [] end # Add user-local path paths << File.('~/.local/share/ukiryu') paths end |
.clear_cache ⇒ Object
Clear the tool cache
639 640 641 |
# File 'lib/ukiryu/tool.rb', line 639 def clear_cache ToolCache.clear end |
.clear_definition_cache ⇒ Object
Clear the definition cache only
646 647 648 |
# File 'lib/ukiryu/tool.rb', line 646 def clear_definition_cache ToolCache.clear_definition_cache end |
.configure(options = {}) ⇒ Object
Configure default options
671 672 673 674 |
# File 'lib/ukiryu/tool.rb', line 671 def configure( = {}) @default_options ||= {} @default_options.merge!() end |
.convert_actions_to_array(actions_data) ⇒ Array<Hash>
Convert actions hash to array format
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/ukiryu/tool.rb', line 503 def convert_actions_to_array(actions_data) return [] if actions_data.nil? || actions_data.empty? # Handle both Hash (old format) and Array (new format from Interface) if actions_data.is_a?(Hash) actions_data.map do |command_name, command_def| # command_def is already a hash, just add the name if not present command_def = command_def.to_h command_def['name'] ||= command_name.to_s command_def end else # Array format - convert to hash and ensure name is set actions_data.map do |command_def| command_def = command_def.to_h # Flatten signature if present (interface format) if command_def[:signature] || command_def['signature'] signature = command_def[:signature] || command_def['signature'] # Merge signature contents into command_def, excluding the signature key itself signature.each do |key, value| # Handle nested structure: signature[:inputs] contains inputs/options/flags if [:inputs, 'inputs'].include?(key) # If value is a hash, merge its contents directly if value.is_a?(Hash) value.each do |nested_key, nested_value| # Rename 'inputs' to 'arguments' for CommandDefinition compatibility target_key = case nested_key.to_s when 'inputs' then 'arguments' else nested_key.to_s end command_def[target_key.to_sym] = nested_value unless [:signature, 'signature'].include?(nested_key) end else command_def[key] = value end else command_def[key] = value unless [:signature, 'signature'].include?(key) end end command_def.delete(:signature) command_def.delete('signature') end command_def end end end |
.convert_hash_to_command_definition(cmd_hash) ⇒ CommandDefinition
Convert hash to CommandDefinition object
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 |
# File 'lib/ukiryu/tool.rb', line 460 def convert_hash_to_command_definition(cmd_hash) require_relative 'models/command_definition' # Create CommandDefinition from hash = cmd_hash['post_options'] || cmd_hash[:post_options] # Debug logging for Ruby 3.4+ CI if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn "[UKIRYU DEBUG build_command_definition] cmd.name: #{cmd_hash['name'] || cmd_hash[:name]}" warn "[UKIRYU DEBUG build_command_definition] post_options_data: #{.inspect}" warn "[UKIRYU DEBUG build_command_definition] post_options_data.class: #{.class}" if if .is_a?(Array) .first(2).each do |opt| warn "[UKIRYU DEBUG build_command_definition] post_option: #{opt.inspect}" end end end Models::CommandDefinition.new( name: cmd_hash['name'] || cmd_hash[:name], description: cmd_hash['description'] || cmd_hash[:description], usage: cmd_hash['usage'] || cmd_hash[:usage], subcommand: cmd_hash['subcommand'] || cmd_hash[:subcommand], belongs_to: cmd_hash['belongs_to'] || cmd_hash[:belongs_to], cli_flag: cmd_hash['cli_flag'] || cmd_hash[:cli_flag], standalone_executable: cmd_hash['standalone_executable'] || cmd_hash[:standalone_executable] || false, aliases: cmd_hash['aliases'] || cmd_hash[:aliases] || [], use_env_vars: cmd_hash['use_env_vars'] || cmd_hash[:use_env_vars] || [], implements: cmd_hash['implements'] || cmd_hash[:implements] || [], options: cmd_hash['options'] || cmd_hash[:options], flags: cmd_hash['flags'] || cmd_hash[:flags], arguments: cmd_hash['arguments'] || cmd_hash[:arguments], post_options: , env_vars: cmd_hash['env_vars'] || cmd_hash[:env_vars], exit_codes: cmd_hash['exit_codes'] || cmd_hash[:exit_codes] ) end |
.convert_profile_to_hash(profile, actions) ⇒ Hash
Convert ExecutionProfile to hash format for ToolDefinition
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/ukiryu/tool.rb', line 347 def convert_profile_to_hash(profile, actions) # Handle both Hash and ExecutionProfile objects actions_hash = actions || {} commands_array = convert_actions_to_array(actions_hash) if profile.is_a?(Hash) # Use the actions parameter (interface.actions), not profile[:actions] # Convert actions hash to array format expected by ToolDefinition { 'name' => profile[:name] || profile['name'], 'display_name' => profile[:display_name] || profile['display_name'], 'platforms' => profile[:platforms] || profile['platforms'], 'shells' => profile[:shells] || profile['shells'], 'option_style' => profile[:option_style] || profile['option_style'], 'executable_name' => profile[:executable_name] || profile['executable_name'], 'commands' => commands_array } else { 'name' => profile.name, 'display_name' => profile.display_name, 'platforms' => profile.platforms, 'shells' => profile.shells, 'option_style' => profile.option_style, 'executable_name' => profile.executable_name, 'commands' => commands_array } end end |
.convert_profile_to_platform_profile(profile, actions) ⇒ PlatformProfile
Convert ExecutionProfile to PlatformProfile object for ToolDefinition
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
# File 'lib/ukiryu/tool.rb', line 381 def convert_profile_to_platform_profile(profile, actions) require_relative 'models/platform_profile' require_relative 'models/command_definition' # Handle both Hash and ExecutionProfile objects if profile.is_a?(Hash) profile_data = profile profile_commands = profile[:commands] || profile['commands'] || [] else profile_data = { name: profile.name, display_name: profile.display_name, platforms: profile.platforms, shells: profile.shells, option_style: profile.option_style } profile_commands = profile.commands || [] end # Convert interface actions to command definitions hash (by name) interface_commands_hash = {} convert_actions_to_array(actions || []).each do |cmd| cmd_name = cmd[:name] || cmd['name'] interface_commands_hash[cmd_name] = cmd end # Build command definitions by merging interface and profile data # If profile has commands, merge them with interface actions # If profile has no commands, use interface actions directly command_definitions = if profile_commands.nil? || profile_commands.empty? # No profile commands - use interface actions directly interface_commands_hash.map do |_cmd_name, cmd_hash| convert_hash_to_command_definition(cmd_hash) end else # Profile has commands - merge with interface actions profile_commands.map do |cmd_hash| # Command name may be specified as 'name' or 'subcommand' field cmd_name = cmd_hash[:name] || cmd_hash['name'] || cmd_hash[:subcommand] || cmd_hash['subcommand'] # Merge profile command data with interface action data interface_cmd = interface_commands_hash[cmd_name] merged_cmd_hash = if interface_cmd # Deep merge: profile data takes precedence deep_merge_hashes(interface_cmd, cmd_hash) else cmd_hash end convert_hash_to_command_definition(merged_cmd_hash) end end # Create PlatformProfile Models::PlatformProfile.new( **profile_data, commands: command_definitions ) end |
.convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version, options = {}) ⇒ ToolDefinition
Convert ImplementationVersion to ToolDefinition for compatibility
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 |
# File 'lib/ukiryu/tool.rb', line 272 def convert_to_tool_definition(tool_name, interface, impl_version, implementation_name, detected_version, = {}) require_relative 'models/tool_definition' require_relative 'models/platform_profile' # Select compatible execution profile profile = impl_version.compatible_profile( platform: [:platform] || Platform.detect, shell: [:shell] || Shell.detect ) return nil unless profile # Build ToolDefinition from execution profile # Note: implements must be an array for the v2 model # Only append implementation name for non-default implementations if implementation_name && implementation_name != 'default' "#{tool_name}_#{implementation_name}" else tool_name end # Use detected version if available, otherwise fall back to YAML version detected_version || impl_version.version # Build ToolDefinition from execution profile # Note: implements must be an array for the v2 model # Only append implementation name for non-default implementations specific_tool_name = if implementation_name && implementation_name != 'default' "#{tool_name}_#{implementation_name}" else tool_name end # Use detected version if available, otherwise fall back to YAML version version = detected_version || impl_version.version tool_def = Models::ToolDefinition.new( name: specific_tool_name, version: version, display_name: impl_version.display_name || "#{interface.name} #{implementation_name} #{version}", implements: Array(interface.name), # v2: expects array profiles: [convert_profile_to_platform_profile(profile, interface.actions)], version_detection: impl_version.version_detection, # Extract from implementation aliases: impl_version.aliases || [] ) # Resolve profile inheritance after creation # Debug logging for Ruby 3.4+ CI if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn '[UKIRYU DEBUG] Before resolve_inheritance!' warn "[UKIRYU DEBUG] tool_def.profiles.size: #{tool_def.profiles.size}" tool_def.profiles.each do |prof| prof_name = prof.name if prof.respond_to?(:name) prof_commands = prof.commands if prof.respond_to?(:commands) warn "[UKIRYU DEBUG] Profile: #{prof_name}, commands: #{prof_commands&.size || 0}" end end tool_def.resolve_inheritance! # Debug logging for Ruby 3.4+ CI if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn '[UKIRYU DEBUG] After resolve_inheritance!' tool_def.profiles.each do |prof| prof_name = prof.name if prof.respond_to?(:name) prof_commands = prof.commands if prof.respond_to?(:commands) warn "[UKIRYU DEBUG] Profile: #{prof_name}, commands: #{prof_commands&.size || 0}" end end tool_def end |
.deep_merge_hashes(base, override) ⇒ Hash
Deep merge two hashes (second hash takes precedence)
444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/ukiryu/tool.rb', line 444 def deep_merge_hashes(base, override) base.merge(override) do |_key, old_val, new_val| if old_val.is_a?(Hash) && new_val.is_a?(Hash) deep_merge_hashes(old_val, new_val) elsif new_val.nil? old_val else new_val end end end |
.detect_implementation_and_version(index, tool_name, options = {}) ⇒ Hash?
Detect implementation and version from ImplementationIndex
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/ukiryu/tool.rb', line 63 def detect_implementation_and_version(index, tool_name, = {}) # Try each implementation in order index.implementations.each do |impl| # Run detection command detection = impl[:detection] || impl['detection'] detection_result = run_detection_command(detection, tool_name, ) next unless detection_result # Extract version using pattern # If pattern has no capture group, detection_result is returned but we should use default version pattern = detection[:pattern] || detection['pattern'] version = extract_version_from_pattern(detection_result, pattern) # If detection succeeded but no version was extracted, check if pattern matched # If pattern was just a presence check (no capture group), use nil version with default spec if version.nil? # Check if pattern matched at all (presence check) has_pattern = detection_result.match?(Regexp.new(pattern)) if pattern # If pattern didn't match, skip this implementation next unless has_pattern # Pattern matched but no version - will use default version spec below end # Resolve versionian scheme require_relative 'version_scheme_resolver' version_scheme = impl[:version_scheme] || impl['version_scheme'] scheme = VersionSchemeResolver.resolve(version_scheme) # Find matching version spec # If version is nil (presence check only), use implementation default if version.nil? versions = impl[:versions] || impl['versions'] # Prefer implementation-level default, then version-level default, then last version impl_default = impl[:default] || impl['default'] version_spec = if impl_default # Find version spec matching the implementation default versions.find do |v| v[:file] == impl_default || v['file'] == impl_default end || versions.last else versions.find { |v| v[:default] || v['default'] } || versions.last end return { implementation_name: impl[:name] || impl['name'], version: nil, file: version_spec[:file] || version_spec['file'] || impl_default } end # Find matching version spec for detected version versions = impl[:versions] || impl['versions'] version_spec = find_matching_version_spec(versions, version, scheme) if version_spec return { implementation_name: impl[:name] || impl['name'], version: version, file: version_spec[:file] || version_spec['file'] } end end # If no implementation matched, use the first one's default return nil if index.implementations.empty? impl = index.implementations.first versions = impl[:versions] || impl['versions'] # Prefer implementation-level default, then version-level default, then last version impl_default = impl[:default] || impl['default'] default_spec = if impl_default # Find version spec matching the implementation default versions.find { |v| v[:file] == impl_default || v['file'] == impl_default } || versions.last else versions.find { |v| v[:default] || v['default'] } || versions.last end { implementation_name: impl[:name] || impl['name'], version: nil, file: default_spec[:file] || default_spec['file'] || impl_default } end |
.extract_definition(tool_name, options = {}) ⇒ Hash
Extract tool definition from an installed CLI tool
Attempts to extract a tool definition by:
-
Trying the tool’s native ‘–ukiryu-definition` flag
-
Parsing the tool’s ‘–help` output as a fallback
780 781 782 783 784 785 786 787 788 789 790 791 792 |
# File 'lib/ukiryu/tool.rb', line 780 def extract_definition(tool_name, = {}) result = Ukiryu::Extractors::Extractor.extract(tool_name, ) # Write to output file if specified output = .delete(:output) if output && result[:success] require 'fileutils' FileUtils.mkdir_p(File.dirname(output)) File.write(output, result[:yaml]) end result end |
.extract_version_from_pattern(output, pattern) ⇒ String?
Extract version from command output using pattern
208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/ukiryu/tool.rb', line 208 def extract_version_from_pattern(output, pattern) return nil unless output && pattern # Scrub output to handle invalid UTF-8 byte sequences scrubbed_output = output.scrub('') match = scrubbed_output.match(Regexp.new(pattern)) return nil unless match # Return capture group if present, otherwise nil (presence check) match[1] end |
.find(name, options = {}) ⇒ Tool?
Find a tool by name, returning nil if not found.
This is a non-raising alternative to get for cases where tool absence is expected and should be handled gracefully.
593 594 595 596 597 |
# File 'lib/ukiryu/tool.rb', line 593 def find(name, = {}) get(name, ) rescue Ukiryu::Errors::ToolNotFoundError nil end |
.find_all(tool_name, options = {}) ⇒ Array<Models::ExecutableInfo>
Find all instances of a tool in PATH and aliases
This is an explicit operation - user must ask for it. Returns an array of ExecutableInfo for all matches found.
624 625 626 |
# File 'lib/ukiryu/tool.rb', line 624 def find_all(tool_name, = {}) ToolFinder.find_all(tool_name, ) end |
.find_by(identifier, options = {}) ⇒ Tool?
Find a tool by name, alias, or interface
Searches for a tool that matches the given identifier by:
-
Exact name match (fastest)
-
Interface match via ToolIndex (O(1) lookup)
-
Alias match via ToolIndex (O(1) lookup)
-
Returns the first tool that is available on the current platform
Debug mode: Set UKIRYU_DEBUG=1 or UKIRYU_DEBUG=true to enable structured debug output
612 613 614 |
# File 'lib/ukiryu/tool.rb', line 612 def find_by(identifier, = {}) ToolFinder.find_by(identifier, ) end |
.find_matching_version_spec(versions, detected_version, scheme) ⇒ Hash?
Find matching version spec using versionian
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/ukiryu/tool.rb', line 226 def find_matching_version_spec(versions, detected_version, scheme) require 'versionian' versions.each do |version_spec| range_type = if version_spec[:equals] :equals elsif version_spec[:before] :before elsif version_spec[:after] :after else version_spec[:between] ? :between : nil end next unless range_type range = case range_type when :equals boundary = version_spec[:equals] || version_spec['equals'] Versionian::VersionRange.new(:equals, scheme, version: boundary) when :before boundary = version_spec[:before] || version_spec['before'] Versionian::VersionRange.new(:before, scheme, version: boundary) when :after boundary = version_spec[:after] || version_spec['after'] Versionian::VersionRange.new(:after, scheme, version: boundary) when :between between = version_spec[:between] || version_spec['between'] from = between[:from] || between['from'] to = between[:to] || between['to'] Versionian::VersionRange.new(:between, scheme, from: from, to: to) end return version_spec if range&.matches?(detected_version) end nil end |
.from_bundled(tool_name, options = {}) ⇒ Tool?
Load a tool from bundled system locations
Searches standard system locations for tool definitions:
-
/usr/share/ukiryu/
-
/usr/local/share/ukiryu/
-
/opt/homebrew/share/ukiryu/
-
C:\Program Files\Ukiryu\
716 717 718 719 720 721 722 723 724 725 726 727 728 729 |
# File 'lib/ukiryu/tool.rb', line 716 def from_bundled(tool_name, = {}) search_paths = bundled_definition_search_paths search_paths.each do |base_path| Dir.glob(File.join(base_path, tool_name.to_s, '*.yaml')).each do |file| return load(file, ) rescue Ukiryu::Errors::DefinitionLoadError, Ukiryu::Errors::DefinitionNotFoundError # Try next file next end end nil end |
.from_definition(yaml_string, options = {}) ⇒ Tool
Alias for load_from_string - load from YAML string
664 665 666 |
# File 'lib/ukiryu/tool.rb', line 664 def from_definition(yaml_string, = {}) load_from_string(yaml_string, ) end |
.from_file(file_path, options = {}) ⇒ Tool
Alias for load - load from file path
655 656 657 |
# File 'lib/ukiryu/tool.rb', line 655 def from_file(file_path, = {}) load(file_path, ) end |
.get(name, options = {}) ⇒ Tool
Get a tool by name using the new ImplementationIndex architecture
560 561 562 563 564 565 566 567 568 569 570 571 572 |
# File 'lib/ukiryu/tool.rb', line 560 def get(name, = {}) # Check cache first cache_key = cache_key_for(name, ) cached = tools_cache[cache_key] return cached if cached # Load using ImplementationIndex architecture tool = load_with_implementation_index(name, ) raise Ukiryu::Errors::ToolNotFoundError, "Tool not found: #{name}" unless tool tools_cache[cache_key] = tool tool end |
.get_class(tool_name) ⇒ Class
Get the tool-specific class (new OOP API)
632 633 634 |
# File 'lib/ukiryu/tool.rb', line 632 def get_class(tool_name) ToolFinder.get_class(tool_name) end |
.load(file_path, options = {}) ⇒ Tool
Load a tool definition from a file path
684 685 686 687 688 |
# File 'lib/ukiryu/tool.rb', line 684 def load(file_path, = {}) source = Ukiryu::Definition::Sources::FileSource.new(file_path) profile = Ukiryu::Definition::Loader.load_from_source(source, ) new(profile, .merge(definition_source: source)) end |
.load_from_string(yaml_string, options = {}) ⇒ Tool
Load a tool definition from a YAML string
699 700 701 702 703 |
# File 'lib/ukiryu/tool.rb', line 699 def load_from_string(yaml_string, = {}) source = Ukiryu::Definition::Sources::StringSource.new(yaml_string) profile = Ukiryu::Definition::Loader.load_from_source(source, ) new(profile, .merge(definition_source: source)) end |
.load_with_implementation_index(name, options = {}) ⇒ Tool?
Try loading a tool using the new ImplementationIndex architecture Delegates to Tool::Loader module
53 54 55 |
# File 'lib/ukiryu/tool.rb', line 53 def load_with_implementation_index(name, = {}) Loader.load_with_implementation_index(name, ) end |
.profile_is_modern?(profile_version, version_detection) ⇒ Boolean?
Check if a profile version is “modern” based on version_detection modern_threshold
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 |
# File 'lib/ukiryu/tool.rb', line 1090 def self.profile_is_modern?(profile_version, version_detection) return nil unless version_detection&.modern_threshold require 'rubygems/version' # Skip version comparison for non-numeric versions (e.g., "generic") return nil unless profile_version.match?(/^\d/) # Handle date-based versions (YYYY.MM.DD format used by some tools like ping_gnu) # These are release dates, not semantic versions # Compare by converting both to comparable formats if profile_version.match?(/^\d{4}\.\d{2}\.\d{2}$/) && version_detection.modern_threshold.match?(/^\d{8}$/) # Convert YYYY.MM.DD to YYYYMMDD for direct comparison profile_date = profile_version.gsub('.', '') threshold_date = version_detection.modern_threshold return profile_date >= threshold_date end profile_ver = Gem::Version.new(profile_version) threshold = Gem::Version.new(version_detection.modern_threshold) profile_ver >= threshold rescue ArgumentError # If version parsing fails, treat as non-versioned (return nil) nil end |
.run_detection_command(detection, tool_name, options = {}) ⇒ String?
Run detection command for an implementation
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/ukiryu/tool.rb', line 151 def run_detection_command(detection, tool_name, = {}) command = detection[:command] || detection['command'] return nil unless command cmd = Array(command) # Support multiple executables for detection (e.g., 'convert' and 'magick' for ImageMagick) executables = detection[:executables] || detection['executables'] if executables # Try each executable until one succeeds Array(executables).each do |executable| full_cmd = [executable] + cmd result = try_execute_command(full_cmd, ) return result if result end nil else # Use the executable from detection config, or fall back to tool_name executable = detection[:executable] || detection['executable'] || [:executable] || tool_name.to_s full_cmd = [executable] + cmd try_execute_command(full_cmd, ) end end |
.tools_cache ⇒ Cache
Get the tools cache (bounded LRU cache)
43 44 45 |
# File 'lib/ukiryu/tool.rb', line 43 def tools_cache ToolCache.cache end |
.try_execute_command(cmd, options = {}) ⇒ String?
Try executing a command and return stdout on success
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/ukiryu/tool.rb', line 180 def try_execute_command(cmd, = {}) require_relative 'executor' require_relative 'shell' result = Executor.execute( cmd.first, cmd.drop(1), env: [:env], shell: [:shell] || Shell.detect, timeout: 5, allow_failure: true # Don't raise on non-zero exit for detection ) # Scrub stdout/stderr to handle invalid UTF-8 byte sequences # If command succeeded, return stdout # If command failed but has stderr output, return stderr (for BusyBox detection) if result.success? result.stdout.scrub('') elsif !result.stderr.to_s.strip.empty? result.stderr.scrub('') end rescue StandardError nil end |
Instance Method Details
#available? ⇒ Boolean
Check if the tool is available
902 903 904 |
# File 'lib/ukiryu/tool.rb', line 902 def available? !@executable.nil? end |
#command?(command_name) ⇒ Boolean
Check if a command is available
1057 1058 1059 |
# File 'lib/ukiryu/tool.rb', line 1057 def command?(command_name) !@command_profile.command(command_name.to_s).nil? end |
#command_definition(command_name) ⇒ CommandDefinition?
Get a command definition by name
932 933 934 |
# File 'lib/ukiryu/tool.rb', line 932 def command_definition(command_name) @command_profile.command(command_name.to_s) end |
#commands ⇒ Hash?
Get the commands defined in the active profile
924 925 926 |
# File 'lib/ukiryu/tool.rb', line 924 def commands @command_profile.commands end |
#definition_mtime ⇒ Time?
Get the definition mtime if loaded from file
885 886 887 |
# File 'lib/ukiryu/tool.rb', line 885 def definition_mtime @definition_source&.mtime if @definition_source.respond_to?(:mtime) end |
#definition_path ⇒ String?
Get the definition path if loaded from file
878 879 880 |
# File 'lib/ukiryu/tool.rb', line 878 def definition_path @definition_source&.path if @definition_source.respond_to?(:path) end |
#execute_simple(command_name, execution_timeout:, **params) ⇒ Executor::Result
Execute a command defined in the profile
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 |
# File 'lib/ukiryu/tool.rb', line 982 def execute_simple(command_name, execution_timeout:, **params) # Debug logging for Ruby 4.0 CI if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn "[UKIRYU DEBUG execute_simple] command_name: #{command_name.inspect}" warn "[UKIRYU DEBUG execute_simple] params (before normalize): #{params.inspect}" warn "[UKIRYU DEBUG execute_simple] params.class: #{params.class}" end command = @command_profile.command(command_name.to_s) raise ArgumentError, "Unknown command: #{command_name}" unless command # Normalize params to hash with symbol keys params = normalize_params(params) warn "[UKIRYU DEBUG execute_simple] params (after normalize): #{params.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] # Extract stdin parameter if present (special parameter, not passed to command) stdin = params.delete(:stdin) # Build command arguments args = build_args(command, params) # Determine the executable to use # For tools with subcommands (v7 style for identify/mogrify), use @executable with the subcommand # For tools without subcommands, the behavior depends on the profile version: # - v7 (modern): convert has no subcommand but uses 'magick' executable # - v6 (legacy): each command (convert, identify, mogrify) is a standalone executable command_executable = if command.respond_to?(:has_subcommand?) && command.has_subcommand? # v7 style: e.g., magick identify -> @executable is 'magick', subcommand is 'identify' @executable elsif command.respond_to?(:has_subcommand?) && !command.has_subcommand? # No subcommand - need to determine if this is v7 or v6 style # Check if profile has a modern_threshold and profile version is modern if self.class.profile_is_modern?(@profile.version, @profile.version_detection) # v7 style: convert command (no subcommand) uses 'magick' executable @executable else # v6 style: each command is a standalone executable # Check if command-specific executable exists on the filesystem exe_dir = File.dirname(@executable) exe_name = command.name exe_path = File.join(exe_dir, exe_name) # Use command-specific executable if profile explicitly allows it # This is determined by checking if the command has standalone_executable: true allows_standalone = if command.respond_to?(:standalone_executable?) command.standalone_executable? else false end same_dir_as_exec = allows_standalone && File.executable?(exe_path) && File.dirname(exe_path) == exe_dir if same_dir_as_exec exe_path else @executable end end else # Fallback to @executable @executable end # Execute with environment and stdin, passing tool_name and command_name for exit code lookups execute_with_config(command_executable, args, command, params, execution_timeout: execution_timeout, stdin: stdin) end |
#execute_with_config(executable, args, command_def, params, execution_timeout:, stdin:) ⇒ Executor::Result
Execute command with common configuration
963 964 965 966 967 968 969 970 971 972 973 974 |
# File 'lib/ukiryu/tool.rb', line 963 def execute_with_config(executable, args, command_def, params, execution_timeout:, stdin:) Ukiryu::Executor.execute( executable, args, env: build_env_vars(command_def, @command_profile, params), timeout: execution_timeout, shell: @shell, stdin: stdin, tool_name: @profile.name, command_name: command_def.name ) end |
#name ⇒ String
Get the tool name
845 846 847 |
# File 'lib/ukiryu/tool.rb', line 845 def name @profile.name end |
#normalize_params(params) ⇒ Hash
Normalize params to hash
Converts params to a hash with symbol keys, handling both hash and options objects.
942 943 944 945 946 947 948 949 950 951 952 |
# File 'lib/ukiryu/tool.rb', line 942 def normalize_params(params) if params.is_a?(Hash) && params.keys.none? { |k| k.is_a?(Symbol) } # Likely has string keys from CLI, convert to symbols params.transform_keys(&:to_sym) elsif !params.is_a?(Hash) # It's an options object, convert to hash Ukiryu::OptionsBuilder.to_hash(params) else params end end |
#options_for(command_name) ⇒ Class
Get the options class for a command
1065 1066 1067 |
# File 'lib/ukiryu/tool.rb', line 1065 def (command_name) Ukiryu::OptionsBuilder.for(@profile.name, command_name) end |
#routing ⇒ Models::Routing?
Get the routing table from the active profile
1072 1073 1074 1075 1076 |
# File 'lib/ukiryu/tool.rb', line 1072 def routing return nil unless @command_profile.routing? @command_profile.routing end |
#routing? ⇒ Boolean
Check if this tool has routing defined
1081 1082 1083 |
# File 'lib/ukiryu/tool.rb', line 1081 def routing? !routing.nil? && !routing.empty? end |
#unavailability_reason ⇒ String?
Get the reason why the tool is not available
Returns nil if the tool is available, or a string explaining why not. This helps users understand issues like:
-
Tool not installed
-
Wrong version installed (e.g., impostor tool)
914 915 916 917 918 919 |
# File 'lib/ukiryu/tool.rb', line 914 def unavailability_reason return nil if available? # Executable not found "Tool '#{name}' not found in PATH. Please install the tool and ensure it's in your PATH." end |
#version ⇒ String?
Get the tool version
852 853 854 855 856 857 858 859 860 861 |
# File 'lib/ukiryu/tool.rb', line 852 def version return @version if @version # Use profile version if available (from implementation detection) profile_version = @profile.version if @profile.respond_to?(:version) return profile_version if profile_version info = detect_version info&.to_s end |
#version_info ⇒ Models::VersionInfo?
Get the tool version info (full metadata)
866 867 868 |
# File 'lib/ukiryu/tool.rb', line 866 def version_info @version_info ||= detect_version end |