cli-topic 0.9.1

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