backticks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4c09ea76ce613d4920ab0f2f0102fa56eb0ddc86
4
+ data.tar.gz: 013d631b062d46df861787bf64107cf802fe4580
5
+ SHA512:
6
+ metadata.gz: 4d05727a43ef3634d734eef36990cf8915072da01ccca1e57993a58b83fc5af2d638366e3d3c9fbe123d7aa99b0e03a9239a79cc178589cb1b0c6d411fab7421
7
+ data.tar.gz: 60dba8be758e0ca850c573882ce14ed3da1200f9a6cb62020b2a9b92e2590b7acb396fd6b2914572b5337d7c9b92c559aa2f429158e203beff3c682ff6e80abf
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.6
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ 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. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in backticks.gemspec
4
+ gemspec
@@ -0,0 +1,63 @@
1
+ # Backticks
2
+
3
+ Backticks is an intuitive OOP wrapper for invoking command-line processes and
4
+ interacting with them. It uses PTYs
5
+
6
+ By default, processes that you invoke
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'backticks'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install backticks
23
+
24
+ ## Usage
25
+
26
+ ```ruby
27
+ require 'backticks'
28
+
29
+ # The easy way
30
+ output = Backticks.command('ls', R:true, '*.rb')
31
+ puts "Exit status #{$?.to_i}. Output:"
32
+ puts output
33
+
34
+ # The hard way; allows customization such as interactive mode, which proxies
35
+ # the child process's stdin, stdout and stderr to the parent process.
36
+ command = Backticks::Runner.new(interactive:true).command('ls', R:true, '*.rb')
37
+ command.join
38
+ puts "Exit status: #{command.status.to_i}. Output:"
39
+ puts command.captured_output
40
+ ```
41
+
42
+ ### Buffering
43
+
44
+ By default, Backticks allocates a pseudo-TTY for stdout and two Unix pipes for
45
+ stderr/stdin; this captures stdout in real-time, but stderr and
46
+ stdin are subject to unavoidable Unix pipe buffering.
47
+
48
+ To use pipes for all io streams, enable buffering when you construct your
49
+ Runner:
50
+
51
+ ```ruby
52
+ Backticks::Runner.new(buffered:true)
53
+ ```
54
+
55
+ ## Development
56
+
57
+ 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.
58
+
59
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
60
+
61
+ ## Contributing
62
+
63
+ Bug reports and pull requests are welcome on GitHub at https://github.com/xeger/backticks. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'backticks/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "backticks"
8
+ spec.version = Backticks::VERSION
9
+ spec.authors = ["Tony Spataro"]
10
+ spec.email = ["xeger@xeger.net"]
11
+
12
+ spec.summary = %q{Intuitive OOP wrapper for command-line processes}
13
+ spec.description = %q{Captures processes' stdout, stderr and (optionally) stdin; uses PTY to avoid buffering.}
14
+ spec.homepage = "https://github.com/xeger/backticks"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "backticks"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ require "backticks/version"
2
+ require "backticks/cli"
3
+ require "backticks/command"
4
+ require "backticks/runner"
5
+
6
+ module Backticks
7
+ # Run a command.
8
+ #
9
+ # @return [Backticks::Command] a running command
10
+ def self.new(*argv)
11
+ Backticks::Runner.new.command(*argv)
12
+ end
13
+
14
+ # Run a command and return its stdout.
15
+ #
16
+ # @return [String] the command's output
17
+ def self.command(*argv)
18
+ command = self.new(*argv)
19
+ command.join
20
+ command.captured_output
21
+ end
22
+ end
@@ -0,0 +1,107 @@
1
+ module Backticks
2
+ module CLI
3
+ # Command-line parameter generator that relies on traditional *nix getopt
4
+ # conventions. Getopt doesn't know about GNU conventions such as short and
5
+ # long options; it doesn't know about abbreviations; it doesn't know about
6
+ # conventions such as `--X` vs. `--no-X` or `-d` vs. `-D`.
7
+ #
8
+ # Although Getopt is simple, it has the tremendous advantage of being
9
+ # compatible with a wide range of other schemes including GNU getopt-long,
10
+ # golang flags, and most Java utilities. It's a great choice of default
11
+ # CLI.
12
+ module Getopt
13
+ # Translate a series of positional and keyword arguments into command-line
14
+ # line parameters consisting of words and options.
15
+ #
16
+ # Each positional argument can be a Hash, an Array, or another object.
17
+ # They are handled as follows:
18
+ # - Hash is translated to a sequence of options; see #options
19
+ # - Array is appended to the command line as a sequence of words
20
+ # - other objects are turned into a strong with #to_s and appended to the command line as a single word
21
+ #
22
+ # @return [Array] list of String words and options
23
+ #
24
+ # @example recursively find all text files
25
+ # parameters('ls', l:true, R:true, '*.txt') => 'ls -l -R *.txt
26
+ #
27
+ # @example install your favorite gem
28
+ # parameters('gem', 'install', no_document:true, 'backticks')
29
+ def self.parameters(*cmd)
30
+ argv = []
31
+
32
+ cmd.each do |item|
33
+ case item
34
+ when Array
35
+ # list of words to append to argv
36
+ argv.concat(item.map { |e| e.to_s })
37
+ when Hash
38
+ # list of options to convert to CLI parameters
39
+ argv.concat(options(item))
40
+ else
41
+ # single word to append to argv
42
+ argv << item.to_s
43
+ end
44
+ end
45
+
46
+ argv
47
+ end
48
+
49
+ # Translate Ruby method parameters into command-line parameters using a
50
+ # notation that is compatible with traditional Unix getopt. Command lines
51
+ # generated by this method are also mostly compatible with the following:
52
+ # - GNU getopt
53
+ # - Ruby trollop gem
54
+ # - Golang flags package
55
+ #
56
+ # This method accepts an unbounded set of keyword arguments (i.e. you can
57
+ # pass it _any_ valid Ruby symbol as a kwarg). Each kwarg has a
58
+ # value; the key/value pair is translated into a CLI option using the
59
+ # following heuristic:
60
+ # 1) Snake-case keys are hyphenated, e.g. :no_foo => "--no-foo"
61
+ # 2) boolean values indicate a CLI flag; true includes the flag, false or nil omits it
62
+ # 3) all other values indicate a CLI option that has a value.
63
+ # 4) single character keys are passed as short options; {X: V} becomes "-X V"
64
+ # 5) multi-character keys are passed as long options; {Xxx: V} becomes "--XXX=V"
65
+ #
66
+ # The generic translator doesn't know about short vs. long option names,
67
+ # abbreviations, or the GNU "X vs. no-X" convention, so it does not
68
+ # produce the most idiomatic or compact command line for a given program;
69
+ # its output is, however, almost always valid for utilities that use
70
+ # Unix-like parameters.
71
+ #
72
+ # @return [Array] list of String command-line options
73
+ def self.options(**opts)
74
+ flags = []
75
+
76
+ # Transform opts into golang flags-style command line parameters;
77
+ # append them to the command.
78
+ opts.each do |kw, arg|
79
+ if kw.length == 1
80
+ if arg == true
81
+ # true: boolean flag
82
+ flags << "-#{kw}"
83
+ elsif arg
84
+ # truthey: option that has a value
85
+ flags << "-#{kw}" << arg.to_s
86
+ else
87
+ # falsey: omit boolean flag
88
+ end
89
+ else
90
+ kw = kw.to_s.gsub('_','-')
91
+ if arg == true
92
+ # true: boolean flag
93
+ flags << "--#{kw}"
94
+ elsif arg
95
+ # truthey: option that has a value
96
+ flags << "--#{kw}=#{arg}"
97
+ else
98
+ # falsey: omit boolean flag
99
+ end
100
+ end
101
+ end
102
+
103
+ flags
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,124 @@
1
+ module Backticks
2
+ # Represents a running process; provides mechanisms for capturing the process's
3
+ # output, passing input, waiting for the process to end, and learning its
4
+ # exitstatus.
5
+ #
6
+ # Interactive commands print their output to Ruby's STDOUT and STDERR
7
+ # in realtime, and also pass input from Ruby's STDIN to the command's stdin.
8
+ class Command
9
+ # Time value that is used internally when a user is willing to wait
10
+ # "forever" for the command.
11
+ #
12
+ # Using a definite time-value helps simplify the looping logic internally,
13
+ # but it does mean that this class will stop working in February of 2106.
14
+ # You have been warned!
15
+ FOREVER = Time.at(2**32-1).freeze
16
+
17
+ # Number of bytes to read from the command in one "chunk".
18
+ CHUNK = 1_024
19
+
20
+ # @return [Integer] child process ID
21
+ attr_reader :pid
22
+
23
+ # @return [String] all data captured (so far) from child's stdin/stdout/stderr
24
+ attr_reader :captured_input, :captured_output, :captured_error, :status
25
+
26
+ # Watch a running command.
27
+ def initialize(pid, stdin, stdout, stderr, interactive:false)
28
+ @pid = pid
29
+ @stdin = stdin
30
+ @stdout = stdout
31
+ @stderr = stderr
32
+ @interactive = interactive
33
+
34
+ @captured_input = String.new.force_encoding(Encoding::BINARY)
35
+ @captured_output = String.new.force_encoding(Encoding::BINARY)
36
+ @captured_error = String.new.force_encoding(Encoding::BINARY)
37
+ end
38
+
39
+ # Block until the command exits, or until limit seconds have passed. If
40
+ # interactive is true, pass user input to the command and print its output
41
+ # to Ruby's output streams. If the time limit expires, return `nil`;
42
+ # otherwise, return self.
43
+ #
44
+ # @param [Float,Integer] limit number of seconds to wait before returning
45
+ def join(limit=nil)
46
+ if limit
47
+ tf = Time.now + limit
48
+ else
49
+ tf = FOREVER
50
+ end
51
+
52
+ until (t = Time.now) >= tf
53
+ capture(tf - t)
54
+ res = Process.waitpid(@pid, Process::WNOHANG)
55
+ if res
56
+ @status = $?
57
+ return self
58
+ end
59
+ end
60
+
61
+ return nil
62
+ end
63
+
64
+ # Block until one of the following happens:
65
+ # - the command produces fresh output on stdout or stderr
66
+ # - the user passes some input to the command (if interactive)
67
+ # - the process exits
68
+ # - the time limit elapses (if provided)
69
+ #
70
+ # Return up to CHUNK bytes of fresh output from the process, or return nil
71
+ # if no fresh output was produced
72
+ #
73
+ # @param [Float,Integer] number of seconds to wait before returning nil
74
+ # @return [String,nil] fresh bytes from stdout/stderr, or nil if no output
75
+ private def capture(limit=nil)
76
+ streams = [@stdout, @stderr]
77
+ streams << STDIN if @interactive
78
+
79
+ if limit
80
+ tf = Time.now + limit
81
+ else
82
+ tf = FOREVER
83
+ end
84
+
85
+ ready, _, _ = IO.select(streams, [], [], 1)
86
+
87
+ # proxy STDIN to child's stdin
88
+ if ready && ready.include?(STDIN)
89
+ input = STDIN.readpartial(CHUNK) rescue nil
90
+ @captured_input << input
91
+ if input
92
+ @stdin.write(input)
93
+ else
94
+ # our own STDIN got closed; proxy this fact to the child
95
+ @stdin.close
96
+ end
97
+ end
98
+
99
+ # capture child's stdout and maybe proxy to STDOUT
100
+ if ready && ready.include?(@stdout)
101
+ data = @stdout.readpartial(CHUNK) rescue nil
102
+ if data
103
+ @captured_output << data
104
+ STDOUT.write(data) if @interactive
105
+ fresh_output = data
106
+ end
107
+ end
108
+
109
+ # capture child's stderr and maybe proxy to STDERR
110
+ if ready && ready.include?(@stderr)
111
+ data = @stderr.readpartial(CHUNK) rescue nil
112
+ if data
113
+ @captured_error << data
114
+ STDERR.write(data) if @interactive
115
+ end
116
+ end
117
+ fresh_output
118
+ rescue Interrupt
119
+ # Proxy Ctrl+C to the child
120
+ (Process.kill('INT', @pid) rescue nil) if @interactive
121
+ raise
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,93 @@
1
+ require 'pty'
2
+
3
+ module Backticks
4
+ # An easy-to-use interface for invoking commands and capturing their output.
5
+ # Instances of Runner can be interactive, which prints the command's output
6
+ # to the terminal and also allows the user to interact with the command.
7
+ # They can also be unbuffered, which uses a pseudo-tty to capture the
8
+ # command's output with no delay or
9
+ class Runner
10
+ # If true, commands will have their stdio streams tied to the parent
11
+ # process so the user can view their output and send input to them.
12
+ # Commands' output is still captured normally when they are interactive.
13
+ #
14
+ # Note that interactivity doesn't work very well with unbuffered commands;
15
+ # we use pipes to connect to the command's stdio, and the OS forcibly
16
+ # buffers pipe I/O. If you want to send some input to your command, you
17
+ # may need to send a LOT of input before it receives any; the same problem
18
+ # applies to reading your command's output. If you set interactive to
19
+ # true, you usually want to set buffered to false!
20
+ #
21
+ # @return [Boolean]
22
+ attr_accessor :interactive
23
+
24
+ # If true, commands will be invoked with a pseudo-TTY for stdout in order
25
+ # to capture output as it is generated instead of waiting for pipe buffers
26
+ # to fill.
27
+ #
28
+ # @return [Boolean]
29
+ attr_accessor :buffered
30
+
31
+ # Create an instance of Runner.
32
+ # @param [#parameters] cli object ysed to convert Ruby method parameters into command-line parameters
33
+ def initialize(cli:Backticks::CLI::Getopt)
34
+ @interactive = false
35
+ @buffered = false
36
+ @cli = cli
37
+ end
38
+
39
+ # Run a command whose parameters are expressed using some Rubyish sugar.
40
+ # This method accepts an arbitrary number of positional parameters; each
41
+ # parameter can be a Hash, an array, or a simple Object. Arrays and simple
42
+ # objects are appended to argv as "bare" words; Hashes are translated to
43
+ # command-line options and then appended to argv.
44
+ #
45
+ # The
46
+ #
47
+ # @return [Command] the running command
48
+ #
49
+ # @example Run docker-compose with complex parameters
50
+ # command('docker-compose', {file: 'joe.yml'}, 'up', {d:true}, 'mysvc')
51
+ #
52
+ # @see #options for information on Hash-to-flag translation
53
+ def command(*cmd)
54
+ argv = @cli.parameters(*cmd)
55
+
56
+ if self.buffered
57
+ run_buffered(argv)
58
+ else
59
+ run_unbuffered(argv)
60
+ end
61
+ end
62
+
63
+ # Run a command. Use a pty to capture the unbuffered output.
64
+ #
65
+ # @param [Array] argv command to run; argv[0] is program name and the
66
+ # remaining elements are parameters and flags
67
+ # @return [Command] the running command
68
+ private def run_unbuffered(argv)
69
+ stdout, stdout_w = PTY.open
70
+ stdin_r, stdin = IO.pipe
71
+ stderr, stderr_w = IO.pipe
72
+ pid = spawn(*argv, in: stdin_r, out: stdout_w, err: stderr_w)
73
+ stdin_r.close
74
+ stdout_w.close
75
+ stderr_w.close
76
+
77
+ Command.new(pid, stdin, stdout, stderr, interactive:@interactive)
78
+ end
79
+
80
+ # Run a command. Perform no translation or substitution. Use a pipe
81
+ # to read the output, which may be buffered by the OS. Return the program's
82
+ # exit status and stdout.
83
+ #
84
+ # @param [Array] argv command to run; argv[0] is program name and the
85
+ # remaining elements are command-line arguments.
86
+ # @return [Command] the running command
87
+ private def run_buffered(argv)
88
+ stdin, stdout, stderr, thr = Open3.popen3(*argv)
89
+
90
+ Command.new(thr.pid, stdin, stdout, stderr, interactive:@interactive)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module Backticks
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: backticks
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tony Spataro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-12-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Captures processes' stdout, stderr and (optionally) stdin; uses PTY to
56
+ avoid buffering.
57
+ email:
58
+ - xeger@xeger.net
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".travis.yml"
66
+ - CODE_OF_CONDUCT.md
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - backticks.gemspec
71
+ - bin/console
72
+ - bin/setup
73
+ - lib/backticks.rb
74
+ - lib/backticks/cli.rb
75
+ - lib/backticks/command.rb
76
+ - lib/backticks/runner.rb
77
+ - lib/backticks/version.rb
78
+ homepage: https://github.com/xeger/backticks
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 2.4.5
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Intuitive OOP wrapper for command-line processes
102
+ test_files: []