fuelcell 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.
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