cmds 0.2.4 → 0.2.5

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.
@@ -7,19 +7,13 @@ require 'shellwords'
7
7
  require 'nrser'
8
8
 
9
9
  # package
10
+ require 'cmds/util/shell_escape'
10
11
  require 'cmds/util/tokenize_options'
11
12
 
12
13
  class Cmds
13
14
  # class methods
14
15
  # =============
15
16
 
16
- # shortcut for Shellwords.escape
17
- #
18
- # also makes it easier to change or customize or whatever
19
- def self.esc str
20
- Shellwords.escape str
21
- end
22
-
23
17
  # tokenize values for the shell. each values is tokenized individually
24
18
  # and the results are joined with a space.
25
19
  #
@@ -32,15 +26,12 @@ class Cmds
32
26
  def self.tokenize *values, **opts
33
27
  values.map {|value|
34
28
  case value
35
- when nil
36
- # nil is just an empty string, NOT an empty string bash token
37
- ''
38
29
  when Hash
39
30
  tokenize_options value, **opts
40
31
  else
41
- esc value.to_s
32
+ tokenize_value value, **opts
42
33
  end
43
- }.join ' '
34
+ }.flatten.join ' '
44
35
  end # .tokenize
45
36
 
46
37
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Cmds
2
4
  # hash of common default values used in method options.
3
5
  #
@@ -13,39 +15,66 @@ class Cmds
13
15
  # you don't even know about may depend on said behavior.
14
16
  #
15
17
  DEFAULTS = {
18
+ # Alphabetical...
19
+
16
20
  # positional arguments for a command
17
21
  args: [],
18
22
 
19
- # keyword arguments for a command
20
- kwds: {},
21
-
22
- # how to format a command string for execution
23
- format: :squish,
23
+ # what to join array option values with when using `array_mode = :join`
24
+ array_join_string: ',',
24
25
 
25
26
  # what to do with array option values
26
27
  array_mode: :join,
27
28
 
28
- # what to join array option values with when using `array_mode = :join`
29
- array_join_string: ',',
29
+ # Don't asset (raise error if exit code is not 0)
30
+ assert: false,
30
31
 
31
- # what to do with false array values
32
- false_mode: :omit,
32
+ # Don't change directories
33
+ chdir: nil,
34
+
35
+ # No additional environment
36
+ env: {},
33
37
 
34
38
  # Stick ENV var defs inline at beginning of command
35
39
  env_mode: :inline,
36
40
 
37
- # No additional environment
38
- env: {},
41
+ # What to do with `false` *option* values (not `false` values as regular
42
+ # values or inside collections)
43
+ #
44
+ # Just leave them out all-together
45
+ false_mode: :omit,
39
46
 
40
- # Don't change directories
41
- chdir: nil,
47
+ # Flatten nested array values to a single array.
48
+ #
49
+ # Many CLI commands accept arrays in some form or another, but I'm hard
50
+ # pressed to think of one that accepts nested arrays. Flattening can make
51
+ # it simpler to generate values.
52
+ #
53
+ flatten_array_values: true,
42
54
 
43
- # Don't asset (raise error if exit code is not 0)
44
- assert: false,
55
+ # how to format a command string for execution
56
+ format: :squish,
57
+
58
+ hash_mode: :join,
59
+
60
+ # Join hash keys and values with `:`
61
+ hash_join_string: ':',
45
62
 
46
63
  # No input
47
64
  input: nil,
48
65
 
66
+ # keyword arguments for a command
67
+ kwds: {},
68
+
69
+ # What to use to separate "long" opt names (more than one character) from
70
+ # their values. I've commonly seen '=' (`--name=VALUE`)
71
+ # and ' ' (`--name VALUE`).
72
+ long_opt_separator: '=',
73
+
74
+ # What to use to separate "short" opt names (single character) from their
75
+ # values. I've commonly seen ' ' (`-x VALUE`) and '' (`-xVALUE`).
76
+ short_opt_separator: ' ',
77
+
49
78
  }.map { |k, v| [k, v.freeze] }.to_h.freeze
50
79
 
51
80
 
@@ -69,7 +98,7 @@ class Cmds
69
98
  def self.defaults opts, keys = '*', extras = {}
70
99
  if keys == '*'
71
100
  DEFAULTS.dup
72
- else
101
+ else
73
102
  keys.
74
103
  map {|key|
75
104
  [key, DEFAULTS.fetch(key)]
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Cmds
4
+ # @!group Shell Quoting and Escaping Methods
5
+
6
+ # Constants
7
+ # ======================================================================
8
+
9
+ # Quote "name" keys `:single` and `:double` mapped to their character.
10
+ #
11
+ # @return [Hash<Symbol, String>]
12
+ #
13
+ QUOTE_TYPES = {
14
+ single: %{'},
15
+ double: %{"},
16
+ }.freeze
17
+
18
+
19
+ # List containing just `'` and `"`.
20
+ #
21
+ # @return [Array<String>]
22
+ #
23
+ QUOTE_VALUES = QUOTE_TYPES.values.freeze
24
+
25
+
26
+ # Class Methods
27
+ # ======================================================================
28
+
29
+ # Shortcut for Shellwords.escape
30
+ #
31
+ # Also makes it easier to change or customize or whatever.
32
+ #
33
+ # @see http://ruby-doc.org/stdlib/libdoc/shellwords/rdoc/Shellwords.html#method-c-escape
34
+ #
35
+ # @param [#to_s] str
36
+ # @return [String]
37
+ #
38
+ def self.esc str
39
+ Shellwords.escape str
40
+ end
41
+
42
+
43
+ # Format a string to be a shell token by wrapping it in either single or
44
+ # double quotes and replacing instances of that quote with what I'm calling
45
+ # a "quote dance":
46
+ #
47
+ # 1. Closing the type of quote in use
48
+ # 2. Quoting the type of quote in use with the *other* type of quote
49
+ # 3. Then opening up the type in use again and keeping going.
50
+ #
51
+ # @example Single quoting string containing single quotes
52
+ #
53
+ # Cmds.quote_dance %{you're}, :single
54
+ # # => %{'you'"'"'re'}
55
+ #
56
+ # @example Double quoting string containing double quotes
57
+ #
58
+ # Cmds.quote_dance %{hey "ho" let's go}, :double
59
+ # # => %{"hey "'"'"ho"'"'" let's go"}
60
+ #
61
+ # **_WARNING:
62
+ # Does NOT escape anything except the quotes! So if you double-quote a
63
+ # string with shell-expansion terms in it and pass it to the shell
64
+ # THEY WILL BE EVALUATED_**
65
+ #
66
+ # @param [String] string
67
+ # String to quote.
68
+ #
69
+ # @return [return_type]
70
+ # @todo Document return value.
71
+ #
72
+ def self.quote_dance string, quote_type
73
+ outside = QUOTE_TYPES.fetch quote_type
74
+ inside = QUOTE_VALUES[QUOTE_VALUES[0] == outside ? 1 : 0]
75
+
76
+ outside +
77
+ string.gsub(
78
+ outside,
79
+ outside + inside + outside + inside + outside
80
+ ) +
81
+ outside
82
+ end # .quote_dance
83
+
84
+
85
+ # Single quote a string for use in the shell.
86
+ #
87
+ # @param [String] string
88
+ # String to quote.
89
+ #
90
+ # @return [String]
91
+ # Single-quoted string.
92
+ #
93
+ def self.single_quote string
94
+ quote_dance string, :single
95
+ end # .single_quote
96
+
97
+ # Set {.single_quote} up as our quote method.
98
+ singleton_class.send :alias_method, :quote, :single_quote
99
+
100
+
101
+ end # class Cmds
@@ -1,43 +1,50 @@
1
1
  require 'json'
2
- require 'nrser/refinements'
2
+ require 'nrser'
3
3
 
4
4
  require_relative "defaults"
5
+ require_relative "tokenize_value"
5
6
 
6
7
  class Cmds
7
- TOKENIZE_OPT_KEYS = [:array_mode, :array_join_string, :false_mode]
8
8
 
9
- # turn an option name and value into an array of shell-escaped string
10
- # token suitable for use in a command.
9
+ # Turn an option name and value into an array of shell-escaped string
10
+ # tokens suitable for use in a command.
11
11
  #
12
12
  # @param [String] name
13
- # string name (one or more characters).
13
+ # String name (one or more characters).
14
14
  #
15
15
  # @param [*] value
16
- # value of the option.
16
+ # Value of the option.
17
17
  #
18
18
  # @param [Hash] **opts
19
- # @option [Symbol] :array_mode (:multiple)
19
+ #
20
+ # @option [Symbol] :array_mode (:join)
20
21
  # one of:
22
+ #
23
+ # 1. `:join` (default) -- join values in one token.
24
+ #
25
+ # tokenize_option 'blah', [1, 2, 3], array_mode: :join
26
+ # => ['--blah=1,2,3']
21
27
  #
22
- # 1. `:multiple` (default) provide one token for each value.
28
+ # 2. `:repeat` repeat the option for each value.
23
29
  #
24
- # expand_option 'blah', [1, 2, 3]
30
+ # tokenize_option 'blah', [1, 2, 3], array_mode: :repeat
25
31
  # => ['--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
  #
32
33
  # @option [String] :array_join_string (',')
33
- # string to join array values with when `:array_mode` is `:join`.
34
+ # String to join array values with when `:array_mode` is `:join`.
34
35
  #
35
36
  # @return [Array<String>]
36
- # string tokens.
37
+ # List of individual shell token strings, escaped for use.
38
+ #
39
+ # @raise [ArgumentError]
40
+ # 1. If `name` is the wrong type or empty.
41
+ # 2. If any options have bad values.
37
42
  #
38
43
  def self.tokenize_option name, value, **opts
44
+ # Set defaults for any options not passed
39
45
  opts = defaults opts, TOKENIZE_OPT_KEYS
40
46
 
47
+ # Validate `name`
41
48
  unless name.is_a?(String) && name.length > 0
42
49
  raise ArgumentError.new NRSER.squish <<-END
43
50
  `name` must be a String of length greater than zero,
@@ -45,74 +52,138 @@ class Cmds
45
52
  END
46
53
  end
47
54
 
48
- prefix, separator = if name.length == 1
49
- # -b <value> style
50
- ['-', ' ']
55
+ # Set type (`:short` or `:long`) prefix and name/value separator depending
56
+ # on if name is "short" (single character) or "long" (anything else)
57
+ #
58
+ type, prefix, separator = if name.length == 1
59
+ # -b <value> style (short)
60
+ [ :short, '-', opts[:short_opt_separator] ]
51
61
  else
52
- # --blah=<value> style
53
- ['--', '=']
62
+ # --blah=<value> style (long)
63
+ [ :long, '--', opts[:long_opt_separator] ]
54
64
  end
55
-
65
+
56
66
  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 + "'" + JSON.dump(value).gsub(%{'}, %{'"'"'}) + "'"]
78
-
79
- else
80
- # SOL
81
- raise ArgumentError.new NRSER.squish <<-END
82
- bad array_mode option: #{ opts[:array_mode] },
83
- should be :repeat, :join or :json
84
- END
85
-
86
- end
87
-
67
+
68
+ # Special cases (booleans), where we may want to emit an option name but
69
+ # no value (depending on options)
70
+ #
88
71
  when true
89
- # `-b` or `--blah`
72
+ # `-b` or `--blah` style token
90
73
  [prefix + esc(name)]
91
74
 
92
75
  when false
93
76
  case opts[:false_mode]
94
- when :omit
95
- # don't emit any token for a false boolean
77
+ when :omit, :ignore
78
+ # Don't emit any token for a false boolean
96
79
  []
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`
80
+
81
+ when :negate, :no
82
+ # Emit `--no-blah` style token
102
83
  #
103
- ["--no-#{ esc(name) }"]
84
+ if type == :long
85
+ # Easy one
86
+ ["--no-#{ esc(name) }"]
104
87
 
88
+ else
89
+ # Short option... there seems to be little general consensus on how
90
+ # to handle these guys; I feel like the most common is to invert the
91
+ # case, which only makes sense for languages that have lower and
92
+ # upper case :/
93
+ case opts[:false_short_opt_mode]
94
+
95
+ when :capitalize, :cap, :upper, :upcase
96
+ # Capitalize the name
97
+ #
98
+ # {x: false} => ["-X"]
99
+ #
100
+ # This only really makes sense for lower case a-z, so raise if it's
101
+ # not in there
102
+ unless "a" <= name <= "z"
103
+ raise ArgumentError.new binding.erb <<-END
104
+ Can't negate CLI option `<%= name %>` by capitalizing name.
105
+
106
+ Trying to tokenize option `<%= name %>` with `false` value and:
107
+
108
+ 1. `:false_mode` is set to `<%= opts[:false_mode] %>`, which
109
+ tells {Cmds.tokenize_option} to emit a "negating" name with
110
+ no value like
111
+
112
+ {update: false} => --no-update
113
+
114
+ 2. `:false_short_opt_mode` is set to `<%= opts[:false_short_opt_mode] %>`,
115
+ which means negate through capitalizing the name character,
116
+ like:
117
+
118
+ {u: false} => -U
119
+
120
+ 3. But this is only implemented for names in `a-z`
121
+
122
+ Either change the {Cmds} instance configuration or provide a
123
+ different CLI option name or value.
124
+ END
125
+ end
126
+
127
+ # Emit {x: false} => ['-X'] style
128
+ ["-#{ name.upcase }"]
129
+
130
+ when :long
131
+ # Treat it the same as a long option,
132
+ # emit {x: false} => ['--no-x'] style
133
+ #
134
+ # Yeah, I've never seen it anywhere else, but it seems reasonable I
135
+ # guess..?
136
+ #
137
+ ["--no-#{ esc(name) }"]
138
+
139
+ when :string
140
+ # Use the string 'false' as a value
141
+ [prefix + esc( name ) + separator + 'false']
142
+
143
+ when String
144
+ # It's some custom string to use
145
+ [prefix + esc( name ) + separator + esc( string )]
146
+
147
+ else
148
+ raise ArgumentError.new binding.erb <<-END
149
+ Bad `:false_short_opt_mode` value:
150
+
151
+ <%= opts[:false_short_opt_mode].pretty_inspect %>
152
+
153
+ Should be
154
+
155
+ 1. :capitalize (or :cap, :upper, :upcase)
156
+ 2. :long
157
+ 3. :string
158
+ 4. any String
159
+
160
+ END
161
+
162
+ end # case opts[:false_short_opt_mode]
163
+ end # if :long else
105
164
  else
106
165
  raise ArgumentError.new NRSER.squish <<-END
107
- bad :false_mode option: #{ opts[:false_mode] },
166
+ bad :false_mode option: #{ opts[:false_mode] },
108
167
  should be :omit or :no
109
168
  END
110
169
  end
111
-
170
+
171
+ # General case
112
172
  else
113
- # we let .esc handle it
114
- [prefix + esc(name) + separator + esc(value)]
115
-
116
- end
117
- end
118
- end
173
+ # Tokenize the value, which may
174
+ #
175
+ # 1. Result in more than one token, like when `:array_mode` is `:repeat`
176
+ # (in which case we want to emit multiple option tokens)
177
+ #
178
+ # 2. Result in zero tokens, like when `value` is `nil`
179
+ # (in which case we want to emit no option tokens)
180
+ #
181
+ # and map the resulting tokens into option tokens
182
+ #
183
+ tokenize_value( value, **opts ).map { |token|
184
+ prefix + esc(name) + separator + token
185
+ }
186
+
187
+ end # case value
188
+ end # .tokenize_option
189
+ end # class Cmds