#-- # Cloud Foundry # Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved. # # This product is licensed to you under the Apache License, Version 2.0 (the "License"). # You may not use this product except in compliance with the License. # # This product includes a number of subcomponents with # separate copyright notices and license terms. Your use of these # subcomponents is subject to the terms and conditions of the # subcomponent's license, as noted in the LICENSE file. #++ require 'highline' require 'optparse' require 'json/pure' require 'io/console' module CF; module UAA end end module CF::UAA class Topic class << self; attr_reader :synonyms end def self.option_defs ; @option_defs || {} end def self.commands; @commands || {} end def self.topic(*args) return @description if args.empty? @synonyms = (args[0].split(' ') + args[1..-1]).map(&:downcase) @description = args[0] end def self.define_option(key, *args) @option_defs ||= {} raise "conflicting option definition for #{key}" if @option_defs.key?(key) && @option_defs[key] != args @option_defs[key] = args end def self.desc(template, desc, *options, &handler) parts, argc = template.split(' '), 0 cmd = parts.each_with_object([]) { |p, o| if p =~ /^\[/ argc = parts[-1] =~ /\.\.\.\]$/ ? -1 : parts.length - o.length break o end o << p } cmd_key = cmd.join('_').to_sym define_method(cmd_key, handler) @commands ||= {} @commands[cmd_key] = {parts: cmd, argc: argc, template: template, desc: desc, options: options} end def initialize(cli_class, options = {}, input = $stdin, output = $stdout) @cli_class, @options, @input, @output = cli_class, options, input, output @highline = HighLine.new(input, output) end def ask(prompt) @highline.ask("#{prompt}: ") end def ask_pwd(prompt) @highline.ask("#{prompt}: ") { |q| q.echo = '*' } end def say(msg) @output.puts(msg); msg end def gripe(msg) @output.puts(msg) end def opts; @options end def terminal_columns return @terminal_columns ||= 0 if @terminal_columns || !@output.tty? cols = IO.console.winsize.last rescue 0 if $stdin.tty? @terminal_columns = !cols || cols < 40 ? 0 : cols end def help_col_start return @help_col_start ||= 35 if @help_col_start || terminal_columns == 0 || terminal_columns > 80 @help_col_start = terminal_columns / 2 end def pp(obj, indent = 0, wrap = terminal_columns, label = nil) case obj when Array if obj.empty? || !obj[0].is_a?(Hash) && !obj[0].is_a?(Array) say_definition(indent, ("#{label}: " if label), Util.strlist(obj), nil, wrap) else say_definition(indent, "#{label}: ", nil, nil, wrap) if label obj.each {|o| pp o, indent, wrap, '-' } end when Hash say_definition(indent, label, nil, nil, wrap) if label obj.each {|k, v| pp v, indent + 2, wrap, k.to_s} when nil else say_definition(indent, ("#{label}: " if label), obj.to_s, nil, wrap) end obj end def say_definition(indent, term, text = nil, start = help_col_start, wrap = terminal_columns) cur = indent + (term ? term.length : 0) indent < 1 ? @output.printf("%s", term) : @output.printf("%*c%s", indent, ' ', term) if start.nil? start = 2 if (start = indent + 4) > wrap else start = 2 if start > wrap if cur < start @output.printf("%*c", start - cur, ' ') elsif cur > start @output.printf("\n%*c", start, ' ') end cur = start end return @output.printf("\n") unless text && !text.empty? text = text.dup text.each_line do |line| width = wrap == 0 ? 4096 : wrap - cur line = line.chomp while line.length > width i = line.rindex(' ', width) || width @output.printf("%s\n%*c", line[0..i - 1], start, ' ') width = wrap == 0 ? 4096 : wrap - start line = line[i..-1].strip end @output.printf("%s\n", line) cur = start end nil end def opt_help(key, args) raise "missing option definition for #{key}" unless args long = short = desc = nil args.each do |a| case a when /^-.$/ then short = a when /^--.*/ then long = a else desc = a end end raise "option definition must include long form (--#{key})" unless long [ short ? "#{short} | #{long}" : "#{long}", desc] end def opt_strs(opts) opts.each_with_object([]) { |o, a| @cli_class.option_defs[o].each { |d| case d when /^--\[no-\](\S+)/ then a << "--#{$1} --no-#{$1}" when /^--(\S+)/ then a << "--#{$1}" end } }.join(' ') end def say_cmd_helper(info, topic, suffix = nil) say_definition 2, info[:template], info[:desc] info[:options].each do |o| odef, desc = opt_help(o, topic.option_defs[o] ? topic.option_defs[o]: @cli_class.option_defs[o]) say_definition help_col_start, "", desc ? "#{odef}, #{desc}" : odef end @output.print suffix end def say_command_help(args) say "" @cli_class.topics.each do |tpc| tpc.commands.each do |k, v| if args[0..v[:parts].length - 1] == v[:parts] say_cmd_helper(v, tpc, "\n") return "help command" end end end args = args.map(&:downcase) @cli_class.topics.each { |tpc| return say_help(tpc) unless (args & tpc.synonyms).empty? } gripe "No command or topic found to match: #{args.join(' ')}\n" end def say_help(topic = nil) @output.print "\n#{@cli_class.overview}\n" unless topic @cli_class.topics.each do |tpc| next if topic && topic != tpc @output.print "\n#{tpc.topic}\n" tpc.commands.each { |k, v| say_cmd_helper v, tpc } end if topic || !@cli_class.global_options @output.print("\n") return topic ? "help topic" : "help" end @output.print "\nGlobal options:\n" @cli_class.global_options.each do |o| odef, desc = opt_help(o, @cli_class.option_defs[o]) say_definition 2, odef, desc end @output.print("\n") "help" end def add_command(branches, parts, opts = nil) if parts.empty? return if opts.nil? || opts.empty? return branches << {label: opt_strs(opts)} end if i = branches.find_index { |b| parts[0] == b[:label] } parts.shift else branches << {label: parts.shift, sub: []} i = -1 end add_command(branches[i][:sub], parts, opts) end def print_tree(branches, indent) return unless branches branches.each do |b| indent.times { @output.print "\t" }; @output.puts b[:label] print_tree b[:sub], indent + 1 end end def say_commands tree = {label: File.basename($0), sub: []} @cli_class.topics.each {|t| t.commands.each {|k, v| add_command(tree[:sub], v[:parts].dup, v[:options])}} add_command(tree[:sub], [], @cli_class.global_options) @output.puts tree[:label] print_tree(tree[:sub], 1) "help commands" end end class BaseCli class << self attr_writer :input attr_reader :input, :output, :option_defs attr_accessor :overview, :topics, :global_options end def self.preprocess_options(args, opts) end # to be implemented in subclass def self.handle_bad_command(args, msg) end # to be implemented in subclass def self.run(args = ARGV) @input ||= $stdin @output ||= $stdout @output.string = "" if @output.respond_to?(:string) args = Shellwords.split(args) if args.respond_to?(:split) @option_defs ||= {} orig = args @parser = OptionParser.new opts = @topics.each_with_object({}) do |tpc, o| tpc.option_defs.each do |k, optdef| @parser.on(*optdef) do |v| o[k] = (o[k] ? Array(o[k]).push(v) : v ) end @option_defs[k] = optdef end end @parser.parse! args preprocess_options(args, opts) @topics.each do |tpc| tpc.commands.each do |k, v| next unless args[0..v[:parts].length - 1] == v[:parts] args = args[v[:parts].length..-1] if v[:argc] == -1 # variable args, leave args alone elsif args.length > v[:argc] return handle_bad_command(orig, "Too many command line parameters given.") elsif args.length < v[:argc] (v[:argc] - args.length).times { args << nil } end opts.keys.each do |o| next if v[:options].include?(o) || @global_options.include?(o) return handle_bad_command(orig, "Invalid option: #{o}") end return tpc.new(self, opts, @input, @output).send(k, *args) end end @output.puts "#{File.basename($0)}: subcommand not found" rescue Exception => e @output.puts "#{File.basename($0)} error", "#{e.class}: #{e.message}", (e.backtrace if opts[:trace]) ensure puts @output.string if opts[:trace] && @print_on_trace && @output.respond_to?(:string) end end end