pax_global_header 0000666 0000000 0000000 00000000064 14012255006 0014505 g ustar 00root root 0000000 0000000 52 comment=1340c436e8c111dc9eb94997c5cf818bc8bfd42e tty-command-0.10.1/ 0000775 0000000 0000000 00000000000 14012255006 0014020 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/.editorconfig 0000664 0000000 0000000 00000000226 14012255006 0016475 0 ustar 00root root 0000000 0000000 root = true [*.rb] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true tty-command-0.10.1/.github/ 0000775 0000000 0000000 00000000000 14012255006 0015360 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/.github/FUNDING.yml 0000664 0000000 0000000 00000000024 14012255006 0017171 0 ustar 00root root 0000000 0000000 github: piotrmurach tty-command-0.10.1/.github/ISSUE_TEMPLATE.md 0000664 0000000 0000000 00000001107 14012255006 0020064 0 ustar 00root root 0000000 0000000 ### Are you in the right place? * For issues or feature requests file a GitHub issue in this repository * For general questions or discussion post in [Gitter](https://gitter.im/piotrmurach/tty) ### Describe the problem A brief description of the issue/feature. ### Steps to reproduce the problem ``` Your code here to reproduce the issue ``` ### Actual behaviour What happened? This could be a description, log output, error raised etc... ### Expected behaviour What did you expect to happen? ### Describe your environment * OS version: * Ruby version: * TTY::Command version: tty-command-0.10.1/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000000677 14012255006 0021173 0 ustar 00root root 0000000 0000000 ### Describe the change What does this Pull Request do? ### Why are we doing this? Any related context as to why is this is a desirable change. ### Benefits How will the library improve? ### Drawbacks Possible drawbacks applying this change. ### Requirements Put an X between brackets on each line if you have done the item: [] Tests written & passing locally? [] Code style checked? [] Rebased with `master` branch? [] Documentation updated? tty-command-0.10.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14012255006 0017415 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/.github/workflows/ci.yml 0000664 0000000 0000000 00000003357 14012255006 0020543 0 ustar 00root root 0000000 0000000 --- name: CI on: push: branches: - master paths-ignore: - "benchmarks/**" - "bin/**" - "examples/**" - "*.md" pull_request: branches: - master paths-ignore: - "benchmarks/**" - "bin/**" - "examples/**" - "*.md" jobs: tests: name: Ruby ${{ matrix.ruby }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest ruby: - 2.3 - 2.4 - 2.5 - 2.6 - 3.0 - ruby-head - jruby-head - truffleruby-head allow_failure: [false] include: - ruby: 2.1 os: ubuntu-latest coverage: false bundler: 1 allow_failure: false - ruby: 2.2 os: ubuntu-latest coverage: false bundler: 1 allow_failure: false - ruby: 2.7 os: ubuntu-latest coverage: true bundler: latest allow_failure: false - ruby: jruby-9.2.13.0 os: ubuntu-latest coverage: false bundler: latest allow_failure: true env: COVERAGE: ${{ matrix.coverage }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.allow_failure }} steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler: ${{ matrix.bundler }} - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake ci tty-command-0.10.1/.gitignore 0000664 0000000 0000000 00000000127 14012255006 0016010 0 ustar 00root root 0000000 0000000 /.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ tty-command-0.10.1/.rspec 0000664 0000000 0000000 00000000066 14012255006 0015137 0 ustar 00root root 0000000 0000000 --require spec_helper --color --format doc --warnings tty-command-0.10.1/.rubocop.yml 0000664 0000000 0000000 00000001662 14012255006 0016277 0 ustar 00root root 0000000 0000000 AllCops: NewCops: enable Layout/FirstArrayElementIndentation: Enabled: false Layout/FirstHashElementIndentation: Enabled: false Layout/LineLength: Max: 80 Lint/AssignmentInCondition: Enabled: false Metrics/AbcSize: Max: 30 Metrics/BlockLength: CountComments: true Max: 25 IgnoredMethods: [] Exclude: - "spec/**/*" Metrics/ClassLength: Max: 1500 Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Max: 20 Naming/BinaryOperatorParameterName: Enabled: false Style/AccessorGrouping: Enabled: false Style/AsciiComments: Enabled: false Style/LambdaCall: EnforcedStyle: braces Style/FormatString: EnforcedStyle: percent Style/ParallelAssignment: Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes Style/TrivialAccessors: Enabled: false # { ... } for multi-line blocks is okay Style/BlockDelimiters: Enabled: false Style/CommentedKeyword: Enabled: false tty-command-0.10.1/CHANGELOG.md 0000664 0000000 0000000 00000012400 14012255006 0015626 0 ustar 00root root 0000000 0000000 # Change log ## [v0.10.1] - 2021-02-14 ### Fixed * Fix undesired persistence of environment variables in Ruby >= 3.0.0 ## [v0.10.0] - 2020-10-22 ### Changed * Change :chdir option to escape directory location path * Change gemspec to add metadata and remove test artefacts * Change to update pastel dependency and restrict version to minor only * Remove bundler as a dev dependency and relax rspec's upper boundary ### Fixed * Fix Ruby 2.7 keyword conversion errors * Fix error when environment variable contains % character ## [v0.9.0] - 2019-09-28 ### Changed * Change gemspec to require Ruby >= 2.0.0 ## [v0.8.2] - 2018-08-07 ### Changed * Change gemspec to load only required files ### Fixed * Fix issue with Ruby greater than 2.5.0 displaying thread error traceback by default ## [v0.8.1] - 2018-05-20 ### Changed * Change ProcessRunner#write_stream to handle all writing logic ## [v0.8.0] - 2018-04-22 ### Added * Add :output_only_on_error option by Iulian Onofrei(@revolter) * Add :verbose flag to toggle warnings ### Changed * Change ProcessRunner to use waitpid2 api for direct status * Change ProcessRunner stdout & stderr reading to use IO.select and be non-blocking ### Fixed * Fix :timeout to raise when long running without input or output * Fix ProcessRunner to ensure no zombie processes on timeouts ## [v0.7.0] - 2017-11-19 ### Added * Add :binmode option to allow configuring input & ouput as binary * Add :pty option to allow runnig commands in PTY(pseudo terminal) ### Changed * Change Command to remove threads synchronization to leave it up to client to handle * Change Cmd to allow updating options * Change Command to accept options for all commands such as :timeout, :binmode etc... * Change Execute to ChildProcess module * Change ChildProcess to skip spawn redirect close options on Windows platform * Change to enforce UTF-8 encoding for process pipes to be cross platform * Change ProcessRunner to stop rescuing runtime failures * Change to stop mutating String instances ### Fixed * Fix ProcessRunner threads deadlocking on exclusive mutex * Fix :timeout option to raise TimeoutExceeded error * Fix test suite to work on Windows * Fix Cmd arguments escaping ## [v0.6.0] - 2017-07-22 ### Added * Add runtime property to command result * Add ability to merge multiple redirects ### Changed * Change to make all strings immutable * Change waiting for pid to recover when already dead ### Fix * Fix redirection to instead of redirecting to parent process, redirect to child process. And hence allow for :out => :err redirection to work with output logging. ## [v0.5.0] - 2017-07-16 ### Added * Add :signal option for timeout * Add :input option for handling stdin input * Add ability for Command#run to specify a callback that is invoked whenever stdout or stderr receive output * Add Command#wait for polling a long running script for matching output ### Changed * Change ProcessRunner to immediately sync write pipe * Change ProcessRunner to write to stdin stream when writable ### Fixed * Fix quiet printer write call by @jamesepatrick * Fix to correctly close all pipe ends between parent and child process * Fix timeout behaviour for writable and readable streams ## [v0.4.0] - 2017-02-22 ### Changed * Remove automatic insertion of semicolons on line breaks and fix issue #27 ## [v0.3.3] - 2017-02-10 ### Changed * Update deprecated Fixnum class to Integer for Ruby 2.4 compatability by Edmund Larden(@admund) * Remove self extension from Execute ## [v0.3.2] - 2017-02-06 ### Fixed * Fix File namespacing ## [v0.3.1] - 2017-01-22 ### Fixed * Fix top level File constant ## [v0.3.0] - 2017-01-13 ### Added * Add ability to enumerate Result output * Add #record_saparator for specifying delimiter for enumeration ### Changed * Change Abstract printer to separate arguments out * Change Cmd to prevent modifications * Change pastel dependency version ## [v0.2.0] - 2016-07-03 ### Added * Add ruby interperter helper ### Fixed * Fix multibyte content truncation for streams by Ondrej Moravcik(@ondra-m) ## [v0.1.0] - 2016-05-29 * Initial implementation and release [v0.10.1]: https://github.com/piotrmurach/tty-command/compare/v0.10.0...v0.10.1 [v0.10.0]: https://github.com/piotrmurach/tty-command/compare/v0.9.0...v0.10.0 [v0.9.0]: https://github.com/piotrmurach/tty-command/compare/v0.8.2...v0.9.0 [v0.8.2]: https://github.com/piotrmurach/tty-command/compare/v0.8.1...v0.8.2 [v0.8.1]: https://github.com/piotrmurach/tty-command/compare/v0.8.0...v0.8.1 [v0.8.0]: https://github.com/piotrmurach/tty-command/compare/v0.7.0...v0.8.0 [v0.7.0]: https://github.com/piotrmurach/tty-command/compare/v0.6.0...v0.7.0 [v0.6.0]: https://github.com/piotrmurach/tty-command/compare/v0.5.0...v0.6.0 [v0.5.0]: https://github.com/piotrmurach/tty-command/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/piotrmurach/tty-command/compare/v0.3.3...v0.4.0 [v0.3.3]: https://github.com/piotrmurach/tty-command/compare/v0.3.2...v0.3.3 [v0.3.2]: https://github.com/piotrmurach/tty-command/compare/v0.3.1...v0.3.2 [v0.3.1]: https://github.com/piotrmurach/tty-command/compare/v0.3.0...v0.3.1 [v0.3.0]: https://github.com/piotrmurach/tty-command/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/piotrmurach/tty-command/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/piotrmurach/tty-command/compare/953ccdd...v0.1.0 tty-command-0.10.1/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006240 14012255006 0016621 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at piotr@piotrmurach.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ tty-command-0.10.1/Gemfile 0000664 0000000 0000000 00000000444 14012255006 0015315 0 ustar 00root root 0000000 0000000 source "https://rubygems.org" gemspec gem "json", "2.4.1" if RUBY_VERSION == "2.0.0" group :test do gem "simplecov", "~> 0.16.1" gem "coveralls", "~> 0.8.22" end if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0") group :perf do gem "memory_profiler", "~> 0.9.8" end end tty-command-0.10.1/LICENSE.txt 0000664 0000000 0000000 00000002121 14012255006 0015637 0 ustar 00root root 0000000 0000000 The MIT License (MIT) Copyright (c) 2016 Piotr Murach (https://piotrmurach.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tty-command-0.10.1/README.md 0000664 0000000 0000000 00000045642 14012255006 0015312 0 ustar 00root root 0000000 0000000
# TTY::Command [][gitter] [][gem] [][gh_actions_ci] [][appveyor] [][codeclimate] [][coverage] [][inchpages] [gitter]: https://gitter.im/piotrmurach/tty [gem]: http://badge.fury.io/rb/tty-command [gh_actions_ci]: https://github.com/piotrmurach/tty-command/actions?query=workflow%3ACI [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-command [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-command [coverage]: https://coveralls.io/github/piotrmurach/tty-command [inchpages]: http://inch-ci.org/github/piotrmurach/tty-command > Run external commands with pretty output logging and capture stdout, stderr and exit status. Redirect stdin, stdout and stderr of each command to a file or a string. **TTY::Command** provides independent command execution component for [TTY](https://github.com/piotrmurach/tty) toolkit. ## Motivation Complex software projects aren't just a single app. These projects usually spawn dozens or hundreds of supplementary standalone scripts which are just as important as the app itself. Examples include - data validation, deployment, monitoring, database maintenance, backup & restore, configuration management, crawling, ETL, analytics, log file processing, custom reports, etc. One of the contributors to **TTY::Command** counted 222 scripts in the `bin` directory for his startup. Why should we be handcuffed to `sh` or `bash` for these scripts when we could be using Ruby? Ruby is easier to write and more fun, and we gain a lot by using a better language. It's nice for everyone to just use Ruby everywhere. **TTY::Command** tries to add value in other ways. It'll halt automatically if a command fails. It's easy to get verbose or quiet output as appropriate, or even capture output and parse it with Ruby. Escaping arguments is a breeze. These are all areas where traditional shell scripts tend to fall flat. ## Installation Add this line to your application's Gemfile: ```ruby gem "tty-command" ``` And then execute: $ bundle Or install it yourself as: $ gem install tty-command ## Contents * [1. Usage](#1-usage) * [2. Interface](#2-interface) * [2.1. Run](#21-run) * [2.2. Run!](#22-run) * [2.3. Logging](#23-logging) * [2.3.1. Color](#231-color) * [2.3.2. UUID](#232-uuid) * [2.3.3. Only output on error](#233-only-output-on-error) * [2.3.4. Verbose](#234-verbose) * [2.4. Dry run](#24-dry-run) * [2.5. Wait](#25-wait) * [2.6. Test](#26-test) * [2.7. Ruby interpreter](#27-ruby-interpreter) * [3. Advanced Interface](#3-advanced-interface) * [3.1. Environment variables](#31-environment-variables) * [3.2. Options](#32-options) * [3.2.1. Redirection](#321-redirection) * [3.2.2. Handling input](#322-handling-input) * [3.2.3. Timeout](#323-timeout) * [3.2.4. Binary mode](#324-binary-mode) * [3.2.5. Signal](#325-signal) * [3.2.6. PTY(pseudo-terminal)](#326-ptypseudo-terminal) * [3.2.7. Current directory](#327-current-directory) * [3.2.8. User](#328-user) * [3.2.9. Group](#329-group) * [3.2.10. Umask](#3210-umask) * [3.3. Result](#33-result) * [3.3.1. success?](#331-success) * [3.3.2. failure?](#332-failure) * [3.3.3. exited?](#333-exited) * [3.3.4. each](#334-each) * [3.4. Custom printer](#34-custom-printer) * [4. Example](#4-example) ## 1. Usage Create a command instance and then run some commands: ```ruby require "tty-command" cmd = TTY::Command.new cmd.run("ls -la") cmd.run("echo Hello!") ``` Note that `run` will throw an exception if the command fails. This is already an improvement over ordinary shell scripts, which just keep on going when things go bad. That usually makes things worse. You can use the return value to capture stdout and stderr: ```ruby out, err = cmd.run("cat ~/.bashrc | grep alias") ``` Instead of using a plain old string, you can break up the arguments and they'll get escaped if necessary: ```ruby path = "hello world" FileUtils.touch(path) cmd.run("sum #{path}") # this will fail due to bad escaping cmd.run("sum", path) # this gets escaped automatically ``` ## 2. Interface ### 2.1 Run Run starts the specified command and waits for it to complete. The argument signature of `run` is as follows: `run([env], command, [argv1, ...], [options])` The `env`, `command` and `options` arguments are described in the following sections. For example, to display file contents: ```ruby cmd.run("cat file.txt") ``` If the command succeeds, a `TTY::Command::Result` is returned that records stdout and stderr: ```ruby out, err = cmd.run("date") puts "The date is #{out}" # => "The date is Tue 10 May 2016 22:30:15 BST\n" ``` You can also pass a block that gets invoked anytime stdout and/or stderr receive output: ```ruby cmd.run("long running script") do |out, err| output << out if out errors << err if err end ``` If the command fails (with a non-zero exit code), a `TTY::Command::ExitError` is raised. The `ExitError` message will include: * the name of command executed * the exit status * stdout bytes * stderr bytes If the error output is very long, the stderr may contain only a prefix, number of omitted bytes and suffix. ### 2.2 Run! If you expect a command to fail occasionally, use `run!` instead. Then you can detect failures and respond appropriately. For example: ```ruby if cmd.run!("which xyzzy").failure? cmd.run("brew install xyzzy") end ``` ### 2.3 Logging By default, when a command is run, the command and the output are printed to `stdout` using the `:pretty` printer. If you wish to change printer you can do so by passing a `:printer` option: * `:null` - no output * `:pretty` - colorful output * `:progress` - minimal output with green dot for success and F for failure * `:quiet` - only output actual command stdout and stderr like so: ```ruby cmd = TTY::Command.new(printer: :progress) ``` By default the printers log to `stdout` but this can be changed by passing an object that responds to `<<` message: ```ruby logger = Logger.new("dev.log") cmd = TTY::Command.new(output: logger) ``` You can force the printer to always in print in color by passing the `:color` option: ```ruby cmd = TTY::Command.new(color: true) ``` If the default printers don't meet your needs you can always create [a custom printer](#34-custom-printer) #### 2.3.1 Color When using printers you can switch off coloring by using `:color` option set to `false`. #### 2.3.2 UUID By default, when logging is enabled and `pretty` printer is used, each log entry is prefixed by specific command run uuid number. This number can be switched off using the `:uuid` option at initialization: ```ruby cmd = TTY::Command.new(uuid: false) cmd.run("rm -R all_my_files") # => # Running rm -r all_my_files # ... # Finished in 6 seconds with exit status 0 (successful) ``` or individually per command run: ```rub cmd = TTY::Command.new cmd.run("echo hello", uuid: false) # => # Running echo hello # hello # Finished in 0.003 seconds with exit status 0 (successful) ``` #### 2.3.3 Only output on error When using a command that can fail, setting `:only_output_on_error` option to `true` hides the output if the command succeeds: ```ruby cmd = TTY::Command.new cmd.run("non_failing_command", only_output_on_error: true) ``` This will only print the `Running` and `Finished` lines, while: ```ruby cmd.run("non_failing_command") ``` will also print any output that the `non_failing_command` might generate. Running either: ```ruby cmd.run("failing_command", only_output_on_error: true) ``` either: ```ruby cmd.run("failing_command") ``` will also print the output. *Setting this option will cause the output to show at once, at the end of the command.* #### 2.3.4 Verbose By default commands will produce warnings when, for example `pty` option is not supported on a given platform. You can switch off such warnings with `:verbose` option set to `false`. ```ruby cmd.run("echo '\e[32mColors!\e[0m'", pty: true, verbose: false) ``` ### 2.4 Dry run Sometimes it can be useful to put your script into a "dry run" mode that prints commands without actually running them. To simulate execution of the command use the `:dry_run` option: ```ruby cmd = TTY::Command.new(dry_run: true) cmd.run(:rm, "all_my_files") # => [123abc] (dry run) rm all_my_files ``` To check what mode the command is in use the `dry_run?` query helper: ```ruby cmd.dry_run? # => true ``` ### 2.5 Wait If you need to wait for a long running script and stop it when a given pattern has been matched use `wait` like so: ```ruby cmd.wait "tail -f /var/log/production.log", /something happened/ ``` ### 2.6 Test To simulate classic bash test command you case use `test` method with expression to check as a first argument: ```ruby if cmd.test "-e /etc/passwd" puts "Sweet..." else puts "Ohh no! Where is it?" exit 1 end ``` ### 2.7 Ruby interpreter In order to run a command with Ruby interpreter do: ```ruby cmd.ruby %q{-e "puts 'Hello world'"} ``` ## 3. Advanced Interface ### 3.1 Environment variables The environment variables need to be provided as hash entries, that can be set directly as a first argument: ```ruby cmd.run({"RAILS_ENV" => "PRODUCTION"}, :rails, "server") ``` or as an option with `:env` key: ```ruby cmd.run(:rails, "server", env: {rails_env: :production}) ``` When a value in env is nil, the variable is unset in the child process: ```ruby cmd.run(:echo, "hello", env: {foo: "bar", baz: nil}) ``` ### 3.2 Options When a hash is given in the last argument (options), it allows to specify a current directory, umask, user, group and zero or more fd redirects for the child process. #### 3.2.1 Redirection There are few ways you can redirect commands output. You can directly use shell redirection like so: ```ruby out, err = cmd.run("ls 1&>2") puts err # => # CHANGELOG.md # CODE_OF_CONDUCT.md # Gemfile # ... ``` You can provide redirection as additional hash options where the key is one of `:in`, `:out`, `:err`, an integer (a file descriptor for the child process), an IO or array. For example, `stderr` can be merged into stdout as follows: ```ruby cmd.run(:ls, :err => :out) cmd.run(:ls, :stderr => :stdout) cmd.run(:ls, 2 => 1) cmd.run(:ls, STDERR => :out) cmd.run(:ls, STDERR => STDOUT) ``` The hash key and value specify a file descriptor in the child process (stderr & stdout in the examples). You can also redirect to a file: ```ruby cmd.run(:cat, :in => "file") cmd.run(:cat, :in => open("/etc/passwd")) cmd.run(:ls, :out => "log") cmd.run(:ls, :out => "/dev/null") cmd.run(:ls, :out => "out.log", :err => "err.log") cmd.run(:ls, [:out, :err] => "log") cmd.run("ls 1>&2", :err => "log") ``` It is possible to specify flags and permissions of file creation explicitly by passing an array value: ```ruby cmd.run(:ls, :out => ["log", "w"]) # 0664 assumed cmd.run(:ls, :out => ["log", "w", 0600]) cmd.run(:ls, :out => ["log", File::WRONLY|File::EXCL|File::CREAT, 0600]) ``` You can, for example, read data from one source and output to another: ```ruby cmd.run("cat", :in => "Gemfile", :out => "gemfile.log") ``` #### 3.2.2 Handling Input You can provide input to stdin stream using the `:input` key. For instance, given the following executable called `cli` that expects name from `stdin`: ```ruby name = $stdin.gets puts "Your name: #{name}" ``` In order to execute `cli` with name input do: ```ruby cmd.run("cli", input: "Piotr\n") # => Your name: Piotr ``` Alternatively, you can pass input via the :in option, by passing a `StringIO` Object. This object might have more than one line, if the executed command reads more than once from STDIN. Assume you have run a program, that first asks for your email address and then for a password: ```ruby in_stream = StringIO.new in_stream.puts "username@example.com" in_stream.puts "password" in_stream.rewind cmd.run("my_cli_program", "login", in: in_stream).out ``` #### 3.2.3 Timeout You can timeout command execution by providing the `:timeout` option in seconds: ```ruby cmd.run("while test 1; sleep 1; done", timeout: 5) ``` And to set it for all commands do: ```ruby cmd = TTY::Command.new(timeout: 5) ``` Please run `examples/timeout.rb` to see timeout in action. #### 3.2.4 Binary mode By default the standard input, output and error are non-binary. However, you can change to read and write in binary mode by using the `:binmode` option like so: ```ruby cmd.run("echo 'hello'", binmode: true) ``` To set all commands to be run in binary mode do: ```ruby cmd = TTY::Command.new(binmode: true) ``` #### 3.2.5 Signal You can specify process termination signal other than the default `SIGTERM`: ```ruby cmd.run("whilte test1; sleep1; done", timeout: 5, signal: :KILL) ``` #### 3.2.6 PTY(pseudo terminal) The `:pty` configuration option causes the command to be executed in subprocess where each stream is a `pseudo terminal`. By default this options is set to `false`. If you require to interface with interactive subprocess then setting this option to `true` will enable a `pty` terminal device. For example, a command may emit colored output only if it is running via terminal device. You may also wish to run a program that waits for user input, and simulates typing in commands and reading responses. This option will only work on systems that support BSD pty devices such as Linux or OS X, and it will gracefully fallback to non-pty device on all the other. In order to run command in `pseudo terminal`, either set the flag globally for all commands: ```ruby cmd = TTY::Command.new(pty: true) ``` or individually for each executed command: ```ruby cmd.run("echo 'hello'", pty: true) ``` Please note that setting `:pty` to `true` may change how the command behaves. It's important to understand the difference between `interactive` and `non-interactive` modes. For example, executing `git log` to view the commit history in default `non-interactive` mode: ```ruby cmd.run("git log") # => finishes and produces full output ``` However, in `interactive` mode with `pty` flag on: ```ruby cmd.run("git log", pty: true) # => uses pager and waits for user input (never returns) ``` In addition, when pty device is used, any input to command may be echoed to the standard output, as well as some redirects may not work. #### 3.2.7 Current directory To change directory in which the command is run pass the `:chdir` option: ```ruby cmd.run(:echo, "hello", chdir: "/var/tmp") ``` #### 3.2.8 User To run command as a given user do: ```ruby cmd.run(:echo, "hello", user: "piotr") ``` #### 3.2.9 Group To run command as part of group do: ```ruby cmd.run(:echo, "hello", group: "devs") ``` #### 3.2.10 Umask To run command with umask do: ```ruby cmd.run(:echo, "hello", umask: "007") ``` ### 3.3 Result Each time you run command the stdout and stderr are captured and return as result. The result can be examined directly by casting it to tuple: ```ruby out, err = cmd.run(:echo, "Hello") ``` However, if you want to you can defer reading: ```ruby result = cmd.run(:echo, "Hello") result.out result.err ``` #### 3.3.1 success? To check if command exited successfully use `success?`: ```ruby result = cmd.run(:echo, "Hello") result.success? # => true ``` #### 3.3.2 failure? To check if command exited unsuccessfully use `failure?` or `failed?`: ```ruby result = cmd.run(:echo, "Hello") result.failure? # => false result.failed? # => false ``` #### 3.3.3 exited? To check if command ran to completion use `exited?` or `complete?`: ```ruby result = cmd.run(:echo, "Hello") result.exited? # => true result.complete? # => true ``` #### 3.3.4 each The result itself is an enumerable and allows you to iterate over the stdout output: ```ruby result = cmd.run(:ls, "-1") result.each { |line| puts line } # => # CHANGELOG.md # CODE_OF_CONDUCT.md # Gemfile # Gemfile.lock # ... # lib # pkg # spec # tasks ``` By default the linefeed character `\n` is used as a delimiter but this can be changed either globally by calling `record_separator`: ```ruby TTY::Command.record_separator = "\n\r" ``` or configured per `each` call by passing delimiter as an argument: ```ruby cmd.run(:ls, "-1").each("\t") { ... } ``` ### 3.4 Custom printer If the built-in printers do not meet your requirements you can create your own. A printer is a regular Ruby class that can be registered through `:printer` option to receive notifications about received command data. As the command runs the custom printer will be notified when the command starts, when data is printed to stdout, when data is printed to stderr and when the command exits. Please see [lib/tty/command/printers/abstract.rb](https://github.com/piotrmurach/tty-command/blob/master/lib/tty/command/printers/abstract.rb) for a full set of methods that you can override. At the very minimum you need to specify the `write` method that will be called during the lifecycle of command execution. The `write` accepts two arguments, first the currently run command instance and second the message to be printed: ```ruby CustomPrinter < TTY::Command::Printers::Abstract def write(cmd, message) puts cmd.to_command + message end end cmd = TTY::Command.new(printer: CustomPrinter) ``` ## 4. Example Here's a slightly more elaborate example to illustrate how tty-command can improve on plain old shell scripts. This example installs a new version of Ruby on an Ubuntu machine. ```ruby cmd = TTY::Command.new # dependencies cmd.run "apt-get -y install build-essential checkinstall" # fetch ruby if necessary if !File.exists?("ruby-2.3.0.tar.gz") puts "Downloading..." cmd.run "wget http://ftp.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0.tar.gz" cmd.run "tar xvzf ruby-2.3.0.tar.gz" end # now install Dir.chdir("ruby-2.3.0") do puts "Building..." cmd.run "./configure --prefix=/usr/local" cmd.run "make" end ``` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-command. 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. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ## Copyright Copyright (c) 2016 Piotr Murach. See LICENSE for further details. tty-command-0.10.1/Rakefile 0000664 0000000 0000000 00000000256 14012255006 0015470 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "bundler/gem_tasks" FileList["tasks/**/*.rake"].each(&method(:import)) task default: :spec desc "Run all specs" task ci: %w[ spec ] tty-command-0.10.1/appveyor.yml 0000664 0000000 0000000 00000001306 14012255006 0016410 0 ustar 00root root 0000000 0000000 --- skip_commits: files: - "benchmarks/**" - "bin/**" - "examples/**" - "*.md" install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - ruby --version - gem --version - bundle install build: off test_script: - bundle exec rake ci environment: matrix: - ruby_version: "200" - ruby_version: "200-x64" - ruby_version: "21" - ruby_version: "21-x64" - ruby_version: "22" - ruby_version: "22-x64" - ruby_version: "23" - ruby_version: "23-x64" - ruby_version: "24" - ruby_version: "24-x64" - ruby_version: "25" - ruby_version: "25-x64" - ruby_version: "26" - ruby_version: "26-x64" matrix: allow_failures: - ruby_version: "25" tty-command-0.10.1/benchmarks/ 0000775 0000000 0000000 00000000000 14012255006 0016135 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/benchmarks/memory.rb 0000664 0000000 0000000 00000000346 14012255006 0017775 0 ustar 00root root 0000000 0000000 # encoding: utf-8 # require 'memory_profiler' require 'tty-command' report = MemoryProfiler.report do cmd = TTY::Command.new(color: false) cmd.run("echo 'hello world!'") end report.pretty_print(to_file: 'memory_report.txt') tty-command-0.10.1/bin/ 0000775 0000000 0000000 00000000000 14012255006 0014570 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/bin/console 0000775 0000000 0000000 00000000133 14012255006 0016155 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby require 'bundler/setup' require 'tty-command' require 'irb' IRB.start tty-command-0.10.1/bin/setup 0000775 0000000 0000000 00000000112 14012255006 0015650 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install tty-command-0.10.1/examples/ 0000775 0000000 0000000 00000000000 14012255006 0015636 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/examples/bash.rb 0000664 0000000 0000000 00000000277 14012255006 0017106 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new f = "file" if cmd.test("[ -f #{f} ]") puts "#{f} already exists!" else cmd.run :touch, f end tty-command-0.10.1/examples/basic.rb 0000664 0000000 0000000 00000000233 14012255006 0017242 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new out, = cmd.run(:echo, "hello world!") puts "Result: #{out}" tty-command-0.10.1/examples/buffer.rb 0000664 0000000 0000000 00000000173 14012255006 0017435 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new(pty: true) cmd.run("rubocop") tty-command-0.10.1/examples/cli 0000775 0000000 0000000 00000000102 14012255006 0016324 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby name = $stdin.gets puts "Your name: #{name}" tty-command-0.10.1/examples/env.rb 0000664 0000000 0000000 00000000261 14012255006 0016752 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new out, = cmd.run("env | grep FOO", env: { "FOO" => "hello" }) puts "Result: #{out}" tty-command-0.10.1/examples/logger.rb 0000664 0000000 0000000 00000000366 14012255006 0017447 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "logger" require_relative "../lib/tty-command" logger = Logger.new("dev.log") logger.level = Logger::WARN logger.warn("Logger captured:") cmd = TTY::Command.new(output: logger, color: false) cmd.run(:ls) tty-command-0.10.1/examples/output.rb 0000664 0000000 0000000 00000000362 14012255006 0017524 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new cmd.run("i=0; while true; do i=$[$i+1]; echo 'hello '$i; sleep 1; done") do |out, err| if out =~ /.*5.*/ raise ArgumentError, "BOOM" end end tty-command-0.10.1/examples/pty.rb 0000664 0000000 0000000 00000000263 14012255006 0017000 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new path = File.expand_path("../spec/fixtures/color", __dir__) cmd.run(path, pty: true) tty-command-0.10.1/examples/redirect_stderr.rb 0000664 0000000 0000000 00000000263 14012255006 0021350 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new out, err = cmd.run("echo 'hello'", out: :err) puts "out: #{out}" puts "err: #{err}" tty-command-0.10.1/examples/redirect_stdin.rb 0000664 0000000 0000000 00000000425 14012255006 0021166 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "stringio" require_relative "../lib/tty-command" cli = File.expand_path("cli", __dir__) cmd = TTY::Command.new stdin = StringIO.new stdin.puts "hello" stdin.puts "world" stdin.rewind out, = cmd.run(cli, in: stdin) puts "out: #{out}" tty-command-0.10.1/examples/redirect_stdout.rb 0000664 0000000 0000000 00000000256 14012255006 0021371 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new out, err = cmd.run(:ls, out: "ls.log") puts "OUT>> #{out}" puts "ERR>> #{err}" tty-command-0.10.1/examples/stdin_input.rb 0000664 0000000 0000000 00000000315 14012255006 0020522 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "pathname" require_relative "../lib/tty-command" cmd = TTY::Command.new cli = Pathname.new("examples/cli") out, = cmd.run(cli, input: "Piotr\n") puts "out: #{out}" tty-command-0.10.1/examples/threaded.rb 0000664 0000000 0000000 00000000361 14012255006 0017743 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new threads = [] 3.times do |i| th = Thread.new do 10.times { cmd.run("echo th#{i}; sleep 0.1") } end threads << th end threads.each(&:join) tty-command-0.10.1/examples/timeout.rb 0000664 0000000 0000000 00000000361 14012255006 0017651 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new begin cmd.run("while test 1; do echo 'hello'; sleep 1; done", timeout: 5, signal: :KILL) rescue TTY::Command::TimeoutExceeded puts "BOOM!" end tty-command-0.10.1/examples/timeout_input.rb 0000664 0000000 0000000 00000000463 14012255006 0021073 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "../lib/tty-command" cmd = TTY::Command.new path = File.expand_path("../spec/fixtures/infinite_input", __dir__) range = 1..Float::INFINITY infinite_input = range.lazy.map { |x| x }.first(10_000).join("\n") cmd.run(path, input: infinite_input, timeout: 2) tty-command-0.10.1/examples/wait.rb 0000664 0000000 0000000 00000000502 14012255006 0017124 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "logger" require_relative "../lib/tty-command" logger = Logger.new("dev.log") cmd = TTY::Command.new Thread.new do 10.times do |i| sleep 1 if i == 5 logger << "error\n" else logger << "hello #{i}\n" end end end cmd.wait("tail -f dev.log", /error/) tty-command-0.10.1/lib/ 0000775 0000000 0000000 00000000000 14012255006 0014566 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/lib/tty-command.rb 0000664 0000000 0000000 00000000037 14012255006 0017347 0 ustar 00root root 0000000 0000000 require_relative "tty/command" tty-command-0.10.1/lib/tty/ 0000775 0000000 0000000 00000000000 14012255006 0015406 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/lib/tty/command.rb 0000664 0000000 0000000 00000013206 14012255006 0017353 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "rbconfig" require_relative "command/cmd" require_relative "command/exit_error" require_relative "command/dry_runner" require_relative "command/process_runner" require_relative "command/printers/null" require_relative "command/printers/pretty" require_relative "command/printers/progress" require_relative "command/printers/quiet" require_relative "command/version" module TTY class Command ExecuteError = Class.new(StandardError) TimeoutExceeded = Class.new(StandardError) # Path to the current Ruby RUBY = ENV["RUBY"] || ::File.join( RbConfig::CONFIG["bindir"], RbConfig::CONFIG["ruby_install_name"] + RbConfig::CONFIG["EXEEXT"] ) WIN_PLATFORMS = /cygwin|mswin|mingw|bccwin|wince|emx/.freeze def self.record_separator @record_separator ||= $/ end def self.record_separator=(sep) @record_separator = sep end def self.windows? !!(RbConfig::CONFIG["host_os"] =~ WIN_PLATFORMS) end attr_reader :printer # Initialize a Command object # # @param [Hash] options # @option options [IO] :output # the stream to which printer prints, defaults to stdout # @option options [Symbol] :printer # the printer to use for output logging, defaults to :pretty # @option options [Symbol] :dry_run # the mode for executing command # # @api public def initialize(**options) @output = options.fetch(:output) { $stdout } @color = options.fetch(:color) { true } @uuid = options.fetch(:uuid) { true } @printer_name = options.fetch(:printer) { :pretty } @dry_run = options.fetch(:dry_run) { false } @printer = use_printer(@printer_name, color: @color, uuid: @uuid) @cmd_options = {} @cmd_options[:verbose] = options.fetch(:verbose, true) @cmd_options[:pty] = true if options[:pty] @cmd_options[:binmode] = true if options[:binmode] @cmd_options[:timeout] = options[:timeout] if options[:timeout] end # Start external executable in a child process # # @example # cmd.run(command, [argv1, ..., argvN], [options]) # # @example # cmd.run(command, ...) do |result| # ... # end # # @param [String] command # the command to run # # @param [Array[String]] argv # an array of string arguments # # @param [Hash] options # hash of operations to perform # @option options [String] :chdir # The current directory. # @option options [Integer] :timeout # Maximum number of seconds to allow the process # to run before aborting with a TimeoutExceeded # exception. # @option options [Symbol] :signal # Signal used on timeout, SIGKILL by default # # @yield [out, err] # Yields stdout and stderr output whenever available # # @raise [ExitError] # raised when command exits with non-zero code # # @api public def run(*args, &block) cmd = command(*args) result = execute_command(cmd, &block) if result && result.failure? raise ExitError.new(cmd.to_command, result) end result end # Start external executable without raising ExitError # # @example # cmd.run!(command, [argv1, ..., argvN], [options]) # # @api public def run!(*args, &block) cmd = command(*args) execute_command(cmd, &block) end # Wait on long running script until output matches a specific pattern # # @example # cmd.wait 'tail -f /var/log/php.log', /something happened/ # # @api public def wait(*args) pattern = args.pop unless pattern raise ArgumentError, "Please provide condition to wait for" end run(*args) do |out, _| raise if out =~ /#{pattern}/ end rescue ExitError # noop end # Execute shell test command # # @api public def test(*args) run!(:test, *args).success? end # Run Ruby interperter with the given arguments # # @example # ruby %q{-e "puts 'Hello world'"} # # @api public def ruby(*args, &block) options = args.last.is_a?(Hash) ? args.pop : {} if args.length > 1 run(*([RUBY] + args + [options]), &block) else run("#{RUBY} #{args.first}", options, &block) end end # Check if in dry mode # # @return [Boolean] # # @public def dry_run? @dry_run end private # @api private def command(*args) cmd = Cmd.new(*args) cmd.update(@cmd_options) cmd end # @api private def execute_command(cmd, &block) dry_run = @dry_run || cmd.options[:dry_run] || false @runner = select_runner(dry_run).new(cmd, @printer, &block) @runner.run! end # @api private def use_printer(class_or_name, options) if class_or_name.is_a?(TTY::Command::Printers::Abstract) return class_or_name end if class_or_name.is_a?(Class) class_or_name else find_printer_class(class_or_name) end.new(@output, options) end # Find printer class or fail # # @raise [ArgumentError] # # @api private def find_printer_class(name) const_name = name.to_s.split("_").map(&:capitalize).join.to_sym if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name) raise ArgumentError, %(Unknown printer type "#{name}") end TTY::Command::Printers.const_get(const_name) end # @api private def select_runner(dry_run) if dry_run DryRunner else ProcessRunner end end end # Command end # TTY tty-command-0.10.1/lib/tty/command/ 0000775 0000000 0000000 00000000000 14012255006 0017024 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/lib/tty/command/child_process.rb 0000664 0000000 0000000 00000013175 14012255006 0022201 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "tempfile" require "securerandom" require "io/console" module TTY class Command module ChildProcess # Execute command in a child process with all IO streams piped # in and out. The interface is similar to Process.spawn # # The caller should ensure that all IO objects are closed # when the child process is finished. However, when block # is provided this will be taken care of automatically. # # @param [Cmd] cmd # the command to spawn # # @return [pid, stdin, stdout, stderr] # # @api public def spawn(cmd) process_opts = normalize_redirect_options(cmd.options) binmode = cmd.options[:binmode] || false pty = cmd.options[:pty] || false verbose = cmd.options[:verbose] pty = try_loading_pty(verbose) if pty require("pty") if pty # load within this scope # Create pipes in_rd, in_wr = pty ? PTY.open : IO.pipe("utf-8") # reading out_rd, out_wr = pty ? PTY.open : IO.pipe("utf-8") # writing err_rd, err_wr = pty ? PTY.open : IO.pipe("utf-8") # error in_wr.sync = true if binmode in_wr.binmode out_rd.binmode err_rd.binmode end if pty in_wr.raw! out_wr.raw! err_wr.raw! end # redirect fds opts = { in: in_rd, out: out_wr, err: err_wr } unless TTY::Command.windows? close_child_fds = { in_wr => :close, out_rd => :close, err_rd => :close } opts.merge!(close_child_fds) end opts.merge!(process_opts) pid = Process.spawn(cmd.to_command, opts) # close streams in parent process talking to the child close_fds(in_rd, out_wr, err_wr) tuple = [pid, in_wr, out_rd, err_rd] if block_given? begin return yield(*tuple) ensure # ensure parent pipes are closed close_fds(in_wr, out_rd, err_rd) end else tuple end end module_function :spawn # Close all streams # @api private def close_fds(*fds) fds.each { |fd| fd && !fd.closed? && fd.close } end module_function :close_fds # Try loading pty module # # @return [Boolean] # # @api private def try_loading_pty(verbose = false) require 'pty' true rescue LoadError warn("Requested PTY device but the system doesn't support it.") if verbose false end module_function :try_loading_pty # Normalize spawn fd into :in, :out, :err keys. # # @return [Hash] # # @api private def normalize_redirect_options(options) options.reduce({}) do |opts, (key, value)| if fd?(key) spawn_key, spawn_value = convert(key, value) opts[spawn_key] = spawn_value elsif key.is_a?(Array) && key.all?(&method(:fd?)) key.each do |k| spawn_key, spawn_value = convert(k, value) opts[spawn_key] = spawn_value end end opts end end module_function :normalize_redirect_options # Convert option pari to recognized spawn option pair # # @api private def convert(spawn_key, spawn_value) key = fd_to_process_key(spawn_key) value = spawn_value if key.to_s == "in" value = convert_to_fd(spawn_value) end if fd?(spawn_value) value = fd_to_process_key(spawn_value) value = [:child, value] # redirect in child process end [key, value] end module_function :convert # Determine if object is a fd # # @return [Boolean] # # @api private def fd?(object) case object when :stdin, :stdout, :stderr, :in, :out, :err, STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, ::IO true when ::Integer object >= 0 else respond_to?(:to_i) && !object.to_io.nil? end end module_function :fd? # Convert fd to name :in, :out, :err # # @api private def fd_to_process_key(object) case object when STDIN, $stdin, :in, :stdin, 0 :in when STDOUT, $stdout, :out, :stdout, 1 :out when STDERR, $stderr, :err, :stderr, 2 :err when Integer object >= 0 ? IO.for_fd(object) : nil when IO object when respond_to?(:to_io) object.to_io else raise ExecuteError, "Wrong execute redirect: #{object.inspect}" end end module_function :fd_to_process_key # Convert file name to file handle # # @api private def convert_to_fd(object) return object if fd?(object) if object.is_a?(::String) && ::File.exist?(object) return object end tmp = ::Tempfile.new(::SecureRandom.uuid.split("-")[0]) content = try_reading(object) tmp.write(content) tmp.rewind tmp end module_function :convert_to_fd # Attempts to read object content # # @api private def try_reading(object) if object.respond_to?(:read) object.read elsif object.respond_to?(:to_s) object.to_s else object end end module_function :try_reading end # ChildProcess end # Command end # TTY tty-command-0.10.1/lib/tty/command/cmd.rb 0000664 0000000 0000000 00000007203 14012255006 0020116 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "securerandom" require "shellwords" module TTY class Command # Encapsulates the executed command # # @api private class Cmd # A string command name, or shell program # @api public attr_reader :command # A string arguments # @api public attr_reader :argv # Hash of operations to peform # @api public attr_reader :options # Unique identifier # @api public attr_reader :uuid # Flag that controls whether to print the output only on error or not attr_reader :only_output_on_error # Initialize a new Cmd object # # @api private def initialize(env_or_cmd, *args) opts = args.last.respond_to?(:to_hash) ? args.pop : {} if env_or_cmd.respond_to?(:to_hash) @env = env_or_cmd unless command = args.shift raise ArgumentError, "Cmd requires command argument" end else command = env_or_cmd end if args.empty? && cmd = command.to_s raise ArgumentError, "No command provided" if cmd.empty? @command = sanitize(cmd) @argv = [] else if command.respond_to?(:to_ary) @command = sanitize(command[0]) args.unshift(*command[1..-1]) else @command = sanitize(command) end @argv = args.map { |i| Shellwords.escape(i) } end @env ||= {} @options = opts @uuid = SecureRandom.uuid.split("-")[0] @only_output_on_error = opts.fetch(:only_output_on_error) { false } freeze end # Extend command options if keys don't already exist # # @api public def update(options) @options.update(options.merge(@options)) end # The shell environment variables # # @api public def environment @env.merge(options.fetch(:env, {})) end def environment_string environment.map do |key, val| converted_key = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s escaped_val = val.to_s.gsub(/"/, '\"') %(#{converted_key}="#{escaped_val}") end.join(" ") end def evars(value, &block) return (value || block) unless environment.any? "( export #{environment_string} ; #{value || block.call} )" end def umask(value) return value unless options[:umask] %(umask #{options[:umask]} && %s) % [value] end def chdir(value) return value unless options[:chdir] %(cd #{Shellwords.escape(options[:chdir])} && #{value}) end def user(value) return value unless options[:user] vars = environment.any? ? "#{environment_string} " : "" %(sudo -u #{options[:user]} #{vars}-- sh -c '%s') % [value] end def group(value) return value unless options[:group] %(sg #{options[:group]} -c \\\"%s\\\") % [value] end # Clear environment variables except specified by env # # @api public def with_clean_env end # Assemble full command # # @api public def to_command chdir(umask(evars(user(group(to_s))))) end # @api public def to_s [command.to_s, *Array(argv)].join(" ") end # @api public def to_hash { command: command, argv: argv, uuid: uuid } end private # Coerce to string # # @api private def sanitize(value) value.to_s.dup end end # Cmd end # Command end # TTY tty-command-0.10.1/lib/tty/command/dry_runner.rb 0000664 0000000 0000000 00000001124 14012255006 0021536 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "result" module TTY class Command class DryRunner attr_reader :cmd def initialize(cmd, printer) @cmd = cmd @printer = printer end # Show command without running # # @api public def run!(*) cmd.to_command message = "#{@printer.decorate("(dry run)", :blue)} " + @printer.decorate(cmd.to_command, :yellow, :bold) @printer.write(cmd, message, cmd.uuid) Result.new(0, "", "") end end # DryRunner end # Command end # TTY tty-command-0.10.1/lib/tty/command/exit_error.rb 0000664 0000000 0000000 00000001463 14012255006 0021537 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true module TTY class Command # An ExitError reports an unsuccessful exit by command. # # The error message includes: # * the name of command executed # * the exit status # * stdout bytes # * stderr bytes # # @api private class ExitError < RuntimeError def initialize(cmd_name, result) super(info(cmd_name, result)) end def info(cmd_name, result) "Running `#{cmd_name}` failed with\n" \ " exit status: #{result.exit_status}\n" \ " stdout: #{extract_output(result.out)}\n" \ " stderr: #{extract_output(result.err)}\n" end def extract_output(value) (value || "").strip.empty? ? "Nothing written" : value.strip end end # ExitError end # Command end # TTY tty-command-0.10.1/lib/tty/command/printers/ 0000775 0000000 0000000 00000000000 14012255006 0020672 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/lib/tty/command/printers/abstract.rb 0000664 0000000 0000000 00000002261 14012255006 0023023 0 ustar 00root root 0000000 0000000 require "pastel" module TTY class Command module Printers class Abstract extend Forwardable def_delegators :@color, :decorate attr_reader :output, :options attr_accessor :out_data, :err_data # Initialize a Printer object # # @param [IO] output # the printer output # # @api public def initialize(output, options = {}) @output = output @options = options @enabled = options.fetch(:color, true) @color = ::Pastel.new(enabled: @enabled) @out_data = "" @err_data = "" end def print_command_start(cmd, *args) write(cmd.to_command + "#{args.join}") end def print_command_out_data(cmd, *args) write(args.join(" ")) end def print_command_err_data(cmd, *args) write(args.join(" ")) end def print_command_exit(cmd, *args) write(args.join(" ")) end def write(cmd, message) raise NotImplemented, "Abstract printer cannot be used" end end # Abstract end # Printers end # Command end # TTY tty-command-0.10.1/lib/tty/command/printers/null.rb 0000664 0000000 0000000 00000000336 14012255006 0022173 0 ustar 00root root 0000000 0000000 require_relative "abstract" module TTY class Command module Printers class Null < Abstract def write(*) # Do nothing end end # Null end # Printers end # Command end # TTY tty-command-0.10.1/lib/tty/command/printers/pretty.rb 0000664 0000000 0000000 00000004350 14012255006 0022550 0 ustar 00root root 0000000 0000000 require_relative "abstract" module TTY class Command module Printers class Pretty < Abstract TIME_FORMAT = "%5.3f %s" def initialize(*) super @uuid = options.fetch(:uuid, true) end def print_command_start(cmd, *args) message = ["Running #{decorate(cmd.to_command, :yellow, :bold)}"] message << args.map(&:chomp).join(" ") unless args.empty? write(cmd, message.join) end def print_command_out_data(cmd, *args) message = args.map(&:chomp).join(" ") write(cmd, "\t#{message}", out_data) end def print_command_err_data(cmd, *args) message = args.map(&:chomp).join(" ") write(cmd, "\t" + decorate(message, :red), err_data) end def print_command_exit(cmd, status, runtime, *args) if cmd.only_output_on_error && !status.zero? output << out_data output << err_data end runtime = TIME_FORMAT % [runtime, pluralize(runtime, "second")] message = ["Finished in #{runtime}"] message << " with exit status #{status}" if status message << " (#{success_or_failure(status)})" write(cmd, message.join) end # Write message out to output # # @api private def write(cmd, message, data = nil) cmd_set_uuid = cmd.options.fetch(:uuid, true) uuid_needed = cmd.options[:uuid].nil? ? @uuid : cmd_set_uuid out = [] if uuid_needed out << "[#{decorate(cmd.uuid, :green)}] " unless cmd.uuid.nil? end out << "#{message}\n" target = (cmd.only_output_on_error && !data.nil?) ? data : output target << out.join end private # Pluralize word based on a count # # @api private def pluralize(count, word) "#{word}#{'s' unless count.to_i == 1}" end # @api private def success_or_failure(status) if status == 0 decorate("successful", :green, :bold) else decorate("failed", :red, :bold) end end end # Pretty end # Printers end # Command end # TTY tty-command-0.10.1/lib/tty/command/printers/progress.rb 0000664 0000000 0000000 00000001037 14012255006 0023064 0 ustar 00root root 0000000 0000000 require_relative "abstract" module TTY class Command module Printers class Progress < Abstract def print_command_exit(cmd, status, runtime, *args) output.print(success_or_failure(status)) end def write(*) end private # @api private def success_or_failure(status) if status == 0 decorate(".", :green) else decorate("F", :red) end end end # Progress end # Printers end # Command end # TTY tty-command-0.10.1/lib/tty/command/printers/quiet.rb 0000664 0000000 0000000 00000001477 14012255006 0022357 0 ustar 00root root 0000000 0000000 require_relative "abstract" module TTY class Command module Printers class Quiet < Abstract def print_command_start(cmd) # quiet end def print_command_out_data(cmd, *args) write(cmd, args.join(" "), out_data) end def print_command_err_data(cmd, *args) write(cmd, args.join(" "), err_data) end def print_command_exit(cmd, status, *args) unless !cmd.only_output_on_error || status.zero? output << out_data output << err_data end # quiet end def write(cmd, message, data = nil) target = (cmd.only_output_on_error && !data.nil?) ? data : output target << message end end # Progress end # Printers end # Command end # TTY tty-command-0.10.1/lib/tty/command/process_runner.rb 0000664 0000000 0000000 00000012750 14012255006 0022425 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require "thread" require_relative "child_process" require_relative "result" require_relative "truncator" module TTY class Command class ProcessRunner # the command to be spawned attr_reader :cmd # Initialize a Runner object # # @param [Printer] printer # the printer to use for logging # # @api private def initialize(cmd, printer, &block) @cmd = cmd @timeout = cmd.options[:timeout] @input = cmd.options[:input] @signal = cmd.options[:signal] || "SIGKILL" @binmode = cmd.options[:binmode] @printer = printer @block = block end # Execute child process # # Write the input if provided to the child's stdin and read # the contents of both the stdout and stderr. # # If a block is provided then yield the stdout and stderr content # as its being read. # # @api public def run! @printer.print_command_start(cmd) start = Time.now pid, stdin, stdout, stderr = ChildProcess.spawn(cmd) write_stream(stdin, @input) stdout_data, stderr_data = read_streams(stdout, stderr) status = waitpid(pid) runtime = Time.now - start @printer.print_command_exit(cmd, status, runtime) Result.new(status, stdout_data, stderr_data, runtime) ensure [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? } if pid # Ensure no zombie processes ::Process.detach(pid) terminate(pid) end end # Stop a process marked by pid # # @param [Integer] pid # # @api public def terminate(pid) ::Process.kill(@signal, pid) rescue nil end private # The buffer size for reading stdout and stderr BUFSIZE = 16 * 1024 # @api private def handle_timeout(runtime) return unless @timeout t = @timeout - runtime raise TimeoutExceeded if t < 0.0 end # Write the input to the process stdin # # @api private def write_stream(stream, input) start = Time.now writers = [input && stream].compact while writers.any? ready = IO.select(nil, writers, writers, @timeout) raise TimeoutExceeded if ready.nil? ready[1].each do |writer| begin err = nil size = writer.write(@input) input = input.byteslice(size..-1) rescue IO::WaitWritable rescue Errno::EPIPE => err # The pipe closed before all input written # Probably process exited prematurely writer.close writers.delete(writer) end if err || input.bytesize == 0 writer.close writers.delete(writer) end # control total time spent writing runtime = Time.now - start handle_timeout(runtime) end end end # Read stdout & stderr streams in the background # # @param [IO] stdout # @param [IO] stderr # # @api private def read_streams(stdout, stderr) stdout_data = [] stderr_data = Truncator.new out_handler = ->(data) { stdout_data << data @printer.print_command_out_data(cmd, data) @block.(data, nil) if @block } err_handler = ->(data) { stderr_data << data @printer.print_command_err_data(cmd, data) @block.(nil, data) if @block } stdout_thread = read_stream(stdout, out_handler) stderr_thread = read_stream(stderr, err_handler) stdout_thread.join stderr_thread.join encoding = @binmode ? Encoding::BINARY : Encoding::UTF_8 [ stdout_data.join.force_encoding(encoding), stderr_data.read.dup.force_encoding(encoding) ] end # Read stream and invoke handler when data becomes available # # @param [IO] stream # the stream to read data from # @param [Proc] handler # the handler to call when data becomes available # # @api private def read_stream(stream, handler) Thread.new do if Thread.current.respond_to?(:report_on_exception) Thread.current.report_on_exception = false end Thread.current[:cmd_start] = Time.now readers = [stream] while readers.any? ready = IO.select(readers, nil, readers, @timeout) raise TimeoutExceeded if ready.nil? ready[0].each do |reader| begin chunk = reader.readpartial(BUFSIZE) handler.(chunk) # control total time spent reading runtime = Time.now - Thread.current[:cmd_start] handle_timeout(runtime) rescue Errno::EAGAIN, Errno::EINTR rescue EOFError, Errno::EPIPE, Errno::EIO # thrown by PTY readers.delete(reader) reader.close end end end end end # @api private def waitpid(pid) _pid, status = ::Process.waitpid2(pid, ::Process::WUNTRACED) status.exitstatus || status.termsig if _pid rescue Errno::ECHILD # In JRuby, waiting on a finished pid raises. end end # ProcessRunner end # Command end # TTY tty-command-0.10.1/lib/tty/command/result.rb 0000664 0000000 0000000 00000003411 14012255006 0020666 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true module TTY class Command # Encapsulates the information on the command executed # # @api public class Result include Enumerable # All data written out to process's stdout stream attr_reader :out alias stdout out # All data written out to process's stdin stream attr_reader :err alias stderr err # Total command execution time attr_reader :runtime # Create a result # # @api public def initialize(status, out, err, runtime = 0.0) @status = status @out = out @err = err @runtime = runtime end # Enumerate over output lines # # @param [String] separator # # @api public def each(separator = nil, &block) sep = separator || TTY::Command.record_separator return unless @out elements = @out.split(sep) if block_given? elements.each(&block) else elements.to_enum end end # Information on how the process exited # # @api public def exit_status @status end alias exitstatus exit_status alias status exit_status def to_i @status end def to_s @status.to_s end def to_ary [@out, @err] end def exited? @status != nil end alias complete? exited? def success? exited? ? @status.zero? : false end def failure? !success? end alias failed? failure? def ==(other) return false unless other.is_a?(TTY::Command::Result) @status == other.to_i && to_ary == other.to_ary end end # Result end # Command end # TTY tty-command-0.10.1/lib/tty/command/truncator.rb 0000664 0000000 0000000 00000005037 14012255006 0021377 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true module TTY class Command # Retains the first N bytes and the last N bytes from written content # # @api private class Truncator # Default maximum byte size for prefix & suffix DEFAULT_SIZE = 32 << 10 # Create a Truncator # # @param [Hash] options # @option options [Number] max_size # # @api public def initialize(options = {}) @max_size = options.fetch(:max_size) { DEFAULT_SIZE } @prefix = "" @suffix = "" @skipped = 0 end # Write content # # @param [String] content # the content to write # # @return [nil] # # @api public def write(content) content = content.to_s.dup content, @prefix = append(content, @prefix) if (over = (content.bytesize - @max_size)) > 0 content = content.byteslice(over..-1) @skipped += over end content, @suffix = append(content, @suffix) # suffix is full but we still have content to write while content.bytesize > 0 content = copy(content, @suffix) end end alias << write # Truncated representation of the content # # @return [String] # # @api public def read return @prefix if @suffix.empty? if @skipped.zero? return @prefix << @suffix end @prefix + "\n... omitting #{@skipped} bytes ...\n" + @suffix end alias to_s read private # Copy minimum bytes from source to destination # # @return [String] # the remaining content # # @api private def copy(value, dest) bytes = value.bytesize n = bytes < dest.bytesize ? bytes : dest.bytesize head, tail = dest.byteslice(0...n), dest.byteslice(n..-1) dest.replace("#{tail}#{value[0...n]}") @skipped += head.bytesize value.byteslice(n..-1) end # Append value to destination # # @param [String] value # # @param [String] dst # # @api private def append(value, dst) remain = @max_size - dst.bytesize remaining = "" if remain > 0 value_bytes = value.to_s.bytesize offset = value_bytes < remain ? value_bytes : remain remaining = value.byteslice(0...offset) value = value.byteslice(offset..-1) end [value, dst + remaining] end end # Truncator end # Command end # TTY tty-command-0.10.1/lib/tty/command/version.rb 0000664 0000000 0000000 00000000153 14012255006 0021035 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true module TTY class Command VERSION = "0.10.1" end # Command end # TTY tty-command-0.10.1/spec/ 0000775 0000000 0000000 00000000000 14012255006 0014752 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/spec/fixtures/ 0000775 0000000 0000000 00000000000 14012255006 0016623 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/spec/fixtures/cli 0000775 0000000 0000000 00000000140 14012255006 0017313 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true name = $stdin.gets puts "Your name: #{name}" tty-command-0.10.1/spec/fixtures/color 0000775 0000000 0000000 00000000203 14012255006 0017662 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true out = "colored" if $stdout.tty? out = "\e[32m#{out}\e[0m" end puts "#{out}\n" tty-command-0.10.1/spec/fixtures/infinite_input 0000775 0000000 0000000 00000000171 14012255006 0021574 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true loop do name = $stdin.gets puts "NAME: #{name}" sleep 0.001 end tty-command-0.10.1/spec/fixtures/infinite_no_output 0000775 0000000 0000000 00000000113 14012255006 0022465 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true loop do sleep 0.1 end tty-command-0.10.1/spec/fixtures/infinite_output 0000775 0000000 0000000 00000000116 14012255006 0021774 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true loop do puts "hello" end tty-command-0.10.1/spec/fixtures/non_zero_exit 0000775 0000000 0000000 00000000106 14012255006 0021430 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true puts "nooo" exit 1 tty-command-0.10.1/spec/fixtures/phased_output 0000775 0000000 0000000 00000000135 14012255006 0021434 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true 10.times do print "." sleep 0.001 end tty-command-0.10.1/spec/fixtures/stream 0000775 0000000 0000000 00000000136 14012255006 0020044 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true 3.times do |i| puts "hello #{i + 1}" end tty-command-0.10.1/spec/fixtures/zero_exit 0000775 0000000 0000000 00000000106 14012255006 0020556 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby # frozen_string_literal: true puts "yess" exit 0 tty-command-0.10.1/spec/spec_helper.rb 0000664 0000000 0000000 00000003422 14012255006 0017571 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true if ENV["COVERAGE"] == "true" require "simplecov" require "coveralls" SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ]) SimpleCov.start do command_name "spec" add_filter "spec" end end require "tty-command" require "tmpdir" module TestHelpers module Paths def gem_root ::File.dirname(__dir__) end def dir_path(*args) path = File.join(gem_root, *args) FileUtils.mkdir_p(path) unless ::File.exist?(path) File.realpath(path) end def fixtures_path(*args) File.expand_path(File.join(dir_path("spec/fixtures"), *args)) end end module Platform def jruby? RUBY_PLATFORM == "java" end end end RSpec.shared_context "sandbox" do around(:each) do |example| ::Dir.mktmpdir do |dir| ::Dir.chdir(dir, &example) end end end RSpec.configure do |config| config.include(TestHelpers::Paths) config.include(TestHelpers::Platform) config.include_context "sandbox", type: :sandbox config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.max_formatted_output_length = nil end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end # Limits the available syntax to the non-monkey patched syntax that is recommended. config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true if config.files_to_run.one? config.default_formatter = "doc" end config.profile_examples = 2 config.order = :random Kernel.srand config.seed end tty-command-0.10.1/spec/unit/ 0000775 0000000 0000000 00000000000 14012255006 0015731 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/spec/unit/binmode_spec.rb 0000664 0000000 0000000 00000001742 14012255006 0020711 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "#run" do it "encodes output as unicode by default" do output = StringIO.new cmd = TTY::Command.new(output: output) out, = cmd.run("echo '昨夜のコンサートは'") expect(out.encoding).to eq(Encoding::UTF_8) # expect(out.chomp).to eq("昨夜のコンサートは") end it "encodes output as binary" do output = StringIO.new cmd = TTY::Command.new(output: output) out, = cmd.run("echo '昨夜のコンサートは'", binmode: true) expect(out.encoding).to eq(Encoding::BINARY) #expect(out.chomp).to eq("\xE6\x98\xA8\xE5\xA4\x9C\xE3\x81\xAE\xE3\x82\xB3\xE3\x83\xB3\xE3\x82\xB5\xE3\x83\xBC\xE3\x83\x88\xE3\x81\xAF".force_encoding(Encoding::BINARY)) end it "encodes all commands output as binary" do output = StringIO.new cmd = TTY::Command.new(output: output, binmode: true) out, = cmd.run("echo 'hello'") expect(out.encoding).to eq(Encoding::BINARY) end end tty-command-0.10.1/spec/unit/cmd_spec.rb 0000664 0000000 0000000 00000014020 14012255006 0020030 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Cmd, "::new" do it "requires at least command argument" do expect { TTY::Command::Cmd.new({}) }.to raise_error(ArgumentError, /Cmd requires command argument/) end it "requires non empty command argument" do expect { TTY::Command::Cmd.new(nil) }.to raise_error(ArgumentError, /No command provided/) end it "accepts a command" do cmd = TTY::Command::Cmd.new(:echo) expect(cmd.command).to eq("echo") expect(cmd.argv).to eq([]) expect(cmd.options).to eq({}) expect(cmd.to_command).to eq("echo") end it "accepts a command as heredoc" do cmd = TTY::Command::Cmd.new <<-EOHEREDOC if [[ $? -eq 0]]; then echo "Bash it!" fi EOHEREDOC expect(cmd.argv).to eq([]) expect(cmd.options).to eq({}) expect(cmd.to_command).to eq([ " if [[ $? -eq 0]]; then", " echo \"Bash it!\"", " fi\n" ].join("\n")) end it "accepts command as [cmdname, arg1, ...]" do cmd = TTY::Command::Cmd.new(:echo, "-n", "hello") expect(cmd.command).to eq("echo") expect(cmd.argv).to eq(["-n", "hello"]) expect(cmd.to_command).to eq("echo -n hello") end it "accepts command as [[cmdname, argv0], arg1, ...]" do cmd = TTY::Command::Cmd.new([:echo, "-n"], "hello") expect(cmd.command).to eq("echo") expect(cmd.argv).to eq(["-n", "hello"]) expect(cmd.to_command).to eq("echo -n hello") end it "accepts command with environment as [cmdname, arg1, ..., opts]" do cmd = TTY::Command::Cmd.new(:echo, "hello", env: { foo: "bar" }) expect(cmd.to_command).to eq(%{( export FOO=\"bar\" ; echo hello )}) end it "accepts command with multiple environment keys" do cmd = TTY::Command::Cmd.new(:echo, "hello", env: { foo: "a", bar: "b" }) expect(cmd.to_command).to eq(%{( export FOO=\"a\" BAR=\"b\" ; echo hello )}) end it "accepts command with environemnt string keys" do cmd = TTY::Command::Cmd.new(:echo, "hello", env: { "FOO_bar" => "a", bar: "b" }) expect(cmd.to_command).to eq(%{( export FOO_bar=\"a\" BAR=\"b\" ; echo hello )}) end it "escapes environment values" do cmd = TTY::Command::Cmd.new(:echo, "hello", env: { foo: 'abc"def' }) expect(cmd.to_command).to eq(%{( export FOO=\"abc\\\"def\" ; echo hello )}) end it "accepts environment as first argument" do cmd = TTY::Command::Cmd.new({ "FOO" => true, "BAR" => 1 }, :echo, "hello") expect(cmd.to_command).to eq(%{( export FOO=\"true\" BAR=\"1\" ; echo hello )}) end it "handles environment with fanky characters" do cmd = TTY::Command::Cmd.new(:echo, "hello", env: { foo: "%16" }) expect(cmd.to_command).to eq(%{( export FOO=\"%16\" ; echo hello )}) end it "runs command in specified directory" do cmd = TTY::Command::Cmd.new(:echo, "hello", chdir: "/tmp") expect(cmd.to_command).to eq("cd /tmp && echo hello") end it "escapes directory path with fanky characters" do cmd = TTY::Command::Cmd.new(:echo, "hello", chdir: "/pa%th/to dir") expect(cmd.to_command).to eq("cd /pa\\%th/to\\ dir && echo hello") end it "runs command in specified directory with environment" do cmd = TTY::Command::Cmd.new(:echo, "hello", chdir: "/tmp", env: { foo: "bar" }) expect(cmd.to_command).to eq(%{cd /tmp && ( export FOO=\"bar\" ; echo hello )}) end it "runs command as a user" do cmd = TTY::Command::Cmd.new(:echo, "hello", user: "piotrmurach") expect(cmd.to_command).to eq("sudo -u piotrmurach -- sh -c 'echo hello'") end it "runs command as a group" do cmd = TTY::Command::Cmd.new(:echo, "hello", group: "devs") expect(cmd.to_command).to eq("sg devs -c \\\"echo hello\\\"") end it "runs command as a user in a group" do cmd = TTY::Command::Cmd.new(:echo, "hello", user: "piotrmurach", group: "devs") expect(cmd.to_command).to eq("sudo -u piotrmurach -- sh -c 'sg devs -c \\\"echo hello\\\"'") end it "runs command with umask" do cmd = TTY::Command::Cmd.new(:echo, "hello", umask: "077") expect(cmd.to_command).to eq("umask 077 && echo hello") end it "runs command with umask, chdir" do cmd = TTY::Command::Cmd.new(:echo, "hello", umask: "077", chdir: "/tmp") expect(cmd.to_command).to eq("cd /tmp && umask 077 && echo hello") end it "runs command with umask, chdir & user" do cmd = TTY::Command::Cmd.new(:echo, "hello", umask: "077", chdir: "/tmp", user: "piotrmurach") expect(cmd.to_command).to eq("cd /tmp && umask 077 && sudo -u piotrmurach -- sh -c 'echo hello'") end it "runs command with umask, user, chdir and env" do cmd = TTY::Command::Cmd.new(:echo, "hello", umask: "077", chdir: "/tmp", user: "piotrmurach", env: { foo: "bar" }) expect(cmd.to_command).to eq(%{cd /tmp && umask 077 && ( export FOO=\"bar\" ; sudo -u piotrmurach FOO=\"bar\" -- sh -c 'echo hello' )}) end it "provides unique identifier" do cmd = TTY::Command::Cmd.new(:echo, "hello") expect(cmd.uuid).to match(/^\w{8}$/) end it "converts command to hash" do cmd = TTY::Command::Cmd.new(:echo, "hello") expect(cmd.to_hash).to include({ command: "echo", argv: ["hello"] }) end it "escapes arguments that need escaping" do cmd = TTY::Command::Cmd.new(:echo, "hello world") expect(cmd.to_hash).to include({ command: "echo", argv: ["hello\\ world"] }) end it "escapes special characters in split arguments" do args = %w(git for-each-ref --format='%(refname)' refs/heads/) cmd = TTY::Command::Cmd.new(*args) expect(cmd.to_hash).to include({ command: "git", argv: ["for-each-ref", "--format\\=\\'\\%\\(refname\\)\\'", "refs/heads/"] }) end it "updates command options with global only when not already present" do cmd_options = { env: { foo: "a" }, out: $stdout } cmd = TTY::Command::Cmd.new(:echo, "hello", cmd_options) expect(cmd.options).to eq(cmd_options) cmd.update({ verbose: true, env: { bar: "b" } }) expect(cmd.options).to eq({ env: { foo: "a" }, out: $stdout, verbose: true }) end end tty-command-0.10.1/spec/unit/dry_run_spec.rb 0000664 0000000 0000000 00000002301 14012255006 0020746 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "dry run" do let(:output) { StringIO.new } it "queries for dry mode" do command = TTY::Command.new(dry_run: false) expect(command.dry_run?).to eq(false) end it "runs command in dry run mode" do uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) command = TTY::Command.new(output: output, dry_run: true) command.run(:echo, "hello", "world") output.rewind expect(output.read).to eq( "[\e[32m#{uuid}\e[0m] \e[34m(dry run)\e[0m " \ "\e[33;1mecho hello world\e[0m\n" ) end it "allows to run command in dry mode" do uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) command = TTY::Command.new(output: output) command.run(:echo, "hello", "world", dry_run: true) output.rewind expect(output.read).to eq( "[\e[32m#{uuid}\e[0m] \e[34m(dry run)\e[0m " \ "\e[33;1mecho hello world\e[0m\n" ) end it "doesn't collect printout to stdin or stderr" do cmd = TTY::Command.new(output: output, dry_run: true) out, err = cmd.run(:echo, "hello", "world") expect(out).to be_empty expect(err).to be_empty end end tty-command-0.10.1/spec/unit/exit_error_spec.rb 0000664 0000000 0000000 00000001367 14012255006 0021461 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::ExitError, "info" do it "displays stdin & stdout" do result = double(exit_status: 157, out: "out content", err: "err content") error = described_class.new(:cat, result) expect(error.message).to eq([ "Running `cat` failed with\n", " exit status: 157\n", " stdout: out content\n", " stderr: err content\n" ].join) end it "explains no stdin & stdout" do result = double(exit_status: 157, out: "", err: "") error = described_class.new(:cat, result) expect(error.message).to eq([ "Running `cat` failed with\n", " exit status: 157\n", " stdout: Nothing written\n", " stderr: Nothing written\n" ].join) end end tty-command-0.10.1/spec/unit/input_spec.rb 0000664 0000000 0000000 00000000503 14012255006 0020425 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "input" do it "reads user input data" do cli = fixtures_path("cli") output = StringIO.new command = TTY::Command.new(output: output) out, = command.run("ruby #{cli}", input: "Piotr\n") expect(out.chomp).to eq("Your name: Piotr") end end tty-command-0.10.1/spec/unit/output_spec.rb 0000664 0000000 0000000 00000001025 14012255006 0020626 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, ":output", type: :sandbox do it "runs command and prints to a file" do stub_const("Tee", Class.new do def initialize(file) @file = file end def <<(message) @file << message @file.close end end) file = "foo.log" output = Tee.new(File.open(file, "w")) command = TTY::Command.new(output: output, printer: :quiet) command.run("echo hello") expect(File.read(file).chomp).to eq("hello") end end tty-command-0.10.1/spec/unit/printer_spec.rb 0000664 0000000 0000000 00000003047 14012255006 0020757 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, ":printer" do it "fails to find printer for nil" do expect { TTY::Command.new(printer: nil) }.to raise_error(ArgumentError, /Unknown printer type ""/) end it "fails to find printer based on name" do expect { TTY::Command.new(printer: :unknown) }.to raise_error(ArgumentError, /Unknown printer type "unknown"/) end it "detects null printer" do cmd = TTY::Command.new(printer: :null) expect(cmd.printer).to be_an_instance_of(TTY::Command::Printers::Null) end it "detects printer based on name" do cmd = TTY::Command.new(printer: :progress) expect(cmd.printer).to be_an_instance_of(TTY::Command::Printers::Progress) end it "uses printer based on class name" do output = StringIO.new printer = TTY::Command::Printers::Pretty cmd = TTY::Command.new(output: output, printer: printer) expect(cmd.printer).to be_an_instance_of(TTY::Command::Printers::Pretty) end it "uses printer based on instance" do output = StringIO.new printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command.new(printer: printer) expect(cmd.printer).to be_an_instance_of(TTY::Command::Printers::Pretty) end it "uses custom printer" do stub_const("CustomPrinter", Class.new(TTY::Command::Printers::Abstract) do def write(message) output << message end end) printer = CustomPrinter cmd = TTY::Command.new(printer: printer) expect(cmd.printer).to be_an_instance_of(CustomPrinter) end end tty-command-0.10.1/spec/unit/printers/ 0000775 0000000 0000000 00000000000 14012255006 0017577 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/spec/unit/printers/custom_spec.rb 0000664 0000000 0000000 00000002142 14012255006 0022447 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe "Custom Printer" do let(:output) { StringIO.new } before do stub_const("CustomPrinter", Class.new(TTY::Command::Printers::Abstract) do def write(message) output << message end end) end it "prints command start" do printer = CustomPrinter.new(output) cmd = TTY::Command::Cmd.new(:echo, "'hello world'") printer.print_command_start(cmd) output.rewind expect(output.string).to eq("echo \\'hello\\ world\\'") end it "prints command stdout data" do printer = CustomPrinter.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello world") printer.print_command_out_data(cmd, "data") output.rewind expect(output.string).to eq("data") end it "prints command exit" do printer = CustomPrinter.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello world") printer.print_command_exit(cmd) output.rewind expect(output.string).to be_empty end it "accepts options" do printer = CustomPrinter.new(output, foo: :bar) expect(printer.options[:foo]).to eq(:bar) end end tty-command-0.10.1/spec/unit/printers/null_spec.rb 0000664 0000000 0000000 00000001652 14012255006 0022114 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Printers::Null do let(:output) { StringIO.new } it "doesn't print command start or exit" do printer = TTY::Command::Printers::Null.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_start(cmd) printer.print_command_exit(cmd, 0) output.rewind expect(output.string).to be_empty end it "doesn't print command stdout data" do printer = TTY::Command::Printers::Null.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_out_data(cmd, "hello", "world") output.rewind expect(output.string).to be_empty end it "doesn't print command stderr data" do printer = TTY::Command::Printers::Null.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_err_data(cmd, "hello", "world") output.rewind expect(output.string).to be_empty end end tty-command-0.10.1/spec/unit/printers/pretty_spec.rb 0000664 0000000 0000000 00000013744 14012255006 0022476 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Printers::Pretty do let(:output) { StringIO.new } let(:uuid) { "aaaaaa-xxx" } it "prints command start in color" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_start(cmd) output.rewind expect(output.string) .to eq("[\e[32maaaaaa\e[0m] Running \e[33;1mecho hello\e[0m\n") end it "prints command start without color" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output, color: false) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_start(cmd) output.rewind expect(output.string).to eq("[aaaaaa] Running echo hello\n") end it "prints command start without uuid" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output, uuid: false) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_start(cmd) output.rewind expect(output.string).to eq("Running \e[33;1mecho hello\e[0m\n") end it "prints command stdout data" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_out_data(cmd, "hello", "world") output.rewind expect(output.string).to eq("[\e[32maaaaaa\e[0m] \thello world\n") end it "prints command stderr data" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_err_data(cmd, "hello", "world") output.rewind expect(output.string) .to eq("[\e[32maaaaaa\e[0m] \t\e[31mhello world\e[0m\n") end it "prints successful command exit in color" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_exit(cmd, 0, 5.321) output.rewind expect(output.string).to eq("[\e[32maaaaaa\e[0m] Finished in 5.321 seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n") end it "prints failure command exit in color" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_exit(cmd, 1, 5.321) output.rewind expect(output.string).to eq("[\e[32maaaaaa\e[0m] Finished in 5.321 seconds with exit status 1 (\e[31;1mfailed\e[0m)\n") end it "prints command exit without exit status in color" do allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_exit(cmd, nil, 5.321) output.rewind expect(output.string).to eq("[\e[32maaaaaa\e[0m] Finished in 5.321 seconds (\e[31;1mfailed\e[0m)\n") end it "doesn't print output on success when only_output_on_error is true" do zero_exit = fixtures_path("zero_exit") allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty cmd = TTY::Command.new(output: output, printer: printer) cmd.run!(:ruby, zero_exit, only_output_on_error: true) cmd.run!(:ruby, zero_exit) output.rewind lines = output.readlines lines.each { |line| line.gsub!(/\d+\.\d+(?= seconds)/, "x") } expect(lines).to eq([ "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n", "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] \tyess\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n" ]) end it "prints output on error & raises ExitError when only_output_on_error is true" do non_zero_exit = fixtures_path("non_zero_exit") allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty cmd = TTY::Command.new(output: output, printer: printer) cmd.run!(:ruby, non_zero_exit, only_output_on_error: true) cmd.run!(:ruby, non_zero_exit) output.rewind lines = output.readlines lines.each { |line| line.gsub!(/\d+\.\d+(?= seconds)/, "x") } expect(lines).to eq([ "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{non_zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] \tnooo\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 1 (\e[31;1mfailed\e[0m)\n", "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{non_zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] \tnooo\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 1 (\e[31;1mfailed\e[0m)\n" ]) end it "prints output on error when only_output_on_error is true" do non_zero_exit = fixtures_path("non_zero_exit") allow(SecureRandom).to receive(:uuid).and_return(uuid) printer = TTY::Command::Printers::Pretty cmd = TTY::Command.new(output: output, printer: printer) expect { cmd.run(:ruby, non_zero_exit, only_output_on_error: true) }.to raise_error(TTY::Command::ExitError) expect { cmd.run(:ruby, non_zero_exit) }.to raise_error(TTY::Command::ExitError) output.rewind lines = output.readlines lines.each { |line| line.gsub!(/\d+\.\d+(?= seconds)/, "x") } expect(lines).to eq([ "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{non_zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] \tnooo\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 1 (\e[31;1mfailed\e[0m)\n", "[\e[32maaaaaa\e[0m] Running \e[33;1mruby #{non_zero_exit}\e[0m\n", "[\e[32maaaaaa\e[0m] \tnooo\n", "[\e[32maaaaaa\e[0m] Finished in x seconds with exit status 1 (\e[31;1mfailed\e[0m)\n" ]) end end tty-command-0.10.1/spec/unit/printers/progress_spec.rb 0000664 0000000 0000000 00000002250 14012255006 0023001 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Printers::Progress do let(:output) { StringIO.new } it "doesn't print command start" do printer = TTY::Command::Printers::Progress.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_start(cmd) output.rewind expect(output.string).to be_empty end it "doesn't print command stdout data" do printer = TTY::Command::Printers::Progress.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_out_data(cmd, "hello", "world") output.rewind expect(output.string).to be_empty end it "prints successful command exit in color" do printer = TTY::Command::Printers::Progress.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_exit(cmd, 0, 5.321) output.rewind expect(output.string).to eq("\e[32m.\e[0m") end it "prints failure command exit in color" do printer = TTY::Command::Printers::Progress.new(output) cmd = TTY::Command::Cmd.new(:echo, "hello") printer.print_command_exit(cmd, 1, 5.321) output.rewind expect(output.string).to eq("\e[31mF\e[0m") end end tty-command-0.10.1/spec/unit/printers/quiet_spec.rb 0000664 0000000 0000000 00000003442 14012255006 0022270 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Printers::Quiet do let(:output) { StringIO.new } it "doesn't print command start or exit" do printer = TTY::Command::Printers::Quiet.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_start(cmd) printer.print_command_exit(cmd, 0) output.rewind expect(output.string).to be_empty end it "prints command stdout data" do printer = TTY::Command::Printers::Quiet.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_out_data(cmd, "hello", "world") output.rewind expect(output.string).to eq("hello world") end it "prints command stderr data" do printer = TTY::Command::Printers::Quiet.new(output) cmd = TTY::Command::Cmd.new("echo hello") printer.print_command_err_data(cmd, "hello", "world") output.rewind expect(output.string).to eq("hello world") end it "doesn't print output on success when only_output_on_error is true" do zero_exit = fixtures_path("zero_exit") printer = TTY::Command::Printers::Quiet cmd = TTY::Command.new(output: output, printer: printer) cmd.run!(:ruby, zero_exit, only_output_on_error: true) cmd.run!(:ruby, zero_exit) output.rewind lines = output.readlines.map(&:chomp) expect(lines).to eq([ "yess" ]) end it "prints output on error when only_output_on_error is true" do non_zero_exit = fixtures_path("non_zero_exit") printer = TTY::Command::Printers::Quiet cmd = TTY::Command.new(output: output, printer: printer) cmd.run!(:ruby, non_zero_exit, only_output_on_error: true) cmd.run!(:ruby, non_zero_exit) output.rewind lines = output.readlines.map(&:chomp) expect(lines).to eq(%w[ nooo nooo ]) end end tty-command-0.10.1/spec/unit/pty_spec.rb 0000664 0000000 0000000 00000003452 14012255006 0020110 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, ":pty" do it "executes command in pseudo terminal mode as global option", unless: RSpec::Support::OS.windows? do color_cli = fixtures_path("color") output = StringIO.new cmd = TTY::Command.new(output: output, pty: true) out, err = cmd.run(color_cli) expect(err).to eq("") expect(out).to eq("\e[32mcolored\e[0m\n") end it "executes command in pseudo terminal mode as command option", unless: RSpec::Support::OS.windows? do color_cli = fixtures_path("color") output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run(color_cli, pty: true) expect(err).to eq("") expect(out).to eq("\e[32mcolored\e[0m\n") end it "logs phased output in pseudo terminal mode", unless: RSpec::Support::OS.windows? do phased_output = fixtures_path("phased_output") uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("ruby #{phased_output}", pty: true) expect(out).to eq("." * 10) expect(err).to eq("") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "[\e[32m#{uuid}\e[0m] Running \e[33;1mruby #{phased_output}\e[0m\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] \t.\n", "[\e[32m#{uuid}\e[0m] Finished in x seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n" ]) end end tty-command-0.10.1/spec/unit/redirect_spec.rb 0000664 0000000 0000000 00000004675 14012255006 0021105 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "redirect" do it "accepts standard shell redirects" do output = StringIO.new command = TTY::Command.new(output: output) out, err = command.run("echo hello 1>& 2") expect(out).to eq("") expect(err).to match(%r{hello\s*\n}) end it "redirects :out -> :err" do output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", out: :err) expect(out).to be_empty expect(err.chomp).to eq("hello") end it "redirects :stdout -> :stderr" do output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", stdout: :stderr) expect(out).to be_empty expect(err.chomp).to eq("hello") end it "redirects 1 -> 2" do output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", 1 => 2) expect(out).to be_empty expect(err.chomp).to eq("hello") end it "redirects STDOUT -> :err" do output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", STDOUT => :err) expect(out).to be_empty expect(err.chomp).to eq("hello") end it "redirects STDOUT -> /dev/null" do output = StringIO.new cmd = TTY::Command.new(output: output) out, = cmd.run("echo hello", out: IO::NULL) expect(out).to eq("") end it "redirects to a file", type: :sandbox do file = "log" output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", out: file) expect(out).to be_empty expect(err).to be_empty expect(File.read(file)).to eq("hello\n") end it "redirects to a file as an array value", type: :sandbox do file = "log" output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", out: [file, "w", 0600]) expect(out).to be_empty expect(err).to be_empty expect(File.read(file)).to eq("hello\n") expect(File.writable?(file)).to eq(true) unless RSpec::Support::OS.windows? expect(File.stat(file).mode.to_s(8)[2..5]).to eq("0600") end end it "redirects multiple fds to a file", type: :sandbox do file = "log" output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("echo hello", %i[out err] => file) expect(out).to be_empty expect(err).to be_empty expect(File.read(file)).to eq("hello\n") end end tty-command-0.10.1/spec/unit/result_spec.rb 0000664 0000000 0000000 00000003647 14012255006 0020620 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Result do it "exits successfully" do result = TTY::Command::Result.new(0, "", "") expect(result.exited?).to eq(true) expect(result.success?).to eq(true) end it "exist with non-zero code" do result = TTY::Command::Result.new(127, "", "") expect(result.exited?).to eq(true) expect(result.success?).to eq(false) end it "accesses exit code" do result = TTY::Command::Result.new(127, "", "") expect(result.to_i).to eq(127) expect(result.to_s).to eq("127") end it "provides runtime" do result = TTY::Command::Result.new(0, "", "", 5.4) expect(result.runtime).to eq(5.4) end it "doesn't exit" do result = TTY::Command::Result.new(nil, "", "") expect(result.exited?).to eq(false) expect(result.success?).to eq(false) end it "reads stdout" do result = TTY::Command::Result.new(0, "foo", "") expect(result.out).to eq("foo") end it "isn't equivalent with another object" do result = TTY::Command::Result.new(0, "", "") expect(result).to_not eq(:other) end it "is the same with equivalent object" do result_foo = TTY::Command::Result.new(0, "foo", "bar") result_bar = TTY::Command::Result.new(0, "foo", "bar") expect(result_foo).to eq(result_bar) end it "iterates over output with default delimiter" do result = TTY::Command::Result.new(0, "line1\nline2\nline3", "") expect(result.to_a).to eq(%w[line1 line2 line3]) end it "iterates over output with global delimiter" do allow(TTY::Command).to receive(:record_separator).and_return("\t") result = TTY::Command::Result.new(0, "line1\tline2\tline3", "") expect(result.each.to_a).to eq(%w[line1 line2 line3]) end it "iterates over output with argument delimiter" do result = TTY::Command::Result.new(0, "line1\tline2\tline3", "") expect(result.each("\t").to_a).to eq(%w[line1 line2 line3]) end end tty-command-0.10.1/spec/unit/ruby_spec.rb 0000664 0000000 0000000 00000001244 14012255006 0020252 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "#ruby" do it "runs ruby with a single string argument" do output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.ruby %q(-e "puts 'Hello World'") expect(out.chomp).to eq("Hello World") expect(err).to be_empty unless jruby? end it "runs ruby with multiple arguments" do output = StringIO.new cmd = TTY::Command.new(output: output) result = double(success?: true) allow(cmd).to receive(:run) .with(TTY::Command::RUBY, "script.rb", "foo", "bar", {}) .and_return(result) expect(cmd.ruby("script.rb", "foo", "bar")).to eq(result) end end tty-command-0.10.1/spec/unit/run_spec.rb 0000664 0000000 0000000 00000012313 14012255006 0020074 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "#run" do it "runs command and prints to stdout" do output = StringIO.new command = TTY::Command.new(output: output) out, err = command.run(:echo, "hello") expect(out.chomp).to eq("hello") expect(err).to eq("") end it "runs command successfully with logging" do output = StringIO.new uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) command = TTY::Command.new(output: output) command.run(:echo, "hello") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "[\e[32m#{uuid}\e[0m] Running \e[33;1mecho hello\e[0m\n", "[\e[32m#{uuid}\e[0m] \thello\n", "[\e[32m#{uuid}\e[0m] Finished in x seconds with exit status 0 " \ "(\e[32;1msuccessful\e[0m)\n" ]) end it "runs command successfully with logging without color" do output = StringIO.new uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) command = TTY::Command.new(output: output, color: false) command.run(:echo, "hello") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "[#{uuid}] Running echo hello\n", "[#{uuid}] \thello\n", "[#{uuid}] Finished in x seconds with exit status 0 (successful)\n" ]) end it "runs command successfully with logging without uuid set globally" do output = StringIO.new command = TTY::Command.new(output: output, uuid: false) command.run(:echo, "hello") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "Running \e[33;1mecho hello\e[0m\n", "\thello\n", "Finished in x seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n" ]) end it "runs command successfully with logging without uuid set locally" do output = StringIO.new command = TTY::Command.new(output: output) command.run(:echo, "hello", uuid: false) output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "Running \e[33;1mecho hello\e[0m\n", "\thello\n", "Finished in x seconds with exit status 0 (\e[32;1msuccessful\e[0m)\n" ]) end it "runs command and fails with logging" do non_zero_exit = fixtures_path("non_zero_exit") output = StringIO.new uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) command = TTY::Command.new(output: output) command.run!("ruby #{non_zero_exit}") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "[\e[32m#{uuid}\e[0m] Running \e[33;1mruby #{non_zero_exit}\e[0m\n", "[\e[32m#{uuid}\e[0m] \tnooo\n", "[\e[32m#{uuid}\e[0m] Finished in x seconds with exit status 1 " \ "(\e[31;1mfailed\e[0m)\n" ]) end it "raises ExitError on command failure" do non_zero_exit = fixtures_path("non_zero_exit") output = StringIO.new command = TTY::Command.new(output: output) expect { command.run("ruby #{non_zero_exit}") }.to raise_error(TTY::Command::ExitError, [ "Running `ruby #{non_zero_exit}` failed with", " exit status: 1", " stdout: nooo", " stderr: Nothing written\n" ].join("\n")) end it "streams output data" do stream = fixtures_path("stream") out_stream = StringIO.new command = TTY::Command.new(output: out_stream) output = [] error = [] command.run("ruby #{stream}") do |out, err| output << out if out error << err if err end expect(output.join.gsub(/\r\n|\n/, "")).to eq("hello 1hello 2hello 3") expect(error.join).to eq("") end it "preserves ANSI codes" do output = StringIO.new command = TTY::Command.new(output: output, printer: :quiet) out, = command.run("echo \e[35mhello\e[0m") expect(out.chomp).to eq("\e[35mhello\e[0m") expect(output.string.chomp).to eq("\e[35mhello\e[0m") end it "logs phased output in one line" do phased_output = fixtures_path("phased_output") uuid = "xxxx" allow(SecureRandom).to receive(:uuid).and_return(uuid) output = StringIO.new cmd = TTY::Command.new(output: output) out, err = cmd.run("ruby #{phased_output}") expect(out).to eq("." * 10) expect(err).to eq("") output.rewind lines = output.readlines lines.last.gsub!(/\d+\.\d+/, "x") expect(lines).to eq([ "[\e[32m#{uuid}\e[0m] Running \e[33;1mruby #{phased_output}\e[0m\n", "[\e[32m#{uuid}\e[0m] \t..........\n", "[\e[32m#{uuid}\e[0m] Finished in x seconds with exit status 0 " \ "(\e[32;1msuccessful\e[0m)\n" ]) end it "does not persist environment variables", unless: RSpec::Support::OS.windows? do output = StringIO.new command = TTY::Command.new(output: output) command.run(:echo, "hello", env: { foo: 1 }) output.rewind lines = output.readlines expect(lines[0]) .to include("Running \e[33;1m( export FOO=\"1\" ; echo hello )\e[0m\n") output.reopen command.run(:echo, "hello") output.rewind lines = output.readlines expect(lines[0]).to include("Running \e[33;1mecho hello\e[0m\n") end end tty-command-0.10.1/spec/unit/test_spec.rb 0000664 0000000 0000000 00000000547 14012255006 0020255 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "#test" do it "implements classic bash command" do cmd = TTY::Command.new result = double(success?: true) allow(cmd).to receive(:run!).with(:test, "-e /etc/passwd").and_return(result) expect(cmd.test("-e /etc/passwd")).to eq(true) expect(cmd).to have_received(:run!) end end tty-command-0.10.1/spec/unit/timeout_spec.rb 0000664 0000000 0000000 00000002106 14012255006 0020755 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command, "#run" do it "times out infinite process without input or output" do infinite = fixtures_path("infinite_no_output") output = StringIO.new cmd = TTY::Command.new(output: output) expect { cmd.run("ruby #{infinite}", timeout: 0.1) }.to raise_error(TTY::Command::TimeoutExceeded) end it "times out an infite process with constant output" do infinite = fixtures_path("infinite_output") output = StringIO.new cmd = TTY::Command.new(output: output, timeout: 0.1) expect { cmd.run("ruby #{infinite}") }.to raise_error(TTY::Command::TimeoutExceeded) end it "times out an infinite process with constant input data" do cli = fixtures_path("infinite_input") output = StringIO.new cmd = TTY::Command.new(output: output) range = 1..Float::INFINITY infinite_input = range.lazy.map { |_x| "hello" }.first(100).join("\n") expect { cmd.run("ruby #{cli}", input: infinite_input, timeout: 0.1) }.to raise_error(TTY::Command::TimeoutExceeded) end end tty-command-0.10.1/spec/unit/truncator_spec.rb 0000664 0000000 0000000 00000003312 14012255006 0021310 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true RSpec.describe TTY::Command::Truncator do it "writes nil content" do truncator = described_class.new(max_size: 2) truncator.write(nil) expect(truncator.read).to eq("") end it "writes content within maximum size" do truncator = described_class.new(max_size: 2) truncator.write("a") expect(truncator.read).to eq("a") end it "writes both prefix and suffix" do truncator = described_class.new(max_size: 2) truncator.write("abc") truncator.write("d") expect(truncator.read).to eq("abcd") end it "writes more bytes letter" do truncator = described_class.new(max_size: 1000) multibytes_string = "’test’" truncator.write(multibytes_string) expect(truncator.read).to eq(multibytes_string) end it "overflows prefix and suffix " do truncator = described_class.new(max_size: 2) truncator.write("abc") truncator.write("d") truncator.write("e") expect(truncator.read).to eq("ab\n... omitting 1 bytes ...\nde") end it "omits bytes " do truncator = described_class.new(max_size: 2) truncator.write("abc___________________yz") expect(truncator.read).to eq("ab\n... omitting 20 bytes ...\nyz") end it "reflows suffix with less content" do truncator = described_class.new(max_size: 2) truncator.write("abc____________________y") truncator.write("z") expect(truncator.read).to eq("ab\n... omitting 21 bytes ...\nyz") end it "reflows suffix with more content" do truncator = described_class.new(max_size: 2) truncator.write("abc____________________y") truncator.write("zwx") expect(truncator.read).to eq("ab\n... omitting 23 bytes ...\nwx") end end tty-command-0.10.1/tasks/ 0000775 0000000 0000000 00000000000 14012255006 0015145 5 ustar 00root root 0000000 0000000 tty-command-0.10.1/tasks/console.rake 0000664 0000000 0000000 00000000327 14012255006 0017455 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true desc "Load gem inside irb console" task :console do require "irb" require "irb/completion" require_relative "../lib/tty-command" ARGV.clear IRB.start end task :c => :console tty-command-0.10.1/tasks/coverage.rake 0000664 0000000 0000000 00000000336 14012255006 0017606 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true desc "Measure code coverage" task :coverage do begin original, ENV["COVERAGE"] = ENV["COVERAGE"], "true" Rake::Task["spec"].invoke ensure ENV["COVERAGE"] = original end end tty-command-0.10.1/tasks/spec.rake 0000664 0000000 0000000 00000001271 14012255006 0016744 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true begin require "rspec/core/rake_task" desc "Run all specs" RSpec::Core::RakeTask.new(:spec) do |task| task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb" end namespace :spec do desc "Run unit specs" RSpec::Core::RakeTask.new(:unit) do |task| task.pattern = "spec/unit{,/*/**}/*_spec.rb" end desc "Run integration specs" RSpec::Core::RakeTask.new(:integration) do |task| task.pattern = "spec/integration{,/*/**}/*_spec.rb" end end rescue LoadError %w[spec spec:unit spec:integration].each do |name| task name do $stderr.puts "In order to run #{name}, do `gem install rspec`" end end end tty-command-0.10.1/tty-command.gemspec 0000664 0000000 0000000 00000002772 14012255006 0017631 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true require_relative "lib/tty/command/version" Gem::Specification.new do |spec| spec.name = "tty-command" spec.version = TTY::Command::VERSION spec.authors = ["Piotr Murach"] spec.email = ["piotr@piotrmurach.com"] spec.summary = %q{Execute shell commands with pretty output logging and capture their stdout, stderr and exit status.} spec.description = %q{Execute shell commands with pretty output logging and capture their stdout, stderr and exit status. Redirect stdin, stdout and stderr of each command to a file or a string.} spec.homepage = "https://ttytoolkit.org" spec.license = "MIT" if spec.respond_to?(:metadata=) spec.metadata = { "allowed_push_host" => "https://rubygems.org", "bug_tracker_uri" => "https://github.com/piotrmurach/tty-command/issues", "changelog_uri" => "https://github.com/piotrmurach/tty-command/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/tty-command", "homepage_uri" => spec.homepage, "source_code_uri" => "https://github.com/piotrmurach/tty-command" } end spec.files = Dir["lib/**/*"] spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE.txt"] spec.bindir = "exe" spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.0.0" spec.add_dependency "pastel", "~> 0.8" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", ">= 3.0" end