fancy_command 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +194 -0
- data/Rakefile +2 -0
- data/fancy_command.gemspec +23 -0
- data/lib/fancy_command/command.rb +143 -0
- data/lib/fancy_command/version.rb +3 -0
- data/lib/fancy_command.rb +16 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a9dea87fd5ed46f6476a07e75be72f244c1c837d
|
4
|
+
data.tar.gz: 4c2cf776cfcdacbfe3db7bc3d52347869dc5afda
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 28bcca367027743454fafe3618969975ff2786710c56dc2a60a94c1f6c4f0643f40d1bdf8807ed38fbe5f5e233a333a6bdf563a60987027856b84fde8c319836
|
7
|
+
data.tar.gz: d35a1575f541ee3c8ed31888eb3c17cdf9cdab246b5e54a50fec5bd13ea8bc4f0fe6bbbb1de024b353cfbc35f14de3f4aefc3c0dc999bd4dc56e7b1f59065e2f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 myobie
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# FancyCommand
|
2
|
+
|
3
|
+
Wrapper around Open3 making it easier to live-stream command output and
|
4
|
+
chain commands together. See [Usage](#usage) below for code examples.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'fancy_command'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
```sh
|
17
|
+
$ bundle
|
18
|
+
```
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
```sh
|
23
|
+
$ gem install fancy_command
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
The best way to explain this is with some example code:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require 'fancy_command'
|
32
|
+
include FancyCommand
|
33
|
+
|
34
|
+
namespace :bower do
|
35
|
+
task :setup do
|
36
|
+
run("which bower").unless_success_then do
|
37
|
+
run "npm install -g bower", must_succeed: true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :install => :setup do
|
42
|
+
run "bower install", must_succeed: true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
There are a few things to note here:
|
48
|
+
|
49
|
+
* Including `FancyCommand` adds a `#run` method
|
50
|
+
* There are chaining methods (run returns the command itself)
|
51
|
+
* The `must_succeed` flag
|
52
|
+
|
53
|
+
### Including
|
54
|
+
|
55
|
+
The `FancyCommand` module impliments two includable methods:
|
56
|
+
|
57
|
+
* `#run(string, **opts, &blk)`
|
58
|
+
* `#command(string, **opts, &blk)`
|
59
|
+
|
60
|
+
`#command` will instantiate a new `FancyCommand::Command` and `#run`
|
61
|
+
will instantiate and then `#call` that command.
|
62
|
+
|
63
|
+
### Chaining
|
64
|
+
|
65
|
+
There are three methods that can be used to chain commands together:
|
66
|
+
|
67
|
+
* `#then`
|
68
|
+
* `#if_success_then`
|
69
|
+
* `#unless_success_then`
|
70
|
+
|
71
|
+
The all accept a block expecting either one or zero arguments. If one
|
72
|
+
argument is expected then it will be the instance of the previous
|
73
|
+
command.
|
74
|
+
|
75
|
+
### `must_succeed: true`
|
76
|
+
|
77
|
+
When this flag is set then any non-zero `exitstatus` will cause an
|
78
|
+
exception to be raised (`FancyCommand::Command::Failed`). The exception
|
79
|
+
impliments `#command` (the string), `#status` (the `exitstatus`), and
|
80
|
+
`#output` (a string of the combined stdout and stderr).
|
81
|
+
|
82
|
+
- - -
|
83
|
+
|
84
|
+
Another example to show off is:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
require 'fancy_command'
|
88
|
+
include FancyCommand
|
89
|
+
|
90
|
+
# get the date
|
91
|
+
|
92
|
+
command = run("date") | "awk '{ print $2 }'"
|
93
|
+
puts command.output
|
94
|
+
|
95
|
+
# find the Gemfiles
|
96
|
+
|
97
|
+
run("ls") | command("grep Gem", accum: $stdout)
|
98
|
+
|
99
|
+
# live stream build logs
|
100
|
+
|
101
|
+
require 'some_websocket_client_library'
|
102
|
+
require 'bugsnag'
|
103
|
+
require 'bugsnag_configuration'
|
104
|
+
socket = SomeWebsocketClientLibrary.new(ENV["WEBSOCKET_URL"])
|
105
|
+
run("build_script.sh", accum: socket).unless_success_then do |command|
|
106
|
+
Bugsnag.notify(RuntimeError.new("'#{command.string}' failed", {
|
107
|
+
command: command.string,
|
108
|
+
stout: command.out,
|
109
|
+
sterr: command.err,
|
110
|
+
status: command.exitstatus,
|
111
|
+
pid: command.pid
|
112
|
+
})
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
A few things to notice:
|
117
|
+
|
118
|
+
* One can `#|` (or `#pipe`) commands to each other
|
119
|
+
* Commands have `#output`
|
120
|
+
* There is an `accum:` argument
|
121
|
+
* A command has `#out`, `#err`, `#exitstatus`, etc
|
122
|
+
|
123
|
+
### Piping
|
124
|
+
|
125
|
+
All `FancyCommand::Command` instances impliment `#pipe` (and `#|`)
|
126
|
+
which:
|
127
|
+
|
128
|
+
1. Instantiates a command from a string copying the `#accum` (unless it's already a command)
|
129
|
+
2. Copies the output of the first command to the input of the second
|
130
|
+
3. Calls and returns the second command
|
131
|
+
|
132
|
+
This is not exactly like a unix pipe, so infinite pipes will not work.
|
133
|
+
Each command waits until it has fully executed to go on to the next one.
|
134
|
+
If you really want to use real pipes either use `Open3.pipeline` or just
|
135
|
+
make a command string with the pipes in it.
|
136
|
+
|
137
|
+
### Output and accum
|
138
|
+
|
139
|
+
The combined result of stdout and stderr is accessible as `#output`.
|
140
|
+
However, the is also an additional object one can use to accumulate
|
141
|
+
output as it streams in:
|
142
|
+
|
143
|
+
For each line of stdout or stderr, the `accum` object receives the `#<<`
|
144
|
+
message with the line as an argument (the line will have a \n
|
145
|
+
character). The accumulator object does not need to be threadsafe, a
|
146
|
+
mutex is used to make sure that the `#<<` message is never delivered
|
147
|
+
twice at once. The easiest example is to provide `$stdout` as the
|
148
|
+
accumulator, which will output every resulting line as it streams in.
|
149
|
+
|
150
|
+
I built this feature so I could stream build logs over websockets.
|
151
|
+
|
152
|
+
### Other methods
|
153
|
+
|
154
|
+
The important methods are:
|
155
|
+
|
156
|
+
* `#out` only stdout
|
157
|
+
* `#err` only stderr
|
158
|
+
* `#output` combined stdout and stderr (also should be interleaved into
|
159
|
+
the order of delivery from the command)
|
160
|
+
* `#exitstatus` is the integer exit code from the command
|
161
|
+
* `#success?` will be true only for zero exit codes
|
162
|
+
|
163
|
+
### Verbose
|
164
|
+
|
165
|
+
There also is a `verbose:` argument that can be passed in as true. All
|
166
|
+
it does right now is output the command to stdout right before it is
|
167
|
+
executed. If you want to output the command's result just use `$stdout`
|
168
|
+
as `accum:`.
|
169
|
+
|
170
|
+
Here is an example:
|
171
|
+
|
172
|
+
```
|
173
|
+
>> include FancyCommand
|
174
|
+
=> Object
|
175
|
+
>> c = run("date", verbose: true, accum: $stdout) | "awk '{ print $2 }'"
|
176
|
+
$ date
|
177
|
+
Wed Jan 7 08:53:02 CET 2015
|
178
|
+
$ awk '{ print $2 }'
|
179
|
+
Jan
|
180
|
+
=> #<FancyCommand::Command:0x007fc2ad7c5b10 @string="awk '{ print $2 }'", @verbose=true, @accum=#<IO:<STDOUT>>, @in="Wed Jan 7 08:53:02 CET 2015\n", @output="Jan\n", @out="Jan\n", @err="", @status=#<Process::Status: pid 90340 exit 0>, @pid=90340, @must_succeed=false, @output_mutex=#<Mutex:0x007fc2ad7c5570>>
|
181
|
+
```
|
182
|
+
|
183
|
+
## Tests
|
184
|
+
|
185
|
+
I am very sorry that there are no tests yet. I am using this in a real
|
186
|
+
application, but yeah it needs some tests.
|
187
|
+
|
188
|
+
## Contributing
|
189
|
+
|
190
|
+
1. Fork it ( https://github.com/myobie/fancy_command/fork )
|
191
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
192
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
193
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
194
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fancy_command/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fancy_command"
|
8
|
+
spec.version = FancyCommand::VERSION
|
9
|
+
spec.authors = ["myobie"]
|
10
|
+
spec.email = ["me@nathanherald.com"]
|
11
|
+
spec.summary = %q{Real-time streaming command output and other nice things}
|
12
|
+
spec.description = %q{I get really tired of not having a good Command class in ruby, so here it is.}
|
13
|
+
spec.homepage = "http://github.com/myobie/fancy_command"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module FancyCommand
|
5
|
+
class Command
|
6
|
+
class Failed < StandardError
|
7
|
+
attr_reader :command, :status, :output
|
8
|
+
|
9
|
+
def initialize(command:, status:, output:)
|
10
|
+
@command = command
|
11
|
+
@status = status
|
12
|
+
@output = output
|
13
|
+
super("'#{command}' failed with status #{status} and output:\n#{output}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :string, :accum, :in, :out, :err, :output, :status, :pid
|
18
|
+
|
19
|
+
def initialize(string, must_succeed: false, **opts)
|
20
|
+
@string = string
|
21
|
+
@verbose = opts.fetch(:verbose, false)
|
22
|
+
@accum = opts.fetch(:accum) { [] }
|
23
|
+
@in = opts[:in]
|
24
|
+
@output = ""
|
25
|
+
@out = nil
|
26
|
+
@err = nil
|
27
|
+
@status = nil
|
28
|
+
@pid = nil
|
29
|
+
@must_succeed = must_succeed
|
30
|
+
@output_mutex = Mutex.new
|
31
|
+
|
32
|
+
if block_given?
|
33
|
+
append_in yield
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def append_in(string)
|
38
|
+
@in ||= ""
|
39
|
+
@in << string
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def must_succeed?
|
44
|
+
!!@must_succeed
|
45
|
+
end
|
46
|
+
|
47
|
+
def verbose?
|
48
|
+
!!@verbose || ENV["VERBOSE"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def call
|
52
|
+
puts "$ #{string}" if verbose?
|
53
|
+
|
54
|
+
@in.freeze
|
55
|
+
|
56
|
+
Open3.popen3(string) do |i, o, e, t|
|
57
|
+
unless @in.nil?
|
58
|
+
i.print @in
|
59
|
+
i.close
|
60
|
+
end
|
61
|
+
|
62
|
+
@out, @err = [o, e].map do |stream|
|
63
|
+
Thread.new do
|
64
|
+
lines = []
|
65
|
+
until (line = stream.gets).nil? do
|
66
|
+
lines << line
|
67
|
+
@output_mutex.synchronize do
|
68
|
+
@accum << line
|
69
|
+
@output << line
|
70
|
+
end
|
71
|
+
end
|
72
|
+
lines.join
|
73
|
+
end
|
74
|
+
end.map(&:value)
|
75
|
+
|
76
|
+
@pid = t.pid
|
77
|
+
@status = t.value
|
78
|
+
end
|
79
|
+
|
80
|
+
[@out, @err, @output].each(&:freeze)
|
81
|
+
|
82
|
+
if must_succeed? && !success?
|
83
|
+
raise Failed, command: string, status: status.exitstatus, output: output
|
84
|
+
end
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def success?
|
90
|
+
status.success?
|
91
|
+
end
|
92
|
+
|
93
|
+
def exitstatus
|
94
|
+
status.exitstatus
|
95
|
+
end
|
96
|
+
|
97
|
+
def then(&blk)
|
98
|
+
if blk.arity == 1
|
99
|
+
blk.call(self)
|
100
|
+
else
|
101
|
+
blk.call
|
102
|
+
end
|
103
|
+
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def if_success_then(&blk)
|
108
|
+
if success?
|
109
|
+
if blk.arity == 1
|
110
|
+
blk.call(self)
|
111
|
+
else
|
112
|
+
blk.call
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def unless_success_then(&blk)
|
120
|
+
unless success?
|
121
|
+
if blk.arity == 1
|
122
|
+
blk.call(self)
|
123
|
+
else
|
124
|
+
blk.call
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def pipe(command_or_string)
|
132
|
+
command = if String === command_or_string
|
133
|
+
self.class.new(command_or_string, accum: accum, verbose: verbose?)
|
134
|
+
else
|
135
|
+
command_or_string
|
136
|
+
end
|
137
|
+
|
138
|
+
command.append_in(output).()
|
139
|
+
end
|
140
|
+
|
141
|
+
alias_method :|, :pipe
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "fancy_command/version"
|
2
|
+
require "fancy_command/command"
|
3
|
+
|
4
|
+
module FancyCommand
|
5
|
+
def self.new(string, **opts, &blk)
|
6
|
+
Command.new(string, **opts, &blk)
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(string, **opts, &blk)
|
10
|
+
command(string, **opts, &blk).()
|
11
|
+
end
|
12
|
+
|
13
|
+
def command(string, **opts, &blk)
|
14
|
+
Command.new(string, **opts, &blk)
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fancy_command
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- myobie
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-07 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.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
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
|
+
description: I get really tired of not having a good Command class in ruby, so here
|
42
|
+
it is.
|
43
|
+
email:
|
44
|
+
- me@nathanherald.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- fancy_command.gemspec
|
55
|
+
- lib/fancy_command.rb
|
56
|
+
- lib/fancy_command/command.rb
|
57
|
+
- lib/fancy_command/version.rb
|
58
|
+
homepage: http://github.com/myobie/fancy_command
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata: {}
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 2.4.5
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Real-time streaming command output and other nice things
|
82
|
+
test_files: []
|