"which in ruby": Checking if program exists in $PATH from ruby

my scripts rely heavily on external programs and scripts. I need to be sure that a program I need to call exists. Manually, I'd check this using 'which' in the commandline.

Is there an equivalent to File.exists? for things in $PATH?

(yes I guess I could parse %x[which scriptINeedToRun] but that's not super elegant.

Thanks! yannick


UPDATE: Here's the solution I retained:

 def command?(command)
system("which #{ command} > /dev/null 2>&1")
end

UPDATE 2: A few new answers have come in - at least some of these offer better solutions.

Update 3: The ptools gem has adds a "which" method to the File class.

27289 次浏览

You can access system environment variables with the ENV hash:

puts ENV['PATH']

It will return the PATH on your system. So if you want to know if program nmap exists, you can do this:

ENV['PATH'].split(':').each {|folder| puts File.exists?(folder+'/nmap')}

This will print true if file was found or false otherwise.

Not so much elegant but it works :).

def cmdExists?(c)
system(c + " > /dev/null")
return false if $?.exitstatus == 127
true
end

Warning: This is NOT recommended, dangerous advice!

This is a tweak of rogeriopvl's answer, making it cross platform:

require 'rbconfig'


def is_windows?
Config::CONFIG["host_os"] =~ /mswin|mingw/
end


def exists_in_path?(file)
entries = ENV['PATH'].split(is_windows? ? ";" : ":")
entries.any? {|f| File.exists?("#{f}/#{file}")}
end
def command?(name)
`which #{name}`
$?.success?
end

Initially taken from hub, which used type -t instead of which though (and which failed for both zsh and bash for me).

Here's what I'm using. This is platform neutral (File::PATH_SEPARATOR is ":" on Unix and ";" on Windows), only looks for program files that actually are executable by the effective user of the current process, and terminates as soon as the program is found:

##
# Returns +true+ if the +program+ executable is found in the user's path.
def has_program?(program)
ENV['PATH'].split(File::PATH_SEPARATOR).any? do |directory|
File.executable?(File.join(directory, program.to_s))
end
end

I'd like to add that which takes the flag -s for silent mode, which only sets the success flag, removing the need for redirecting the output.

True cross-platform solution, works properly on Windows:

# Cross-platform way of finding an executable in the $PATH.
#
#   which('ruby') #=> /usr/bin/ruby
def which(cmd)
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
exts.each do |ext|
exe = File.join(path, "#{cmd}#{ext}")
return exe if File.executable?(exe) && !File.directory?(exe)
end
end
nil
end

This doesn't use host OS sniffing, and respects $PATHEXT which lists valid file extensions for executables on Windows.

Shelling out to which works on many systems but not all.

Solution based on rogeriovl, but complete function with execution test rather than existence test.

def command_exists?(command)
ENV['PATH'].split(':').each {|folder| File.executable?(File.join(folder, command))}
end

Will work only for UNIX (Windows does not use colon as a separator)

On linux I use:

exists = `which #{command}`.size.>(0)

Unfortunately, which is not a POSIX command and so behaves differently on Mac, BSD, etc (i.e., throws an error if the command is not found). Maybe the ideal solution would be to use

`command -v #{command}`.size.>(0)  # fails!: ruby can't access built-in functions

But this fails because ruby seems to not be capable of accessing built-in functions. But command -v would be the POSIX way to do this.

Use find_executable method from mkmf which is included to stdlib.

require 'mkmf'


find_executable 'ruby'
#=> "/Users/narkoz/.rvm/rubies/ruby-2.0.0-p0/bin/ruby"


find_executable 'which-ruby'
#=> nil

This is an improved version based on @mislav's answer. This would allow any type of path input and strictly follows how cmd.exe chooses the file to execute in Windows.

# which(cmd) :: string or nil
#
# Multi-platform implementation of "which".
# It may be used with UNIX-based and DOS-based platforms.
#
# The argument can not only be a simple command name but also a command path
# may it be relative or complete.
#
def which(cmd)
raise ArgumentError.new("Argument not a string: #{cmd.inspect}") unless cmd.is_a?(String)
return nil if cmd.empty?
case RbConfig::CONFIG['host_os']
when /cygwin/
exts = nil
when /dos|mswin|^win|mingw|msys/
pathext = ENV['PATHEXT']
exts = pathext ? pathext.split(';').select{ |e| e[0] == '.' } : ['.com', '.exe', '.bat']
else
exts = nil
end
if cmd[File::SEPARATOR] or (File::ALT_SEPARATOR and cmd[File::ALT_SEPARATOR])
if exts
ext = File.extname(cmd)
if not ext.empty? and exts.any?{ |e| e.casecmp(ext).zero? } \
and File.file?(cmd) and File.executable?(cmd)
return File.absolute_path(cmd)
end
exts.each do |ext|
exe = "#{cmd}#{ext}"
return File.absolute_path(exe) if File.file?(exe) and File.executable?(exe)
end
else
return File.absolute_path(cmd) if File.file?(cmd) and File.executable?(cmd)
end
else
paths = ENV['PATH']
paths = paths ? paths.split(File::PATH_SEPARATOR).select{ |e| File.directory?(e) } : []
if exts
ext = File.extname(cmd)
has_valid_ext = (not ext.empty? and exts.any?{ |e| e.casecmp(ext).zero? })
paths.unshift('.').each do |path|
if has_valid_ext
exe = File.join(path, "#{cmd}")
return File.absolute_path(exe) if File.file?(exe) and File.executable?(exe)
end
exts.each do |ext|
exe = File.join(path, "#{cmd}#{ext}")
return File.absolute_path(exe) if File.file?(exe) and File.executable?(exe)
end
end
else
paths.each do |path|
exe = File.join(path, cmd)
return File.absolute_path(exe) if File.file?(exe) and File.executable?(exe)
end
end
end
nil
end

Use MakeMakefile#find_executable0 with Logging Disabled

There are a number of good answers already, but here's what I use:

require 'mkmf'


def set_mkmf_log(logfile=File::NULL)
MakeMakefile::Logging.instance_variable_set(:@logfile, logfile)
end


# Return path to cmd as a String, or nil if not found.
def which(cmd)
old_mkmf_log = MakeMakefile::Logging.instance_variable_get(:@logfile)
set_mkmf_log(nil)
path_to_cmd = find_executable0(cmd)
set_mkmf_log(old_mkmf_log)
path_to_cmd
end

This uses the undocumented #find_executable0 method invoked by MakeMakefile#find_executable to return the path without cluttering standard output. The #which method also temporarily redirects the mkmf logfile to /dev/null to prevent cluttering the current working directory with "mkmf.log" or similar.

for jruby, any of the solutions that depend on mkmf may not work, as it has a C extension.

for jruby, the following is an easy way to check if something is executable on the path:

main » unix_process = java.lang.Runtime.getRuntime().exec("git status")
=> #<Java::JavaLang::UNIXProcess:0x64fa1a79>
main » unix_process.exitValue()
=> 0
main »

if the executable isn't there, it will raise a runtime error, so you may want to do this in a try/catch block in your actual usage.

#####################################################
# add methods to see if there's an executable that's executable
#####################################################
class File
class << self
###########################################
# exists and executable
###########################################
def cmd_executable?(cmd)
!ENV['PATH'].split(':').select { |f| executable?(join(f, cmd[/^[^ \n\r]*/])) }.empty?
end
end
end

I have this:

def command?(name)
[name,
*ENV['PATH'].split(File::PATH_SEPARATOR).map {|p| File.join(p, name)}
].find {|f| File.executable?(f)}
end

works for full paths as well as commands:

irb(main):043:0> command?("/bin/bash")
=> "/bin/bash"
irb(main):044:0> command?("bash")
=> "/bin/bash"
irb(main):006:0> command?("bush")
=> nil