Class: Cmds
- Inherits:
-
Object
- Object
- Cmds
- Defined in:
- lib/cmds/debug.rb,
lib/cmds.rb,
lib/cmds/pipe.rb,
lib/cmds/util.rb,
lib/cmds/spawn.rb,
lib/cmds/sugar.rb,
lib/cmds/result.rb,
lib/cmds/stream.rb,
lib/cmds/capture.rb,
lib/cmds/version.rb,
lib/cmds/io_handler.rb,
lib/cmds/erb_context.rb,
lib/cmds/shell_eruby.rb,
lib/cmds/util/params.rb,
lib/cmds/util/defaults.rb,
lib/cmds/util/tokenize_option.rb,
lib/cmds/util/tokenize_options.rb
Overview
debug logging stuff
Defined Under Namespace
Modules: Debug Classes: ERBContext, IOHandler, Params, Pipe, Result, ShellEruby
Constant Summary collapse
- VERSION =
"0.2.3"
- DEFAULTS =
hash of common default values used in method options.
don't use them directly -- use defaults.
the values themselves are frozen so we don't have to worry about cloning them before providing them for use.
the constant Hash itself is not frozen -- you can mutate this to change the default options for ALL Cmds method calls... just be aware of what you're doing. not recommended outside of quick hacks and small scripts since other pieces and parts you don't even know about may depend on said behavior.
{ # positional arguments for a command args: [], # keyword arguments for a command kwds: {}, # how to format a command string for execution format: :squish, # what to do with array option values array_mode: :join, # what to join array option values with when using `array_mode = :join` array_join_string: ',', # what to do with false array values false_mode: :omit, # Stick ENV var defs inline at beginning of command env_mode: :inline, # No additional environment env: {}, # Don't change directories chdir: nil, # Don't asset (raise error if exit code is not 0) assert: false, # No input input: nil, }.map { |k, v| [k, v.freeze] }.to_h.freeze
- TOKENIZE_OPT_KEYS =
[:array_mode, :array_join_string, :false_mode]
Instance Attribute Summary collapse
-
#args ⇒ Array<Object>
readonly
base/common positional parameters to render into the command template.
-
#assert ⇒ Boolean
readonly
if
true
, will execution will raise an error on non-zero exit code. -
#chdir ⇒ nil, String | Pathname
readonly
Optional directory to run the command in, set by the
:chdir
option in #initialize. -
#env ⇒ Hash{String | Symbol => String}
readonly
Environment variables to set for command execution.
-
#env_mode ⇒ :inline, :spawn_arg
readonly
How environment variables will be set for command execution - inline at the top of the command, or passed to
Process.spawn
as an argument. -
#format ⇒ :squish | :pretty
readonly
format specifier symbol:.
-
#input ⇒ String | #read
readonly
string or readable IO-like object to use as default input to the command.
-
#kwds ⇒ Hash{Symbol => Object}
readonly
base/common keyword parameters to render into the command template.
-
#last_prepared_cmd ⇒ nil, String
readonly
The results of the last time #prepare was called on the instance.
-
#template ⇒ String
readonly
ERB stirng template (with Cmds-specific extensions) for the command.
Class Method Summary collapse
-
.assert(template, *args, **kwds, &io_block) ⇒ Object
create a new Cmds and.
- .capture(template, *args, **kwds, &input_block) ⇒ Result
-
.check_status(cmd, status, err = nil) ⇒ nil
raise an error unless the exit status is 0.
-
.chomp(template, *args, **kwds, &input_block) ⇒ String
captures a new Cmds, captures and chomps stdout (sugar for
Cmds.out(template, *args, **kwds, &input_block).chomp
). -
.chomp!(template, *args, **kwds, &input_block) ⇒ String
captures and chomps stdout, raising an error if the command fails.
-
.debug(msg, values = {}) ⇒ Object
log a debug message along with an optional hash of values.
-
.defaults(opts, keys = '*', extras = {}) ⇒ Hash<Symbol, Object>
merge an method call options hash with common defaults for the module.
-
.err(template, *args, **kwds, &input_block) ⇒ String
captures and returns stderr (sugar for
Cmds.capture(template, *args, **kwds, &input_block).err
). - .error?(template, *args, **kwds, &io_block) ⇒ Boolean
-
.esc(str) ⇒ Object
shortcut for Shellwords.escape.
-
.format(string, with = :squish) ⇒ Object
Formats a command string.
-
.ok?(template, *args, **kwds, &io_block) ⇒ Result
create a new Cmds from template with parameters and call Cmd#ok? on it.
-
.out(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout (sugar for
Cmds.capture(template, *args, **kwds, &input_block).out
). -
.out!(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout.
- .prepare(template, *args, **kwds, &options_block) ⇒ String
- .pretty_format(string) ⇒ Object
- .replace_shortcuts(template) ⇒ Object
-
.spawn(cmd, **opts, &io_block) ⇒ Fixnum
internal core function to spawn and stream inputs and/or outputs using threads.
- .stream(template, *subs, &input_block) ⇒ Object
- .stream!(template, *args, **kwds, &io_block) ⇒ Object
-
.tokenize(*values, **opts) ⇒ String
tokenize values for the shell.
-
.tokenize_option(name, value, **opts) ⇒ Array<String>
turn an option name and value into an array of shell-escaped string token suitable for use in a command.
-
.tokenize_options(hash, **opts) ⇒ Object
escape option hash.
Instance Method Summary collapse
-
#capture(*args, **kwds, &input_block) ⇒ Cmds::Result
(also: #call)
executes the command and returns a Result with the captured outputs.
-
#chomp(*args, **kwds, &input_block) ⇒ String
captures and chomps stdout (sugar for
#out(*subs, &input_block).chomp
). -
#chomp!(*args, **kwds, &input) ⇒ String
captures and chomps stdout, raising an error if the command failed.
-
#curry(*args, **kwds, &input_block) ⇒ Object
returns a new Cmds with the parameters and input merged in.
-
#defaults(opts, keys = '*', extras = {}) ⇒ Object
proxy through to class method Cmds.defaults.
-
#err(*args, **kwds, &input_block) ⇒ String
captures and returns stdout (sugar for
#capture(*subs, &input_block).err
). -
#error?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return
true
if it failed. -
#initialize(template, **opts) ⇒ Cmds
constructor
Construct a
Cmds
instance. -
#ok?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return
true
if it exited successfully. -
#out(*args, **kwds, &input_block) ⇒ String
captures and returns stdout (sugar for
#capture(*args, **kwds, &input_block).out
). -
#out!(*args, **kwds, &input) ⇒ String
captures and returns stdout (sugar for
#capture(*args, **kwds, &input_block).out
). -
#prepare(*args, **kwds) ⇒ String
prepare a shell-safe command string for execution.
- #proxy ⇒ Object
-
#render(*args, **kwds) ⇒ String
render parameters into
@template
. -
#stream(*args, **kwds, &io_block) ⇒ Fixnum
stream a command.
-
#stream!(*args, **kwds, &io_block) ⇒ Object
stream and raise an error if exit code is not 0.
Constructor Details
#initialize(template, **opts) ⇒ Cmds
Construct a Cmds
instance.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/cmds.rb', line 211 def initialize template, **opts opts = defaults opts Cmds.debug "Cmd constructing...", template: template, opts: opts @template = template # Assign options to instance variables opts.each { |key, value| instance_variable_set "@#{ key }", value } # An internal cache of the last result of calling {#prepare}, or `nil` if # {#prepare} has never been called. Kinda funky but ends up being useful. @last_prepared_cmd = nil end |
Instance Attribute Details
#args ⇒ Array<Object> (readonly)
35 36 37 |
# File 'lib/cmds.rb', line 35 def args @args end |
#assert ⇒ Boolean (readonly)
if true
, will execution will raise an error on non-zero exit code.
defaults to false
.
69 70 71 |
# File 'lib/cmds.rb', line 69 def assert @assert end |
#chdir ⇒ nil, String | Pathname (readonly)
Optional directory to run the command in, set by the :chdir
option
in #initialize.
113 114 115 |
# File 'lib/cmds.rb', line 113 def chdir @chdir end |
#env ⇒ Hash{String | Symbol => String} (readonly)
Environment variables to set for command execution.
defaults to {}
.
78 79 80 |
# File 'lib/cmds.rb', line 78 def env @env end |
#env_mode ⇒ :inline, :spawn_arg (readonly)
How environment variables will be set for command execution - inline at
the top of the command, or passed to Process.spawn
as an argument.
See the inline
88 89 90 |
# File 'lib/cmds.rb', line 88 def env_mode @env_mode end |
#format ⇒ :squish | :pretty (readonly)
format specifier symbol:
-
:squish
- collapse rendered command string to one line.
-
:pretty
- clean up and backslash suffix line endings.
defaults to :squish
.
101 102 103 |
# File 'lib/cmds.rb', line 101 def format @format end |
#input ⇒ String | #read (readonly)
60 61 62 |
# File 'lib/cmds.rb', line 60 def input @input end |
#kwds ⇒ Hash{Symbol => Object} (readonly)
48 49 50 |
# File 'lib/cmds.rb', line 48 def kwds @kwds end |
#last_prepared_cmd ⇒ nil, String (readonly)
The results of the last time #prepare was called on the instance.
A little bit funky, I know, but it turns out to be quite useful.
127 128 129 |
# File 'lib/cmds.rb', line 127 def last_prepared_cmd @last_prepared_cmd end |
#template ⇒ String (readonly)
ERB stirng template (with Cmds-specific extensions) for the command.
21 22 23 |
# File 'lib/cmds.rb', line 21 def template @template end |
Class Method Details
.assert(template, *args, **kwds, &io_block) ⇒ Object
create a new Cmds and
92 93 94 |
# File 'lib/cmds/sugar.rb', line 92 def self.assert template, *args, **kwds, &io_block Cmds.new(template).capture(*args, **kwds, &io_block).assert end |
.capture(template, *args, **kwds, &input_block) ⇒ Result
65 66 67 |
# File 'lib/cmds/sugar.rb', line 65 def self.capture template, *args, **kwds, &input_block Cmds.new(template).capture *args, **kwds, &input_block end |
.check_status(cmd, status, err = nil) ⇒ nil
raise an error unless the exit status is 0.
137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/cmds/util.rb', line 137 def self.check_status cmd, status, err = nil unless status.equal? 0 msg = NRSER.squish <<-END command `#{ cmd }` exited with status #{ status } END if err msg += " and stderr:\n\n" + err end raise SystemCallError.new msg, status end end |
.chomp(template, *args, **kwds, &input_block) ⇒ String
captures a new Cmds, captures and chomps stdout
(sugar for Cmds.out(template, *args, **kwds, &input_block).chomp
).
159 160 161 |
# File 'lib/cmds/sugar.rb', line 159 def self.chomp template, *args, **kwds, &input_block out(template, *args, **kwds, &input_block).chomp end |
.chomp!(template, *args, **kwds, &input_block) ⇒ String
captures and chomps stdout, raising an error if the command fails.
(sugar for Cmds.out!(template, *args, **kwds, &input_block).chomp
).
180 181 182 |
# File 'lib/cmds/sugar.rb', line 180 def self.chomp! template, *args, **kwds, &input_block out!(template, *args, **kwds, &input_block).chomp end |
.debug(msg, values = {}) ⇒ Object
log a debug message along with an optional hash of values.
96 97 98 99 100 |
# File 'lib/cmds/debug.rb', line 96 def self.debug msg, values = {} # don't even bother unless debug logging is turned on return unless Debug.on? Debug.logger.debug Debug.format(msg, values) end |
.defaults(opts, keys = '*', extras = {}) ⇒ Hash<Symbol, Object>
merge an method call options hash with common defaults for the module.
this makes it easy to use the same defaults in many different methods without repeating the declarations everywhere.
69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/cmds/util/defaults.rb', line 69 def self.defaults opts, keys = '*', extras = {} if keys == '*' DEFAULTS.dup else keys. map {|key| [key, DEFAULTS.fetch(key)] }. to_h end. merge!( extras ). merge!( opts ) end |
.err(template, *args, **kwds, &input_block) ⇒ String
captures and returns stderr
(sugar for Cmds.capture(template, *args, **kwds, &input_block).err
).
198 199 200 |
# File 'lib/cmds/sugar.rb', line 198 def self.err template, *args, **kwds, &input_block capture(template, *args, **kwds, &input_block).err end |
.error?(template, *args, **kwds, &io_block) ⇒ Boolean
86 87 88 |
# File 'lib/cmds/sugar.rb', line 86 def self.error? template, *args, **kwds, &io_block Cmds.new(template).error? *args, **kwds, &io_block end |
.esc(str) ⇒ Object
shortcut for Shellwords.escape
also makes it easier to change or customize or whatever
19 20 21 |
# File 'lib/cmds/util.rb', line 19 def self.esc str Shellwords.escape str end |
.format(string, with = :squish) ⇒ Object
Formats a command string.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/cmds/util.rb', line 55 def self.format string, with = :squish case with when nil string when :squish NRSER.squish string when :pretty pretty_format string else with.call string end end |
.ok?(template, *args, **kwds, &io_block) ⇒ Result
create a new Cmds from template with parameters and call Cmd#ok? on it.
81 82 83 |
# File 'lib/cmds/sugar.rb', line 81 def self.ok? template, *args, **kwds, &io_block Cmds.new(template).ok? *args, **kwds, &io_block end |
.out(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout
(sugar for Cmds.capture(template, *args, **kwds, &input_block).out
).
120 121 122 |
# File 'lib/cmds/sugar.rb', line 120 def self.out template, *args, **kwds, &input_block Cmds.new(template).out *args, **kwds, &input_block end |
.out!(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout. raises an error if the command fails.
141 142 143 |
# File 'lib/cmds/sugar.rb', line 141 def self.out! template, *args, **kwds, &input_block Cmds.new(template).out! *args, **kwds, &input_block end |
.prepare(template, *args, **kwds, &options_block) ⇒ String
40 41 42 43 44 45 46 47 48 |
# File 'lib/cmds/sugar.rb', line 40 def self.prepare template, *args, **kwds, & = if .call else {} end Cmds.new(template, **).prepare *args, **kwds end |
.pretty_format(string) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/cmds/util.rb', line 72 def self.pretty_format string string = string.gsub(/\n(\s*\n)+\n/, "\n\n") string.lines.map {|line| line = line.rstrip if line.end_with? '\\' line elsif line == '' '\\' elsif line =~ /\s$/ line + '\\' else line + ' \\' end }.join("\n") end |
.replace_shortcuts(template) ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/cmds/util.rb', line 90 def self.replace_shortcuts template template .gsub( # %s => <%= arg %> /(?<=\A|\=|[[:space:]])\%s(?=\Z|[[:space:]])/, '<%= arg %>' ) .gsub( # %%s => %s (escaping) /(?<=\A|[[:space:]])(\%+)\%s(?=\Z|[[:space:]])/, '\1s' ) .gsub( # %{key} => <%= key %>, %{key?} => <%= key? %> /(?<=\A|\=|[[:space:]])\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/, '<%= \1 %>' ) .gsub( # %%{key} => %{key}, %%{key?} => %{key?} (escaping) /(?<=\A|[[:space:]])(\%+)\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/, '\1{\2}\3' ) .gsub( # %<key>s => <%= key %>, %<key?>s => <%= key? %> /(?<=\A|\=|[[:space:]])\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/, '<%= \1 %>' ) .gsub( # %%<key>s => %<key>s, %%<key?>s => %<key?>s (escaping) /(?<=\A|[[:space:]])(\%+)\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/, '\1<\2>s' ) end |
.spawn(cmd, **opts, &io_block) ⇒ Fixnum
internal core function to spawn and stream inputs and/or outputs using threads.
originally inspired by
https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
with major modifications from looking at Ruby's open3 module.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/cmds/spawn.rb', line 49 def self.spawn cmd, **opts, &io_block Cmds.debug "entering Cmds#spawn", cmd: cmd, opts: opts, io_block: io_block env = opts[:env] || {} input = opts[:input] chdir = opts[:chdir] # create the handler that will be yielded to the input block handler = Cmds::IOHandler.new # handle input # # if a block was provided it overrides the `input` argument. # if io_block case io_block.arity when 0 # when the input block takes no arguments it returns the input input = io_block.call when 1 # when the input block takes one argument, give it the handler and # ignore the return value io_block.call handler # if input was assigned to the handler in the block, use it as input input = handler.in unless handler.in.nil? else # bad block provided raise ArgumentError.new NRSER.squish <<-BLOCK provided input block must have arity 0 or 1 BLOCK end # case io_block.arity end # if io_block # hash of options that will be passed to `spawn` spawn_opts = {} # add chdir if provided spawn_opts[:chdir] = chdir if chdir Cmds.debug "looking at input...", input: input # (possibly) create the input pipe... this will be nil if the provided # input is io-like. in this case it will be used directly in the # `spawn` options. in_pipe = case input when nil, String Cmds.debug "input is a String or nil, creating pipe..." in_pipe = Cmds::Pipe.new "INPUT", :in spawn_opts[:in] = in_pipe.r # don't buffer input in_pipe.w.sync = true in_pipe else Cmds.debug "input should be io-like, setting spawn opt.", input: input if input == $stdin Cmds.debug "input is $stdin." end spawn_opts[:in] = input nil end # case input # (possibly) create the output pipes. # # `stream` can be told to send it's output to either: # # 1. a Proc that will invoked with each line. # 2. an io-like object that can be provided as `spawn`'s `:out` or # `:err` options. # # in case (1) a `Cmds::Pipe` wrapping read and write piped `IO` instances # will be created and assigned to the relevant of `out_pipe` or `err_pipe`. # # in case (2) the io-like object will be sent directly to `spawn` and # the relevant `out_pipe` or `err_pipe` will be `nil`. # out_pipe, err_pipe = [ ["ERROR", :err], ["OUTPUT", :out], ].map do |name, sym| Cmds.debug "looking at #{ name }..." # see if hanlder.out or hanlder.err is a Proc if handler.send(sym).is_a? Proc Cmds.debug "#{ name } is a Proc, creating pipe..." pipe = Cmds::Pipe.new name, sym # the corresponding :out or :err option for spawn needs to be # the pipe's write handle spawn_opts[sym] = pipe.w # return the pipe pipe else Cmds.debug "#{ name } should be io-like, setting spawn opt.", output: handler.send(sym) spawn_opts[sym] = handler.send(sym) # the pipe is nil! nil end end # map outputs Cmds.debug "spawning...", env: env, cmd: cmd, opts: spawn_opts pid = Process.spawn env.map {|k, v| [k.to_s, v]}.to_h, cmd, spawn_opts Cmds.debug "spawned.", pid: pid wait_thread = Process.detach pid wait_thread[:name] = "WAIT" Cmds.debug "wait thread created.", thread: wait_thread # close child ios if created # the spawned process will read from in_pipe.r so we don't need it in_pipe.r.close if in_pipe # and we don't need to write to the output pipes, that will also happen # in the spawned process [out_pipe, err_pipe].each {|pipe| pipe.w.close if pipe} # create threads to handle any pipes that were created in_thread = if in_pipe Thread.new do Thread.current[:name] = in_pipe.name Cmds.debug "thread started, writing input..." in_pipe.w.write input unless input.nil? Cmds.debug "write done, closing in_pipe.w..." in_pipe.w.close Cmds.debug "thread done." end # Thread end out_thread, err_thread = [out_pipe, err_pipe].map do |pipe| if pipe Thread.new do Thread.current[:name] = pipe.name Cmds.debug "thread started" loop do Cmds.debug "blocking on gets..." line = pipe.r.gets if line.nil? Cmds.debug "received nil, output done." else Cmds.debug NRSER.squish <<-BLOCK received #{ line.bytesize } bytes, passing to handler. BLOCK end handler.thread_send_line pipe.sym, line break if line.nil? end Cmds.debug "reading done, closing pipe.r (unless already closed)..." pipe.r.close unless pipe.r.closed? Cmds.debug "thread done." end # thread end # if pipe end # map threads Cmds.debug "handing off main thread control to the handler..." begin handler.start Cmds.debug "handler done." ensure # wait for the threads to complete Cmds.debug "joining threads..." [in_thread, out_thread, err_thread, wait_thread].each do |thread| if thread Cmds.debug "joining #{ thread[:name] } thread..." thread.join end end Cmds.debug "all threads done." end status = wait_thread.value.exitstatus Cmds.debug "exit status: #{ status.inspect }" Cmds.debug "checking @assert and exit status..." if @assert && status != 0 # we don't necessarily have the err output, so we can't include it # in the error message msg = NRSER.squish <<-BLOCK streamed command `#{ cmd }` exited with status #{ status } BLOCK raise SystemCallError.new msg, status end Cmds.debug "streaming completed." return status end |
.stream(template, *subs, &input_block) ⇒ Object
97 98 99 |
# File 'lib/cmds/sugar.rb', line 97 def self.stream template, *subs, &input_block Cmds.new(template).stream *subs, &input_block end |
.stream!(template, *args, **kwds, &io_block) ⇒ Object
102 103 104 |
# File 'lib/cmds/sugar.rb', line 102 def self.stream! template, *args, **kwds, &io_block Cmds.new(template).stream! *args, **kwds, &io_block end |
.tokenize(*values, **opts) ⇒ String
tokenize values for the shell. each values is tokenized individually and the results are joined with a space.
32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/cmds/util.rb', line 32 def self.tokenize *values, **opts values.map {|value| case value when nil # nil is just an empty string, NOT an empty string bash token '' when Hash value, **opts else esc value.to_s end }.join ' ' end |
.tokenize_option(name, value, **opts) ⇒ Array<String>
turn an option name and value into an array of shell-escaped string token suitable for use in a command.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/cmds/util/tokenize_option.rb', line 38 def self.tokenize_option name, value, **opts pp opts: opts opts = defaults opts, TOKENIZE_OPT_KEYS unless name.is_a?(String) && name.length > 0 raise ArgumentError.new NRSER.squish <<-END `name` must be a String of length greater than zero, found #{ name.inspect } END end prefix, separator = if name.length == 1 # -b <value> style ['-', ' '] else # --blah=<value> style ['--', '='] end case value when nil [] when Array # the PITA one case opts[:array_mode] when :repeat # `-b 1 -b 2 -b 3` / `--blah=1 --blah=2 --blah=3` style value.flatten.map {|v| prefix + esc(name) + separator + esc(v) } when :join # `-b 1,2,3` / `--blah=1,2,3` style [ prefix + esc(name) + separator + esc(value.join opts[:array_join_string]) ] when :json [prefix + esc(name) + separator + "'" + JSON.dump(value).gsub(%{'}, %{'"'"'}) + "'"] else # SOL raise ArgumentError.new NRSER.squish <<-END bad array_mode option: #{ opts[:array_mode] }, should be :repeat, :join or :json END end when true # `-b` or `--blah` [prefix + esc(name)] when false case opts[:false_mode] when :omit # don't emit any token for a false boolean [] when :no # `--no-blah` style # # but there's not really a great way to handle short names... # we use `--no-b` # ["--no-#{ esc(name) }"] else raise ArgumentError.new NRSER.squish <<-END bad :false_mode option: #{ opts[:false_mode] }, should be :omit or :no END end else # we let .esc handle it [prefix + esc(name) + separator + esc(value)] end end |
.tokenize_options(hash, **opts) ⇒ Object
escape option hash.
this is only useful for the two common option styles:
single character keys become
-<char> <value>
{x: 1} => "-x 1"
longer keys become
--<key>=<value>
options{blah: 2} => "--blah=2"
if you have something else, you're going to have to just put it in the cmd itself, like:
Cmds "blah -assholeOptionOn:%{s}", "ok"
or whatever similar shit said command requires.
however, if the value is an Array, it will repeat the option for each value:
{x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
{blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
i can't think of any right now, but i swear i've seen commands that take opts that way.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/cmds/util/tokenize_options.rb', line 32 def self. hash, **opts opts = defaults opts, TOKENIZE_OPT_KEYS hash.map {|key, value| # keys need to be strings key = key.to_s unless key.is_a? String [key, value] }.sort {|(key_a, value_a), (key_b, value_b)| # sort by the (now string) keys key_a <=> key_b }.map {|key, value| tokenize_option key, value, **opts }.flatten.join ' ' end |
Instance Method Details
#capture(*args, **kwds, &input_block) ⇒ Cmds::Result Also known as: call
executes the command and returns a Result with the captured outputs.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/cmds/capture.rb', line 21 def capture *args, **kwds, &input_block Cmds.debug "entering Cmds#capture", args: args, kwds: kwds, input: input # extract input from block via `call` if one is provided, # otherwise default to instance variable (which may be `nil`) input = input_block.nil? ? input : input_block.call Cmds.debug "configured input", input: input # strings output will be concatenated onto out = '' err = '' Cmds.debug "calling Cmds.spawn..." status = spawn(*args, **kwds) do |io| # send the input to stream, which sends it to spawn io.in = input # and concat the output lines as they come in io.on_out do |line| out += line end io.on_err do |line| err += line end end Cmds.debug "Cmds.spawn completed", status: status # build a Result # result = Cmds::Result.new cmd, status, out_reader.value, err_reader.value result = Cmds::Result.new last_prepared_cmd, status, out, err # tell the Result to assert if the Cmds has been told to, which will # raise a SystemCallError with the exit status if it was non-zero result.assert if assert return result end |
#chomp(*args, **kwds, &input_block) ⇒ String
captures and chomps stdout
(sugar for #out(*subs, &input_block).chomp
).
384 385 386 |
# File 'lib/cmds.rb', line 384 def chomp *args, **kwds, &input_block out(*args, **kwds, &input_block).chomp end |
#chomp!(*args, **kwds, &input) ⇒ String
captures and chomps stdout, raising an error if the command failed.
(sugar for #out!(*subs, &input_block).chomp
).
405 406 407 |
# File 'lib/cmds.rb', line 405 def chomp! *args, **kwds, &input out!(*args, **kwds, &input).chomp end |
#curry(*args, **kwds, &input_block) ⇒ Object
returns a new Cmds with the parameters and input merged in
232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/cmds.rb', line 232 def curry *args, **kwds, &input_block self.class.new template, { args: (self.args + args), kwds: (self.kwds.merge kwds), input: (input_block ? input_block.call : self.input), assert: self.assert, env: self.env, format: self.format, chdir: self.chdir, } end |
#defaults(opts, keys = '*', extras = {}) ⇒ Object
proxy through to class method defaults.
86 87 88 |
# File 'lib/cmds/util/defaults.rb', line 86 def defaults opts, keys = '*', extras = {} self.class.defaults opts, keys, extras end |
#err(*args, **kwds, &input_block) ⇒ String
captures and returns stdout
(sugar for #capture(*subs, &input_block).err
).
423 424 425 |
# File 'lib/cmds.rb', line 423 def err *args, **kwds, &input_block capture(*args, **kwds, &input_block).err end |
#error?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return true
if it failed.
324 325 326 |
# File 'lib/cmds.rb', line 324 def error? *args, **kwds, &io_block stream(*args, **kwds, &io_block) != 0 end |
#ok?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return true
if it exited successfully.
310 311 312 |
# File 'lib/cmds.rb', line 310 def ok? *args, **kwds, &io_block stream(*args, **kwds, &io_block) == 0 end |
#out(*args, **kwds, &input_block) ⇒ String
captures and returns stdout
(sugar for #capture(*args, **kwds, &input_block).out
).
349 350 351 |
# File 'lib/cmds.rb', line 349 def out *args, **kwds, &input_block capture(*args, **kwds, &input_block).out end |
#out!(*args, **kwds, &input) ⇒ String
captures and returns stdout
(sugar for #capture(*args, **kwds, &input_block).out
).
367 368 369 |
# File 'lib/cmds.rb', line 367 def out! *args, **kwds, &input capture(*args, **kwds, &input).assert.out end |
#prepare(*args, **kwds) ⇒ String
prepare a shell-safe command string for execution.
296 297 298 |
# File 'lib/cmds.rb', line 296 def prepare *args, **kwds @last_prepared_cmd = Cmds.format render(*args, **kwds), self.format end |
#proxy ⇒ Object
329 330 331 332 333 |
# File 'lib/cmds.rb', line 329 def proxy stream do |io| io.in = $stdin end end |
#render(*args, **kwds) ⇒ String
the returned string is not formatted for shell execution.
Cmds passes this string through format before execution,
which addresses newlines in the rendered string through "squishing"
everything down to one line or adding \
to line ends.
render parameters into @template
.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/cmds.rb', line 258 def render *args, **kwds # Create the context for ERB context = Cmds::ERBContext.new( (self.args + args), self.kwds.merge( kwds ), tokenize_options_opts: TOKENIZE_OPT_KEYS. each_with_object( {} ) { |key, hash| value = instance_variable_get "@#{ key}" hash[key] = value unless value.nil? } ) erb = Cmds::ShellEruby.new Cmds.replace_shortcuts( self.template ) rendered = NRSER.dedent erb.result(context.get_binding) if self.env_mode == :inline && !self.env.empty? rendered = self.env.sort_by {|name, value| name }.map {|name, value| "#{ name }=#{ Cmds.esc value }" }.join("\n\n") + "\n\n" + rendered end rendered end |
#stream(*args, **kwds, &io_block) ⇒ Fixnum
stream a command.
13 14 15 16 17 18 19 20 |
# File 'lib/cmds/stream.rb', line 13 def stream *args, **kwds, &io_block Cmds.debug "entering Cmds#stream", args: args, kwds: kwds, io_block: io_block spawn *args, **kwds, &io_block end |
#stream!(*args, **kwds, &io_block) ⇒ Object
stream and raise an error if exit code is not 0.
32 33 34 35 36 37 38 |
# File 'lib/cmds/stream.rb', line 32 def stream! *args, **kwds, &io_block status = stream *args, **kwds, &io_block Cmds.check_status last_prepared_cmd, status status end |