Skip to content

Instantly share code, notes, and snippets.

@danielsdeleo
Created December 16, 2011 22:13
Show Gist options
  • Save danielsdeleo/1488254 to your computer and use it in GitHub Desktop.
Save danielsdeleo/1488254 to your computer and use it in GitHub Desktop.
A wrapper for Chef's `knife` that saves you typing
#!/usr/bin/env ruby
#= k
#== Requirements
# I install Chef with rubygems. May/may not work with other install methods.
#
#== INSTALL
# copy this to ~/bin/k
#
#== USE
# Type `k` instead of `knife`
#
# You can abbreviate commands using the first letter of each command, or a
# fuzzy matching substring from each word. The only restriction is that you
# have to join several "sub-words" with a hyphen. For example:
# * knife cookbook upload => k cb-up
# * knife node list => k nl
# * knife role from file => k rff
# * knife status => k st
#
#== Disclaimer
# This code is a collection of complete hacks. Any updates to core knife are probably going to break it.
require 'rubygems'
require 'chef/application/knife'
module K
class CommandSelector
attr_reader :cmd_in
attr_reader :possibilities
def initialize(cmd_in, possibilities)
@cmd_in, @possibilities = cmd_in, possibilities
end
def pick
first_letters = cmd_in.map {|word| word[0]}
possible_matches = []
possibilities.each do |cmd|
# using the first letters grouped together?
# "rff" => "role from file"
# or, using a few letters from each command?
# "cb up" => "cookbook upload"
next unless cmd_in.size == 1 or cmd_in.size == cmd.size
first_letter_match = true
first_letters.each_with_index do |letter, word_index|
if word = cmd[word_index]
first_letter_match &&= (word[0] == letter)
end
end
next unless first_letter_match
# try to match each letter in the cmd to the first letter of a command word
if cmd_in.size == 1
reject = false
0.upto(cmd_in[0].size) do |i|
if word = cmd[i]
reject ||= (cmd_in[0][i] != word[0])
end
end
next if reject
end
reject = false
# make sure that every letter in cmd_in exists in the candidate command
if cmd_in.size == 1
#could have "st" => "status" or "rff" => "role from file"
cmd_in[0].each_char do |c|
reject ||= (!cmd.any? {|word| word.index(c)})
end
else
cmd_in.each_with_index do |abbrev, i|
abbrev.each_char do |c|
reject ||= (!cmd[i].index(c))
end
end
end
next if reject
possible_matches << cmd
end
possible_matches
end
end
class App < Chef::Application::Knife
def self.options=(new_opts)
superclass.options = new_opts
end
def self.options
superclass.options
end
def run
Mixlib::Log::Formatter.show_time = false
validate_and_parse_options
quiet_traps
Controller.run(ARGV, options)
exit 0
end
end
class Controller < Chef::Knife
# This will hide us from help output.
category 'deprecated'
def self.subcommand_class_from(args)
# BEGIN COPYPASTA :(
command_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ }
subcommand_class = nil
while ( !subcommand_class ) && ( !command_words.empty? )
snake_case_class_name = command_words.join("_")
unless subcommand_class = subcommands[snake_case_class_name]
command_words.pop
end
end
# see if we got the command as e.g., knife node-list
subcommand_class ||= subcommands[args.first.gsub('-', '_')]
# END COPYPASTA
subcommand_class ||= begin
picker = CommandSelector.new(args.first.split('-'), subcommands.keys.map {|cmd| cmd.split('_')} )
matching_cmds = picker.pick
#puts "found match(es) #{matching_cmds.map {|c| c.join(' ')}.inspect} for args #{args}"
if matching_cmds.size > 1
cmd_normal_names = matching_cmds.map {|cmd| cmd.join(' ')}
ui.fatal "Several commands matched '#{args}':\n#{matching_cmds.join("\n")}"
exit 11
elsif matching_cmds.empty?
nil
else
# Trick knife into thinking the original arguments were what it normally wants.
#puts "old arg list: #{args.inspect}"
args.delete_at(0)
args.replace(matching_cmds.first + args)
#puts "new arg list: #{args.inspect}"
subcommands[matching_cmds.first.join('_')]
end
end
# THIS LINE ALSO COPYPASTA
subcommand_class || subcommand_not_found!(args)
end
end
end
K::App.new.run
__END__
COMMANDS_AS_ARRAYS = [["bootstrap"],
["client", "bulk", "delete"],
["client", "create"],
["client", "delete"],
["client", "edit"],
["client", "list"],
["client", "reregister"],
["client", "show"],
["configure"],
["configure", "client"],
["cookbook", "bulk", "delete"],
["cookbook", "create"],
["cookbook", "delete"],
["cookbook", "download"],
["cookbook", "list"],
["cookbook", "metadata"],
["cookbook", "metadata", "from", "file"],
["cookbook", "show"],
["cookbook", "site", "download"],
["cookbook", "site", "install"],
["cookbook", "site", "list"],
["cookbook", "site", "search"],
["cookbook", "site", "share"],
["cookbook", "site", "show"],
["cookbook", "site", "unshare"],
["cookbook", "site", "vendor"],
["cookbook", "test"],
["cookbook", "upload"],
["data", "bag", "create"],
["data", "bag", "delete"],
["data", "bag", "edit"],
["data", "bag", "from", "file"],
["data", "bag", "list"],
["data", "bag", "show"],
["environment", "create"],
["environment", "delete"],
["environment", "edit"],
["environment", "from", "file"],
["environment", "list"],
["environment", "show"],
["exec"],
["help"],
["index", "rebuild"],
["node", "bulk", "delete"],
["node", "create"],
["node", "delete"],
["node", "edit"],
["node", "from", "file"],
["node", "list"],
["node", "run", "list", "add"],
["node", "run", "list", "remove"],
["node", "show"],
["recipe", "list"],
["role", "bulk", "delete"],
["role", "create"],
["role", "delete"],
["role", "edit"],
["role", "from", "file"],
["role", "list"],
["role", "show"],
["search"],
["ssh"],
["status"],
["tag", "create"],
["tag", "delete"],
["tag", "list"],
["ec2", "instance", "data"],
["ec2", "server", "create"],
["ec2", "server", "delete"],
["ec2", "server", "list"],
["deploy"],
["edit", "env"],
["grep"],
["push", "env"],
["set", "rev"],
["show", "rev"]]
TEST_CASES = {
%w[cb up] => %w[cookbook upload],
%w[rff] => %w[role from file],
%w[ne] => %w[node edit],
%w[st] => %w[status],
%w[dbff] => %w[data bag from file]
}
#require 'pp'
#TEST_CASES.each do |input, expected|
#pp input => K::CommandSelector.new(input).pick
#end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment