hammer_cli 0.19.1 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/hammer-complete +28 -0
- data/config/cli_config.template.yml +2 -0
- data/config/hammer.completion +5 -0
- data/doc/commands_extension.md +12 -0
- data/doc/creating_commands.md +100 -0
- data/doc/installation.md +47 -4
- data/doc/release_notes.md +32 -6
- data/lib/hammer_cli.rb +1 -0
- data/lib/hammer_cli/abstract.rb +61 -4
- data/lib/hammer_cli/apipie/api_connection.rb +5 -1
- data/lib/hammer_cli/apipie/command.rb +3 -2
- data/lib/hammer_cli/apipie/option_builder.rb +15 -13
- data/lib/hammer_cli/apipie/option_definition.rb +9 -7
- data/lib/hammer_cli/bash.rb +2 -0
- data/lib/hammer_cli/bash/completion.rb +159 -0
- data/lib/hammer_cli/bash/prebuild_command.rb +21 -0
- data/lib/hammer_cli/command_extensions.rb +21 -1
- data/lib/hammer_cli/connection.rb +4 -0
- data/lib/hammer_cli/exception_handler.rb +11 -2
- data/lib/hammer_cli/full_help.rb +8 -1
- data/lib/hammer_cli/help/builder.rb +10 -3
- data/lib/hammer_cli/logger_watch.rb +1 -1
- data/lib/hammer_cli/main.rb +5 -3
- data/lib/hammer_cli/options/normalizers.rb +7 -3
- data/lib/hammer_cli/options/option_definition.rb +26 -6
- data/lib/hammer_cli/options/option_family.rb +114 -0
- data/lib/hammer_cli/options/predefined.rb +1 -1
- data/lib/hammer_cli/output/adapter/abstract.rb +1 -5
- data/lib/hammer_cli/output/adapter/base.rb +1 -1
- data/lib/hammer_cli/output/adapter/csv.rb +3 -2
- data/lib/hammer_cli/output/adapter/json.rb +14 -3
- data/lib/hammer_cli/output/adapter/silent.rb +1 -1
- data/lib/hammer_cli/output/adapter/table.rb +27 -8
- data/lib/hammer_cli/output/adapter/yaml.rb +6 -3
- data/lib/hammer_cli/output/output.rb +2 -4
- data/lib/hammer_cli/settings.rb +2 -1
- data/lib/hammer_cli/subcommand.rb +25 -1
- data/lib/hammer_cli/testing/command_assertions.rb +2 -2
- data/lib/hammer_cli/utils.rb +22 -0
- data/lib/hammer_cli/version.rb +1 -1
- data/locale/ca/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/de/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/en/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/en_GB/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/es/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/fr/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/it/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ja/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ko/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/pt_BR/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/ru/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/zh_CN/LC_MESSAGES/hammer-cli.mo +0 -0
- data/locale/zh_TW/LC_MESSAGES/hammer-cli.mo +0 -0
- data/man/hammer.1.gz +0 -0
- data/test/unit/abstract_test.rb +23 -2
- data/test/unit/apipie/api_connection_test.rb +1 -0
- data/test/unit/apipie/option_builder_test.rb +8 -0
- data/test/unit/bash_test.rb +138 -0
- data/test/unit/command_extensions_test.rb +67 -49
- data/test/unit/exception_handler_test.rb +44 -0
- data/test/unit/help/builder_test.rb +22 -0
- data/test/unit/options/option_family_test.rb +48 -0
- data/test/unit/output/adapter/base_test.rb +58 -0
- data/test/unit/output/adapter/csv_test.rb +63 -1
- data/test/unit/output/adapter/json_test.rb +61 -0
- data/test/unit/output/adapter/table_test.rb +70 -1
- data/test/unit/output/adapter/yaml_test.rb +59 -0
- data/test/unit/output/output_test.rb +3 -3
- metadata +82 -71
- data/hammer_cli_complete +0 -13
@@ -65,8 +65,8 @@ module HammerCLI::Apipie
|
|
65
65
|
method_options(options)
|
66
66
|
end
|
67
67
|
|
68
|
-
def print_data(data)
|
69
|
-
print_collection(output_definition, data) unless output_definition.empty?
|
68
|
+
def print_data(data, options = {})
|
69
|
+
print_collection(output_definition, data, options) unless output_definition.empty?
|
70
70
|
print_success_message(data) unless success_message.nil?
|
71
71
|
end
|
72
72
|
|
@@ -86,6 +86,7 @@ module HammerCLI::Apipie
|
|
86
86
|
declared_options << option
|
87
87
|
block ||= option.default_conversion_block
|
88
88
|
define_accessors_for(option, &block)
|
89
|
+
completion_type_for(option, opts)
|
89
90
|
end
|
90
91
|
extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
|
91
92
|
option
|
@@ -41,12 +41,11 @@ module HammerCLI::Apipie
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def create_option(param, resource_name_map)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
)
|
44
|
+
family = HammerCLI::Options::OptionFamily.new
|
45
|
+
family.parent(option_switch(param, resource_name_map),
|
46
|
+
option_type(param, resource_name_map),
|
47
|
+
option_desc(param),
|
48
|
+
option_opts(param, resource_name_map))
|
50
49
|
end
|
51
50
|
|
52
51
|
def option_switch(param, resource_name_map)
|
@@ -61,7 +60,7 @@ module HammerCLI::Apipie
|
|
61
60
|
param.description || " "
|
62
61
|
end
|
63
62
|
|
64
|
-
def option_opts(param)
|
63
|
+
def option_opts(param, resource_name_map)
|
65
64
|
opts = {}
|
66
65
|
opts[:required] = true if (param.required? and require_options?)
|
67
66
|
if param.expected_type.to_s == 'array'
|
@@ -80,19 +79,22 @@ module HammerCLI::Apipie
|
|
80
79
|
end
|
81
80
|
opts[:attribute_name] = HammerCLI.option_accessor_name(param.name)
|
82
81
|
opts[:referenced_resource] = resource_name(param)
|
82
|
+
opts[:aliased_resource] = aliased_name(resource_name(param), resource_name_map)
|
83
83
|
|
84
84
|
return opts
|
85
85
|
end
|
86
86
|
|
87
|
+
def aliased_name(name, resource_name_map)
|
88
|
+
return if name.nil?
|
89
|
+
|
90
|
+
resource_name_map[name.to_s] || resource_name_map[name.to_sym] || name
|
91
|
+
end
|
92
|
+
|
87
93
|
def aliased(param, resource_name_map)
|
88
94
|
resource_name = resource_name(param)
|
95
|
+
return param.name if resource_name.nil?
|
89
96
|
|
90
|
-
|
91
|
-
return param.name
|
92
|
-
else
|
93
|
-
aliased_name = resource_name_map[resource_name.to_s] || resource_name_map[resource_name.to_sym] || resource_name
|
94
|
-
return param.name.gsub(resource_name, aliased_name.to_s)
|
95
|
-
end
|
97
|
+
param.name.gsub(resource_name, aliased_name(resource_name, resource_name_map).to_s)
|
96
98
|
end
|
97
99
|
|
98
100
|
def resource_name(param)
|
@@ -1,21 +1,23 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'options')
|
2
2
|
|
3
3
|
module HammerCLI::Apipie
|
4
|
-
|
5
4
|
class OptionDefinition < HammerCLI::Options::OptionDefinition
|
6
|
-
|
7
|
-
attr_accessor :referenced_resource
|
5
|
+
attr_accessor :referenced_resource, :aliased_resource, :family
|
8
6
|
|
9
7
|
def initialize(switches, type, description, options = {})
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
@referenced_resource = options[:referenced_resource].to_s if options[:referenced_resource]
|
9
|
+
@aliased_resource = options[:aliased_resource].to_s if options[:aliased_resource]
|
10
|
+
@family = options[:family]
|
13
11
|
super
|
14
12
|
# Apipie currently sends descriptions as escaped HTML once this is changed this should be removed.
|
15
13
|
# See #15198 on Redmine.
|
16
14
|
@description = CGI::unescapeHTML(description)
|
17
15
|
end
|
18
16
|
|
19
|
-
|
17
|
+
def child?
|
18
|
+
return unless @family
|
20
19
|
|
20
|
+
@family.children.include?(self)
|
21
|
+
end
|
22
|
+
end
|
21
23
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module HammerCLI
|
4
|
+
module Bash
|
5
|
+
class Completion
|
6
|
+
def initialize(dict)
|
7
|
+
@dict = dict
|
8
|
+
end
|
9
|
+
|
10
|
+
def complete(line)
|
11
|
+
@complete_line = line.end_with?(' ')
|
12
|
+
full_path = line.split(' ')
|
13
|
+
complete_path = @complete_line ? full_path : full_path[0..-2]
|
14
|
+
dict, path = traverse_tree(@dict, complete_path)
|
15
|
+
|
16
|
+
return [] unless path.empty? # lost during traversing
|
17
|
+
|
18
|
+
partial = @complete_line ? '' : full_path.last
|
19
|
+
finish_word(dict, partial)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.load_description(path)
|
23
|
+
JSON.load(File.open(path))
|
24
|
+
rescue Errno::ENOENT
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def finish_word(dict, incomplete)
|
31
|
+
finish_option_value(dict, incomplete) ||
|
32
|
+
(finish_option_or_subcommand(dict, incomplete) + finish_param(dict, incomplete))
|
33
|
+
end
|
34
|
+
|
35
|
+
def finish_option_or_subcommand(dict, incomplete)
|
36
|
+
dict.keys.select { |k| k.is_a?(String) && k =~ /^#{incomplete}/ }.map { |k| k + ' ' }
|
37
|
+
end
|
38
|
+
|
39
|
+
def complete_value(value_description, partial, is_param)
|
40
|
+
case value_description['type']
|
41
|
+
when 'value'
|
42
|
+
if !partial.empty?
|
43
|
+
[]
|
44
|
+
elsif is_param
|
45
|
+
['--->', 'Add parameter']
|
46
|
+
else
|
47
|
+
['--->', 'Add option <value>']
|
48
|
+
end
|
49
|
+
when 'directory'
|
50
|
+
directories(partial)
|
51
|
+
when 'file'
|
52
|
+
files(partial, value_description)
|
53
|
+
when 'enum'
|
54
|
+
enum(partial, value_description['values'])
|
55
|
+
when 'multienum'
|
56
|
+
multienum(partial, value_description['values'])
|
57
|
+
when 'schema'
|
58
|
+
schema(value_description['schema'])
|
59
|
+
when 'list'
|
60
|
+
['--->', 'Add comma-separated list of values']
|
61
|
+
when 'key_value_list'
|
62
|
+
['--->', 'Add comma-separated list of key=value']
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def finish_param(dict, incomplete)
|
67
|
+
if dict['params'] && !dict['params'].empty?
|
68
|
+
complete_value(dict['params'].first, incomplete, true)
|
69
|
+
else
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def finish_option_value(dict, incomplete)
|
75
|
+
complete_value(dict, incomplete, false) if dict.key?('type')
|
76
|
+
end
|
77
|
+
|
78
|
+
def traverse_tree(dict, path)
|
79
|
+
return [dict, []] if path.nil? || path.empty?
|
80
|
+
result = if dict.key?(path.first)
|
81
|
+
if path.first.start_with?('-')
|
82
|
+
parse_option(dict, path)
|
83
|
+
else
|
84
|
+
parse_subcommand(dict, path)
|
85
|
+
end
|
86
|
+
elsif dict['params']
|
87
|
+
# traverse params one by one
|
88
|
+
parse_params(dict, path)
|
89
|
+
else
|
90
|
+
# not found
|
91
|
+
[{}, path]
|
92
|
+
end
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_params(dict, path)
|
97
|
+
traverse_tree({ 'params' => dict['params'][1..-1] }, path[1..-1])
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_subcommand(dict, path)
|
101
|
+
traverse_tree(dict[path.first], path[1..-1])
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_option(dict, path)
|
105
|
+
if dict[path.first]['type'] == 'flag' # flag
|
106
|
+
traverse_tree(dict, path[1..-1])
|
107
|
+
elsif path.length >= 2 # option with value
|
108
|
+
traverse_tree(dict, path[2..-1])
|
109
|
+
else # option with value missing
|
110
|
+
[dict[path.first], path[1..-1]]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def directories(partial = '')
|
115
|
+
dirs = []
|
116
|
+
dirs += Dir.glob("#{partial}*").select { |f| File.directory?(f) }
|
117
|
+
dirs = dirs.map { |d| d + '/' } if dirs.length == 1
|
118
|
+
dirs
|
119
|
+
end
|
120
|
+
|
121
|
+
def files(partial = '', opts = {})
|
122
|
+
filter = opts.fetch('filter', '.*')
|
123
|
+
file_names = []
|
124
|
+
file_names += Dir.glob("#{partial}*").select do |f|
|
125
|
+
File.directory?(f) || f =~ /#{filter}/
|
126
|
+
end
|
127
|
+
file_names.map { |f| File.directory?(f) ? f + '/' : f + ' ' }
|
128
|
+
end
|
129
|
+
|
130
|
+
def enum(partial = '', values = [])
|
131
|
+
values.select { |v| v.start_with?(partial) }.map { |v| v + ' ' }
|
132
|
+
end
|
133
|
+
|
134
|
+
def multienum(partial = '', values = [])
|
135
|
+
return values if partial.empty?
|
136
|
+
|
137
|
+
parts = partial.split(',')
|
138
|
+
resolved = []
|
139
|
+
to_complete = parts.each_with_object([]) do |part, res|
|
140
|
+
next resolved << part if values.include?(part)
|
141
|
+
|
142
|
+
res << part
|
143
|
+
end
|
144
|
+
|
145
|
+
hints = to_complete.map do |p|
|
146
|
+
values.select { |v| v.start_with?(p) }
|
147
|
+
end.flatten(1).uniq
|
148
|
+
return values - parts if hints.empty?
|
149
|
+
return [(resolved + hints).join(',')] if hints.size == 1
|
150
|
+
|
151
|
+
hints
|
152
|
+
end
|
153
|
+
|
154
|
+
def schema(template = '')
|
155
|
+
['--->', "Add value by following schema: #{template}"]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module HammerCLI
|
2
|
+
module Bash
|
3
|
+
class PrebuildCompletionCommand < HammerCLI::AbstractCommand
|
4
|
+
def execute
|
5
|
+
map = HammerCLI::MainCommand.completion_map
|
6
|
+
cache_file = HammerCLI::Settings.get(:completion_cache_file)
|
7
|
+
cache_dir = File.dirname(cache_file)
|
8
|
+
FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
|
9
|
+
File.write(File.expand_path(cache_file), map.to_json)
|
10
|
+
|
11
|
+
HammerCLI::EX_OK
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
HammerCLI::MainCommand.subcommand(
|
17
|
+
'prebuild-bash-completion',
|
18
|
+
_('Prepare map of options and subcommands for Bash completion'),
|
19
|
+
HammerCLI::Bash::PrebuildCompletionCommand
|
20
|
+
)
|
21
|
+
end
|
@@ -15,7 +15,7 @@ module HammerCLI
|
|
15
15
|
ALLOWED_EXTENSIONS = %i[
|
16
16
|
option command_options before_print data output help request
|
17
17
|
request_headers headers request_options options request_params params
|
18
|
-
option_sources predefined_options use_option
|
18
|
+
option_sources predefined_options use_option option_family
|
19
19
|
].freeze
|
20
20
|
|
21
21
|
def initialize(options = {})
|
@@ -86,6 +86,11 @@ module HammerCLI
|
|
86
86
|
@option_sources_block = block
|
87
87
|
end
|
88
88
|
|
89
|
+
def self.option_family(options = {}, &block)
|
90
|
+
@option_family_opts = options
|
91
|
+
@option_family_block = block
|
92
|
+
end
|
93
|
+
|
89
94
|
# Object
|
90
95
|
|
91
96
|
def extend_options(command_class)
|
@@ -151,6 +156,13 @@ module HammerCLI
|
|
151
156
|
self.class.extend_option_sources(sources, command)
|
152
157
|
end
|
153
158
|
|
159
|
+
def extend_option_family(command_class)
|
160
|
+
allowed = @only & %i[option_family]
|
161
|
+
return if allowed.empty? || (allowed & @except).any?
|
162
|
+
|
163
|
+
self.class.extend_option_family(command_class)
|
164
|
+
end
|
165
|
+
|
154
166
|
def delegatee(command_class)
|
155
167
|
self.class.delegatee = command_class
|
156
168
|
end
|
@@ -234,5 +246,13 @@ module HammerCLI
|
|
234
246
|
@option_sources_block.call(sources, command)
|
235
247
|
logger.debug("Called block for #{@delegatee} option sources:\n\t#{@option_sources_block}")
|
236
248
|
end
|
249
|
+
|
250
|
+
def self.extend_option_family(command_class)
|
251
|
+
return if @option_family_block.nil?
|
252
|
+
|
253
|
+
@option_family_opts[:creator] = command_class
|
254
|
+
command_class.send(:option_family, @option_family_opts, &@option_family_block)
|
255
|
+
logger.debug("Called option family block for #{command_class}:\n\t#{@option_family_block}")
|
256
|
+
end
|
237
257
|
end
|
238
258
|
end
|
@@ -69,7 +69,7 @@ module HammerCLI
|
|
69
69
|
|
70
70
|
def handle_usage_exception(e)
|
71
71
|
print_error (_("Error: %{message}") + "\n\n" +
|
72
|
-
_("See: '%{path} --help'.")) % {:
|
72
|
+
_("See: '%{path} --help'.")) % { message: e.message, path: HammerCLI.expand_invocation_path(e.command.invocation_path) }
|
73
73
|
log_full_error e
|
74
74
|
HammerCLI::EX_USAGE
|
75
75
|
end
|
@@ -138,7 +138,16 @@ module HammerCLI
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def handle_apipie_missing_arguments_error(e)
|
141
|
-
|
141
|
+
params = e.params.map do |p|
|
142
|
+
param = p[/\[.+\]/]
|
143
|
+
param = if param.nil?
|
144
|
+
p.tr('_', '-')
|
145
|
+
else
|
146
|
+
p.scan(/\[[^\[\]]+\]/).first[1...-1].tr('_', '-')
|
147
|
+
end
|
148
|
+
"--#{param}"
|
149
|
+
end
|
150
|
+
message = _("Missing arguments for %s.") % "'#{params.uniq.join("', '")}'"
|
142
151
|
print_error message
|
143
152
|
log_full_error e, message
|
144
153
|
HammerCLI::EX_USAGE
|
data/lib/hammer_cli/full_help.rb
CHANGED
@@ -6,8 +6,11 @@ module HammerCLI
|
|
6
6
|
|
7
7
|
def execute
|
8
8
|
@adapter = option_md? ? MDAdapter.new : TxtAdapter.new
|
9
|
+
HammerCLI.context[:full_help] = true
|
10
|
+
@invocation_paths = {}
|
9
11
|
print_heading
|
10
12
|
print_help
|
13
|
+
HammerCLI.context[:full_help] = false
|
11
14
|
HammerCLI::EX_OK
|
12
15
|
end
|
13
16
|
|
@@ -19,9 +22,13 @@ module HammerCLI
|
|
19
22
|
end
|
20
23
|
|
21
24
|
def print_help(name='hammer', command=HammerCLI::MainCommand, desc='')
|
22
|
-
@
|
25
|
+
@invocation_paths[name] ||= []
|
26
|
+
@adapter.print_command(name, desc, command.new(name, path: @invocation_paths[name]).help)
|
23
27
|
|
24
28
|
command.recognised_subcommands.each do |sub_cmd|
|
29
|
+
path = "#{name} #{sub_cmd.names.first}"
|
30
|
+
@invocation_paths[path] ||= []
|
31
|
+
@invocation_paths[path] += @invocation_paths[name]
|
25
32
|
print_help(@adapter.command_name(name, sub_cmd.names.first), sub_cmd.subcommand_class, sub_cmd.description)
|
26
33
|
end
|
27
34
|
end
|
@@ -14,7 +14,7 @@ module HammerCLI
|
|
14
14
|
def add_usage(invocation_path, usage_descriptions)
|
15
15
|
heading(Clamp.message(:usage_heading))
|
16
16
|
usage_descriptions.each do |usage|
|
17
|
-
puts " #{invocation_path} #{usage}".rstrip
|
17
|
+
puts " #{HammerCLI.expand_invocation_path(invocation_path)} #{usage}".rstrip
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -29,12 +29,19 @@ module HammerCLI
|
|
29
29
|
|
30
30
|
label_width = DEFAULT_LABEL_INDENT
|
31
31
|
items.each do |item|
|
32
|
-
label
|
32
|
+
label = item.help.first
|
33
33
|
label_width = label.size if label.size > label_width
|
34
34
|
end
|
35
35
|
|
36
36
|
items.each do |item|
|
37
|
-
|
37
|
+
if item.respond_to?(:child?) && item.child?
|
38
|
+
next unless HammerCLI.context[:full_help]
|
39
|
+
end
|
40
|
+
label, description = if !HammerCLI.context[:full_help] && item.respond_to?(:family) && item.family
|
41
|
+
[item.family.switch, item.family.description || item.help[1]]
|
42
|
+
else
|
43
|
+
item.help
|
44
|
+
end
|
38
45
|
description.gsub(/^(.)/) { Unicode::capitalize($1) }.each_line do |line|
|
39
46
|
puts " %-#{label_width}s %s" % [label, line]
|
40
47
|
label = ''
|
data/lib/hammer_cli/main.rb
CHANGED
@@ -47,9 +47,11 @@ module HammerCLI
|
|
47
47
|
:context_target => :interactive
|
48
48
|
option ["--no-headers"], :flag, _("Hide headers from output")
|
49
49
|
option ["--csv"], :flag, _("Output as CSV (same as --output=csv)")
|
50
|
-
option [
|
51
|
-
format: HammerCLI::Options::Normalizers::Enum.new(
|
52
|
-
|
50
|
+
option ['--output'], 'ADAPTER', _('Set output format'),
|
51
|
+
format: HammerCLI::Options::Normalizers::Enum.new(
|
52
|
+
HammerCLI::Output::Output.adapters.keys.map(&:to_s)
|
53
|
+
),
|
54
|
+
context_target: :adapter
|
53
55
|
option ["--output-file"], "OUTPUT_FILE", _("Path to custom output file") do |filename|
|
54
56
|
begin
|
55
57
|
context[:output_file] = File.new(filename, 'w')
|