cliffy 0.6.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0d9c2a9ac15921ff581a227eaa98bdbe1ac7285841f300dae47cb9e729bdd2f2
4
+ data.tar.gz: bf08815d09bae712f08f99365f5f9b2608ffe73fd9696fcadaa127c2856206bd
5
+ SHA512:
6
+ metadata.gz: db28a47f16402d3cdf1a846d8077f72228964043936ef2684a3d6f8fd89f7c051fc13a682c8d08032fe1490bf75bc0e52616661efedba25f30a4e57bc2189c5a
7
+ data.tar.gz: 57e0dfde41e33c0e7cee5b61824e65562341fb12500db7aa7f3c8f525194264100ee37d59e2621e20f2135bf559826d9de6c0a20060cc6551c9e1d4e7a72693b
data/lib/cliffy/api.rb ADDED
@@ -0,0 +1,135 @@
1
+ require 'set'
2
+ require_relative 'internal/wrapper'
3
+
4
+ module Cliffy
5
+ def self.run *commands, arguments: ARGV, help_points: []
6
+ service = Service.new commands, arguments, help_points, method(:abort), method(:puts)
7
+ service.run
8
+ end
9
+
10
+ class Service
11
+ def initialize commands, arguments, help_points, abort_method, puts_method
12
+ @commands = commands
13
+ @arguments = arguments
14
+ @help_points = help_points
15
+ @abort_method = abort_method
16
+ @puts_method = puts_method
17
+ end
18
+
19
+ def run
20
+ begin
21
+ validate_commands
22
+ if @arguments.empty?
23
+ show_usage
24
+ elsif @arguments.first == 'help'
25
+ show_help
26
+ else
27
+ run_command
28
+ end
29
+ rescue => e
30
+ @abort_method.call e.to_s
31
+ end
32
+ nil
33
+ end
34
+
35
+ private
36
+
37
+ # Run Methods
38
+
39
+ def validate_commands
40
+ command_names = Set.new
41
+ wrappers.each do |wrapper|
42
+ command_name = wrapper.command_name
43
+ if command_names.include? command_name
44
+ raise "Found more than one command called '#{command_name}'."
45
+ end
46
+ command_names << command_name
47
+ wrapper.validate
48
+ end
49
+ end
50
+
51
+ def show_usage
52
+ help_content = "Run `#{executable_name} help <command>` for help with a specific command."
53
+ help_content = [help_content] + @help_points unless @help_points.empty?
54
+ show_titles_and_contents(
55
+ 'Usage' => "#{executable_name} <command> [arguments]",
56
+ 'Commands' => wrappers.to_h { |wrapper| [wrapper.command_name, wrapper.command_description] },
57
+ 'Help' => help_content
58
+ )
59
+ end
60
+
61
+ def show_help
62
+ if @arguments.count == 2
63
+ wrapper = wrappers.find { |wrapper| wrapper.command_name == @arguments[1] }
64
+ if wrapper
65
+ help_data = wrapper.generate_help_data executable_name
66
+ show_titles_and_contents help_data
67
+ return
68
+ end
69
+ end
70
+ raise invalid_arguments_message
71
+ end
72
+
73
+ def run_command
74
+ wrapper = wrappers.find { |wrapper| wrapper.command_name == @arguments.first }
75
+ if wrapper
76
+ wrapper.run @arguments[1...], executable_name
77
+ else
78
+ raise invalid_arguments_message
79
+ end
80
+ end
81
+
82
+ # Helpers
83
+
84
+ def wrappers
85
+ unless @wrappers
86
+ @wrappers = @commands.map { |command| Internal::Wrapper.new command }
87
+ end
88
+ @wrappers
89
+ end
90
+
91
+ def executable_name
92
+ @executable_name ||= File.basename $PROGRAM_NAME
93
+ end
94
+
95
+ def invalid_arguments_message
96
+ @invalid_arguments_message ||= "Invalid arguments. Run `#{executable_name}` for help."
97
+ end
98
+
99
+ def show_titles_and_contents titles_and_contents
100
+ key_padding = 0
101
+ titles_and_contents.each do |title, content|
102
+ next unless content.is_a? Hash
103
+ next unless content.count > 0
104
+ longest_key = content.keys.map(&:length).max
105
+ key_padding = longest_key if longest_key > key_padding
106
+ end
107
+ index_format = " %-#{key_padding}s %s"
108
+ lines = []
109
+ titles_and_contents.each do |title, content|
110
+ case content
111
+ when String then
112
+ next if content.length < 1
113
+ lines << "#{title}:"
114
+ lines << ' ' + content
115
+ when Array then
116
+ next if content.count < 1
117
+ lines << "#{title}:"
118
+ content.each do |line|
119
+ lines << ' - ' + line
120
+ end
121
+ when Hash then
122
+ next if content.count < 1
123
+ lines << "#{title}:"
124
+ content.each do |key, value|
125
+ line = sprintf index_format, key, value
126
+ lines << line
127
+ end
128
+ end
129
+ lines << ''
130
+ end
131
+ message = lines.join $/
132
+ @puts_method.call message
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,53 @@
1
+ module Cliffy
2
+ module Internal
3
+ def self.generate_help_data command, command_name, executable_name
4
+ usage_tokens = [executable_name, command_name]
5
+ parameter_names_and_descriptions = {}
6
+ options_and_descriptions = {}
7
+
8
+ command.method(:run).parameters.each do |parameter|
9
+ symbol = parameter[1]
10
+ description = command.signature[symbol][:description]
11
+ symbol_name = parameter[1].to_s.gsub '_', '-'
12
+ case parameter.first
13
+ when :req
14
+ usage_tokens << "<#{symbol_name}>"
15
+ parameter_names_and_descriptions[symbol_name] = description
16
+ when :rest
17
+ usage_tokens << "<#{symbol_name}...>"
18
+ parameter_names_and_descriptions[symbol_name] = description
19
+ when :key
20
+ option = "--#{symbol_name}"
21
+ type = command.signature[symbol][:type]
22
+ case type
23
+ when :boolean
24
+ usage_tokens << "[#{option}]"
25
+ when :integer, :float, :string
26
+ usage_tokens << "[#{option} value]"
27
+ when Hash
28
+ sub_symbol_names = type.keys.map { |sub_symbol| sub_symbol.to_s.gsub '_', '-' }.join ' '
29
+ usage_tokens << "[#{option} #{sub_symbol_names}]"
30
+ end
31
+
32
+ options_and_descriptions[option] = description
33
+ end
34
+ end
35
+
36
+ usage = usage_tokens.join ' '
37
+ titles_and_contents = {
38
+ 'Description' => command.description,
39
+ 'Usage' => usage
40
+ }
41
+ unless parameter_names_and_descriptions.empty?
42
+ titles_and_contents.merge! 'Parameters' => parameter_names_and_descriptions
43
+ end
44
+ unless options_and_descriptions.empty?
45
+ titles_and_contents.merge! 'Options' => options_and_descriptions
46
+ end
47
+ if command.respond_to? :notes
48
+ titles_and_contents.merge! 'Notes' => command.notes
49
+ end
50
+ titles_and_contents
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,155 @@
1
+ require 'set'
2
+
3
+ module Cliffy
4
+ module Internal
5
+ def self.run command, arguments, command_name, executable_name
6
+ unless command.respond_to? :signature
7
+ command.run
8
+ return
9
+ end
10
+
11
+ true_values = ['yes', 'true', '1']
12
+ false_values = ['no', 'false', '0']
13
+ invalid_arguments_message = "Invalid arguments. Run `#{executable_name} help #{command_name}` for help."
14
+ run_method = command.method :run
15
+ signature = command.signature
16
+ remaining_arguments = arguments.dup
17
+ keyword_parameters = {}
18
+
19
+ optional_symbols_and_data = signature.filter { |symbol, data| data[:kind] == :optional }
20
+ unless optional_symbols_and_data.empty?
21
+ options_and_symbols = optional_symbols_and_data.to_h do |symbol, data|
22
+ option_name = symbol.to_s.gsub '_', '-'
23
+ option = "--#{option_name}"
24
+ [option, symbol]
25
+ end
26
+ remaining_options = options_and_symbols.keys.to_set
27
+ options = optional_symbols_and_data.keys.map { |key| "--#{key.to_s.gsub '_', '-'}" }.to_set
28
+ queued_arguments = []
29
+ while ! remaining_arguments.empty?
30
+ argument = remaining_arguments.pop
31
+ if argument.start_with?('--') && remaining_options.include?(argument)
32
+ symbol = options_and_symbols[argument]
33
+ data_type = optional_symbols_and_data[symbol][:type]
34
+ value = nil
35
+ case data_type
36
+
37
+ when :boolean
38
+ if queued_arguments.empty?
39
+ value = true
40
+ end
41
+
42
+ when :integer, :float, :string
43
+ if queued_arguments.count == 1
44
+ queued_argument = queued_arguments.first
45
+ case data_type
46
+ when :integer
47
+ value = Integer queued_argument, exception: false
48
+ when :float
49
+ value = Float queued_argument, exception: false
50
+ when :string
51
+ value = queued_argument
52
+ end
53
+ end
54
+
55
+ when Hash
56
+ if queued_arguments.count == data_type.count
57
+ sub_symbols_and_values = {}
58
+ queued_arguments.zip(data_type).each do |queued_argument, sub_type|
59
+ sub_value = nil
60
+ case sub_type[1]
61
+ when :boolean
62
+ sub_value = true if true_values.include? argument
63
+ sub_value = false if false_values.include? argument
64
+ when :integer
65
+ sub_value = Integer queued_argument, exception: false
66
+ when :float
67
+ sub_value = Float queued_argument, exception: false
68
+ when :string
69
+ sub_value = queued_argument
70
+ end
71
+ raise invalid_arguments_message if sub_value == nil
72
+ sub_symbols_and_values[sub_type.first] = sub_value
73
+ end
74
+ value = sub_symbols_and_values
75
+ end
76
+
77
+ end
78
+ raise invalid_arguments_message if value === nil
79
+ keyword_parameters[symbol] = value
80
+ remaining_options.delete argument
81
+ queued_arguments = []
82
+ else
83
+ queued_arguments.unshift argument
84
+ end
85
+ end
86
+ remaining_arguments = queued_arguments
87
+ end
88
+
89
+ strict_parsing = true
90
+ if command.respond_to?(:configuration) && command.configuration.include?(:strict_parsing)
91
+ strict_parsing = command.configuration[:strict_parsing]
92
+ end
93
+ if strict_parsing
94
+ if remaining_arguments.any? { |argument| argument.start_with? '--' }
95
+ raise invalid_arguments_message
96
+ end
97
+ end
98
+
99
+ required_parameters = []
100
+ run_method.parameters.each do |parameter|
101
+ next unless parameter.first == :req
102
+ data = signature[parameter[1]]
103
+ argument = remaining_arguments.shift
104
+ raise invalid_arguments_message if argument == nil
105
+ value = nil
106
+ case data[:type]
107
+ when :boolean
108
+ value = true if true_values.include? argument
109
+ value = false if false_values.include? argument
110
+ when :integer
111
+ value = Integer argument, exception: false
112
+ when :float
113
+ value = Float argument, exception: false
114
+ when :string
115
+ value = argument
116
+ end
117
+ raise invalid_arguments_message if value == nil
118
+ required_parameters << value
119
+ end
120
+
121
+ variadic_parameters = []
122
+ variadic_data = signature.values.find { |data| data[:kind] == :variadic }
123
+ if variadic_data
124
+ data_type = variadic_data[:type]
125
+ while ! remaining_arguments.empty?
126
+ argument = remaining_arguments.shift
127
+ value = nil
128
+ case data_type
129
+ when :boolean
130
+ value = true if true_values.include? argument
131
+ value = false if false_values.include? argument
132
+ when :integer
133
+ value = Integer argument, exception: false
134
+ when :float
135
+ value = Float argument, exception: false
136
+ when :string
137
+ value = argument
138
+ end
139
+ raise invalid_arguments_message if value == nil
140
+ variadic_parameters << value
141
+ end
142
+ variadic_count = variadic_parameters.count
143
+ minimum = variadic_data[:minimum]
144
+ raise invalid_arguments_message if minimum && variadic_parameters.count < minimum
145
+ maximum = variadic_data[:maximum]
146
+ raise invalid_arguments_message if maximum && variadic_parameters.count > maximum
147
+ else
148
+ raise invalid_arguments_message unless remaining_arguments.empty?
149
+ end
150
+
151
+ positional_parameters = required_parameters + variadic_parameters
152
+ command.run *positional_parameters, **keyword_parameters
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,134 @@
1
+ module Cliffy
2
+ module Internal
3
+ def self.validate command
4
+ prefix = "Error validating '#{command.class}' command:"
5
+ raise "#{prefix} No run method." unless command.respond_to? :run
6
+ raise "#{prefix} No description." unless command.respond_to? :description
7
+ description = command.description
8
+ unless description.is_a?(String) && ! description.empty? && description.lines.count == 1
9
+ raise "#{prefix} Invalid description."
10
+ end
11
+
12
+ if command.respond_to? :notes
13
+ notes = command.notes
14
+ raise "#{prefix} Notes is not an array." unless notes.is_a?(Array)
15
+ notes.each_with_index do |note, index|
16
+ unless note.is_a?(String) && ! note.empty? && note.lines.count == 1
17
+ raise "#{prefix} Invalid note at index #{index}."
18
+ end
19
+ end
20
+ end
21
+
22
+ parameters = command.method(:run).parameters
23
+ return if parameters.empty?
24
+
25
+ unless command.respond_to?(:signature) && command.signature.is_a?(Hash)
26
+ raise "#{prefix} Signature missing or not defined correctly."
27
+ end
28
+
29
+ command.signature.each do |symbol, data|
30
+ raise "#{prefix} signature key '#{symbol}' is not a symbol." unless symbol.is_a? Symbol
31
+ data_prefix = "#{prefix} Signature data for '#{symbol}'"
32
+ raise "#{data_prefix} is not a hash." unless data.is_a? Hash
33
+ raise "#{data_prefix} does not have a kind." unless data.include? :kind
34
+ raise "#{data_prefix} does not have a type." unless data.include? :type
35
+ raise "#{data_prefix} does not have a description." unless data.include? :description
36
+
37
+ data_kind = data[:kind]
38
+ data_type = data[:type]
39
+ data_description = data[:description]
40
+
41
+ case data_kind
42
+ when :required, :optional
43
+ valid_data_keys = [:kind, :description, :type]
44
+ when :variadic
45
+ valid_data_keys = [:kind, :description, :type, :minimum, :maximum]
46
+ else
47
+ raise "#{data_prefix} has an invalid kind."
48
+ end
49
+
50
+ unless data.keys.to_set.subset? valid_data_keys.to_set
51
+ raise "#{data_prefix} contains an unrecognized key."
52
+ end
53
+ unless data_description.is_a?(String) && ! data_description.empty? && data_description.lines.count == 1
54
+ raise "#{data_prefix} does not have a valid description."
55
+ end
56
+
57
+ primitives = [:string, :integer, :float, :boolean]
58
+ case data_kind
59
+ when :required, :variadic
60
+ unless primitives.include? data_type
61
+ raise "#{data_prefix} is of kind '#{data_kind}' and must have a valid primitive type."
62
+ end
63
+ when :optional
64
+ case data_type
65
+ when :string, :integer, :float, :boolean
66
+ when Hash
67
+ unless data_type.keys.all? { |key| key.is_a? Symbol }
68
+ raise "#{data_prefix} contains an invalid key in the value hash."
69
+ end
70
+ unless data_type.values.all? { |value| primitives.include? value }
71
+ raise "#{data_prefix} contains an invalid value in the value hash."
72
+ end
73
+ else
74
+ raise "#{data_prefix} is of kind 'optional' and must have a valid primitive or hash type."
75
+ end
76
+ end
77
+
78
+ if data_kind == :variadic
79
+ if data.include? :minimum
80
+ minimum = data[:minimum]
81
+ unless minimum.is_a?(Integer) && minimum >= 0
82
+ raise "#{data_prefix} has an invalid minimum value."
83
+ end
84
+ else
85
+ minimum = 0
86
+ end
87
+ if data.include? :maximum
88
+ maximum = data[:maximum]
89
+ unless maximum.is_a?(Integer) && maximum >= 0 && maximum > minimum
90
+ raise "#{data_prefix} has an invalid maximum value."
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ method_symbols = parameters.map { |parameter| parameter[1] }.to_set
97
+ signature_symbols = command.signature.keys.to_set
98
+ missing_method_symbols = signature_symbols - method_symbols
99
+ missing_signature_symbols = method_symbols - signature_symbols
100
+ unless missing_method_symbols.empty?
101
+ raise "#{prefix} Symbols missing from run method that are in signature: #{missing_method_symbols.join ', '}"
102
+ end
103
+ unless missing_signature_symbols.empty?
104
+ raise "#{prefix} Symbols missing from signature that are in run method: #{missing_signature_symbols.join ', '}"
105
+ end
106
+
107
+ found_req = false
108
+ found_rest = false
109
+ found_key = false
110
+ parameters.each do |parameter|
111
+ parameter_prefix = "#{prefix} '#{parameter[1]}'"
112
+ case parameter.first
113
+ when :req
114
+ kind = :required
115
+ found_req = true
116
+ raise "#{prefix} Required positional after variadic." if found_rest
117
+ raise "#{prefix} Required positional after required keyword." if found_key
118
+ when :rest
119
+ kind = :variadic
120
+ found_rest = true
121
+ raise "#{prefix} Variadic after required keyword." if found_key
122
+ when :key
123
+ kind = :optional
124
+ found_key = true
125
+ else
126
+ raise "#{parameter_prefix} is not a supported kind of method parameter."
127
+ end
128
+ unless command.signature[parameter[1]][:kind] == kind
129
+ raise "#{parameter_prefix} should have a kind of '#{kind}', in the signature."
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'generate_help_data'
2
+ require_relative 'run'
3
+ require_relative 'validate'
4
+
5
+ module Cliffy
6
+ module Internal
7
+ class Wrapper
8
+ def initialize command
9
+ @command = command
10
+ end
11
+
12
+ def command_name
13
+ @command_name ||= @command.class.to_s.split('::').last.gsub(/([A-Z][a-z]+)([A-Z][a-z]+)/, '\1-\2').downcase
14
+ end
15
+
16
+ def command_description
17
+ @command.description
18
+ end
19
+
20
+ def validate
21
+ Internal::validate @command
22
+ end
23
+
24
+ def generate_help_data executable_name
25
+ Internal::generate_help_data @command, command_name, executable_name
26
+ end
27
+
28
+ def run arguments, executable_name
29
+ Internal::run @command, arguments, command_name, executable_name
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Cliffy
2
+ VERSION = '0.6.0'
3
+ end
data/lib/cliffy.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative 'cliffy/version'
2
+ require_relative 'cliffy/api'
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cliffy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Jared O'Connor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Cliffy is a command line interface framework for you.
14
+ email:
15
+ - jaredoconnor@hotmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/cliffy.rb
21
+ - lib/cliffy/api.rb
22
+ - lib/cliffy/internal/generate_help_data.rb
23
+ - lib/cliffy/internal/run.rb
24
+ - lib/cliffy/internal/validate.rb
25
+ - lib/cliffy/internal/wrapper.rb
26
+ - lib/cliffy/version.rb
27
+ homepage: https://github.com/jaredoconnor/cliffy
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://github.com/jaredoconnor/cliffy
32
+ source_code_uri: https://github.com/jaredoconnor/cliffy
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.3.3
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Command Line Interface Framework For You
52
+ test_files: []