Module: Ukiryu::VersionDetector

Defined in:
lib/ukiryu/version_detector.rb

Overview

Version detector for external CLI tools

This module provides centralized version detection logic with:

  • Configurable version command patterns

  • Regex pattern matching for version strings

  • Proper shell handling for command execution

  • Support for man-page based version detection (BSD/system tools)

  • Fallback hierarchy: try multiple methods, use first success

Examples:

Detecting version from command output (GNU tools)

info = VersionDetector.detect(
  executable: '/usr/bin/ffmpeg',
  command: '-version',
  pattern: /version (\d+\.\d+)/,
  shell: :bash
)

Detecting version with fallback hierarchy

info = VersionDetector.detect_with_methods(
  executable: '/usr/bin/xargs',
  methods: [
    { type: :command, command: '--version', pattern: /xargs \(GNU findutils\) ([\d.]+)/ },
    { type: :man_page, paths: { macos: '/usr/share/man/man1/xargs.1' } }
  ],
  shell: :bash
)

Class Method Summary collapse

Class Method Details

.detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command', timeout: 30) ⇒ String?

Detect the version of an external tool (legacy API)

Parameters:

  • executable (String)

    the executable path

  • command (String, Array<String>) (defaults to: '--version')

    the version command (default: ‘–version’)

  • pattern (Regexp) (defaults to: /(\d+\.\d+)/)

    the regex pattern to extract version

  • shell (Symbol) (defaults to: nil)

    the shell to use for execution

  • source (String) (defaults to: 'command')

    the version source: ‘command’ (default) or ‘man’

  • timeout (Integer) (defaults to: 30)

    timeout in seconds (default: 30)

Returns:

  • (String, nil)

    the detected version or nil if not found



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ukiryu/version_detector.rb', line 41

def detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command', timeout: 30)
  result = detect_info(
    executable: executable,
    command: command,
    pattern: pattern,
    shell: shell,
    source: source,
    timeout: timeout
  )

  result&.value
end

.detect_info(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command', timeout: 30) ⇒ VersionInfo?

Detect version with full info (VersionInfo)

Parameters:

  • executable (String)

    the executable path

  • command (String, Array<String>) (defaults to: '--version')

    the version command (default: ‘–version’)

  • pattern (Regexp) (defaults to: /(\d+\.\d+)/)

    the regex pattern to extract version

  • shell (Symbol) (defaults to: nil)

    the shell to use for execution

  • source (String) (defaults to: 'command')

    the version source: ‘command’ (default) or ‘man’

  • timeout (Integer) (defaults to: 30)

    timeout in seconds (default: 30 for version detection)

Returns:

  • (VersionInfo, nil)

    the version info or nil if not found



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
# File 'lib/ukiryu/version_detector.rb', line 63

def detect_info(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command',
                timeout: 30)
  # Return nil if executable is not found
  return nil if executable.nil? || executable.empty?

  shell ||= Ukiryu::Shell.detect

  # Normalize command to array
  command_args = command.is_a?(Array) ? command : [command]

  # DEBUG: Log timing to investigate timeout issues
  start_time = Time.now
  result = Ukiryu::Executor.execute(executable, command_args, shell: shell, allow_failure: true, timeout: timeout)
  elapsed = Time.now - start_time
  warn "[UKIRYU DEBUG] Version detection for #{File.basename(executable)} took #{elapsed.round(2)}s (expected <0.1s)" if elapsed > 1

  return nil unless result.success?

  # Sanitize strings to handle invalid UTF-8 sequences
  stdout = result.stdout.scrub
  stderr = result.stderr.scrub

  # For man pages, look at the tail (last few lines)
  if source == 'man'
    output = stdout + stderr
    # Get last 500 characters to catch the OS version at bottom
    tail = output[-500..] || output
    match = tail.match(pattern)
    if match
      return Models::VersionInfo.new(
        value: match[1],
        method_used: :man_page,
        available_methods: [:man_page]
      )
    end
  end

  match = stdout.match(pattern) || stderr.match(pattern)

  return nil unless match

  Models::VersionInfo.new(
    value: match[1],
    method_used: :command,
    available_methods: [:command]
  )
rescue StandardError
  # Return nil on any error (command not found, execution error, etc.)
  nil
end

.detect_with_methods(executable:, methods:, shell: nil, timeout: 30) ⇒ VersionInfo?

Detect version using multiple methods with fallback hierarchy

Tries each method in order and returns the first successful result. Methods are NOT mutually exclusive - they work together as fallbacks.

Parameters:

  • executable (String)

    the tool executable path

  • methods (Array<Hash>)

    array of method definitions

  • shell (Symbol) (defaults to: nil)

    the shell to use

  • timeout (Integer) (defaults to: 30)

    timeout in seconds (default: 30)

Returns:

  • (VersionInfo, nil)

    version info or nil if all methods fail



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ukiryu/version_detector.rb', line 124

def detect_with_methods(executable:, methods:, shell: nil, timeout: 30)
  shell ||= Ukiryu::Shell.detect

  # Track available methods for VersionInfo
  available_methods = methods.map { |m| m[:type] }.uniq

  # Try each method in order
  methods.each do |method|
    case method[:type]
    when :command
      # Try command-based detection
      info = detect_info(
        executable: executable,
        command: method[:command] || '--version',
        pattern: method[:pattern] || /(\d+\.\d+)/,
        shell: shell,
        source: 'command',
        timeout: timeout
      )

      return info if info

    when :man_page
      # Try man page date extraction
      paths = method[:paths] || {}

      # Resolve man page path for current platform
      platform = Ukiryu::Platform.detect
      man_path = paths[platform] || paths[platform.to_s]

      next unless man_path

      # Parse date from man page
      date_str = Ukiryu::ManPageParser.parse_date(man_path)

      next unless date_str

      return Models::VersionInfo.new(
        value: date_str,
        method_used: :man_page,
        available_methods: available_methods
      )
    end
  end

  # All methods failed
  nil
end