cmds 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/Gemfile +1 -1
- data/README.md +352 -224
- data/cmds.gemspec +46 -11
- data/lib/cmds.rb +55 -20
- data/lib/cmds/io_handler.rb +15 -2
- data/lib/cmds/spawn.rb +70 -34
- data/lib/cmds/util.rb +3 -12
- data/lib/cmds/util/defaults.rb +45 -16
- data/lib/cmds/util/shell_escape.rb +101 -0
- data/lib/cmds/util/tokenize_option.rb +141 -70
- data/lib/cmds/util/tokenize_value.rb +154 -0
- data/lib/cmds/version.rb +1 -1
- data/spec/cmds/env_spec.rb +71 -1
- data/spec/cmds/prepare_spec.rb +58 -7
- data/spec/cmds/stream/output_spec.rb +69 -0
- data/spec/cmds/util/shell_escape/quote_dance_spec.rb +18 -0
- data/spec/spec_helper.rb +2 -0
- metadata +36 -24
data/cmds.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
# coding: utf-8
|
3
|
+
|
2
4
|
lib = File.expand_path('../lib', __FILE__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require 'cmds/version'
|
@@ -17,18 +19,51 @@ Gem::Specification.new do |spec|
|
|
17
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
21
|
spec.require_paths = ["lib"]
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
|
23
|
+
|
24
|
+
# Dependencies
|
25
|
+
# ============================================================================
|
26
|
+
|
27
|
+
# Runtime Dependencies
|
28
|
+
# ----------------------------------------------------------------------------
|
29
|
+
|
30
|
+
# Mu guns
|
31
|
+
spec.add_dependency "nrser", '~> 0.1', ">= 0.1.1"
|
32
|
+
|
33
|
+
# ERB replacement with more features
|
34
|
+
#
|
35
|
+
# Allows custom auto-escaping, which allows us to shell quote when rendering
|
36
|
+
# values into command templates.
|
37
|
+
#
|
38
|
+
spec.add_dependency 'erubis', '~> 2.7'
|
39
|
+
|
40
|
+
|
41
|
+
# Development Dependencies
|
42
|
+
# ----------------------------------------------------------------------------
|
43
|
+
|
44
|
+
# You've probably heard of Bundler. I like Bundler.
|
45
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
46
|
+
|
47
|
+
# And you've probably hear of Rake. I don't like Rake.
|
25
48
|
spec.add_development_dependency "rake"
|
26
|
-
|
49
|
+
|
50
|
+
# For coloring debug logs. Want to get rid of it when I finally get around to
|
51
|
+
# fixing up logging on config in NRSER and can just use that.
|
27
52
|
spec.add_development_dependency "pastel"
|
28
|
-
spec.add_development_dependency "yard", "~> 0.9.9"
|
29
|
-
spec.add_development_dependency "redcarpet"
|
30
|
-
spec.add_development_dependency 'github-markup'
|
31
53
|
|
32
|
-
#
|
33
|
-
spec.add_development_dependency '
|
54
|
+
# Testing with `rspec`
|
55
|
+
spec.add_development_dependency "rspec", '~> 3.7'
|
56
|
+
|
57
|
+
# Doc site generation with `yard`
|
58
|
+
spec.add_development_dependency "yard", '~> 0.9.12'
|
59
|
+
|
60
|
+
# These, along with `//.yardopts` config, are *supposed to* result in
|
61
|
+
# rendering markdown files and doc comments using
|
62
|
+
# GitHub-Flavored Markdown (GFM), though I'm not sure if it's totally working
|
63
|
+
spec.add_development_dependency "redcarpet", '~> 3.4'
|
64
|
+
spec.add_development_dependency "github-markup", '~> 1.6'
|
65
|
+
|
66
|
+
# Nicer REPL experience
|
67
|
+
spec.add_development_dependency "pry", '~> 0.10.4'
|
68
|
+
|
34
69
|
end
|
data/lib/cmds.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# Requirements
|
2
|
+
# =======================================================================
|
3
|
+
|
4
|
+
# Stdlib
|
5
|
+
# -----------------------------------------------------------------------
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
# Deps
|
9
|
+
# -----------------------------------------------------------------------
|
2
10
|
require 'nrser'
|
3
11
|
|
4
|
-
#
|
12
|
+
# Project / Package
|
13
|
+
# -----------------------------------------------------------------------
|
5
14
|
require 'cmds/version'
|
6
15
|
require 'cmds/debug'
|
7
16
|
require 'cmds/util'
|
@@ -13,7 +22,25 @@ require 'cmds/sugar'
|
|
13
22
|
require 'cmds/stream'
|
14
23
|
require 'cmds/capture'
|
15
24
|
|
25
|
+
|
26
|
+
# Definitions
|
27
|
+
# =======================================================================
|
28
|
+
|
16
29
|
class Cmds
|
30
|
+
|
31
|
+
# Constants
|
32
|
+
# ============================================================================
|
33
|
+
|
34
|
+
# Absolute, expanded path to the gem's root directory.
|
35
|
+
#
|
36
|
+
# @return [Pathname]
|
37
|
+
#
|
38
|
+
ROOT = ( Pathname.new(__FILE__).dirname / '..' ).expand_path
|
39
|
+
|
40
|
+
|
41
|
+
# Attributes
|
42
|
+
# ============================================================================
|
43
|
+
|
17
44
|
# ERB stirng template (with Cmds-specific extensions) for the command.
|
18
45
|
#
|
19
46
|
# @return [String]
|
@@ -26,9 +53,9 @@ class Cmds
|
|
26
53
|
#
|
27
54
|
# defaults to `[]`.
|
28
55
|
#
|
29
|
-
# {#prepare} and the methods that invoke it (like {#capture},
|
56
|
+
# {#prepare} and the methods that invoke it (like {#capture},
|
30
57
|
# {#stream}, etc.) accept `*args`, which will be appended to
|
31
|
-
# these values to create the final array for rendering.
|
58
|
+
# these values to create the final array for rendering.
|
32
59
|
#
|
33
60
|
# @return [Array<Object>]
|
34
61
|
#
|
@@ -39,7 +66,7 @@ class Cmds
|
|
39
66
|
#
|
40
67
|
# defaults to `{}`.
|
41
68
|
#
|
42
|
-
# {#prepare} and the methods that invoke it (like {#capture},
|
69
|
+
# {#prepare} and the methods that invoke it (like {#capture},
|
43
70
|
# {#stream}, etc.) accept `**kwds`, which will be merged on top of
|
44
71
|
# these values to create the final hash for rendering.
|
45
72
|
#
|
@@ -51,7 +78,7 @@ class Cmds
|
|
51
78
|
# string or readable IO-like object to use as default input to the
|
52
79
|
# command.
|
53
80
|
#
|
54
|
-
# {#prepare} and the methods that invoke it (like {#capture},
|
81
|
+
# {#prepare} and the methods that invoke it (like {#capture},
|
55
82
|
# {#stream}, etc.) accept an optional block that will override this
|
56
83
|
# value if present.
|
57
84
|
#
|
@@ -113,7 +140,6 @@ class Cmds
|
|
113
140
|
attr_reader :chdir
|
114
141
|
|
115
142
|
|
116
|
-
|
117
143
|
# The results of the last time {Cmds#prepare} was called on the instance.
|
118
144
|
#
|
119
145
|
# A little bit funky, I know, but it turns out to be quite useful.
|
@@ -126,15 +152,17 @@ class Cmds
|
|
126
152
|
#
|
127
153
|
attr_reader :last_prepared_cmd
|
128
154
|
|
129
|
-
|
130
|
-
|
155
|
+
|
156
|
+
# Constructor
|
157
|
+
# ============================================================================
|
158
|
+
|
131
159
|
# Construct a `Cmds` instance.
|
132
160
|
#
|
133
161
|
# @param [String] template
|
134
|
-
# String template to use when creating the command string to send to the
|
162
|
+
# String template to use when creating the command string to send to the
|
135
163
|
# shell via {#prepare}.
|
136
164
|
#
|
137
|
-
# Allows ERB (positional and keyword), `%s` (positional) and `%{name}`
|
165
|
+
# Allows ERB (positional and keyword), `%s` (positional) and `%{name}`
|
138
166
|
# (keyword) placeholders.
|
139
167
|
#
|
140
168
|
# Available as the {#template} attribute.
|
@@ -145,10 +173,10 @@ class Cmds
|
|
145
173
|
# Available as the {#args} attribute.
|
146
174
|
#
|
147
175
|
# @param [Boolean] assert:
|
148
|
-
# When `true`, execution will raise an error if the command doesn't exit
|
176
|
+
# When `true`, execution will raise an error if the command doesn't exit
|
149
177
|
# successfully (if the command exits with any status other than `0`).
|
150
178
|
#
|
151
|
-
# Available as the {#assert} attribute.
|
179
|
+
# Available as the {#assert} attribute.
|
152
180
|
#
|
153
181
|
# @param [nil | String | Pathname] chdir:
|
154
182
|
# Optional directory to change into when executing.
|
@@ -167,8 +195,8 @@ class Cmds
|
|
167
195
|
# if you want do print the command out and paste it into a terminal.
|
168
196
|
# This is the default.
|
169
197
|
#
|
170
|
-
# - `:spawn_arg` passes them as an argument to `Process.spawn`. In this
|
171
|
-
# case they will not be included in the output of {#prepare}
|
198
|
+
# - `:spawn_arg` passes them as an argument to `Process.spawn`. In this
|
199
|
+
# case they will not be included in the output of {#prepare}
|
172
200
|
# (or {#render}).
|
173
201
|
#
|
174
202
|
# Available as the {#env_mode} attribute.
|
@@ -182,11 +210,11 @@ class Cmds
|
|
182
210
|
#
|
183
211
|
# - `nil` performs **no formatting at all*.
|
184
212
|
#
|
185
|
-
# - `:squish` reduces any consecutive whitespace (including newlines) to
|
213
|
+
# - `:squish` reduces any consecutive whitespace (including newlines) to
|
186
214
|
# a single space. This is the default.
|
187
215
|
#
|
188
|
-
# - `:pretty` tries to keep the general formatting but make it acceptable
|
189
|
-
# to the shell by adding `\` at the end of lines. See
|
216
|
+
# - `:pretty` tries to keep the general formatting but make it acceptable
|
217
|
+
# to the shell by adding `\` at the end of lines. See
|
190
218
|
# {Cmds.pretty_format}.
|
191
219
|
#
|
192
220
|
# - An object that responds to `#call` will be called with the command
|
@@ -226,7 +254,14 @@ class Cmds
|
|
226
254
|
# {#prepare} has never been called. Kinda funky but ends up being useful.
|
227
255
|
@last_prepared_cmd = nil
|
228
256
|
end # #initialize
|
229
|
-
|
257
|
+
|
258
|
+
|
259
|
+
# Instance Methods
|
260
|
+
# ============================================================================
|
261
|
+
#
|
262
|
+
# That ended up here. There are many more topically organized in
|
263
|
+
# `//lib/cmds/*.rb` files.
|
264
|
+
#
|
230
265
|
|
231
266
|
# returns a new {Cmds} with the parameters and input merged in
|
232
267
|
def curry *args, **kwds, &input_block
|
@@ -432,4 +467,4 @@ class Cmds
|
|
432
467
|
|
433
468
|
# @!endgroup Execution Instance Methods
|
434
469
|
|
435
|
-
end # Cmds
|
470
|
+
end # Cmds
|
data/lib/cmds/io_handler.rb
CHANGED
@@ -3,11 +3,20 @@ class Cmds
|
|
3
3
|
attr_accessor :in, :out, :err
|
4
4
|
|
5
5
|
def initialize
|
6
|
-
@queue = Queue.new
|
7
6
|
@in = nil
|
8
7
|
@out = $stdout
|
9
8
|
@err = $stderr
|
10
9
|
end
|
10
|
+
|
11
|
+
def out= value
|
12
|
+
value = value.to_s if value.is_a? Pathname
|
13
|
+
@out = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def err= value
|
17
|
+
value = value.to_s if value.is_a? Pathname
|
18
|
+
@err = value
|
19
|
+
end
|
11
20
|
|
12
21
|
def on_out &block
|
13
22
|
@out = block
|
@@ -32,6 +41,10 @@ class Cmds
|
|
32
41
|
end
|
33
42
|
|
34
43
|
def start
|
44
|
+
# Initialize a thread-safe queue for passing output from the IO threads
|
45
|
+
# back to the main thread
|
46
|
+
@queue = Queue.new
|
47
|
+
|
35
48
|
# if out is a proc, it's not done
|
36
49
|
out_done = ! @out.is_a?(Proc)
|
37
50
|
# same for err
|
@@ -73,4 +86,4 @@ class Cmds
|
|
73
86
|
|
74
87
|
# end private
|
75
88
|
end # end IOHandler
|
76
|
-
end # class Cmds
|
89
|
+
end # class Cmds
|
data/lib/cmds/spawn.rb
CHANGED
@@ -1,26 +1,51 @@
|
|
1
|
-
|
1
|
+
##
|
2
|
+
# Handle the actual spawning of child processes via {Process.spawn}
|
3
|
+
#
|
4
|
+
# These methods are the low-level core of the library. Everything ends up here
|
5
|
+
# to actually execute a command, but users should not need to call them
|
6
|
+
# directly in most cases.
|
7
|
+
##
|
8
|
+
|
9
|
+
# Requirements
|
10
|
+
# =======================================================================
|
11
|
+
|
12
|
+
# Stdlib
|
13
|
+
# -----------------------------------------------------------------------
|
2
14
|
require 'open3'
|
3
15
|
require 'thread'
|
4
16
|
|
5
|
-
#
|
17
|
+
# Deps
|
18
|
+
# -----------------------------------------------------------------------
|
6
19
|
require 'nrser'
|
7
|
-
require 'nrser/refinements'
|
8
20
|
|
9
|
-
#
|
21
|
+
# Project / Package
|
22
|
+
# -----------------------------------------------------------------------
|
10
23
|
require 'cmds/pipe'
|
11
24
|
require 'cmds/io_handler'
|
12
25
|
|
26
|
+
|
27
|
+
# Refinements
|
28
|
+
# =======================================================================
|
29
|
+
|
30
|
+
using NRSER
|
31
|
+
|
32
|
+
|
33
|
+
# Definitions
|
34
|
+
# =======================================================================
|
35
|
+
|
13
36
|
class Cmds
|
37
|
+
# @!group Spawn Methods
|
38
|
+
|
14
39
|
# Low-level static method to spawn and stream inputs and/or outputs using
|
15
40
|
# threads.
|
16
41
|
#
|
17
42
|
# This is the core execution functionality of the whole library - everything
|
18
|
-
#
|
43
|
+
# ends up here.
|
19
44
|
#
|
20
|
-
# **
|
21
|
-
# formatting, interpolation, etc. are done at this point.
|
45
|
+
# **_WARNING - This method runs the `cmd` string AS IS - no escaping,
|
46
|
+
# formatting, interpolation, etc. are done at this point._**
|
22
47
|
#
|
23
|
-
# The whole rest of the library is built on top of this method to provide
|
48
|
+
# The whole rest of the library is built on top of this method to provide
|
24
49
|
# that stuff, and if you're using this library, you probably want to use that
|
25
50
|
# stuff.
|
26
51
|
#
|
@@ -31,7 +56,9 @@ class Cmds
|
|
31
56
|
#
|
32
57
|
# https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
|
33
58
|
#
|
34
|
-
# with major modifications from looking at Ruby's open3 module.
|
59
|
+
# with major modifications from looking at Ruby's [open3][] module.
|
60
|
+
#
|
61
|
+
# [open3]: https://ruby-doc.org/stdlib/libdoc/open3/rdoc/Open3.html
|
35
62
|
#
|
36
63
|
# At the end of the day ends up calling `Process.spawn`.
|
37
64
|
#
|
@@ -39,13 +66,6 @@ class Cmds
|
|
39
66
|
# **SHELL-READY** command string. This is important - whatever you feed in
|
40
67
|
# here will be run **AS IS** - no escaping, formatting, etc.
|
41
68
|
#
|
42
|
-
# @param [nil | String | #read] input
|
43
|
-
# String or readable input, or `nil` (meaning no input).
|
44
|
-
#
|
45
|
-
# Allows {Cmds} instances can pass their `@input` instance variable.
|
46
|
-
#
|
47
|
-
# Don't provide input here and via `io_block`.
|
48
|
-
#
|
49
69
|
# @param [Hash{(Symbol | String) => Object}] env
|
50
70
|
# Hash of `ENV` vars to provide for the command.
|
51
71
|
#
|
@@ -55,10 +75,23 @@ class Cmds
|
|
55
75
|
# Pretty much you want to have everything be strings or symbols for this
|
56
76
|
# to make any sense but we're not checking shit at the moment.
|
57
77
|
#
|
58
|
-
# If the {Cmds#env_mode} is `:inline` it should have already prefixed
|
59
|
-
# `cmd` with the definitions and not provide this keyword (or provide
|
78
|
+
# If the {Cmds#env_mode} is `:inline` it should have already prefixed
|
79
|
+
# `cmd` with the definitions and not provide this keyword (or provide
|
60
80
|
# `{}`).
|
61
81
|
#
|
82
|
+
# @param [nil | String | #read] input
|
83
|
+
# String or readable input, or `nil` (meaning no input).
|
84
|
+
#
|
85
|
+
# Allows {Cmds} instances can pass their `@input` instance variable.
|
86
|
+
#
|
87
|
+
# Don't provide input here and via `io_block`.
|
88
|
+
#
|
89
|
+
# @param [Hash<Symbol, Object>] **spawn_opts
|
90
|
+
# Any additional options are passed as the [options][Process.spawn options]
|
91
|
+
# to {Process.spawn}
|
92
|
+
#
|
93
|
+
# [Process.spawn options]: http://ruby-doc.org/core/Process.html#method-c-spawn
|
94
|
+
#
|
62
95
|
# @param [#call & (#arity ∈ {0, 1})] &io_block
|
63
96
|
# Optional block to handle io. Behavior depends on arity:
|
64
97
|
#
|
@@ -83,15 +116,20 @@ class Cmds
|
|
83
116
|
def self.spawn cmd,
|
84
117
|
env: {},
|
85
118
|
input: nil,
|
86
|
-
|
119
|
+
**spawn_opts,
|
87
120
|
&io_block
|
88
121
|
Cmds.debug "entering Cmds#spawn",
|
89
122
|
cmd: cmd,
|
90
123
|
env: env,
|
91
124
|
input: input,
|
92
|
-
|
125
|
+
spawn_opts: spawn_opts,
|
93
126
|
io_block: io_block
|
94
127
|
|
128
|
+
# Process.spawn doesn't like a `nil` chdir
|
129
|
+
if spawn_opts.key?( :chdir ) && spawn_opts[:chdir].nil?
|
130
|
+
spawn_opts.delete :chdir
|
131
|
+
end
|
132
|
+
|
95
133
|
# create the handler that will be yielded to the input block
|
96
134
|
handler = Cmds::IOHandler.new
|
97
135
|
|
@@ -99,7 +137,7 @@ class Cmds
|
|
99
137
|
#
|
100
138
|
# if a block was provided it overrides the `input` argument.
|
101
139
|
#
|
102
|
-
if io_block
|
140
|
+
if io_block
|
103
141
|
case io_block.arity
|
104
142
|
when 0
|
105
143
|
# when the input block takes no arguments it returns the input
|
@@ -137,17 +175,11 @@ class Cmds
|
|
137
175
|
end # case io_block.arity
|
138
176
|
end # if io_block
|
139
177
|
|
140
|
-
# hash of options that will be passed to `spawn`
|
141
|
-
spawn_opts = {}
|
142
|
-
|
143
|
-
# add chdir if provided
|
144
|
-
spawn_opts[:chdir] = chdir if chdir
|
145
|
-
|
146
178
|
Cmds.debug "looking at input...",
|
147
179
|
input: input
|
148
180
|
|
149
181
|
# (possibly) create the input pipe... this will be nil if the provided
|
150
|
-
# input is io-like. in this case it will be used directly in the
|
182
|
+
# input is io-like. in this case it will be used directly in the
|
151
183
|
# `spawn` options.
|
152
184
|
in_pipe = case input
|
153
185
|
when nil, String
|
@@ -176,7 +208,7 @@ class Cmds
|
|
176
208
|
# `stream` can be told to send it's output to either:
|
177
209
|
#
|
178
210
|
# 1. a Proc that will invoked with each line.
|
179
|
-
# 2. an io-like object that can be provided as `spawn`'s `:out` or
|
211
|
+
# 2. an io-like object that can be provided as `spawn`'s `:out` or
|
180
212
|
# `:err` options.
|
181
213
|
#
|
182
214
|
# in case (1) a `Cmds::Pipe` wrapping read and write piped `IO` instances
|
@@ -190,8 +222,11 @@ class Cmds
|
|
190
222
|
["OUTPUT", :out],
|
191
223
|
].map do |name, sym|
|
192
224
|
Cmds.debug "looking at #{ name }..."
|
225
|
+
|
226
|
+
dest = handler.public_send sym
|
227
|
+
|
193
228
|
# see if hanlder.out or hanlder.err is a Proc
|
194
|
-
if
|
229
|
+
if dest.is_a? Proc
|
195
230
|
Cmds.debug "#{ name } is a Proc, creating pipe..."
|
196
231
|
pipe = Cmds::Pipe.new name, sym
|
197
232
|
# the corresponding :out or :err option for spawn needs to be
|
@@ -202,8 +237,8 @@ class Cmds
|
|
202
237
|
|
203
238
|
else
|
204
239
|
Cmds.debug "#{ name } should be io-like, setting spawn opt.",
|
205
|
-
output:
|
206
|
-
spawn_opts[sym] =
|
240
|
+
output: dest
|
241
|
+
spawn_opts[sym] = dest
|
207
242
|
# the pipe is nil!
|
208
243
|
nil
|
209
244
|
end
|
@@ -321,7 +356,7 @@ class Cmds
|
|
321
356
|
protected
|
322
357
|
# ========================================================================
|
323
358
|
|
324
|
-
# Internal method that simply passes through to {Cmds.spawn}, serving as
|
359
|
+
# Internal method that simply passes through to {Cmds.spawn}, serving as
|
325
360
|
# a hook point for subclasses.
|
326
361
|
#
|
327
362
|
# Accepts and returns the same things as {Cmds#stream}.
|
@@ -335,10 +370,11 @@ class Cmds
|
|
335
370
|
# include env if mode is spawn argument
|
336
371
|
env: (env_mode == :spawn_arg ? env : {}),
|
337
372
|
chdir: chdir,
|
373
|
+
unsetenv_others: !!@unsetenv_others,
|
338
374
|
&io_block
|
339
375
|
end # #spawn
|
340
376
|
|
341
377
|
# end protected
|
342
378
|
|
343
379
|
|
344
|
-
end # Cmds
|
380
|
+
end # Cmds
|