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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- data/.github/workflows/test.yml +0 -143
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_extractor'
|
|
4
|
+
require_relative 'native_extractor'
|
|
5
|
+
require_relative 'help_parser'
|
|
6
|
+
|
|
7
|
+
module Ukiryu
|
|
8
|
+
module Extractors
|
|
9
|
+
# Main extractor class that orchestrates extraction strategies
|
|
10
|
+
#
|
|
11
|
+
# Tries multiple extraction strategies in order:
|
|
12
|
+
# 1. Native flag extraction (--ukiryu-definition)
|
|
13
|
+
# 2. Help parsing (--help output)
|
|
14
|
+
#
|
|
15
|
+
# @example Extract definition from a tool
|
|
16
|
+
# result = Ukiryu::Extractor.extract(:git)
|
|
17
|
+
# if result[:success]
|
|
18
|
+
# puts result[:yaml]
|
|
19
|
+
# else
|
|
20
|
+
# puts "Failed: #{result[:error]}"
|
|
21
|
+
# end
|
|
22
|
+
class Extractor
|
|
23
|
+
# Extract definition from a tool
|
|
24
|
+
#
|
|
25
|
+
# Tries multiple extraction strategies in order until one succeeds.
|
|
26
|
+
#
|
|
27
|
+
# @param tool_name [String, Symbol] the tool name
|
|
28
|
+
# @param options [Hash] extraction options
|
|
29
|
+
# @option options [Symbol] :method specific method to use (:native, :help, :auto)
|
|
30
|
+
# @option options [Boolean] :verbose enable verbose output
|
|
31
|
+
# @return [Hash] result with :success, :yaml, :method, :error keys
|
|
32
|
+
def self.extract(tool_name, options = {})
|
|
33
|
+
new(tool_name, options).extract
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Initialize the extractor
|
|
37
|
+
#
|
|
38
|
+
# @param tool_name [String, Symbol] the tool name
|
|
39
|
+
# @param options [Hash] extraction options
|
|
40
|
+
def initialize(tool_name, options = {})
|
|
41
|
+
@tool_name = tool_name
|
|
42
|
+
@options = options
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extract definition using available strategies
|
|
46
|
+
#
|
|
47
|
+
# @return [Hash] result with :success, :yaml, :method, :error keys
|
|
48
|
+
def extract
|
|
49
|
+
method = @options[:method] || :auto
|
|
50
|
+
|
|
51
|
+
if method == :auto
|
|
52
|
+
extract_auto
|
|
53
|
+
elsif method == :native
|
|
54
|
+
extract_with_native
|
|
55
|
+
elsif method == :help
|
|
56
|
+
extract_with_help
|
|
57
|
+
else
|
|
58
|
+
{
|
|
59
|
+
success: false,
|
|
60
|
+
error: "Unknown extraction method: #{method}",
|
|
61
|
+
method: nil,
|
|
62
|
+
yaml: nil
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Try all extraction strategies in order
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] result hash
|
|
72
|
+
def extract_auto
|
|
73
|
+
# Try native flag first
|
|
74
|
+
result = extract_with_native
|
|
75
|
+
return result if result[:success]
|
|
76
|
+
|
|
77
|
+
# Fall back to help parsing
|
|
78
|
+
extract_with_help
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Extract using native flag
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash] result hash
|
|
84
|
+
def extract_with_native
|
|
85
|
+
extractor = NativeExtractor.new(@tool_name, @options)
|
|
86
|
+
|
|
87
|
+
unless extractor.available?
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: "Tool '#{@tool_name}' does not support native definition extraction",
|
|
91
|
+
method: :native,
|
|
92
|
+
yaml: nil
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
yaml = extractor.extract
|
|
97
|
+
|
|
98
|
+
if yaml
|
|
99
|
+
{
|
|
100
|
+
success: true,
|
|
101
|
+
yaml: yaml,
|
|
102
|
+
method: :native,
|
|
103
|
+
error: nil
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
{
|
|
107
|
+
success: false,
|
|
108
|
+
error: "Native extraction failed",
|
|
109
|
+
method: :native,
|
|
110
|
+
yaml: nil
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Extract using help parser
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] result hash
|
|
118
|
+
def extract_with_help
|
|
119
|
+
extractor = HelpParser.new(@tool_name, @options)
|
|
120
|
+
|
|
121
|
+
unless extractor.available?
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: "Tool '#{@tool_name}' does not have help output",
|
|
125
|
+
method: :help,
|
|
126
|
+
yaml: nil
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
yaml = extractor.extract
|
|
131
|
+
|
|
132
|
+
if yaml
|
|
133
|
+
{
|
|
134
|
+
success: true,
|
|
135
|
+
yaml: yaml,
|
|
136
|
+
method: :help,
|
|
137
|
+
error: nil
|
|
138
|
+
}
|
|
139
|
+
else
|
|
140
|
+
{
|
|
141
|
+
success: false,
|
|
142
|
+
error: "Help parsing failed",
|
|
143
|
+
method: :help,
|
|
144
|
+
yaml: nil
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_extractor'
|
|
4
|
+
|
|
5
|
+
module Ukiryu
|
|
6
|
+
module Extractors
|
|
7
|
+
# Help parser extraction strategy
|
|
8
|
+
#
|
|
9
|
+
# Reverse-engineers a tool definition by parsing the output
|
|
10
|
+
# of the tool's `--help` command.
|
|
11
|
+
class HelpParser < BaseExtractor
|
|
12
|
+
# Extract definition by parsing help output
|
|
13
|
+
#
|
|
14
|
+
# @return [String, nil] the YAML definition or nil if extraction failed
|
|
15
|
+
def extract
|
|
16
|
+
return nil unless available?
|
|
17
|
+
|
|
18
|
+
help_result = execute_command([@tool_name.to_s, '--help'])
|
|
19
|
+
return nil unless help_result[:exit_status].zero?
|
|
20
|
+
|
|
21
|
+
help_text = help_result[:stdout] + help_result[:stderr]
|
|
22
|
+
return nil if help_text.strip.empty?
|
|
23
|
+
|
|
24
|
+
# Parse help output and generate YAML
|
|
25
|
+
parse_help_to_yaml(help_text)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if the tool has help output
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean] true if --help produces output
|
|
31
|
+
def available?
|
|
32
|
+
help_result = execute_command([@tool_name.to_s, '--help'])
|
|
33
|
+
help_result[:exit_status].zero? && !(help_result[:stdout] + help_result[:stderr]).strip.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Parse help text and convert to YAML format
|
|
39
|
+
#
|
|
40
|
+
# @param help_text [String] the help output
|
|
41
|
+
# @return [String] YAML definition
|
|
42
|
+
def parse_help_to_yaml(help_text)
|
|
43
|
+
require 'yaml'
|
|
44
|
+
|
|
45
|
+
# Extract tool name from help text (usually first word)
|
|
46
|
+
name = extract_name(help_text)
|
|
47
|
+
|
|
48
|
+
# Try to detect version
|
|
49
|
+
version = extract_version
|
|
50
|
+
|
|
51
|
+
# Build basic YAML structure
|
|
52
|
+
definition = {
|
|
53
|
+
'ukiryu_schema' => '1.1',
|
|
54
|
+
'$self' => "https://www.ukiryu.com/register/1.1/#{name}/#{version || '1.0'}",
|
|
55
|
+
'name' => name,
|
|
56
|
+
'version' => version || '1.0',
|
|
57
|
+
'display_name' => name.capitalize,
|
|
58
|
+
'profiles' => [
|
|
59
|
+
{
|
|
60
|
+
'name' => 'default',
|
|
61
|
+
'platforms' => %w[macos linux windows],
|
|
62
|
+
'shells' => %w[bash zsh fish powershell cmd],
|
|
63
|
+
'commands' => []
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Parse commands/options/flags from help text
|
|
69
|
+
parse_help_elements(help_text, definition)
|
|
70
|
+
|
|
71
|
+
definition.to_yaml
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Extract tool name from help text
|
|
75
|
+
#
|
|
76
|
+
# @param help_text [String] the help output
|
|
77
|
+
# @return [String] the tool name
|
|
78
|
+
def extract_name(help_text)
|
|
79
|
+
# Use the tool name passed to the extractor
|
|
80
|
+
@tool_name.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Try to detect tool version
|
|
84
|
+
#
|
|
85
|
+
# @return [String, nil] the detected version
|
|
86
|
+
def extract_version
|
|
87
|
+
version_result = execute_command([@tool_name.to_s, '--version'])
|
|
88
|
+
if version_result[:exit_status].zero?
|
|
89
|
+
version_text = version_result[:stdout]
|
|
90
|
+
# Try to extract version number
|
|
91
|
+
if version_text =~ /(\d+\.\d+(?:\.\d+)?)/
|
|
92
|
+
return Regexp.last_match(1)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Parse help elements (commands, options, flags)
|
|
99
|
+
#
|
|
100
|
+
# @param help_text [String] the help output
|
|
101
|
+
# @param definition [Hash] the definition hash to modify
|
|
102
|
+
def parse_help_elements(help_text, definition)
|
|
103
|
+
# This is a basic implementation - real-world parsing would be more sophisticated
|
|
104
|
+
# For now, we create a basic structure
|
|
105
|
+
|
|
106
|
+
lines = help_text.split("\n")
|
|
107
|
+
|
|
108
|
+
# Look for option patterns like:
|
|
109
|
+
# -v, --verbose
|
|
110
|
+
# --output=FILE
|
|
111
|
+
# -h, --help
|
|
112
|
+
|
|
113
|
+
options = []
|
|
114
|
+
|
|
115
|
+
lines.each do |line|
|
|
116
|
+
# Match short and long options
|
|
117
|
+
if line =~ /^\s*(-[a-z]),?\s*\[--([a-z-]+)(?:[=\s]+([A-Z_]+))?\]/i
|
|
118
|
+
short_opt = Regexp.last_match(1)
|
|
119
|
+
long_opt = Regexp.last_match(2)
|
|
120
|
+
param = Regexp.last_match(3)
|
|
121
|
+
|
|
122
|
+
option = {
|
|
123
|
+
'name' => long_opt.gsub(/-/, '_'),
|
|
124
|
+
'description' => line.strip
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if short_opt
|
|
128
|
+
option['cli'] = short_opt
|
|
129
|
+
else
|
|
130
|
+
option['cli'] = "--#{long_opt}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if param
|
|
134
|
+
option['type'] = infer_type(param)
|
|
135
|
+
else
|
|
136
|
+
option['type'] = 'boolean'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
options << option
|
|
140
|
+
elsif line =~ /^\s*\[--([a-z-]+)(?:[=\s]+([A-Z_]+))?\]/i
|
|
141
|
+
long_opt = Regexp.last_match(1)
|
|
142
|
+
param = Regexp.last_match(2)
|
|
143
|
+
|
|
144
|
+
option = {
|
|
145
|
+
'name' => long_opt.gsub(/-/, '_'),
|
|
146
|
+
'cli' => "--#{long_opt}",
|
|
147
|
+
'description' => line.strip
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if param
|
|
151
|
+
option['type'] = infer_type(param)
|
|
152
|
+
else
|
|
153
|
+
option['type'] = 'boolean'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
options << option
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Add default command with parsed options
|
|
161
|
+
definition['profiles'][0]['commands'] = [
|
|
162
|
+
{
|
|
163
|
+
'name' => 'default',
|
|
164
|
+
'description' => "Default #{@tool_name} command",
|
|
165
|
+
'options' => options.uniq { |opt| opt['name'] }
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Infer type from parameter name
|
|
171
|
+
#
|
|
172
|
+
# @param param_name [String] the parameter name
|
|
173
|
+
# @return [String] the inferred type
|
|
174
|
+
def infer_type(param_name)
|
|
175
|
+
case param_name.upcase
|
|
176
|
+
when /FILE|PATH/
|
|
177
|
+
'file'
|
|
178
|
+
when /NUM|COUNT|LEVEL/
|
|
179
|
+
'integer'
|
|
180
|
+
when /DIR|FOLDER/
|
|
181
|
+
'directory'
|
|
182
|
+
else
|
|
183
|
+
'string'
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_extractor'
|
|
4
|
+
|
|
5
|
+
module Ukiryu
|
|
6
|
+
module Extractors
|
|
7
|
+
# Native flag extraction strategy
|
|
8
|
+
#
|
|
9
|
+
# Attempts to extract definition using the tool's native
|
|
10
|
+
# `--ukiryu-definition` flag if supported.
|
|
11
|
+
class NativeExtractor < BaseExtractor
|
|
12
|
+
# Native flag to try
|
|
13
|
+
NATIVE_FLAG = '--ukiryu-definition'
|
|
14
|
+
|
|
15
|
+
# Extract definition using native flag
|
|
16
|
+
#
|
|
17
|
+
# @return [String, nil] the YAML definition or nil if extraction failed
|
|
18
|
+
def extract
|
|
19
|
+
return nil unless available?
|
|
20
|
+
|
|
21
|
+
result = execute_command([@tool_name.to_s, NATIVE_FLAG])
|
|
22
|
+
|
|
23
|
+
return nil unless result[:exit_status].zero?
|
|
24
|
+
return nil if result[:stdout].strip.empty?
|
|
25
|
+
|
|
26
|
+
result[:stdout]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if the tool supports native definition extraction
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] true if the tool exists and the flag is supported
|
|
32
|
+
def available?
|
|
33
|
+
# First check if tool exists
|
|
34
|
+
which_result = execute_command(['which', @tool_name.to_s])
|
|
35
|
+
return false unless which_result[:exit_status].zero?
|
|
36
|
+
|
|
37
|
+
# Then check if it supports the flag
|
|
38
|
+
help_result = execute_command([@tool_name.to_s, '--help'])
|
|
39
|
+
return false unless help_result[:exit_status].zero?
|
|
40
|
+
|
|
41
|
+
# Check if help mentions ukiryu
|
|
42
|
+
help_output = help_result[:stdout] + help_result[:stderr]
|
|
43
|
+
help_output.downcase.include?('ukiryu') || help_output.include?(NATIVE_FLAG)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/ukiryu/io.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
# I/O stream primitives for CLI tools
|
|
5
|
+
#
|
|
6
|
+
# This module defines standard I/O primitives that are common across
|
|
7
|
+
# command-line tools, providing a consistent interface for:
|
|
8
|
+
# - Standard input (stdin)
|
|
9
|
+
# - Standard output (stdout)
|
|
10
|
+
# - Standard error (stderr)
|
|
11
|
+
# - Pipes (inter-process communication)
|
|
12
|
+
# - File redirections
|
|
13
|
+
#
|
|
14
|
+
# These are GENERAL PRIMITIVES that apply to ALL CLI tools, not tool-specific.
|
|
15
|
+
|
|
16
|
+
# Standard stream marker for stdin
|
|
17
|
+
STDIN = '-'
|
|
18
|
+
|
|
19
|
+
# Standard stream marker for stdout
|
|
20
|
+
STDOUT = '-'
|
|
21
|
+
# Also available as "%stdout%" in some tools (Ghostscript)
|
|
22
|
+
|
|
23
|
+
# Special value for reading from stdin (double dash)
|
|
24
|
+
STDIN_MARKER = '--'
|
|
25
|
+
|
|
26
|
+
# Standard stream constants
|
|
27
|
+
module Stream
|
|
28
|
+
# Standard input stream descriptor
|
|
29
|
+
#
|
|
30
|
+
# Used for commands that can read from stdin instead of a file
|
|
31
|
+
# @example
|
|
32
|
+
# # Read from stdin
|
|
33
|
+
# tool.options_for(:process).tap do |opts|
|
|
34
|
+
# opts.inputs = [Ukiryu::IO::Stream::STDIN]
|
|
35
|
+
# opts.output = "output.pdf"
|
|
36
|
+
# end
|
|
37
|
+
STDIN = :stdin
|
|
38
|
+
|
|
39
|
+
# Standard output stream descriptor
|
|
40
|
+
#
|
|
41
|
+
# Used for commands that can write to stdout instead of a file
|
|
42
|
+
# @example
|
|
43
|
+
# tool.options_for(:export).tap do |opts|
|
|
44
|
+
# opts.inputs = ["input.svg"]
|
|
45
|
+
# opts.output = Ukiryu::IO::Stream::STDOUT
|
|
46
|
+
# end
|
|
47
|
+
STDOUT = :stdout
|
|
48
|
+
|
|
49
|
+
# Standard error stream descriptor
|
|
50
|
+
#
|
|
51
|
+
# Used for separating stderr from stdout
|
|
52
|
+
STDERR = :stderr
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Pipe redirection for inter-process communication
|
|
56
|
+
#
|
|
57
|
+
# Pipes allow the output of one command to become the input of another.
|
|
58
|
+
# This is represented by special file path markers.
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# # Pipe output of command1 to command2
|
|
62
|
+
# result1 = command1.execute(output: Pipe.to("command2"))
|
|
63
|
+
#
|
|
64
|
+
# @example Using special syntax in tools
|
|
65
|
+
# # Ghostscript: -sOutputFile=%pipe%lpr
|
|
66
|
+
# # Tar: --to-command=COMMAND
|
|
67
|
+
class Pipe
|
|
68
|
+
# Special marker for pipe output
|
|
69
|
+
MARKER = '%pipe%'
|
|
70
|
+
|
|
71
|
+
# Create a pipe to a command
|
|
72
|
+
#
|
|
73
|
+
# @param command [String] the command to pipe to
|
|
74
|
+
# @return [String] the pipe marker for use in CLI options
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# Pipe.to("lpr") # => "%pipe%lpr"
|
|
78
|
+
def self.to(command)
|
|
79
|
+
"#{MARKER}#{command}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Parse a pipe marker
|
|
83
|
+
#
|
|
84
|
+
# @param value [String] the pipe marker string
|
|
85
|
+
# @return [String, nil] the command if it's a pipe marker
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# Pipe.parse("%pipe%lpr") # => "lpr"
|
|
89
|
+
def self.parse(value)
|
|
90
|
+
return nil unless value.is_a?(String)
|
|
91
|
+
return nil unless value.start_with?(MARKER)
|
|
92
|
+
|
|
93
|
+
value.sub(MARKER, '')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if a value is a pipe marker
|
|
97
|
+
#
|
|
98
|
+
# @param value [String] the value to check
|
|
99
|
+
# @return [Boolean] true if it's a pipe marker
|
|
100
|
+
def self.pipe?(value)
|
|
101
|
+
return false unless value.is_a?(String)
|
|
102
|
+
|
|
103
|
+
value.start_with?(MARKER)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# File redirection primitives
|
|
108
|
+
#
|
|
109
|
+
# Provides utilities for file redirection operations
|
|
110
|
+
module Redirection
|
|
111
|
+
# Redirect output to a file
|
|
112
|
+
#
|
|
113
|
+
# @param output [Symbol, String] :stdout or :stderr
|
|
114
|
+
# @param path [String] the file path to redirect to
|
|
115
|
+
# @return [Hash] redirection specification
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# Redirection.to(:stdout, "/tmp/output.txt")
|
|
119
|
+
def self.to(stream, path)
|
|
120
|
+
{ stream => path }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Redirect stderr to stdout (2>&1 in shell)
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] redirection specification
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# Redirection.stderr_to_stdout
|
|
129
|
+
def self.stderr_to_stdout
|
|
130
|
+
{ stderr: :stdout }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Standard input/output file handles
|
|
135
|
+
#
|
|
136
|
+
# This class represents special file handles for stdin/stdout/stderr
|
|
137
|
+
# that are recognized by CLI tools.
|
|
138
|
+
#
|
|
139
|
+
# @example
|
|
140
|
+
# # Reading from stdin
|
|
141
|
+
# input = Ukiryu::IO::StandardInput.new
|
|
142
|
+
# input.read
|
|
143
|
+
#
|
|
144
|
+
# # Writing to stdout
|
|
145
|
+
# output = Ukiryu::IO::StandardOutput.new
|
|
146
|
+
# output.write("data")
|
|
147
|
+
class StandardInput
|
|
148
|
+
# The stdin file descriptor
|
|
149
|
+
FILENO = 0
|
|
150
|
+
|
|
151
|
+
# Check if a path represents stdin
|
|
152
|
+
#
|
|
153
|
+
# @param path [String, Symbol] the path to check
|
|
154
|
+
# @return [Boolean] true if the path represents stdin
|
|
155
|
+
#
|
|
156
|
+
# @example
|
|
157
|
+
# Ukiryu::IO::StandardInput.stdin?("-") # => true
|
|
158
|
+
# Ukiryu::IO::StandardInput.stdin?(:stdin) # => true
|
|
159
|
+
def self.stdin?(path)
|
|
160
|
+
path = path.to_s if path.is_a?(Symbol)
|
|
161
|
+
[$stdin, '-', '/dev/stdin'].include?(path)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class StandardOutput
|
|
166
|
+
# The stdout file descriptor
|
|
167
|
+
FILENO = 1
|
|
168
|
+
|
|
169
|
+
# Check if a path represents stdout
|
|
170
|
+
#
|
|
171
|
+
# @param path [String, Symbol] the path to check
|
|
172
|
+
# @return [Boolean] true if the path represents stdout
|
|
173
|
+
#
|
|
174
|
+
# @example
|
|
175
|
+
# Ukiryu::IO::StandardOutput.stdout?("-") # => true
|
|
176
|
+
# Ukiryu::IO::StandardOutput.stdout?(:stdout) # => true
|
|
177
|
+
def self.stdout?(path)
|
|
178
|
+
path = path.to_s if path.is_a?(Symbol)
|
|
179
|
+
[$stdout, '-', '/dev/stdout', '%stdout%'].include?(path)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class StandardError
|
|
184
|
+
# The stderr file descriptor
|
|
185
|
+
FILENO = 2
|
|
186
|
+
|
|
187
|
+
# Check if a path represents stderr
|
|
188
|
+
#
|
|
189
|
+
# @param path [String, Symbol] the path to check
|
|
190
|
+
# @return [Boolean] true if the path represents stderr
|
|
191
|
+
def self.stderr?(path)
|
|
192
|
+
path = path.to_s if path.is_a?(Symbol)
|
|
193
|
+
[:stderr, '/dev/stderr'].include?(path)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|