lib/tty/which.rb
# frozen_string_literal: true
require_relative "which/version"
module TTY
# A class responsible for finding an executable in the PATH
module Which
# Find an executable in a platform independent way
#
# @param [String] cmd
# the command to search for
# @param [Array<String>] paths
# the paths to look through
#
# @example
# which("ruby") # => "/usr/local/bin/ruby"
# which("/usr/local/bin/ruby") # => "/usr/local/bin/ruby"
# which("foo") # => nil
#
# @example
# which("ruby", paths: ["/usr/locale/bin", "/usr/bin", "/bin"])
#
# @return [String, nil]
# the absolute path to executable if found, `nil` otherwise
#
# @api public
def which(cmd, paths: search_paths)
if file_with_path?(cmd)
return cmd if executable_file?(cmd)
extensions.each do |ext|
exe = "#{cmd}#{ext}"
return ::File.absolute_path(exe) if executable_file?(exe)
end
return nil
end
paths.each do |path|
if file_with_exec_ext?(cmd)
exe = ::File.join(path, cmd)
return ::File.absolute_path(exe) if executable_file?(exe)
end
extensions.each do |ext|
exe = ::File.join(path, "#{cmd}#{ext}")
return ::File.absolute_path(exe) if executable_file?(exe)
end
end
nil
end
module_function :which
# Check if executable exists in the path
#
# @param [String] cmd
# the executable to check
#
# @param [Array<String>] paths
# paths to check
#
# @return [Boolean]
#
# @api public
def exist?(cmd, paths: search_paths)
!which(cmd, paths: paths).nil?
end
module_function :exist?
# Find default system paths
#
# @param [String] path
# the path to search through
#
# @example
# search_paths("/usr/local/bin:/bin")
# # => ["/bin"]
#
# @return [Array<String>]
# the array of paths to search
#
# @api private
def search_paths(path = ENV["PATH"])
paths = if path && !path.empty?
path.split(::File::PATH_SEPARATOR)
else
%w[/usr/local/bin /usr/ucb /usr/bin /bin]
end
paths.select(&Dir.method(:exist?))
end
module_function :search_paths
# All possible file extensions
#
# @example
# extensions(".exe;cmd;.bat")
# # => [".exe", ".bat"]
#
# @param [String] path_ext
# a string of semicolon separated filename extensions
#
# @return [Array<String>]
# an array with valid file extensions
#
# @api private
def extensions(path_ext = ENV["PATHEXT"])
return [""] unless path_ext
path_ext.split(::File::PATH_SEPARATOR).select { |part| part.include?(".") }
end
module_function :extensions
# Determines if filename is an executable file
#
# @example Basic usage
# executable_file?("/usr/bin/less") # => true
#
# @example Executable in directory
# executable_file?("less", "/usr/bin") # => true
# executable_file?("less", "/usr") # => false
#
# @param [String] filename
# the path to file
# @param [String] dir
# the directory within which to search for filename
#
# @return [Boolean]
#
# @api private
def executable_file?(filename, dir = nil)
path = ::File.join(dir, filename) if dir
path ||= filename
::File.file?(path) && ::File.executable?(path)
end
module_function :executable_file?
# Check if command itself has executable extension
#
# @param [String] filename
# the path to executable file
#
# @example
# file_with_exec_ext?("file.bat")
# # => true
#
# @return [Boolean]
#
# @api private
def file_with_exec_ext?(filename)
extension = ::File.extname(filename)
return false if extension.empty?
extensions.any? { |ext| extension.casecmp(ext).zero? }
end
module_function :file_with_exec_ext?
# Check if executable file is part of absolute/relative path
#
# @param [String] cmd
# the executable to check
#
# @return [Boolean]
#
# @api private
def file_with_path?(cmd)
::File.expand_path(cmd) == cmd
end
module_function :file_with_path?
end # Which
end # TTY