cmds 0.0.9 → 0.1.0

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.
@@ -0,0 +1,71 @@
1
+ module Cmds
2
+ # hash of common default values used in method options.
3
+ #
4
+ # don't use them directly -- use {Cmds.defaults}.
5
+ #
6
+ # the values themselves are frozen so we don't have to worry about cloning
7
+ # them before providing them for use.
8
+ #
9
+ # the constant Hash itself is **not** frozen -- you can mutate this to
10
+ # change the default options for **ALL** Cmds method calls...
11
+ # just be aware of what you're doing. not recommended
12
+ # outside of quick hacks and small scripts since other pieces and parts
13
+ # you don't even know about may depend on said behavior.
14
+ #
15
+ DEFAULTS = {
16
+ # positional arguments for a command
17
+ args: [],
18
+
19
+ # keyword arguments for a command
20
+ kwds: {},
21
+
22
+ # how to format a command string for execution
23
+ format: :squish,
24
+
25
+ # what to do with array option values
26
+ array_mode: :join,
27
+
28
+ # what to join array option values with when using `array_mode = :join`
29
+ array_join_string: ',',
30
+
31
+ # what to do with false array values
32
+ false_mode: :omit,
33
+ }.map {|k, v| [k, v.freeze]}.to_h
34
+
35
+ # merge an method call options hash with common defaults for the module.
36
+ #
37
+ # this makes it easy to use the same defaults in many different methods
38
+ # without repeating the declarations everywhere.
39
+ #
40
+ # @param [Hash] opts
41
+ # hash of overrides provided by method caller.
42
+ #
43
+ # @param [Array<Symbol>, '*'] keys
44
+ # keys for the defaults you want to use.
45
+ #
46
+ # @param [Hash<Symbol, Object>] extras
47
+ # extra keys and values to add to the returned defaults.
48
+ #
49
+ # @return [Hash<Symbol, Object>]
50
+ # defaults to use in the method call.
51
+ #
52
+ def self.defaults opts, keys = '*', extras = {}
53
+ if keys == '*'
54
+ DEFAULTS
55
+ else
56
+ keys.
57
+ map {|key|
58
+ [key, DEFAULTS.fetch(key)]
59
+ }.
60
+ to_h
61
+ end.
62
+ merge!(extras).
63
+ merge!(opts)
64
+ end
65
+
66
+ # proxy through to class method {Cmds.defaults}.
67
+ #
68
+ def defaults opts, keys = '*', extras = {}
69
+ self.class.defaults opts, keys, extras
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ module Cmds
2
+ class Params
3
+ # Cmds instance execution methods take a splat and block
4
+ def self.normalize *params, &input
5
+ args = []
6
+ kwds = {}
7
+ input = input_block.nil? ? nil : input_block.call
8
+
9
+ case subs.length
10
+ when 0
11
+ # nothing to do
12
+ when 1
13
+ # can either be a hash, which is interpreted as a keywords,
14
+ # or an array, which is interpreted as positional arguments
15
+ case subs[0]
16
+ when Hash
17
+ kwds = subs[0]
18
+
19
+ when Array
20
+ args = subs[0]
21
+
22
+ else
23
+ raise TypeError.new NRSER.squish <<-BLOCK
24
+ first *subs arg must be Array or Hash, not #{ subs[0].inspect }
25
+ BLOCK
26
+ end
27
+
28
+ when 2
29
+ # first arg needs to be an array, second a hash
30
+ unless subs[0].is_a? Array
31
+ raise TypeError.new NRSER.squish <<-BLOCK
32
+ first *subs arg needs to be an array, not #{ subs[0].inspect }
33
+ BLOCK
34
+ end
35
+
36
+ unless subs[1].is_a? Hash
37
+ raise TypeError.new NRSER.squish <<-BLOCK
38
+ second *subs arg needs to be a Hash, not #{ subs[1].inspect }
39
+ BLOCK
40
+ end
41
+
42
+ args, kwds = subs
43
+ else
44
+ raise ArgumentError.new NRSER.squish <<-BLOCK
45
+ must provide one or two *subs arguments, received #{ 1 + subs.length }
46
+ BLOCK
47
+ end
48
+
49
+ return {
50
+ args: args,
51
+ kwds: kwds,
52
+ input: input,
53
+ }
54
+ end # .normalize
55
+ end # Params
56
+ end # Cmds
@@ -0,0 +1,118 @@
1
+ require 'json'
2
+ require 'nrser/refinements'
3
+
4
+ using NRSER
5
+
6
+ require_relative "defaults"
7
+
8
+ module Cmds
9
+ # turn an option name and value into an array of shell-escaped string
10
+ # token suitable for use in a command.
11
+ #
12
+ # @param [String] name
13
+ # string name (one or more characters).
14
+ #
15
+ # @param [*] value
16
+ # value of the option.
17
+ #
18
+ # @param [Hash] opts
19
+ # @option [Symbol] :array_mode (:multiple)
20
+ # one of:
21
+ #
22
+ # 1. `:multiple` (default) provide one token for each value.
23
+ #
24
+ # expand_option 'blah', [1, 2, 3]
25
+ # => ['--blah=1', '--blah=2', '--blah=3']
26
+ #
27
+ # 2. `:join` -- join values in one token.
28
+ #
29
+ # expand_option 'blah', [1, 2, 3], array_mode: :join
30
+ # => ['--blah=1,2,3']
31
+ #
32
+ # @option [String] :array_join_string (',')
33
+ # string to join array values with when `:array_mode` is `:join`.
34
+ #
35
+ # @return [Array<String>]
36
+ # string tokens.
37
+ #
38
+ def self.tokenize_option name, value, opts = {}
39
+ opts = defaults opts, [:array_mode, :array_join_string, :false_mode]
40
+
41
+ unless name.is_a?(String) && name.length > 0
42
+ raise ArgumentError.new <<-END.squish
43
+ `name` must be a String of length greater than zero,
44
+ found #{ name.inspect }
45
+ END
46
+ end
47
+
48
+ prefix, separator = if name.length == 1
49
+ # -b <value> style
50
+ ['-', ' ']
51
+ else
52
+ # --blah=<value> style
53
+ ['--', '=']
54
+ end
55
+
56
+ case value
57
+ when nil
58
+ []
59
+
60
+ when Array
61
+ # the PITA one
62
+ case opts[:array_mode]
63
+ when :repeat
64
+ # `-b 1 -b 2 -b 3` / `--blah=1 --blah=2 --blah=3` style
65
+ value.flatten.map {|v|
66
+ prefix + esc(name) + separator + esc(v)
67
+ }
68
+
69
+ when :join
70
+ # `-b 1,2,3` / `--blah=1,2,3` style
71
+ [ prefix +
72
+ esc(name) +
73
+ separator +
74
+ esc(value.join opts[:array_join_string]) ]
75
+
76
+ when :json
77
+ [prefix + esc(name) + separator + esc(JSON.dump value)]
78
+
79
+ else
80
+ # SOL
81
+ raise ArgumentError.new <<-END.squish
82
+ bad array_mode option: #{ opts[:array_mode] },
83
+ should be :repeat, :join or :json
84
+ END
85
+
86
+ end
87
+
88
+ when true
89
+ # `-b` or `--blah`
90
+ [prefix + esc(name)]
91
+
92
+ when false
93
+ case opts[:false_mode]
94
+ when :omit
95
+ # don't emit any token for a false boolean
96
+ []
97
+ when :no
98
+ # `--no-blah` style
99
+ #
100
+ # but there's not really a great way to handle short names...
101
+ # we use `--no-b`
102
+ #
103
+ ["--no-#{ esc(name) }"]
104
+
105
+ else
106
+ raise ArgumentError.new <<-END.squish
107
+ bad :false_mode option: #{ opts[:false_mode] },
108
+ should be :omit or :no
109
+ END
110
+ end
111
+
112
+ else
113
+ # we let .esc handle it
114
+ [prefix + esc(name) + separator + esc(value)]
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'tokenize_option'
2
+
3
+ module Cmds
4
+ # escape option hash.
5
+ #
6
+ # this is only useful for the two common option styles:
7
+ #
8
+ # - single character keys become `-<char> <value>`
9
+ #
10
+ # {x: 1} => "-x 1"
11
+ #
12
+ # - longer keys become `--<key>=<value>` options
13
+ #
14
+ # {blah: 2} => "--blah=2"
15
+ #
16
+ # if you have something else, you're going to have to just put it in
17
+ # the cmd itself, like:
18
+ #
19
+ # Cmds "blah -assholeOptionOn:%{s}", "ok"
20
+ #
21
+ # or whatever similar shit said command requires.
22
+ #
23
+ # however, if the value is an Array, it will repeat the option for each
24
+ # value:
25
+ #
26
+ # {x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
27
+ # {blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
28
+ #
29
+ # i can't think of any right now, but i swear i've seen commands that take
30
+ # opts that way.
31
+ #
32
+ def self.tokenize_options hash, opts = {}
33
+ opts = defaults opts, [:array_mode, :array_join_string, :false_mode]
34
+
35
+ hash.map {|key, value|
36
+ # keys need to be strings
37
+ key = key.to_s unless key.is_a? String
38
+
39
+ [key, value]
40
+
41
+ }.sort {|(key_a, value_a), (key_b, value_b)|
42
+ # sort by the (now string) keys
43
+ key_a <=> key_b
44
+
45
+ }.map {|key, value|
46
+ tokenize_option key, value
47
+
48
+ }.flatten.join ' '
49
+ end # .tokenize_options
50
+ end
data/lib/cmds/util.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  # util functions
2
- class Cmds
2
+
3
+ # stdlib
4
+ require 'shellwords'
5
+
6
+ require_relative 'util/tokenize_options'
7
+
8
+ module Cmds
3
9
  # class methods
4
10
  # =============
5
11
 
@@ -8,73 +14,7 @@ class Cmds
8
14
  # also makes it easier to change or customize or whatever
9
15
  def self.esc str
10
16
  Shellwords.escape str
11
- end
12
-
13
- # escape option hash.
14
- #
15
- # this is only useful for the two common option styles:
16
- #
17
- # - single character keys become `-<char> <value>`
18
- #
19
- # {x: 1} => "-x 1"
20
- #
21
- # - longer keys become `--<key>=<value>` options
22
- #
23
- # {blah: 2} => "--blah=2"
24
- #
25
- # if you have something else, you're going to have to just put it in
26
- # the cmd itself, like:
27
- #
28
- # Cmds "blah -assholeOptionOn:%{s}", "ok"
29
- #
30
- # or whatever similar shit said command requires.
31
- #
32
- # however, if the value is an Array, it will repeat the option for each
33
- # value:
34
- #
35
- # {x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
36
- # {blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
37
- #
38
- # i can't think of any right now, but i swear i've seen commands that take
39
- # opts that way.
40
- #
41
- def self.expand_option_hash hash
42
- hash.map {|key, values|
43
- # keys need to be strings
44
- key = key.to_s unless key.is_a? String
45
-
46
- [key, values]
47
-
48
- }.sort {|(key_a, values_a), (key_b, values_b)|
49
- # sort by the (now string) keys
50
- key_a <=> key_b
51
-
52
- }.map {|key, values|
53
- # for simplicity's sake, treat all values like an array
54
- values = [values] unless values.is_a? Array
55
-
56
- # keys of length 1 expand to `-x v` form
57
- expanded = if key.length == 1
58
- values.map {|value|
59
- if value.nil?
60
- "-#{ esc key }"
61
- else
62
- "-#{ esc key } #{ esc value}"
63
- end
64
- }
65
-
66
- # longer keys expand to `--key=value` form
67
- else
68
- values.map {|value|
69
- if value.nil?
70
- "--#{ esc key }"
71
- else
72
- "--#{ esc key }=#{ esc value }"
73
- end
74
- }
75
- end
76
- }.flatten.join ' '
77
- end # ::expand_option_hash
17
+ end
78
18
 
79
19
  # expand one of the substitutions
80
20
  def self.expand_sub sub
@@ -83,108 +23,37 @@ class Cmds
83
23
  # nil is just an empty string, NOT an empty string bash token
84
24
  ''
85
25
  when Hash
86
- expand_option_hash sub
26
+ tokenize_options sub
87
27
  else
88
28
  esc sub.to_s
89
29
  end
90
30
  end # ::expand_sub
91
-
92
- # substitute values into a command template, escaping them for the shell and
93
- # offering convenient expansions for some structures. uses ERB templating,
94
- # so logic is supported as well.
95
- #
96
- # check out the {file:README.md#substitutions README} for details on use.
97
- #
98
- # @param template [String] command template with token to be replaced /
99
- # expanded / escaped.
100
- #
101
- # @param args [Array] positional substitutions for occurances of
102
- # - `<%= arg %>`
103
- # - `%s`
104
- #
105
- # tokens in the `template` parameter.
106
- #
107
- # @param kwds [Hash] keyword subsitutions for occurances of the form
108
- #
109
- # - `<%= key %>`
110
- # - `%{key}`
111
- # - `%<key>s`
112
- #
113
- # as well as optional
114
- #
115
- # - `<%= key? %>`
116
- # - `%{key?}`
117
- # - `%<key?>s`
118
- #
119
- # tokens in the `template` parameter (where `key` is replaced with the
120
- # symbol name in the hash).
121
- #
122
- # @return [String] formated command string suitable for execution.
123
- #
124
- # @raise [TypeError] if `args` is not an {Array}.
125
- # @raise [TypeError] if `kwds` is not a {Hash}.
126
- #
127
- def self.sub template, args = [], kwds = {}
128
- raise TypeError.new("args must be an Array") unless args.is_a? Array
129
- raise TypeError.new("kwds must be an Hash") unless kwds.is_a? Hash
130
-
131
- context = ERBContext.new(args, kwds)
132
- erb = ShellEruby.new replace_shortcuts(template)
133
-
134
- NRSER.squish erb.result(context.get_binding)
135
- end # ::sub
136
-
137
- def self.options subs, input_block
138
- args = []
139
- kwds = {}
140
- input = input_block.nil? ? nil : input_block.call
141
-
142
- case subs.length
143
- when 0
144
- # nothing to do
145
- when 1
146
- # can either be a hash, which is interpreted as a keywords,
147
- # or an array, which is interpreted as positional arguments
148
- case subs[0]
149
- when Hash
150
- kwds = subs[0]
151
-
152
- when Array
153
- args = subs[0]
154
-
155
- else
156
- raise TypeError.new NRSER.squish <<-BLOCK
157
- first *subs arg must be Array or Hash, not #{ subs[0].inspect }
158
- BLOCK
159
- end
160
-
161
- when 2
162
- # first arg needs to be an array, second a hash
163
- unless subs[0].is_a? Array
164
- raise TypeError.new NRSER.squish <<-BLOCK
165
- first *subs arg needs to be an array, not #{ subs[0].inspect }
166
- BLOCK
167
- end
168
-
169
- unless subs[1].is_a? Hash
170
- raise TypeError.new NRSER.squish <<-BLOCK
171
- second *subs arg needs to be a Hash, not #{ subs[1].inspect }
172
- BLOCK
173
- end
174
-
175
- args, kwds = subs
31
+
32
+ # formats a command string
33
+ def self.format string, with = :squish
34
+ case with
35
+ when :squish
36
+ NRSER.squish string
37
+
38
+ when :pretty
39
+ pretty_format string
40
+
176
41
  else
177
- raise ArgumentError.new NRSER.squish <<-BLOCK
178
- must provide one or two *subs arguments, received #{ 1 + subs.length }
179
- BLOCK
42
+ with.call string
180
43
  end
181
-
182
- return {
183
- args: args,
184
- kwds: kwds,
185
- input: input,
186
- }
187
- end # ::options
44
+ end
45
+
46
+ def self.pretty_format string
47
+ string.lines.map {|line|
48
+ line = line.rstrip
49
+
50
+ if line.end_with? '\\'
51
+ line
52
+ else
53
+ line + ' \\'
54
+ end
55
+ }.join("\n")
56
+ end
188
57
 
189
58
  def self.replace_shortcuts template
190
59
  template
@@ -219,34 +88,5 @@ class Cmds
219
88
  '\1<\2>s'
220
89
  )
221
90
  end # ::replace_shortcuts
222
-
223
- # instance methods
224
- # ================
225
-
226
- # returns a new `Cmds` with the subs and input block merged in
227
- def curry *subs, &input_block
228
- self.class.new @template, merge_options(subs, input_block)
229
- end
230
-
231
- private
232
-
233
- # merges options already present on the object with options
234
- # provided via subs and input_block and returns a new options
235
- # Hash
236
- def merge_options subs, input_block
237
- # get the options present in the arguments
238
- options = Cmds.options subs, input_block
239
- # the new args are created by appending the provided args to the
240
- # existing ones
241
- options[:args] = @args + options[:args]
242
- # the new kwds are created by merging the provided kwds into the
243
- # exising ones (new values override previous)
244
- options[:kwds] = @kwds.merge options[:kwds]
245
- # if there is input present via the provided block, it is used.
246
- # otherwise, previous input is used, which may be `nil`
247
- options[:input] ||= @input
248
- return options
249
- end # #merge_options
250
-
251
- # end private
252
- end # class Cmds
91
+
92
+ end # module Cmds
data/lib/cmds/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- class Cmds
2
- VERSION = "0.0.9"
1
+ module Cmds
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/cmds.rb CHANGED
@@ -1,36 +1,3 @@
1
- # stdlib
2
- require 'shellwords'
3
- require 'open3'
4
- require 'thread'
5
-
6
- # deps
7
- require 'nrser'
8
-
9
- # project
10
- require "cmds/capture"
11
- require "cmds/debug"
12
- require "cmds/erb_context"
13
- require "cmds/io_handler"
14
- require "cmds/pipe"
15
- require "cmds/result"
16
- require "cmds/shell_eruby"
17
- require "cmds/stream"
18
- require "cmds/sugar"
19
- require "cmds/util"
20
- require "cmds/version"
21
-
22
- class Cmds
23
- attr_reader :template, :args, :kwds, :input, :assert
24
-
25
- def initialize template, opts = {}
26
- Cmds.debug "Cmds constructed",
27
- template: template,
28
- options: opts
29
-
30
- @template = template
31
- @args = opts[:args] || []
32
- @kwds = opts[:kwds] || {}
33
- @input = opts[:input] || nil
34
- @assert = opts[:assert] || false
35
- end # #initialize
36
- end
1
+ require 'cmds/version'
2
+ require 'cmds/cmd'
3
+ require 'cmds/sugar'
data/scratch/proxy.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'cmds'
2
2
 
3
3
  Cmds.enable_debug do
4
- Cmds.new("./test/questions.rb").proxy
4
+ Cmds::Cmd.new("./test/questions.rb").proxy
5
5
  end
@@ -11,6 +11,6 @@ describe "Cmds::assert" do
11
11
 
12
12
  it "should be chainable when the command is ok" do
13
13
  expect( Cmds!("echo hey").out ).to eq "hey\n"
14
- expect( Cmds.new("echo hey").capture.assert.out ).to eq "hey\n"
14
+ expect( Cmds::Cmd.new("echo hey").capture.assert.out ).to eq "hey\n"
15
15
  end
16
16
  end # Cmds::run
@@ -3,13 +3,13 @@ require 'spec_helper'
3
3
  describe "Cmds::capture" do
4
4
  it "captures stdout" do
5
5
  expect(
6
- Cmds.new(%{ruby -e '$stdout.puts "hey"'}).capture.out
6
+ Cmds::Cmd.new(%{ruby -e '$stdout.puts "hey"'}).capture.out
7
7
  ).to eq "hey\n"
8
8
  end
9
9
 
10
10
  it "captures stderr" do
11
11
  expect(
12
- Cmds.new(%{ruby -e '$stderr.puts "ho"'}).capture.err
12
+ Cmds::Cmd.new(%{ruby -e '$stderr.puts "ho"'}).capture.err
13
13
  ).to eq "ho\n"
14
14
  end
15
15
 
@@ -25,7 +25,7 @@ describe "Cmds::capture" do
25
25
 
26
26
  context "positional args" do
27
27
  let(:result) {
28
- Cmds "./test/echo_cmd.rb <%= arg %>", ["hello world!"]
28
+ Cmds "./test/echo_cmd.rb <%= arg %>", "hello world!"
29
29
  }
30
30
 
31
31
  it_behaves_like "executes correctly"
@@ -41,31 +41,15 @@ describe "Cmds::capture" do
41
41
 
42
42
  end # context echo_cmd.rb 'hello world!'
43
43
 
44
- # context "feeding kwargs to args cmd" do
45
- # let(:result) {
46
- # Cmds "./test/echo_cmd.rb %s", s: "sup y'all"
47
- # }
48
-
49
- # it "" do
50
- # expect( result.cmd ).to eq nil
51
- # end
52
- # end
53
-
54
- it "should error when second (subs) arg is not a hash or array" do
55
- expect {
56
- Cmds "./test/echo_cmd.rb <%= arg %>", "hello world!"
57
- }.to raise_error TypeError
58
- end
59
-
60
44
  it "is reusable" do
61
- args_cmd = Cmds.new "./test/echo_cmd.rb <%= arg %>"
62
- kwds_cmd = Cmds.new "./test/echo_cmd.rb <%= s %>"
45
+ args_cmd = Cmds::Cmd.new "./test/echo_cmd.rb <%= arg %>"
46
+ kwds_cmd = Cmds::Cmd.new "./test/echo_cmd.rb <%= s %>"
63
47
 
64
48
  args = ["arg one", "arg two", "arg three"]
65
49
 
66
50
  args.each do |arg|
67
51
  results = [
68
- args_cmd.capture([arg]),
52
+ args_cmd.capture(arg),
69
53
  kwds_cmd.capture(s: arg)
70
54
  ]
71
55
 
@@ -86,12 +70,12 @@ describe "Cmds::capture" do
86
70
  }
87
71
 
88
72
  it "accepts input via options" do
89
- cmd = Cmds.new(ECHO_CMD, input: input)
73
+ cmd = Cmds::Cmd.new(ECHO_CMD, input: input)
90
74
  expect( echo_cmd_stdin cmd.capture ).to eq input
91
75
  end
92
76
 
93
77
  it "accepts input via block" do
94
- cmd = Cmds.new ECHO_CMD
78
+ cmd = Cmds::Cmd.new ECHO_CMD
95
79
  expect( echo_cmd_stdin cmd.capture { input } ).to eq input
96
80
  end
97
81
 
@@ -100,7 +84,7 @@ describe "Cmds::capture" do
100
84
  input = f.read
101
85
  f.rewind
102
86
 
103
- cmd = Cmds.new ECHO_CMD
87
+ cmd = Cmds::Cmd.new ECHO_CMD
104
88
  expect( echo_cmd_stdin cmd.capture { f } ).to eq input
105
89
  end
106
90
  end