3
  • OS: Debian GNU/Linux 8
  • Ruby version: 2.4.1
  • Rails version: 5.1.4

I just used rails new to create a brand new Rails project. In its directory, with irb, I tried to execute a shell command for an executable that did not exist, and an exception was thrown:

irb(main):001:0> `foobar`
Errno::ENOENT: No such file or directory - foobar
    from (irb):1:in ``'
    from (irb):1
    from /home/jackson/.rbenv/versions/2.4.1/bin/irb:11:in `<top (required)>'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli/exec.rb:74:in `load'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli/exec.rb:74:in `kernel_load'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli/exec.rb:27:in `run'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli.rb:335:in `exec'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/vendor/thor/lib/thor.rb:359:in `dispatch'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli.rb:20:in `dispatch'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/vendor/thor/lib/thor/base.rb:440:in `start'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/cli.rb:11:in `start'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/exe/bundle:32:in `block in <top (required)>'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/lib/bundler/friendly_errors.rb:121:in `with_friendly_errors'
    from /home/jackson/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/bundler-1.14.6/exe/bundle:24:in `<top (required)>'
    from /home/jackson/.rbenv/versions/2.4.1/bin/bundle:22:in `load'
    from /home/jackson/.rbenv/versions/2.4.1/bin/bundle:22:in `<main>'

Yet when I try to do the same thing in my Rails environment with rails c, an exception is not thrown. The backticks return nil instead:

irb(main):001:0> `foobar`
rails_console: No such file or directory - foobar
=> nil

This inconsistency seems to be reflected beyond the Rails console. Backticks also return nil in my Rails application. This is an issue because error handling for missing commands in one of our gems is broken because it doesn't thrown an exception: https://github.com/dstil/localtunnel/blob/87b1e4b98f600c2a767654caf0a2d94fef5be0e5/lib/localtunnel/client.rb#L37-L41

Is this difference in behavior intentional? If not, what's wrong, and how can I fix this?

Jackson
  • 9,188
  • 6
  • 52
  • 77

1 Answers1

6

Your problem seems to be that Rails overrides backticks in an attempt to standardize behavior:

class Object
  # Makes backticks behave (somewhat more) similarly on all platforms.
  # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
  # spawned shell prints a message to stderr and sets $?. We emulate
  # Unix on the former but not the latter.
  def `(command) #:nodoc:
    super
  rescue Errno::ENOENT => e
    STDERR.puts "#$0: #{e}"
  end
end

There's blame to go around:

  • Rails for replacing a core method.
  • The gem for using backticks rather than something safer and cleaner such as Open3.
  • Ruby for not specifying how Kernel#` is supposed to behave (and possibly even having backticks if you're opinionated enough).

There are ways around this. You could try to remove the ActiveSupport override in an initializer:

Object.send(:remove_method, :'`')

so that the standard backticks from Kernel will be used. Or if you never expect to have an lt command you could replace the gem's method:

module LocalTunnel
  class Client
    def self.package_installed?
      false
    end
  end
end

Or maybe replace LocalTunnel::Client.package_installed? with your own implementation that works around the Rails override by checking the various ways that backticks can fail or uses the methods in Open3 instead of backticks. You could also manually look for lv yourself by breaking up the PATH environment variable (using File::PATH_SEPARATOR) and File.executable?.

Which kludge you use depends on what portability requirements you have and what works in your environments.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • 1
    It sounds to me like I should just throw backticks into the garbage from here on out. Do you have a suggestion on how I might generally replace backticks with "something safer and cleaner such as `Open3`", preferably with a synchronous code example? – Jackson Nov 07 '17 at 22:25
  • 1
    I tend to stay away from backticks, the one argument version of `system`, and anything else that will invoke a shell, far too fragile and error prone to use properly. Instead of `%x{some_command -arg}` you'd probably use `Open3.capture2('some_command', '-arg')` instead (http://ruby-doc.org/stdlib-2.4.2/libdoc/open3/rdoc/Open3.html#method-c-capture2). – mu is too short Nov 07 '17 at 23:06