├── .circleci └── config.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── scallop.rb └── scallop │ ├── command_builder.rb │ ├── errors.rb │ ├── executor.rb │ ├── param.rb │ ├── public_api.rb │ ├── result.rb │ └── version.rb ├── scallop.gemspec ├── scallop.png └── spec ├── fixtures └── lorem.txt ├── scallop_spec.rb └── spec_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | main: 5 | jobs: 6 | - ruby26 7 | - ruby25 8 | - ruby24 9 | - ruby23 10 | 11 | executors: 12 | ruby26: 13 | docker: 14 | - image: circleci/ruby:2.6 15 | ruby25: 16 | docker: 17 | - image: circleci/ruby:2.5 18 | ruby24: 19 | docker: 20 | - image: circleci/ruby:2.4 21 | ruby23: 22 | docker: 23 | - image: circleci/ruby:2.3 24 | 25 | commands: 26 | test: 27 | steps: 28 | - restore_cache: 29 | keys: 30 | - bundler-{{ checksum "Gemfile.lock" }} 31 | 32 | - run: 33 | name: Bundle Install 34 | command: bundle check --path vendor/bundle || bundle install --deployment 35 | 36 | - save_cache: 37 | key: bundler-{{ checksum "Gemfile.lock" }} 38 | paths: 39 | - vendor/bundle 40 | 41 | - run: 42 | name: Run rspec 43 | command: | 44 | bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml 45 | 46 | jobs: 47 | ruby26: 48 | executor: ruby26 49 | steps: 50 | - checkout 51 | - test 52 | 53 | - run: 54 | name: Report Test Coverage 55 | command: | 56 | wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter 57 | chmod +x cc-test-reporter 58 | ./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json 59 | ./cc-test-reporter upload-coverage -i coverage/codeclimate.json 60 | 61 | - store_test_results: 62 | path: test_results 63 | 64 | ruby25: 65 | executor: ruby25 66 | steps: 67 | - checkout 68 | - test 69 | 70 | ruby24: 71 | executor: ruby24 72 | steps: 73 | - checkout 74 | - test 75 | 76 | ruby23: 77 | executor: ruby23 78 | steps: 79 | - checkout 80 | - test 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /*.gem 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'spec/**/*' 4 | 5 | Metrics/LineLength: 6 | Max: 120 7 | Metrics/MethodLength: 8 | Max: 20 9 | 10 | Style/TrailingCommaInArguments: 11 | EnforcedStyleForMultiline: comma 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rene.klacan@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in scallop.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | scallop (0.9.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.0) 10 | coderay (1.1.2) 11 | diff-lcs (1.3) 12 | docile (1.3.2) 13 | jaro_winkler (1.5.3) 14 | json (2.3.1) 15 | method_source (0.9.2) 16 | parallel (1.17.0) 17 | parser (2.6.3.0) 18 | ast (~> 2.4.0) 19 | pry (0.12.2) 20 | coderay (~> 1.1.0) 21 | method_source (~> 0.9.0) 22 | rainbow (3.0.0) 23 | rake (13.0.1) 24 | rspec (3.8.0) 25 | rspec-core (~> 3.8.0) 26 | rspec-expectations (~> 3.8.0) 27 | rspec-mocks (~> 3.8.0) 28 | rspec-core (3.8.0) 29 | rspec-support (~> 3.8.0) 30 | rspec-expectations (3.8.3) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.8.0) 33 | rspec-mocks (3.8.0) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.8.0) 36 | rspec-support (3.8.0) 37 | rspec_junit_formatter (0.4.1) 38 | rspec-core (>= 2, < 4, != 2.12.0) 39 | rubocop (0.71.0) 40 | jaro_winkler (~> 1.5.1) 41 | parallel (~> 1.10) 42 | parser (>= 2.6) 43 | rainbow (>= 2.2.2, < 4.0) 44 | ruby-progressbar (~> 1.7) 45 | unicode-display_width (>= 1.4.0, < 1.7) 46 | ruby-progressbar (1.10.1) 47 | simplecov (0.16.1) 48 | docile (~> 1.1) 49 | json (>= 1.8, < 3) 50 | simplecov-html (~> 0.10.0) 51 | simplecov-html (0.10.2) 52 | unicode-display_width (1.6.0) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | bundler (~> 1.17) 59 | pry (~> 0.12) 60 | rake (~> 13.0) 61 | rspec (~> 3.0) 62 | rspec_junit_formatter (~> 0.4) 63 | rubocop (~> 0.71) 64 | scallop! 65 | simplecov (~> 0.16) 66 | 67 | BUNDLED WITH 68 | 1.17.2 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Rene Klacan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/scallop.svg)](https://badge.fury.io/rb/scallop) 2 | [![CircleCI](https://circleci.com/gh/fetlife/scallop.svg?style=svg)](https://circleci.com/gh/fetlife/scallop) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/b7d9660aa51c7205fbac/maintainability)](https://codeclimate.com/github/fetlife/scallop/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b7d9660aa51c7205fbac/test_coverage)](https://codeclimate.com/github/fetlife/scallop/test_coverage) 5 | 6 | # Scallop 7 | 8 | ![](./scallop.png) 9 | 10 | Ergonomic shell wrapper. 11 | 12 | Features: 13 | 14 | * Easy access to command's output (stdout & stderr) 15 | * Failure handling 16 | * Parameterization 17 | * Measuring execution time 18 | * Built-in string escaping 19 | * No dependencies 20 | 21 | ## Installation 22 | 23 | Add this line to your application's Gemfile: 24 | 25 | ```ruby 26 | gem 'scallop' 27 | ``` 28 | 29 | And then execute: 30 | 31 | $ bundle 32 | 33 | Or install it yourself as: 34 | 35 | $ gem install scallop 36 | 37 | ## Usage 38 | 39 | To run `sudo -u chuck grep -R /home/chuck` 40 | 41 | ```ruby 42 | result = Scallop.sudo(:chuck).cmd(:grep, '-R', '/home/chuck').run 43 | ``` 44 | 45 | You can then check whether command succeeded 46 | 47 | ```ruby 48 | result.success? 49 | ``` 50 | 51 | See its output 52 | 53 | ```ruby 54 | result.stdout 55 | result.stderr 56 | result.output # STDOUT and STDERR combined 57 | ``` 58 | 59 | You can also access information about command execution time 60 | 61 | ```ruby 62 | result.timing.real # Elapsed real time 63 | result.timing.stime # System CPU time 64 | result.timing.utime # User CPU time 65 | result.timing.total # Total time, that is utime + stime + cutime + cstime 66 | ``` 67 | 68 | ### Handling failures with exceptions 69 | 70 | If you replace `run` with `run!`, exception will be raised in case command fails 71 | 72 | ```ruby 73 | begin 74 | Scallop.cmd(some_command).run! 75 | rescue Scallop::Errors::CommandFailed => error 76 | # you can access result right on the error itself 77 | error.result.stderr 78 | end 79 | ``` 80 | 81 | ### Piping 82 | 83 | To run `cat /some/file | grep something` 84 | 85 | ```ruby 86 | command = Scallop.cmd(:cat, '/some/file') | Scallop.cmd(:grep, 'something') 87 | command.run 88 | ``` 89 | 90 | ### Parameterization 91 | 92 | ```ruby 93 | stored_command = Scallop.cmd(:rm, '-rf', Scallop::Param[:path]) 94 | 95 | stored_command.set(path: '/foo').run # rm -rf /foo 96 | stored_command.set(path: '/bar').run # rm -rf /bar 97 | ``` 98 | 99 | -------- 100 | 101 | You can also [check specs](./spec/scallop_spec.rb) for examples. 102 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'scallop' 6 | 7 | require 'pry' 8 | Pry.start 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/scallop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark' 4 | require 'open3' 5 | require 'shellwords' 6 | 7 | require 'scallop/command_builder' 8 | require 'scallop/errors' 9 | require 'scallop/executor' 10 | require 'scallop/param' 11 | require 'scallop/public_api' 12 | require 'scallop/result' 13 | require 'scallop/version' 14 | -------------------------------------------------------------------------------- /lib/scallop/command_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | # Implements command building interface with immutability. 5 | class CommandBuilder 6 | def initialize 7 | @params = {} 8 | @cmd = [] 9 | end 10 | 11 | def sudo(sudo = true) 12 | dup 13 | .tap do |instance| 14 | instance.instance_eval { @sudo = sudo } 15 | end 16 | .freeze 17 | end 18 | 19 | def cmd(*cmd) 20 | dup 21 | .tap do |instance| 22 | instance.instance_eval { @cmd += cmd } 23 | end 24 | .freeze 25 | end 26 | 27 | def read_cmd 28 | @cmd 29 | end 30 | 31 | def set(params) 32 | new_params = @params.merge(params) 33 | 34 | dup 35 | .tap do |instance| 36 | instance.instance_eval { @params = new_params } 37 | end 38 | .freeze 39 | end 40 | 41 | def |(other) 42 | cmd(:|, other.read_cmd) 43 | end 44 | 45 | def run(args = {}) 46 | Executor.run(to_command, args) 47 | end 48 | 49 | def run! 50 | Executor.run!(to_command) 51 | end 52 | 53 | def to_command 54 | raise Errors::ValidationFailed, 'cmd missing' if @cmd.empty? 55 | 56 | [build_prefix, build_command].compact.join(' ') 57 | end 58 | 59 | private 60 | 61 | def build_prefix 62 | case @sudo 63 | when true then 'sudo' 64 | when String, Symbol then "sudo -u #{@sudo}" 65 | end 66 | end 67 | 68 | def build_command 69 | [*@cmd] 70 | .flatten 71 | .map do |cmd_part| 72 | case cmd_part 73 | when Param 74 | value = @params[cmd_part.key] 75 | raise Errors::ValidationFailed, "value for param '#{cmd_part.key}' not set" if value.nil? 76 | 77 | Shellwords.escape(value.to_s) 78 | when :| 79 | cmd_part 80 | else 81 | Shellwords.escape(cmd_part.to_s) 82 | end 83 | end 84 | .join(' ') 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/scallop/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | module Errors 5 | # Error representing command failure. 6 | class CommandFailed < StandardError 7 | attr_reader :result 8 | 9 | def initialize(message, result) 10 | @result = result 11 | super(message) 12 | end 13 | end 14 | 15 | # Error representing absense of required parameters or incorrect usage 16 | class ValidationFailed < StandardError 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/scallop/executor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | # Executes command and returns result. 5 | module Executor 6 | def self.run(command, args = {}) 7 | capture3, timing = measure do 8 | Open3.capture3(command, args) 9 | end 10 | build_result(capture3, timing) 11 | end 12 | 13 | def self.run!(command, args = {}) 14 | run(command, args).tap do |result| 15 | raise Errors::CommandFailed.new(result.stderr, result) unless result.success? 16 | end 17 | end 18 | 19 | def self.measure 20 | result = nil 21 | timing = Benchmark.measure { result = yield } 22 | [result, timing] 23 | end 24 | 25 | def self.build_result(capture3, timing) 26 | stdout, stderr, status = capture3 27 | 28 | Result 29 | .new( 30 | stdout: stdout.to_s.strip, 31 | stderr: stderr.to_s.strip, 32 | status: status, 33 | timing: timing, 34 | ) 35 | .freeze 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/scallop/param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | # Represents named command parameter. 5 | class Param 6 | def self.[](key) 7 | new(key) 8 | end 9 | 10 | attr_reader :key 11 | 12 | def initialize(key) 13 | @key = key 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/scallop/public_api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # :nodoc: 4 | module Scallop 5 | # Adds ergonomic public API methods. 6 | module PublicAPI 7 | METHODS = %i[cmd sudo].freeze 8 | 9 | def self.included(base) 10 | base.extend ClassMethods 11 | end 12 | 13 | # :nodoc: 14 | module ClassMethods 15 | METHODS.each do |method| 16 | define_method(method) do |*args| 17 | CommandBuilder.new.public_send(method, *args) 18 | end 19 | end 20 | end 21 | end 22 | 23 | include PublicAPI 24 | end 25 | -------------------------------------------------------------------------------- /lib/scallop/result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | # Represents a result of command execution. 5 | class Result 6 | attr_reader :stdout 7 | attr_reader :stderr 8 | attr_reader :status 9 | attr_reader :timing 10 | attr_reader :output 11 | 12 | def initialize(stdout:, stderr:, status:, timing:) 13 | @stdout = stdout 14 | @stderr = stderr 15 | @status = status 16 | @timing = timing 17 | @output = [stdout, stderr].reject { |s| s.nil? || s.empty? }.join("\n") 18 | end 19 | 20 | def success? 21 | @status.success? 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/scallop/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Scallop 4 | VERSION = '0.9.1' 5 | end 6 | -------------------------------------------------------------------------------- /scallop.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'scallop/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'scallop' 9 | spec.version = Scallop::VERSION 10 | spec.authors = ['FetLife'] 11 | spec.email = ['dev@fetlife.com'] 12 | 13 | spec.summary = 'Ergonomic shell wrapper.' 14 | spec.description = 'Ergonomic shell wrapper.' 15 | spec.homepage = 'https://github.com/fetlife/scallop' 16 | spec.license = 'MIT' 17 | 18 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 19 | `git ls-files -z`.split("\x0").reject do |f| 20 | f.match(%r{^(test|spec|features|.circleci)/}) || f.end_with?('.png') 21 | end 22 | end 23 | spec.bindir = 'bin' 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 25 | spec.require_paths = ['lib'] 26 | 27 | spec.required_ruby_version = '>= 2.3.0' 28 | 29 | spec.add_development_dependency 'bundler', '~> 1.17' 30 | spec.add_development_dependency 'pry', '~> 0.12' 31 | spec.add_development_dependency 'rake', '~> 13.0' 32 | spec.add_development_dependency 'rspec', '~> 3.0' 33 | spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4' 34 | spec.add_development_dependency 'rubocop', '~> 0.71' 35 | spec.add_development_dependency 'simplecov', '~> 0.16' 36 | end 37 | -------------------------------------------------------------------------------- /scallop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetlife/scallop/78b4a99442ac846b638c1f5e545b51d2f5312db1/scallop.png -------------------------------------------------------------------------------- /spec/fixtures/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc posuere ex nec nunc sagittis lobortis. Aenean est mauris, lacinia eu odio quis, pellentesque pharetra nisi. Ut tristique tincidunt pellentesque. In lobortis pulvinar risus, in consequat lectus aliquet vitae. Duis aliquam eget dui sed sollicitudin. Nulla facilisi. Vestibulum finibus nunc sed sodales porttitor. Sed at lacus diam. Donec luctus sapien ut tempus laoreet. Phasellus eu libero ac erat maximus euismod et et arcu. Nulla in felis nulla. 2 | -------------------------------------------------------------------------------- /spec/scallop_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'scallop' 4 | 5 | RSpec.describe Scallop do 6 | describe '#to_command & #cmd & #sudo' do 7 | specify 'command building' do 8 | expect(Scallop.cmd(:ls).to_command).to eq 'ls' 9 | expect(Scallop.cmd('ls', '/home/scallop').to_command).to eq 'ls /home/scallop' 10 | expect(Scallop.sudo(:chucknorris).cmd(:rm, '-rf', '/').to_command).to eq 'sudo -u chucknorris rm -rf /' 11 | expect(Scallop.sudo.cmd('ls').to_command).to eq 'sudo ls' 12 | expect(Scallop.cmd(:echo, %w[1 2 3]).to_command).to eq 'echo 1 2 3' 13 | end 14 | 15 | specify 'escaping' do 16 | expect(Scallop.cmd('ls', '/path to/awesome file').to_command).to eq 'ls /path\\ to/awesome\\ file' 17 | end 18 | 19 | specify 'parametrization' do 20 | expect(Scallop.cmd(:echo, Scallop::Param[:foo]).set(foo: 'bar').to_command).to eq 'echo bar' 21 | end 22 | 23 | specify 'parametrization merging' do 24 | expect( 25 | Scallop 26 | .cmd(:echo, Scallop::Param[:foo], Scallop::Param[:bar]) 27 | .set(foo: 'foo') 28 | .set(bar: 'bar') 29 | .to_command 30 | ).to eq 'echo foo bar' 31 | end 32 | 33 | specify 'error on missing command' do 34 | expect do 35 | Scallop.sudo(:chucknorris).to_command 36 | end.to raise_error(Scallop::Errors::ValidationFailed) 37 | end 38 | 39 | specify 'error on missing parameters' do 40 | expect do 41 | Scallop.cmd(:echo, Scallop::Param[:foo]).to_command 42 | end.to raise_error(Scallop::Errors::ValidationFailed) 43 | end 44 | 45 | specify 'immutability' do 46 | cmd1 = Scallop.cmd(:ls) 47 | cmd2 = cmd1.sudo(:chucknorris) 48 | 49 | expect(cmd1.to_command).to eq 'ls' 50 | expect(cmd2.to_command).to eq 'sudo -u chucknorris ls' 51 | 52 | cmd3 = Scallop.cmd(:echo, Scallop::Param[:foo]) 53 | cmd4 = cmd3.set(foo: 'bar') 54 | cmd5 = cmd3.set(foo: 'foo') 55 | 56 | expect(cmd4.to_command).to eq 'echo bar' 57 | expect(cmd5.to_command).to eq 'echo foo' 58 | end 59 | 60 | specify 'piping' do 61 | cmd = Scallop.cmd(:ls, '/home') | Scallop.cmd(:grep, 'chuck') 62 | expect(cmd.to_command).to eq 'ls /home | grep chuck' 63 | end 64 | 65 | specify 'timing' do 66 | result = Scallop.cmd(:ps, '-ef').run! 67 | 68 | expect(result.timing.real).to be_a Float 69 | expect(result.timing.stime).to be_a Float 70 | expect(result.timing.utime).to be_a Float 71 | expect(result.timing.total).to be_a Float 72 | end 73 | end 74 | 75 | describe '#run' do 76 | specify 'successful command' do 77 | result = Scallop.cmd(:grep, 'Lorem', fixture_path('lorem.txt')).run 78 | 79 | expect(result.stdout).to include('Lorem ipsum') 80 | expect(result.stderr).to be_empty 81 | expect(result.output).to include('Lorem ipsum') 82 | expect(result.success?).to eq true 83 | end 84 | 85 | specify 'failed command without stderr' do 86 | result = Scallop.cmd(:grep, 'bollocks', fixture_path('lorem.txt')).run 87 | 88 | expect(result.stdout).to be_empty 89 | expect(result.stderr).to be_empty 90 | expect(result.output).to be_empty 91 | expect(result.success?).to eq false 92 | end 93 | 94 | specify 'failed command with stderr' do 95 | result = Scallop.cmd(:grep, 'bollocks', 'bollocks.txt').run 96 | 97 | expect(result.stdout).to be_empty 98 | expect(result.stderr).to include('No such file or directory') 99 | expect(result.output).to include('No such file or directory') 100 | expect(result.success?).to eq false 101 | end 102 | 103 | specify 'working directory specified' do 104 | expect(Open3).to receive(:capture3).with("ls -l", chdir: "/some/path") 105 | Scallop.cmd(:ls, "-l").run(chdir: "/some/path") 106 | end 107 | end 108 | 109 | describe '#run!' do 110 | specify 'successful command' do 111 | result = Scallop.cmd(:grep, 'Lorem', fixture_path('lorem.txt')).run! 112 | 113 | expect(result.stdout).to include('Lorem ipsum') 114 | expect(result.stderr).to be_empty 115 | expect(result.output).to include('Lorem ipsum') 116 | expect(result.success?).to eq true 117 | end 118 | 119 | specify 'failed command with stderr' do 120 | expect do 121 | Scallop.cmd(:grep, 'bollocks', 'bollocks.txt').run! 122 | end.to raise_error do |error| 123 | expect(error).to be_a(Scallop::Errors::CommandFailed) 124 | expect(error).to respond_to(:result) 125 | 126 | expect(error.result.stdout).to be_empty 127 | expect(error.result.stderr).to include('No such file or directory') 128 | expect(error.result.output).to include('No such file or directory') 129 | expect(error.result.success?).to eq false 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | 5 | SimpleCov.start 6 | 7 | require 'bundler/setup' 8 | require 'scallop' 9 | 10 | FIXTURES_PATH = File.join(__dir__, 'fixtures') 11 | 12 | def fixture_path(filename) 13 | File.join(FIXTURES_PATH, filename).tap do |filepath| 14 | raise "Fixture doesn't exists: #{filename}" unless File.exist?(filepath) 15 | end 16 | end 17 | 18 | RSpec.configure do |config| 19 | # Enable flags like --only-failures and --next-failure 20 | config.example_status_persistence_file_path = '.rspec_status' 21 | 22 | # Disable RSpec exposing methods globally on `Module` and `main` 23 | config.disable_monkey_patching! 24 | 25 | config.expect_with :rspec do |c| 26 | c.syntax = :expect 27 | end 28 | end 29 | --------------------------------------------------------------------------------