cli-topic 0.9.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: db41d9576d7783ae342b2baf3038f6ec6b352c0c
4
+ data.tar.gz: 6b4599889c0274065ae9476057cde4578c8d1fd8
5
+ SHA512:
6
+ metadata.gz: 43c307785885c77a48275e7816178d086310c828b708ab0aa2e7f20969842713854be436cc35535203e320b2992541c73d2039f0d267dce208e86397df58792e
7
+ data.tar.gz: 9acf2789a9ffea0eed9bcebfea86de716e3e2cc2ce2018d695392796a88da7bf8457d919d4668612a453d75257eb33b2cea43de87697c02bc3e5e7c0ee8e92ef
data/Changelog ADDED
@@ -0,0 +1,3 @@
1
+ - 0.0.1
2
+ * Initial version
3
+ -- Antoine Legrand <antoine.legrand@ubudu.com> 27/07/2013
data/License ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Antoine Legrand (ant.legrand@gmail.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+ Topicli
2
+ ==========
3
+ 'Small framework to build CLI. It provide simple way to declare options/descriptions/topics to focus only on the "action" part of commands.
4
+ Some features:
5
+ - Light DSL
6
+ - Commands are organised in Topic (aka Subcommands)
7
+ - DRY options declaration, it\'s use 3 layers: global -> topic -> command
8
+ - Each topic has it\'s own description/options list
9
+ - Load options values from a config file
10
+ - Built-in Help command: ./cli help TOPIC/COMMAND
11
+ - Flexible option-parser, Cli-topic use the stdlib OptionParser by default, but can be changed to Slop/Trollop or any custom one.
12
+ - Command suggestions',
@@ -0,0 +1,34 @@
1
+ require 'clitopic/commands'
2
+ require 'clitopic/command'
3
+ require 'clitopic/helpers'
4
+ module Clitopic
5
+ module Cli
6
+
7
+ class << self
8
+
9
+ def run(args)
10
+ args = args.dup
11
+ $stdin.sync = true if $stdin.isatty
12
+ $stdout.sync = true if $stdout.isatty
13
+ command = args.shift.strip rescue "help"
14
+ if !Clitopic.commands_dir.nil?
15
+ Clitopic::Commands.load_commands(Clitopic.commands_dir)
16
+ end
17
+ Clitopic::Commands.run(command, args)
18
+ rescue Errno::EPIPE => e
19
+ puts e.message #error(e.message)
20
+ puts e.backtrace
21
+ rescue Interrupt => e
22
+ `stty icanon echo`
23
+ if Clitopic.debug
24
+ Clitopic::Helpers.styled_error(e)
25
+ else
26
+ Clitopic::Helpers.error("Command cancelled.", false)
27
+ end
28
+ rescue => error
29
+ Clitopic::Helpers.styled_error(error)
30
+ exit(1)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,136 @@
1
+ require 'clitopic/utils'
2
+ require 'clitopic/parsers'
3
+ require 'clitopic/topic'
4
+
5
+ module Clitopic
6
+ module Command
7
+ class Base
8
+ class << self
9
+ include Clitopic.parser
10
+
11
+ attr_accessor :name, :banner, :description, :hidden, :short_description
12
+ attr_accessor :arguments, :options
13
+
14
+ def cmd_options
15
+ @cmd_options ||= []
16
+ end
17
+
18
+ def banner
19
+ @banner ||= "Usage: #{Clitopic.name} #{self.fullname} [options]"
20
+ end
21
+
22
+ def short_description
23
+ if @short_description.nil?
24
+ if description
25
+ @short_description = description.split("\n").first
26
+ end
27
+ end
28
+ return @short_description
29
+ end
30
+
31
+ def option(name, *args, &blk)
32
+ opt = Clitopic::Utils.parse_option(name, *args, &blk)
33
+ if !opt[:default].nil?
34
+ options[name] = opt[:default]
35
+ end
36
+ cmd_options << opt
37
+ end
38
+
39
+ def fullname
40
+ if topic.nil?
41
+ return name
42
+ elsif name == 'index'
43
+ "#{topic.name}"
44
+ else
45
+ "#{topic.name}:#{name}"
46
+ end
47
+ end
48
+
49
+ def call()
50
+ puts "call with #{options} #{arguments}"
51
+ end
52
+
53
+ def arguments
54
+ @arguments ||= []
55
+ end
56
+
57
+ def options
58
+ @options ||= {}
59
+ end
60
+
61
+ def load_defaults(file=nil)
62
+ if file.nil?
63
+ Clitopic.default_files.each do |f|
64
+ if File.exist?(f)
65
+ file = f
66
+ break
67
+ end
68
+ end
69
+ end
70
+
71
+ if file.nil?
72
+ return
73
+ end
74
+
75
+ defaults = YAML.load_file(file)
76
+ if self.topic.nil?
77
+ cmd_defaults = defaults[self.name]
78
+ else
79
+ cmd_defaults = defaults[self.topic.name][self.name]
80
+ end
81
+
82
+ if cmd_defaults.nil?
83
+ return
84
+ end
85
+
86
+ cmd_defaults[:options].each do |name, value|
87
+ if !value.nil?
88
+ if options[name].nil?
89
+ options[name] = value
90
+ elsif options[name].is_a?(Array)
91
+ options[name] += value
92
+ end
93
+ end
94
+ end
95
+ if cmd_defaults[:arguments] && !arguments
96
+ arguments += Array(cmd_defaults[:arguments])
97
+ end
98
+ end
99
+
100
+ def topic(arg=nil)
101
+ if !arg.nil?
102
+ if arg.is_a?(String)
103
+ @topic ||= Topics[arg]
104
+ elsif arg.is_a?(Class) && arg < Clitopic::Topic::Base
105
+ @topic ||= arg.instance
106
+ elsif arg.is_a?(Hash)
107
+ @topic ||= Clitopic::Topic::Base.register(arg)
108
+ end
109
+ end
110
+ return @topic
111
+ end
112
+
113
+
114
+ def register(opts={})
115
+ opts = {hidden: false}.merge(opts)
116
+ if !opts.has_key?(:name)
117
+ raise ArgumentError.new("missing Command name")
118
+ end
119
+
120
+ topic(opts[:topic])
121
+ @description = opts[:description]
122
+ @name = opts[:name]
123
+ @hidden = opts[:hidden]
124
+ @banner = opts[:banner]
125
+ @short_description = opts[:short_description]
126
+
127
+ if @topic.nil?
128
+ Clitopic::Commands.global_commands[name] = self
129
+ else
130
+ topic.commands[name] = self
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,99 @@
1
+ require 'yaml'
2
+ require 'clitopic/command/base'
3
+ require 'clitopic/helpers'
4
+
5
+ module Clitopic
6
+ module Command
7
+ class ClitoTopic < Clitopic::Topic::Base
8
+ register name: 'clito',
9
+ description: 'clitopic commands',
10
+ hidden: true
11
+ end
12
+
13
+ class Suggestions < Clitopic::Command::Base
14
+ register name: 'suggestions',
15
+ description: 'suggests available commands base on incomplete input',
16
+ hidden: true,
17
+ topic: 'clito'
18
+
19
+ def self.call
20
+ puts Clitopic::Helpers.suggestion(@arguments[0], Clitopic::Commands.all_commands)
21
+ end
22
+
23
+ end
24
+
25
+ class DefaultFile < Clitopic::Command::Base
26
+ register name: 'defaults_file',
27
+ description: "create default file",
28
+ hidden: true,
29
+ topic: 'clito'
30
+
31
+ option :merge, "--[no-]merge", "Merge options with current file", default: true
32
+ option :force, "-f", "--force", "Overwrite file", default: false
33
+ option :hidden, "--with-hidden", "include hidden cmds/topics", default: false
34
+
35
+ class << self
36
+ def cmd_opts(cmd, opts)
37
+ if cmd.cmd_options.size > 0 && (!cmd.hidden || options[:hidden])
38
+ opts[cmd.name] = {options: {}, args: []}
39
+ cmd.cmd_options.each do |opt|
40
+ opts[cmd.name][:options][opt[:name].to_s] = opt[:default]
41
+ end
42
+ end
43
+ end
44
+
45
+ def dump_options(file, merge=true, force=false)
46
+ opts = {}
47
+ Clitopic::Commands.global_commands.each do |c, cmd|
48
+ cmd_opts(cmd, opts)
49
+ end
50
+ Clitopic::Topics.topics.each do |topic_name, topic|
51
+ if topic.commands.size > 0 && (!topic.hidden || options[:hidden])
52
+ opts[topic_name] = {}
53
+ topic.commands.each do |c, cmd|
54
+ cmd_opts(cmd, opts[topic_name])
55
+ end
56
+ end
57
+ end
58
+
59
+ if File.exist?(file)
60
+ if merge == false && force == false
61
+ raise ArgumentError.new("File #{file} exists, use --merge or --force")
62
+ end
63
+ if merge && !force
64
+ opts = opts.merge(YAML.load_file(file))
65
+ end
66
+ end
67
+ puts "write: #{file}"
68
+ File.open(file, 'wb') do |file|
69
+ file.write(opts.to_yaml)
70
+ end
71
+ puts opts.to_yaml
72
+ end
73
+
74
+ def call
75
+ puts @options
76
+ if @arguments.size == 0
77
+ raise ArgumentError.new("Missing file")
78
+ end
79
+ file = @arguments[0]
80
+ dump_options(file, @options[:merge], @options[:force])
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ class ClitoVersion < Clitopic::Command::Base
87
+ register name: 'version',
88
+ description: "Display clitopic version",
89
+ hidden: true,
90
+ topic: 'clito'
91
+
92
+ class << self
93
+ def call
94
+ puts "cli-topic version: #{Clitopic::VERSION}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,173 @@
1
+ require 'clitopic/command/base'
2
+
3
+ module Clitopic
4
+ module Topic
5
+ class Help < Clitopic::Topic::Base
6
+ register name: "help", description: "topic description"
7
+ end
8
+ end
9
+
10
+ module Command
11
+ class Help2 < Clitopic::Command::Base
12
+ register name: 'help2',
13
+ description: "Display helps",
14
+ banner: "Display helps",
15
+ topic: "help"
16
+
17
+ option :all, "--all", "Display all topics with all commands"
18
+ option :topics, "--topics", "Display all availables topics"
19
+ option :topic, "--topic=TOPIC", "Display availables commands for the TOPIC"
20
+ option :with_hidden, "--with-hidden", "Include hidden commands/topics"
21
+ end
22
+
23
+ class Help < Clitopic::Command::Base
24
+ register name: 'index',
25
+ banner: "Usage: #{Clitopic.name} help [COMMAND]",
26
+ description: "list available commands or display help for a specific command",
27
+ topic: "help"
28
+
29
+ option :all, "--all", "Display all topics with all commands"
30
+ option :topics, "--topics", "Display all availables topics"
31
+ option :topic, "--topic=TOPIC", "Display availables commands for the TOPIC"
32
+ option :with_hidden, "--with-hidden", "Include hidden commands/topics"
33
+ class << self
34
+
35
+
36
+ def header(obj)
37
+ puts obj.description
38
+ puts "\n"
39
+ end
40
+
41
+ def display_globals
42
+ puts "Primary help topics, type \"#{Clitopic.name} help TOPIC\" for more details:\n\n"
43
+ Clitopic::Commands.global_commands.each do |name, cmd|
44
+ puts ("%-#{longest_global_cmd + 3}s # %s" % [ "#{name}", "#{cmd.short_description}"]).indent(2)
45
+ end
46
+ end
47
+
48
+ def display_cmd(cmd, with_header=false)
49
+ if with_header
50
+ header(cmd)
51
+ end
52
+ if cmd.hidden == false || options[:with_hidden] == true
53
+ puts cmd.help
54
+ puts "\n\n"
55
+ end
56
+ end
57
+
58
+ def longest_global_cmd
59
+ @longest_global_cmd ||= Clitopic::Commands.global_commands.keys.map{|k| k.size}.max
60
+ end
61
+
62
+ def longest_cmd
63
+ @longest_cmd ||= Clitopic::Commands.all_commands.map{|k| k.size}.max
64
+ end
65
+
66
+ def longest_topic
67
+ @longest_topic ||= Topics.topics.keys.map{|k| k.size}.max
68
+ end
69
+
70
+ def display_topic(topic_name, with_commands=false, with_header=false)
71
+ if with_commands
72
+ longest = longest_cmd
73
+ else
74
+ longest = longest_topic
75
+ end
76
+ topic = Topics[topic_name]
77
+ if with_header
78
+ header(topic)
79
+ end
80
+ if topic.hidden == false || options[:with_hidden] == true
81
+ if with_header
82
+ if !topic.commands['index'].nil?
83
+ display_cmd(topic.commands['index'])
84
+ end
85
+ else
86
+ puts ("%-#{longest + 3}s # %s" % [ "#{topic_name}", "#{topic.short_description}" ]).indent(2)
87
+ end
88
+ if with_commands
89
+ puts "Additional commands, type \"#{Clitopic.name} help COMMAND\" for more details:\n\n" if with_header
90
+ topic.commands.each do |cmd_name, cmd|
91
+ puts (" %-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
92
+ end
93
+ puts ""
94
+ end
95
+ end
96
+ end
97
+
98
+ def display_topic_help(topic_name)
99
+ topic = Topics[topic_name]
100
+ longest = topic.commands.keys.map{|a| "#{topic_name}:#{a}".size}.max
101
+ header(topic)
102
+
103
+ if topic.hidden == false || options[:with_hidden] == true
104
+ if !topic.commands['index'].nil?
105
+ display_cmd(topic.commands['index'], true)
106
+ end
107
+ puts "Additional commands, type \"#{Clitopic.name} help COMMAND\" for more details:\n\n"
108
+ topic.commands.each do |cmd_name, cmd|
109
+ puts ("%-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
110
+ end
111
+ end
112
+ end
113
+
114
+ def display_topic(topic_name, with_commands=false, with_header=false)
115
+ if with_commands
116
+ longest = longest_cmd
117
+ else
118
+ longest = longest_topic
119
+ end
120
+ topic = Topics[topic_name]
121
+ header(topic) if with_header
122
+
123
+ if topic.hidden == false || options[:with_hidden] == true
124
+ puts ("%-#{longest + 3}s # %s" % [ "#{topic_name}", "#{topic.short_description}" ]).indent(2)
125
+ if with_commands
126
+ topic.commands.each do |cmd_name, cmd|
127
+ puts (" %-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
128
+ end
129
+ puts ""
130
+ end
131
+ end
132
+ end
133
+
134
+ def display_topics(with_commands=false)
135
+ puts "Additional topics:\n\n"
136
+ Clitopic::Topics.topics.each do |topic_name, topic|
137
+ display_topic(topic_name, with_commands)
138
+ end
139
+ end
140
+
141
+ def display_all(with_commands=false)
142
+ puts "Usage: #{Clitopic.name} COMMAND [command-specific-options]\n\n"
143
+
144
+ header(self)
145
+ display_globals
146
+ puts "\n"
147
+ display_topics(with_commands)
148
+ end
149
+
150
+ def call
151
+ if options[:all] == true
152
+ display_all(true)
153
+ elsif options.has_key?(:topic)
154
+ display_topic_help(options[:topic])
155
+ elsif arguments.size > 0
156
+ if Clitopic::Topics.topics[arguments[0]] != nil
157
+ display_topic_help(arguments[0])
158
+ else
159
+ cmd, topic = Clitopic::Commands.find_cmd(arguments[0])
160
+ if cmd.nil?
161
+ puts "Unknown command: #{arguments[0]}\n show all available commands with help --all"
162
+ else
163
+ display_cmd(cmd, true)
164
+ end
165
+ end
166
+ else
167
+ display_all
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,22 @@
1
+ require 'clitopic/command/base'
2
+
3
+ module Clitopic
4
+ module Command
5
+
6
+ class Version < Clitopic::Command::Base
7
+ register name: 'version',
8
+ description: "show #{Clitopic.name} current version
9
+
10
+ Example:
11
+
12
+ $ #{Clitopic.name} version
13
+ #{Clitopic.version}
14
+ "
15
+ class << self
16
+ def call
17
+ puts Clitopic.version
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ require 'clitopic/command/base'
2
+ Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
3
+ require file
4
+ end
@@ -0,0 +1,123 @@
1
+ require 'clitopic/utils'
2
+ require 'clitopic/topics'
3
+
4
+ module Clitopic
5
+ module Commands
6
+ class CommandFailed < RuntimeError; end
7
+
8
+ module ClassMethods
9
+ attr_accessor :binary, :current_cmd, :current_topic
10
+
11
+ def load_commands(dir)
12
+ Dir[File.join(dir, "*.rb")].each do |file|
13
+ require file
14
+ end
15
+ end
16
+
17
+ def current_args
18
+ @current_args
19
+ end
20
+
21
+ def current_options
22
+ @current_options ||= {}
23
+ end
24
+
25
+ def global_options
26
+ @global_options ||= []
27
+ end
28
+
29
+ def global_commands
30
+ @global_commands ||= {}
31
+ end
32
+
33
+ def global_option(name, *args, &blk)
34
+ # args.sort.reverse gives -l, --long order
35
+ global_options << Clitopic::Utils.parse_option(name, *args, &blk)
36
+ end
37
+
38
+ def invalid_arguments
39
+ @invalid_arguments
40
+ end
41
+
42
+ def shift_argument
43
+ # dup argument to get a non-frozen string
44
+ @invalid_arguments.shift.dup rescue nil
45
+ end
46
+
47
+ def validate_arguments!(invalid_options)
48
+ unless invalid_options.empty?
49
+ arguments = invalid_options.map {|arg| "\"#{arg}\""}
50
+ if arguments.length == 1
51
+ message = "Invalid option: #{arguments.first}"
52
+ elsif arguments.length > 1
53
+ message = "Invalid options: "
54
+ message << arguments[0...-1].join(", ")
55
+ message << " and "
56
+ message << arguments[-1]
57
+ end
58
+ $stderr.puts(Clitopic::Helpers.format_with_bang(message) + "\n\n")
59
+ run(@current_cmd.fullname, ["--help"])
60
+ end
61
+ end
62
+
63
+ def all_commands
64
+ cmds = []
65
+ Topics.topics.each do |k,topic|
66
+ topic.commands.each do |name, cmd|
67
+ if name == 'index'
68
+ cmds << topic.name
69
+ else
70
+ cmds << "#{topic.name}:#{name}"
71
+ end
72
+ end
73
+ end
74
+ cmds += global_commands.keys
75
+ return cmds
76
+ end
77
+
78
+ def prepare_run(cmd, arguments)
79
+ @current_options, @current_args = cmd.parse(arguments.dup)
80
+ rescue OptionParser::ParseError => e
81
+ $stderr.puts Clitopic::Helpers.format_with_bang(e.message)
82
+ run("help", [cmd.fullname])
83
+ end
84
+
85
+ def run(cmd, arguments=[])
86
+ if cmd == "-h" || cmd == "--help"
87
+ cmd = 'help'
88
+ end
89
+ @current_cmd, @current_topic = find_cmd(cmd)
90
+ if !@current_cmd
91
+ Clitopic::Helpers.error([ "`#{cmd}` is not a command.",
92
+ Clitopic::Helpers.display_suggestion(cmd, all_commands),
93
+ "See `help` for a list of available commands."
94
+ ].compact.join("\n\n"))
95
+ end
96
+ prepare_run(@current_cmd, arguments)
97
+ if @current_cmd.options[:load_defaults] != true && Clitopic.load_defaults?
98
+ @current_cmd.load_defaults
99
+ end
100
+ @current_cmd.call
101
+ end
102
+
103
+ def find_cmd(command)
104
+ cmd_name, sub_cmd_name = command.split(':')
105
+ if global_commands.has_key?(command)
106
+ current_cmd = global_commands[cmd_name]
107
+ elsif !Topics[cmd_name].nil?
108
+ sub_cmd_name = 'index' if sub_cmd_name.nil?
109
+ current_topic = Topics[cmd_name]
110
+ current_cmd = current_topic.commands[sub_cmd_name]
111
+ else
112
+ current_cmd = global_commands[:help]
113
+ end
114
+ return current_cmd, current_topic
115
+ end
116
+ end
117
+
118
+ class << self
119
+ include ClassMethods
120
+ end
121
+ end
122
+
123
+ end