rbcli 0.1.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.
@@ -0,0 +1,179 @@
1
+ class Rbcli::Command
2
+
3
+ #include InheritableTraits
4
+ #traits :description
5
+
6
+ @commands = {}
7
+
8
+ def self.inherited subklass
9
+ @commands[subklass.name.downcase] = subklass.new
10
+ end
11
+
12
+ def self.add_command name, klass
13
+ @commands[name.downcase] = klass.new
14
+ end
15
+
16
+ def self.commands
17
+ @commands
18
+ end
19
+
20
+ ##
21
+ # Interface Functions
22
+ ##
23
+ def self.description desc; @desc = desc end
24
+ def description; self.class.instance_variable_get :@desc end
25
+
26
+ def self.usage usage; @usage = usage end
27
+ def usage; self.class.instance_variable_get :@usage end
28
+
29
+ def self.action █ @action = block end
30
+ def action; self.class.instance_variable_get :@action end
31
+
32
+ def self.parameter name, description, type: :boolean, default: nil, required: false
33
+ @paramlist ||= {}
34
+ @paramlist[name.to_sym] = {
35
+ description: description,
36
+ type: type,
37
+ default: default,
38
+ required: required
39
+ }
40
+ end
41
+ def paramlist; self.class.instance_variable_get :@paramlist end
42
+
43
+ def self.config_defaults filename
44
+ Rbcli::Config::add_defaults filename
45
+ end
46
+
47
+ def self.config_default *params
48
+ Rbcli::Config::add_default *params
49
+ end
50
+
51
+ ##
52
+ # END Interface Functions
53
+ ##
54
+
55
+ ##
56
+ # Run a given command
57
+ ##
58
+ def self.runcmd cmd, local_params, cliopts
59
+ args = local_params.delete :args
60
+ params = local_params
61
+ global_opts = cliopts
62
+ config = Rbcli::config
63
+
64
+ @commands[cmd].action.call params, args, global_opts, config
65
+ end
66
+
67
+ ##
68
+ # Returns all descriptions for display in CLI help
69
+ ##
70
+ def self.descriptions indent_size, justification
71
+ #descmap = @commands.map { |name, klass| [name, klass.description] }.to_h
72
+ @commands.map do |name, cmdobj|
73
+ desc = ''
74
+ indent_size.times { desc << ' ' }
75
+ desc << name.ljust(justification)
76
+ desc << cmdobj.description
77
+ end.join("\n")
78
+ end
79
+
80
+ ##
81
+ # This method reads the parameters provided by the class and parses them from the CLI
82
+ ##
83
+ def parseopts *args
84
+ params = paramlist
85
+ command_name = self.class.name.split('::')[-1].downcase
86
+ command_desc = description
87
+ command_usage = usage
88
+ optx = Trollop::options do
89
+ data = Rbcli.configuration
90
+ banner <<-EOS
91
+ #{data[:description]}
92
+ Selected Command:
93
+ #{command_name.ljust(21)}#{command_desc}
94
+
95
+ Usage:
96
+ #{data[:scriptname]} [options] #{command_name} [parameters]
97
+
98
+ #{command_usage}
99
+
100
+ Command-specific Parameters:
101
+ EOS
102
+ params.each do |name, opts|
103
+ opt name, opts[:description], type: opts[:type], default: opts[:default], required: opts[:required]
104
+ end if params.is_a? Hash
105
+ end
106
+ optx[:args] = ARGV
107
+ optx
108
+ end
109
+
110
+ ##
111
+ # Inject metadata into response
112
+ ##
113
+ # def self.wrap_metadata resp
114
+ # {
115
+ # meta: {
116
+ # status: 'ok',
117
+ # timestamp: (Time.now.to_f * 1000).floor
118
+ # },
119
+ # response: resp
120
+ # }.deep_stringify!
121
+ # end
122
+
123
+ ####
124
+ ### DEPRECATED
125
+ ####
126
+ # Now we automatically pull in the plugins and register them as commands.
127
+ # Note that filenames must be the same as the class name and are case
128
+ # sensitive. Only one class per file.
129
+ ##
130
+ # This is commented out as this functionality is deprecated. Instead we rely on subclassing to
131
+ # add the commands.
132
+ ###
133
+ # Dir.glob("#{File.dirname(__FILE__)}/commands/*.rb") do |f|
134
+ # Rbcli::log.debug {"Loading CLI command #{f.split('commands/')[1].split('.')[0]}"}
135
+ # require f
136
+ # klassname = "Rbcli::Command::#{f.match(/.*\/([^\/]+)\.rb$/i)[1].capitalize}"
137
+ # klass = Object.const_get(klassname)
138
+ # klass.send :include, Rbcli::Command
139
+ # self.add_command klassname.split('::')[-1], klass
140
+ # end
141
+
142
+ end
143
+
144
+
145
+ # module InheritableTraits
146
+ #
147
+ # def self.included(base)
148
+ # base.extend ClassMethods
149
+ # end
150
+ #
151
+ # module ClassMethods
152
+ # def traits(*attrs)
153
+ # @traits ||= []
154
+ # @traits += attrs
155
+ # attrs.each do |attr|
156
+ # class_eval %{
157
+ # def self.#{attr}(string = nil)
158
+ # @#{attr} = string || @#{attr}
159
+ # end
160
+ # def self.#{attr}=(string = nil)
161
+ # #{attr}(string)
162
+ # end
163
+ # }
164
+ # end
165
+ # @traits
166
+ # end
167
+ #
168
+ # def inherited(subclass)
169
+ # (["traits"] + traits).each do |t|
170
+ # ivar = "@#{t}"
171
+ # subclass.instance_variable_set(
172
+ # ivar,
173
+ # instance_variable_get(ivar)
174
+ # )
175
+ # end
176
+ # end
177
+ # end
178
+ #
179
+ # end
@@ -0,0 +1,68 @@
1
+ require 'trollop'
2
+
3
+ module Rbcli::Parser
4
+
5
+ @cliopts = nil
6
+
7
+ def self.parse
8
+ @cliopts = Trollop::options do
9
+ data = Rbcli.configuration
10
+ version "#{data[:scriptname]} version: #{data[:version]}"
11
+ banner <<-EOS
12
+ #{data[:description]}
13
+ For more information on individual commands, run `#{data[:scriptname]} <command> -h`.
14
+
15
+ Usage:
16
+ #{data[:scriptname]} [options] command [parameters]
17
+
18
+ Commands:
19
+ #{Rbcli::Command.descriptions 6, 21}
20
+
21
+ [options]:
22
+ EOS
23
+ data[:options].each do |name, opts|
24
+ opt name.to_sym, opts[:description], type: opts[:type], default: opts[:default]
25
+ end
26
+ opt :json_output, 'Output result in machine-friendly JSON format', :type => :boolean, :default => false if data[:allow_json]
27
+ opt :config_file, 'Specify a config file manually', :type => :string, :default => data[:config_userfile]
28
+ opt :generate_config, 'Generate a new config file' #defaults to false
29
+ stop_on Rbcli::Command.commands.keys
30
+ end
31
+
32
+ @cmd = [ARGV.shift] # get the subcommand
33
+ if @cliopts[:generate_config]
34
+ Rbcli::Config::generate_userconf @cliopts[:config_file]
35
+ puts "User config generated at #{@cliopts[:config_file]} using default values."
36
+ elsif @cmd[0].nil?
37
+ if Rbcli.configuration[:default_action].nil?
38
+ Trollop::educate
39
+ else
40
+ Rbcli.configuration[:default_action].call @cliopts
41
+ end
42
+ elsif Rbcli::Command.commands.key? @cmd[0]
43
+ @cmd << Rbcli::Command.commands[@cmd[0]].parseopts
44
+
45
+ Rbcli.configuration[:pre_hook].call @cliopts unless Rbcli.configuration[:pre_hook].nil?
46
+ Rbcli::Command.runcmd(@cmd.shift, @cmd[0], @cliopts)
47
+ Rbcli.configuration[:post_hook].call @cliopts unless Rbcli.configuration[:pre_hook].nil?
48
+ else
49
+ Trollop::die "Unknown subcommand #{@cmd[0].inspect}"
50
+ end
51
+
52
+ end
53
+
54
+ def self.opts
55
+ @cliopts
56
+ end
57
+
58
+ def self.cmd
59
+ @cmd
60
+ end
61
+
62
+ end
63
+
64
+ module Rbcli
65
+ def self.parse
66
+ Rbcli::Parser::parse
67
+ end
68
+ end
@@ -0,0 +1,140 @@
1
+ ####
2
+ # Config Module
3
+ ####
4
+ # The config module manages two distinct sets of config: one is the user's, and one is the application's default.
5
+ # This allows you to set default values for your configuration files, and can use the default values to generate a
6
+ # new user config file at any time by writing them to a file.
7
+ #
8
+ # Usage
9
+ #
10
+ # Rbcli::config[:key]
11
+ # Provides access to config file hash values under :key.
12
+ # Note the lowercase 'c' in config, which is different from the rest of the functions below.
13
+ #
14
+ # Rbcli::Config::set_file(filename, autoload:true, merge_defaults: false)
15
+ # This function allows you to set the path for the user's config file (eg: /etc/myscript/config.yml).
16
+ # autoload will load the file at the same time
17
+ # merge_defaults will fill in missing values in the user's config with default ones
18
+ #
19
+ # Rbcli::Config::add_defaults(filename)
20
+ # This function loads the contents of a YAML or JSON file into the default config. Used for custom configuration.
21
+ #
22
+ # Rbcli::Config::load
23
+ # This forces a reload of the config file.
24
+ #
25
+ # Rbcli::Config::generate_userconf
26
+ # This writes the config defaults to the filename set using the set_file command.
27
+ ####
28
+
29
+ require 'yaml'
30
+ require 'fileutils'
31
+ require 'deep_merge'
32
+
33
+ module Rbcli
34
+ def self.config
35
+ Rbcli::Config::config
36
+ end
37
+ end
38
+
39
+ module Rbcli::Config
40
+
41
+ @config = nil
42
+ @config_file = nil
43
+ @config_defaults = nil
44
+ @merge_defaults = false
45
+ @categorized_defaults = nil
46
+ @loaded = false
47
+
48
+ def self.set_userfile filename, merge_defaults: false, required: false
49
+ @config_file = filename
50
+ @merge_defaults = merge_defaults
51
+ @userfile_required = required
52
+ @loaded = false
53
+ end
54
+
55
+ def self.add_categorized_defaults name, description, config
56
+ @categorized_defaults ||= {}
57
+ @categorized_defaults[name.to_sym] = {
58
+ description: description,
59
+ config: config
60
+ }
61
+
62
+ @config_defaults ||= {}
63
+ @config_defaults[name.to_sym] = {}
64
+ config.each do |k, v|
65
+ @config_defaults[name.to_sym][k.to_sym] = v[:value]
66
+ end
67
+ @loaded = false
68
+ end
69
+
70
+ def self.add_default name, description: nil, value: nil
71
+ @config_individual_lines ||= []
72
+ text = "#{name.to_s}: #{value}".ljust(30) + " # #{description}"
73
+ @config_individual_lines.push text unless @config_individual_lines.include? text
74
+ @config_defaults[name.to_sym] = value
75
+ @loaded = false
76
+ end
77
+
78
+ def self.add_defaults filename=nil, text: nil
79
+ return unless filename and File.exists? filename
80
+ @config_text ||= ''
81
+ @config_text += "\n" unless @config_text.empty?
82
+ File.readlines(filename).each do |line|
83
+ if (line.start_with? '---' or line.start_with? '...')
84
+ @config_text << "\n\n"
85
+ else
86
+ @config_text << line unless @config_text.include? line
87
+ end
88
+ end if filename and File.exists? filename
89
+ @config_text << "\n\n" << text if text
90
+
91
+ @config_defaults ||= {}
92
+ @config_defaults.deep_merge! YAML::load(@config_text).deep_symbolize!
93
+ @loaded = false
94
+ end
95
+
96
+ def self.load
97
+ if (! @config_file.nil?) and File.exists? @config_file
98
+ @config = YAML::load(File.read(@config_file)).deep_symbolize!
99
+ @config.deep_merge! @config_defaults if @merge_defaults
100
+ elsif @userfile_required
101
+ puts "User's config file not found at #{@config_file}. Please run this tool with the -g option to generate it."
102
+ exit 1
103
+ else
104
+ @config = @config_defaults
105
+ end
106
+ @loaded = true
107
+ @config
108
+ end
109
+
110
+ def self.config
111
+ self.load unless @loaded
112
+ (@config.nil?) ? @config_defaults : @config
113
+ end
114
+
115
+ def self.generate_userconf filename
116
+ filepath = "#{(filename) ? filename : "#{Dir.pwd}/config.yml"}"
117
+ File.write filepath, @config_text
118
+ File.open(filepath, 'a') do |f|
119
+ f.puts "# Individual Settings"
120
+ @config_individual_lines.each { |l| f.puts l }
121
+ end if @config_individual_lines
122
+
123
+
124
+ if @categorized_defaults
125
+ text = ''
126
+ @categorized_defaults.each do |name, opts|
127
+ text += "\n# #{opts[:description]}\n"
128
+ text += "#{name.to_s}:\n"
129
+ opts[:config].each do |opt, v|
130
+ text += " #{opt.to_s}: #{v[:value]}".ljust(30) + " # #{v[:description]}\n"
131
+ end
132
+ end
133
+ File.open(filepath, 'a') do |f|
134
+ f.puts text
135
+ end
136
+ end
137
+ end
138
+
139
+ end
140
+
@@ -0,0 +1,28 @@
1
+ ##
2
+ # Functions to convert hash keys to all symbols or all strings
3
+ ##
4
+ class Hash
5
+ def deep_symbolize! hsh = nil
6
+ hsh ||= self
7
+ hsh.keys.each do |k|
8
+ if k.is_a? String
9
+ hsh[k.to_sym] = hsh[k]
10
+ hsh.delete k
11
+ end
12
+ deep_symbolize! hsh[k.to_sym] if hsh[k.to_sym].is_a? Hash
13
+ end
14
+ hsh
15
+ end
16
+
17
+ def deep_stringify! hsh = nil
18
+ hsh ||= self
19
+ hsh.keys.each do |k|
20
+ if k.is_a? Symbol
21
+ hsh[k.to_s] = hsh[k]
22
+ hsh.delete k
23
+ end
24
+ deep_stringify! hsh[k.to_s] if hsh[k.to_s].is_a? Hash
25
+ end
26
+ hsh
27
+ end
28
+ end
@@ -0,0 +1,73 @@
1
+ ####
2
+ # Logging Module
3
+ ####
4
+ # The logging module automatically initializes and formats the Ruby logger, and makes it available for the application.
5
+ # Note that all of the built-in log commands are available.
6
+ #
7
+ # Additionally, a convenience function Rbcli::debug is available, which does not log, but prints an object.to_s
8
+ # in red so that it is easily identifiable in the coutput.
9
+ #
10
+ # Usage
11
+ #
12
+ # Rbcli::log.info {'Some Message'}
13
+ #
14
+ # Rbcli::debug myobj
15
+ #
16
+ ####
17
+
18
+ require 'logger'
19
+ require 'colorize'
20
+
21
+ module Rbcli::Logger
22
+
23
+ @default_level = 'info'
24
+ @default_target = 'stderr'
25
+
26
+ def self.save_defaults level: nil, target: nil
27
+ @default_level = level if level
28
+ @default_target = target if target
29
+
30
+ Rbcli::Config::add_categorized_defaults :logger, 'Log Settings', {
31
+ log_level: {
32
+ description: '0-5, or DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN',
33
+ value: @default_level || 'info'
34
+ },
35
+ log_target: {
36
+ description: 'STDOUT, STDERR, or a file path',
37
+ value: @default_target || 'stderr'
38
+ }
39
+ }
40
+ end
41
+ self.save_defaults
42
+
43
+
44
+ if Rbcli::config[:logger][:log_target].downcase == 'stdout'
45
+ target = STDOUT
46
+ elsif Rbcli::config[:logger][:log_target].downcase == 'stderr'
47
+ target = STDERR
48
+ else
49
+ target = Rbcli::config[:logger][:log_target]
50
+ end
51
+ @logger = Logger.new(target)
52
+ @logger.level = Rbcli::config[:logger][:log_level]
53
+
54
+ original_formatter = Logger::Formatter.new
55
+ @logger.formatter = proc do |severity, datetime, progname, msg|
56
+ original_formatter.call(severity, datetime, progname || caller_locations[3].path.split('/')[-1], msg.dump)
57
+ end
58
+
59
+ def self.log
60
+ @logger
61
+ end
62
+
63
+ end
64
+
65
+ module Rbcli
66
+ def self.log
67
+ Rbcli::Logger::log
68
+ end
69
+
70
+ def self.debug obj
71
+ puts obj.to_s.red
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ class String
2
+ def black; "\e[30m#{self}\e[0m" end
3
+ def red; "\e[31m#{self}\e[0m" end
4
+ def green; "\e[32m#{self}\e[0m" end
5
+ def brown; "\e[33m#{self}\e[0m" end
6
+ def blue; "\e[34m#{self}\e[0m" end
7
+ def magenta; "\e[35m#{self}\e[0m" end
8
+ def cyan; "\e[36m#{self}\e[0m" end
9
+ def gray; "\e[37m#{self}\e[0m" end
10
+
11
+ def bg_black; "\e[40m#{self}\e[0m" end
12
+ def bg_red; "\e[41m#{self}\e[0m" end
13
+ def bg_green; "\e[42m#{self}\e[0m" end
14
+ def bg_brown; "\e[43m#{self}\e[0m" end
15
+ def bg_blue; "\e[44m#{self}\e[0m" end
16
+ def bg_magenta; "\e[45m#{self}\e[0m" end
17
+ def bg_cyan; "\e[46m#{self}\e[0m" end
18
+ def bg_gray; "\e[47m#{self}\e[0m" end
19
+
20
+ def bold; "\e[1m#{self}\e[22m" end
21
+ def italic; "\e[3m#{self}\e[23m" end
22
+ def underline; "\e[4m#{self}\e[24m" end
23
+ def blink; "\e[5m#{self}\e[25m" end
24
+ def reverse_color; "\e[7m#{self}\e[27m" end
25
+ end
data/lib/rbcli/util.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rbcli/util/hash_deep_symbolize'
2
+ #require 'rbcli/util/string_colorize' # We are using the colorize gem instead. The code is kept here for reference.
3
+ require 'rbcli/util/config'
4
+ require 'rbcli/util/logging'
@@ -0,0 +1,3 @@
1
+ module Rbcli
2
+ VERSION = "0.1.0"
3
+ end
data/lib/rbcli.rb ADDED
@@ -0,0 +1,11 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ module Rbcli end # Empty module declaration required to declare submodules freely
5
+ require "rbcli/version"
6
+
7
+ require "rbcli/util"
8
+ require "rbcli/configurate"
9
+
10
+ require "rbcli/engine/command"
11
+ require "rbcli/engine/parser"
data/rbcli.gemspec ADDED
@@ -0,0 +1,42 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "rbcli/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'rbcli'
8
+ spec.version = Rbcli::VERSION
9
+ spec.authors = ['Andrew Khoury']
10
+ spec.email = ['akhoury@live.com']
11
+
12
+ spec.summary = %q{A CLI Framework for Ruby}
13
+ spec.description = %q{RBCli is a framework to quickly develop command-line tools.}
14
+ spec.homepage = 'https://github.com/akhoury6/rbcli'
15
+ spec.license = 'MIT'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata["allowed_push_host"] = "http://mygemserver.com"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against " \
23
+ # "public gem pushes."
24
+ # end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_development_dependency 'bundler', '~> 1.16'
36
+ spec.add_development_dependency 'rake', '~> 10.0'
37
+ spec.add_development_dependency 'minitest', '~> 5.0'
38
+
39
+ spec.add_dependency 'colorize', '~> 0.8'
40
+ spec.add_dependency 'deep_merge', '~> 1.2'
41
+ spec.add_dependency 'trollop', '~> 2.1'
42
+ end