cmds 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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