tty 0.0.5 → 0.0.6
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.
- data/README.md +60 -5
- data/lib/tty/shell/question/modifier.rb +96 -0
- data/lib/tty/shell/question/validation.rb +91 -0
- data/lib/tty/shell/question.rb +304 -0
- data/lib/tty/shell/statement.rb +55 -0
- data/lib/tty/shell.rb +159 -0
- data/lib/tty/table/operation/wrapped.rb +6 -0
- data/lib/tty/terminal/color.rb +143 -0
- data/lib/tty/terminal.rb +46 -21
- data/lib/tty/version.rb +1 -1
- data/lib/tty.rb +19 -1
- data/spec/tty/shell/ask_spec.rb +65 -0
- data/spec/tty/shell/error_spec.rb +28 -0
- data/spec/tty/shell/print_table_spec.rb +25 -0
- data/spec/tty/shell/question/initialize_spec.rb +227 -0
- data/spec/tty/shell/question/modifier/apply_to_spec.rb +30 -0
- data/spec/tty/shell/question/modifier/letter_case_spec.rb +27 -0
- data/spec/tty/shell/question/modifier/whitespace_spec.rb +33 -0
- data/spec/tty/shell/question/validation/coerce_spec.rb +25 -0
- data/spec/tty/shell/question/validation/valid_value_spec.rb +28 -0
- data/spec/tty/shell/say_spec.rb +64 -0
- data/spec/tty/shell/statement/initialize_spec.rb +15 -0
- data/spec/tty/shell/warn_spec.rb +28 -0
- data/spec/tty/table/renderer_spec.rb +0 -1
- data/spec/tty/terminal/color/code_spec.rb +19 -0
- data/spec/tty/terminal/color/remove_spec.rb +12 -0
- data/spec/tty/terminal/color/set_spec.rb +30 -0
- data/spec/tty/terminal/color_spec.rb +15 -0
- data/spec/tty/terminal/home_spec.rb +37 -0
- data/tasks/metrics/reek.rake +1 -3
- metadata +48 -11
- data/lib/tty/color.rb +0 -14
- data/spec/tty/color_spec.rb +0 -5
data/lib/tty/shell.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
|
5
|
+
# A class responsible for shell prompt interactions.
|
6
|
+
class Shell
|
7
|
+
|
8
|
+
# @api private
|
9
|
+
attr_reader :input
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
attr_reader :output
|
13
|
+
|
14
|
+
# Initialize a Shell
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def initialize(input=stdin, output=stdout)
|
18
|
+
@input = input
|
19
|
+
@output = output
|
20
|
+
end
|
21
|
+
|
22
|
+
# Ask a question.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# shell = TTY::Shell.new
|
26
|
+
# shell.ask("What is your name?")
|
27
|
+
#
|
28
|
+
# @param [String] statement
|
29
|
+
# string question to be asked
|
30
|
+
#
|
31
|
+
# @yieldparam [TTY::Question] question
|
32
|
+
# further configure the question
|
33
|
+
#
|
34
|
+
# @yield [question]
|
35
|
+
#
|
36
|
+
# @return [TTY::Question]
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def ask(statement, *args, &block)
|
40
|
+
options = Utils.extract_options!(args)
|
41
|
+
|
42
|
+
question = Question.new self, statement, options
|
43
|
+
question.instance_eval(&block) if block_given?
|
44
|
+
question.prompt(statement)
|
45
|
+
end
|
46
|
+
|
47
|
+
# A shortcut method to ask the user positive question and return
|
48
|
+
# true for 'yes' reply.
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def yes?(statement, *args, &block)
|
54
|
+
ask(statement, *args, &block).read_bool
|
55
|
+
end
|
56
|
+
|
57
|
+
# A shortcut method to ask the user negative question and return
|
58
|
+
# true for 'no' reply.
|
59
|
+
#
|
60
|
+
# @return [Boolean]
|
61
|
+
#
|
62
|
+
# @api public
|
63
|
+
def no?(statement, *args, &block)
|
64
|
+
!yes?(statement, *args, &block)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Print statement out. If the supplied message ends with a space or
|
68
|
+
# tab character, a new line will not be appended.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# say("Simple things.")
|
72
|
+
#
|
73
|
+
# @param [String] message
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def say(message="", options={})
|
79
|
+
message = message.to_str
|
80
|
+
return unless message.length > 0
|
81
|
+
|
82
|
+
statement = Statement.new(self, options)
|
83
|
+
statement.declare message
|
84
|
+
end
|
85
|
+
|
86
|
+
# Print statement(s) out in red green.
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# shell.confirm "Are you sure?"
|
90
|
+
# shell.confirm "All is fine!", "This is fine too."
|
91
|
+
#
|
92
|
+
# @param [Array] messages
|
93
|
+
#
|
94
|
+
# @return [Array] messages
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
def confirm(*args)
|
98
|
+
options = Utils.extract_options!(args)
|
99
|
+
args.each { |message| say message, options.merge(:color => :green) }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Print statement(s) out in yellow color.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# shell.warn "This action can have dire consequences"
|
106
|
+
# shell.warn "Carefull young apprentice", "This is potentially dangerous."
|
107
|
+
#
|
108
|
+
# @param [Array] messages
|
109
|
+
#
|
110
|
+
# @return [Array] messages
|
111
|
+
#
|
112
|
+
# @api public
|
113
|
+
def warn(*args)
|
114
|
+
options = Utils.extract_options!(args)
|
115
|
+
args.each { |message| say message, options.merge(:color => :yellow) }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Print statement(s) out in red color.
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# shell.error "Shutting down all systems!"
|
122
|
+
# shell.error "Nothing is fine!", "All is broken!"
|
123
|
+
#
|
124
|
+
# @param [Array] messages
|
125
|
+
#
|
126
|
+
# @return [Array] messages
|
127
|
+
#
|
128
|
+
# @api public
|
129
|
+
def error(*args)
|
130
|
+
options = Utils.extract_options!(args)
|
131
|
+
args.each { |message| say message, options.merge(:color => :red) }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Print a table to shell.
|
135
|
+
#
|
136
|
+
# @return [undefined]
|
137
|
+
#
|
138
|
+
# @api public
|
139
|
+
def print_table(*args, &block)
|
140
|
+
table = TTY::Table.new *args, &block
|
141
|
+
say table.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
def stdin
|
147
|
+
$stdin
|
148
|
+
end
|
149
|
+
|
150
|
+
def stdout
|
151
|
+
$stdout
|
152
|
+
end
|
153
|
+
|
154
|
+
def stderr
|
155
|
+
$stderr
|
156
|
+
end
|
157
|
+
|
158
|
+
end # Shell
|
159
|
+
end # TTY
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Terminal
|
5
|
+
|
6
|
+
# A class responsible for coloring strings.
|
7
|
+
class Color
|
8
|
+
|
9
|
+
# Embed in a String to clear all previous ANSI sequences.
|
10
|
+
CLEAR = "\e[0m"
|
11
|
+
# The start of an ANSI bold sequence.
|
12
|
+
BOLD = "\e[1m"
|
13
|
+
# The start of an ANSI underlined sequence.
|
14
|
+
UNDERLINE = "\e[4m"
|
15
|
+
|
16
|
+
STYLES = %w[ BOLD CLEAR UNDERLINE ].freeze
|
17
|
+
|
18
|
+
# Escape codes for text color.
|
19
|
+
BLACK = "\e[30m"
|
20
|
+
RED = "\e[31m"
|
21
|
+
GREEN = "\e[32m"
|
22
|
+
YELLOW = "\e[33m"
|
23
|
+
BLUE = "\e[34m"
|
24
|
+
MAGENTA = "\e[35m"
|
25
|
+
CYAN = "\e[36m"
|
26
|
+
WHITE = "\e[37m"
|
27
|
+
|
28
|
+
TEXT_COLORS = %w[ BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE ].freeze
|
29
|
+
|
30
|
+
# Escape codes for background color.
|
31
|
+
ON_BLACK = "\e[40m"
|
32
|
+
ON_RED = "\e[41m"
|
33
|
+
ON_GREEN = "\e[42m"
|
34
|
+
ON_YELLOW = "\e[43m"
|
35
|
+
ON_BLUE = "\e[44m"
|
36
|
+
ON_MAGENTA = "\e[45m"
|
37
|
+
ON_CYAN = "\e[46m"
|
38
|
+
ON_WHITE = "\e[47m"
|
39
|
+
|
40
|
+
BACKGROUND_COLORS = %w[ ON_BLACK ON_RED ON_GREEN ON_YELLOW ON_BLUE ON_MAGENTA ON_CYAN ON_WHITE ].freeze
|
41
|
+
|
42
|
+
attr_reader :enabled
|
43
|
+
|
44
|
+
# Initialize a Terminal Color
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def initialize(enabled=false)
|
48
|
+
@enabled = enabled
|
49
|
+
end
|
50
|
+
|
51
|
+
# Disable coloring of this terminal session
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def disable!
|
55
|
+
@enabled = false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if coloring is on
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def enabled?
|
62
|
+
@enabled
|
63
|
+
end
|
64
|
+
|
65
|
+
# Apply ANSI color to the given string.
|
66
|
+
#
|
67
|
+
# @param [String] string
|
68
|
+
# text to add ANSI strings
|
69
|
+
#
|
70
|
+
# @param [Array[Symbol]] colors
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# apply "text", :yellow, :on_green, :underline
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def set(string, *colors)
|
79
|
+
validate *colors
|
80
|
+
ansi_colors = colors.map { |color| lookup(color) }
|
81
|
+
"#{ansi_colors.join}#{string}#{CLEAR}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Same as instance method.
|
85
|
+
#
|
86
|
+
# @return [undefined]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def self.set(string, *colors)
|
90
|
+
new.set(string, *colors)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Remove color codes from a string.
|
94
|
+
#
|
95
|
+
# @param [String] string
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def remove(string)
|
101
|
+
string.gsub(/\e\[\d+(;\d+)*m/, '')
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return raw color code without embeding it into a string.
|
105
|
+
#
|
106
|
+
# @return [Array[String]]
|
107
|
+
# ANSI escape codes
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
def code(*colors)
|
111
|
+
validate *colors
|
112
|
+
colors.map { |color| lookup(color) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# All ANSI color names as strings.
|
116
|
+
#
|
117
|
+
# @return [Array[String]]
|
118
|
+
#
|
119
|
+
# @api public
|
120
|
+
def names
|
121
|
+
(STYLES + BACKGROUND_COLORS + TEXT_COLORS).map { |color| color.to_s.downcase }
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
# Find color representation.
|
127
|
+
#
|
128
|
+
# @api private
|
129
|
+
def lookup(color)
|
130
|
+
self.class.const_get(color.to_s.upcase)
|
131
|
+
end
|
132
|
+
|
133
|
+
# @api private
|
134
|
+
def validate(*colors)
|
135
|
+
unless colors.all? { |color| names.include?(color.to_s) }
|
136
|
+
raise ArgumentError, "Bad color or unintialized constant, valid colors are: #{names.join(', ')}."
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end # Color
|
141
|
+
|
142
|
+
end
|
143
|
+
end # TTY
|
data/lib/tty/terminal.rb
CHANGED
@@ -3,10 +3,6 @@
|
|
3
3
|
module TTY
|
4
4
|
class Terminal
|
5
5
|
|
6
|
-
@@default_width = 80
|
7
|
-
|
8
|
-
@@default_height = 24
|
9
|
-
|
10
6
|
# Return default width of terminal
|
11
7
|
#
|
12
8
|
# @example
|
@@ -15,31 +11,36 @@ module TTY
|
|
15
11
|
# @return [Integer]
|
16
12
|
#
|
17
13
|
# @api public
|
18
|
-
|
19
|
-
@@default_width
|
20
|
-
end
|
14
|
+
attr_reader :default_width
|
21
15
|
|
22
|
-
#
|
16
|
+
# Return default height of terminal
|
23
17
|
#
|
24
|
-
# @
|
18
|
+
# @example
|
19
|
+
# default_height = TTY::Terminal.default_height
|
25
20
|
#
|
26
21
|
# @return [Integer]
|
27
22
|
#
|
28
23
|
# @api public
|
29
|
-
|
30
|
-
|
24
|
+
attr_reader :default_height
|
25
|
+
|
26
|
+
# @api public
|
27
|
+
attr_reader :color
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@color = TTY::Terminal::Color.new(self.color?)
|
31
|
+
@default_width = 80
|
32
|
+
@default_height = 24
|
31
33
|
end
|
32
34
|
|
33
|
-
#
|
35
|
+
# Set default width of terminal
|
34
36
|
#
|
35
|
-
# @
|
36
|
-
# default_height = TTY::Terminal.default_height
|
37
|
+
# @param [Integer] width
|
37
38
|
#
|
38
39
|
# @return [Integer]
|
39
40
|
#
|
40
41
|
# @api public
|
41
|
-
def
|
42
|
-
|
42
|
+
def default_width=(width)
|
43
|
+
@default_width = width
|
43
44
|
end
|
44
45
|
|
45
46
|
# Set default height of terminal
|
@@ -51,7 +52,7 @@ module TTY
|
|
51
52
|
#
|
52
53
|
# @api public
|
53
54
|
def default_height=(height)
|
54
|
-
|
55
|
+
@default_height = height
|
55
56
|
end
|
56
57
|
|
57
58
|
# Determine current width
|
@@ -60,8 +61,9 @@ module TTY
|
|
60
61
|
#
|
61
62
|
# @api width
|
62
63
|
def width
|
63
|
-
|
64
|
-
|
64
|
+
env_tty_columns = ENV['TTY_COLUMNS']
|
65
|
+
if env_tty_columns =~ /^\d+$/
|
66
|
+
result = env_tty_columns.to_i
|
65
67
|
else
|
66
68
|
result = TTY::System.unix? ? dynamic_width : default_width
|
67
69
|
end
|
@@ -73,8 +75,9 @@ module TTY
|
|
73
75
|
#
|
74
76
|
# @api public
|
75
77
|
def height
|
76
|
-
|
77
|
-
|
78
|
+
env_tty_lines = ENV['TTY_LINES']
|
79
|
+
if env_tty_lines =~ /^\d+$/
|
80
|
+
result = env_tty_lines.to_i
|
78
81
|
else
|
79
82
|
result = TTY::System.unix? ? dynamic_height : self.default_height
|
80
83
|
end
|
@@ -145,5 +148,27 @@ module TTY
|
|
145
148
|
%x{tput colors 2>/dev/null}.to_i > 2
|
146
149
|
end
|
147
150
|
|
151
|
+
# Find user home directory
|
152
|
+
#
|
153
|
+
# @return [String]
|
154
|
+
#
|
155
|
+
# @api public
|
156
|
+
def home
|
157
|
+
@home ||= if (env_home = ENV['HOME'])
|
158
|
+
env_home
|
159
|
+
else
|
160
|
+
begin
|
161
|
+
require 'etc'
|
162
|
+
File.expand_path("~#{Etc.getlogin}")
|
163
|
+
rescue
|
164
|
+
if TTY::System.windows?
|
165
|
+
"C:/"
|
166
|
+
else
|
167
|
+
"/"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
148
173
|
end # Terminal
|
149
174
|
end # TTY
|
data/lib/tty/version.rb
CHANGED
data/lib/tty.rb
CHANGED
@@ -9,11 +9,17 @@ require 'tty/support/coercion'
|
|
9
9
|
require 'tty/support/equatable'
|
10
10
|
require 'tty/support/unicode'
|
11
11
|
|
12
|
-
require 'tty/color'
|
13
12
|
require 'tty/terminal'
|
13
|
+
require 'tty/terminal/color'
|
14
14
|
require 'tty/system'
|
15
15
|
require 'tty/table'
|
16
16
|
require 'tty/vector'
|
17
|
+
require 'tty/shell'
|
18
|
+
|
19
|
+
require 'tty/shell/question'
|
20
|
+
require 'tty/shell/question/validation'
|
21
|
+
require 'tty/shell/question/modifier'
|
22
|
+
require 'tty/shell/statement'
|
17
23
|
|
18
24
|
require 'tty/table/border'
|
19
25
|
require 'tty/table/border/unicode'
|
@@ -32,6 +38,18 @@ module TTY
|
|
32
38
|
# Raised when the argument type is different from expected
|
33
39
|
class TypeError < ArgumentError; end
|
34
40
|
|
41
|
+
# Raised when the required argument is not supplied
|
42
|
+
class ArgumentRequired < ArgumentError; end
|
43
|
+
|
44
|
+
# Raised when the argument validation fails
|
45
|
+
class ArgumentValidation < ArgumentError; end
|
46
|
+
|
47
|
+
# Raised when the argument is not expected
|
48
|
+
class InvalidArgument < ArgumentError; end
|
49
|
+
|
50
|
+
# Raised when the passed in validation argument is of wrong type
|
51
|
+
class ValidationCoercion < TypeError; end
|
52
|
+
|
35
53
|
class << self
|
36
54
|
|
37
55
|
# Return terminal instance
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe TTY::Shell, '#ask' do
|
6
|
+
let(:input) { StringIO.new }
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
|
9
|
+
subject(:shell) { TTY::Shell.new(input, output) }
|
10
|
+
|
11
|
+
it 'prints message' do
|
12
|
+
shell.ask "What is your name?"
|
13
|
+
expect(output.string).to eql "What is your name?\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'prints an empty message ' do
|
17
|
+
shell.ask ""
|
18
|
+
expect(output.string).to eql ""
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'prints an empty message and returns nil if EOF is sent to stdin' do
|
22
|
+
input << nil
|
23
|
+
input.rewind
|
24
|
+
q = shell.ask ""
|
25
|
+
expect(q.read).to eql nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'asks a question with block' do
|
29
|
+
input << ''
|
30
|
+
input.rewind
|
31
|
+
q = shell.ask "What is your name?" do
|
32
|
+
default 'Piotr'
|
33
|
+
end
|
34
|
+
expect(q.read).to eql "Piotr"
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'yes?' do
|
38
|
+
it 'agrees' do
|
39
|
+
input << 'yes'
|
40
|
+
input.rewind
|
41
|
+
expect(shell.yes?("Are you a human?")).to be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'disagrees' do
|
45
|
+
input << 'no'
|
46
|
+
input.rewind
|
47
|
+
expect(shell.yes?("Are you a human?")).to be_false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'no?' do
|
52
|
+
it 'agrees' do
|
53
|
+
input << 'no'
|
54
|
+
input.rewind
|
55
|
+
expect(shell.no?("Are you a human?")).to be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'disagrees' do
|
59
|
+
input << 'yes'
|
60
|
+
input.rewind
|
61
|
+
expect(shell.no?("Are you a human?")).to be_false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe TTY::Shell, '#error' do
|
6
|
+
let(:input) { StringIO.new }
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
|
9
|
+
subject(:shell) { TTY::Shell.new(input, output) }
|
10
|
+
|
11
|
+
after { output.rewind }
|
12
|
+
|
13
|
+
it 'displays one message' do
|
14
|
+
shell.error "Nothing is fine!"
|
15
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'displays many messages' do
|
19
|
+
shell.error "Nothing is fine!", "All is broken!"
|
20
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m\n\e[31mAll is broken!\e[0m\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'displays message with option' do
|
24
|
+
shell.error "Nothing is fine!", :newline => false
|
25
|
+
expect(output.string).to eql "\e[31mNothing is fine!\e[0m"
|
26
|
+
end
|
27
|
+
|
28
|
+
end # error
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe TTY::Shell, '#print_table' do
|
6
|
+
let(:input) { StringIO.new }
|
7
|
+
let(:output) { StringIO.new }
|
8
|
+
let(:header) { ['h1', 'h2'] }
|
9
|
+
let(:rows) { [['a1', 'a2'], ['b1', 'b2']] }
|
10
|
+
|
11
|
+
subject(:shell) { TTY::Shell.new(input, output) }
|
12
|
+
|
13
|
+
it 'prints a table' do
|
14
|
+
shell.print_table header, rows, :renderer => :ascii
|
15
|
+
expect(output.string).to eql <<-EOS.normalize
|
16
|
+
+--+--+
|
17
|
+
|h1|h2|
|
18
|
+
+--+--+
|
19
|
+
|a1|a2|
|
20
|
+
|b1|b2|
|
21
|
+
+--+--+\n
|
22
|
+
EOS
|
23
|
+
end
|
24
|
+
|
25
|
+
end # print_table
|