imapcli 1.0.7 → 2.0.0

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 CHANGED
@@ -9,15 +9,15 @@ IMAP mailbox sizes.
9
9
 
10
10
  ## Table of contents
11
11
 
12
- * [Motivation](#motivation)
13
- * [Warning](#warning)
14
- * [Installing and executing `imapcli`](#installing-and-executing-imapcli)
15
- * [Terminology](#terminology)
16
- * [Usage](#usage)
17
- * [Alternative resources](#alternative-resources)
18
- * [State of the project](#state-of-the-project)
19
- * [Credits](#credits)
20
- * [License](#license)
12
+ - [Motivation](#motivation)
13
+ - [Warning](#warning)
14
+ - [Installing and executing `imapcli`](#installing-and-executing-imapcli)
15
+ - [Terminology](#terminology)
16
+ - [Usage](#usage)
17
+ - [Alternative resources](#alternative-resources)
18
+ - [State of the project](#state-of-the-project)
19
+ - [Credits](#credits)
20
+ - [License](#license)
21
21
 
22
22
  ## Motivation
23
23
 
@@ -83,7 +83,7 @@ Install:
83
83
  Run:
84
84
 
85
85
  cd imapcli
86
- bundle exec bin/imapcli
86
+ bundle exec exe/imapcli
87
87
 
88
88
  ### Install the gem
89
89
 
@@ -106,11 +106,11 @@ smaller).
106
106
 
107
107
  Run:
108
108
 
109
- docker run -it bovender/imapcli <arguments>
109
+ docker run -it --rm bovender/imapcli <arguments>
110
110
 
111
111
  Example:
112
112
 
113
- docker run -it bovender/imapcli -s myserver.example.com -u user -P info
113
+ docker run -it --rm bovender/imapcli -s myserver.example.com -u user -P info
114
114
 
115
115
  The Docker repository is at <https://hub.docker.com/r/bovender/imapcli>.
116
116
 
@@ -124,7 +124,7 @@ have their mails organized in **folders**; in IMAP speak, a folder is a **maibox
124
124
  For basic usage instructions and possible options, run `imapcli` and examine
125
125
  the output. Please note that `imapcli` distinguishes between global and
126
126
  command-specific options. Global options *precede* and command-specific options
127
- *follow* a `command`, see the output of `imapcli` (without command or options)
127
+ -follow* a `command`, see the output of `imapcli` (without command or options)
128
128
  for more information.
129
129
 
130
130
  Note: The following examples use the command `imapcli`. Depending on how you
@@ -187,13 +187,15 @@ of messages in them, this may take a little while.
187
187
 
188
188
  `imapcli` prints the following statistics about the message sizes in a mailbox:
189
189
 
190
- * `Count`: Number of individual messages
191
- * `Total size`: Total size of all messages in the mailbox (in kiB)
192
- * `Min`: Size of the smallest message in the mailbox (in kiB)
193
- * `Q1`: First quartile of message sizes in the mailbox (in kiB)
194
- * `Median`: Median of all message sizes in the mailbox (in kiB)
195
- * `Q3`: First quartile of message sizes in the mailbox (in kiB)
196
- * `Max`: Size of the largest message in the mailbox (in kiB)
190
+ - `Count`: Number of individual messages
191
+ - `Total size`: Total size of all messages in the mailbox
192
+ - `Min`: Size of the smallest message in the mailbox
193
+ - `Q1`: First quartile of message sizes in the mailbox
194
+ - `Median`: Median of all message sizes in the mailbox
195
+ - `Q3`: Third quartile of message sizes in the mailbox
196
+ - `Max`: Size of the largest message in the mailbox
197
+
198
+ Use the `-H` or `--human` switch to output the message sizes with SI prefixes.
197
199
 
198
200
  #### All mailboxes
199
201
 
@@ -245,13 +247,20 @@ Use the `-r`/`--recurse` flag:
245
247
  By default, mailboxes are sorted alphabetically. To sort by a specific statistic,
246
248
  use an `-o`/`--sort` option:
247
249
 
248
- * `-o count`
249
- * `-o total_size`
250
- * `-o min_size`
251
- * `-o q1`
252
- * `-o median_size`
253
- * `-o q3`
254
- * `-o max_size`
250
+ - `-o count`
251
+ - `-o total_size`
252
+ - `-o min_size`
253
+ - `-o q1_size`
254
+ - `-o median_size`
255
+ - `-o q3_size`
256
+ - `-o max_size`
257
+
258
+ This will sort the output from smallest to largest (ascending).
259
+
260
+ Use the `--reverse` switch to sort from largest to smallest (descending).
261
+
262
+ Note that these are options to the `stats` command and need to come after the `stats`
263
+ keyword, as shown below.
255
264
 
256
265
  Example:
257
266
 
@@ -268,18 +277,18 @@ following:
268
277
 
269
278
  ### IMAP folder size script
270
279
 
271
- * <https://code.iamcal.com/pl/imap_folders>
280
+ - <https://code.iamcal.com/pl/imap_folders>
272
281
 
273
282
  Ad-hoc perl script that computes the sizes of each mailbox. `imapcli` was
274
283
  inspired by this!
275
284
 
276
285
  ### IMAP synchronization and backup tools
277
286
 
278
- * <https://github.com/OfflineIMAP/imapfw>
287
+ - <https://github.com/OfflineIMAP/imapfw>
279
288
 
280
289
  Framework to work with mails
281
290
 
282
- * <https://github.com/polo2ro/imapbox>
291
+ - <https://github.com/polo2ro/imapbox>
283
292
 
284
293
  Pull down e-mails from an IMAP server to your local disk
285
294
 
@@ -288,18 +297,15 @@ following:
288
297
  I have not been able to work on this project for quite some time. It still
289
298
  serves me well when I occasionally need it. Pull requests are of course welcome.
290
299
 
291
- I've decided to have one `main` branch, and to get rid of the `master` and
292
- `
293
-
294
300
  This project is [semantically versioned](https://semver.org).
295
301
 
296
302
  ### To do
297
303
 
298
- * More human-friendly number formatting (e.g., MiB/GiB as appropriate)
299
- * Output to file
300
- * Deal with server-specific mailbox separator characters (e.g. '.' vs. '/')
301
- * Man page
302
- * More commands?
304
+ [x] More human-friendly number formatting (e.g., MiB/GiB as appropriate)
305
+ [ ] Output to file
306
+ [ ] Deal with server-specific mailbox separator characters (e.g. '.' vs. '/')
307
+ [ ] Man page
308
+ [ ] More commands?
303
309
 
304
310
  ## Credits
305
311
 
@@ -308,9 +314,14 @@ gem by [David Copeland](https://github.com/davetron5000) and makes extensive use
308
314
  of [Piotr Murach's](https://github.com/piotrmurach) excellent `TTY` tools. See
309
315
  the `Gemfile` for other work that this tool depends on.
310
316
 
317
+ ## Contributors
318
+
319
+ - [@bovender](https://github.com/bovender)
320
+ - [@n-rodriguez](https://github.com/n-rodriguez)
321
+
311
322
  ## License
312
323
 
313
- &copy; 2017, 2022 Daniel Kraus (bovender)
324
+ &copy; 2017-2025 Daniel Kraus (@bovender)
314
325
 
315
326
  Licensed under the Apache License, Version 2.0 (the "License");
316
327
  you may not use this file except in compliance with the License.
data/Rakefile CHANGED
@@ -1,20 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/clean'
2
4
  require 'rubygems'
3
5
  require 'rubygems/package_task'
4
6
  require 'rdoc/task'
5
7
  # require 'cucumber'
6
8
  # require 'cucumber/rake/task'
9
+
7
10
  Rake::RDocTask.new do |rd|
8
- rd.main = "README.md"
9
- rd.rdoc_files.include("README.md","lib/**/*.rb","bin/**/*")
11
+ rd.main = 'README.md'
12
+ rd.rdoc_files.include('README.md', 'lib/**/*.rb', 'bin/**/*')
10
13
  rd.title = 'imapcli'
11
14
  end
12
15
 
13
- spec = eval(File.read('imapcli.gemspec'))
14
-
15
- Gem::PackageTask.new(spec) do |pkg|
16
- end
17
-
18
16
  # CUKE_RESULTS = 'results.html'
19
17
  # CLEAN << CUKE_RESULTS
20
18
  # desc 'Run features'
data/bin/bundle ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN)
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../Gemfile", __dir__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_requirement
64
+ @bundler_requirement ||=
65
+ env_var_version ||
66
+ cli_arg_version ||
67
+ bundler_requirement_for(lockfile_version)
68
+ end
69
+
70
+ def bundler_requirement_for(version)
71
+ return "#{Gem::Requirement.default}.a" unless version
72
+
73
+ bundler_gem_version = Gem::Version.new(version)
74
+
75
+ bundler_gem_version.approximate_recommendation
76
+ end
77
+
78
+ def load_bundler!
79
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
80
+
81
+ activate_bundler
82
+ end
83
+
84
+ def activate_bundler
85
+ gem_error = activation_error_handling do
86
+ gem "bundler", bundler_requirement
87
+ end
88
+ return if gem_error.nil?
89
+ require_error = activation_error_handling do
90
+ require "bundler/version"
91
+ end
92
+ return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
93
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
94
+ exit 42
95
+ end
96
+
97
+ def activation_error_handling
98
+ yield
99
+ nil
100
+ rescue StandardError, LoadError => e
101
+ e
102
+ end
103
+ end
104
+
105
+ m.load_bundler!
106
+
107
+ if m.invoked_as_script?
108
+ load Gem.bin_path("bundler", "bundle")
109
+ end
data/bin/rspec ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
12
+
13
+ bundle_binstub = File.expand_path('bundle', __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require 'rubygems'
25
+ require 'bundler/setup'
26
+
27
+ load Gem.bin_path('rubocop', 'rubocop')
data/exe/imapcli ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/imapcli'
5
+
6
+ Dotenv.load
7
+
8
+ exit Imapcli::Cli.run(ARGV)
data/imapcli.gemspec CHANGED
@@ -1,27 +1,35 @@
1
- # Ensure we require the local version and not one we might have installed already
2
- require File.join([File.dirname(__FILE__),'lib','imapcli','version.rb'])
3
- spec = Gem::Specification.new do |s|
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/imapcli/version'
4
+
5
+ Gem::Specification.new do |s|
4
6
  s.name = 'imapcli'
5
7
  s.version = Imapcli::VERSION
8
+ s.platform = Gem::Platform::RUBY
6
9
  s.author = 'Daniel Kraus (bovender)'
7
10
  s.email = 'bovender@bovender.de'
8
11
  s.homepage = 'https://github.com/bovender/imapcli'
9
- s.license = 'Apache-2.0'
10
- s.platform = Gem::Platform::RUBY
11
12
  s.summary = 'Command-line tool to query IMAP servers'
13
+ s.license = 'Apache-2.0'
14
+
15
+ s.required_ruby_version = '>= 3.1.0'
16
+
12
17
  s.files = `git ls-files`.split("\n")
13
- s.require_paths << 'lib'
14
- s.extra_rdoc_files = ['README.md','imapcli.rdoc']
18
+
19
+ s.extra_rdoc_files = ['README.md', 'imapcli.rdoc']
15
20
  s.rdoc_options << '--title' << 'imapcli' << '--main' << 'README.md' << '-ri'
16
- s.bindir = 'bin'
17
- s.executables << 'imapcli'
18
- s.add_development_dependency('rake', '~> 12.3.3')
19
- s.add_development_dependency('rdoc', '~> 6.3')
20
- s.add_runtime_dependency('descriptive_statistics', '~> 2.5')
21
- s.add_runtime_dependency('dotenv', '~> 2.2')
22
- s.add_runtime_dependency('filesize', '~> 0.1')
23
- s.add_runtime_dependency('gli','~> 2.17')
24
- s.add_runtime_dependency('tty-progressbar', '~> 0.13')
25
- s.add_runtime_dependency('tty-prompt', '~> 0.13')
26
- s.add_runtime_dependency('tty-table', '~> 0.9')
21
+
22
+ s.bindir = 'exe'
23
+ s.executables = ['imapcli']
24
+
25
+ s.add_dependency('csv')
26
+ s.add_dependency('descriptive_statistics', '~> 2.5')
27
+ s.add_dependency('dotenv', '~> 3.1')
28
+ s.add_dependency('activesupport', '~> 8.0')
29
+ s.add_dependency('gli', '~> 2.22')
30
+ s.add_dependency('net-imap')
31
+ s.add_dependency('tty-progressbar', '~> 0.18')
32
+ s.add_dependency('tty-prompt', '~> 0.23')
33
+ s.add_dependency('tty-table', '~> 0.12')
34
+ s.add_dependency('zeitwerk', '~> 2.7.0')
27
35
  end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imapcli
4
+ class Cli # rubocop:disable Metrics/ClassLength,Style/Documentation
5
+ extend GLI::App
6
+
7
+ program_desc 'Command-line interface for IMAP servers'
8
+
9
+ version Imapcli::VERSION
10
+
11
+ subcommand_option_handling :normal
12
+ arguments :strict
13
+ sort_help :manually
14
+ wrap_help_text :tty_only
15
+
16
+ desc 'Domain name (FQDN) of the IMAP server'
17
+ default_value ENV.fetch('IMAP_SERVER', nil)
18
+ arg_name 'imap.example.com'
19
+ flag %i[s server]
20
+
21
+ desc 'Log-in name (username/email)'
22
+ default_value ENV.fetch('IMAP_USER', nil)
23
+ arg_name 'user'
24
+ flag %i[u user]
25
+
26
+ desc 'Log-in password'
27
+ # default_value ENV['IMAP_PASS']
28
+ arg_name 'password'
29
+ flag %i[p password]
30
+
31
+ desc 'Prompt for password'
32
+ switch %i[P prompt], negatable: false
33
+
34
+ desc 'Verbose output (e.g., response values from Rubys Net::IMAP)'
35
+ switch %i[v verbose], negatable: false
36
+
37
+ desc 'Tests if the server is available and log-in succeeds with the credentials'
38
+ command :check do |c|
39
+ c.action do |_global_options, _options, _args|
40
+ @command.check ? @prompt.ok('login successful') : @prompt.error('login failed')
41
+ end
42
+ end
43
+
44
+ desc 'Prints information about the server'
45
+ command :info do |c|
46
+ c.action do |_global_options, _options, _args|
47
+ @command.info.each { |line| @prompt.say line }
48
+ end
49
+ end
50
+
51
+ desc 'Lists mailboxes (folders)'
52
+ command :list do |c|
53
+ c.action do |_global_options, _options, _args|
54
+ @command.list.each { |line| @prompt.say line }
55
+ end
56
+ end
57
+
58
+ desc 'Collects mailbox statistics'
59
+ arg_name :mailbox, optional: true, multiple: true
60
+ command :stats do |c| # rubocop:disable Metrics/BlockLength
61
+ c.switch %i[r recurse],
62
+ desc: 'Recurse into sub mailboxes',
63
+ negatable: false
64
+ c.switch %i[R no_recurse],
65
+ desc: 'Do not recurse into sub mailboxes',
66
+ negatable: false
67
+ c.switch %i[H human],
68
+ desc: 'Convert byte counts to human-friendly formats',
69
+ negatable: false
70
+ c.flag %i[o sort],
71
+ desc: 'Ordered (sorted) results',
72
+ arg_name: 'sort_property',
73
+ must_match: %w[count total_size median_size min_size q1_size q3_size max_size],
74
+ default: 'total_size'
75
+ c.switch %i[reverse], desc: 'Reverse sort order (largest first)', negatable: false
76
+ c.switch [:csv], desc: 'Output comma-separated values (CSV)'
77
+
78
+ c.action do |_global_options, options, args| # rubocop:disable Metrics/BlockLength
79
+ raise unless @validator.stats_options_valid?(options, args)
80
+
81
+ progress_bar = nil
82
+
83
+ head = ['Mailbox', 'Count', 'Total size', 'Min', 'Q1', 'Median', 'Q3', 'Max']
84
+ body = @command.stats(args, options) do |n|
85
+ if progress_bar
86
+ progress_bar.advance
87
+ else
88
+ @prompt.say "info: collecting stats for #{n} folders" if n > 1
89
+ progress_bar = TTY::ProgressBar.new(
90
+ 'collecting stats... :current/:total (:percent, :eta remaining)',
91
+ total: n, clear: true
92
+ )
93
+ end
94
+ end
95
+ formatted_body = body.map do |row|
96
+ row[0..1] + row[2..].map { |cell| format_bytes(cell, options[:human]) }
97
+ end
98
+
99
+ if options[:csv]
100
+ unless options[:human]
101
+ @prompt.warn 'notice: BREAKING CHANGE IN VERSION 2: messages sizes in CSV output are now given in bytes, not kiB'
102
+ end
103
+ @prompt.say head.to_csv
104
+ last_mailbox_line = body.length == 1 ? -1 : -2 # skip grand total if present
105
+ formatted_body[0..last_mailbox_line].each { |row| @prompt.say row.to_csv }
106
+ else
107
+ formatted_body = formatted_body.insert(0, :separator).insert(-2, :separator)
108
+
109
+ if options[:human]
110
+ @prompt.say "notice: -H/--human flag present, message sizes are given with SI prefixes"
111
+ else
112
+ @prompt.say "notice: message sizes are given in bytes"
113
+ end
114
+
115
+ table = TTY::Table.new(head, formatted_body)
116
+ rendered_table = table.render(:unicode) do |renderer|
117
+ renderer.alignments = [:left] + Array.new(7, :right)
118
+ renderer.border.style = :blue
119
+ end
120
+ @prompt.say rendered_table
121
+
122
+ # If any unknown mailboxes were requested, print an informative footer
123
+ if body.any? { |line| line[0].start_with? Imapcli::Command.unknown_mailbox_prefix }
124
+ @prompt.warn "#{Imapcli::Command.unknown_mailbox_prefix}unknown mailbox"
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def self.format_bytes(bytes, human = false)
131
+ human ? ActiveSupport::NumberHelper.number_to_human_size(bytes) : bytes
132
+ end
133
+
134
+ pre do |global, _command, _options, _args|
135
+ @prompt = TTY::Prompt.new
136
+ @validator = Imapcli::OptionValidator.new
137
+ raise unless @validator.global_options_valid?(global)
138
+
139
+ global[:p] = @prompt.mask 'Enter password:' if global[:P]
140
+ global[:p] ||= ENV.fetch('IMAP_PASS', nil)
141
+
142
+ client = Imapcli::Client.new(global[:s], global[:u], global[:p])
143
+ @prompt.say "server: #{global[:s]}"
144
+ @prompt.say "user: #{global[:u]}"
145
+ raise 'invalid server name' unless client.server_valid?
146
+ raise 'invalid user name' unless client.user_valid?
147
+
148
+ @prompt.warn 'warning: no password was provided (missing -p/-P option)' unless global[:p]
149
+ raise 'unable to connect to server' unless client.connection
150
+
151
+ @command = Imapcli::Command.new(client)
152
+
153
+ true
154
+ end
155
+
156
+ post do |global, _command, _options, _args|
157
+ @client&.logout
158
+ if global[:v]
159
+ @prompt.say "\n>>> --verbose switch on, listing server responses <<<"
160
+ @client.responses.each do |response|
161
+ @prompt.say response
162
+ end
163
+ end
164
+ end
165
+
166
+ on_error do |exception|
167
+ @client&.logout
168
+ if @validator&.errors&.any?
169
+ @validator.errors.each { |error| @prompt.error error }
170
+ else
171
+ @prompt&.error "error: #{exception}"
172
+ end
173
+ @prompt.nil? # if we do not have a prompt yet, let GLI handle the exception
174
+ end
175
+
176
+ def print_warnings
177
+ @validator.warnings.each { |warning| @prompt.warn warning }
178
+ end
179
+ end
180
+ end