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 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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fancy_command.gemspec
4
+ gemspec
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,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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,3 @@
1
+ module FancyCommand
2
+ VERSION = "0.0.2"
3
+ 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: []