125

New to Ruby and ROR and loving it each day, so here is my question since I have not idea how to google it (and I have tried :) )

we have method

def foo(first_name, last_name, age, sex, is_plumber)
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{SOMETHING}"    
end

So what I am looking for way to get all arguments passed to method, without listing each one. Since this is Ruby I assume there is a way :) if it was java I would just list them :)

Output would be:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Dan McClain
  • 11,780
  • 9
  • 47
  • 67
Haris Krajina
  • 14,824
  • 12
  • 64
  • 81
  • 1
    Reha kralj svegami! – ant Jul 31 '16 at 15:41
  • 1
    I think all of the answers should point out that if "some code" changes the values of the arguments before the argument discovery method is run, it will show the new values, not the values that were passed in. So you should grab them right away to be sure. That said, my favorite one-liner for this (with credit given to the previous answers) is: ```method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }``` – Brian Deterling Mar 28 '19 at 02:41

11 Answers11

183

In Ruby 1.9.2 and later you can use the parameters method on a method to get the list of parameters for that method. This will return a list of pairs indicating the name of the parameter and whether it is required.

e.g.

If you do

def foo(x, y)
end

then

method(:foo).parameters # => [[:req, :x], [:req, :y]]

You can use the special variable __method__ to get the name of the current method. So within a method the names of its parameters can be obtained via

args = method(__method__).parameters.map { |arg| arg[1].to_s }

You could then display the name and value of each parameter with

logger.error "Method failed with " + args.map { |arg| "#{arg} = #{eval arg}" }.join(', ')

Note: since this answer was originally written, in current versions of Ruby eval can no longer be called with a symbol. To address this, an explicit to_s has been added when building the list of parameter names i.e. parameters.map { |arg| arg[1].to_s }

mikej
  • 65,295
  • 17
  • 152
  • 131
  • 4
    I am going to need some time to decipher this :) – Haris Krajina Feb 09 '12 at 14:05
  • 3
    Let me know which bits need deciphering and I'll add some explanation :) – mikej Feb 09 '12 at 14:30
  • Is there any way to show a calling method's details instead? That way this could be put in its own method `divulge`, and used appropriately for debugging. – Peter Ehrlich Oct 03 '12 at 16:34
  • 5
    I tried with Ruby 1.9.3, and you have to do #{eval arg.to_s} to get it to work, otherwise you get a TypeError: can't convert Symbol into String – Javid Jamae Oct 11 '12 at 03:03
  • 5
    Meanwhile, got better and my skills and understand this code now. – Haris Krajina Oct 11 '12 at 10:45
  • 1
    Here's my shortened version: `method(__method__).parameters.map(&:last).map{|p|eval(p.to_s)}.to_s` – d_ethier Jan 12 '14 at 18:14
  • In Ruby 2.3.4 it will fail with `\`eval': no implicit conversion of Symbol into String (TypeError)` – reducing activity May 28 '17 at 12:50
  • 1
    @MateuszKonieczny thanks for the heads up. I've updated the answer with a working version and an explanation of why the previous code no longer works. Cheers! – mikej May 29 '17 at 09:31
  • How to get parameters if that function is inside some class? – Nikhil Wagh Jul 22 '19 at 09:54
  • 1
    @NikhilWagh you can call `method` on an instance of your class, for example: `my_object.method(:some_method).parameters` or if you don't have an instance of the class you can use `instance_method`: `MyClass.instance_method(:some_method).parameters` – mikej Jul 22 '19 at 15:00
  • Yeah, I added the answer after some searching. – Nikhil Wagh Jul 22 '19 at 15:55
65

Since Ruby 2.1 you can use binding.local_variable_get to read value of any local variable, including method parameters (arguments). Thanks to that you can improve the accepted answer to avoid evil eval.

def foo(x, y)
  method(__method__).parameters.map do |_, name|
    binding.local_variable_get(name)
  end
end

foo(1, 2)  # => 1, 2
Community
  • 1
  • 1
Jakub Jirutka
  • 10,269
  • 4
  • 42
  • 35
21

One way to handle this is:

def foo(*args)
    first_name, last_name, age, sex, is_plumber = *args
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{args.inspect}"    
end
Arun Kumar Arjunan
  • 6,827
  • 32
  • 35
  • 2
    Working and will be voted as accepted unless there are better answers, my only problem with this is I don't want to lose method signature, some there there will be Inteli sense and I would hate to lose it. – Haris Krajina Feb 09 '12 at 14:00
9

This is an interesting question. Maybe using local_variables? But there must be a way other than using eval. I'm looking in Kernel doc

class Test
  def method(first, last)
    local_variables.each do |var|
      puts eval var.to_s
    end
  end
end

Test.new().method("aaa", 1) # outputs "aaa", 1
Community
  • 1
  • 1
Raffaele
  • 20,627
  • 6
  • 47
  • 86
  • This is not so bad, why is this evil solution? – Haris Krajina Feb 09 '12 at 14:07
  • It's not bad in this case - using eval() can sometimes lead to security holes. Just I think there may be a better way :) but I admit Google is not our friend in this case – Raffaele Feb 09 '12 at 14:36
  • I am going to go with this, downside is you can not make helper (module) which would take care of this, since as soon as it leaves original method it cannot do evals of local vars. Thanks all for info. – Haris Krajina Feb 09 '12 at 14:45
  • This gives me "TypeError: cannot convert Symbol to String" unless I change it to `eval var.to_s`. Also, a caveat to this is that if you define any local variables *before* running this loop, they will be included in addition to the method parameters. – Andrew Marshall Feb 09 '12 at 15:04
  • 6
    This is not the most elegant and secure approach - if you define local variable inside your method and then call `local_variables`, it will return method arguments + all local variables. This may cause mistakes when your code. – Aliaksei Kliuchnikau Feb 09 '12 at 15:04
  • You need to make var.to_s, I made edit but Raffaeke didn't approve it I guess. With ruby 1.9.3 you need to put var.to_s – Haris Krajina Feb 10 '12 at 10:08
  • I would have approved, but I didn't receive any message. Anyway I'll change it myself, but on 1.8.x it works as is – Raffaele Feb 10 '12 at 13:39
7

If you need arguments as a Hash, and you don't want to pollute method's body with tricky extraction of parameters, use this:

def mymethod(firstarg, kw_arg1:, kw_arg2: :default)
  args = MethodArguments.(binding) # All arguments are in `args` hash now
  ...
end

Just add this class to your project:

class MethodArguments
  def self.call(ext_binding)
    raise ArgumentError, "Binding expected, #{ext_binding.class.name} given" unless ext_binding.is_a?(Binding)
    method_name = ext_binding.eval("__method__")
    ext_binding.receiver.method(method_name).parameters.map do |_, name|
      [name, ext_binding.local_variable_get(name)]
    end.to_h
  end
end
greenback
  • 111
  • 1
  • 3
5

This may be helpful...

  def foo(x, y)
    args(binding)
  end

  def args(callers_binding)
    callers_name = caller[0][/`.*'/][1..-2]
    parameters = method(callers_name).parameters
    parameters.map { |_, arg_name|
      callers_binding.local_variable_get(arg_name)
    }    
  end
Community
  • 1
  • 1
Jon Jagger
  • 728
  • 7
  • 13
  • 1
    Rather than this slightly hacky `callers_name` implementation, you could also pass `__method__` along with the `binding`. – Tom Lord Oct 31 '19 at 11:37
4

If the function is inside some class then you can do something like this:

class Car
  def drive(speed)
  end
end

car = Car.new
method = car.method(:drive)

p method.parameters #=> [[:req, :speed]] 
Nikhil Wagh
  • 1,376
  • 1
  • 24
  • 44
3

You can define a constant such as:

ARGS_TO_HASH = "method(__method__).parameters.map { |arg| arg[1].to_s }.map { |arg| { arg.to_sym => eval(arg) } }.reduce Hash.new, :merge"

And use it in your code like:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
  • 1,729
  • 22
  • 23
2

It seems like what this question is trying to accomplish could be done with a gem I just released, https://github.com/ericbeland/exception_details. It will list local variables and vlaues (and instance variables) from rescued exceptions. Might be worth a look...

ebeland
  • 1,543
  • 10
  • 21
2

If you would change the method signature, you can do something like this:

def foo(*args)
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{args}"    
end

Or:

def foo(opts={})
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{opts.values}"    
end

In this case, interpolated args or opts.values will be an array, but you can join if on comma. Cheers

Simon Bagreev
  • 2,879
  • 1
  • 23
  • 24
1

Before I go further, you're passing too many arguments into foo. It looks like all of those arguments are attributes on a Model, correct? You should really be passing the object itself. End of speech.

You could use a "splat" argument. It shoves everything into an array. It would look like:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L
  • 3,389
  • 1
  • 16
  • 14
  • Disagree on this, method signature is important for readability and re-usability of code. Object itself is fine, but you have to create instance somewhere, so before u call the method or in the method. Better in method in my opinion. e.g. create user method. – Haris Krajina Feb 09 '12 at 14:05
  • To quote a smarter man than I, Bob Martin, in his book, Clean Code, "the ideal number of arguments for a function is zero(niladic). Next comes one (monoadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification - and then shouldn't be used anyway." He goes on to say what I said, many related arguments should be wrapped in a class and passed as an object. It's a good book, I highly recommend it. – Tom L Feb 09 '12 at 15:40
  • Not to put too fine a point on it, but consider this: if you find that you need more/less/different arguments then you'll have broken your API and have to update every call to that method. On the other hand, if you pass an object then other parts of your app (or consumers of your service) can chug merrily along. – Tom L Feb 10 '12 at 15:46
  • I do agree with your points and e.g. in Java I would always enforce your approach. But I think with ROR is different and here is why: – Haris Krajina Feb 12 '12 at 21:28
  • I do agree with your points and e.g. in Java I would always enforce your approach. But I think with ROR is different and here is why: If you want to save ActiveRecord to DB and you have method that saves it you would have to assemble hash before I pass it to save method. For user example we set first, last name, username, etc. and than pass hash to save method which would do something and save it. And here is problem how does every developer know what to put in hash? It's active record so you would have to go see db schema than assemble hash, and be very careful not to miss any symbols. – Haris Krajina Feb 12 '12 at 21:34
  • ... continued my approach is to wrap ActiveRecord assembly in method and give API to developers which they can understand. Not pretty I admit but with ROR your get a lot lose some hehe :) Hope it made sense, cheers – Haris Krajina Feb 12 '12 at 21:35