rbcli 0.1.0

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