Class: Ukiryu::Shell::Cmd

Inherits:
Base
  • Object
show all
Defined in:
lib/ukiryu/shell/cmd.rb

Overview

Windows cmd.exe shell implementation

cmd.exe uses caret (^) as the escape character and double quotes for strings containing spaces. Environment variables use %VAR% syntax.

Constant Summary collapse

SHELL_NAME =
:cmd
PLATFORM =
:windows
EXECUTABLE =
'cmd'
WHITESPACE_PATTERN =

Pre-compiled pattern for whitespace detection

/[ \t]/.freeze

Constants inherited from Base

Base::SPECIAL_CHARS_PATTERN

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#encoding, #environment_to_h, #format_environment, #needs_quoting?, #supports?

Class Method Details

.detect_alias(_command_name) ⇒ Hash?

Detect if a command is a cmd.exe alias

cmd.exe has doskey macros but they are not traditional shell aliases. We don’t detect doskey macros for executable discovery.

Parameters:

  • command_name (String)

    the command to check

Returns:

  • (Hash, nil)

    always returns nil (no alias detection)



27
28
29
30
# File 'lib/ukiryu/shell/cmd.rb', line 27

def self.detect_alias(_command_name)
  # cmd.exe doskey macros are not shell aliases in the Unix sense
  nil
end

Instance Method Details

#capabilitiesHash

cmd.exe capabilities on Windows

Returns:

  • (Hash)

    capability flags



127
128
129
130
131
132
133
# File 'lib/ukiryu/shell/cmd.rb', line 127

def capabilities
  {
    supports_display: false, # Windows doesn't use DISPLAY
    supports_ansi_colors: true,
    encoding: Encoding::CP_1252 # cmd.exe default is CP1252 (Windows-1252)
  }
end

#env_var(name) ⇒ String

Format an environment variable reference

Parameters:

  • name (String)

    the variable name

Returns:

  • (String)

    the formatted reference (%VAR%)



74
75
76
# File 'lib/ukiryu/shell/cmd.rb', line 74

def env_var(name)
  "%#{name}%"
end

#escape(string) ⇒ String

Escape a string for cmd.exe Caret is the escape character for special characters: % ^ < > & |

Parameters:

  • string (String)

    the string to escape

Returns:

  • (String)

    the escaped string



41
42
43
# File 'lib/ukiryu/shell/cmd.rb', line 41

def escape(string)
  string.to_s.gsub(/[%^<>&|]/) { "^#{::Regexp.last_match(0)}" }
end

#execute_command(executable, args, env, timeout, cwd = nil) ⇒ Hash

Execute a command using cmd.exe

Uses cmd.exe’s /c flag to execute the command string.

Parameters:

  • executable (String)

    the executable path

  • args (Array<String>)

    command arguments

  • env (Environment)

    environment variables

  • timeout (Integer)

    timeout in seconds

  • cwd (String, nil) (defaults to: nil)

    working directory (nil for current directory)

Returns:

  • (Hash)

    execution result with :status, :stdout, :stderr keys

Raises:

  • (Timeout::Error)

    if command times out



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
172
173
# File 'lib/ukiryu/shell/cmd.rb', line 146

def execute_command(executable, args, env, timeout, cwd = nil)
  # Build command string using this shell's quoting rules
  command_string = join(executable, *args)

  # Convert Environment to Hash ONLY at Open3 call site
  env_hash = environment_to_h(env)

  # Execute using cmd.exe's /c flag
  Timeout.timeout(timeout) do
    execution = lambda do
      stdout, stderr, status = Open3.capture3(env_hash, 'cmd', '/c', command_string)
      {
        status: Ukiryu::Executor.extract_status(status),
        stdout: stdout,
        stderr: stderr
      }
    end

    if cwd
      Dir.chdir(cwd) { execution.call }
    else
      execution.call
    end
  end
rescue Timeout::Error, Timeout::ExitException
  # Re-raise with context
  raise Timeout::Error, "Command timed out after #{timeout}s: #{executable}"
end

#execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data) ⇒ Hash

Execute a command with stdin input using cmd.exe

Parameters:

  • executable (String)

    the executable path

  • args (Array<String>)

    command arguments

  • env (Environment)

    environment variables

  • timeout (Integer)

    timeout in seconds

  • cwd (String, nil)

    working directory (nil for current directory)

  • stdin_data (String, IO)

    stdin input data

Returns:

  • (Hash)

    execution result with :status, :stdout, :stderr keys

Raises:

  • (Timeout::Error)

    if command times out



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/ukiryu/shell/cmd.rb', line 185

def execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data)
  # Build command string using this shell's quoting rules
  command_string = join(executable, *args)

  # Convert Environment to Hash ONLY at Open3 call site
  env_hash = environment_to_h(env)

  # Execute using cmd.exe's /c flag
  Timeout.timeout(timeout) do
    execution = lambda do
      Open3.popen3(env_hash, 'cmd', '/c', command_string) do |stdin, stdout, stderr, wait_thr|
        # Write stdin data
        begin
          if stdin_data.is_a?(IO)
            IO.copy_stream(stdin_data, stdin)
          elsif stdin_data.is_a?(String)
            stdin.write(stdin_data)
          end
        rescue Errno::EPIPE
          # Process closed stdin early
        ensure
          stdin.close
        end

        # Read output
        out = stdout.read
        err = stderr.read

        # Wait for process to complete
        status = wait_thr.value

        {
          status: Ukiryu::Executor.extract_status(status),
          stdout: out,
          stderr: err
        }
      end
    end

    if cwd
      Dir.chdir(cwd) { execution.call }
    else
      execution.call
    end
  end
rescue Timeout::Error, Timeout::ExitException
  # Re-raise with context
  raise Timeout::Error, "Command timed out after #{timeout}s: #{executable}"
end

#format_path(path) ⇒ String

Format a file path for cmd.exe Convert forward slashes to backslashes

Parameters:

  • path (String)

    the file path

Returns:

  • (String)

    the formatted path



66
67
68
# File 'lib/ukiryu/shell/cmd.rb', line 66

def format_path(path)
  path.to_s.gsub('/', '\\')
end

#headless_environmentHash

cmd.exe doesn’t need DISPLAY variable

Returns:

  • (Hash)

    empty hash (no headless environment needed)



120
121
122
# File 'lib/ukiryu/shell/cmd.rb', line 120

def headless_environment
  {}
end

#join(executable, *args) ⇒ String

Join executable and arguments into a command line Uses smart quoting: only quote arguments that need it

Special handling for /c: When using cmd.exe’s /c flag, the command string that follows should NOT be quoted, as cmd.exe treats it as a single command to execute. The quotes become literal characters.

Parameters:

  • executable (String)

    the executable path

  • args (Array<String>)

    the arguments

Returns:

  • (String)

    the complete command line



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
# File 'lib/ukiryu/shell/cmd.rb', line 88

def join(executable, *args)
  # Quote executable if it needs quoting (e.g., contains spaces)
  exe_formatted = needs_quoting?(executable) ? quote(executable) : escape(executable)

  # Special handling for cmd.exe /c flag
  # When using /c, the rest is a single command string - don't quote it
  if args[0] == '/c' && args.length > 1
    # Build command string from the remaining arguments
    # They form a single command that cmd.exe will execute
    command_parts = args[1..]
    # For the command string, we escape special chars but don't wrap in quotes
    # This allows cmd.exe to parse operators like &&, |, etc.
    command_string = command_parts.map { |a| escape(a) }.join(' ')
    [exe_formatted, args[0], command_string].join(' ')
  else
    # Normal quoting for all arguments
    args_formatted = args.map do |a|
      if needs_quoting?(a)
        quote(a)
      else
        # For simple strings, pass without quotes
        # cmd.exe treats them as literal strings
        escape(a)
      end
    end
    [exe_formatted, *args_formatted].join(' ')
  end
end

#nameObject



32
33
34
# File 'lib/ukiryu/shell/cmd.rb', line 32

def name
  :cmd
end

#quote(string) ⇒ String

Quote an argument for cmd.exe Uses double quotes for strings with spaces

Parameters:

  • string (String)

    the string to quote

Returns:

  • (String)

    the quoted string



50
51
52
53
54
55
56
57
58
59
# File 'lib/ukiryu/shell/cmd.rb', line 50

def quote(string)
  if string.to_s =~ WHITESPACE_PATTERN
    # Contains whitespace, use double quotes
    # Note: cmd.exe doesn't escape quotes inside double quotes the same way
    "\"#{string}\""
  else
    # No whitespace, escape special characters
    escape(string)
  end
end