Class: Ukiryu::Shell::PowerShell
- Defined in:
- lib/ukiryu/shell/powershell.rb
Overview
PowerShell shell implementation
PowerShell uses single quotes for literal strings and backtick for escaping special characters inside double quotes. Environment variables are referenced with $ENV:NAME syntax.
Constant Summary collapse
- SHELL_NAME =
:powershell- PLATFORM =
:powershell- EXECUTABLE =
'pwsh'
Constants inherited from Base
Base::SPECIAL_CHARS_PATTERN, Base::WHITESPACE_PATTERN
Class Method Summary collapse
-
.detect_alias(_command_name) ⇒ Hash?
Detect if a command is a PowerShell alias.
Instance Method Summary collapse
-
#capabilities ⇒ Hash
PowerShell capabilities on Windows.
-
#env_var(name) ⇒ String
Format an environment variable reference.
-
#escape(string) ⇒ String
Escape a string for PowerShell - Single quotes are escaped by doubling them (for single-quoted strings) - Backtick, dollar, and double quotes are escaped with backtick (for double-quoted strings).
-
#escape_for_double_quotes(string) ⇒ String
Escape a string for double-quoted PowerShell strings Used for executable paths which need double quotes.
-
#execute_command(executable, args, env, timeout, cwd = nil) ⇒ Hash
Execute a command using PowerShell.
-
#execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data) ⇒ Hash
Execute a command with stdin input using PowerShell.
-
#format_path(path) ⇒ String
Format a file path for PowerShell on Windows.
-
#headless_environment ⇒ Hash
PowerShell doesn’t need DISPLAY variable.
-
#join(executable, *args) ⇒ String
Join executable and arguments into a command line Uses smart quoting: only quote arguments that need it.
- #name ⇒ Object
-
#needs_quoting?(string) ⇒ Boolean
Check if a string needs quoting for PowerShell Overrides base class to add PowerShell-specific handling.
-
#powershell_command ⇒ String
Get the PowerShell executable for the current platform On Windows: powershell.exe On Unix/macOS: pwsh (PowerShell Core).
-
#quote(string, for_exe: false) ⇒ String
Quote an argument for PowerShell Uses double quotes for all arguments to prevent PowerShell’s parameter binder from stripping dash prefixes.
Methods inherited from Base
#encoding, #environment_to_h, #format_environment, #supports?
Class Method Details
.detect_alias(_command_name) ⇒ Hash?
Detect if a command is a PowerShell alias
PowerShell has aliases but they are PowerShell-level constructs, not shell aliases. We don’t detect PowerShell aliases for executable discovery.
26 27 28 29 30 |
# File 'lib/ukiryu/shell/powershell.rb', line 26 def self.detect_alias(_command_name) # PowerShell aliases are runtime constructs, not shell aliases # They don't help with executable discovery nil end |
Instance Method Details
#capabilities ⇒ Hash
PowerShell capabilities on Windows
190 191 192 193 194 195 196 |
# File 'lib/ukiryu/shell/powershell.rb', line 190 def capabilities { supports_display: false, # Windows doesn't use DISPLAY supports_ansi_colors: true, encoding: Encoding::UTF_8 # PowerShell uses UTF-8 } end |
#env_var(name) ⇒ String
Format an environment variable reference
112 113 114 |
# File 'lib/ukiryu/shell/powershell.rb', line 112 def env_var(name) "$ENV:#{name}" end |
#escape(string) ⇒ String
Escape a string for PowerShell
-
Single quotes are escaped by doubling them (for single-quoted strings)
-
Backtick, dollar, and double quotes are escaped with backtick (for double-quoted strings)
Note: This method escapes for single-quoted strings by default since we use single quotes for arguments to prevent parameter binding issues.
54 55 56 57 |
# File 'lib/ukiryu/shell/powershell.rb', line 54 def escape(string) # For single-quoted strings, escape single quotes by doubling them string.to_s.gsub("'", "''") end |
#escape_for_double_quotes(string) ⇒ String
Escape a string for double-quoted PowerShell strings Used for executable paths which need double quotes
64 65 66 |
# File 'lib/ukiryu/shell/powershell.rb', line 64 def escape_for_double_quotes(string) string.to_s.gsub(/[`"$]/) { "`#{::Regexp.last_match(0)}" } end |
#execute_command(executable, args, env, timeout, cwd = nil) ⇒ Hash
Execute a command using PowerShell
On Windows: Uses cmd /c to execute commands. This provides more predictable quoting behavior than PowerShell’s Start-Process for native executables with arguments containing spaces.
On Unix: Uses the call operator (&) since PowerShell on Unix doesn’t have the parameter binding issues that Windows PowerShell has.
214 215 216 217 218 219 220 221 222 223 224 225 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 263 264 265 266 267 268 269 270 271 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 |
# File 'lib/ukiryu/shell/powershell.rb', line 214 def execute_command(executable, args, env, timeout, cwd = nil) # Debug logging for CI - helps identify where prefix stripping might occur if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn "[UKIRYU DEBUG PowerShell#execute_command] executable: #{executable.inspect}" warn "[UKIRYU DEBUG PowerShell#execute_command] args: #{args.inspect}" warn "[UKIRYU DEBUG PowerShell#execute_command] args.class: #{args.class}" args.each_with_index do |a, i| warn "[UKIRYU DEBUG PowerShell#execute_command] args[#{i}]: #{a.inspect} (#{a.class})" # Check for nested arrays which would cause stringification issues warn "[UKIRYU DEBUG PowerShell#execute_command] WARNING: args[#{i}] is a NESTED ARRAY!" if a.is_a?(Array) end end # Build the command line with proper quoting if Platform.windows? # On Windows: Use PowerShell call operator with single quotes for all arguments # Single quotes are completely literal in PowerShell - no parameter binding issues # This works for both paths with spaces and without spaces exe_normalized = executable.to_s.gsub('/', '\\') exe_escaped = exe_normalized.gsub("'", "''") args_escaped = args.map do |a| # NOTE: Do NOT convert forward slashes to backslashes in arguments! # Many tools (like Inkscape) on Windows expect forward slashes in paths # because backslashes can be interpreted as escape characters. # PowerShell single quotes handle paths correctly regardless of separator. arg_str = a.to_s if arg_str.start_with?('-') # Arguments starting with - must be single-quoted to prevent PowerShell's # parameter binder from stripping the prefix (e.g., -sDEVICE=pdfwrite -> =pdfwrite) escaped = arg_str.gsub("'", "''") "'#{escaped}'" elsif arg_str.include?('$') || arg_str.include?('`') # Use single quotes for arguments with $ or ` to prevent expansion escaped = arg_str.gsub("'", "''") "'#{escaped}'" elsif arg_str.include?(' ') || arg_str.include?('"') # Use single quotes for spaces or quotes (completely literal) escaped = arg_str.gsub("'", "''") "'#{escaped}'" else arg_str end end # Propagate exit code from external command using $LASTEXITCODE else # On Unix: Use the call operator directly with single quotes # Single quotes are completely literal in PowerShell (no variable expansion) exe_escaped = executable.to_s.gsub("'", "''") args_escaped = args.map do |a| arg_str = a.to_s # Quote arguments that contain special PowerShell characters or # start with dash (to prevent parameter binding) if arg_str.include?(' ') || arg_str.start_with?('-') || arg_str.include?('$') || arg_str.include?('`') || arg_str.include?(';') # Use single quotes for completely literal strings escaped = arg_str.gsub("'", "''") "'#{escaped}'" else arg_str end end # Propagate exit code from external command using $LASTEXITCODE end full_command = ["'#{exe_escaped}'", *args_escaped].join(' ') warn "[UKIRYU DEBUG PowerShell#execute_command] full_command: #{full_command.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] ps_command = "& #{full_command}; exit $LASTEXITCODE" warn "[UKIRYU DEBUG PowerShell#execute_command] ps_command:\n#{ps_command}" if ENV['UKIRYU_DEBUG_EXECUTABLE'] # Convert Environment to Hash ONLY at Open3 call site env_hash = environment_to_h(env) # Execute using PowerShell's -Command flag Timeout.timeout(timeout) do execution = lambda do stdout, stderr, status = Open3.capture3(env_hash, powershell_command, '-NoLogo', '-Command', ps_command) { 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 PowerShell
Uses platform-specific execution with stdin redirection.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 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 375 376 377 378 379 380 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 |
# File 'lib/ukiryu/shell/powershell.rb', line 323 def execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data) # Write stdin to temp file for redirection stdin_file = Tempfile.new('ukiryu_stdin') begin if stdin_data.is_a?(IO) IO.copy_stream(stdin_data, stdin_file) elsif stdin_data.is_a?(String) stdin_file.write(stdin_data) end stdin_file.close stdin_path = stdin_file.path if Platform.windows? # On Windows: Use PowerShell call operator with single quotes for all arguments # Single quotes are completely literal in PowerShell - no parameter binding issues # This works for both paths with spaces and without spaces exe_normalized = executable.to_s.gsub('/', '\\') exe_escaped = exe_normalized.gsub("'", "''") args_escaped = args.map do |a| arg_str = a.to_s.gsub('/', '\\') if arg_str.start_with?('-') # Arguments starting with - must be single-quoted to prevent PowerShell's # parameter binder from stripping the prefix (e.g., -sDEVICE=pdfwrite -> =pdfwrite) escaped = arg_str.gsub("'", "''") "'#{escaped}'" elsif arg_str.include?('$') || arg_str.include?('`') # Use single quotes for arguments with $ or ` to prevent expansion escaped = arg_str.gsub("'", "''") "'#{escaped}'" elsif arg_str.include?(' ') || arg_str.include?('"') # Use single quotes for spaces or quotes (completely literal) escaped = arg_str.gsub("'", "''") "'#{escaped}'" else arg_str end end # Propagate exit code from external command using $LASTEXITCODE else # On Unix: Use the call operator with stdin redirection # Single quotes are completely literal in PowerShell (no variable expansion) exe_escaped = executable.to_s.gsub("'", "''") args_escaped = args.map do |a| arg_str = a.to_s # Quote arguments that contain special PowerShell characters or # start with dash (to prevent parameter binding) if arg_str.include?(' ') || arg_str.start_with?('-') || arg_str.include?('$') || arg_str.include?('`') || arg_str.include?(';') escaped = arg_str.gsub("'", "''") "'#{escaped}'" else arg_str end end # Use Get-Content to read stdin file and pipe to command # Propagate exit code from external command using $LASTEXITCODE end full_command = ["'#{exe_escaped}'", *args_escaped].join(' ') ps_command = "Get-Content '#{stdin_path.gsub("'", "''")}' | & #{full_command}; exit $LASTEXITCODE" env_hash = environment_to_h(env) Timeout.timeout(timeout) do execution = lambda do stdout, stderr, status = Open3.capture3(env_hash, powershell_command, '-NoLogo', '-Command', ps_command) { status: Ukiryu::Executor.extract_status(status), stdout: stdout, stderr: stderr } end if cwd Dir.chdir(cwd) { execution.call } else execution.call end end ensure stdin_file.unlink end rescue Timeout::Error, Timeout::ExitException raise Timeout::Error, "Command timed out after #{timeout}s: #{executable}" end |
#format_path(path) ⇒ String
Format a file path for PowerShell on Windows
Returns the path unchanged. Quoting for paths with spaces is handled by the quote method, which wraps paths in double quotes with proper escaping.
124 125 126 |
# File 'lib/ukiryu/shell/powershell.rb', line 124 def format_path(path) path.to_s end |
#headless_environment ⇒ Hash
PowerShell doesn’t need DISPLAY variable
183 184 185 |
# File 'lib/ukiryu/shell/powershell.rb', line 183 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 -Command and -File: The argument after these parameters should NOT be quoted because PowerShell will parse it
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 172 173 174 175 176 177 178 |
# File 'lib/ukiryu/shell/powershell.rb', line 137 def join(executable, *args) # Debug logging for CI - helps identify where prefix stripping might occur if ENV['UKIRYU_DEBUG_EXECUTABLE'] warn "[UKIRYU DEBUG PowerShell#join] executable: #{executable.inspect}" warn "[UKIRYU DEBUG PowerShell#join] args: #{args.inspect}" warn "[UKIRYU DEBUG PowerShell#join] args.size: #{args.size}" warn "[UKIRYU DEBUG PowerShell#join] args.class: #{args.class}" args.each_with_index do |a, i| warn "[UKIRYU DEBUG PowerShell#join] args[#{i}]: #{a.inspect} (#{a.class})" # Check for nested arrays which would cause stringification issues if a.is_a?(Array) warn "[UKIRYU DEBUG PowerShell#join] WARNING: args[#{i}] is a NESTED ARRAY!" warn "[UKIRYU DEBUG PowerShell#join] This will be converted to string: #{a.to_s.inspect}" end end end # Quote executable if it needs quoting (e.g., contains spaces) # Use double quotes for executables (works in both cmd.exe and PowerShell) # Ruby's Open3 on Windows uses cmd.exe, not PowerShell exe_formatted = needs_quoting?(executable) ? quote(executable, for_exe: true) : executable # Track when we see -Command or -File to skip quoting the next argument skip_quote = false args_formatted = args.map do |a| if skip_quote # Don't quote the script/file argument - PowerShell will parse it skip_quote = false a elsif ['-Command', '-File'].include?(a) skip_quote = true a elsif needs_quoting?(a) quote(a) else # For simple strings, pass without quotes # PowerShell treats them as literal strings a end end [exe_formatted, *args_formatted].join(' ') end |
#name ⇒ Object
32 33 34 |
# File 'lib/ukiryu/shell/powershell.rb', line 32 def name :powershell end |
#needs_quoting?(string) ⇒ Boolean
Check if a string needs quoting for PowerShell Overrides base class to add PowerShell-specific handling
In PowerShell, arguments starting with - are interpreted as PowerShell parameters when passed to the call operator (&). This causes the prefix to be stripped (e.g., -sDEVICE=pdfwrite becomes =pdfwrite). To prevent this, we must quote all arguments starting with -.
Also, arguments containing $ must be quoted to prevent variable expansion.
80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/ukiryu/shell/powershell.rb', line 80 def needs_quoting?(string) str = string.to_s # Call super for base checks (empty, whitespace, special chars) return true if super(string) # PowerShell-specific: arguments starting with - must be quoted # to prevent PowerShell's parameter binder from stripping the prefix return true if str.start_with?('-') # PowerShell-specific: arguments containing $ must be quoted # to prevent variable expansion return true if str.include?('$') false end |
#powershell_command ⇒ String
Get the PowerShell executable for the current platform On Windows: powershell.exe On Unix/macOS: pwsh (PowerShell Core)
41 42 43 |
# File 'lib/ukiryu/shell/powershell.rb', line 41 def powershell_command Platform.windows? ? 'powershell' : 'pwsh' end |
#quote(string, for_exe: false) ⇒ String
Quote an argument for PowerShell Uses double quotes for all arguments to prevent PowerShell’s parameter binder from stripping dash prefixes. Double quotes work consistently across both the call operator (&) and Start-Process.
102 103 104 105 106 |
# File 'lib/ukiryu/shell/powershell.rb', line 102 def quote(string, for_exe: false) # Always use double quotes - this prevents PowerShell's parameter binder # from stripping dash prefixes in all contexts (call operator, Start-Process) "\"#{escape_for_double_quotes(string)}\"" end |