ukiryu 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ukiryu/cache.rb +6 -0
- data/lib/ukiryu/cache_registry.rb +64 -0
- data/lib/ukiryu/cli_commands/base_command.rb +6 -5
- data/lib/ukiryu/cli_commands/config_command.rb +7 -10
- data/lib/ukiryu/cli_commands/register_command.rb +27 -18
- data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
- data/lib/ukiryu/command_builder.rb +83 -50
- data/lib/ukiryu/config.rb +13 -2
- data/lib/ukiryu/debug.rb +20 -9
- data/lib/ukiryu/definition/loader.rb +3 -3
- data/lib/ukiryu/errors.rb +37 -37
- data/lib/ukiryu/executable_locator.rb +40 -16
- data/lib/ukiryu/extractors/base_extractor.rb +2 -1
- data/lib/ukiryu/extractors/help_parser.rb +3 -0
- data/lib/ukiryu/logger.rb +51 -0
- data/lib/ukiryu/models/implementation_index.rb +2 -1
- data/lib/ukiryu/models/implementation_version.rb +18 -1
- data/lib/ukiryu/models/interface.rb +2 -1
- data/lib/ukiryu/models/run_environment.rb +0 -2
- data/lib/ukiryu/models/semantic_version.rb +174 -0
- data/lib/ukiryu/models/stage_metrics.rb +0 -1
- data/lib/ukiryu/register.rb +473 -232
- data/lib/ukiryu/shell/powershell.rb +209 -89
- data/lib/ukiryu/shell/sh.rb +4 -1
- data/lib/ukiryu/shell.rb +60 -2
- data/lib/ukiryu/tool/command_resolution.rb +2 -1
- data/lib/ukiryu/tool/executable_discovery.rb +14 -15
- data/lib/ukiryu/tool/loader.rb +543 -0
- data/lib/ukiryu/tool/version_detection.rb +1 -3
- data/lib/ukiryu/tool.rb +79 -87
- data/lib/ukiryu/tool_index.rb +127 -62
- data/lib/ukiryu/tools/base.rb +4 -2
- data/lib/ukiryu/type.rb +26 -15
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu.rb +1 -1
- data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
- data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/tool_helper.rb +2 -0
- data/spec/ukiryu/definition/loader_spec.rb +2 -2
- data/spec/ukiryu/executor_spec.rb +6 -3
- data/spec/ukiryu/models/execution_report_spec.rb +3 -2
- data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
- data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
- data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
- data/spec/ukiryu/shell/powershell_spec.rb +286 -51
- data/spec/ukiryu/tool/loader_spec.rb +148 -0
- data/spec/ukiryu/tool_index_spec.rb +110 -18
- data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
- data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
- data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
- metadata +14 -2
- data/lib/ukiryu/register_auto_manager.rb +0 -342
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'timeout'
|
|
5
|
+
require 'tempfile'
|
|
5
6
|
|
|
6
7
|
module Ukiryu
|
|
7
8
|
module Shell
|
|
@@ -42,30 +43,66 @@ module Ukiryu
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Escape a string for PowerShell
|
|
45
|
-
#
|
|
46
|
+
# - Single quotes are escaped by doubling them (for single-quoted strings)
|
|
47
|
+
# - Backtick, dollar, and double quotes are escaped with backtick (for double-quoted strings)
|
|
48
|
+
#
|
|
49
|
+
# Note: This method escapes for single-quoted strings by default since we use
|
|
50
|
+
# single quotes for arguments to prevent parameter binding issues.
|
|
46
51
|
#
|
|
47
52
|
# @param string [String] the string to escape
|
|
48
53
|
# @return [String] the escaped string
|
|
49
54
|
def escape(string)
|
|
55
|
+
# For single-quoted strings, escape single quotes by doubling them
|
|
56
|
+
string.to_s.gsub("'", "''")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Escape a string for double-quoted PowerShell strings
|
|
60
|
+
# Used for executable paths which need double quotes
|
|
61
|
+
#
|
|
62
|
+
# @param string [String] the string to escape
|
|
63
|
+
# @return [String] the escaped string
|
|
64
|
+
def escape_for_double_quotes(string)
|
|
50
65
|
string.to_s.gsub(/[`"$]/) { "`#{::Regexp.last_match(0)}" }
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
# Check if a string needs quoting for PowerShell
|
|
69
|
+
# Overrides base class to add PowerShell-specific handling
|
|
70
|
+
#
|
|
71
|
+
# In PowerShell, arguments starting with - are interpreted as PowerShell
|
|
72
|
+
# parameters when passed to the call operator (&). This causes the prefix
|
|
73
|
+
# to be stripped (e.g., -sDEVICE=pdfwrite becomes =pdfwrite).
|
|
74
|
+
# To prevent this, we must quote all arguments starting with -.
|
|
75
|
+
#
|
|
76
|
+
# Also, arguments containing $ must be quoted to prevent variable expansion.
|
|
77
|
+
#
|
|
78
|
+
# @param string [String] the string to check
|
|
79
|
+
# @return [Boolean] true if quoting is needed
|
|
80
|
+
def needs_quoting?(string)
|
|
81
|
+
str = string.to_s
|
|
82
|
+
# Call super for base checks (empty, whitespace, special chars)
|
|
83
|
+
return true if super(string)
|
|
84
|
+
# PowerShell-specific: arguments starting with - must be quoted
|
|
85
|
+
# to prevent PowerShell's parameter binder from stripping the prefix
|
|
86
|
+
return true if str.start_with?('-')
|
|
87
|
+
# PowerShell-specific: arguments containing $ must be quoted
|
|
88
|
+
# to prevent variable expansion
|
|
89
|
+
return true if str.include?('$')
|
|
90
|
+
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
53
94
|
# Quote an argument for PowerShell
|
|
54
|
-
# Uses
|
|
55
|
-
#
|
|
95
|
+
# Uses double quotes for all arguments to prevent PowerShell's parameter
|
|
96
|
+
# binder from stripping dash prefixes. Double quotes work consistently
|
|
97
|
+
# across both the call operator (&) and Start-Process.
|
|
56
98
|
#
|
|
57
99
|
# @param string [String] the string to quote
|
|
58
|
-
# @param for_exe [Boolean] true if quoting for executable path
|
|
100
|
+
# @param for_exe [Boolean] true if quoting for executable path (same behavior)
|
|
59
101
|
# @return [String] the quoted string
|
|
60
102
|
def quote(string, for_exe: false)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"\"#{string}\""
|
|
65
|
-
else
|
|
66
|
-
# For arguments, use single quotes for literal strings
|
|
67
|
-
"'#{escape(string)}'"
|
|
68
|
-
end
|
|
103
|
+
# Always use double quotes - this prevents PowerShell's parameter binder
|
|
104
|
+
# from stripping dash prefixes in all contexts (call operator, Start-Process)
|
|
105
|
+
"\"#{escape_for_double_quotes(string)}\""
|
|
69
106
|
end
|
|
70
107
|
|
|
71
108
|
# Format an environment variable reference
|
|
@@ -76,6 +113,18 @@ module Ukiryu
|
|
|
76
113
|
"$ENV:#{name}"
|
|
77
114
|
end
|
|
78
115
|
|
|
116
|
+
# Format a file path for PowerShell on Windows
|
|
117
|
+
#
|
|
118
|
+
# Returns the path unchanged. Quoting for paths with spaces is handled
|
|
119
|
+
# by the quote method, which wraps paths in double quotes with proper
|
|
120
|
+
# escaping.
|
|
121
|
+
#
|
|
122
|
+
# @param path [String] the file path
|
|
123
|
+
# @return [String] the formatted path
|
|
124
|
+
def format_path(path)
|
|
125
|
+
path.to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
79
128
|
# Join executable and arguments into a command line
|
|
80
129
|
# Uses smart quoting: only quote arguments that need it
|
|
81
130
|
#
|
|
@@ -86,12 +135,20 @@ module Ukiryu
|
|
|
86
135
|
# @param args [Array<String>] the arguments
|
|
87
136
|
# @return [String] the complete command line
|
|
88
137
|
def join(executable, *args)
|
|
89
|
-
# Debug logging for
|
|
138
|
+
# Debug logging for CI - helps identify where prefix stripping might occur
|
|
90
139
|
if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
91
140
|
warn "[UKIRYU DEBUG PowerShell#join] executable: #{executable.inspect}"
|
|
92
141
|
warn "[UKIRYU DEBUG PowerShell#join] args: #{args.inspect}"
|
|
93
142
|
warn "[UKIRYU DEBUG PowerShell#join] args.size: #{args.size}"
|
|
94
143
|
warn "[UKIRYU DEBUG PowerShell#join] args.class: #{args.class}"
|
|
144
|
+
args.each_with_index do |a, i|
|
|
145
|
+
warn "[UKIRYU DEBUG PowerShell#join] args[#{i}]: #{a.inspect} (#{a.class})"
|
|
146
|
+
# Check for nested arrays which would cause stringification issues
|
|
147
|
+
if a.is_a?(Array)
|
|
148
|
+
warn "[UKIRYU DEBUG PowerShell#join] WARNING: args[#{i}] is a NESTED ARRAY!"
|
|
149
|
+
warn "[UKIRYU DEBUG PowerShell#join] This will be converted to string: #{a.to_s.inspect}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
95
152
|
end
|
|
96
153
|
|
|
97
154
|
# Quote executable if it needs quoting (e.g., contains spaces)
|
|
@@ -140,9 +197,12 @@ module Ukiryu
|
|
|
140
197
|
|
|
141
198
|
# Execute a command using PowerShell
|
|
142
199
|
#
|
|
143
|
-
# Uses
|
|
144
|
-
#
|
|
145
|
-
#
|
|
200
|
+
# On Windows: Uses cmd /c to execute commands. This provides more predictable
|
|
201
|
+
# quoting behavior than PowerShell's Start-Process for native executables
|
|
202
|
+
# with arguments containing spaces.
|
|
203
|
+
#
|
|
204
|
+
# On Unix: Uses the call operator (&) since PowerShell on Unix doesn't have
|
|
205
|
+
# the parameter binding issues that Windows PowerShell has.
|
|
146
206
|
#
|
|
147
207
|
# @param executable [String] the executable path
|
|
148
208
|
# @param args [Array<String>] command arguments
|
|
@@ -152,28 +212,72 @@ module Ukiryu
|
|
|
152
212
|
# @return [Hash] execution result with :status, :stdout, :stderr keys
|
|
153
213
|
# @raise [Timeout::Error] if command times out
|
|
154
214
|
def execute_command(executable, args, env, timeout, cwd = nil)
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
a
|
|
215
|
+
# Debug logging for CI - helps identify where prefix stripping might occur
|
|
216
|
+
if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
217
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] executable: #{executable.inspect}"
|
|
218
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] args: #{args.inspect}"
|
|
219
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] args.class: #{args.class}"
|
|
220
|
+
args.each_with_index do |a, i|
|
|
221
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] args[#{i}]: #{a.inspect} (#{a.class})"
|
|
222
|
+
# Check for nested arrays which would cause stringification issues
|
|
223
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] WARNING: args[#{i}] is a NESTED ARRAY!" if a.is_a?(Array)
|
|
165
224
|
end
|
|
166
225
|
end
|
|
167
226
|
|
|
168
|
-
# Build
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
# Build the command line with proper quoting
|
|
228
|
+
if Platform.windows?
|
|
229
|
+
# On Windows: Use PowerShell call operator with single quotes for all arguments
|
|
230
|
+
# Single quotes are completely literal in PowerShell - no parameter binding issues
|
|
231
|
+
# This works for both paths with spaces and without spaces
|
|
232
|
+
exe_normalized = executable.to_s.gsub('/', '\\')
|
|
233
|
+
exe_escaped = exe_normalized.gsub("'", "''")
|
|
234
|
+
|
|
235
|
+
args_escaped = args.map do |a|
|
|
236
|
+
arg_str = a.to_s.gsub('/', '\\')
|
|
237
|
+
if arg_str.start_with?('-')
|
|
238
|
+
# Arguments starting with - must be single-quoted to prevent PowerShell's
|
|
239
|
+
# parameter binder from stripping the prefix (e.g., -sDEVICE=pdfwrite -> =pdfwrite)
|
|
240
|
+
escaped = arg_str.gsub("'", "''")
|
|
241
|
+
"'#{escaped}'"
|
|
242
|
+
elsif arg_str.include?('$') || arg_str.include?('`')
|
|
243
|
+
# Use single quotes for arguments with $ or ` to prevent expansion
|
|
244
|
+
escaped = arg_str.gsub("'", "''")
|
|
245
|
+
"'#{escaped}'"
|
|
246
|
+
elsif arg_str.include?(' ') || arg_str.include?('"')
|
|
247
|
+
# Use single quotes for spaces or quotes (completely literal)
|
|
248
|
+
escaped = arg_str.gsub("'", "''")
|
|
249
|
+
"'#{escaped}'"
|
|
250
|
+
else
|
|
251
|
+
arg_str
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Propagate exit code from external command using $LASTEXITCODE
|
|
256
|
+
else
|
|
257
|
+
# On Unix: Use the call operator directly with single quotes
|
|
258
|
+
# Single quotes are completely literal in PowerShell (no variable expansion)
|
|
259
|
+
exe_escaped = executable.to_s.gsub("'", "''")
|
|
260
|
+
|
|
261
|
+
args_escaped = args.map do |a|
|
|
262
|
+
arg_str = a.to_s
|
|
263
|
+
# Quote arguments that contain special PowerShell characters or
|
|
264
|
+
# start with dash (to prevent parameter binding)
|
|
265
|
+
if arg_str.include?(' ') || arg_str.start_with?('-') || arg_str.include?('$') || arg_str.include?('`') || arg_str.include?(';')
|
|
266
|
+
# Use single quotes for completely literal strings
|
|
267
|
+
escaped = arg_str.gsub("'", "''")
|
|
268
|
+
"'#{escaped}'"
|
|
269
|
+
else
|
|
270
|
+
arg_str
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Propagate exit code from external command using $LASTEXITCODE
|
|
275
|
+
end
|
|
276
|
+
full_command = ["'#{exe_escaped}'", *args_escaped].join(' ')
|
|
277
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] full_command: #{full_command.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
278
|
+
ps_command = "& #{full_command}; exit $LASTEXITCODE"
|
|
279
|
+
|
|
280
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] ps_command:\n#{ps_command}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
177
281
|
|
|
178
282
|
# Convert Environment to Hash ONLY at Open3 call site
|
|
179
283
|
env_hash = environment_to_h(env)
|
|
@@ -202,9 +306,7 @@ module Ukiryu
|
|
|
202
306
|
|
|
203
307
|
# Execute a command with stdin input using PowerShell
|
|
204
308
|
#
|
|
205
|
-
# Uses
|
|
206
|
-
# The executable and arguments are quoted individually and passed to
|
|
207
|
-
# PowerShell's call operator &.
|
|
309
|
+
# Uses platform-specific execution with stdin redirection.
|
|
208
310
|
#
|
|
209
311
|
# @param executable [String] the executable path
|
|
210
312
|
# @param args [Array<String>] command arguments
|
|
@@ -215,73 +317,91 @@ module Ukiryu
|
|
|
215
317
|
# @return [Hash] execution result with :status, :stdout, :stderr keys
|
|
216
318
|
# @raise [Timeout::Error] if command times out
|
|
217
319
|
def execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data)
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
%("#{escape(a)}")
|
|
226
|
-
else
|
|
227
|
-
a
|
|
320
|
+
# Write stdin to temp file for redirection
|
|
321
|
+
stdin_file = Tempfile.new('ukiryu_stdin')
|
|
322
|
+
begin
|
|
323
|
+
if stdin_data.is_a?(IO)
|
|
324
|
+
IO.copy_stream(stdin_data, stdin_file)
|
|
325
|
+
elsif stdin_data.is_a?(String)
|
|
326
|
+
stdin_file.write(stdin_data)
|
|
228
327
|
end
|
|
229
|
-
|
|
328
|
+
stdin_file.close
|
|
230
329
|
|
|
231
|
-
|
|
232
|
-
# Append "; exit $LASTEXITCODE" to properly propagate exit codes from
|
|
233
|
-
# the invoked command/program back through the PowerShell wrapper
|
|
234
|
-
ps_command_base = if args_quoted.empty?
|
|
235
|
-
"& #{exe_quoted}"
|
|
236
|
-
else
|
|
237
|
-
"& #{exe_quoted} #{args_quoted.join(' ')}"
|
|
238
|
-
end
|
|
239
|
-
ps_command = "#{ps_command_base}; exit $LASTEXITCODE"
|
|
330
|
+
stdin_path = stdin_file.path
|
|
240
331
|
|
|
241
|
-
|
|
242
|
-
|
|
332
|
+
if Platform.windows?
|
|
333
|
+
# On Windows: Use PowerShell call operator with single quotes for all arguments
|
|
334
|
+
# Single quotes are completely literal in PowerShell - no parameter binding issues
|
|
335
|
+
# This works for both paths with spaces and without spaces
|
|
336
|
+
exe_normalized = executable.to_s.gsub('/', '\\')
|
|
337
|
+
exe_escaped = exe_normalized.gsub("'", "''")
|
|
243
338
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
339
|
+
args_escaped = args.map do |a|
|
|
340
|
+
arg_str = a.to_s.gsub('/', '\\')
|
|
341
|
+
if arg_str.start_with?('-')
|
|
342
|
+
# Arguments starting with - must be single-quoted to prevent PowerShell's
|
|
343
|
+
# parameter binder from stripping the prefix (e.g., -sDEVICE=pdfwrite -> =pdfwrite)
|
|
344
|
+
escaped = arg_str.gsub("'", "''")
|
|
345
|
+
"'#{escaped}'"
|
|
346
|
+
elsif arg_str.include?('$') || arg_str.include?('`')
|
|
347
|
+
# Use single quotes for arguments with $ or ` to prevent expansion
|
|
348
|
+
escaped = arg_str.gsub("'", "''")
|
|
349
|
+
"'#{escaped}'"
|
|
350
|
+
elsif arg_str.include?(' ') || arg_str.include?('"')
|
|
351
|
+
# Use single quotes for spaces or quotes (completely literal)
|
|
352
|
+
escaped = arg_str.gsub("'", "''")
|
|
353
|
+
"'#{escaped}'"
|
|
354
|
+
else
|
|
355
|
+
arg_str
|
|
260
356
|
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Propagate exit code from external command using $LASTEXITCODE
|
|
360
|
+
else
|
|
361
|
+
# On Unix: Use the call operator with stdin redirection
|
|
362
|
+
# Single quotes are completely literal in PowerShell (no variable expansion)
|
|
363
|
+
exe_escaped = executable.to_s.gsub("'", "''")
|
|
261
364
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
365
|
+
args_escaped = args.map do |a|
|
|
366
|
+
arg_str = a.to_s
|
|
367
|
+
# Quote arguments that contain special PowerShell characters or
|
|
368
|
+
# start with dash (to prevent parameter binding)
|
|
369
|
+
if arg_str.include?(' ') || arg_str.start_with?('-') || arg_str.include?('$') || arg_str.include?('`') || arg_str.include?(';')
|
|
370
|
+
escaped = arg_str.gsub("'", "''")
|
|
371
|
+
"'#{escaped}'"
|
|
372
|
+
else
|
|
373
|
+
arg_str
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Use Get-Content to read stdin file and pipe to command
|
|
378
|
+
# Propagate exit code from external command using $LASTEXITCODE
|
|
379
|
+
end
|
|
380
|
+
full_command = ["'#{exe_escaped}'", *args_escaped].join(' ')
|
|
381
|
+
ps_command = "Get-Content '#{stdin_path.gsub("'", "''")}' | & #{full_command}; exit $LASTEXITCODE"
|
|
265
382
|
|
|
266
|
-
|
|
267
|
-
status = wait_thr.value
|
|
383
|
+
env_hash = environment_to_h(env)
|
|
268
384
|
|
|
385
|
+
Timeout.timeout(timeout) do
|
|
386
|
+
execution = lambda do
|
|
387
|
+
stdout, stderr, status = Open3.capture3(env_hash, powershell_command, '-NoLogo', '-Command', ps_command)
|
|
269
388
|
{
|
|
270
389
|
status: Ukiryu::Executor.extract_status(status),
|
|
271
|
-
stdout:
|
|
272
|
-
stderr:
|
|
390
|
+
stdout: stdout,
|
|
391
|
+
stderr: stderr
|
|
273
392
|
}
|
|
274
393
|
end
|
|
275
|
-
end
|
|
276
394
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
395
|
+
if cwd
|
|
396
|
+
Dir.chdir(cwd) { execution.call }
|
|
397
|
+
else
|
|
398
|
+
execution.call
|
|
399
|
+
end
|
|
281
400
|
end
|
|
401
|
+
ensure
|
|
402
|
+
stdin_file.unlink
|
|
282
403
|
end
|
|
283
404
|
rescue Timeout::Error, Timeout::ExitException
|
|
284
|
-
# Re-raise with context
|
|
285
405
|
raise Timeout::Error, "Command timed out after #{timeout}s: #{executable}"
|
|
286
406
|
end
|
|
287
407
|
end
|
data/lib/ukiryu/shell/sh.rb
CHANGED
|
@@ -20,7 +20,10 @@ module Ukiryu
|
|
|
20
20
|
result = `type #{command_name} 2>/dev/null`
|
|
21
21
|
return nil unless result
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
if result =~ /^#{command_name} is aliased to `(.*)'`$/
|
|
24
|
+
{ definition: result.strip,
|
|
25
|
+
target: ::Regexp.last_match(1) }
|
|
26
|
+
end
|
|
24
27
|
nil
|
|
25
28
|
end
|
|
26
29
|
|
data/lib/ukiryu/shell.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Ukiryu
|
|
|
25
25
|
autoload :Tcsh, 'ukiryu/shell/tcsh'
|
|
26
26
|
autoload :PowerShell, 'ukiryu/shell/powershell'
|
|
27
27
|
autoload :Cmd, 'ukiryu/shell/cmd'
|
|
28
|
+
autoload :InstanceCache, 'ukiryu/shell/instance_cache'
|
|
28
29
|
|
|
29
30
|
# Platform-grouped shell types (new schema v1)
|
|
30
31
|
PLATFORM_GROUPS = %i[unix windows powershell].freeze
|
|
@@ -56,6 +57,47 @@ module Ukiryu
|
|
|
56
57
|
powershell: %i[powershell pwsh]
|
|
57
58
|
}.freeze
|
|
58
59
|
|
|
60
|
+
# Shell registry for custom shells
|
|
61
|
+
# @api private
|
|
62
|
+
class Registry
|
|
63
|
+
class << self
|
|
64
|
+
def shells
|
|
65
|
+
@shells ||= {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Register a custom shell class
|
|
69
|
+
#
|
|
70
|
+
# @param name [Symbol] the shell name
|
|
71
|
+
# @param shell_class [Class] the shell class (must inherit from Shell::Base)
|
|
72
|
+
# @raise [ArgumentError] if shell_class is not a Shell::Base subclass
|
|
73
|
+
def register(name, shell_class)
|
|
74
|
+
raise ArgumentError, 'Shell class must inherit from Ukiryu::Shell::Base' unless shell_class.ancestors.include?(Base)
|
|
75
|
+
|
|
76
|
+
shells[name.to_sym] = shell_class
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Lookup a shell class by name
|
|
80
|
+
#
|
|
81
|
+
# @param name [Symbol] the shell name
|
|
82
|
+
# @return [Class, nil] the shell class or nil if not registered
|
|
83
|
+
def lookup(name)
|
|
84
|
+
shells[name.to_sym]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get all registered shell names
|
|
88
|
+
#
|
|
89
|
+
# @return [Array<Symbol>] list of registered shell names
|
|
90
|
+
def registered_shells
|
|
91
|
+
shells.keys
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clear all registered shells (mainly for testing)
|
|
95
|
+
def clear
|
|
96
|
+
@shells = nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
59
101
|
class << self
|
|
60
102
|
# Get or set the current shell (for explicit configuration)
|
|
61
103
|
attr_writer :current_shell
|
|
@@ -94,7 +136,10 @@ module Ukiryu
|
|
|
94
136
|
def platform_group_for(shell_sym)
|
|
95
137
|
return shell_sym if PLATFORM_GROUPS.include?(shell_sym)
|
|
96
138
|
|
|
97
|
-
|
|
139
|
+
unless SHELL_TO_PLATFORM.key?(shell_sym)
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"Unknown shell: #{shell_sym}. Valid shells: #{VALID_SHELLS.join(', ')}"
|
|
142
|
+
end
|
|
98
143
|
|
|
99
144
|
SHELL_TO_PLATFORM[shell_sym]
|
|
100
145
|
end
|
|
@@ -231,7 +276,11 @@ module Ukiryu
|
|
|
231
276
|
# @return [Class] the shell class
|
|
232
277
|
# @raise [UnknownShellError] if shell class not found
|
|
233
278
|
def class_for(name)
|
|
234
|
-
#
|
|
279
|
+
# Check registry first (for custom shells)
|
|
280
|
+
registered = Registry.lookup(name)
|
|
281
|
+
return registered if registered
|
|
282
|
+
|
|
283
|
+
# Built-in shells
|
|
235
284
|
case name
|
|
236
285
|
when :unix
|
|
237
286
|
Bash # Most common Unix shell, all Unix shells share the same quoting rules
|
|
@@ -260,6 +309,15 @@ module Ukiryu
|
|
|
260
309
|
end
|
|
261
310
|
end
|
|
262
311
|
|
|
312
|
+
# Register a custom shell class
|
|
313
|
+
#
|
|
314
|
+
# @param name [Symbol] the shell name
|
|
315
|
+
# @param shell_class [Class] the shell class (must inherit from Shell::Base)
|
|
316
|
+
# @raise [ArgumentError] if shell_class is not a Shell::Base subclass
|
|
317
|
+
def register(name, shell_class)
|
|
318
|
+
Registry.register(name, shell_class)
|
|
319
|
+
end
|
|
320
|
+
|
|
263
321
|
private
|
|
264
322
|
|
|
265
323
|
# Detect shell on Windows
|
|
@@ -92,7 +92,8 @@ module Ukiryu
|
|
|
92
92
|
args = build_args(action, params)
|
|
93
93
|
|
|
94
94
|
# Execute with the routed executable, passing tool_name and command_name for exit code lookups
|
|
95
|
-
execute_with_config(resolution[:executable], args, action, params, execution_timeout: execution_timeout,
|
|
95
|
+
execute_with_config(resolution[:executable], args, action, params, execution_timeout: execution_timeout,
|
|
96
|
+
stdin: stdin)
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
# Execute a command with root-path notation (for hierarchical tools)
|
|
@@ -22,16 +22,15 @@ module Ukiryu
|
|
|
22
22
|
# Use executable_name from command profile, falling back to profile name
|
|
23
23
|
executable_name = @command_profile.executable_name || @profile.name
|
|
24
24
|
|
|
25
|
-
# Debug logging for
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
25
|
+
# Debug logging for executable discovery
|
|
26
|
+
Logger.debug("Tool: #{@profile.name}", category: :executable)
|
|
27
|
+
Logger.debug("Command profile executable_name: #{@command_profile.executable_name.inspect}",
|
|
28
|
+
category: :executable)
|
|
29
|
+
Logger.debug("Profile name: #{@profile.name.inspect}", category: :executable)
|
|
30
|
+
Logger.debug("Resolved executable_name: #{executable_name.inspect}", category: :executable)
|
|
31
|
+
Logger.debug("Profile aliases: #{@profile.aliases.inspect}", category: :executable)
|
|
32
|
+
Logger.debug("Shell: #{@shell.inspect}", category: :executable)
|
|
33
|
+
Logger.debug("Platform: #{@platform.inspect}", category: :executable)
|
|
35
34
|
|
|
36
35
|
result = ::Ukiryu::ExecutableLocator.find_with_info(
|
|
37
36
|
tool_name: executable_name,
|
|
@@ -39,11 +38,11 @@ module Ukiryu
|
|
|
39
38
|
platform: @platform
|
|
40
39
|
)
|
|
41
40
|
|
|
42
|
-
if result
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
if result
|
|
42
|
+
Logger.debug("Found executable: #{result[:path]}", category: :executable)
|
|
43
|
+
Logger.debug("Discovery source: #{result[:info].source.inspect}", category: :executable)
|
|
44
|
+
else
|
|
45
|
+
Logger.debug('EXECUTABLE NOT FOUND!', category: :executable)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
return nil unless result
|