Skip to content

Commit

Permalink
Automatically create aliases for unambiguous substrings of commands
Browse files Browse the repository at this point in the history
Inspired by the code in RubyGems, this code goes beyond that basic case
to correctly resolve ambiguous aliases that map to the same command.

Closes rails#158.
Closes rails#160.
Closes rails#162.
  • Loading branch information
sferik committed Aug 28, 2011
1 parent 1fbea01 commit a0e3e20
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 6 deletions.
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2008 Yehuda Katz
Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
43 changes: 38 additions & 5 deletions lib/thor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,52 @@ def initialize_added #:nodoc:
# Retrieve the task name from given args.
def retrieve_task_name(args) #:nodoc:
meth = args.first.to_s unless args.empty?

if meth && (map[meth] || meth !~ /^\-/)
args.shift
else
nil
end
end

# Receives a task name (can be nil), and try to get a map from it.
# If a map can't be found use the sent name or the default task.
# receives a (possibly nil) task name and returns a name that is in
# the tasks hash. In addition to normalizing aliases, this logic
# will determine if a shortened command is an unambiguous prefix of
# a task or alias.
#
# +normalize_task_name+ also converts names like +animal-prison+
# into +animal_prison+.
def normalize_task_name(meth) #:nodoc:
meth = map[meth.to_s] || meth || default_task
meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
return default_task.to_s.gsub('-', '_') unless meth

possibilities = find_task_possibilities(meth)
if possibilities.size > 1
raise ArgumentError, "Ambiguous task #{meth} matches [#{possibilities.join(', ')}]"
elsif possibilities.size < 1
meth = meth || default_task
elsif map[meth]
meth = map[meth]
else
meth = possibilities.first
end

meth.to_s.gsub('-','_') # treat foo-bar as foo_bar
end

# this is the logic that takes the task name passed in by the user
# and determines whether it is an unambiguous prefix of a task or
# alias name.
def find_task_possibilities(meth)
len = meth.length
possibilities = all_tasks.merge(map).keys.select { |n| meth == n[0, len] }.sort
unique_possibilities = possibilities.map { |k| map[k] || k }.uniq

if possibilities.include?(meth)
[meth]
elsif unique_possibilities.size == 1
unique_possibilities
else
possibilities
end
end

def subcommand_help(cmd)
Expand Down
6 changes: 6 additions & 0 deletions spec/fixtures/script.thor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class MyScript < Thor

map "-T" => :animal, ["-f", "--foo"] => :foo

map "animal_prison" => "zoo"

desc "zoo", "zoo around"
def zoo
true
Expand All @@ -26,11 +28,15 @@ class MyScript < Thor
[type]
end

map "hid" => "hidden"

desc "hidden TYPE", "this is hidden", :hide => true
def hidden(type)
[type]
end

map "fu" => "zoo"

desc "foo BAR", <<END
do some fooing
This is more info!
Expand Down
25 changes: 25 additions & 0 deletions spec/thor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,31 @@
it "raises when an exception happens within the task call" do
lambda { MyScript.start(["call_myself_with_wrong_arity"]) }.should raise_error(ArgumentError)
end

context "when the user enters an unambiguous substring of a command" do
it "should invoke a command" do
MyScript.start(["z"]).should == MyScript.start(["zoo"])
end

it "should invoke a command, even when there's an alias the resolves to the same command" do
MyScript.start(["hi"]).should == MyScript.start(["hidden"])
end

it "should invoke an alias" do
MyScript.start(["animal_pri"]).should == MyScript.start(["zoo"])
end
end

context "when the user enters an ambiguous substring of a command" do
it "should raise an exception that explains the ambiguity" do
lambda { MyScript.start(["call"]) }.should raise_error(ArgumentError, 'Ambiguous task call matches [call_myself_with_wrong_arity, call_unexistent_method]')
end

it "should raise an exception when there is an alias" do
lambda { MyScript.start(["f"]) }.should raise_error(ArgumentError, 'Ambiguous task f matches [foo, fu]')
end
end

end

describe "#subcommand" do
Expand Down

0 comments on commit a0e3e20

Please sign in to comment.