ukiryu 0.1.7 → 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 +188 -99
- 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 +228 -60
- 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,11 +43,25 @@ 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
|
|
|
@@ -58,6 +73,8 @@ module Ukiryu
|
|
|
58
73
|
# to be stripped (e.g., -sDEVICE=pdfwrite becomes =pdfwrite).
|
|
59
74
|
# To prevent this, we must quote all arguments starting with -.
|
|
60
75
|
#
|
|
76
|
+
# Also, arguments containing $ must be quoted to prevent variable expansion.
|
|
77
|
+
#
|
|
61
78
|
# @param string [String] the string to check
|
|
62
79
|
# @return [Boolean] true if quoting is needed
|
|
63
80
|
def needs_quoting?(string)
|
|
@@ -67,26 +84,25 @@ module Ukiryu
|
|
|
67
84
|
# PowerShell-specific: arguments starting with - must be quoted
|
|
68
85
|
# to prevent PowerShell's parameter binder from stripping the prefix
|
|
69
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?('$')
|
|
70
90
|
|
|
71
91
|
false
|
|
72
92
|
end
|
|
73
93
|
|
|
74
94
|
# Quote an argument for PowerShell
|
|
75
|
-
# Uses
|
|
76
|
-
#
|
|
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.
|
|
77
98
|
#
|
|
78
99
|
# @param string [String] the string to quote
|
|
79
|
-
# @param for_exe [Boolean] true if quoting for executable path
|
|
100
|
+
# @param for_exe [Boolean] true if quoting for executable path (same behavior)
|
|
80
101
|
# @return [String] the quoted string
|
|
81
102
|
def quote(string, for_exe: false)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"\"#{string}\""
|
|
86
|
-
else
|
|
87
|
-
# For arguments, use single quotes for literal strings
|
|
88
|
-
"'#{escape(string)}'"
|
|
89
|
-
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)}\""
|
|
90
106
|
end
|
|
91
107
|
|
|
92
108
|
# Format an environment variable reference
|
|
@@ -97,6 +113,18 @@ module Ukiryu
|
|
|
97
113
|
"$ENV:#{name}"
|
|
98
114
|
end
|
|
99
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
|
+
|
|
100
128
|
# Join executable and arguments into a command line
|
|
101
129
|
# Uses smart quoting: only quote arguments that need it
|
|
102
130
|
#
|
|
@@ -107,12 +135,20 @@ module Ukiryu
|
|
|
107
135
|
# @param args [Array<String>] the arguments
|
|
108
136
|
# @return [String] the complete command line
|
|
109
137
|
def join(executable, *args)
|
|
110
|
-
# Debug logging for
|
|
138
|
+
# Debug logging for CI - helps identify where prefix stripping might occur
|
|
111
139
|
if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
112
140
|
warn "[UKIRYU DEBUG PowerShell#join] executable: #{executable.inspect}"
|
|
113
141
|
warn "[UKIRYU DEBUG PowerShell#join] args: #{args.inspect}"
|
|
114
142
|
warn "[UKIRYU DEBUG PowerShell#join] args.size: #{args.size}"
|
|
115
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
|
|
116
152
|
end
|
|
117
153
|
|
|
118
154
|
# Quote executable if it needs quoting (e.g., contains spaces)
|
|
@@ -161,9 +197,12 @@ module Ukiryu
|
|
|
161
197
|
|
|
162
198
|
# Execute a command using PowerShell
|
|
163
199
|
#
|
|
164
|
-
# Uses
|
|
165
|
-
#
|
|
166
|
-
#
|
|
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.
|
|
167
206
|
#
|
|
168
207
|
# @param executable [String] the executable path
|
|
169
208
|
# @param args [Array<String>] command arguments
|
|
@@ -173,33 +212,72 @@ module Ukiryu
|
|
|
173
212
|
# @return [Hash] execution result with :status, :stdout, :stderr keys
|
|
174
213
|
# @raise [Timeout::Error] if command times out
|
|
175
214
|
def execute_command(executable, args, env, timeout, cwd = nil)
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
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
|
|
191
272
|
end
|
|
273
|
+
|
|
274
|
+
# Propagate exit code from external command using $LASTEXITCODE
|
|
192
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"
|
|
193
279
|
|
|
194
|
-
|
|
195
|
-
# Append "; exit $LASTEXITCODE" to properly propagate exit codes from
|
|
196
|
-
# the invoked command/program back through the PowerShell wrapper
|
|
197
|
-
ps_command_base = if args_quoted.empty?
|
|
198
|
-
"& #{exe_quoted}"
|
|
199
|
-
else
|
|
200
|
-
"& #{exe_quoted} #{args_quoted.join(' ')}"
|
|
201
|
-
end
|
|
202
|
-
ps_command = "#{ps_command_base}; exit $LASTEXITCODE"
|
|
280
|
+
warn "[UKIRYU DEBUG PowerShell#execute_command] ps_command:\n#{ps_command}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
|
|
203
281
|
|
|
204
282
|
# Convert Environment to Hash ONLY at Open3 call site
|
|
205
283
|
env_hash = environment_to_h(env)
|
|
@@ -228,9 +306,7 @@ module Ukiryu
|
|
|
228
306
|
|
|
229
307
|
# Execute a command with stdin input using PowerShell
|
|
230
308
|
#
|
|
231
|
-
# Uses
|
|
232
|
-
# The executable and arguments are quoted individually and passed to
|
|
233
|
-
# PowerShell's call operator &.
|
|
309
|
+
# Uses platform-specific execution with stdin redirection.
|
|
234
310
|
#
|
|
235
311
|
# @param executable [String] the executable path
|
|
236
312
|
# @param args [Array<String>] command arguments
|
|
@@ -241,78 +317,91 @@ module Ukiryu
|
|
|
241
317
|
# @return [Hash] execution result with :status, :stdout, :stderr keys
|
|
242
318
|
# @raise [Timeout::Error] if command times out
|
|
243
319
|
def execute_command_with_stdin(executable, args, env, timeout, cwd, stdin_data)
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# not a command line for the shell. The -Command/-File handling in join()
|
|
252
|
-
# is for when building command lines where PowerShell's parameter binder
|
|
253
|
-
# would interpret -prefixed arguments.
|
|
254
|
-
args_quoted = args.map do |a|
|
|
255
|
-
if needs_quoting?(a)
|
|
256
|
-
%("#{escape(a)}")
|
|
257
|
-
else
|
|
258
|
-
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)
|
|
259
327
|
end
|
|
260
|
-
|
|
328
|
+
stdin_file.close
|
|
261
329
|
|
|
262
|
-
|
|
263
|
-
# Append "; exit $LASTEXITCODE" to properly propagate exit codes from
|
|
264
|
-
# the invoked command/program back through the PowerShell wrapper
|
|
265
|
-
ps_command_base = if args_quoted.empty?
|
|
266
|
-
"& #{exe_quoted}"
|
|
267
|
-
else
|
|
268
|
-
"& #{exe_quoted} #{args_quoted.join(' ')}"
|
|
269
|
-
end
|
|
270
|
-
ps_command = "#{ps_command_base}; exit $LASTEXITCODE"
|
|
330
|
+
stdin_path = stdin_file.path
|
|
271
331
|
|
|
272
|
-
|
|
273
|
-
|
|
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("'", "''")
|
|
274
338
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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("'", "''")
|
|
364
|
+
|
|
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
|
|
291
374
|
end
|
|
375
|
+
end
|
|
292
376
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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"
|
|
296
382
|
|
|
297
|
-
|
|
298
|
-
status = wait_thr.value
|
|
383
|
+
env_hash = environment_to_h(env)
|
|
299
384
|
|
|
385
|
+
Timeout.timeout(timeout) do
|
|
386
|
+
execution = lambda do
|
|
387
|
+
stdout, stderr, status = Open3.capture3(env_hash, powershell_command, '-NoLogo', '-Command', ps_command)
|
|
300
388
|
{
|
|
301
389
|
status: Ukiryu::Executor.extract_status(status),
|
|
302
|
-
stdout:
|
|
303
|
-
stderr:
|
|
390
|
+
stdout: stdout,
|
|
391
|
+
stderr: stderr
|
|
304
392
|
}
|
|
305
393
|
end
|
|
306
|
-
end
|
|
307
394
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
395
|
+
if cwd
|
|
396
|
+
Dir.chdir(cwd) { execution.call }
|
|
397
|
+
else
|
|
398
|
+
execution.call
|
|
399
|
+
end
|
|
312
400
|
end
|
|
401
|
+
ensure
|
|
402
|
+
stdin_file.unlink
|
|
313
403
|
end
|
|
314
404
|
rescue Timeout::Error, Timeout::ExitException
|
|
315
|
-
# Re-raise with context
|
|
316
405
|
raise Timeout::Error, "Command timed out after #{timeout}s: #{executable}"
|
|
317
406
|
end
|
|
318
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
|