How to call methods dynamically based on their name?

82,078

Solution 1

What you want to do is called dynamic dispatch. It’s very easy in Ruby, just use public_send:

method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name

If the method is private/protected, use send instead, but prefer public_send.

This is a potential security risk if the value of method_name comes from the user. To prevent vulnerabilities, you should validate which methods can be actually called. For example:

if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
  obj.send(method_name)
end

Solution 2

There are multiple ways to accomplish dynamic dispatch in Ruby, each with their own advantages and disadvantages. Care should be taken to select the most appropriate method for the situation.

The following table breaks down some of the more common techniques:

+---------------+-----------------+-----------------+------------+------------+
|    Method     | Arbitrary Code? | Access Private? | Dangerous? | Fastest On |
+---------------+-----------------+-----------------+------------+------------+
| eval          | Yes             | No              | Yes        | TBD        |
| instance_eval | Yes             | No              | Yes        | TBD        |
| send          | No              | Yes             | Yes        | TBD        |
| public_send   | No              | No              | Yes        | TBD        |
| method        | No              | Yes             | Yes        | TBD        |
+---------------+-----------------+-----------------+------------+------------+

Arbitrary Code

Some techniques are limited to calling methods only, while others can execute basically anything. Methods that allow execution of arbitrary code should be used with extreme caution, if not avoided altogether.

Access Private

Some techniques are limited to calling public methods only, while others can call both public and private methods. Ideally, you should strive to use the method with the least amount of visibility that fulfills your requirements.

Note: If a technique can execute arbitrary code, it can easily be used to access private methods that it might not otherwise have access to.

Dangerous

Just because a technique can not execute arbitrary code or call a private method does not mean that it is safe, particularly if you are using user-provided values. Delete is a public method.

Fastest on

Some of these techniques may be more performant than others, depending on your Ruby version. Benchmarks to follow....


Examples

class MyClass
  def foo(*args); end

  private

  def bar(*args); end
end

obj = MyClass.new

eval

eval('obj.foo') #=> nil
eval('obj.bar') #=> NoMethodError: private method `bar' called

# With arguments:
eval('obj.foo(:arg1, :arg2)') #=> nil
eval('obj.bar(:arg1, :arg2)') #=> NoMethodError: private method `bar' called

instance_eval

obj.instance_eval('foo') #=> nil 
obj.instance_eval('bar') #=> nil 

# With arguments:
obj.instance_eval('foo(:arg1, :arg2)') #=> nil 
obj.instance_eval('bar(:arg1, :arg2)') #=> nil 

send

obj.send('foo') #=> nil 
obj.send('bar') #=> nil 

# With arguments:
obj.send('foo', :arg1, :arg2) #=> nil 
obj.send('bar', :arg1, :arg2) #=> nil 

public_send

obj.public_send('foo') #=> nil 
obj.public_send('bar') #=> NoMethodError: private method `bar' called

# With arguments:
obj.public_send('foo', :arg1, :arg2) #=> nil 
obj.public_send('bar', :arg1, :arg2) #=> NoMethodError: private method `bar' called

method

obj.method('foo').call #=> nil 
obj.method('bar').call #=> nil

# With arguments:
obj.method('foo').call(:arg1, :arg2) #=> nil 
obj.method('bar').call(:arg1, :arg2) #=> nil

Solution 3

You are really going to want to be careful with this. Using user data to call any method via send could leave room open for users to execute any method they want. send is often used to call method names dynamically—but make sure the input values are trusted and can't be manipulated by users.

Golden rule is never trust any input that comes from the user.

Solution 4

Use send to call a method dynamically:

obj.send(str)

Solution 5

You can check the method availability using respond_to?. If it's available then you call send. For example:

if obj.respond_to?(str)
  obj.send(str)
end
Share:
82,078

Related videos on Youtube

user502052
Author by

user502052

Updated on July 08, 2022

Comments

  • user502052
    user502052 almost 2 years

    How can I call a method dynamically when its name is contained in a string variable? For example:

    class MyClass
      def foo; end
      def bar; end
    end
    
    obj = MyClass.new
    str = get_data_from_user  # e.g. `gets`, `params`, DB access, etc.
    str  #=> "foo"
    # somehow call `foo` on `obj` using the value in `str`.
    

    How can I do this? Is doing so a security risk?

    • Gareth
      Gareth over 13 years
      This sounds like a code smell. It sounds like those methods could be refactored so you don't have to go down this confusing path
  • Matt Schuchard
    Matt Schuchard almost 8 years
    I have a good bit of interest in this answer. I don't care a huge amount about the benchmarks, but I do want to know if you can assume from the above description that public_send is the least dangerous of these possibilities.
  • Brad Werth
    Brad Werth almost 8 years
    @MattSchuchard You could probably make that argument, but it is still not really safe if the method is coming from unvalidated user input. If it is coming from user input it really should be checked against a whitelist of allowed methods, unless you are ok with a user supplying "delete_all" or something similar...
  • philomory
    philomory over 7 years
    It is especially worth noting that, if the user is supplying both the method names and the arguments, then public_send can be used to call send, which can in turn be used to call eval, or system: obj.public_send('send','system','rm','-r','-f','/') will ruin your day.
  • Rudi Strydom
    Rudi Strydom over 6 years
    Great explanations, thank you @BradWerth
  • roxdurazo
    roxdurazo almost 6 years
    "Golden rule is never trust any input that comes from the user." taking notes!