ukiryu 0.1.0 → 0.1.1

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -0,0 +1,2952 @@
1
+ # Ukiryu - Platform-Adaptive Command Execution Framework
2
+
3
+ ## Project Name: Ukiryu (浮流)
4
+
5
+ **Ukiryu** (pronounced *oo-kee-ryoo*, 浮流) means "floating flow" in Japanese:
6
+
7
+ * **Floating** (浮) - The framework *adapts* to different implementations, platforms, and shells. It doesn't force a single way of working—instead, it flexibly accommodates the unique characteristics of each command-line tool.
8
+
9
+ * **Flow** (流) - The framework *unifies* diverse external tools into a consistent Ruby API. Like streams converging into a river, Ukiryu brings together Inkscape, Ghostscript, ImageMagick, Git, Docker, and countless other tools under one consistent interface.
10
+
11
+ ---
12
+
13
+ ## Executive Summary
14
+
15
+ Ukiryu is a Ruby framework for creating robust, cross-platform wrappers around external command-line tools through declarative YAML profiles. **Ukiryu turns external CLIs into Ruby APIs** with explicit type safety, shell detection, and platform profiles—no hidden magic, no silent fallbacks.
16
+
17
+ ### Two-Repository Architecture
18
+
19
+ Ukiryu consists of two separate repositories:
20
+
21
+ ```
22
+ ┌─────────────────────────────────────────────────────────────────────┐
23
+ │ ukiryu/ukiryu (Core Framework) │
24
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
25
+ │ │ Ruby Gem: Framework Logic │ │
26
+ │ │ │ │
27
+ │ │ - Shell detection (EXPLICIT: bash, zsh, powershell, cmd) │ │
28
+ │ │ - Shell escaping (each shell knows its own rules) │ │
29
+ │ │ - Type validation & conversion │ │
30
+ │ │ - Command execution (timeout, error handling) │ │
31
+ │ │ - Profile selection (EXACT matching on platform+shell+version) │ │
32
+ │ │ - Environment variable management │ │
33
+ │ │ - YAML profile loader │ │
34
+ │ │ - Version detection │ │
35
+ │ └─────────────────────────────────────────────────────────────────┘ │
36
+ │ │
37
+ │ gem: ukiryu │
38
+ │ Zero dependencies (Ruby stdlib only) │
39
+ └─────────────────────────────────────────────────────────────────────┘
40
+ │ loads
41
+
42
+ ┌─────────────────────────────────────────────────────────────────────┐
43
+ │ ukiryu/register (Tool Registry) │
44
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
45
+ │ │ YAML Profiles: Tool Definitions │ │
46
+ │ │ │ │
47
+ │ │ tools/inkscape/1.0.yaml tools/ghostscript/10.0.yaml │ │
48
+ │ │ tools/imagemagick/7.0.yaml tools/git/2.45.yaml │ │
49
+ │ │ tools/docker/25.0.yaml tools/ffmpeg/7.0.yaml │ │
50
+ │ │ ... (community-contributed) │ │
51
+ │ │ │
52
+ │ │ schemas/*.yaml.schema (YAML Schema validation) │ │
53
+ │ │ docs/*.adoc (AsciiDoc documentation) │ │
54
+ │ └─────────────────────────────────────────────────────────────────┘ │
55
+ │ │
56
+ │ gem: ukiryu-register (optional, can use as git repo) │
57
+ │ Validates with: json-schema gem │
58
+ └─────────────────────────────────────────────────────────────────────┘
59
+ ```
60
+
61
+ ### Why Two Repositories?
62
+
63
+ | Aspect | Single Repo | Two Repos (Ukiryu) |
64
+ |--------|-----------|-------------------|
65
+ | **Release cycle** | Framework + tools together | Framework independent of tool updates |
66
+ | **Contributors** | Developers only | Anyone can add tools (no Ruby needed) |
67
+ | **Maintenance** | Code changes for new tools | YAML file changes for new tools |
68
+ | **Validation** | Test framework | Validate YAML against schema |
69
+ | **Discovery** | Gem update | `git pull` on registry |
70
+ | **Flexibility** | Coupled | Decoupled - use custom registries |
71
+
72
+ ---
73
+
74
+ ## The Vision: Ukiryu as "Reverse Thor" for External Executables
75
+
76
+ ### The Thor Model (for context)
77
+
78
+ Thor turns Ruby methods into CLI commands:
79
+
80
+ ```ruby
81
+ class MyCLI < Thor
82
+ desc "greet NAME", "Say hello"
83
+ option :loud
84
+ def greet(name)
85
+ puts "Hello #{name}!"
86
+ end
87
+ end
88
+
89
+ # Generates CLI:
90
+ # mycli greet Alice --loud
91
+ ```
92
+
93
+ ### The Ukiryu Model (Our Approach)
94
+
95
+ Ukiryu turns external CLI commands into Ruby methods:
96
+
97
+ ```ruby
98
+ # Load tool profiles from registry
99
+ Ukiryu::Registry.load_from("ukiryu/register")
100
+
101
+ # Get the tool
102
+ inkscape = Ukiryu::Tool.get("inkscape")
103
+
104
+ # Use like a Ruby API
105
+ inkscape.export(
106
+ inputs: ["diagram.svg"],
107
+ output: "diagram.pdf",
108
+ format: :pdf,
109
+ plain: true
110
+ )
111
+ # CLI: inkscape --export-filename=diagram.pdf --export-type pdf --export-plain-svg diagram.svg
112
+ ```
113
+
114
+ **The key insight:** Just as Thor provides a DSL to define a CLI, Ukiryu provides a YAML DSL to define a Ruby wrapper around a CLI. Ukiryu becomes the bridge between your Ruby code and external tools, handling all platform differences explicitly.
115
+
116
+ **"Floating"** - Ukiryu adapts to each tool's unique characteristics (different option formats, argument orders, version behaviors).
117
+
118
+ **"Flow"** - Ukiryu unifies diverse tools into one consistent Ruby API.
119
+
120
+ ---
121
+
122
+ ## The Need
123
+
124
+ ### Problem: Four Layers of Cross-Platform Complexity
125
+
126
+ #### Layer 1: Executable Discovery
127
+
128
+ | Platform | Inkscape Binary | Ghostscript Binary |
129
+ |----------|-----------------|-------------------|
130
+ | Windows | `inkscape.exe` | `gswin64c.exe` |
131
+ | macOS | `inkscape` (app bundle) | `gs` |
132
+ | Linux | `inkscape` | `gs` |
133
+
134
+ **Search strategy:**
135
+ - Unix: `ENV["PATH"]` is sufficient (tools installed in standard locations)
136
+ - Windows: `ENV["PATH"]` plus common installation directories
137
+ - `C:/Program Files/Tool/`
138
+ - `C:/Program Files (x86)/Tool/`
139
+ - App bundles on macOS
140
+
141
+ #### Layer 2: Shell Escaping (CRITICAL!)
142
+
143
+ Each shell has fundamentally different escaping rules:
144
+
145
+ **Bash/Zsh:**
146
+ ```bash
147
+ # Single quotes: literal (no escaping inside)
148
+ echo 'Hello $USER' # => Hello $USER
149
+
150
+ # Double quotes: variable expansion
151
+ echo "Hello $USER" # => Hello alice
152
+
153
+ # Backslash escaping inside double quotes
154
+ echo "Path: \"file\"" # => Path: "file"
155
+ ```
156
+
157
+ **PowerShell:**
158
+ ```powershell
159
+ # Single quotes: literal
160
+ Write-Host 'Hello $ENV:USER' # => Hello $ENV:USER
161
+
162
+ # Double quotes: variable expansion
163
+ Write-Host "Hello $ENV:USER" # => Hello alice
164
+
165
+ # Backtick escaping inside double quotes
166
+ Write-Host "Path: `"` # => Path: "
167
+ ```
168
+
169
+ **cmd.exe:**
170
+ ```cmd
171
+ # Caret is escape character
172
+ echo ^^%USERNAME%^ # alice (caret escapes, % expands)
173
+ ```
174
+
175
+ #### Layer 3: Command Syntax Differences
176
+
177
+ | Tool Aspect | Unix | Windows (some ports) |
178
+ |------------|------|-------------------|
179
+ | Long options | `--option=value` | `/Option:value` |
180
+ | Short options | `-o value` | `/Ovalue` |
181
+ | Output specification | `--output=file.pdf` | `/Output:file.pdf` |
182
+ | Flags | `--plain` | `/plain` |
183
+ | Path format | `/usr/bin/file` | `C:\Program Files\file` |
184
+
185
+ #### Layer 4: Type Safety & Validation
186
+
187
+ | Issue | Example | Consequence |
188
+ |-------|---------|--------------|
189
+ | Wrong shell escaping | Unescaped `$USER` | Security vulnerability |
190
+ | Wrong path for platform | `/usr/bin/file` on Windows | "file not found" |
191
+ | Invalid enum value | `format: :docx` | Tool fails with cryptic error |
192
+ | Out-of-range integer | `quality: 150` | Tool uses default or fails |
193
+
194
+ ### Current Solutions Are Insufficient
195
+
196
+ | Solution | Problems |
197
+ |----------|----------|
198
+ | `Open3.capture3` | No types, no escaping, no platform awareness |
199
+ | `Shellwords.escape` | Bash-only, doesn't handle cmd/PowerShell |
200
+ | `posix-spawn` | Unix-only |
201
+ | **Each library reimplements everything poorly** | Incomplete, inconsistent, error-prone |
202
+
203
+ ---
204
+
205
+ ## What Ukiryu Does
206
+
207
+ Ukiryu provides a complete execution pipeline with **explicit** (no fallbacks):
208
+
209
+ ```
210
+ Ruby API Call (Typed Parameters)
211
+
212
+ Type Validation (Semantic: paths, URIs, ranges, enums)
213
+ ↓ (Error if invalid)
214
+ Shell Detection (EXPLICIT or raise error)
215
+ ↓ (Error if unknown)
216
+ Profile Selection (EXACT match on platform+shell+version)
217
+ ↓ (Error if no match)
218
+ Command Building (Shell-specific escaping)
219
+
220
+ Execution (Platform-specific methods)
221
+
222
+ Result Parsing
223
+ ```
224
+
225
+ **Key principle:** Explicit over implicit. If Ukiryu can't determine shell or profile, it raises a clear error rather than guessing.
226
+
227
+ ---
228
+
229
+ ## Architecture: Hybrid Approach (Ruby Framework + YAML Profiles)
230
+
231
+ ### Two-Layer Design
232
+
233
+ Ukiryu separates **framework logic** (Ruby) from **tool definitions** (YAML):
234
+
235
+ ```
236
+ ┌─────────────────────────────────────────────────────────────┐
237
+ │ UKIRYU FRAMEWORK (Ruby) │
238
+ │ - Shell detection & escaping │
239
+ │ - Type validation & conversion │
240
+ │ - Execution & timeout handling │
241
+ │ - Profile selection algorithm │
242
+ │ - Environment variable management │
243
+ └───────────────────────────┬─────────────────────────────────┘
244
+ │ loads
245
+
246
+ ┌─────────────────────────────────────────────────────────────┐
247
+ │ TOOL PROFILES (YAML Registry) │
248
+ │ - inkscape.yaml (all versions) │
249
+ │ - ghostscript.yaml (all versions) │
250
+ │ - imagemagick.yaml (all versions) │
251
+ │ - git.yaml (all subcommands) │
252
+ │ - ... (community-contributed) │
253
+ └─────────────────────────────────────────────────────────────┘
254
+ ```
255
+
256
+ ### Why YAML Profiles?
257
+
258
+ | Aspect | Ruby DSL | YAML Profiles |
259
+ |--------|----------|---------------|
260
+ | **New tool support** | Requires code + release | Add YAML file |
261
+ | **Version update** | Requires code + release | Update YAML |
262
+ | **Maintenance** | Developers only | Anyone can edit |
263
+ | **Validation** | Compile-time | Load-time |
264
+ | **Distribution** | Gem releases | Separate registry |
265
+ | **Community** | Pull requests | PRs to registry repo |
266
+
267
+ ### YAML Profile Structure
268
+
269
+ ```yaml
270
+ # profiles/inkscape.yaml
271
+
272
+ name: inkscape
273
+ aliases:
274
+ - inkscapecom
275
+ - ink
276
+
277
+ version_detection:
278
+ command: "--version"
279
+ pattern: "Inkscape (\\d+\\.\\d+)"
280
+ modern_threshold: "1.0"
281
+
282
+ search_paths:
283
+ windows:
284
+ - "C:/Program Files/Inkscape*/inkscape.exe"
285
+ macos:
286
+ - "/Applications/Inkscape.app/Contents/MacOS/inkscape"
287
+ # Unix: rely on PATH only
288
+
289
+ profiles:
290
+ # Modern Inkscape (1.0+) on Unix
291
+ - name: modern_unix
292
+ platforms: [macos, linux]
293
+ shells: [bash, zsh, fish, sh]
294
+ version: ">= 1.0"
295
+ option_style: double_dash_equals
296
+ commands:
297
+ export:
298
+ arguments:
299
+ - name: inputs
300
+ type: file
301
+ variadic: true
302
+ position: last
303
+ min: 1
304
+
305
+ options:
306
+ - name: output
307
+ type: file
308
+ cli: "--export-filename="
309
+ format: double_dash_equals
310
+
311
+ - name: format
312
+ type: symbol
313
+ values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
314
+ cli: "--export-type"
315
+ format: double_dash_space
316
+ separator: " "
317
+
318
+ - name: dpi
319
+ type: integer
320
+ cli: "-d"
321
+ format: single_dash_space
322
+ separator: " "
323
+
324
+ - name: quality
325
+ type: integer
326
+ cli: "-d"
327
+ range: [0, 100]
328
+
329
+ - name: export_background
330
+ type: string
331
+ cli: "--export-background="
332
+
333
+ flags:
334
+ - name: plain
335
+ cli: "--export-plain-svg"
336
+ cli_short: "-l"
337
+
338
+ - name: export_text_to_path
339
+ cli: "--export-text-to-path"
340
+ cli_short: "-T"
341
+
342
+ env_vars:
343
+ - name: DISPLAY
344
+ value: ""
345
+ platforms: [macos, linux]
346
+
347
+ query:
348
+ arguments:
349
+ - name: input
350
+ type: file
351
+ required: true
352
+
353
+ flags:
354
+ - name: width
355
+ cli: "--query-width"
356
+ cli_short: "-W"
357
+
358
+ - name: height
359
+ cli: "--query-height"
360
+ cli_short: "-H"
361
+
362
+ - name: x
363
+ cli: "--query-x"
364
+ cli_short: "-X"
365
+
366
+ - name: y
367
+ cli: "--query-y"
368
+ cli_short: "-Y"
369
+
370
+ # Modern Inkscape (1.0+) on Windows PowerShell
371
+ - name: modern_windows_powershell
372
+ platforms: [windows]
373
+ shells: [powershell]
374
+ version: ">= 1.0"
375
+ option_style: double_dash_equals
376
+ # Same command structure as modern_unix
377
+ # (inherits from base profile)
378
+ inherits: modern_unix
379
+
380
+ # Modern Inkscape (1.0+) on Windows cmd
381
+ - name: modern_windows_cmd
382
+ platforms: [windows]
383
+ shells: [cmd]
384
+ version: ">= 1.0"
385
+ option_style: slash_space
386
+ commands:
387
+ export:
388
+ # Same structure but different CLI syntax
389
+ options:
390
+ - name: output
391
+ type: file
392
+ cli: "/ExportFilename"
393
+ format: slash_space
394
+ separator: " "
395
+ ```
396
+
397
+ ### YAML Profile Registry
398
+
399
+ **Registry structure:**
400
+ ```
401
+ ukiryu-register/
402
+ ├── tools/
403
+ │ ├── inkscape/
404
+ │ │ ├── 1.0.yaml
405
+ │ │ ├── 0.92.yaml
406
+ │ │ └── 0.9.yaml
407
+ │ ├── ghostscript/
408
+ │ │ ├── 10.0.yaml
409
+ │ │ ├── 9.5.yaml
410
+ │ │ └── 9.0.yaml
411
+ │ ├── imagemagick/
412
+ │ │ ├── 7.0.yaml
413
+ │ │ └── 6.0.yaml
414
+ │ ├── git/
415
+ │ │ ├── 2.45.yaml
416
+ │ │ └── 2.40.yaml
417
+ │ ├── docker/
418
+ │ │ ├── 25.0.yaml
419
+ │ │ └── 24.0.yaml
420
+ │ └── ...
421
+ ├── schemas/
422
+ │ ├── tool-profile.yaml.schema
423
+ │ ├── command-definition.yaml.schema
424
+ │ └── registry.yaml.schema
425
+ ├── docs/
426
+ │ ├── inkscape.adoc
427
+ │ ├── ghostscript.adoc
428
+ │ ├── contributing.adoc
429
+ │ └── registry.adoc
430
+ ├── lib/
431
+ │ └── ukiryu/
432
+ │ └── registry.rb # Registry helper library
433
+ ├── Gemfile # json-schema gem
434
+ ├── Rakefile # Validation tasks
435
+ └── README.adoc
436
+ ```
437
+
438
+ **Version file naming:**
439
+ - Use semantic version: `1.0.yaml`, `0.9.5.yaml`
440
+ - Multiple profiles per version (platform/shell combos) in one file
441
+ - Registry selects newest compatible version
442
+
443
+ **Loading profiles in Ukiryu:**
444
+ ```ruby
445
+ # Load from registry
446
+ Ukiryu::Registry.load_from("/path/to/ukiryu/register")
447
+
448
+ # Or load from gem-builtin profiles
449
+ Ukiryu::Registry.load_builtins
450
+
451
+ # Load specific tool/version
452
+ Ukiryu::Registry.load_tool("inkscape", version: "1.0")
453
+ Ukiryu::Registry.load_tool("inkscape") # Auto-detect latest
454
+
455
+ # Use the tool
456
+ inkscape = Ukiryu::Tool.get("inkscape")
457
+ inkscape.export(
458
+ inputs: ["diagram.svg"],
459
+ output: "diagram.pdf",
460
+ format: :pdf
461
+ )
462
+ ```
463
+
464
+ **Schema validation with `json-schema` gem:**
465
+ ```ruby
466
+ # In registry Rakefile
467
+ require 'json-schema'
468
+
469
+ namespace :validate do
470
+ task :all do
471
+ # Validate all YAML files against schemas
472
+ Dir.glob("tools/*/*.yaml").each do |file|
473
+ schema = JSON::Schema.new(parse_schema("schemas/tool-profile.yaml.schema"))
474
+ schema.validate(YAML.load_file(file))
475
+ end
476
+ end
477
+ end
478
+ ```
479
+
480
+ ### Profile Inheritance
481
+
482
+ Avoid duplication with profile inheritance:
483
+
484
+ ```yaml
485
+ # ghostscript/10.0.yaml
486
+ name: ghostscript
487
+ version: "10.0"
488
+ display_name: Ghostscript 10.0
489
+ homepage: https://www.ghostscript.com/
490
+
491
+ profiles:
492
+ # Base Ghostscript profile
493
+ - name: base_ghostscript
494
+ commands:
495
+ convert:
496
+ options:
497
+ - name: device
498
+ type: symbol
499
+ values: [pdfwrite, eps2write, png16m, jpeg]
500
+ cli: "-sDEVICE="
501
+
502
+ - name: output
503
+ type: file
504
+ cli: "-sOutputFile="
505
+
506
+ flags:
507
+ - name: safer
508
+ cli: "-dSAFER"
509
+
510
+ - name: quiet
511
+ cli: "-q"
512
+
513
+ # Unix variant
514
+ - name: unix
515
+ platforms: [macos, linux]
516
+ shells: [bash, zsh, fish, sh]
517
+ inherits: base_ghostscript
518
+ # Add Unix-specific options
519
+ commands:
520
+ convert:
521
+ options:
522
+ - name: lib_paths
523
+ type: array
524
+ of: file
525
+ cli: "-I"
526
+ separator: " "
527
+
528
+ # Windows variant
529
+ - name: windows
530
+ platforms: [windows]
531
+ shells: [powershell, cmd]
532
+ inherits: base_ghostscript
533
+ # Same structure, shell handles escaping
534
+ ```
535
+
536
+ ### Real-World: Inkscape Complete YAML Profile
537
+
538
+ ```yaml
539
+ # ukiryu-register/tools/inkscape.yaml
540
+
541
+ name: inkscape
542
+ display_name: Inkscape Vector Graphics Editor
543
+ homepage: https://inkscape.org/
544
+ version_detection:
545
+ command: "--version"
546
+ pattern: "Inkscape (\\d+\\.\\d+)"
547
+ modern_threshold: "1.0"
548
+
549
+ search_paths:
550
+ windows:
551
+ - "C:/Program Files/Inkscape*/inkscape.exe"
552
+ - "C:/Program Files (x86)/Inkscape*/inkscape.exe"
553
+ macos:
554
+ - "/Applications/Inkscape.app/Contents/MacOS/inkscape"
555
+ # Unix: PATH only, no hardcoded paths
556
+
557
+ aliases: [inkscapecom, ink]
558
+
559
+ timeout: 90
560
+ terminate_signal: TERM
561
+
562
+ profiles:
563
+ # ============================================
564
+ # Modern Inkscape (1.0+) - Unix
565
+ # ============================================
566
+ - name: modern_unix
567
+ display_name: Modern Inkscape on Unix
568
+ platforms: [macos, linux]
569
+ shells: [bash, zsh, fish, sh]
570
+ version: ">= 1.0"
571
+ option_style: double_dash_equals
572
+ escape_quotes: single
573
+
574
+ commands:
575
+ # ============================================
576
+ # COMMAND: export
577
+ # ============================================
578
+ export:
579
+ description: "Export document to different format"
580
+ usage: "inkscape [OPTIONS] input1.svg [input2.svg ...]"
581
+
582
+ arguments:
583
+ - name: inputs
584
+ type: file
585
+ variadic: true
586
+ position: last
587
+ min: 1
588
+ description: "Input file(s)"
589
+
590
+ options:
591
+ # Output file
592
+ - name: output
593
+ type: file
594
+ cli: "--export-filename="
595
+ format: double_dash_equals
596
+ description: "Output filename"
597
+ required: true
598
+
599
+ # Export format
600
+ - name: format
601
+ type: symbol
602
+ cli: "--export-type"
603
+ format: double_dash_space
604
+ separator: " "
605
+ values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
606
+ description: "Output format"
607
+
608
+ # Multiple export types (comma-separated)
609
+ - name: export_types
610
+ type: array
611
+ of: symbol
612
+ cli: "--export-type="
613
+ separator: ","
614
+ values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
615
+ description: "Multiple export formats"
616
+
617
+ # DPI for bitmap export
618
+ - name: dpi
619
+ type: integer
620
+ cli: "-d"
621
+ format: single_dash_space
622
+ separator: " "
623
+ range: [1, 10000]
624
+ description: "Resolution for bitmaps (default: 96)"
625
+
626
+ # Width
627
+ - name: width
628
+ type: integer
629
+ cli: "--export-width="
630
+ description: "Bitmap width in pixels"
631
+
632
+ # Height
633
+ - name: height
634
+ type: integer
635
+ cli: "--export-height="
636
+ description: "Bitmap height in pixels"
637
+
638
+ # Export area
639
+ - name: area
640
+ type: string
641
+ cli: "--export-area="
642
+ description: "Export area (x0:y0:x1:y1)"
643
+
644
+ # Export page
645
+ - name: export_page
646
+ type: string
647
+ cli: "--export-page="
648
+ description: "Page number to export"
649
+
650
+ # Background color
651
+ - name: background
652
+ type: string
653
+ cli: "--export-background="
654
+ description: "Background color"
655
+
656
+ # Background opacity
657
+ - name: background_opacity
658
+ type: float
659
+ cli: "--export-background-opacity="
660
+ range: [0.0, 1.0]
661
+ description: "Background opacity (0.0 to 1.0)"
662
+
663
+ # Object IDs to export (semicolon-separated)
664
+ - name: export_ids
665
+ type: array
666
+ of: string
667
+ cli: "--export-id="
668
+ separator: ";"
669
+ description: "Object IDs to export"
670
+
671
+ flags:
672
+ # Plain SVG export
673
+ - name: plain
674
+ cli: "--export-plain-svg"
675
+ cli_short: "-l"
676
+ description: "Remove Inkscape-specific attributes"
677
+
678
+ # Export area = page
679
+ - name: area_page
680
+ cli: "--export-area-page"
681
+ cli_short: "-C"
682
+ description: "Export area is page"
683
+
684
+ # Export area = drawing
685
+ - name: area_drawing
686
+ cli: "--export-area-drawing"
687
+ cli_short: "-D"
688
+ description: "Export area is drawing"
689
+
690
+ # Text to path
691
+ - name: text_to_path
692
+ cli: "--export-text-to-path"
693
+ cli_short: "-T"
694
+ description: "Convert text to paths"
695
+
696
+ # Ignore filters
697
+ - name: ignore_filters
698
+ cli: "--export-ignore-filters"
699
+ description: "Render without filters"
700
+
701
+ # Vacuum defs
702
+ - name: vacuum_defs
703
+ cli: "--vacuum-defs"
704
+ description: "Remove unused definitions"
705
+
706
+ env_vars:
707
+ - name: DISPLAY
708
+ value: ""
709
+ platforms: [macos, linux]
710
+ description: "Disable display for headless operation"
711
+
712
+ # ============================================
713
+ # COMMAND: query
714
+ # ============================================
715
+ query:
716
+ description: "Query document dimensions"
717
+ usage: "inkscape --query-width/--query-height input.svg"
718
+
719
+ arguments:
720
+ - name: input
721
+ type: file
722
+ required: true
723
+ description: "Input file"
724
+
725
+ flags:
726
+ - name: width
727
+ cli: "--query-width"
728
+ cli_short: "-W"
729
+ description: "Query width"
730
+
731
+ - name: height
732
+ cli: "--query-height"
733
+ cli_short: "-H"
734
+ description: "Query height"
735
+
736
+ - name: x
737
+ cli: "--query-x"
738
+ cli_short: "-X"
739
+ description: "Query X coordinate"
740
+
741
+ - name: y
742
+ cli: "--query-y"
743
+ cli_short: "-Y"
744
+ description: "Query Y coordinate"
745
+
746
+ parse_output:
747
+ type: hash
748
+ pattern: "key:\\s*value"
749
+
750
+ # ============================================
751
+ # Modern Inkscape (1.0+) - Windows PowerShell
752
+ # ============================================
753
+ - name: modern_windows_powershell
754
+ display_name: Modern Inkscape on Windows (PowerShell)
755
+ platforms: [windows]
756
+ shells: [powershell]
757
+ version: ">= 1.0"
758
+ option_style: double_dash_equals
759
+ inherits: modern_unix
760
+ # Inherits all commands, shell handles escaping
761
+
762
+ # ============================================
763
+ # Modern Inkscape (1.0+) - Windows cmd
764
+ # ============================================
765
+ - name: modern_windows_cmd
766
+ display_name: Modern Inkscape on Windows (cmd)
767
+ platforms: [windows]
768
+ shells: [cmd]
769
+ version: ">= 1.0"
770
+ option_style: slash_space
771
+ # Need to redefine commands for different option syntax
772
+ commands:
773
+ export:
774
+ arguments:
775
+ - name: inputs
776
+ type: file
777
+ variadic: true
778
+ position: last
779
+ min: 1
780
+
781
+ options:
782
+ - name: output
783
+ type: file
784
+ cli: "/ExportFilename"
785
+ format: slash_space
786
+ separator: " "
787
+
788
+ # ... other options with /Option syntax
789
+
790
+ flags:
791
+ - name: plain
792
+ cli: "/Plain"
793
+ ```
794
+
795
+ ### Schema Validation
796
+
797
+ YAML profiles are validated against YAML Schema using the `json-schema` gem:
798
+
799
+ ```yaml
800
+ # schemas/tool-profile.yaml.schema
801
+ ---
802
+ $schema: "http://json-schema.org/draft-07/schema#"
803
+ $title: "Ukiryu Tool Profile"
804
+ $description: "Schema for Ukiryu tool command profiles"
805
+ $type: "object"
806
+ $required:
807
+ - name
808
+ - version
809
+ - display_name
810
+ - profiles
811
+
812
+ properties:
813
+ name:
814
+ type: string
815
+ description: "Tool command name (e.g., 'inkscape', 'gs')"
816
+
817
+ version:
818
+ type: string
819
+ description: "Tool semantic version (e.g., '1.0', '10.0.0')"
820
+
821
+ display_name:
822
+ type: string
823
+ description: "Human-readable tool name"
824
+
825
+ homepage:
826
+ type: string
827
+ format: uri
828
+ description: "Tool homepage URL"
829
+
830
+ aliases:
831
+ type: array
832
+ items:
833
+ type: string
834
+ description: "Alternative command names"
835
+
836
+ search_paths:
837
+ type: object
838
+ properties:
839
+ windows:
840
+ type: array
841
+ items:
842
+ type: string
843
+ macos:
844
+ type: array
845
+ items:
846
+ type: string
847
+ # Unix: no hardcoded paths, rely on PATH
848
+
849
+ version_detection:
850
+ type: object
851
+ $required:
852
+ - command
853
+ - pattern
854
+ properties:
855
+ command:
856
+ type: string
857
+ pattern:
858
+ type: string
859
+ modern_threshold:
860
+ type: string
861
+
862
+ profiles:
863
+ type: array
864
+ items:
865
+ type: object
866
+ $required:
867
+ - name
868
+ - platforms
869
+ - shells
870
+ - commands
871
+ properties:
872
+ name:
873
+ type: string
874
+
875
+ platforms:
876
+ type: array
877
+ items:
878
+ type: string
879
+ enum: [windows, macos, linux]
880
+
881
+ shells:
882
+ type: array
883
+ items:
884
+ type: string
885
+ enum: [bash, zsh, fish, sh, powershell, cmd]
886
+
887
+ version:
888
+ type: string
889
+ description: "Version constraint (e.g., '>= 1.0', '< 2.0')"
890
+
891
+ inherits:
892
+ type: string
893
+ description: "Profile name to inherit from"
894
+
895
+ option_style:
896
+ type: string
897
+ enum: [double_dash_equals, double_dash_space,
898
+ single_dash_equals, single_dash_space,
899
+ slash_space, slash_colon]
900
+
901
+ commands:
902
+ type: object
903
+ patternProperties:
904
+ "^[a-z_]+$":
905
+ type: object
906
+ $required:
907
+ - arguments
908
+ properties:
909
+ description:
910
+ type: string
911
+
912
+ usage:
913
+ type: string
914
+
915
+ arguments:
916
+ type: array
917
+ items:
918
+ type: object
919
+ $required:
920
+ - name
921
+ - type
922
+ properties:
923
+ name:
924
+ type: string
925
+
926
+ type:
927
+ type: string
928
+ enum: [file, string, integer, float, symbol, boolean, uri, datetime, hash, array]
929
+
930
+ variadic:
931
+ type: boolean
932
+
933
+ min:
934
+ type: integer
935
+
936
+ position:
937
+ oneOf:
938
+ - type: integer
939
+ - type: string
940
+ enum: [last]
941
+
942
+ required:
943
+ type: boolean
944
+
945
+ values:
946
+ type: array
947
+ items:
948
+ type: string
949
+
950
+ range:
951
+ type: array
952
+ items:
953
+ type: number
954
+ minItems: 2
955
+ maxItems: 2
956
+
957
+ separator:
958
+ type: string
959
+
960
+ default:
961
+ type: boolean
962
+
963
+ description:
964
+ type: string
965
+
966
+ options:
967
+ type: array
968
+ items:
969
+ type: object
970
+ $required:
971
+ - name
972
+ - cli
973
+ properties:
974
+ name:
975
+ type: string
976
+
977
+ type:
978
+ type: string
979
+ enum: [file, string, integer, float, symbol, boolean, uri, datetime, hash, array]
980
+
981
+ cli:
982
+ type: string
983
+
984
+ cli_short:
985
+ type: string
986
+
987
+ format:
988
+ type: string
989
+ enum: [double_dash_equals, double_dash_space,
990
+ single_dash_equals, single_dash_space,
991
+ slash_space, slash_colon]
992
+
993
+ separator:
994
+ type: string
995
+
996
+ values:
997
+ type: array
998
+ items:
999
+ type: string
1000
+
1001
+ range:
1002
+ type: array
1003
+ items:
1004
+ type: number
1005
+ minItems: 2
1006
+ maxItems: 2
1007
+
1008
+ size:
1009
+ oneOf:
1010
+ - type: integer
1011
+ - type: array
1012
+ items:
1013
+ type: integer
1014
+
1015
+ required:
1016
+ type: boolean
1017
+
1018
+ default:
1019
+ type: [string, integer, float, boolean]
1020
+
1021
+ description:
1022
+ type: string
1023
+
1024
+ flags:
1025
+ type: array
1026
+ items:
1027
+ type: object
1028
+ $required:
1029
+ - name
1030
+ - cli
1031
+ properties:
1032
+ name:
1033
+ type: string
1034
+
1035
+ cli:
1036
+ type: string
1037
+
1038
+ cli_short:
1039
+ type: string
1040
+
1041
+ default:
1042
+ type: boolean
1043
+
1044
+ description:
1045
+ type: string
1046
+
1047
+ env_vars:
1048
+ type: array
1049
+ items:
1050
+ type: object
1051
+ $required:
1052
+ - name
1053
+ properties:
1054
+ name:
1055
+ type: string
1056
+
1057
+ value:
1058
+ type: string
1059
+
1060
+ from:
1061
+ type: string
1062
+
1063
+ default:
1064
+ type: string
1065
+
1066
+ platforms:
1067
+ type: array
1068
+ items:
1069
+ type: string
1070
+
1071
+ description:
1072
+ type: string
1073
+
1074
+ parse_output:
1075
+ type: object
1076
+ properties:
1077
+ type:
1078
+ type: string
1079
+ enum: [hash, array, string, integer]
1080
+
1081
+ pattern:
1082
+ type: string
1083
+
1084
+ format:
1085
+ type: string
1086
+ ```
1087
+
1088
+ **Validation with `json-schema` gem:**
1089
+
1090
+ ```ruby
1091
+ # In registry Rakefile
1092
+ require 'json-schema'
1093
+ require 'yaml'
1094
+
1095
+ namespace :validate do
1096
+ desc "Validate all tool profiles against schema"
1097
+ task :tools do
1098
+ schema = JSON::Schema.new(YAML.load_file('schemas/tool-profile.yaml.schema'))
1099
+
1100
+ Dir.glob('tools/*/*.yaml').each do |file|
1101
+ puts "Validating #{file}..."
1102
+ profile = YAML.load_file(file)
1103
+ schema.validate(profile)
1104
+ puts " ✓ Valid"
1105
+ end
1106
+ rescue JSON::Schema::ValidationError => e
1107
+ puts " ✗ Validation failed: #{e.message}"
1108
+ exit 1
1109
+ end
1110
+
1111
+ desc "Validate schema files themselves"
1112
+ task :schemas do
1113
+ # Verify schemas are valid YAML Schema
1114
+ Dir.glob('schemas/*.yaml.schema').each do |file|
1115
+ puts "Validating schema #{file}..."
1116
+ schema = YAML.load_file(file)
1117
+ puts " ✓ Valid YAML Schema"
1118
+ end
1119
+ end
1120
+
1121
+ task all: [:schemas, :tools]
1122
+ end
1123
+ ```
1124
+
1125
+ ### Usage Examples
1126
+
1127
+ ```ruby
1128
+ # Load from registry
1129
+ Ukiryu::Registry.load_from("~/.ukiryu/register")
1130
+
1131
+ # Or load from gem
1132
+ Ukiryu::Registry.load_builtins
1133
+
1134
+ # Get tool
1135
+ inkscape = Ukiryu::Tool.get("inkscape")
1136
+
1137
+ # Detect platform, shell, version automatically
1138
+ # (or configure explicitly)
1139
+ inkscape.configure(
1140
+ platform: :macos,
1141
+ shell: :zsh
1142
+ )
1143
+
1144
+ # Export command
1145
+ inkscape.export(
1146
+ inputs: ["diagram.svg"],
1147
+ output: "diagram.pdf",
1148
+ format: :pdf,
1149
+ plain: true,
1150
+ dpi: 300
1151
+ )
1152
+
1153
+ # Query command
1154
+ result = inkscape.query(
1155
+ input: "diagram.svg",
1156
+ width: true,
1157
+ height: true
1158
+ )
1159
+ # => { width: 1024, height: 768 }
1160
+ ```
1161
+
1162
+ ---
1163
+
1164
+ ## Core DSL Design (For Custom Tools)
1165
+
1166
+ For tools not in the registry, users can still use Ruby DSL:
1167
+
1168
+ ### Tool Declaration
1169
+
1170
+ ```ruby
1171
+ class MyToolWrapper < Ukiryu::Wrapper
1172
+ tool "mytool" do
1173
+ # ---------- EXECUTABLE ----------
1174
+
1175
+ # Names to try (tried in order until one is found)
1176
+ names "mytool", "mt", "mytool-cli"
1177
+
1178
+ # Search paths (always includes ENV["PATH"])
1179
+ # Platform-specific additions:
1180
+ search_paths do
1181
+ # Windows-specific
1182
+ on_windows do
1183
+ "C:/Program Files/MyTool/*/mytool.exe"
1184
+ "#{ENV['PROGRAMFILES']}/MyTool/mytool.exe"
1185
+ end
1186
+
1187
+ # macOS app bundles
1188
+ on_macos do
1189
+ "/Applications/MyTool.app/Contents/MacOS/mytool"
1190
+ end
1191
+
1192
+ # Unix (no hardcoded paths - rely on PATH)
1193
+ # on_unix do
1194
+ # No hardcoded paths - rely on PATH only
1195
+ end
1196
+ end
1197
+
1198
+ # ---------- VERSION ----------
1199
+
1200
+ detect_version do
1201
+ run "--version"
1202
+ match /MyTool (\d+\.\d+\.\d+)/
1203
+ modern "2.0.0"
1204
+ end
1205
+
1206
+ # ---------- PROFILES ----------
1207
+
1208
+ # Each profile matches: platform + shell + version
1209
+ # All three must match for profile to be selected
1210
+
1211
+ # Modern (2.0+) on Bash/Zsh on Unix
1212
+ profile :modern_unix_bash,
1213
+ platform: [:macos, :linux],
1214
+ shell: [:bash, :zsh],
1215
+ version: ">= 2.0" do
1216
+ arguments do |params|
1217
+ [params.input, params.option_as_cli("--format="), params.flag_as_flag(:plain)]
1218
+ end
1219
+ end
1220
+
1221
+ # Modern on PowerShell on Windows
1222
+ profile :modern_windows_powershell,
1223
+ platform: :windows,
1224
+ shell: :powershell,
1225
+ version: ">= 2.0" do
1226
+ arguments do |params|
1227
+ [params.input, params.option_as_cli("/Format:"), params.flag_as_flag("/Plain")]
1228
+ end
1229
+ end
1230
+
1231
+ # Legacy on cmd on Windows
1232
+ profile :legacy_windows_cmd,
1233
+ platform: :windows,
1234
+ shell: :cmd,
1235
+ version: "< 2.0" do
1236
+ arguments do |params|
1237
+ [params.input, params.option_as_cli("/Format:"), params.flag_as_flag("/Plain")]
1238
+ end
1239
+ end
1240
+
1241
+ # ---------- GLOBAL DEFAULTS ----------
1242
+
1243
+ timeout 90
1244
+ terminate_with :TERM, after: 5, then: :KILL
1245
+ on_windows { terminate_with :KILL } # Windows only supports KILL reliably
1246
+ shell :bash # Default shell detection fallback
1247
+ end
1248
+ end
1249
+ ```
1250
+
1251
+ ### Type System
1252
+
1253
+ All parameter types with shell escaping behavior:
1254
+
1255
+ | Type | Ruby Type | Shell Escaping | Example |
1256
+ |------|-----------|----------------|---------|
1257
+ | `:file` | String path | Platform-specific | `'/path/file.pdf'` (Bash) |
1258
+ | `:string` | String | Fully escaped | `'Hello $USER'` (Bash, literal) |
1259
+ | `:integer` | Integer | No escaping (numeric) | `95` |
1260
+ | `:float` | Float | No escaping (numeric) | `95.5` |
1261
+ | `:symbol` | Symbol | No escaping (alnum) | `pdf` |
1262
+ | `:boolean` | Boolean | Flag or value | `--plain` added/not added |
1263
+ | `:uri` | String | Fully escaped | `'https://example.com'` |
1264
+ | `:datetime` | DateTime/String | Formatted then escaped | `'2025-01-21'` |
1265
+ | `:hash` | Hash | Recursively escaped/merged | `{:key => "value"}` |
1266
+
1267
+ ### Parameter Definitions
1268
+
1269
+ ```ruby
1270
+ command :convert do
1271
+ # POSITIONAL argument (order matters!)
1272
+ argument :input,
1273
+ type: :file,
1274
+ position: 1,
1275
+ required: true
1276
+
1277
+ # OPTION (named with value)
1278
+ option :format,
1279
+ type: :symbol,
1280
+ values: [:pdf, :eps, :svg],
1281
+ cli: "--format=",
1282
+ required: true
1283
+
1284
+ # FLAG (boolean, presence = true)
1285
+ flag :plain,
1286
+ cli: "--plain",
1287
+ default: false
1288
+
1289
+ # OUTPUT (where output goes)
1290
+ output :file,
1291
+ type: :file,
1292
+ via: :option, # Uses --export-filename= option
1293
+ cli: "--export-filename="
1294
+ end
1295
+ ```
1296
+
1297
+ ### Type Validation Examples
1298
+
1299
+ | Type | Input | Validation | Shell Escaped As |
1300
+ |------|-------|------------|------------------|
1301
+ | `:file` | `"file.pdf"` | File exists? | `'file.pdf'` (Bash) |
1302
+ | `:string` | `"Hello World"` | Not empty | `'Hello World'` |
1303
+ | `:integer` | `95` | In 1..100 | `95` (no escape) |
1304
+ | `:symbol` | `:pdf` | In whitelist | `pdf` (no escape) |
1305
+ | `:boolean` | `true` | N/A | `--plain` added |
1306
+ | `:uri` | `https://example.com` | Valid URI | `'https://example.com'` |
1307
+ | `:datetime` | `DateTime.now` | Parseable | `'2025-01-21'` |
1308
+
1309
+ ---
1310
+
1311
+ ## Shell Detection (EXPLICIT)
1312
+
1313
+ ### Detection Algorithm
1314
+
1315
+ ```ruby
1316
+ module Ukiryu
1317
+ class Shell
1318
+ class << self
1319
+ def detect
1320
+ # Unix/macOS
1321
+ if unix?
1322
+ shell_from_env
1323
+ end
1324
+
1325
+ # Windows
1326
+ if windows?
1327
+ detect_windows_shell
1328
+ end
1329
+
1330
+ raise Ukiryu::UnknownShellError, <<~ERROR
1331
+ Unable to detect shell automatically. Please configure explicitly:
1332
+
1333
+ Ukiryu::Shell.configure do |config|
1334
+ config.shell = :bash # or :powershell, :cmd, :zsh
1335
+ end
1336
+
1337
+ Current environment:
1338
+ Platform: #{RbConfig::CONFIG['host_os']}
1339
+ SHELL: #{ENV['SHELL']}
1340
+ PSModulePath: #{ENV['PSModulePATH']}
1341
+ ERROR
1342
+ end
1343
+
1344
+ private
1345
+
1346
+ def unix?
1347
+ RbConfig::CONFIG['host_os'] !~ /mswin|mingw|windows/
1348
+ end
1349
+
1350
+ def windows?
1351
+ Gem.win_platform?
1352
+ end
1353
+
1354
+ def detect_windows_shell
1355
+ return :powershell if ENV['PSModulePath']
1356
+ return :bash if ENV['MSYSTEM'] || ENV['MINGW_PREFIX']
1357
+ return :bash if ENV['WSL_DISTRO']
1358
+ return :cmd # Default Windows shell
1359
+ end
1360
+
1361
+ def shell_from_env
1362
+ return :bash if ENV['SHELL'].end_with?('bash')
1363
+ return :zsh if ENV['SHELL'].end_with?('zsh')
1364
+ return :fish if ENV['SHELL'].end_with?('fish')
1365
+ return :sh if ENV['SHELL'].end_with?('sh')
1366
+
1367
+ # Unknown shell in ENV - try to detect
1368
+ shell_path = ENV['SHELL']
1369
+ if File.executable?(shell_path)
1370
+ name = File.basename(shell_path)
1371
+ return name.to_sym
1372
+ end
1373
+
1374
+ raise Ukiryu::UnknownShellError, "Unknown shell: #{ENV['SHELL']}"
1375
+ end
1376
+ end
1377
+ end
1378
+ end
1379
+ ```
1380
+
1381
+ ### Supported Shells
1382
+
1383
+ | Shell | Platform | Detection | Quote | Escape | Env Var |
1384
+ |-------|----------|-----------|------|--------|---------|
1385
+ | Bash | Unix/macOS/Linux | `$SHELL` ends with `bash` | `'str'` | `'\\''` | `$VAR` |
1386
+ | Zsh | Unix/macOS/Linux | `$SHELL` ends with `zsh` | `'str'` | `'\\''` | `$VAR` |
1387
+ | Fish | Unix/macOS/Linux | `$SHELL` ends with `fish` | `'str'` | `'\\''` | `$VAR` |
1388
+ | Sh | Unix/minimal | `$SHELL` ends with `sh` | `'str'` | `'\\''` | `$VAR` |
1389
+ | PowerShell | Windows | `ENV['PSModulePath']` exists | `'str'` | `` ` `` | `$ENV:NAME` |
1390
+ | Cmd | Windows | Default on Windows | `"` | `^` | `%VAR%` |
1391
+
1392
+ ### Shell Configuration
1393
+
1394
+ ```ruby
1395
+ # Global configuration
1396
+ Ukiryu::Shell.configure do |config|
1397
+ config.shell = :powershell
1398
+ end
1399
+
1400
+ # Per-tool configuration
1401
+ tool "mytool" do
1402
+ shell :bash # Force bash for this tool
1403
+ end
1404
+
1405
+ # Per-execution override
1406
+ result = execute(cmd, shell: :zsh)
1407
+ ```
1408
+
1409
+ ---
1410
+
1411
+ ## Command Profiles (EXACT Matching)
1412
+
1413
+ ### Profile Matching Rules
1414
+
1415
+ **Rule 1: EXACT match on Platform + Shell**
1416
+ - Must match both platform and shell exactly
1417
+ - `platform: :windows, shell: :powershell` ≠ `platform: :windows, shell: :bash`
1418
+
1419
+ **Rule 2: Version compatibility**
1420
+ - If tool is version 8.0 and profile exists for 7.0 → can use 7.0 profile
1421
+ - Assumes backward compatibility within major versions
1422
+
1423
+ **Rule 3: No partial matches**
1424
+ - If no exact profile matches → raise `Ukiryu::ProfileNotFoundError`
1425
+ - No "fallback to generic" - explicit or fail
1426
+
1427
+ ### Profile Declaration
1428
+
1429
+ ```ruby
1430
+ tool "inkscape" do
1431
+
1432
+ # ---------- EXACT PROFILES ----------
1433
+
1434
+ # macOS + Bash + Modern (1.0+)
1435
+ profile :macos_bash_modern,
1436
+ platform: :macos,
1437
+ shell: :bash,
1438
+ version: ">= 1.0" do
1439
+ # Define how to build command line from parameters
1440
+ arguments do |params|
1441
+ [
1442
+ params.input, # Positional arg 1
1443
+ params.option_as_cli("--export-type="), # Named option
1444
+ params.flag_as_flag(:plain) # Optional flag
1445
+ ]
1446
+ end
1447
+ end
1448
+
1449
+ # Windows + PowerShell + Modern (1.0+)
1450
+ profile :windows_powershell_modern,
1451
+ platform: :windows,
1452
+ shell: :powershell,
1453
+ version: ">= 1.0" do
1454
+ arguments do |params|
1455
+ [
1456
+ params.input,
1457
+ params.option_as_cli("--export-type="),
1458
+ params.flag_as_flag(:plain)
1459
+ ]
1460
+ end
1461
+
1462
+ # Windows + cmd + Legacy (< 1.0)
1463
+ profile :windows_cmd_legacy,
1464
+ platform: :windows,
1465
+ shell: :cmd,
1466
+ version: "< 1.0" do
1467
+ arguments do |params|
1468
+ [
1469
+ params.input,
1470
+ params.option_as_cli("--export-type="),
1471
+ params.flag_as_flag(:plain)
1472
+ ]
1473
+ end
1474
+
1475
+ # Windows + Git Bash + Modern
1476
+ profile :windows_bash_modern,
1477
+ platform: :windows,
1478
+ shell: :bash,
1479
+ version: ">= 1.0" do
1480
+ arguments do |params|
1481
+ [
1482
+ params.input,
1483
+ params.option_as_cli("--export-type="),
1484
+ params.flag_as_flag(:plain)
1485
+ ]
1486
+ end
1487
+ end
1488
+ ```
1489
+
1490
+ ### Profile Selection Algorithm
1491
+
1492
+ ```ruby
1493
+ module Ukiryu
1494
+ class ProfileSelector
1495
+ class << self
1496
+ def select(tool_name, platform:, shell:, version:)
1497
+ profiles = profiles_for(tool_name)
1498
+
1499
+ # Find EXACT matches
1500
+ matches = profiles.select do |profile|
1501
+ profile.platform == platform &&
1502
+ profile.shell == shell &&
1503
+ profile.satisfies_version?(version)
1504
+ end
1505
+
1506
+ return matches.first if matches.any?
1507
+
1508
+ # No exact match: try compatible versions
1509
+ compatible = profiles.select do |profile|
1510
+ profile.platform == platform &&
1511
+ profile.shell == shell &&
1512
+ profile.compatible_with?(version)
1513
+ end
1514
+
1515
+ return compatible.first if compatible.any?
1516
+
1517
+ # Nothing matches: raise error
1518
+ available = profiles.map(&:description).join(", ")
1519
+ raise ProfileNotFoundError, <<~ERROR
1520
+ No matching profile found for:
1521
+ Tool: #{tool_name}
1522
+ Platform: #{platform}
1523
+ Shell: #{shell}
1524
+ Version: #{version}
1525
+
1526
+ Available profiles:
1527
+ #{available}
1528
+
1529
+ Please configure Ukiryu with the correct profile.
1530
+ ERROR
1531
+ end
1532
+ end
1533
+ end
1534
+ end
1535
+ ```
1536
+
1537
+ ---
1538
+
1539
+ ## Complete DSL Specification
1540
+
1541
+ ### Tool Declaration
1542
+
1543
+ ```ruby
1544
+ class MyToolWrapper < Ukiryu::Wrapper
1545
+ tool "mytool" do
1546
+ # ---------- EXECUTABLE ----------
1547
+
1548
+ # Names to try (tried in order until found)
1549
+ names "mytool", "mt", "mytool-cli"
1550
+
1551
+ # Search paths (always includes ENV["PATH"])
1552
+ # Add platform-specific paths only if not in PATH
1553
+ search_paths do
1554
+ on_windows { "C:/Program Files/MyTool/*/mytool.exe" }
1555
+ on_macos { "/Applications/MyTool.app/Contents/MacOS/mytool" }
1556
+ # Unix: no hardcoded paths (rely on PATH)
1557
+ end
1558
+
1559
+ # ---------- VERSION ----------
1560
+
1561
+ detect_version do
1562
+ run "--version"
1563
+ match /MyTool (\d+\.\d+)/
1564
+ modern "2.0.0"
1565
+ end
1566
+
1567
+ # ---------- PROFILES ----------
1568
+
1569
+ # Define multiple profiles for different combos
1570
+ profile :modern_bash,
1571
+ platform: [:macos, :linux],
1572
+ shell: [:bash, :zsh],
1573
+ version: ">= 2.0" do
1574
+ option_style :double_dash
1575
+ separator "="
1576
+ end
1577
+
1578
+ profile :legacy_cmd,
1579
+ platform: :windows,
1580
+ shell: :cmd,
1581
+ version: "< 2.0" do
1582
+ option_style :slash
1583
+ separator ":"
1584
+ end
1585
+
1586
+ # ---------- GLOBAL DEFAULTS ----------
1587
+
1588
+ timeout 90
1589
+ terminate_with :TERM, after: 5, then: :KILL
1590
+ shell :bash # Default detection fallback
1591
+ end
1592
+ end
1593
+ ```
1594
+
1595
+ ---
1596
+
1597
+ ## Option Format Variations
1598
+
1599
+ CLI tools use many different option formats. Ukiryu supports all common patterns:
1600
+
1601
+ ### Format Types
1602
+
1603
+ | Format | Example | CLI Syntax | DSL Specification |
1604
+ |--------|---------|------------|------------------|
1605
+ | Double-dash equals | `--format=pdf` | `--flag=value` | `cli: "--format="` |
1606
+ | Double-dash space | `--format pdf` | `--flag value` | `cli: "--format", separator: " "` |
1607
+ | Single-dash equals | `-f=pdf` | `-f=value` | `cli: "-f="` |
1608
+ | Single-dash space | `-f pdf` | `-f value` | `cli: "-f", separator: " "` |
1609
+ | Windows slash | `/format pdf` | `/flag value` | `cli: "/format", separator: " "` |
1610
+ | Single-letter flag | `-v` | Just the flag | `flag :verbose, cli: "-v"` |
1611
+ | Combined flags | `-v -a -q` → `-vaq` | `-vaq` | Automatic for single-letter flags |
1612
+ | Value in flag | `-r300` | Value embedded | `cli: "-r", value_position: :embedded` |
1613
+
1614
+ ### Option Format DSL
1615
+
1616
+ ```ruby
1617
+ command :export do
1618
+ # Double-dash with equals (GNU style)
1619
+ option :format,
1620
+ type: :symbol,
1621
+ values: [:pdf, :eps, :svg],
1622
+ cli: "--format=",
1623
+ format: :double_dash_equals # --format=pdf
1624
+
1625
+ # Double-dash with space (POSIX long)
1626
+ option :dpi,
1627
+ type: :integer,
1628
+ cli: "--dpi",
1629
+ format: :double_dash_space, # --dpi 96
1630
+ separator: " "
1631
+
1632
+ # Single-dash short option
1633
+ option :quality,
1634
+ type: :integer,
1635
+ cli: "-q",
1636
+ format: :single_dash_equals, # -q=95
1637
+ separator: "="
1638
+
1639
+ # Windows-style slash
1640
+ option :output,
1641
+ type: :file,
1642
+ cli: "/Output",
1643
+ format: :slash_space, # /Output file.pdf
1644
+ separator: " "
1645
+ end
1646
+ ```
1647
+
1648
+ ### Profile-Specific Option Formats
1649
+
1650
+ Different platforms/shells use different formats:
1651
+
1652
+ ```ruby
1653
+ tool "mytool" do
1654
+ # Unix/modern: double-dash equals
1655
+ profile :modern_unix,
1656
+ platform: [:macos, :linux],
1657
+ shell: [:bash, :zsh] do
1658
+ option_style :double_dash_equals
1659
+ end
1660
+
1661
+ # Windows PowerShell: slash with space
1662
+ profile :windows_powershell,
1663
+ platform: :windows,
1664
+ shell: :powershell do
1665
+ option_style :slash_space
1666
+ end
1667
+
1668
+ # Windows cmd: slash with colon
1669
+ profile :windows_cmd,
1670
+ platform: :windows,
1671
+ shell: :cmd do
1672
+ option_style :slash_colon
1673
+ end
1674
+ end
1675
+ ```
1676
+
1677
+ ### Real-World Inkscape Example
1678
+
1679
+ ```ruby
1680
+ class Inkscape < Ukiryu::Wrapper
1681
+ tool "inkscape" do
1682
+ names "inkscape", "inkscapecom"
1683
+ detect_version { run "--version"; match /Inkscape (\d+\.\d+)/ }
1684
+ end
1685
+
1686
+ command :export do
1687
+ # Inkscape has MANY option formats!
1688
+
1689
+ # Double-dash equals: --export-filename=out.pdf
1690
+ option :output,
1691
+ type: :file,
1692
+ cli: "--export-filename=",
1693
+ format: :double_dash_equals
1694
+
1695
+ # Double-dash space: --export-type pdf
1696
+ option :format,
1697
+ type: :symbol,
1698
+ values: [:svg, :png, :ps, :eps, :pdf, :emf, :wmf, :xaml],
1699
+ cli: "--export-type",
1700
+ format: :double_dash_space,
1701
+ separator: " "
1702
+
1703
+ # Single-letter with equals: -o=out.pdf (alias for --export-filename)
1704
+ option :output_short,
1705
+ type: :file,
1706
+ cli: "-o=",
1707
+ format: :single_dash_equals
1708
+
1709
+ # Single-letter space: -d 96
1710
+ option :dpi,
1711
+ type: :integer,
1712
+ cli: "-d",
1713
+ format: :single_dash_space,
1714
+ separator: " "
1715
+
1716
+ # Flag: --export-plain-svg
1717
+ flag :plain,
1718
+ cli: "--export-plain-svg"
1719
+
1720
+ # Single-letter flag: -l (alias for --export-plain-svg)
1721
+ flag :plain_short,
1722
+ cli: "-l"
1723
+
1724
+ # Variadic input files: inkscape [options] file1 [file2 ...]
1725
+ argument :inputs,
1726
+ type: :file,
1727
+ position: :last,
1728
+ variadic: true,
1729
+ min: 1
1730
+ end
1731
+ end
1732
+ ```
1733
+
1734
+ **Usage examples:**
1735
+
1736
+ ```ruby
1737
+ # Modern double-dash equals
1738
+ Inkscape.export(
1739
+ inputs: ["diagram.svg"],
1740
+ output: "diagram.pdf",
1741
+ format: :pdf,
1742
+ plain: true
1743
+ )
1744
+ # CLI: inkscape --export-filename=diagram.pdf --export-type pdf --export-plain-svg diagram.svg
1745
+
1746
+ # Using short options
1747
+ Inkscape.export(
1748
+ inputs: ["diagram.svg"],
1749
+ output_short: "diagram.png",
1750
+ dpi: 300
1751
+ )
1752
+ # CLI: inkscape -o=diagram.png -d 300 diagram.svg
1753
+
1754
+ # Multiple input files
1755
+ Inkscape.export(
1756
+ inputs: ["a.svg", "b.svg", "c.svg"],
1757
+ format: :png
1758
+ )
1759
+ # CLI: inkscape --export-type png a.svg b.svg c.svg
1760
+ # Results in: a.png, b.png, c.png (same directory, same base name)
1761
+ ```
1762
+
1763
+ ---
1764
+
1765
+ ## Value Separators & Special Values
1766
+
1767
+ Many tools accept multiple values in a single option using separators:
1768
+
1769
+ ### Separator Types
1770
+
1771
+ | Separator | Example | DSL | Use Case |
1772
+ |-----------|---------|-----|----------|
1773
+ | Comma | `--types=svg,png,pdf` | `separator: ","` | File types, extensions |
1774
+ | Semicolon | `--ids=obj1;obj2;obj3` | `separator: ";"` | Object IDs, lists |
1775
+ | Colon | `-r300x600` | `separator: "x"` | Dimensions (width×height) |
1776
+ | Pipe | `--files=a\|b\|c` | `separator: "\|"` | Alternative paths |
1777
+ | Space | `--search path1 path2` | `separator: " "` | Multiple paths |
1778
+ | Plus | `--pages=1+3+5` | `separator: "+"` | Page numbers |
1779
+
1780
+ ### Value Separator DSL
1781
+
1782
+ ```ruby
1783
+ command :process do
1784
+ # Comma-separated values
1785
+ option :formats,
1786
+ type: :array,
1787
+ of: :symbol,
1788
+ values: [:svg, :png, :pdf, :eps],
1789
+ cli: "--export-type=",
1790
+ separator: "," # --export-type=svg,png,pdf
1791
+
1792
+ # Semicolon-separated (Inkscape object IDs)
1793
+ option :objects,
1794
+ type: :array,
1795
+ of: :string,
1796
+ cli: "--export-id=",
1797
+ separator: ";" # --export-id=obj1;obj2;obj3
1798
+
1799
+ # Colon-separated (Ghostscript resolution)
1800
+ option :resolution,
1801
+ type: :array,
1802
+ of: :integer,
1803
+ size: 2, # Exactly 2 values
1804
+ cli: "-r",
1805
+ separator: "x", # -r300x600
1806
+ format: :single_dash_space
1807
+
1808
+ # Space-separated paths (Ghostscript -I)
1809
+ option :lib_paths,
1810
+ type: :array,
1811
+ of: :file,
1812
+ cli: "-I",
1813
+ separator: " ", # -I path1 path2 path3
1814
+ format: :single_dash_space
1815
+ end
1816
+ ```
1817
+
1818
+ ### Real-World Ghostscript Example
1819
+
1820
+ ```ruby
1821
+ class Ghostscript < Ukiryu::Wrapper
1822
+ tool "gs" do
1823
+ names "gs", "gswin64c", "gswin32c", "gsos2"
1824
+ end
1825
+
1826
+ command :convert do
1827
+ # -sDEVICE=pdfwrite (string parameter, dash-s)
1828
+ option :device,
1829
+ type: :symbol,
1830
+ values: [:pdfwrite, :eps2write, :png16m, :jpeg],
1831
+ cli: "-sDEVICE=",
1832
+ format: :single_dash_equals
1833
+
1834
+ # -r300 or -r300x600 (resolution, dash-r)
1835
+ option :resolution,
1836
+ type: :array,
1837
+ of: :integer,
1838
+ size: [1, 2], # 1 or 2 values
1839
+ cli: "-r",
1840
+ format: :single_dash_space,
1841
+ separator: "x",
1842
+ value_position: :embedded # -r300 (no space)
1843
+
1844
+ # -sOutputFile=out.pdf (output, dash-s string)
1845
+ option :output,
1846
+ type: :file,
1847
+ cli: "-sOutputFile=",
1848
+ format: :single_dash_equals
1849
+
1850
+ # -dSAFER (define name, dash-d)
1851
+ flag :safer,
1852
+ cli: "-dSAFER",
1853
+ format: :single_dash
1854
+
1855
+ # -q (quiet, single letter flag)
1856
+ flag :quiet,
1857
+ cli: "-q"
1858
+
1859
+ # -I path1 path2 path3 (search paths, variadic option)
1860
+ option :lib_paths,
1861
+ type: :array,
1862
+ of: :file,
1863
+ cli: "-I",
1864
+ format: :single_dash_space,
1865
+ separator: " ",
1866
+ repeatable: true # Can use -I multiple times
1867
+
1868
+ # Input file(s) at end
1869
+ argument :inputs,
1870
+ type: :file,
1871
+ position: :last,
1872
+ variadic: true,
1873
+ min: 1
1874
+ end
1875
+ end
1876
+ ```
1877
+
1878
+ **Usage examples:**
1879
+
1880
+ ```ruby
1881
+ # Simple PDF conversion
1882
+ Ghostscript.convert(
1883
+ inputs: ["input.ps"],
1884
+ output: "output.pdf",
1885
+ device: :pdfwrite,
1886
+ safer: true,
1887
+ quiet: true
1888
+ )
1889
+ # CLI: gs -sDEVICE=pdfwrite -sOutputFile=output.pdf -dSAFER -q input.ps
1890
+
1891
+ # With resolution (single value)
1892
+ Ghostscript.convert(
1893
+ inputs: ["input.pdf"],
1894
+ output: "output.png",
1895
+ device: :png16m,
1896
+ resolution: [300]
1897
+ )
1898
+ # CLI: gs -sDEVICE=png16m -sOutputFile=output.png -r300 input.pdf
1899
+
1900
+ # With resolution (two values: X×Y)
1901
+ Ghostscript.convert(
1902
+ inputs: ["input.pdf"],
1903
+ output: "output.png",
1904
+ device: :png16m,
1905
+ resolution: [300, 600]
1906
+ )
1907
+ # CLI: gs -sDEVICE=png16m -sOutputFile=output.png -r300x600 input.pdf
1908
+
1909
+ # With library paths
1910
+ Ghostscript.convert(
1911
+ inputs: ["input.ps"],
1912
+ output: "output.pdf",
1913
+ lib_paths: ["/usr/local/share/ghostscript", "/custom/path"]
1914
+ )
1915
+ # CLI: gs -I /usr/local/share/ghostscript /custom/path -sDEVICE=pdfwrite ...
1916
+ ```
1917
+
1918
+ ### Special Values
1919
+
1920
+ Some tools use special values with meaning:
1921
+
1922
+ ```ruby
1923
+ command :convert do
1924
+ # Ghostscript: -sOutputFile=- (stdout)
1925
+ option :output,
1926
+ type: :file,
1927
+ cli: "-sOutputFile=",
1928
+ special_values: {
1929
+ stdout: "-", # -sOutputFile=-
1930
+ pipe: "%pipe%lpr", # -sOutputFile=%pipe%lpr
1931
+ template: "%d" # -sOutputFile=file%d.pdf
1932
+ }
1933
+
1934
+ # Inkscape: --export-type=TYPE[,TYPE]* (multiple types)
1935
+ option :export_types,
1936
+ type: :array,
1937
+ of: :symbol,
1938
+ values: [:svg, :png, :ps, :eps, :pdf],
1939
+ cli: "--export-type=",
1940
+ separator: ","
1941
+ end
1942
+ ```
1943
+
1944
+ ---
1945
+
1946
+ ## Environment Variables
1947
+
1948
+ Tools often require custom environment variables:
1949
+
1950
+ ### Environment Variable DSL
1951
+
1952
+ ```ruby
1953
+ command :run do
1954
+ # Set environment variables for this command
1955
+ env_vars do
1956
+ set "MY_VAR", from: :option # From user-provided option
1957
+ set "PATH", append: "/usr/local/bin" # Append to existing
1958
+ set "DISPLAY", value: "" # Set explicitly (headless mode)
1959
+ set "MAGICK_CONFIGURE_PATH", from: :option, default: "/etc/ImageMagick"
1960
+ end
1961
+
1962
+ option :my_var,
1963
+ type: :string,
1964
+ desc: "Value for MY_VAR environment variable"
1965
+
1966
+ option :config_path,
1967
+ type: :file,
1968
+ desc: "Path for MAGICK_CONFIGURE_PATH"
1969
+ end
1970
+ ```
1971
+
1972
+ ### Real-World Examples
1973
+
1974
+ **Inkscape headless operation:**
1975
+
1976
+ ```ruby
1977
+ class Inkscape < Ukiryu::Wrapper
1978
+ command :export do
1979
+ # Disable display on Unix/macOS for headless operation
1980
+ env_vars do
1981
+ set "DISPLAY", value: "", on: [:macos, :linux]
1982
+ # Windows: no DISPLAY variable needed
1983
+ end
1984
+ end
1985
+ end
1986
+ ```
1987
+
1988
+ **ImageMagick with custom config:**
1989
+
1990
+ ```ruby
1991
+ class ImageMagick < Ukiryu::Wrapper
1992
+ command :convert do
1993
+ env_vars do
1994
+ set "MAGICK_CONFIGURE_PATH", from: :config_dir
1995
+ set "MAGICK_HOME", from: :install_dir
1996
+ end
1997
+
1998
+ option :config_dir,
1999
+ type: :file,
2000
+ desc: "Custom configuration directory"
2001
+
2002
+ option :install_dir,
2003
+ type: :file,
2004
+ desc: "ImageMagick installation directory"
2005
+ end
2006
+ end
2007
+ ```
2008
+
2009
+ **Ghostscript with library path:**
2010
+
2011
+ ```ruby
2012
+ class Ghostscript < Ukiryu::Wrapper
2013
+ command :convert do
2014
+ env_vars do
2015
+ # GS_LIB is used to find initialization files and fonts
2016
+ set "GS_LIB", from: :lib_path
2017
+ end
2018
+
2019
+ option :lib_path,
2020
+ type: :file,
2021
+ desc: "Ghostscript library path"
2022
+ end
2023
+ end
2024
+ ```
2025
+
2026
+ ### Shell-Specific Environment Variable Syntax
2027
+
2028
+ | Shell | Syntax | Example |
2029
+ |-------|--------|---------|
2030
+ | Bash/Zsh | `VAR=value` | `DISPLAY="" ./command` |
2031
+ | PowerShell | `$ENV:VAR = "value"` | `$ENV:DISPLAY = ""; ./command` |
2032
+ | Cmd | `set VAR=value` | `set DISPLAY= && command` |
2033
+
2034
+ Ukiryu handles the correct syntax for each shell automatically.
2035
+
2036
+ ---
2037
+
2038
+ ## Subcommands (Git-Style)
2039
+
2040
+ Many tools use subcommands with their own options:
2041
+
2042
+ ### Subcommand DSL
2043
+
2044
+ ```ruby
2045
+ class Git < Ukiryu::Wrapper
2046
+ tool "git" do
2047
+ names "git"
2048
+ end
2049
+
2050
+ # Subcommand: git add
2051
+ subcommand :add, desc: "Add files to staging" do
2052
+ argument :files,
2053
+ type: :file,
2054
+ variadic: true,
2055
+ min: 0 # Can add nothing (stage current changes)
2056
+
2057
+ flag :all, cli: "--all"
2058
+ flag :update, cli: "--update"
2059
+ flag :verbose, cli: "-v"
2060
+ end
2061
+
2062
+ # Subcommand: git commit
2063
+ subcommand :commit, desc: "Commit changes" do
2064
+ flag :all, cli: "--all", cli_short: "-a"
2065
+ flag :amend, cli: "--amend"
2066
+ flag :verbose, cli: "--verbose", cli_short: "-v"
2067
+
2068
+ option :message,
2069
+ type: :string,
2070
+ cli: "-m",
2071
+ format: :single_dash_space,
2072
+ separator: " "
2073
+
2074
+ flag :no_edit, cli: "--no-edit"
2075
+ end
2076
+
2077
+ # Subcommand: git push
2078
+ subcommand :push, desc: "Push to remote" do
2079
+ argument :remote,
2080
+ type: :string,
2081
+ required: false,
2082
+ default: "origin"
2083
+
2084
+ argument :branch,
2085
+ type: :string,
2086
+ required: false
2087
+
2088
+ flag :force, cli: "--force", cli_short: "-f"
2089
+ flag :set_upstream, cli: "--set-upstream", cli_short: "-u"
2090
+ flag :verbose, cli: "--verbose", cli_short: "-v"
2091
+ end
2092
+ end
2093
+ ```
2094
+
2095
+ ### Subcommand Usage
2096
+
2097
+ ```ruby
2098
+ # git add file1.rb file2.rb
2099
+ Git.add(files: ["file1.rb", "file2.rb"], all: false)
2100
+ # CLI: git add file1.rb file2.rb
2101
+
2102
+ # git add -A
2103
+ Git.add(files: [], all: true)
2104
+ # CLI: git add --all
2105
+
2106
+ # git commit -m "Fix bug"
2107
+ Git.commit(message: "Fix bug")
2108
+ # CLI: git commit -m "Fix bug"
2109
+
2110
+ # git commit -am "Update"
2111
+ Git.commit(all: true, message: "Update")
2112
+ # CLI: git commit -a -m "Update"
2113
+
2114
+ # git push origin main
2115
+ Git.push(remote: "origin", branch: "main")
2116
+ # CLI: git push origin main
2117
+
2118
+ # git push (uses defaults)
2119
+ Git.push
2120
+ # CLI: git push
2121
+ ```
2122
+
2123
+ ### Subcommand with Profiles
2124
+
2125
+ Different subcommands may have different profiles:
2126
+
2127
+ ```ruby
2128
+ tool "docker" do
2129
+ # docker build
2130
+ subcommand :build do
2131
+ profile :modern,
2132
+ platform: :any,
2133
+ shell: :any,
2134
+ version: ">= 20.0" do
2135
+ # Docker 20+ uses --build-arg
2136
+ option :build_args,
2137
+ type: :hash,
2138
+ cli: "--build-arg="
2139
+ end
2140
+
2141
+ profile :legacy,
2142
+ platform: :any,
2143
+ shell: :any,
2144
+ version: "< 20.0" do
2145
+ # Docker < 20 used different syntax
2146
+ option :build_args,
2147
+ type: :hash,
2148
+ cli: "--build-arg"
2149
+ end
2150
+ end
2151
+ end
2152
+ ```
2153
+
2154
+ ---
2155
+
2156
+ ## Shell-Specific Examples (cmd vs PowerShell)
2157
+
2158
+ ### Windows cmd.exe
2159
+
2160
+ ```ruby
2161
+ tool "mytool" do
2162
+ profile :windows_cmd,
2163
+ platform: :windows,
2164
+ shell: :cmd do
2165
+ # cmd.exe uses: /option value
2166
+ option_style :slash_space
2167
+
2168
+ # Special escaping for cmd (^ is escape char)
2169
+ escape_with :caret
2170
+
2171
+ # Environment variables: %VAR%
2172
+ env_var_format :percent
2173
+ end
2174
+ end
2175
+ ```
2176
+
2177
+ **cmd.exe escaping:**
2178
+ ```ruby
2179
+ # In cmd: paths with spaces need quotes
2180
+ MyTool.run(file: "C:\\Program Files\\input.txt")
2181
+ # CLI: mytool /Input "C:\Program Files\input.txt"
2182
+
2183
+ # Special characters need caret escaping
2184
+ MyTool.run(text: "hello & world")
2185
+ # CLI: mytool /Input "hello ^& world"
2186
+ ```
2187
+
2188
+ ### Windows PowerShell
2189
+
2190
+ ```ruby
2191
+ tool "mytool" do
2192
+ profile :windows_powershell,
2193
+ platform: :windows,
2194
+ shell: :powershell do
2195
+ # PowerShell can use: --option=value (Unix-style)
2196
+ option_style :double_dash_equals
2197
+
2198
+ # Special escaping for PowerShell (` is escape char)
2199
+ escape_with :backtick
2200
+
2201
+ # Environment variables: $ENV:VAR
2202
+ env_var_format :env_prefix
2203
+ end
2204
+ end
2205
+ ```
2206
+
2207
+ **PowerShell escaping:**
2208
+ ```ruby
2209
+ # In PowerShell: backtick escaping for special chars
2210
+ MyTool.run(text: "hello `& world")
2211
+ # CLI: mytool --input "hello `& world"
2212
+
2213
+ # Environment variables
2214
+ env_vars do
2215
+ set "MY_VAR", from: :option
2216
+ # PowerShell: $ENV:MY_VAR = "value"
2217
+ end
2218
+ ```
2219
+
2220
+ ### Comparison Table
2221
+
2222
+ | Feature | Bash/Zsh | PowerShell | cmd.exe |
2223
+ |---------|----------|------------|---------|
2224
+ | Option style | `--flag=value` | `--flag=value` | `/flag value` |
2225
+ | Quote | `'` | `'` | `"` |
2226
+ | Escape char | `\` | `` ` `` | `^` |
2227
+ | Env var | `$VAR` | `$ENV:VAR` | `%VAR%` |
2228
+ | Path separator | `/` | `/` or `\` | `\` |
2229
+ | Special chars escape | `\` & \| > < | `` ` `` $ " | `^` & \| < > |
2230
+
2231
+ ### Example: Same Command, Different Shells
2232
+
2233
+ ```ruby
2234
+ class Converter < Ukiryu::Wrapper
2235
+ tool "convert" do
2236
+ names "convert"
2237
+
2238
+ command :process do
2239
+ option :input,
2240
+ type: :file,
2241
+ cli: "--input="
2242
+
2243
+ option :output,
2244
+ type: :file,
2245
+ cli: "--output="
2246
+
2247
+ option :quality,
2248
+ type: :integer,
2249
+ cli: "--quality="
2250
+
2251
+ flag :verbose, cli: "--verbose"
2252
+
2253
+ argument :files,
2254
+ type: :file,
2255
+ variadic: true
2256
+ end
2257
+ end
2258
+ end
2259
+ ```
2260
+
2261
+ **Bash (macOS/Linux):**
2262
+ ```ruby
2263
+ Converter.process(
2264
+ input: "diagram.svg",
2265
+ output: "result.png",
2266
+ quality: 95,
2267
+ verbose: true,
2268
+ files: ["overlay.png"]
2269
+ )
2270
+ # CLI: convert --input='diagram.svg' --output='result.png' --quality=95 --verbose 'overlay.png'
2271
+ ```
2272
+
2273
+ **PowerShell:**
2274
+ ```ruby
2275
+ Converter.process(
2276
+ input: "C:\\Files\\diagram.svg",
2277
+ output: "C:\\Files\\result.png",
2278
+ quality: 95,
2279
+ verbose: true,
2280
+ files: ["C:\\Files\\overlay.png"]
2281
+ )
2282
+ # CLI: convert --input='C:\Files\diagram.svg' --output='C:\Files\result.png' --quality=95 --verbose 'C:\Files\overlay.png'
2283
+ ```
2284
+
2285
+ **cmd.exe:**
2286
+ ```ruby
2287
+ Converter.process(
2288
+ input: "C:\\Files\\diagram.svg",
2289
+ output: "C:\\Files\\result.png",
2290
+ quality: 95,
2291
+ verbose: true,
2292
+ files: ["C:\\Files\\overlay.png"]
2293
+ )
2294
+ # CLI: convert /Input "C:\Files\diagram.svg" /Output "C:\Files\result.png" /Quality 95 /Verbose "C:\Files\overlay.png"
2295
+ ```
2296
+
2297
+ ---
2298
+
2299
+ ## Command Declaration
2300
+
2301
+ ```ruby
2302
+ class MyToolWrapper < Ukiryu::Wrapper
2303
+ tool "mytool" do
2304
+ # ==================================================
2305
+ # COMMAND: export
2306
+ # ==================================================
2307
+ command :export, desc: "Export to different format" do
2308
+
2309
+ # POSITIONAL argument (order matters!)
2310
+ argument :input,
2311
+ type: :file,
2312
+ position: 1,
2313
+ required: true
2314
+
2315
+ # OPTION (named parameter with value)
2316
+ option :format,
2317
+ type: :symbol,
2318
+ values: [:pdf, :eps, :svg, :png],
2319
+ cli: "--format=",
2320
+ required: true
2321
+
2322
+ # FLAG (boolean, presence indicates true)
2323
+ flag :plain,
2324
+ cli: "--plain",
2325
+ default: false
2326
+
2327
+ # OUTPUT (where output goes)
2328
+ output :file,
2329
+ type: :file,
2330
+ via: :option, # Uses --export-filename= option
2331
+ cli: "--export-filename=",
2332
+ position: :last
2333
+ end
2334
+
2335
+ # ==================================================
2336
+ # COMMAND: query
2337
+ # ==================================================
2338
+ command :query, desc: "Query document properties" do
2339
+
2340
+ argument :input,
2341
+ type: :file,
2342
+ required: true
2343
+
2344
+ flag :width,
2345
+ cli: "--query-width"
2346
+
2347
+ flag :height,
2348
+ cli: "--query-height"
2349
+
2350
+ # Parse output into hash
2351
+ parse_output do |stdout|
2352
+ {
2353
+ width: stdout[/Width:\s*(\d+)/, 1].to_i,
2354
+ height: stdout[/Height:\s*(\d+)/, 1].to_i
2355
+ }
2356
+ end
2357
+ end
2358
+ end
2359
+ ```
2360
+
2361
+ ### Parameter/Option/Flag Distinctions
2362
+
2363
+ | Concept | DSL | Description | Example |
2364
+ |---------|-----|-------------|--------|
2365
+ | **Positional Argument** | `argument` | Position in command line, no `--flag` | `input.pdf` |
2366
+ | **Variadic Argument** | `argument variadic: true` | Accepts multiple values (one or more) | `file1.pdf file2.pdf file3.pdf` |
2367
+ | **Option** | `option` | Named parameter with value, has `--flag=` syntax | `--format=pdf` |
2368
+ | **Flag** | `flag` | Boolean option (present/absent), no value | `--plain` |
2369
+
2370
+ ### Variadic Arguments (Multiple Values)
2371
+
2372
+ Many CLI tools accept multiple values for a single argument position:
2373
+
2374
+ | Pattern | Example CLI | DSL |
2375
+ |---------|-------------|-----|
2376
+ | `command arg1 arg2*` | `cp file1 file2... dest` | `arg1` (pos 1), `arg2` (pos 2, variadic) |
2377
+ | `command arg1* arg2` | `convert input... output` | `arg1` (pos 1, variadic), `arg2` (pos 2) |
2378
+ | `command arg1 arg2* arg3` | `tar -cf archive files...` | `arg1`, `arg2*`, `arg3` with positions |
2379
+ | `command arg1*` | `cat files...` | Single variadic argument |
2380
+ | `command arg1 arg2*` (zero ok) | `git add [files...]` | `variadic: true, min: 0` |
2381
+
2382
+ #### DSL for Variadic Arguments
2383
+
2384
+ ```ruby
2385
+ command :export do
2386
+ # Single argument (standard)
2387
+ argument :input,
2388
+ type: :file,
2389
+ position: 1,
2390
+ required: true
2391
+
2392
+ # Variadic: one or more files
2393
+ argument :sources,
2394
+ type: :file,
2395
+ position: 2,
2396
+ variadic: true # Default: min: 1
2397
+
2398
+ # Variadic: zero or more files (optional)
2399
+ argument :extras,
2400
+ type: :file,
2401
+ position: 3,
2402
+ variadic: true,
2403
+ min: 0
2404
+
2405
+ # Required argument after variadic (must come after)
2406
+ argument :output,
2407
+ type: :file,
2408
+ position: :last,
2409
+ required: true
2410
+ end
2411
+ ```
2412
+
2413
+ #### Usage Examples
2414
+
2415
+ ```ruby
2416
+ # Single variadic at end
2417
+ # CLI: inkscape output.pdf input1.svg input2.svg input3.svg
2418
+ Inkscape.export(
2419
+ output: "output.pdf",
2420
+ inputs: ["input1.svg", "input2.svg", "input3.svg"]
2421
+ )
2422
+
2423
+ # Variadic in middle
2424
+ # CLI: convert input1.jpg input2.jpg output.png
2425
+ Convert.convert(
2426
+ sources: ["input1.jpg", "input2.jpg"],
2427
+ output: "output.png"
2428
+ )
2429
+
2430
+ # Multiple variadic arguments
2431
+ # CLI: tool required.txt optional1.txt optional2.txt final.txt
2432
+ Tool.run(
2433
+ required: "required.txt",
2434
+ optional: ["optional1.txt", "optional2.txt"], # Can be empty []
2435
+ final: "final.txt"
2436
+ )
2437
+
2438
+ # Variadic with min: 0 (optional)
2439
+ # CLI: git add [files...]
2440
+ Git.add(
2441
+ files: ["file1.rb", "file2.rb"]
2442
+ )
2443
+ Git.add # No files - valid when min: 0
2444
+ ```
2445
+
2446
+ #### Variadic Argument Rules
2447
+
2448
+ 1. **Cardinality options:**
2449
+ - `variadic: true` - One or more values (default `min: 1`)
2450
+ - `variadic: true, min: 0` - Zero or more values (optional)
2451
+ - `variadic: true, min: 2` - At least N values (custom minimum)
2452
+
2453
+ 2. **Position specification:**
2454
+ - Single variadic can use `position: N`
2455
+ - Multiple variadic arguments require explicit `position: N` for each
2456
+ - `position: :last` always comes after all other arguments
2457
+
2458
+ 3. **Type validation:**
2459
+ - Each value in the array is validated individually
2460
+ - If any value fails validation, entire command fails before execution
2461
+
2462
+ 4. **Command building:**
2463
+ - Variadic arguments expand to N CLI arguments in order
2464
+ - `inputs: ["a.svg", "b.svg"]` → `a.svg b.svg` in command line
2465
+
2466
+ #### Real-World Examples
2467
+
2468
+ **Copy command (cp):**
2469
+ ```ruby
2470
+ class Cp < Ukiryu::Wrapper
2471
+ tool "cp" do
2472
+ names "cp"
2473
+ end
2474
+
2475
+ command :copy do
2476
+ # cp source... destination
2477
+ argument :sources,
2478
+ type: :file,
2479
+ position: 1,
2480
+ variadic: true,
2481
+ min: 1 # At least one source
2482
+
2483
+ argument :destination,
2484
+ type: :file,
2485
+ position: :last,
2486
+ required: true
2487
+ end
2488
+ end
2489
+
2490
+ # Usage:
2491
+ Cp.copy(sources: ["file1.txt", "file2.txt"], destination: "dest/")
2492
+ # CLI: cp file1.txt file2.txt dest/
2493
+ ```
2494
+
2495
+ **ImageMagick convert (inputs before output):**
2496
+ ```ruby
2497
+ class ImageMagick < Ukiryu::Wrapper
2498
+ tool "convert" do
2499
+ names "convert", "magick"
2500
+ end
2501
+
2502
+ command :convert do
2503
+ # convert input... [options] output
2504
+ argument :inputs,
2505
+ type: :file,
2506
+ position: 1,
2507
+ variadic: true,
2508
+ min: 1
2509
+
2510
+ option :resize,
2511
+ type: :string,
2512
+ cli: "-resize "
2513
+
2514
+ argument :output,
2515
+ type: :file,
2516
+ position: :last,
2517
+ required: true
2518
+ end
2519
+ end
2520
+
2521
+ # Usage:
2522
+ ImageMagick.convert(
2523
+ inputs: ["a.jpg", "b.jpg", "c.jpg"],
2524
+ resize: "50%",
2525
+ output: "combined.png"
2526
+ )
2527
+ # CLI: convert a.jpg b.jpg c.jpg -resize 50% combined.png
2528
+ ```
2529
+
2530
+ **Git add (zero or more files):**
2531
+ ```ruby
2532
+ class Git < Ukiryu::Wrapper
2533
+ tool "git" do
2534
+ names "git"
2535
+ end
2536
+
2537
+ command :add do
2538
+ argument :files,
2539
+ type: :file,
2540
+ position: 1,
2541
+ variadic: true,
2542
+ min: 0 # Optional: can add nothing to stage current changes
2543
+ flag :all, cli: "--all"
2544
+ flag :update, cli: "--update"
2545
+ end
2546
+ end
2547
+
2548
+ # Usage:
2549
+ Git.add(files: ["file1.rb", "file2.rb"])
2550
+ # CLI: git add file1.rb file2.rb
2551
+
2552
+ Git.add # No files, but valid
2553
+ # CLI: git add
2554
+ ```
2555
+
2556
+ ### Type System
2557
+
2558
+ All types with validation and escaping:
2559
+
2560
+ | Type | Description | Validation | Escaped | Example |
2561
+ |------|-------------|------------|---------|---------|
2562
+ | `:file` | File path | Must exist, platform-validated | Shell-specific | `file.pdf` |
2563
+ | `:string` | Text string | Custom validation | Shell-specific | `"hello"` |
2564
+ | `:integer` | Whole number | Range check | No escaping | `95` |
2565
+ | `:float` | Decimal | Custom validation | No escaping | `1.5` |
2566
+ | `:symbol` | Enum value | Must be in whitelist | No escaping | `:pdf` |
2567
+ | `:boolean` | Boolean flag | N/A | Flag presence | `--verbose` |
2568
+ | `:uri` | URL/URI | Must be valid URI | Shell-specific | `https://example.com` |
2569
+ | `:datetime` | Date/time | Must be parseable | Formatted, then escaped | `2025-01-21` |
2570
+ | `:hash` | Key-value pairs | Recursively escaped | Merge into env vars | `{key: "value"}` |
2571
+ | `:array` | Multiple values | Separator-based | Multiple arguments | `["a", "b"]` |
2572
+
2573
+ ---
2574
+
2575
+ ## Shell Classes
2576
+
2577
+ ### Base Interface
2578
+
2579
+ ```ruby
2580
+ module Ukiryu
2581
+ module Shell
2582
+ class Base
2583
+ # Identify the shell
2584
+ def name => Symbol
2585
+
2586
+ # Escape a string for this shell
2587
+ def escape(string) => String
2588
+
2589
+ # Quote an argument
2590
+ def quote(string) => String
2591
+
2592
+ # Format a file path
2593
+ def format_path(path) => String
2594
+
2595
+ # Format environment variable reference
2596
+ def env_var(name) => String
2597
+
2598
+ # Join executable and arguments
2599
+ def join(executable, *args) => String
2600
+ end
2601
+ end
2602
+ end
2603
+ ```
2604
+
2605
+ ### Bash Implementation
2606
+
2607
+ ```ruby
2608
+ module Ukiryu
2609
+ module Shell
2610
+ class Bash < Base
2611
+ def name; :bash; end
2612
+
2613
+ def escape(string)
2614
+ # Single-quote string (literal)
2615
+ string.gsub("'") { "'\\''" }
2616
+ end
2617
+
2618
+ def quote(string)
2619
+ "'#{escape(string)}'"
2620
+ end
2621
+
2622
+ def format_path(path)
2623
+ path # Unix paths are fine
2624
+ end
2625
+
2626
+ def env_var(name)
2627
+ "$#{name}"
2628
+ end
2629
+
2630
+ def join(executable, *args)
2631
+ [executable, *args.map { |a| quote(a) }].join(" ")
2632
+ end
2633
+ end
2634
+ end
2635
+ end
2636
+ ```
2637
+
2638
+ ### PowerShell Implementation
2639
+
2640
+ ```ruby
2641
+ module Ukiryu
2642
+ module Shell
2643
+ class PowerShell < Base
2644
+ def name; :powershell; end
2645
+
2646
+ def escape(string)
2647
+ # Backtick escaping for: ` $ "
2648
+ string.gsub(/[`"$]/) { "`$0" }
2649
+ end
2650
+
2651
+ def quote(string)
2652
+ "'#{escape(string)}'"
2653
+ end
2654
+
2655
+ def format_path(path)
2656
+ path # PowerShell handles forward slashes fine
2657
+ end
2658
+
2659
+ def env_var(name)
2660
+ "$ENV:#{name}"
2661
+ end
2662
+
2663
+ def join(executable, *args)
2664
+ [executable, *args.map { |a| quote(a) }].join(" ")
2665
+ end
2666
+ end
2667
+ end
2668
+ end
2669
+ ```
2670
+
2671
+ ### Cmd Implementation
2672
+
2673
+ ```ruby
2674
+ module Ukiryu
2675
+ module Shell
2676
+ class Cmd < Base
2677
+ def name; :cmd; end
2678
+
2679
+ def escape(string)
2680
+ # Caret is escape character
2681
+ string.gsub(/[%^<>&|]/, '^^0')
2682
+ end
2683
+
2684
+ def quote(string)
2685
+ if string =~ /[ \t]/
2686
+ "\"#{escape(string)}\""
2687
+ else
2688
+ escape(string)
2689
+ end
2690
+ end
2691
+
2692
+ def format_path(path)
2693
+ # Convert to backslashes
2694
+ path.gsub('/', '\\')
2695
+ end
2696
+
2697
+ def env_var(name)
2698
+ "%#{name}%"
2699
+ end
2700
+
2701
+ def join(executable, *args)
2702
+ [executable, *args.map { |a| quote(a) }].join(" ")
2703
+ end
2704
+ end
2705
+ end
2706
+ end
2707
+ ```
2708
+
2709
+ ---
2710
+
2711
+ ## Implementation Phases
2712
+
2713
+ ### Phase 1: Foundation (6-8 weeks)
2714
+ - Core gem structure
2715
+ - Platform detection
2716
+ - Shell detection (EXPLICIT)
2717
+ - Basic type system (file, string, integer, symbol, boolean)
2718
+ - Basic command execution
2719
+ - Bash shell implementation
2720
+
2721
+ ### Phase 2: Additional Types (3-4 weeks)
2722
+ - Float, URI, datetime, hash types
2723
+ - Array type with separators
2724
+ - Path validation (platform-specific)
2725
+
2726
+ ### Phase 3: More Shells (3-4 weeks)
2727
+ - PowerShell shell implementation
2728
+ - Cmd shell implementation
2729
+ - Zsh fish implementation
2730
+
2731
+ ### Phase 4: Profile System (4-6 weeks)
2732
+ - Profile DSL
2733
+ - Exact matching algorithm
2734
+ - Version compatibility
2735
+ - Profile error messages
2736
+
2737
+ ### Phase 5: Complete DSL (4-6 weeks)
2738
+ - Argument/option/flag distinction
2739
+ - Position specification
2740
+ - Output specification
2741
+ - Conditional flags
2742
+ - Option mapping
2743
+
2744
+ ### Phase 6: Testing & Docs (6-8 weeks)
2745
+ - GHA test matrix (platform × shell × ruby)
2746
+ - Shell-specific documentation
2747
+ - Platform-specific documentation
2748
+ - Example wrappers
2749
+
2750
+ ### Phase 7: Vectory Migration (4-6 weeks)
2751
+ - Replace InkscapeWrapper
2752
+ - Replace GhostscriptWrapper
2753
+ - Full testing on all platforms
2754
+
2755
+ ---
2756
+
2757
+ ## Success Criteria
2758
+
2759
+ Ukiryu succeeds when:
2760
+
2761
+ 1. ✅ **Explicit shell detection** - Raises clear error if shell unknown
2762
+ 2. ✅ **Exact profile matching** - No fallbacks (version compatibility OK)
2763
+ 3. ✅ **Type-safe parameters** - All types validated, shell-escaped automatically
2764
+ 4. ✅ **Semantic validation** - Platform-specific paths rejected on wrong platforms
2765
+ 5. ✅ **YAML profile registry** - Tool definitions maintained in YAML, not code
2766
+ 6. ✅ **Schema validation** - All YAML profiles validated against JSON Schema
2767
+ 7. ✅ **Profile inheritance** - Avoid duplication with YAML inheritance
2768
+ 8. ✅ **Vectory replacement** - Full replacement of Vectory wrappers in 70% less code
2769
+ 9. ✅ **GHA testing** - All platform×shell combinations tested
2770
+ 10. ✅ **Complete documentation** - Separate docs for each shell/platform
2771
+ 11. ✅ **Zero dependencies** - Ruby stdlib only
2772
+ 12. ✅ **Argument/option/flag distinction** - Clear DSL concepts
2773
+ 13. ✅ **PATH-only on Unix** - No hardcoded paths on Unix/macOS
2774
+ 14. ✅ **Community registry** - Separate registry repo for tool profiles
2775
+ 15. ✅ **Option format variations** - Support all CLI option formats
2776
+ 16. ✅ **Value separators** - Comma, semicolon, colon, space, pipe, plus
2777
+ 17. ✅ **Environment variables** - Per-command environment variable support
2778
+ 18. ✅ **Subcommands** - Git-style subcommand support
2779
+ 19. ✅ **Shell-specific examples** - cmd.exe vs PowerShell examples
2780
+
2781
+ ---
2782
+
2783
+ ## Key Insights
2784
+
2785
+ 1. **YAML profiles over Ruby DSL**: Tool definitions should be maintained in YAML files, not Ruby code. This allows:
2786
+ - Non-developers to add/update tools
2787
+ - Faster iteration without gem releases
2788
+ - Community-contributed profile registry
2789
+ - Separation of framework (Ruby) from configuration (YAML)
2790
+
2791
+ 2. **Hybrid architecture**:
2792
+ - **Ruby framework**: Shell detection, escaping, execution, validation
2793
+ - **YAML registry**: Tool definitions, versions, platform profiles
2794
+ - Users can still use Ruby DSL for custom tools not in registry
2795
+
2796
+ 3. **"symbols in shell"**: User correctly pointed out that `:symbol` is a Ruby type, not a shell concept. In the shell, we pass strings. The `:symbol` type is for Ruby-level validation only.
2797
+
2798
+ 4. **PATH environment**: On Unix, tools should be in PATH. No need to hardcode `/usr/bin`. Ukiryu should:
2799
+ - Always search `ENV["PATH"]` first
2800
+ - Add platform-specific paths only for platforms that need them (Windows app bundles, etc.)
2801
+
2802
+ 5. **Option format variations**: CLI tools use many different formats:
2803
+ - `--flag=value` (double-dash equals)
2804
+ - `--flag value` (double-dash space)
2805
+ - `-f=value` (single-dash equals)
2806
+ - `-f value` (single-dash space)
2807
+ - `/flag value` (Windows slash)
2808
+ - `-r300` (embedded value)
2809
+
2810
+ 6. **Value separators**: Many tools accept multiple values:
2811
+ - Comma: `--types=svg,png,pdf`
2812
+ - Semicolon: `--ids=obj1;obj2;obj3`
2813
+ - Colon: `-r300x600` (dimensions)
2814
+ - Space: `-I path1 path2`
2815
+ - Pipe, plus: for special cases
2816
+
2817
+ 7. **Environment variables**: Per-command environment variables:
2818
+ - Inkscape: `DISPLAY=""` for headless on Unix
2819
+ - Ghostscript: `GS_LIB` for library path
2820
+ - ImageMagick: `MAGICK_CONFIGURE_PATH`
2821
+
2822
+ 8. **Subcommands**: Git-style command structure:
2823
+ - `git add`, `git commit`, `git push`
2824
+ - Each subcommand has its own options
2825
+ - Shared options can be inherited
2826
+
2827
+ 9. **Shell differences**:
2828
+ - **Bash/Zsh**: Single-quote escaping, `$VAR`, `/` paths
2829
+ - **PowerShell**: Backtick escaping, `$ENV:VAR`, `/` or `\` paths
2830
+ - **cmd.exe**: Caret escaping, `%VAR%`, `\` paths
2831
+
2832
+ 10. **Explicit > Implicit**: Ukiryu must detect shell and profile explicitly, never guess.
2833
+
2834
+ 11. **Type safety**: Users shouldn't deal with escaping at all—that's Ukiryu's job.
2835
+
2836
+ 12. **Vectory as reference**: Vectory will be the first implementation driving Ukiryu development.
2837
+
2838
+ ---
2839
+
2840
+ ## Registry Organization
2841
+
2842
+ **Registry repo structure:**
2843
+
2844
+ ```
2845
+ ukiryu-register/ # Community registry repo
2846
+ ├── tools/ # Tool definitions (dir by command name)
2847
+ │ ├── inkscape/
2848
+ │ │ ├── 1.0.yaml # Version-specific profiles
2849
+ │ │ ├── 0.92.yaml
2850
+ │ │ └── 0.9.yaml
2851
+ │ ├── ghostscript/
2852
+ │ │ ├── 10.0.yaml
2853
+ │ │ ├── 9.5.yaml
2854
+ │ │ └── 9.0.yaml
2855
+ │ ├── imagemagick/
2856
+ │ │ ├── 7.0.yaml
2857
+ │ │ └── 6.0.yaml
2858
+ │ ├── git/
2859
+ │ │ ├── 2.45.yaml
2860
+ │ │ └── 2.40.yaml
2861
+ │ ├── docker/
2862
+ │ │ ├── 25.0.yaml
2863
+ │ │ └── 24.0.yaml
2864
+ │ └── ...
2865
+ ├── schemas/ # YAML Schema files
2866
+ │ ├── tool-profile.yaml.schema
2867
+ │ ├── command-definition.yaml.schema
2868
+ │ └── registry.yaml.schema
2869
+ ├── docs/ # AsciiDoc documentation
2870
+ │ ├── inkscape.adoc
2871
+ │ ├── ghostscript.adoc
2872
+ │ ├── imagemagick.adoc
2873
+ │ ├── git.adoc
2874
+ │ ├── contributing.adoc
2875
+ │ ├── registry.adoc
2876
+ │ └── README.adoc
2877
+ ├── lib/ # Registry helper library
2878
+ │ └── ukiryu/
2879
+ │ └── registry.rb
2880
+ ├── Gemfile # json-schema gem
2881
+ ├── Rakefile # Validation tasks
2882
+ └── README.adoc # Registry overview
2883
+ ```
2884
+
2885
+ **Ukiryu gem structure:**
2886
+
2887
+ ```
2888
+ ukiryu/ # Ruby gem
2889
+ ├── lib/
2890
+ │ ├── ukiryu.rb
2891
+ │ ├── ukiryu/
2892
+ │ │ ├── registry.rb # YAML loader from registry
2893
+ │ │ ├── tool.rb # Tool from YAML
2894
+ │ │ ├── shell.rb # Shell detection (EXPLICIT)
2895
+ │ │ ├── shell/
2896
+ │ │ │ ├── bash.rb
2897
+ │ │ │ ├── zsh.rb
2898
+ │ │ │ ├── powershell.rb
2899
+ │ │ │ └── cmd.rb
2900
+ │ │ ├── type.rb # Type validation
2901
+ │ │ ├── executor.rb # Command execution
2902
+ │ │ ├── version.rb # Version detection
2903
+ │ │ └── schema_validator.rb # JSON::Schema wrapper
2904
+ │ └── ukiryu.yml # Bundled profiles (optional)
2905
+ ├── spec/
2906
+ │ └── ...
2907
+ ├── Gemfile # json-schema dependency
2908
+ ├── Rakefile
2909
+ └── README.adoc
2910
+ ```
2911
+
2912
+ **Gemfile:**
2913
+
2914
+ ```ruby
2915
+ # ukiryu-register/Gemfile
2916
+ source 'https://rubygems.org'
2917
+
2918
+ gem 'json-schema', '~> 3.0'
2919
+ gem 'yaml', '~> 0.3' # For schema validation
2920
+ ```
2921
+
2922
+ **Registry README.adoc structure:**
2923
+
2924
+ ```asciidoc
2925
+ = Ukiryu Tool Registry
2926
+
2927
+ == Overview
2928
+
2929
+ The Ukiryu Tool Registry maintains YAML profiles for command-line tools.
2930
+
2931
+ == Tools
2932
+
2933
+ * link:tools/inkscape/[Inkscape]
2934
+ * link:tools/ghostscript/[Ghostscript]
2935
+ * link:tools/imagemagick/[ImageMagick]
2936
+
2937
+ == Contributing
2938
+
2939
+ See link:contributing.adoc[Contributing Guidelines].
2940
+
2941
+ == Validation
2942
+
2943
+ Run `rake validate` to validate all tool profiles.
2944
+ ```
2945
+
2946
+ ---
2947
+
2948
+ ## File Location
2949
+
2950
+ ```
2951
+ docs/ukiryu-proposal.md
2952
+ ```