cliffy 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: []