├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── appveyor.yml ├── lib ├── tty-which.rb └── tty │ ├── which.rb │ └── which │ └── version.rb ├── spec ├── spec_helper.rb └── unit │ ├── executable_file_spec.rb │ ├── exist_spec.rb │ ├── extensions_spec.rb │ ├── file_with_exec_ext_spec.rb │ ├── search_paths_spec.rb │ └── which_spec.rb ├── tasks ├── console.rake ├── coverage.rake └── spec.rake └── tty-which.gemspec /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.rb] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: piotrmurach 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report something not working correctly or as expected 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the problem 10 | 11 | A brief description of the issue. 12 | 13 | ### Steps to reproduce the problem 14 | 15 | ``` 16 | Your code here to reproduce the issue 17 | ``` 18 | 19 | ### Actual behaviour 20 | 21 | What happened? This could be a description, log output, error raised etc. 22 | 23 | ### Expected behaviour 24 | 25 | What did you expect to happen? 26 | 27 | ### Describe your environment 28 | 29 | * OS version: 30 | * Ruby version: 31 | * TTY::Which version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest new functionality 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the problem 10 | 11 | A brief description of the problem you're trying to solve. 12 | 13 | ### How would the new feature work? 14 | 15 | A short explanation of the new feature. 16 | 17 | ``` 18 | Example code that shows possible usage 19 | ``` 20 | 21 | ### Drawbacks 22 | 23 | Can you see any potential drawbacks? 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: TTY Community Discussions 4 | url: https://github.com/piotrmurach/tty/discussions 5 | about: Suggest ideas, ask and answer questions 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe the change 2 | 3 | What does this Pull Request do? 4 | 5 | ### Why are we doing this? 6 | 7 | Any related context as to why is this is a desirable change. 8 | 9 | ### Benefits 10 | 11 | How will the library improve? 12 | 13 | ### Drawbacks 14 | 15 | Possible drawbacks applying this change. 16 | 17 | ### Requirements 18 | 19 | - [ ] Tests written & passing locally? 20 | - [ ] Code style checked? 21 | - [ ] Rebased with `master` branch? 22 | - [ ] Documentation updated? 23 | - [ ] Changelog updated? 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - "*.md" 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - "*.md" 14 | jobs: 15 | tests: 16 | name: Ruby ${{ matrix.ruby }} 17 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | ruby: 22 | - "2.0" 23 | - "2.1" 24 | - "2.3" 25 | - "2.4" 26 | - "2.5" 27 | - "2.6" 28 | - "3.0" 29 | - "3.1" 30 | - "3.2" 31 | - "3.3" 32 | - ruby-head 33 | - jruby-9.2 34 | - jruby-9.3 35 | - jruby-9.4 36 | - jruby-head 37 | - truffleruby-head 38 | include: 39 | - ruby: "2.2" 40 | os: ubuntu-20.04 41 | - ruby: "2.7" 42 | coverage: true 43 | env: 44 | COVERAGE: ${{ matrix.coverage }} 45 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 46 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Set up Ruby 50 | uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: ${{ matrix.ruby }} 53 | bundler-cache: true 54 | - name: Run tests 55 | run: bundle exec rake ci 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --warnings 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | 4 | Layout/FirstArrayElementIndentation: 5 | Enabled: false 6 | 7 | Layout/LineLength: 8 | Max: 82 9 | 10 | Layout/SpaceInsideHashLiteralBraces: 11 | EnforcedStyle: no_space 12 | 13 | Metrics/AbcSize: 14 | Max: 35 15 | 16 | Metrics/BlockLength: 17 | CountComments: true 18 | Max: 25 19 | IgnoredMethods: [] 20 | Exclude: 21 | - "spec/**/*" 22 | 23 | Metrics/ClassLength: 24 | Max: 1500 25 | 26 | Metrics/CyclomaticComplexity: 27 | Max: 10 28 | 29 | Metrics/MethodLength: 30 | Max: 20 31 | 32 | Metrics/PerceivedComplexity: 33 | Max: 10 34 | 35 | Naming/FileName: 36 | Exclude: 37 | - "lib/tty-which.rb" 38 | 39 | Style/AsciiComments: 40 | Enabled: false 41 | 42 | Style/BlockDelimiters: 43 | Enabled: false 44 | 45 | Style/CommentedKeyword: 46 | Enabled: false 47 | 48 | Style/LambdaCall: 49 | EnforcedStyle: braces 50 | 51 | Style/StringLiterals: 52 | EnforcedStyle: double_quotes 53 | 54 | Style/StringLiteralsInInterpolation: 55 | EnforcedStyle: double_quotes 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [v0.5.0] - 2021-08-11 4 | 5 | ### Changed 6 | * Change to use double-quoted strings 7 | * Remove bundler as development dependency 8 | 9 | ### Fixed 10 | * Fix to stop joining absolute path and extension with a file path separator 11 | 12 | ## [v0.4.2] - 2020-01-20 13 | 14 | ### Changed 15 | * Change gemspec to add metadata and remove test artifacts 16 | 17 | ## [v0.4.1] - 2019-06-02 18 | 19 | ### Changed 20 | * Change to relax bundler dependency version 21 | 22 | ## [v0.4.0] - 2018-10-13 23 | 24 | ### Added 25 | * Add ability to specify search paths for #which and #exist? calls 26 | 27 | ### Changed 28 | * Change to freeze all strings 29 | * Change gemspec to require Ruby >= 2.0.0 30 | * Change gemspec to load files without calling git 31 | * Change gemspec to add rspec as dev dependency 32 | 33 | ## [v0.3.0] - 2017-03-20 34 | 35 | ### Changed 36 | * Change #extensions to use file path separator 37 | * Change files loading 38 | * Remove search paths caching 39 | 40 | ## [v0.2.2] - 2017-02-06 41 | 42 | ### Fixed 43 | * Fix File namespacing issue 44 | 45 | ## [v0.2.1] - 2016-12-26 46 | 47 | ### Changed 48 | * Change to stop shadowing path var in Which#search_paths 49 | 50 | ## [v0.2.0] - 2016-07-01 51 | 52 | ### Added 53 | * Add Which#exist? to check if file exists based on found path 54 | 55 | ### Changed 56 | * Change Which#search_paths to allow for paths argument 57 | * Rename Which#executable_file_with_ext? to #file_with_exec_ext? 58 | * Rename Which#path_with_executable_file? to #file_with_path? 59 | 60 | ### Fixed 61 | * Fix bug with Which#file_with_exec_ext? when comparing extensions 62 | 63 | ## [v0.1.0] - 2015-05-30 64 | 65 | * Initial implementation and release 66 | 67 | [v0.5.0]: https://github.com/piotrmurach/tty-which/compare/v0.4.2...v0.5.0 68 | [v0.4.2]: https://github.com/piotrmurach/tty-which/compare/v0.4.1...v0.4.2 69 | [v0.4.1]: https://github.com/piotrmurach/tty-which/compare/v0.4.0...v0.4.1 70 | [v0.4.0]: https://github.com/piotrmurach/tty-which/compare/v0.3.0...v0.4.0 71 | [v0.3.0]: https://github.com/piotrmurach/tty-which/compare/v0.2.2...v0.3.0 72 | [v0.2.2]: https://github.com/piotrmurach/tty-which/compare/v0.2.1...v0.2.2 73 | [v0.2.1]: https://github.com/piotrmurach/tty-which/compare/v0.2.0...v0.2.1 74 | [v0.2.0]: https://github.com/piotrmurach/tty-which/compare/v0.1.0...v0.2.0 75 | [v0.1.0]: https://github.com/piotrmurach/tty-which/compare/v0.1.0 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at piotr@piotrmurach.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "json", "2.4.1" if RUBY_VERSION == "2.0.0" 6 | 7 | group :test do 8 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5.0") 9 | gem "coveralls_reborn", "~> 0.22.0" 10 | gem "simplecov", "~> 0.21.0" 11 | end 12 | end 13 | 14 | group :metrics do 15 | gem "yardstick", "~> 0.9.9" 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Piotr Murach (piotrmurach.com) 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | TTY Toolkit logo 3 |
4 | 5 | # TTY::Which [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter] 6 | 7 | [![Gem Version](https://badge.fury.io/rb/tty-which.svg)][gem] 8 | [![Actions CI](https://github.com/piotrmurach/tty-which/workflows/CI/badge.svg?branch=master)][gh_actions_ci] 9 | [![Build status](https://ci.appveyor.com/api/projects/status/2rpm67huf1nh98d0?svg=true)][appveyor] 10 | [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-which/badges/gpa.svg)][codeclimate] 11 | [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-which/badge.svg?branch=master)][coveralls] 12 | [![Inline docs](https://inch-ci.org/github/piotrmurach/tty-which.svg?branch=master)][inchpages] 13 | 14 | [gitter]: https://gitter.im/piotrmurach/tty 15 | [gem]: https://badge.fury.io/rb/tty-which 16 | [gh_actions_ci]: https://github.com/piotrmurach/tty-which/actions?query=workflow%3ACI 17 | [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-which 18 | [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-which 19 | [coveralls]: https://coveralls.io/github/piotrmurach/tty-which 20 | [inchpages]: https://inch-ci.org/github/piotrmurach/tty-which 21 | 22 | > Platform independent implementation of Unix `which` utility that searches for executable file in the path variable. 23 | 24 | **TTY::Which** provides cross-platform executables search component for [TTY](https://github.com/piotrmurach/tty) toolkit. 25 | 26 | ## Installation 27 | 28 | Add this line to your application's Gemfile: 29 | 30 | gem "tty-which" 31 | 32 | And then execute: 33 | 34 | $ bundle 35 | 36 | Or install it yourself as: 37 | 38 | $ gem install tty-which 39 | 40 | ## Usage 41 | 42 | **TTY::Which** has `which` method that searches set of directories for an executable file based on the `PATH` environment variable. 43 | 44 | When the path to an executable program exists, an absolute path is returned, otherwise `nil`. 45 | 46 | For example, to find location for an executable program do: 47 | 48 | ```ruby 49 | TTY::Which.which("less") # => "/usr/bin/less" 50 | TTY::Which.which("git") # => "C:\Program Files\Git\bin\git" 51 | ``` 52 | 53 | You can also check an absolute path to executable: 54 | 55 | ```ruby 56 | TTY::Which.which("/usr/bin/ruby") # => "/usr/bin/ruby" 57 | ``` 58 | 59 | You can also specify directly the paths to search using `:paths` keyword: 60 | 61 | ```ruby 62 | TTY::Which.which("ruby", paths: ["/usr/local/bin", "/usr/bin", "/bin"]) 63 | # => "/usr/local/bin/ruby" 64 | ``` 65 | 66 | When you're only interesting in knowing that an executable exists on the system use the `exist?` call: 67 | 68 | ```ruby 69 | TTY::Which.exist?("ruby") # => true 70 | ``` 71 | 72 | ## Contributing 73 | 74 | Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-which. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 75 | 76 | 1. Fork it ( https://github.com/piotrmurach/tty-which/fork ) 77 | 2. Create your feature branch (`git checkout -b my-new-feature`) 78 | 3. Commit your changes (`git commit -am 'Add some feature'`) 79 | 4. Push to the branch (`git push origin my-new-feature`) 80 | 5. Create a new Pull Request 81 | 82 | ## Copyright 83 | 84 | Copyright (c) 2015 Piotr Murach. See LICENSE for further details. 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | FileList["tasks/**/*.rake"].each(&method(:import)) 6 | 7 | desc "Run all specs" 8 | task ci: %w[spec] 9 | 10 | task default: :spec 11 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | install: 3 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% 4 | - gem install bundler -v '< 2.0' 5 | - bundle install 6 | before_test: 7 | - ruby -v 8 | - gem -v 9 | - bundle -v 10 | build: off 11 | test_script: 12 | - bundle exec rake ci 13 | environment: 14 | matrix: 15 | - ruby_version: "200" 16 | - ruby_version: "200-x64" 17 | - ruby_version: "21" 18 | - ruby_version: "21-x64" 19 | - ruby_version: "22" 20 | - ruby_version: "22-x64" 21 | - ruby_version: "23" 22 | - ruby_version: "23-x64" 23 | - ruby_version: "24" 24 | - ruby_version: "24-x64" 25 | - ruby_version: "25" 26 | - ruby_version: "25-x64" 27 | - ruby_version: "26" 28 | - ruby_version: "26-x64" 29 | -------------------------------------------------------------------------------- /lib/tty-which.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "tty/which" 4 | -------------------------------------------------------------------------------- /lib/tty/which.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "which/version" 4 | 5 | module TTY 6 | # A class responsible for finding an executable in the PATH 7 | module Which 8 | # Find an executable in a platform independent way 9 | # 10 | # @param [String] cmd 11 | # the command to search for 12 | # @param [Array] paths 13 | # the paths to look through 14 | # 15 | # @example 16 | # which("ruby") # => "/usr/local/bin/ruby" 17 | # which("/usr/local/bin/ruby") # => "/usr/local/bin/ruby" 18 | # which("foo") # => nil 19 | # 20 | # @example 21 | # which("ruby", paths: ["/usr/locale/bin", "/usr/bin", "/bin"]) 22 | # 23 | # @return [String, nil] 24 | # the absolute path to executable if found, `nil` otherwise 25 | # 26 | # @api public 27 | def which(cmd, paths: search_paths) 28 | if file_with_path?(cmd) 29 | return cmd if executable_file?(cmd) 30 | 31 | extensions.each do |ext| 32 | exe = "#{cmd}#{ext}" 33 | return ::File.absolute_path(exe) if executable_file?(exe) 34 | end 35 | return nil 36 | end 37 | 38 | paths.each do |path| 39 | if file_with_exec_ext?(cmd) 40 | exe = ::File.join(path, cmd) 41 | return ::File.absolute_path(exe) if executable_file?(exe) 42 | end 43 | extensions.each do |ext| 44 | exe = ::File.join(path, "#{cmd}#{ext}") 45 | return ::File.absolute_path(exe) if executable_file?(exe) 46 | end 47 | end 48 | nil 49 | end 50 | module_function :which 51 | 52 | # Check if executable exists in the path 53 | # 54 | # @param [String] cmd 55 | # the executable to check 56 | # 57 | # @param [Array] paths 58 | # paths to check 59 | # 60 | # @return [Boolean] 61 | # 62 | # @api public 63 | def exist?(cmd, paths: search_paths) 64 | !which(cmd, paths: paths).nil? 65 | end 66 | module_function :exist? 67 | 68 | # Find default system paths 69 | # 70 | # @param [String] path 71 | # the path to search through 72 | # 73 | # @example 74 | # search_paths("/usr/local/bin:/bin") 75 | # # => ["/bin"] 76 | # 77 | # @return [Array] 78 | # the array of paths to search 79 | # 80 | # @api private 81 | def search_paths(path = ENV["PATH"]) 82 | paths = if path && !path.empty? 83 | path.split(::File::PATH_SEPARATOR) 84 | else 85 | %w[/usr/local/bin /usr/ucb /usr/bin /bin] 86 | end 87 | paths.select(&Dir.method(:exist?)) 88 | end 89 | module_function :search_paths 90 | 91 | # All possible file extensions 92 | # 93 | # @example 94 | # extensions(".exe;cmd;.bat") 95 | # # => [".exe", ".bat"] 96 | # 97 | # @param [String] path_ext 98 | # a string of semicolon separated filename extensions 99 | # 100 | # @return [Array] 101 | # an array with valid file extensions 102 | # 103 | # @api private 104 | def extensions(path_ext = ENV["PATHEXT"]) 105 | return [""] unless path_ext 106 | 107 | path_ext.split(::File::PATH_SEPARATOR).select { |part| part.include?(".") } 108 | end 109 | module_function :extensions 110 | 111 | # Determines if filename is an executable file 112 | # 113 | # @example Basic usage 114 | # executable_file?("/usr/bin/less") # => true 115 | # 116 | # @example Executable in directory 117 | # executable_file?("less", "/usr/bin") # => true 118 | # executable_file?("less", "/usr") # => false 119 | # 120 | # @param [String] filename 121 | # the path to file 122 | # @param [String] dir 123 | # the directory within which to search for filename 124 | # 125 | # @return [Boolean] 126 | # 127 | # @api private 128 | def executable_file?(filename, dir = nil) 129 | path = ::File.join(dir, filename) if dir 130 | path ||= filename 131 | ::File.file?(path) && ::File.executable?(path) 132 | end 133 | module_function :executable_file? 134 | 135 | # Check if command itself has executable extension 136 | # 137 | # @param [String] filename 138 | # the path to executable file 139 | # 140 | # @example 141 | # file_with_exec_ext?("file.bat") 142 | # # => true 143 | # 144 | # @return [Boolean] 145 | # 146 | # @api private 147 | def file_with_exec_ext?(filename) 148 | extension = ::File.extname(filename) 149 | return false if extension.empty? 150 | 151 | extensions.any? { |ext| extension.casecmp(ext).zero? } 152 | end 153 | module_function :file_with_exec_ext? 154 | 155 | # Check if executable file is part of absolute/relative path 156 | # 157 | # @param [String] cmd 158 | # the executable to check 159 | # 160 | # @return [Boolean] 161 | # 162 | # @api private 163 | def file_with_path?(cmd) 164 | ::File.expand_path(cmd) == cmd 165 | end 166 | module_function :file_with_path? 167 | end # Which 168 | end # TTY 169 | -------------------------------------------------------------------------------- /lib/tty/which/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TTY 4 | module Which 5 | VERSION = "0.5.0" 6 | end # Which 7 | end # TTY 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV["COVERAGE"] == "true" 4 | require "simplecov" 5 | require "coveralls" 6 | 7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 8 | SimpleCov::Formatter::HTMLFormatter, 9 | Coveralls::SimpleCov::Formatter 10 | ]) 11 | 12 | SimpleCov.start do 13 | command_name "spec" 14 | add_filter "spec" 15 | end 16 | end 17 | 18 | require "tty-which" 19 | 20 | RSpec.configure do |config| 21 | config.expect_with :rspec do |expectations| 22 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 23 | end 24 | 25 | config.mock_with :rspec do |mocks| 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 30 | config.disable_monkey_patching! 31 | 32 | # This setting enables warnings. It's recommended, but in some cases may 33 | # be too noisy due to issues in dependencies. 34 | config.warnings = true 35 | 36 | if config.files_to_run.one? 37 | config.default_formatter = "doc" 38 | end 39 | 40 | config.profile_examples = 2 41 | 42 | config.order = :random 43 | 44 | Kernel.srand config.seed 45 | end 46 | -------------------------------------------------------------------------------- /spec/unit/executable_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#executable_file?" do 4 | it "checks if file in directory is executable" do 5 | path = "/usr/local/bin/ruby" 6 | allow(::File).to receive(:join).and_call_original 7 | allow(::File).to receive(:file?).and_call_original 8 | allow(::File).to receive(:join).with("/usr/local/bin", "ruby").and_return(path) 9 | allow(::File).to receive(:file?).with(path).and_return(true) 10 | allow(::File).to receive(:executable?).with(path).and_return(true) 11 | 12 | expect(TTY::Which.executable_file?("ruby", "/usr/local/bin")).to eq(true) 13 | end 14 | 15 | it "checks if only a file is executable" do 16 | allow(::File).to receive(:file?).and_call_original 17 | allow(::File).to receive(:file?).with("ruby").and_return(true) 18 | allow(::File).to receive(:executable?).with("ruby").and_return(true) 19 | 20 | expect(TTY::Which.executable_file?("ruby")).to eql(true) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/exist_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#exist?" do 4 | it "finds executable in the path" do 5 | allow(TTY::Which).to receive(:which).with("ruby", a_hash_including(:paths)) 6 | .and_return("/usr/loca/bin/ruby") 7 | 8 | expect(TTY::Which.exist?("ruby")).to be(true) 9 | end 10 | 11 | it "fails to find executable in the path" do 12 | allow(TTY::Which).to receive(:which).with("ruby", a_hash_including(:paths)) 13 | .and_return(nil) 14 | 15 | expect(TTY::Which.exist?("ruby")).to be(false) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#extensions" do 4 | it "provides extensions" do 5 | exts = [".COM", ".EXE", ".BAT", ".CMD", ".VBS", ".RB", ".RBW"] 6 | exts_path = exts.join(::File::PATH_SEPARATOR) 7 | allow(ENV).to receive(:[]).with("PATHEXT").and_return(exts_path) 8 | 9 | expect(TTY::Which.extensions).to eq(exts) 10 | end 11 | 12 | it "finds no extensions" do 13 | allow(ENV).to receive(:[]).with("PATHEXT").and_return(nil) 14 | expect(TTY::Which.extensions).to eq([""]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/unit/file_with_exec_ext_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#file_with_exec_ext?" do 4 | it "detects executable extension" do 5 | filename = "file.exe" 6 | allow(TTY::Which).to receive(:extensions).and_return([".EXE", ".BAT", ".CMD"]) 7 | 8 | expect(TTY::Which.file_with_exec_ext?(filename)).to eq(true) 9 | end 10 | 11 | it "fails to detect executable extension" do 12 | filename = "file.unknown" 13 | allow(TTY::Which).to receive(:extensions).and_return([".EXE", ".BAT"]) 14 | 15 | expect(TTY::Which.file_with_exec_ext?(filename)).to eq(false) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/search_paths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#search_paths" do 4 | it "defauls search paths" do 5 | allow(ENV).to receive(:[]).with("PATH").and_return([]) 6 | allow(Dir).to receive(:exist?).and_return(true) 7 | 8 | expect(TTY::Which.search_paths).to eq([ 9 | "/usr/local/bin", "/usr/ucb", "/usr/bin", "/bin" 10 | ]) 11 | end 12 | 13 | it "finds paths in path environment" do 14 | paths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/local/bin"] 15 | path = paths.join(::File::PATH_SEPARATOR) 16 | allow(ENV).to receive(:[]).with("PATH").and_return(path) 17 | allow(Dir).to receive(:exist?).and_return(true) 18 | 19 | expect(TTY::Which.search_paths).to eq(paths) 20 | end 21 | 22 | it "accepts paths to search as an argument" do 23 | paths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/local/bin"] 24 | path = paths.join(::File::PATH_SEPARATOR) 25 | allow(Dir).to receive(:exist?).and_return(true) 26 | 27 | expect(TTY::Which.search_paths(path)).to eq(paths) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/which_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe TTY::Which, "#which" do 4 | before { stub_const("Which", described_class) } 5 | 6 | context "without extension" do 7 | let(:path) { %w[/bin /usr/bin /usr/local/bin /opt/local/bin].join(":") } 8 | let(:cmds) { %w[/usr/bin/ls /bin/sh /usr/bin/ruby /usr/local/git/bin/git] } 9 | 10 | before do 11 | allow(ENV).to receive(:[]).with("PATHEXT").and_return(nil) 12 | allow(ENV).to receive(:[]).with("PATH").and_return(path) 13 | stub_const("::File::PATH_SEPARATOR", ":") 14 | stub_const("::File::SEPARATOR", "/") 15 | allow(Dir).to receive(:exist?) { true } 16 | end 17 | 18 | it "handles path with executable file /bin/sh" do 19 | allow(Which).to receive(:file_with_path?) { true } 20 | allow(Which).to receive(:executable_file?) { true } 21 | 22 | expect(Which.which("/bin/sh")).to eq("/bin/sh") 23 | end 24 | 25 | it "fails to find path executable" do 26 | allow(Which).to receive(:file_with_path?) { true } 27 | allow(Which).to receive(:executable_file?) { false } 28 | 29 | expect(Which.which("/bin/sh")).to eq(nil) 30 | end 31 | 32 | it "searches executable file git" do 33 | dir_path = "/usr/local/bin" 34 | cmd = "git" 35 | expected_path = "#{dir_path}/#{cmd}" 36 | allow(Which).to receive(:file_with_path?) { false } 37 | allow(Which).to receive(:file_with_exec_ext?) { false } 38 | 39 | allow(::File).to receive(:join).and_call_original 40 | allow(::File).to receive(:join).with(dir_path, cmd) 41 | .and_return(expected_path) 42 | allow(Which).to receive(:executable_file?) { false } 43 | allow(Which).to receive(:executable_file?).with(expected_path) { true } 44 | allow(::File).to receive(:absolute_path).with(expected_path) 45 | .and_return(expected_path) 46 | 47 | expect(Which.which(cmd)).to eq(expected_path) 48 | end 49 | 50 | it "allows to search through custom paths" do 51 | paths = %w[/usr/local/bin /usr/bin /bin] 52 | allow(Which).to receive(:executable_file?).with("/usr/local/bin/ruby") { false } 53 | allow(Which).to receive(:executable_file?).with("/usr/bin/ruby") { true } 54 | allow(::File).to receive(:absolute_path).with("/usr/bin/ruby") 55 | .and_return("/usr/bin/ruby") 56 | 57 | expect(TTY::Which.which("ruby", paths: paths)).to eq("/usr/bin/ruby") 58 | end 59 | end 60 | 61 | context "with extension" do 62 | let(:path) { ["C:\\Program Files\\Git\\bin"].join(";") } 63 | let(:exts) { %w[.msi .exe .bat .cmd].join(";") } 64 | 65 | before do 66 | allow(ENV).to receive(:[]).with("PATHEXT").and_return(exts) 67 | allow(ENV).to receive(:[]).with("PATH").and_return(path) 68 | stub_const("::File::PATH_SEPARATOR", ";") 69 | stub_const("::File::SEPARATOR", "\\") 70 | allow(Dir).to receive(:exist?) { true } 71 | end 72 | 73 | it "handles path with executable file C:\\Program Files\\Git\\bin\\git" do 74 | allow(Which).to receive(:file_with_path?) { true } 75 | allow(Which).to receive(:executable_file?).with(any_args) { false } 76 | 77 | path_with_exe_file = "C:\\Program Files\\Git\\bin\\git" 78 | expected_path = "#{path_with_exe_file}.exe" 79 | 80 | allow(Which).to receive(:executable_file?).with(expected_path) { true } 81 | allow(::File).to receive(:absolute_path).and_return(expected_path) 82 | 83 | expect(Which.which(path_with_exe_file)).to eq(expected_path) 84 | expect(Which).to have_received(:executable_file?) 85 | .with("#{path_with_exe_file}.msi") 86 | end 87 | 88 | it "searches path for executable git.exe" do 89 | dir_path = "C:\\Program Files\\Git\\bin" 90 | cmd = "git.exe" 91 | expected_path = "#{dir_path}\\#{cmd}" 92 | allow(Which).to receive(:file_with_path?) { false } 93 | allow(Which).to receive(:file_with_exec_ext?).with(cmd) { true } 94 | 95 | allow(::File).to receive(:join).and_call_original 96 | allow(::File).to receive(:join).with(dir_path, any_args) 97 | allow(::File).to receive(:join).with(dir_path, cmd) 98 | .and_return(expected_path) 99 | allow(Which).to receive(:executable_file?).with(any_args) { false } 100 | allow(Which).to receive(:executable_file?).with(expected_path) { true } 101 | allow(::File).to receive(:absolute_path).with(expected_path) 102 | .and_return(expected_path) 103 | 104 | expect(Which.which(cmd)).to eq(expected_path) 105 | expect(::File).to have_received(:absolute_path).with(expected_path) 106 | end 107 | 108 | it "searches path for executable git" do 109 | dir_path = "C:\\Program Files\\Git\\bin" 110 | cmd = "git" 111 | expected_path = "#{dir_path}\\#{cmd}.exe" 112 | allow(Which).to receive(:file_with_path?) { false } 113 | allow(Which).to receive(:file_with_exec_ext?).with(cmd) { false } 114 | 115 | allow(::File).to receive(:join).and_call_original 116 | allow(::File).to receive(:join).with(dir_path, any_args) 117 | allow(::File).to receive(:join).with(dir_path, "#{cmd}.exe") 118 | .and_return(expected_path) 119 | allow(Which).to receive(:executable_file?).with(any_args) { false } 120 | allow(Which).to receive(:executable_file?).with(expected_path) { true } 121 | allow(::File).to receive(:absolute_path).with(expected_path) 122 | .and_return(expected_path) 123 | 124 | expect(Which.which(cmd)).to eq(expected_path) 125 | expect(::File).to have_received(:absolute_path).with(expected_path) 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /tasks/console.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Load gem inside irb console" 4 | task :console do 5 | require "irb" 6 | require "irb/completion" 7 | require_relative "../lib/tty-which" 8 | ARGV.clear 9 | IRB.start 10 | end 11 | task c: %w[console] 12 | -------------------------------------------------------------------------------- /tasks/coverage.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Measure code coverage" 4 | task :coverage do 5 | begin 6 | original, ENV["COVERAGE"] = ENV["COVERAGE"], "true" 7 | Rake::Task["spec"].invoke 8 | ensure 9 | ENV["COVERAGE"] = original 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "rspec/core/rake_task" 5 | 6 | desc "Run all specs" 7 | RSpec::Core::RakeTask.new(:spec) do |task| 8 | task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb" 9 | end 10 | 11 | namespace :spec do 12 | desc "Run unit specs" 13 | RSpec::Core::RakeTask.new(:unit) do |task| 14 | task.pattern = "spec/unit{,/*/**}/*_spec.rb" 15 | end 16 | 17 | desc "Run integration specs" 18 | RSpec::Core::RakeTask.new(:integration) do |task| 19 | task.pattern = "spec/integration{,/*/**}/*_spec.rb" 20 | end 21 | 22 | desc "Run performance specs" 23 | RSpec::Core::RakeTask.new(:perf) do |task| 24 | task.pattern = "spec/perf{,/*/**}/*_spec.rb" 25 | end 26 | end 27 | rescue LoadError 28 | %w[spec spec:unit spec:integration].each do |name| 29 | task name do 30 | warn "In order to run #{name}, do `gem install rspec`" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /tty-which.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/tty/which/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "tty-which" 7 | spec.version = TTY::Which::VERSION 8 | spec.authors = ["Piotr Murach"] 9 | spec.email = ["piotr@piotrmurach.com"] 10 | spec.summary = "Platform independent implementation of Unix which command." 11 | spec.description = "Platform independent implementation of Unix which command." 12 | spec.homepage = "https://ttytoolkit.org" 13 | spec.license = "MIT" 14 | spec.metadata = { 15 | "allowed_push_host" => "https://rubygems.org", 16 | "bug_tracker_uri" => "https://github.com/piotrmurach/tty-which/issues", 17 | "changelog_uri" => "https://github.com/piotrmurach/tty-which/blob/master/CHANGELOG.md", 18 | "documentation_uri" => "https://www.rubydoc.info/gems/tty-which", 19 | "homepage_uri" => spec.homepage, 20 | "source_code_uri" => "https://github.com/piotrmurach/tty-which" 21 | } 22 | spec.files = Dir["lib/**/*"] 23 | spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] 24 | spec.bindir = "exe" 25 | spec.require_paths = ["lib"] 26 | spec.required_ruby_version = ">= 2.0.0" 27 | 28 | spec.add_development_dependency "rake" 29 | spec.add_development_dependency "rspec", ">= 3.0" 30 | end 31 | --------------------------------------------------------------------------------