fuelcell 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +15 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +5 -0
  5. data/.travis.yml +15 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +51 -0
  10. data/Rakefile +5 -0
  11. data/bin/console +9 -0
  12. data/bin/example.rb +9 -0
  13. data/bin/setup +7 -0
  14. data/bin/test +20 -0
  15. data/bin/world.rb +6 -0
  16. data/fuelcell.gemspec +26 -0
  17. data/lib/fuelcell/action/arg_definition.rb +36 -0
  18. data/lib/fuelcell/action/arg_results.rb +57 -0
  19. data/lib/fuelcell/action/args_manager.rb +66 -0
  20. data/lib/fuelcell/action/callable.rb +54 -0
  21. data/lib/fuelcell/action/command.rb +72 -0
  22. data/lib/fuelcell/action/not_found.rb +55 -0
  23. data/lib/fuelcell/action/opt_definition.rb +79 -0
  24. data/lib/fuelcell/action/opt_results.rb +68 -0
  25. data/lib/fuelcell/action/opts_manager.rb +80 -0
  26. data/lib/fuelcell/action/root.rb +76 -0
  27. data/lib/fuelcell/action/subcommands.rb +81 -0
  28. data/lib/fuelcell/action.rb +11 -0
  29. data/lib/fuelcell/cli.rb +89 -0
  30. data/lib/fuelcell/help/base_formatter.rb +24 -0
  31. data/lib/fuelcell/help/builder.rb +71 -0
  32. data/lib/fuelcell/help/cmds_formatter.rb +57 -0
  33. data/lib/fuelcell/help/desc_formatter.rb +21 -0
  34. data/lib/fuelcell/help/opts_formatter.rb +62 -0
  35. data/lib/fuelcell/help/usage_formatter.rb +88 -0
  36. data/lib/fuelcell/help.rb +27 -0
  37. data/lib/fuelcell/parser/arg_handler.rb +31 -0
  38. data/lib/fuelcell/parser/base_handler.rb +133 -0
  39. data/lib/fuelcell/parser/cmd_args_strategy.rb +28 -0
  40. data/lib/fuelcell/parser/ignore_handler.rb +25 -0
  41. data/lib/fuelcell/parser/opt_handler.rb +58 -0
  42. data/lib/fuelcell/parser/opt_name_handler.rb +89 -0
  43. data/lib/fuelcell/parser/opt_value_equal_handler.rb +26 -0
  44. data/lib/fuelcell/parser/parsing_strategy.rb +80 -0
  45. data/lib/fuelcell/parser/short_opt_no_space_handler.rb +54 -0
  46. data/lib/fuelcell/parser.rb +4 -0
  47. data/lib/fuelcell/shell.rb +102 -0
  48. data/lib/fuelcell/version.rb +3 -0
  49. data/lib/fuelcell.rb +114 -0
  50. metadata +148 -0
@@ -0,0 +1,68 @@
1
+ module Fuelcell
2
+ module Action
3
+ class OptResults < ::Hash
4
+
5
+ def initialize(hash = {})
6
+ super()
7
+ hash.each do |key, value|
8
+ self[normalize(key)] = value
9
+ end
10
+ end
11
+
12
+ def [](key)
13
+ super(normalize(key))
14
+ end
15
+
16
+ def []=(key, value)
17
+ super(normalize(key), value)
18
+ end
19
+
20
+ def delete(key)
21
+ super(normalize(key))
22
+ end
23
+
24
+ def fetch(key, *args)
25
+ super(normalize(key), *args)
26
+ end
27
+
28
+ def key?(key)
29
+ super(normalize(key))
30
+ end
31
+
32
+ def values_at(*indices)
33
+ indices.map { |key| self[normalize(key)] }
34
+ end
35
+
36
+ def merge(hash)
37
+ dup.merge!(hash)
38
+ end
39
+
40
+ def merge!(hash)
41
+ hash.each do |key, value|
42
+ self[normalize(key)] = value
43
+ end
44
+ self
45
+ end
46
+
47
+ def to_hash
48
+ Hash.new(default).merge!(self)
49
+ end
50
+
51
+ def respond_to?(method_name, include_private = false)
52
+ return key?(method_name.to_s.chomp('?')) || super
53
+ end
54
+
55
+ protected
56
+
57
+ def normalize(key)
58
+ key.is_a?(Symbol) ? key.to_s : key
59
+ end
60
+
61
+ def method_missing(method_name, *args, &_block)
62
+ name = method_name.to_s
63
+ return self.key?(name.chomp('?')) if name[-1] == '?'
64
+ return self[name]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,80 @@
1
+ module Fuelcell
2
+ module Action
3
+ # Used by the Command to manage adding option from its dsl. It is also
4
+ # used during option parsing to find options, add global options, or check
5
+ # if any required options have been missed
6
+ class OptsManager
7
+ attr_reader :options
8
+
9
+ def initialize
10
+ @options = {}
11
+ end
12
+
13
+ def each
14
+ options.each {|_, option| yield option }
15
+ end
16
+
17
+ # Check to determine if any of the options are callable, meaning do
18
+ # they point to another command or have a lambda to be executed
19
+ #
20
+ # @return [Hash]
21
+ def callable
22
+ list = options.select { |_, opt| opt.callable? || opt.cmd_path? }
23
+ _, value = list.first
24
+ value
25
+ end
26
+
27
+ def required_opts
28
+ options.select { |_, opt| opt.required? }
29
+ end
30
+
31
+ def globals
32
+ options.select { |_, opt| opt.global? }
33
+ end
34
+
35
+ def missing_opts(names)
36
+ missing = required_opts.keys - names
37
+ return [] if missing.empty?
38
+
39
+ list = options.select { |(key, _)| missing.include?(key) }.values
40
+ yield list if block_given?
41
+ list
42
+ end
43
+
44
+ def add(option, config = {})
45
+ option = create(option, config) if option.is_a?(String)
46
+ if options.key?(option.name)
47
+ fail "can not add option: duplicate exists with name #{option.name}"
48
+ end
49
+ options[option.name] = option
50
+ end
51
+ alias_method :opt, :add
52
+
53
+ def remove(name)
54
+ # looks like an option definition so lets use it's name
55
+ if name.respond_to?(:name) && options.key?(name.name)
56
+ return options.delete(name.name)
57
+ end
58
+
59
+ opt = find(name)
60
+ return false unless opt
61
+
62
+ options.delete(opt.name)
63
+ end
64
+
65
+ def find(name)
66
+ target = false
67
+ options.each do |(_, option)|
68
+ target = option if option.name?(name)
69
+ end
70
+ target
71
+ end
72
+ alias_method :find_opt, :find
73
+ alias_method :[], :find
74
+
75
+ def create(name, config = {})
76
+ OptDefinition.new(name, config)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,76 @@
1
+ module Fuelcell
2
+ module Action
3
+ # Top most command in the command hierarchy. The root command holds all
4
+ # other commands that would appear on the command line, it holds the
5
+ # name of the script that uses it.
6
+ class Root < Command
7
+ attr_reader :help
8
+
9
+ def initialize(name = nil)
10
+ name = script_name if name.nil?
11
+ super(name)
12
+ install_help
13
+ end
14
+
15
+ # Find any command in the root command.
16
+ #
17
+ # Using the cmd_args, which form a command hierarchy, we search for the
18
+ # deepest sub command first. If that it not found we assume that command
19
+ # arg is really a regular arg and we put it back. We do this until we
20
+ # reach the top command
21
+ #
22
+ # == Parameters:
23
+ # cmd_args <Array>:: A hierarchal list of commands to be searched
24
+ # remaining_args <Array>:: All remaining raw args from ARGV
25
+ #
26
+ # == Returns:
27
+ # <Fuelcell::Command> command object
28
+ def locate(cmd_args, raw_args = [])
29
+ return self if cmd_args.empty?
30
+
31
+ target = NotFound.new(cmd_args)
32
+
33
+ loop do
34
+ terms = cmd_args.dup
35
+ break if cmd_args.empty?
36
+
37
+ target = search(terms)
38
+ break unless target.is_a?(NotFound)
39
+
40
+ raw_args.unshift(cmd_args.pop)
41
+ end
42
+
43
+ # this must be an arg for the root command's action and not a command
44
+ return self if callable? && target.is_a?(NotFound)
45
+
46
+ target
47
+ end
48
+
49
+ def ensure_command_hierarchy(cmd_args)
50
+ create_tree(self, cmd_args)
51
+ end
52
+
53
+ private
54
+
55
+ def script_name
56
+ File.basename($PROGRAM_NAME, File.extname($PROGRAM_NAME))
57
+ end
58
+
59
+ def install_help
60
+ helper = callable_helper
61
+ command 'help' do
62
+ usage '[COMMAND]', 'describes subcommands or a specific command'
63
+ run helper
64
+ end
65
+ end
66
+
67
+ def callable_helper
68
+ root = self
69
+ lambda do |_opts, args, shell|
70
+ text = Fuelcell::Help.generate(root, args, shell.terminal_width)
71
+ shell.puts text
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,81 @@
1
+ module Fuelcell
2
+ module Action
3
+ module Subcommands
4
+ extend Forwardable
5
+ delegate [:empty?, :[], :each] => :subcommands
6
+
7
+ def add(cmds)
8
+ cmds = [cmds] unless cmds.is_a?(Array)
9
+ cmds.each { |cmd| subcommands[cmd.name] = cmd }
10
+ end
11
+ alias_method :<<, :add
12
+
13
+ def [](name)
14
+ list = name.split(' ')
15
+ deep_find(self, list)
16
+ end
17
+
18
+ def exist?(name)
19
+ list = name.split(' ')
20
+ result = deep_find(self, list)
21
+ result.is_a?(NotFound) ? false : true
22
+ end
23
+
24
+ # Finds a subcommand by name.
25
+ #
26
+ # @param names [Array] hierarchical order of commands
27
+ # @return [Fuelcell::Command] of nil when not found
28
+ def search(names)
29
+ deep_find(self, names)
30
+ end
31
+
32
+ # Collect global options from the option manager of every
33
+ # command in the Hierarchy
34
+ #
35
+ # @return [Hash]
36
+ def global_options
37
+ collect_global_options(self, {})
38
+ end
39
+
40
+ protected
41
+
42
+ def subcommands
43
+ @subcommands ||= {}
44
+ end
45
+
46
+ def deep_find(cmd, names)
47
+ return cmd if names.empty?
48
+
49
+ name = names.shift
50
+ unless cmd.subcommands.key?(name)
51
+ names.unshift name
52
+ return NotFound.new(names)
53
+ end
54
+ deep_find(cmd.subcommands[name], names)
55
+ end
56
+
57
+ def create_tree(current_cmd, tree)
58
+ while search_key = tree.shift
59
+ unless current_cmd.exist?(search_key)
60
+ current_cmd.command(search_key) {}
61
+ end
62
+ child = current_cmd[search_key]
63
+ create_tree(child, tree)
64
+ end
65
+ child
66
+ end
67
+
68
+ def collect_global_options(cmd, list)
69
+ globals = cmd.opts.globals
70
+ list.merge!(globals)
71
+
72
+ return list if cmd.empty?
73
+
74
+ cmd.each do |_, subcommand|
75
+ collect_global_options(subcommand, list)
76
+ end
77
+ list
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ require 'fuelcell/action/callable'
2
+ require 'fuelcell/action/opt_definition'
3
+ require 'fuelcell/action/arg_definition'
4
+ require 'fuelcell/action/arg_results'
5
+ require 'fuelcell/action/opt_results'
6
+ require 'fuelcell/action/opts_manager'
7
+ require 'fuelcell/action/args_manager'
8
+ require 'fuelcell/action/subcommands'
9
+ require 'fuelcell/action/command'
10
+ require 'fuelcell/action/not_found'
11
+ require 'fuelcell/action/root'
@@ -0,0 +1,89 @@
1
+ require 'fuelcell/shell'
2
+ require 'fuelcell/help'
3
+ require 'fuelcell/action'
4
+ require 'fuelcell/parser'
5
+ module Fuelcell
6
+ class Cli
7
+ attr_reader :root, :shell, :cmd_args_extractor, :parser
8
+
9
+ # Initializes with a root command object
10
+ #
11
+ # When nothing is given we default to the script name otherwise you choose
12
+ # the name of the root command or the command itself
13
+ #
14
+ def initialize(config = {})
15
+ @exit = config.fetch(:exit) { true }
16
+ @exit = @exit == false ? false : true
17
+ @root = config.fetch(:root) { Action::Root.new }
18
+ @shell = config.fetch(:shell) { Shell.new }
19
+ @parser = config.fetch(:parser) {
20
+ Parser::ParsingStrategy.new
21
+ }
22
+
23
+ @cmd_args_extractor = config.fetch(:cmd_args_extractor) {
24
+ Parser::CmdArgsStrategy.new
25
+ }
26
+ end
27
+
28
+ # Delegates all parsing responsiblities to a series of handlers, returning
29
+ # a structured hash needed to execute a command. The command being executed
30
+ # is determined by the CmdArgsStrategy unless you override it, it extracts
31
+ # all args upto the first option or ignore. The RootCommand is used to find
32
+ # the command using the extracted args, it accounts for sub commands. The
33
+ # parser Parser::ParsingStategy handles processing opts, args and ignored
34
+ # args
35
+ #
36
+ # @param raw_args [Array] cli args usually from ARGV
37
+ # @return [Hash] structured context for executing a command
38
+ def parse(raw_args)
39
+ cmd_args = cmd_args_extractor.call(raw_args)
40
+ cmd = root.locate(cmd_args, raw_args)
41
+ root.add_global_options(cmd)
42
+ begin
43
+ parser.call(cmd, raw_args)
44
+ rescue Exception => e
45
+ shell.error e.message
46
+ shell.failure_exit
47
+ end
48
+ end
49
+
50
+ # Executes the callable object in a command. All command callable object
51
+ # expect to be called the the options hash, arg hash and shell object.
52
+ #
53
+ # @param context [Hash]
54
+ # @return [Integer]
55
+ def execute(context)
56
+ cmd = context[:cmd]
57
+ opts = context[:opts] || {}
58
+ args = context[:args] || []
59
+ cli_shell = context[:shell] || shell
60
+
61
+ cmd = handle_callable_option(root, cmd)
62
+
63
+ cmd.call(opts, args, cli_shell)
64
+
65
+ end
66
+
67
+ def handle_callable_option(root, cmd)
68
+ opt_manager = cmd.opts
69
+ opt = opt_manager.callable
70
+ return cmd unless opt
71
+ return root.locate(opt.cmd_ath) if opt.cmd_path?
72
+ opt
73
+ end
74
+
75
+ def exit?
76
+ @exit
77
+ end
78
+
79
+ # Allows the system to by pass the exit call which is helpful in testing
80
+ # and when trying to manually control the system
81
+ #
82
+ # @param code [Int] integer from 0 .. 255 representing the exit code
83
+ # @return [Int] when exit is false
84
+ def handle_exit(code)
85
+ shell.exit code if exit?
86
+ code
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ module Fuelcell
2
+ module Help
3
+ # Hold common functionality and information needed by all formatters
4
+ class BaseFormatter
5
+ DEFAULT_WIDTH = 80
6
+ DEFAULT_PADDING = 4
7
+ attr_reader :width, :padding, :max_width
8
+
9
+ def initialize(config = {})
10
+ @width = config[:width] || DEFAULT_WIDTH
11
+ @padding = config[:padding] || DEFAULT_PADDING
12
+ @max_width = width - padding
13
+ end
14
+
15
+ def short_opt(data, no_opt = '')
16
+ data[:short].nil? ? no_opt : "-#{data[:short]}"
17
+ end
18
+
19
+ def long_opt(data, no_opt = '')
20
+ data[:long].nil? ? no_opt : "--#{data[:long]}=#{data[:long].upcase}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ module Fuelcell
2
+ module Help
3
+ class Builder
4
+
5
+ def call(root, args, help = {})
6
+ cmd = args.empty? ? root : root.locate(args.raw)
7
+ path = args.join(' ').strip
8
+ build_usage(root.name, path, cmd, help)
9
+ build_options(cmd.opts, help)
10
+ build_commands(cmd, path, help)
11
+ build_desc(cmd, help)
12
+ help
13
+ end
14
+
15
+ # @param script_name [String] name of script you asked help from
16
+ # @param path [String] path of command you need help for
17
+ # @param cmd [Fuelcell::Action::Command] command you need help with
18
+ # @param data [Hash] stores the structure of the help content
19
+ # @return [Hash]
20
+ def build_usage(script_name, path, cmd, data = {})
21
+ usage = {}
22
+ usage[:label] = 'Usage:'
23
+ usage[:path] = "#{script_name} #{path}".strip
24
+ usage[:text] = cmd.usage.nil? ? '' : cmd.usage
25
+ usage[:args] = []
26
+ usage[:opts] = []
27
+
28
+ cmd.opts.required_opts.each do |(_, opt)|
29
+ usage[:opts] << common_opt_data(opt)
30
+ end
31
+
32
+ data[:usage] = usage
33
+ data
34
+ end
35
+
36
+ def build_options(opt_manager, data = {})
37
+ options = []
38
+ opt_manager.each do |opt|
39
+ next if opt.required?
40
+ options << common_opt_data(opt).merge!(banner: opt.banner)
41
+ end
42
+
43
+ data[:options] = options
44
+ data
45
+ end
46
+
47
+ def build_commands(cmd, path, data = {})
48
+ data[:commands] = []
49
+ cmd.each do |_, command|
50
+ data[:commands] << {
51
+ name: command.name,
52
+ path: path,
53
+ desc: command.desc || ''
54
+ }
55
+ end
56
+ data
57
+ end
58
+
59
+ def build_desc(cmd, data = {})
60
+ data[:desc] = cmd.desc
61
+ data
62
+ end
63
+
64
+ protected
65
+
66
+ def common_opt_data(opt)
67
+ { short: opt.short, long: opt.long, type: opt.type }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ module Fuelcell
2
+ module Help
3
+ class CmdsFormatter < BaseFormatter
4
+ attr_reader :indent, :desc_space
5
+ def initialize(config = {})
6
+ super
7
+ @indent = (config[:indent] || 2).to_i
8
+ @desc_space = (config[:banner_space] || 2).to_i
9
+ end
10
+
11
+ def call(data)
12
+ return '' if empty?(data)
13
+ str = "Commands:\n"
14
+ widest = widest_cmd(data[:commands])
15
+ data[:commands].each do |cmd|
16
+ str << line(cmd[:name], widest, cmd[:desc])
17
+ end
18
+ str
19
+ end
20
+
21
+ def line(cmd, widest_cmd, desc)
22
+ max = indent + desc_space + widest_cmd
23
+ desc = wrap(desc || '', max)
24
+ pad = create_padding(widest_cmd, cmd)
25
+ indent_str = ' ' * indent
26
+ column_space = ' ' * desc_space
27
+ "#{indent_str}#{cmd}#{pad}#{column_space}# #{desc}\n"
28
+ end
29
+
30
+ def create_padding(widest, cmd)
31
+ ' ' * (widest - cmd.size)
32
+ end
33
+
34
+ def empty?(data)
35
+ !data.key?(:commands) || data[:commands].empty?
36
+ end
37
+
38
+ def wrap(text, widest_text)
39
+ return text if (widest_text + text.size) < max_width
40
+
41
+ pad = ' ' * widest_text
42
+ pattern = /(.{1,#{max_width - widest_text}})(\s+|$)/
43
+ text = text.gsub(pattern, "\\1\n#{pad}# ")
44
+ text.gsub(/# $/, '').strip
45
+ end
46
+
47
+ def widest_cmd(commands)
48
+ max = 0
49
+ commands.each do |data|
50
+ size = data[:name].size
51
+ max = size if size > max
52
+ end
53
+ max
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ module Fuelcell
2
+ module Help
3
+ class DescFormatter < BaseFormatter
4
+
5
+ def call(data)
6
+ return '' if empty?(data)
7
+ wrap(data[:long_desc] || data[:desc]) + "\n"
8
+ end
9
+
10
+ def wrap(text)
11
+ pattern = /(.{1,#{max_width}})(\s+|$)/
12
+ text.gsub(pattern, "\\1\n").strip
13
+ end
14
+
15
+ def empty?(data)
16
+ (!data.key?(:desc) || data[:desc].to_s.empty?) &&
17
+ (!data.key?(:long_desc) || data[:long_desc].to_s.empty?)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ module Fuelcell
2
+ module Help
3
+ class OptsFormatter < BaseFormatter
4
+ attr_reader :indent, :banner_space
5
+ def initialize(config = {})
6
+ super
7
+ @indent = (config[:indent] || 2).to_i
8
+ @banner_space = (config[:banner_space] || 2).to_i
9
+ end
10
+
11
+ def call(data)
12
+ return '' if empty?(data)
13
+
14
+ str = "Options:\n"
15
+ widest = widest_opt(data[:options])
16
+ data[:options].each do |opt|
17
+ opt, banner = opt_data(opt)
18
+ str << line(opt, widest, banner)
19
+ end
20
+ str
21
+ end
22
+
23
+ def line(opt, widest_opt, banner_text)
24
+ max = indent + banner_space + widest_opt
25
+ banner = wrap(banner_text, max)
26
+ pad = create_padding(widest_opt, opt)
27
+ indent_str = ' ' * indent
28
+ column_space = ' ' * banner_space
29
+ "#{indent_str}#{opt}#{pad}#{column_space}#{banner}\n"
30
+ end
31
+
32
+ def create_padding(widest, opt)
33
+ ' ' * (widest - opt.size)
34
+ end
35
+
36
+ def empty?(data)
37
+ !data.key?(:options) || data[:options].empty?
38
+ end
39
+
40
+ def wrap(text, widest_text)
41
+ return text if (widest_text + text.size) < max_width
42
+
43
+ pad = ' ' * widest_text
44
+ pattern = /(.{1,#{max_width - widest_text}})(\s+|$)/
45
+ text.gsub(pattern, "\\1\n#{pad}").strip
46
+ end
47
+
48
+ def widest_opt(options)
49
+ max = 0
50
+ options.each do |data|
51
+ size = "#{short_opt(data, ' ')} #{long_opt(data)}".size
52
+ max = size if size > max
53
+ end
54
+ max
55
+ end
56
+
57
+ def opt_data(data)
58
+ ["#{short_opt(data, ' ')} #{long_opt(data)}", data[:banner] || '']
59
+ end
60
+ end
61
+ end
62
+ end