Chrononaut-hirb 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ module Hirb
2
+ module ObjectMethods
3
+ # Takes same options as Hirb::View.render_output.
4
+ def view(*args)
5
+ Hirb::View.console_render_output(*(args.unshift(self)))
6
+ end
7
+ end
8
+ end
9
+
10
+ Object.send :include, Hirb::ObjectMethods
data/lib/hirb/menu.rb ADDED
@@ -0,0 +1,47 @@
1
+ module Hirb
2
+ # This class provides a selection menu using Hirb's table helpers by default to display choices.
3
+ class Menu
4
+ # Menu which asks to select from the given array and returns the selected menu items as an array. See Hirb::Util.choose_from_array
5
+ # for the syntax for specifying selections. All options except for the ones below are passed to render the menu.
6
+ #
7
+ # ==== Options:
8
+ # [:helper_class] Helper class to render menu. Helper class is expected to implement numbering given a :number option.
9
+ # To use a very basic menu, set this to false. Defaults to Hirb::Helpers::AutoTable.
10
+ # [:prompt] String for menu prompt. Defaults to "Choose: ".
11
+ # [:validate_one] Validates that only one item in array is chosen and returns just that item. Default is false.
12
+ # [:ask] Always ask for input, even if there is only one choice. Default is true.
13
+ # Examples:
14
+ # extend Hirb::Console
15
+ # menu([1,2,3], :fields=>[:field1, :field2], :validate_one=>true)
16
+ # menu([1,2,3], :helper_class=>Hirb::Helpers::Table)
17
+ def self.render(output, options={})
18
+ default_options = {:helper_class=>Hirb::Helpers::AutoTable, :prompt=>"Choose #{options[:validate_one] ? 'one' : ''}: ", :ask=>true}
19
+ options = default_options.merge(options)
20
+ output = [output] unless output.is_a?(Array)
21
+ chosen = choose_from_menu(output, options)
22
+ yield(chosen) if block_given? && chosen.is_a?(Array) && chosen.size > 0
23
+ chosen
24
+ end
25
+
26
+ def self.choose_from_menu(output, options) #:nodoc:
27
+ return output if output.size == 1 && !options[:ask]
28
+ if (helper_class = Util.any_const_get(options[:helper_class]))
29
+ View.render_output(output, :class=>options[:helper_class], :options=>options.merge(:number=>true))
30
+ else
31
+ output.each_with_index {|e,i| puts "#{i+1}: #{e}" }
32
+ end
33
+ print options[:prompt]
34
+ input = $stdin.gets.chomp.strip
35
+ chosen = Util.choose_from_array(output, input)
36
+ if options[:validate_one]
37
+ if chosen.size != 1
38
+ $stderr.puts "Choose one. You chose #{chosen.size} items."
39
+ return nil
40
+ else
41
+ return chosen[0]
42
+ end
43
+ end
44
+ chosen
45
+ end
46
+ end
47
+ end
data/lib/hirb/pager.rb ADDED
@@ -0,0 +1,94 @@
1
+ module Hirb
2
+ # This class provides class methods for paging and an object which can conditionally page given a terminal size that is exceeded.
3
+ class Pager
4
+ class<<self
5
+ # Pages using a configured or detected shell command.
6
+ def command_pager(output, options={})
7
+ basic_pager(output) if valid_pager_command?(options[:pager_command])
8
+ end
9
+
10
+ def pager_command(*commands) #:nodoc:
11
+ @pager_command = (!@pager_command.nil? && commands.empty?) ? @pager_command :
12
+ begin
13
+ commands = [ENV['PAGER'], 'less', 'more', 'pager'] if commands.empty?
14
+ commands.compact.uniq.find {|e| Util.command_exists?(e[/\w+/]) }
15
+ end
16
+ end
17
+
18
+ # Pages with a ruby-only pager which either pages or quits.
19
+ def default_pager(output, options={})
20
+ pager = new(options[:width], options[:height])
21
+ while pager.activated_by?(output, options[:inspect])
22
+ puts pager.slice!(output, options[:inspect])
23
+ return unless continue_paging?
24
+ end
25
+ puts output
26
+ puts "=== Pager finished. ==="
27
+ end
28
+
29
+ #:stopdoc:
30
+ def valid_pager_command?(cmd)
31
+ cmd ? pager_command(cmd) : pager_command
32
+ end
33
+
34
+ private
35
+ def basic_pager(output)
36
+ pager = IO.popen(pager_command, "w")
37
+ begin
38
+ save_stdout = STDOUT.clone
39
+ STDOUT.reopen(pager)
40
+ STDOUT.puts output
41
+ ensure
42
+ STDOUT.reopen(save_stdout)
43
+ save_stdout.close
44
+ pager.close
45
+ end
46
+ end
47
+
48
+ def continue_paging?
49
+ puts "=== Press enter/return to continue or q to quit: ==="
50
+ !$stdin.gets.chomp[/q/i]
51
+ end
52
+ #:startdoc:
53
+ end
54
+
55
+ attr_reader :width, :height
56
+
57
+ def initialize(width, height, options={})
58
+ resize(width, height)
59
+ @pager_command = options[:pager_command] if options[:pager_command]
60
+ end
61
+
62
+ # Pages given string using configured pager.
63
+ def page(string, inspect_mode)
64
+ if self.class.valid_pager_command?(@pager_command)
65
+ self.class.command_pager(string, :pager_command=>@pager_command)
66
+ else
67
+ self.class.default_pager(string, :width=>@width, :height=>@height, :inspect=>inspect_mode)
68
+ end
69
+ end
70
+
71
+ def slice!(output, inspect_mode=false) #:nodoc:
72
+ effective_height = @height - 2 # takes into account pager prompt
73
+ if inspect_mode
74
+ sliced_output = output.slice(0, @width * effective_height)
75
+ output.replace output.slice(@width * effective_height..-1)
76
+ sliced_output
77
+ else
78
+ # could use output.scan(/[^\n]*\n?/) instead of split
79
+ sliced_output = output.split("\n").slice(0, effective_height).join("\n")
80
+ output.replace output.split("\n").slice(effective_height..-1).join("\n")
81
+ sliced_output
82
+ end
83
+ end
84
+
85
+ # Determines if string should be paged based on configured width and height.
86
+ def activated_by?(string_to_page, inspect_mode=false)
87
+ inspect_mode ? (string_to_page.size > @height * @width) : (string_to_page.count("\n") > @height)
88
+ end
89
+
90
+ def resize(width, height) #:nodoc:
91
+ @width, @height = Hirb::View.determine_terminal_size(width, height)
92
+ end
93
+ end
94
+ end
data/lib/hirb/util.rb ADDED
@@ -0,0 +1,80 @@
1
+ module Hirb
2
+ # Group of handy utility functions used throughout Hirb.
3
+ module Util
4
+ extend self
5
+ # Returns a constant like Module#const_get no matter what namespace it's nested in.
6
+ # Returns nil if the constant is not found.
7
+ def any_const_get(name)
8
+ return name if name.is_a?(Module)
9
+ begin
10
+ klass = Object
11
+ name.split('::').each {|e|
12
+ klass = klass.const_get(e)
13
+ }
14
+ klass
15
+ rescue
16
+ nil
17
+ end
18
+ end
19
+
20
+ # Recursively merge hash1 with hash2.
21
+ def recursive_hash_merge(hash1, hash2)
22
+ hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
23
+ end
24
+
25
+ # From Rails ActiveSupport, converting undescored lowercase to camel uppercase.
26
+ def camelize(string)
27
+ string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
28
+ end
29
+
30
+ # Used by Hirb::Menu to select items from an array. Array counting starts at 1. Ranges of numbers are specified with a '-' or '..'.
31
+ # Multiple ranges can be comma delimited. Anything that isn't a valid number is ignored. All elements can be returned with a '*'.
32
+ # Examples:
33
+ # 1-3,5-6 -> [1,2,3,5,6]
34
+ # * -> all elements in array
35
+ # '' -> []
36
+ def choose_from_array(array, input, options={})
37
+ options = {:splitter=>","}.merge(options)
38
+ return array if input.strip == '*'
39
+ result = []
40
+ input.split(options[:splitter]).each do |e|
41
+ if e =~ /-|\.\./
42
+ min,max = e.split(/-|\.\./)
43
+ slice_min = min.to_i - 1
44
+ result.push(*array.slice(slice_min, max.to_i - min.to_i + 1))
45
+ elsif e =~ /\s*(\d+)\s*/
46
+ index = $1.to_i - 1
47
+ next if index < 0
48
+ result.push(array[index]) if array[index]
49
+ end
50
+ end
51
+ return result
52
+ end
53
+
54
+ # Determines if a shell command exists by searching for it in ENV['PATH'].
55
+ def command_exists?(command)
56
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
57
+ end
58
+
59
+ # Returns [width, height] of terminal when detected, nil if not detected.
60
+ # Think of this as a simpler version of Highline's Highline::SystemExtensions.terminal_size()
61
+ def detect_terminal_size
62
+ (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/) ? [ENV['COLUMNS'].to_i, ENV['LINES'].to_i] :
63
+ ( command_exists?('stty') ? `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse : nil )
64
+ rescue
65
+ nil
66
+ end
67
+
68
+ # Captures STDOUT of anything run in its block and returns it as string.
69
+ def capture_stdout(&block)
70
+ original_stdout = $stdout
71
+ $stdout = fake = StringIO.new
72
+ begin
73
+ yield
74
+ ensure
75
+ $stdout = original_stdout
76
+ end
77
+ fake.string
78
+ end
79
+ end
80
+ end
data/lib/hirb/view.rb ADDED
@@ -0,0 +1,177 @@
1
+ module Hirb
2
+ # This class is responsible for managing all view-related functionality. Its functionality is determined by setting up a configuration file
3
+ # as explained in Hirb and/or passed configuration directly to Hirb.enable. Most of the functionality in this class is dormant until enabled.
4
+ module View
5
+ DEFAULT_WIDTH = 120
6
+ DEFAULT_HEIGHT = 40
7
+ class << self
8
+ attr_accessor :render_method
9
+ attr_reader :config
10
+
11
+ # This activates view functionality i.e. the formatter, pager and size detection. If irb exists, it overrides irb's output
12
+ # method with Hirb::View.view_output. If using Wirble, you should call this after it. The view configuration
13
+ # can be specified in a hash via a config file, as options to this method, as this method's block or any combination of these three.
14
+ # In addition to the config keys mentioned in Hirb, the options also take the following keys:
15
+ # Options:
16
+ # * config_file: Name of config file to read.
17
+ # Examples:
18
+ # Hirb::View.enable
19
+ # Hirb::View.enable :formatter=>false
20
+ # Hirb::View.enable {|c| c.output = {'String'=>{:class=>'Hirb::Helpers::Table'}} }
21
+ def enable(options={}, &block)
22
+ return puts("Already enabled.") if @enabled
23
+ @enabled = true
24
+ Hirb.config_file = options.delete(:config_file) if options[:config_file]
25
+ load_config(Util.recursive_hash_merge(options, HashStruct.block_to_hash(block)))
26
+ resize(config[:width], config[:height])
27
+ if Object.const_defined?(:IRB)
28
+ ::IRB::Irb.class_eval do
29
+ alias :non_hirb_view_output :output_value
30
+ def output_value #:nodoc:
31
+ Hirb::View.view_output(@context.last_value) || Hirb::View.page_output(@context.last_value.inspect, true) ||
32
+ non_hirb_view_output
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # Indicates if Hirb::View is enabled.
39
+ def enabled?
40
+ @enabled || false
41
+ end
42
+
43
+ # Disable's Hirb's output and revert's irb's output method if irb exists.
44
+ def disable
45
+ @enabled = false
46
+ if Object.const_defined?(:IRB)
47
+ ::IRB::Irb.class_eval do
48
+ alias :output_value :non_hirb_view_output
49
+ end
50
+ end
51
+ end
52
+
53
+ # Toggles pager on or off. The pager only works while Hirb::View is enabled.
54
+ def toggle_pager
55
+ config[:pager] = !config[:pager]
56
+ end
57
+
58
+ # Toggles formatter on or off.
59
+ def toggle_formatter
60
+ config[:formatter] = !config[:formatter]
61
+ end
62
+
63
+ # Resizes the console width and height for use with the table and pager i.e. after having resized the console window. *nix users
64
+ # should only have to call this method. Non-*nix users should call this method with explicit width and height. If you don't know
65
+ # your width and height, in irb play with "a"* width to find width and puts "a\n" * height to find height.
66
+ def resize(width=nil, height=nil)
67
+ config[:width], config[:height] = determine_terminal_size(width, height)
68
+ pager.resize(config[:width], config[:height])
69
+ end
70
+
71
+ # This is the main method of this class. When view is enabled, this method searches for a formatter it can use for the output and if
72
+ # successful renders it using render_method(). The options this method takes are helper config hashes as described in
73
+ # Hirb::Formatter.format_output(). Returns true if successful and false if no formatting is done or if not enabled.
74
+ def view_output(output, options={})
75
+ enabled? && config[:formatter] && render_output(output, options)
76
+ end
77
+
78
+ # Captures STDOUT and renders it using render_method(). The main use case is to conditionally page captured stdout.
79
+ def capture_and_render(&block)
80
+ render_method.call Util.capture_stdout(&block)
81
+ end
82
+
83
+ # A lambda or proc which handles the final formatted object.
84
+ # Although this pages/puts the object by default, it could be set to do other things
85
+ # i.e. write the formatted object to a file.
86
+ def render_method
87
+ @render_method ||= default_render_method
88
+ end
89
+
90
+ # Resets render_method back to its default.
91
+ def reset_render_method
92
+ @render_method = default_render_method
93
+ end
94
+
95
+ # Current console width
96
+ def width
97
+ config ? config[:width] : DEFAULT_WIDTH
98
+ end
99
+
100
+ # Current console height
101
+ def height
102
+ config ? config[:height] : DEFAULT_HEIGHT
103
+ end
104
+
105
+ # Current formatter config
106
+ def formatter_config
107
+ formatter.config
108
+ end
109
+
110
+ # Sets the helper config for the given output class.
111
+ def format_class(klass, helper_config)
112
+ formatter.format_class(klass, helper_config)
113
+ end
114
+
115
+ #:stopdoc:
116
+ def render_output(output, options={})
117
+ if (formatted_output = formatter.format_output(output, options))
118
+ render_method.call(formatted_output)
119
+ true
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ def determine_terminal_size(width, height)
126
+ detected = (width.nil? || height.nil?) ? Util.detect_terminal_size || [] : []
127
+ [width || detected[0] || DEFAULT_WIDTH , height || detected[1] || DEFAULT_HEIGHT]
128
+ end
129
+
130
+ def page_output(output, inspect_mode=false)
131
+ if enabled? && config[:pager] && pager.activated_by?(output, inspect_mode)
132
+ pager.page(output, inspect_mode)
133
+ true
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ def pager
140
+ @pager ||= Pager.new(config[:width], config[:height], :pager_command=>config[:pager_command])
141
+ end
142
+
143
+ def pager=(value); @pager = value; end
144
+
145
+ def formatter(reload=false)
146
+ @formatter = reload || @formatter.nil? ? Formatter.new(config[:output]) : @formatter
147
+ end
148
+
149
+ def formatter=(value); @formatter = value; end
150
+
151
+ def load_config(additional_config={})
152
+ @config = Util.recursive_hash_merge default_config, additional_config
153
+ formatter(true)
154
+ true
155
+ end
156
+
157
+ def config_loaded?; !!@config; end
158
+
159
+ def config
160
+ @config
161
+ end
162
+
163
+ def default_render_method
164
+ lambda {|output| page_output(output) || puts(output) }
165
+ end
166
+
167
+ def default_config
168
+ Util.recursive_hash_merge({:pager=>true, :formatter=>true}, Hirb.config || {})
169
+ end
170
+ #:startdoc:
171
+ end
172
+ end
173
+
174
+ # Namespace for autoloaded views
175
+ module Views
176
+ end
177
+ end
@@ -0,0 +1,9 @@
1
+ class Hirb::Views::ActiveRecord_Base #:nodoc:
2
+ def self.default_options
3
+ {:ancestor=>true}
4
+ end
5
+
6
+ def self.render(*args)
7
+ Hirb::Helpers::ActiveRecordTable.render(*args)
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Hirb::ConsoleTest < Test::Unit::TestCase
4
+ context "parse_input" do
5
+ test "config is set if it wasn't before" do
6
+ reset_config
7
+ Hirb::View.expects(:render_output)
8
+ Hirb::Console.render_output('blah')
9
+ Hirb::View.config.is_a?(Hash).should == true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,172 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ module Hirb
4
+ class FormatterTest < Test::Unit::TestCase
5
+ context "formatter" do
6
+ def set_formatter(hash={})
7
+ @formatter = Formatter.new(hash)
8
+ end
9
+
10
+ before(:all) { eval "module ::Dooda; end" }
11
+
12
+ test "klass_config merges ancestor options" do
13
+ set_formatter "String"=>{:args=>[1,2]}, "Object"=>{:method=>:object_output, :ancestor=>true}, "Kernel"=>{:method=>:default_output}
14
+ expected_result = {:method=>:object_output, :args=>[1, 2], :ancestor=>true}
15
+ @formatter.klass_config(String).should == expected_result
16
+ end
17
+
18
+ test "klass_config doesn't merge ancestor options" do
19
+ set_formatter "String"=>{:args=>[1,2]}, "Object"=>{:method=>:object_output}, "Kernel"=>{:method=>:default_output}
20
+ expected_result = {:args=>[1, 2]}
21
+ @formatter.klass_config(String).should == expected_result
22
+ end
23
+
24
+ test "klass_config returns hash when nothing found" do
25
+ set_formatter.klass_config(String).should == {}
26
+ end
27
+
28
+ test "reload detects new Hirb::Views" do
29
+ set_formatter
30
+ @formatter.config.keys.include?('Zzz').should be(false)
31
+ eval "module ::Hirb::Views::Zzz; def self.render; end; end"
32
+ @formatter.reload
33
+ @formatter.config.keys.include?('Zzz').should be(true)
34
+ end
35
+
36
+ test "format_class sets formatter config" do
37
+ set_formatter
38
+ @formatter.format_class ::Dooda, :class=>"DoodaView"
39
+ @formatter.klass_config(::Dooda).should == {:class=>"DoodaView"}
40
+ end
41
+
42
+ test "format_class overwrites existing formatter config" do
43
+ set_formatter "Dooda"=>{:class=>"DoodaView"}
44
+ @formatter.format_class ::Dooda, :class=>"DoodaView2"
45
+ @formatter.klass_config(::Dooda).should == {:class=>"DoodaView2"}
46
+ end
47
+
48
+ test "parse_console_options passes all options except for formatter options into :options" do
49
+ set_formatter
50
+ options = {:class=>'blah', :method=>'blah', :output_method=>'blah', :blah=>'blah'}
51
+ expected_options = {:class=>'blah', :method=>'blah', :output_method=>'blah', :options=>{:blah=>'blah'}}
52
+ @formatter.parse_console_options(options).should == expected_options
53
+ end
54
+ end
55
+
56
+ context "enable" do
57
+ before(:each) { View.formatter = nil; reset_config }
58
+ after(:each) { Hirb.disable }
59
+
60
+ def formatter_config
61
+ View.formatter.config
62
+ end
63
+
64
+ test "sets default formatter config" do
65
+ eval "module ::Hirb::Views::Something_Base; def self.render; end; end"
66
+ Hirb.enable
67
+ formatter_config["Something::Base"].should == {:class=>"Hirb::Views::Something_Base"}
68
+ end
69
+
70
+ test "sets default formatter config with default_options" do
71
+ eval "module ::Hirb::Views::Blah; def self.render; end; def self.default_options; {:ancestor=>true}; end; end"
72
+ Hirb.enable
73
+ formatter_config["Blah"].should == {:class=>"Hirb::Views::Blah", :ancestor=>true}
74
+ end
75
+
76
+ test "with block sets formatter config" do
77
+ class_hash = {"Something::Base"=>{:class=>"BlahBlah"}}
78
+ Hirb.enable {|c| c.output = class_hash }
79
+ formatter_config['Something::Base'].should == class_hash['Something::Base']
80
+ end
81
+ end
82
+
83
+ context "format_output" do
84
+ def view_output(*args, &block); View.view_output(*args, &block); end
85
+ def render_method(*args); View.render_method(*args); end
86
+
87
+ def enable_with_output(value)
88
+ Hirb.enable :output=>value
89
+ end
90
+
91
+ before(:all) {
92
+ eval %[module ::Commify
93
+ def self.render(strings)
94
+ strings = [strings] unless strings.is_a?(Array)
95
+ strings.map {|e| e.split('').join(',')}.join("\n")
96
+ end
97
+ end]
98
+ reset_config
99
+ }
100
+ before(:each) { View.formatter = nil; reset_config }
101
+ after(:each) { Hirb.disable }
102
+
103
+ test "formats with method option" do
104
+ eval "module ::Kernel; def commify(string); string.split('').join(','); end; end"
105
+ enable_with_output "String"=>{:method=>:commify}
106
+ render_method.expects(:call).with('d,u,d,e')
107
+ view_output('dude')
108
+ end
109
+
110
+ test "formats with class option" do
111
+ enable_with_output "String"=>{:class=>"Commify"}
112
+ render_method.expects(:call).with('d,u,d,e')
113
+ view_output('dude')
114
+ end
115
+
116
+ test "formats with class option as symbol" do
117
+ enable_with_output "String"=>{:class=>:auto_table}
118
+ Helpers::AutoTable.expects(:render)
119
+ view_output('dude')
120
+ end
121
+
122
+ test "formats output array" do
123
+ enable_with_output "String"=>{:class=>"Commify"}
124
+ render_method.expects(:call).with('d,u,d,e')
125
+ view_output(['dude'])
126
+ end
127
+
128
+ test "formats with options option" do
129
+ eval "module ::Blahify; def self.render(*args); end; end"
130
+ enable_with_output "String"=>{:class=>"Blahify", :options=>{:fields=>%w{a b}}}
131
+ Blahify.expects(:render).with('dude', :fields=>%w{a b})
132
+ view_output('dude')
133
+ end
134
+
135
+ test "doesn't format and returns false when no format method found" do
136
+ Hirb.enable
137
+ render_method.expects(:call).never
138
+ view_output(Date.today).should == false
139
+ end
140
+
141
+ test "formats with output_method option as method" do
142
+ enable_with_output 'String'=>{:class=>"Commify", :output_method=>:chop}
143
+ render_method.expects(:call).with('d,u,d')
144
+ view_output('dude')
145
+ end
146
+
147
+ test "formats with output_method option as proc" do
148
+ enable_with_output 'String'=>{:class=>"Commify", :output_method=>lambda {|e| e.chop}}
149
+ render_method.expects(:call).with('d,u,d')
150
+ view_output('dude')
151
+ end
152
+
153
+ test "formats output array with output_method option" do
154
+ enable_with_output 'String'=>{:class=>"Commify", :output_method=>:chop}
155
+ render_method.expects(:call).with("d,u,d\nm,a")
156
+ view_output(['dude', 'man'])
157
+ end
158
+
159
+ test "formats with explicit class option" do
160
+ enable_with_output 'String'=>{:class=>"Blahify"}
161
+ render_method.expects(:call).with('d,u,d,e')
162
+ view_output('dude', :class=>"Commify")
163
+ end
164
+
165
+ test "formats with explicit options option merges with existing options" do
166
+ enable_with_output "String"=>{:class=>"Commify", :options=>{:fields=>%w{f1 f2}}}
167
+ Commify.expects(:render).with('dude', :max_width=>10, :fields=>%w{f1 f2})
168
+ view_output('dude', :options=>{:max_width=>10})
169
+ end
170
+ end
171
+ end
172
+ end