docheck 1.1.2 → 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/exe/docheck ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/docheck/cli"
5
+
6
+ exit Docheck::CLI.new(ARGV).run
@@ -1,76 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Docheck
4
+ # The 1.x API, kept for compatibility. New code should use Checker.
2
5
  class Application
3
-
4
- # DNS name of the generic top-level domain
5
- # http://en.wikipedia.org/wiki/List_of_Internet_top-level_domains
6
- TLD = %w[aero asia biz cat com coop edu gov info
7
- int jobs mil mobi museum name net org pro tel travel]
8
-
9
6
  attr_reader :available_domains, :registered_domains
10
7
 
11
- # base_name is sld (Second Level Domain) and
12
- # converted to lower case.
13
- def initialize(base_name)
14
- @base_name = base_name.to_s.downcase
8
+ def initialize(base_name, checker: Checker.new)
9
+ @base_name = base_name
10
+ @checker = checker
15
11
  @available_domains = []
16
12
  @registered_domains = []
17
13
  end
18
14
 
19
- # Print the fetch result.
20
- def print
21
- result = fetch
22
-
23
- puts "\nDocheck Result\n"
24
- puts "--------------\n"
25
- puts "Available Domains:"
26
- result.first.map{|domain_name| puts "- #{domain_name}"}
27
- puts "\n"
28
- puts "Registered Domains:"
29
- result.last.map{|domain_name| puts "- #{domain_name}"}
30
-
31
- true
32
- end
33
-
34
- # Check domain name availability
35
- # and return the result as array.
36
- #
37
- # The first element of array will return all
38
- # available domains as array.
39
- #
40
- # The last element of array will return all
41
- # registered domains as array.
15
+ # Returns [available_domains, registered_domains]. Each call replaces the
16
+ # previous result. Raises InvalidNameError for an invalid name.
42
17
  def fetch
43
- attempts = 0
44
- TLD.each do |tld|
45
- domain_name = "#{@base_name}.#{tld}"
46
- begin
47
- whois_domain = Whois.whois(domain_name)
48
-
49
- if whois_domain.available?
50
- @available_domains << domain_name
51
- elsif whois_domain.registered?
52
- @registered_domains << domain_name
53
- end
54
- rescue Timeout::Error
55
- if attempts <= 3
56
- attempts += 1
57
- retry
58
- else
59
- attempts = 0
60
- next
61
- end
62
- rescue
63
- if attempts <= 3
64
- attempts += 1
65
- retry
66
- else
67
- attempts = 0
68
- next
69
- end
70
- end
71
- end
18
+ results = @checker.check(@base_name)
19
+ @available_domains = results.select(&:available?).map(&:domain)
20
+ @registered_domains = results.select(&:registered?).map(&:domain)
21
+ [@available_domains, @registered_domains]
22
+ end
72
23
 
73
- return [@available_domains, @registered_domains]
24
+ # Prints the fetch result to +out+ and returns true.
25
+ def print(out = $stdout) # rubocop:disable Naming/PredicateMethod -- 1.x public API name
26
+ available, registered = fetch
27
+ out.puts "Available Domains:"
28
+ available.each { |domain| out.puts "- #{domain}" }
29
+ out.puts
30
+ out.puts "Registered Domains:"
31
+ registered.each { |domain| out.puts "- #{domain}" }
32
+ true
74
33
  end
75
34
  end
76
35
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Docheck
6
+ # Maps each TLD to its RDAP base URL, using the IANA bootstrap file format
7
+ # (RFC 9224). The gem bundles a snapshot in data/rdap_dns.json so that a
8
+ # lookup needs no extra network call. Refresh it with `rake rdap:update`.
9
+ class Bootstrap
10
+ DEFAULT_PATH = File.expand_path("../../data/rdap_dns.json", __dir__)
11
+
12
+ def self.default
13
+ @default ||= load_file(DEFAULT_PATH)
14
+ end
15
+
16
+ def self.load_file(path)
17
+ parse(File.read(path))
18
+ end
19
+
20
+ def self.parse(json)
21
+ services = JSON.parse(json).fetch("services")
22
+ new(services.each_with_object({}) do |(tlds, urls), map|
23
+ url = urls.find { |u| u.start_with?("https://") }
24
+ tlds.each { |tld| map[tld.downcase] = url } if url
25
+ end)
26
+ rescue JSON::ParserError, KeyError, TypeError, NoMethodError => e
27
+ raise BootstrapError, "invalid RDAP bootstrap data: #{e.message}"
28
+ end
29
+
30
+ def initialize(base_urls)
31
+ @base_urls = base_urls.to_h do |tld, url|
32
+ raise BootstrapError, "RDAP base URL for #{tld} must use https: #{url}" unless url.start_with?("https://")
33
+
34
+ [tld.downcase.freeze, (url.end_with?("/") ? url : "#{url}/").freeze]
35
+ end.freeze
36
+ freeze
37
+ end
38
+
39
+ # Returns the RDAP base URL for +tld+ (ending in "/"), or nil.
40
+ def base_url(tld)
41
+ @base_urls[tld.to_s.downcase]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docheck
4
+ # Checks a name against a list of TLDs, one RDAP lookup per TLD in parallel.
5
+ class Checker
6
+ # Generic TLDs open to public registration.
7
+ DEFAULT_TLDS = %w[com net org info biz name pro mobi asia tel travel jobs aero cat coop museum].freeze
8
+
9
+ # Upper bound in seconds for a whole #check call.
10
+ DEADLINE = 10
11
+
12
+ attr_reader :tlds
13
+
14
+ def initialize(tlds: DEFAULT_TLDS, bootstrap: Bootstrap.default, http: HttpClient.new, deadline: DEADLINE)
15
+ @tlds = tlds.map { |tld| tld.to_s.downcase.delete_prefix(".").freeze }.freeze
16
+ @bootstrap = bootstrap
17
+ @http = http
18
+ @deadline = deadline
19
+ freeze
20
+ end
21
+
22
+ # Returns one Result per TLD, in TLD order. Raises InvalidNameError when
23
+ # +name+ is not a valid DNS label. Lookups still running at the deadline
24
+ # get +:unknown+.
25
+ def check(name)
26
+ label = Name.normalize(name)
27
+ threads = tlds.map { |tld| Thread.new { lookup("#{label}.#{tld}", tld) } }
28
+ collect(threads, tlds.map { |tld| "#{label}.#{tld}" })
29
+ end
30
+
31
+ private
32
+
33
+ def lookup(domain, tld)
34
+ base_url = @bootstrap.base_url(tld)
35
+ return :unsupported unless base_url
36
+
37
+ @http.status("#{base_url}domain/#{domain}")
38
+ rescue StandardError
39
+ :unknown
40
+ end
41
+
42
+ def collect(threads, domains)
43
+ ends_at = now + @deadline
44
+ threads.zip(domains).map do |thread, domain|
45
+ finished = thread.join([ends_at - now, 0].max)
46
+ thread.kill unless finished
47
+ Result.new(domain: domain, status: finished ? thread.value : :unknown)
48
+ end
49
+ end
50
+
51
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
+ end
53
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../docheck"
5
+
6
+ module Docheck
7
+ # The docheck command. #run returns the process exit status.
8
+ class CLI
9
+ EX_OK = 0
10
+ EX_USAGE = 64
11
+
12
+ LABELS = {
13
+ available: "Available",
14
+ registered: "Registered",
15
+ unknown: "Unknown (lookup failed)",
16
+ unsupported: "Unsupported (no RDAP server)"
17
+ }.freeze
18
+
19
+ def initialize(argv, out: $stdout, err: $stderr, checker: nil)
20
+ @argv = argv.dup
21
+ @out = out
22
+ @err = err
23
+ @checker = checker
24
+ @done = false
25
+ end
26
+
27
+ def run
28
+ name = nil
29
+ parser = option_parser { |value| name = value }
30
+ parser.parse!(@argv)
31
+ return EX_OK if @done
32
+ return usage_error(parser, "missing --name") unless name
33
+
34
+ print_results((@checker || Checker.new).check(name))
35
+ EX_OK
36
+ rescue OptionParser::ParseError, InvalidNameError => e
37
+ usage_error(parser, e.message)
38
+ end
39
+
40
+ private
41
+
42
+ def option_parser(&on_name)
43
+ OptionParser.new do |opts|
44
+ opts.banner = "Usage: docheck -n NAME (example: docheck -n google)"
45
+ opts.on("-n", "--name NAME", "Check NAME against each TLD", &on_name)
46
+ opts.on("-v", "--version", "Show version") { finish("docheck #{VERSION}") }
47
+ opts.on("-h", "--help", "Show help") { finish(opts.help) }
48
+ end
49
+ end
50
+
51
+ def finish(text)
52
+ @out.puts text
53
+ @done = true
54
+ end
55
+
56
+ def usage_error(parser, message)
57
+ @err.puts "docheck: #{message}"
58
+ @err.puts parser.help
59
+ EX_USAGE
60
+ end
61
+
62
+ def print_results(results)
63
+ results.group_by(&:status).sort_by { |status, _| LABELS.keys.index(status) }.each do |status, group|
64
+ @out.puts "#{LABELS.fetch(status)}:"
65
+ group.each { |result| @out.puts "- #{result.domain}" }
66
+ @out.puts
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docheck
4
+ # Base class for all Docheck errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a name is not a valid DNS label.
8
+ class InvalidNameError < Error; end
9
+
10
+ # Raised when the RDAP bootstrap data is malformed.
11
+ class BootstrapError < Error; end
12
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "uri"
6
+
7
+ module Docheck
8
+ # Queries one RDAP domain URL and maps the HTTP response to a status.
9
+ #
10
+ # It sends HEAD, not GET (RFC 7480 section 4.1). The status code is all it
11
+ # needs, and some registries (Verisign) end a GET body by closing TLS without
12
+ # close_notify, which OpenSSL 3 reports as an error.
13
+ class HttpClient
14
+ TIMEOUT = 3
15
+ MAX_REDIRECTS = 1
16
+ RETRIES = 1
17
+
18
+ # +requester+ takes a URI and returns a Net::HTTPResponse. Tests pass a
19
+ # fake one; the default performs a real HTTPS HEAD request.
20
+ def initialize(timeout: TIMEOUT, requester: nil)
21
+ @timeout = timeout
22
+ @requester = requester || method(:head)
23
+ end
24
+
25
+ # Returns +:registered+, +:available+, or +:unknown+ for +url+.
26
+ def status(url)
27
+ fetch_status(URI(url), MAX_REDIRECTS, RETRIES)
28
+ rescue URI::Error
29
+ :unknown
30
+ end
31
+
32
+ private
33
+
34
+ NETWORK_ERRORS = [IOError, SystemCallError, SocketError, OpenSSL::SSL::SSLError, Net::ProtocolError].freeze
35
+ private_constant :NETWORK_ERRORS
36
+
37
+ def fetch_status(uri, redirects_left, retries_left)
38
+ return :unknown unless uri.is_a?(URI::HTTPS)
39
+
40
+ response = @requester.call(uri)
41
+ case response
42
+ when Net::HTTPOK then :registered
43
+ when Net::HTTPNotFound then :available
44
+ when Net::HTTPRedirection then follow(uri, response["location"], redirects_left, retries_left)
45
+ when Net::HTTPServerError then retry_once(uri, redirects_left, retries_left)
46
+ else :unknown
47
+ end
48
+ rescue Timeout::Error
49
+ retry_once(uri, redirects_left, retries_left)
50
+ rescue *NETWORK_ERRORS
51
+ :unknown
52
+ end
53
+
54
+ def retry_once(uri, redirects_left, retries_left)
55
+ return :unknown unless retries_left.positive?
56
+
57
+ fetch_status(uri, redirects_left, retries_left - 1)
58
+ end
59
+
60
+ def follow(uri, location, redirects_left, retries_left)
61
+ return :unknown if redirects_left.zero? || location.nil?
62
+
63
+ fetch_status(uri.merge(location), redirects_left - 1, retries_left)
64
+ end
65
+
66
+ def head(uri)
67
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true,
68
+ open_timeout: @timeout, read_timeout: @timeout,
69
+ ssl_timeout: @timeout) do |http|
70
+ request = Net::HTTP::Head.new(uri)
71
+ request["Accept"] = "application/rdap+json"
72
+ request["User-Agent"] = "docheck/#{VERSION} (+https://github.com/kuntoaji/docheck)"
73
+ http.request(request)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docheck
4
+ # Validates the second-level name that Docheck appends each TLD to.
5
+ module Name
6
+ # One LDH (letter, digit, hyphen) label of 1 to 63 characters that does
7
+ # not start or end with a hyphen. IDNs must be passed in punycode (xn--).
8
+ LABEL = /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/
9
+
10
+ # Returns +name+ stripped and downcased, or raises InvalidNameError.
11
+ def self.normalize(name)
12
+ label = name.to_s.strip.downcase
13
+ return label if LABEL.match?(label)
14
+
15
+ raise InvalidNameError,
16
+ "#{name.to_s.inspect} is not a valid domain name label " \
17
+ "(1-63 letters, digits, or hyphens, no leading or trailing hyphen)"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docheck
4
+ # The availability of one domain name.
5
+ #
6
+ # +status+ is one of:
7
+ # - +:available+: the registry has no record of the domain
8
+ # - +:registered+: the registry has a record of the domain
9
+ # - +:unknown+: the lookup failed (timeout, rate limit, or server error)
10
+ # - +:unsupported+: the TLD has no RDAP server in the bootstrap data
11
+ Result = Data.define(:domain, :status) do
12
+ def initialize(domain:, status:)
13
+ raise ArgumentError, "unknown status: #{status.inspect}" unless Result::STATUSES.include?(status)
14
+
15
+ super
16
+ end
17
+
18
+ def available? = status == :available
19
+ def registered? = status == :registered
20
+ end
21
+
22
+ Result::STATUSES = %i[available registered unknown unsupported].freeze
23
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Docheck
2
- VERSION = '1.1.2'
4
+ VERSION = "2.0.0"
3
5
  end
data/lib/docheck.rb CHANGED
@@ -1,6 +1,18 @@
1
- libdir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
1
+ # frozen_string_literal: true
3
2
 
4
- require 'whois'
5
- require 'docheck/version'
6
- require 'docheck/application'
3
+ require_relative "docheck/version"
4
+ require_relative "docheck/errors"
5
+ require_relative "docheck/name"
6
+ require_relative "docheck/result"
7
+ require_relative "docheck/bootstrap"
8
+ require_relative "docheck/http_client"
9
+ require_relative "docheck/checker"
10
+ require_relative "docheck/application"
11
+
12
+ # Domain name availability checker built on RDAP.
13
+ module Docheck
14
+ # Checks +name+ against the default TLDs. See Checker#check.
15
+ def self.check(name)
16
+ Checker.new.check(name)
17
+ end
18
+ end
metadata CHANGED
@@ -1,70 +1,47 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docheck
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kunto Aji Kristianto
8
- autorequire:
9
- bindir: bin
8
+ bindir: exe
10
9
  cert_chain: []
11
- date: 2019-10-03 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: whois
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '='
18
- - !ruby/object:Gem::Version
19
- version: 3.6.5
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - '='
25
- - !ruby/object:Gem::Version
26
- version: 3.6.5
27
- - !ruby/object:Gem::Dependency
28
- name: shoulda
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 3.6.0
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 3.6.0
41
- description: " Domain name availability checker.\n"
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Checks whether a domain name is available or registered across generic
13
+ TLDs, using RDAP.
42
14
  email: kuntoaji@kaklabs.com
43
15
  executables:
44
16
  - docheck
45
17
  extensions: []
46
18
  extra_rdoc_files:
47
- - README.rdoc
48
19
  - LICENSE
20
+ - README.rdoc
49
21
  files:
50
- - ".gitignore"
51
- - Gemfile
52
- - Gemfile.lock
22
+ - CHANGELOG.md
53
23
  - LICENSE
54
24
  - README.rdoc
55
- - Rakefile
56
- - bin/docheck
57
- - docheck.gemspec
25
+ - data/rdap_dns.json
26
+ - exe/docheck
58
27
  - lib/docheck.rb
59
28
  - lib/docheck/application.rb
60
- - lib/docheck/command.rb
29
+ - lib/docheck/bootstrap.rb
30
+ - lib/docheck/checker.rb
31
+ - lib/docheck/cli.rb
32
+ - lib/docheck/errors.rb
33
+ - lib/docheck/http_client.rb
34
+ - lib/docheck/name.rb
35
+ - lib/docheck/result.rb
61
36
  - lib/docheck/version.rb
62
- - test/docheck_test.rb
63
- - test/helper.rb
64
- homepage: http://github.com/kuntoaji/docheck
65
- licenses: []
66
- metadata: {}
67
- post_install_message:
37
+ homepage: https://github.com/kuntoaji/docheck
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ source_code_uri: https://github.com/kuntoaji/docheck
42
+ changelog_uri: https://github.com/kuntoaji/docheck/blob/master/CHANGELOG.md
43
+ bug_tracker_uri: https://github.com/kuntoaji/docheck/issues
44
+ rubygems_mfa_required: 'true'
68
45
  rdoc_options:
69
46
  - "--main"
70
47
  - README.rdoc
@@ -74,15 +51,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
51
  requirements:
75
52
  - - ">="
76
53
  - !ruby/object:Gem::Version
77
- version: 1.9.2
54
+ version: '3.2'
78
55
  required_rubygems_version: !ruby/object:Gem::Requirement
79
56
  requirements:
80
57
  - - ">="
81
58
  - !ruby/object:Gem::Version
82
59
  version: '0'
83
60
  requirements: []
84
- rubygems_version: 3.0.3
85
- signing_key:
61
+ rubygems_version: 4.0.20
86
62
  specification_version: 4
87
63
  summary: Domain name availability checker
88
64
  test_files: []
data/.gitignore DELETED
@@ -1,3 +0,0 @@
1
- *~
2
- *.swp
3
- *.swo
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- gemspec
data/Gemfile.lock DELETED
@@ -1,39 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- docheck (1.1.2)
5
- whois (= 3.6.5)
6
-
7
- GEM
8
- remote: http://rubygems.org/
9
- specs:
10
- activesupport (5.2.3)
11
- concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- concurrent-ruby (1.1.5)
16
- i18n (1.6.0)
17
- concurrent-ruby (~> 1.0)
18
- minitest (5.11.3)
19
- shoulda (3.6.0)
20
- shoulda-context (~> 1.0, >= 1.0.1)
21
- shoulda-matchers (~> 3.0)
22
- shoulda-context (1.2.2)
23
- shoulda-matchers (3.1.3)
24
- activesupport (>= 4.0.0)
25
- thread_safe (0.3.6)
26
- tzinfo (1.2.5)
27
- thread_safe (~> 0.1)
28
- whois (3.6.5)
29
- activesupport (>= 3)
30
-
31
- PLATFORMS
32
- ruby
33
-
34
- DEPENDENCIES
35
- docheck!
36
- shoulda (~> 3.6.0)
37
-
38
- BUNDLED WITH
39
- 1.17.2
data/Rakefile DELETED
@@ -1,2 +0,0 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
data/bin/docheck DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require File.expand_path('../../lib/docheck/command.rb', __FILE__)
data/docheck.gemspec DELETED
@@ -1,28 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "docheck/version"
4
-
5
- Gem::Specification.new do |s|
6
- s.name = "docheck"
7
- s.version = Docheck::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = "Kunto Aji Kristianto"
10
- s.email = 'kuntoaji@kaklabs.com'
11
- s.homepage = "http://github.com/kuntoaji/docheck"
12
- s.summary = 'Domain name availability checker'
13
- s.description = <<-EOF
14
- Domain name availability checker.
15
- EOF
16
-
17
- s.files = `git ls-files`.split("\n")
18
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
- s.require_paths = ["lib"]
21
-
22
- s.rdoc_options = ['--main', 'README.rdoc']
23
- s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
24
-
25
- s.required_ruby_version = '>= 1.9.2'
26
- s.add_dependency('whois', '3.6.5')
27
- s.add_development_dependency('shoulda', '~> 3.6.0')
28
- end
@@ -1,31 +0,0 @@
1
- begin
2
- require File.expand_path('../../docheck.rb', __FILE__)
3
- rescue LoadError
4
- require 'rubygems'
5
- require File.expand_path('../../docheck.rb', __FILE__)
6
- end
7
-
8
- require 'optparse'
9
-
10
- begin
11
- OptionParser.new do |opts|
12
- opts.banner = "Usage: docheck -n [basename]. Example: docheck -n google"
13
-
14
- opts.on("-v", "--version", "Show version") do
15
- puts "Docheck #{Docheck::VERSION}"
16
- end
17
-
18
- opts.on("-n", "--name NAME", "Execute domain name checker") do |name|
19
- Docheck::Application.new(name).print
20
- end
21
-
22
- opts.on_tail("-h", "--help", "Show help") do
23
- puts opts
24
- exit
25
- end
26
- end.parse!
27
- rescue OptionParser::InvalidOption
28
- puts "Invalid option"
29
- rescue OptionParser::MissingArgument
30
- puts "Missing argument"
31
- end