vv 0.0.8 → 0.0.9
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.
- checksums.yaml +4 -4
- data/lib/vv/array_methods.rb +38 -0
- data/lib/vv/cli.rb +304 -0
- data/lib/vv/file_methods.rb +132 -0
- data/lib/vv/hash_methods.rb +36 -0
- data/lib/vv/integer_methods.rb +25 -0
- data/lib/vv/object_methods.rb +39 -0
- data/lib/vv/string_methods.rb +133 -7
- data/lib/vv/symbol_methods.rb +31 -0
- data/lib/vv/utility/automate.rb +80 -2
- data/lib/vv/utility/lookup_table.rb +87 -0
- data/lib/vv/version.rb +1 -1
- data/lib/vv.rb +8 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 052ef19db592bebd51a14310cc4eea838dbd10645f43c05f15cb774292222990
|
4
|
+
data.tar.gz: 2e23028b168d6cb104a0b631c9b80bcb11696120854fbd603126bc962aa380f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a62626e31c21dbe0c5fd426681c2b0007cb4f0a0e844e77a0f1bf5faa9c49edcc7b2da9a4d2e6650220982f95708d3563ffa68090e2371233f6b66f4db780aa9
|
7
|
+
data.tar.gz: 998917dbf37b9900ef500517674d42fad1c64cacbd4aab294a13c29c5c795cbda0926cd0efd0106969287a58fa3fe0f5398e2980e83e39e7786120d54e871f9d
|
data/lib/vv/array_methods.rb
CHANGED
@@ -3,6 +3,7 @@ module VV
|
|
3
3
|
|
4
4
|
def self.included(base)
|
5
5
|
base.extend(ClassMethods)
|
6
|
+
base.attr_accessor :cli_print_separator
|
6
7
|
end
|
7
8
|
|
8
9
|
module ClassMethods
|
@@ -93,6 +94,43 @@ module VV
|
|
93
94
|
self[9]
|
94
95
|
end
|
95
96
|
|
97
|
+
def cli_print width: 80,
|
98
|
+
padding: 0,
|
99
|
+
position: 0,
|
100
|
+
separator: nil
|
101
|
+
|
102
|
+
@cli_print_separator ||= String.space
|
103
|
+
separator ||= @cli_print_separator
|
104
|
+
|
105
|
+
pad_length = padding - position
|
106
|
+
position += pad_length
|
107
|
+
print pad_length.spaces
|
108
|
+
|
109
|
+
separator_required = false
|
110
|
+
self.each do | elem |
|
111
|
+
printable = String.capture_stdout {
|
112
|
+
elem.cli_print width: width,
|
113
|
+
padding: padding,
|
114
|
+
position: position
|
115
|
+
}
|
116
|
+
string = printable.dup
|
117
|
+
string.prepend separator if separator_required
|
118
|
+
delta = string.unstyled.length
|
119
|
+
|
120
|
+
if position + delta > width
|
121
|
+
puts
|
122
|
+
print padding.spaces
|
123
|
+
print printable
|
124
|
+
position = padding + printable.unstyled.length
|
125
|
+
else
|
126
|
+
print string
|
127
|
+
position += delta
|
128
|
+
end
|
129
|
+
separator_required = true
|
130
|
+
end
|
131
|
+
position
|
132
|
+
end
|
133
|
+
|
96
134
|
end
|
97
135
|
|
98
136
|
end
|
data/lib/vv/cli.rb
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
module VV
|
2
|
+
|
3
|
+
class CLI
|
4
|
+
|
5
|
+
attr_reader :option_router,
|
6
|
+
:settings,
|
7
|
+
:cache_path,
|
8
|
+
:config_path,
|
9
|
+
:data_path
|
10
|
+
|
11
|
+
def initialize version: nil,
|
12
|
+
name: nil,
|
13
|
+
config_path: nil,
|
14
|
+
cache_path: nil,
|
15
|
+
data_path: nil
|
16
|
+
|
17
|
+
default_version = "0.0.1"
|
18
|
+
@version = version || default_version
|
19
|
+
|
20
|
+
@config_path = config_path
|
21
|
+
@cache_path = cache_path
|
22
|
+
@data_path = data_path
|
23
|
+
|
24
|
+
@option_router = OptionRouter.new( name: name ) do |router|
|
25
|
+
yield router if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
@settings = nil
|
29
|
+
|
30
|
+
self.set_default_paths
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_default_paths
|
34
|
+
@config_path ||= File.join File.config_home, name_version
|
35
|
+
@cache_path ||= File.join File.cache_home, name_version
|
36
|
+
@data_path ||= File.join File.data_home, name_version
|
37
|
+
end
|
38
|
+
|
39
|
+
def name
|
40
|
+
@option_router.name
|
41
|
+
end
|
42
|
+
|
43
|
+
def name_version
|
44
|
+
[ self.name.unstyled, @version ].join("-")
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_flags argv
|
48
|
+
argv = argv.split " " if argv.is_a? String
|
49
|
+
@settings = @option_router.parse argv
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class OptionRouter
|
55
|
+
|
56
|
+
attr_reader :flag_settings, :name
|
57
|
+
|
58
|
+
attr_accessor :version, :help, :testing
|
59
|
+
|
60
|
+
def initialize name: nil
|
61
|
+
@flag_settings = LookupTable.new
|
62
|
+
@commands = Hash.new
|
63
|
+
@current_flag = nil
|
64
|
+
|
65
|
+
@name = name
|
66
|
+
@name ||= "check".style :lightblue, :italic
|
67
|
+
|
68
|
+
self.set_reserved_flags
|
69
|
+
self.set_reserved_commands
|
70
|
+
|
71
|
+
yield self if block_given?
|
72
|
+
end
|
73
|
+
|
74
|
+
def register flags, type: :string
|
75
|
+
flags = [ flags.to_s ] unless flags.is_a? Array
|
76
|
+
type.one_of! :string,
|
77
|
+
:integer,
|
78
|
+
:decimal,
|
79
|
+
:float,
|
80
|
+
:boolean,
|
81
|
+
:trilean,
|
82
|
+
:ternary,
|
83
|
+
:reserved
|
84
|
+
|
85
|
+
help = block_given? ? yield.squish : nil
|
86
|
+
|
87
|
+
first_flag = flags.first
|
88
|
+
|
89
|
+
flags.each do |flag|
|
90
|
+
if @flag_settings[flag].blank?
|
91
|
+
next if flag == first_flag
|
92
|
+
@flag_settings.alias key: flag, to: first_flag
|
93
|
+
next
|
94
|
+
end
|
95
|
+
|
96
|
+
set_type = @flag_settings[flag][:type]
|
97
|
+
type_ok = set_type != :reserved
|
98
|
+
|
99
|
+
message = "Duplicate flag `#{flag}` cannot be set."
|
100
|
+
fail message if type_ok
|
101
|
+
fail "Reserved flag `#{flag}` cannot be set."
|
102
|
+
end
|
103
|
+
|
104
|
+
settings = { type: type }
|
105
|
+
settings[:help] = help unless help.blank?
|
106
|
+
|
107
|
+
@flag_settings[first_flag] = settings
|
108
|
+
end
|
109
|
+
|
110
|
+
def set_new_flag
|
111
|
+
message = \
|
112
|
+
"Duplicate command line flag #{@flag} encountered."
|
113
|
+
|
114
|
+
@flag = lookup_canonical_flag @flag
|
115
|
+
fail message if @response.include? @flag
|
116
|
+
|
117
|
+
self.set_flag
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_flag
|
121
|
+
@flag = lookup_canonical_flag @flag
|
122
|
+
|
123
|
+
@current_flag = @flag if @value.nil?
|
124
|
+
|
125
|
+
self.set_value
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_value
|
129
|
+
value = @value
|
130
|
+
value &&= @value.to_d if decimal?
|
131
|
+
value ||= true
|
132
|
+
@response[@flag] = value
|
133
|
+
end
|
134
|
+
|
135
|
+
# This needs a refactor.
|
136
|
+
def parse argv
|
137
|
+
@flags_cease = false
|
138
|
+
@response = {}
|
139
|
+
|
140
|
+
argv.each do |arg|
|
141
|
+
next add_input_arg arg if @flags_cease
|
142
|
+
|
143
|
+
@flag, *@value = arg.split String.equals_sign
|
144
|
+
self.standardize_value
|
145
|
+
|
146
|
+
next @flags_cease = true if self.termination_flag?
|
147
|
+
next handle_command if @commands.include? @flag
|
148
|
+
next set_flag if @flag_settings.include? @flag
|
149
|
+
|
150
|
+
self.ensure_known_flag!
|
151
|
+
|
152
|
+
next handle_short_flags if self.short_flag?
|
153
|
+
next handle_current if @current_flag.present?
|
154
|
+
|
155
|
+
self.cease_flag_consideration
|
156
|
+
end
|
157
|
+
|
158
|
+
@response
|
159
|
+
end
|
160
|
+
|
161
|
+
def end_of_commands
|
162
|
+
"--"
|
163
|
+
end
|
164
|
+
|
165
|
+
def reserve flags
|
166
|
+
register flags, type: :reserved
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_flag flag, type: nil
|
170
|
+
raise NotImplementedError
|
171
|
+
end
|
172
|
+
|
173
|
+
def set_reserved_commands
|
174
|
+
set_command :help, alias_flag: "-h"
|
175
|
+
set_command :version, alias_flag: "-V"
|
176
|
+
end
|
177
|
+
|
178
|
+
def set_reserved_flags
|
179
|
+
[ %w[ -h -? --help ],
|
180
|
+
%w[ -V --version ],
|
181
|
+
%w[ -v --verbose ],
|
182
|
+
%w[ -vv --very-verbose ],
|
183
|
+
%w[ -vvv --very-very-verbose ],
|
184
|
+
%w[ -q --quiet ],
|
185
|
+
%w[ -s -qq --absolute-silence ],
|
186
|
+
%w[ -- ] ].each do |flags|
|
187
|
+
|
188
|
+
self.register flags, type: :reserved
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def set_command command, alias_flag: nil
|
193
|
+
command = command.to_s
|
194
|
+
if @commands.include? command
|
195
|
+
raise "Command #{command} already set."
|
196
|
+
end
|
197
|
+
|
198
|
+
@commands[command] = [ :alias_flag, alias_flag ]
|
199
|
+
end
|
200
|
+
|
201
|
+
def lookup_canonical_flag flag
|
202
|
+
@flag_settings.lookup_canonical flag
|
203
|
+
end
|
204
|
+
|
205
|
+
def help_doc
|
206
|
+
ending_flag = %w[ -- ]
|
207
|
+
keys = @flag_settings.canonical_keys - ending_flag
|
208
|
+
keys += ending_flag
|
209
|
+
|
210
|
+
cli_flags = keys.map do |key|
|
211
|
+
flags = [ key ] + @flag_settings.aliases[key].to_a
|
212
|
+
"[#{flags.join(" | ")}]"
|
213
|
+
end
|
214
|
+
|
215
|
+
{ "usage: #{@name}" => cli_flags }
|
216
|
+
end
|
217
|
+
|
218
|
+
def termination_flag?
|
219
|
+
@flag == "--"
|
220
|
+
end
|
221
|
+
|
222
|
+
def handle_command
|
223
|
+
command = @commands[@flag]
|
224
|
+
|
225
|
+
if command.first == :alias_flag
|
226
|
+
@flag = command.second
|
227
|
+
self.set_flag
|
228
|
+
else
|
229
|
+
raise NotImplementedError
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def handle_short_flags
|
234
|
+
short_flags = \
|
235
|
+
@flag.after(String.dash).split String.empty_string
|
236
|
+
|
237
|
+
duplicates = \
|
238
|
+
short_flags.count != short_flags.uniq.count
|
239
|
+
|
240
|
+
message = \
|
241
|
+
"Duplicate command line flags in #{@flag}."
|
242
|
+
fail message if duplicates
|
243
|
+
end
|
244
|
+
|
245
|
+
def handle_current
|
246
|
+
|
247
|
+
@value = @flag
|
248
|
+
@flag = @current_flag
|
249
|
+
|
250
|
+
collection = \
|
251
|
+
@flag_settings[@flag][:type] == :collection
|
252
|
+
if collection
|
253
|
+
@response[@flag] ||= []
|
254
|
+
@response[@flag] << @value
|
255
|
+
else
|
256
|
+
@current_flag = nil
|
257
|
+
self.set_flag
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
def add_input_arg arg
|
263
|
+
@response[:input_arguments] ||= []
|
264
|
+
@response[:input_arguments] << arg
|
265
|
+
end
|
266
|
+
|
267
|
+
def standardize_value
|
268
|
+
present = @value.present?
|
269
|
+
@value = @value.join( String.equals_sign ) if present
|
270
|
+
@value = nil unless @value.present?
|
271
|
+
end
|
272
|
+
|
273
|
+
def short_flag?
|
274
|
+
return false if long_flag?
|
275
|
+
@flag.starts_with? String.dash
|
276
|
+
end
|
277
|
+
|
278
|
+
def long_flag?
|
279
|
+
@flag.starts_with? 2.dashes
|
280
|
+
end
|
281
|
+
|
282
|
+
def ensure_known_flag!
|
283
|
+
message = "Unknown flag `#{@flag}` provided."
|
284
|
+
fail message if @value.present? or self.long_flag?
|
285
|
+
end
|
286
|
+
|
287
|
+
def cease_flag_consideration
|
288
|
+
@flags_cease = true
|
289
|
+
@current_flag = nil
|
290
|
+
@flag.concat String.equals_sign, @value if @value
|
291
|
+
add_input_arg @flag
|
292
|
+
@flag = @value = nil
|
293
|
+
end
|
294
|
+
|
295
|
+
def flag_type
|
296
|
+
@flag_settings[@flag][:type]
|
297
|
+
end
|
298
|
+
|
299
|
+
def decimal?
|
300
|
+
flag_type == :decimal
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
data/lib/vv/file_methods.rb
CHANGED
@@ -75,6 +75,138 @@ module VV
|
|
75
75
|
File::SEPARATOR
|
76
76
|
end
|
77
77
|
|
78
|
+
def copy_into filepath,
|
79
|
+
directory,
|
80
|
+
allow_hidden: true,
|
81
|
+
allow_absolute: true
|
82
|
+
|
83
|
+
message = "Filepath `#{filepath}` is unsafe."
|
84
|
+
fail message unless filepath.safe_path?
|
85
|
+
|
86
|
+
message = "Filepath `#{filepath}` is a directory."
|
87
|
+
fail message if filepath.is_directory_path?
|
88
|
+
|
89
|
+
message = "No such `#{directory}` directory."
|
90
|
+
fail message unless directory.is_directory_path?
|
91
|
+
|
92
|
+
FileUtils.cp filepath, directory
|
93
|
+
end
|
94
|
+
|
95
|
+
def rename_directory from,
|
96
|
+
to,
|
97
|
+
allow_hidden: true,
|
98
|
+
allow_absolute: true
|
99
|
+
safe = from.safe_dir_path? allow_hidden: allow_hidden,
|
100
|
+
allow_absolute: allow_absolute
|
101
|
+
|
102
|
+
safe &&= to.safe_dir_path? allow_hidden: allow_hidden,
|
103
|
+
allow_absolute: allow_absolute
|
104
|
+
|
105
|
+
message = "Refusing to rename unsafe directory"
|
106
|
+
fail message unless safe
|
107
|
+
|
108
|
+
message = "Source #{from} is not a directory"
|
109
|
+
fail message unless from.is_directory_path?
|
110
|
+
|
111
|
+
message = "Target directory name `#{to}` already exists"
|
112
|
+
fail message if to.is_directory_path?
|
113
|
+
|
114
|
+
return FileUtils.mv from, to
|
115
|
+
|
116
|
+
end
|
117
|
+
alias_method :rename_dir, :rename_directory
|
118
|
+
|
119
|
+
# TODO: Think about making a `directory` method on
|
120
|
+
# string so from / to is super clear.
|
121
|
+
def move_directory *args, **kwargs
|
122
|
+
message = \
|
123
|
+
%w[ Moving directories is confusing. Call either
|
124
|
+
`rename_directory` or `move_directory_into`
|
125
|
+
depending on your needs. There are many aliases. ]
|
126
|
+
.spaced
|
127
|
+
|
128
|
+
fail NoMethodError, message
|
129
|
+
end
|
130
|
+
|
131
|
+
def move_directory_into dir,
|
132
|
+
into,
|
133
|
+
allow_hidden: true,
|
134
|
+
allow_absolute: true
|
135
|
+
|
136
|
+
safe = dir.safe_dir_path? allow_hidden: allow_hidden,
|
137
|
+
allow_absolute: allow_absolute
|
138
|
+
|
139
|
+
safe &&= into.safe_dir_path? allow_hidden: allow_hidden,
|
140
|
+
allow_absolute: allow_absolute
|
141
|
+
|
142
|
+
message = "Refusing to rename unsafe directory"
|
143
|
+
fail message unless safe
|
144
|
+
|
145
|
+
message = "Target #{into} is not a directory"
|
146
|
+
fail message unless into.is_directory_path?
|
147
|
+
|
148
|
+
message = "Source #{dir} is not a directory"
|
149
|
+
fail message unless dir.is_directory_path?
|
150
|
+
|
151
|
+
FileUtils.mv dir, into
|
152
|
+
end
|
153
|
+
alias_method :move_dir_into, :move_directory_into
|
154
|
+
alias_method :mv_dir_into, :move_directory_into
|
155
|
+
|
156
|
+
def config_home
|
157
|
+
path = ENV['XDG_CACHE_HOME']
|
158
|
+
return path unless path.blank?
|
159
|
+
path ||= File.join ENV['HOME'], ".config"
|
160
|
+
end
|
161
|
+
alias_method :xdg_config_home, :config_home
|
162
|
+
|
163
|
+
def data_home
|
164
|
+
path = ENV['XDG_DATA_HOME']
|
165
|
+
return path unless path.blank?
|
166
|
+
path ||= File.join ENV['HOME'], ".local", "share"
|
167
|
+
end
|
168
|
+
alias_method :output_home, :data_home
|
169
|
+
alias_method :xdg_data_home, :data_home
|
170
|
+
|
171
|
+
def cache_home sub_directory=nil
|
172
|
+
response = ENV['XDG_CACHE_HOME']
|
173
|
+
response ||= File.join ENV['HOME'], ".cache"
|
174
|
+
|
175
|
+
return response unless sub_directory
|
176
|
+
|
177
|
+
File.join response, sub_directory
|
178
|
+
end
|
179
|
+
alias_method :xdg_cache_home, :cache_home
|
180
|
+
|
181
|
+
def cache_home! sub_directory
|
182
|
+
path = cache_home sub_directory
|
183
|
+
File.make_dir_if_not_exists path
|
184
|
+
path
|
185
|
+
end
|
186
|
+
alias_method :xdg_cache_home!, :cache_home!
|
187
|
+
|
188
|
+
def make_directory_if_not_exists directory
|
189
|
+
FileUtils.mkdir_p(directory).first
|
190
|
+
end
|
191
|
+
alias_method :make_dir_if_not_exists,
|
192
|
+
:make_directory_if_not_exists
|
193
|
+
|
194
|
+
def make_directory directory
|
195
|
+
FileUtils.mkdir(directory).first
|
196
|
+
end
|
197
|
+
alias_method :create_directory, :make_directory
|
198
|
+
alias_method :create_dir, :make_directory
|
199
|
+
alias_method :make_dir, :make_directory
|
200
|
+
|
201
|
+
def remove_directory directory, quiet_if_gone: false
|
202
|
+
no_directory_exists = ! directory.is_directory_path?
|
203
|
+
return if quiet_if_gone && no_directory_exists
|
204
|
+
FileUtils.remove_dir directory
|
205
|
+
end
|
206
|
+
alias_method :rm_directory, :remove_directory
|
207
|
+
alias_method :remove_dir, :remove_directory
|
208
|
+
alias_method :rm_dir, :remove_directory
|
209
|
+
|
78
210
|
end
|
79
211
|
|
80
212
|
def vv_readlines
|
data/lib/vv/hash_methods.rb
CHANGED
@@ -38,5 +38,41 @@ module VV
|
|
38
38
|
self
|
39
39
|
end
|
40
40
|
|
41
|
+
def cli_print width: 80,
|
42
|
+
position: 0,
|
43
|
+
padding: 0
|
44
|
+
|
45
|
+
key_padding = nil
|
46
|
+
|
47
|
+
String.capture_stdout {
|
48
|
+
key_padding = self.keys.map { | key |
|
49
|
+
key.cli_print width: width,
|
50
|
+
position: position,
|
51
|
+
padding: padding
|
52
|
+
}.max
|
53
|
+
}
|
54
|
+
|
55
|
+
key_padding += 1
|
56
|
+
|
57
|
+
self.each do | key, value |
|
58
|
+
position = key.cli_print width: width,
|
59
|
+
position: position,
|
60
|
+
padding: padding
|
61
|
+
|
62
|
+
print ( key_padding - position ).spaces
|
63
|
+
|
64
|
+
value_padding = position = key_padding
|
65
|
+
|
66
|
+
position = value.cli_print width: width,
|
67
|
+
position: position,
|
68
|
+
padding: value_padding
|
69
|
+
|
70
|
+
puts
|
71
|
+
position = 0
|
72
|
+
end
|
73
|
+
|
74
|
+
position
|
75
|
+
end
|
76
|
+
|
41
77
|
end
|
42
78
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Integer
|
2
|
+
|
3
|
+
def spaces
|
4
|
+
characters String.space
|
5
|
+
end
|
6
|
+
|
7
|
+
def dashes
|
8
|
+
characters String.dash
|
9
|
+
end
|
10
|
+
|
11
|
+
def characters character, fail_on_negative: false
|
12
|
+
message = "Expected single character, not #{character}."
|
13
|
+
fail ArgumentError, message if character.length > 1
|
14
|
+
|
15
|
+
message = "Expected non-negative integer, not `#{self}`."
|
16
|
+
fail message if self < 0 and fail_on_negative
|
17
|
+
|
18
|
+
( self > 0 ) ? ( character * self ) : String.empty_string
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_i!
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/vv/object_methods.rb
CHANGED
@@ -1,11 +1,50 @@
|
|
1
1
|
class Object
|
2
2
|
|
3
|
+
alias_method :responds_to?, :respond_to?
|
4
|
+
|
3
5
|
def blank?
|
4
6
|
respond_to?(:empty?) ? !!empty? : !self
|
5
7
|
end unless method_defined? :blank?
|
6
8
|
|
9
|
+
def cli_printable **kwargs
|
10
|
+
String.get_stdout { self.cli_print( **kwargs ) }
|
11
|
+
rescue NoMethodError
|
12
|
+
message = \
|
13
|
+
"`cli_printable` requires `cli_print` on child class"
|
14
|
+
fail NoMethodError, message
|
15
|
+
end unless method_defined? :cli_printable
|
16
|
+
|
7
17
|
def present?
|
8
18
|
!blank?
|
9
19
|
end unless method_defined? :present?
|
10
20
|
|
21
|
+
def one_of? *collection, mixed: false, allow_unsafe: false
|
22
|
+
nested = collection.first.is_a? Array
|
23
|
+
nested ||= collection.first.is_a? Hash
|
24
|
+
nested ||= collection.first.is_a? Set
|
25
|
+
|
26
|
+
message = \
|
27
|
+
"Unexpected nested argument. If desired set `allow_unsafe: true`."
|
28
|
+
fail ArgumentError, message if nested unless allow_unsafe
|
29
|
+
|
30
|
+
return collection.include? self if mixed
|
31
|
+
|
32
|
+
klass = self.class
|
33
|
+
ok = collection.reject {|s| s.is_a? klass }.blank?
|
34
|
+
|
35
|
+
message = "Invalid types: #{klass} collection required."
|
36
|
+
fail ArgumentError, message unless ok
|
37
|
+
|
38
|
+
collection.include? self
|
39
|
+
end
|
40
|
+
|
41
|
+
def one_of! *collection, mixed: false
|
42
|
+
return true if self.one_of?( *collection, mixed: mixed )
|
43
|
+
|
44
|
+
klass = self.class
|
45
|
+
collection = collection.stringify_collection grave: true
|
46
|
+
message = "#{klass} `#{self}` is invalid. Must be one of: #{collection}."
|
47
|
+
fail message
|
48
|
+
end
|
49
|
+
|
11
50
|
end
|
data/lib/vv/string_methods.rb
CHANGED
@@ -25,12 +25,22 @@ module VV
|
|
25
25
|
("A".."Z").to_a
|
26
26
|
end
|
27
27
|
|
28
|
-
def letters_and_numbers
|
28
|
+
def letters_and_numbers capitals: false
|
29
29
|
response = self.letters
|
30
30
|
response += self.capitals if capitals
|
31
31
|
response += self.numbers
|
32
32
|
end
|
33
33
|
|
34
|
+
def capture_stdout(&block)
|
35
|
+
original_stdout = $stdout
|
36
|
+
$stdout = StringIO.new
|
37
|
+
yield
|
38
|
+
$stdout.string
|
39
|
+
ensure
|
40
|
+
$stdout = original_stdout
|
41
|
+
end
|
42
|
+
alias_method :get_stdout, :capture_stdout
|
43
|
+
|
34
44
|
end
|
35
45
|
|
36
46
|
module SharedInstanceAndClassMethods
|
@@ -47,14 +57,26 @@ module VV
|
|
47
57
|
"-"
|
48
58
|
end
|
49
59
|
|
60
|
+
def equals_sign
|
61
|
+
"="
|
62
|
+
end
|
63
|
+
|
50
64
|
def period
|
51
65
|
"."
|
52
66
|
end
|
53
67
|
|
68
|
+
def colon
|
69
|
+
":"
|
70
|
+
end
|
71
|
+
|
54
72
|
def underscore_character
|
55
73
|
"_"
|
56
74
|
end
|
57
75
|
|
76
|
+
def space
|
77
|
+
" "
|
78
|
+
end
|
79
|
+
|
58
80
|
def newline
|
59
81
|
"\n"
|
60
82
|
end
|
@@ -75,6 +97,12 @@ module VV
|
|
75
97
|
|
76
98
|
end
|
77
99
|
|
100
|
+
# Instance methods start
|
101
|
+
|
102
|
+
def includes? *args
|
103
|
+
self.include?(*args)
|
104
|
+
end
|
105
|
+
|
78
106
|
def starts_with? *args
|
79
107
|
self.start_with?(*args)
|
80
108
|
end
|
@@ -98,6 +126,10 @@ module VV
|
|
98
126
|
self.chomp(string) + string
|
99
127
|
end
|
100
128
|
|
129
|
+
def with_newline
|
130
|
+
self.with_ending newline
|
131
|
+
end
|
132
|
+
|
101
133
|
def squish!
|
102
134
|
self.gsub!(/\A[[:space:]]+/, "")
|
103
135
|
self.gsub!(/[[:space:]]+\z/, "")
|
@@ -146,9 +178,10 @@ module VV
|
|
146
178
|
self.to_regex_filter
|
147
179
|
end
|
148
180
|
|
149
|
-
def safe_filename?
|
181
|
+
def safe_filename?( allow_hidden: false )
|
150
182
|
unsafe = self.blank?
|
151
|
-
|
183
|
+
|
184
|
+
unsafe ||= self.starts_with?(period) unless allow_hidden
|
152
185
|
unsafe ||= self.starts_with? dash
|
153
186
|
|
154
187
|
unsafe ||= self.end_with? period
|
@@ -159,16 +192,52 @@ module VV
|
|
159
192
|
! unsafe
|
160
193
|
end
|
161
194
|
|
162
|
-
def safe_path?
|
195
|
+
def safe_path?( allow_hidden: false, allow_absolute: false )
|
196
|
+
safe = self.safe_dir_path? allow_hidden: allow_hidden,
|
197
|
+
allow_absolute: allow_absolute
|
198
|
+
|
199
|
+
unsafe = ( ! safe )
|
200
|
+
unsafe ||= self.ends_with? File::SEPARATOR
|
201
|
+
|
202
|
+
! unsafe
|
203
|
+
end
|
204
|
+
|
205
|
+
def safe_dir_path? allow_hidden: false,
|
206
|
+
allow_absolute: true
|
163
207
|
separator = File::SEPARATOR
|
164
208
|
|
165
|
-
unsafe =
|
166
|
-
unsafe ||= self.
|
167
|
-
unsafe ||= self.
|
209
|
+
unsafe = false
|
210
|
+
unsafe ||= self.starts_with?(separator) unless allow_absolute
|
211
|
+
unsafe ||= self.after(separator, safe: false)
|
212
|
+
.split(separator).map do |fragment|
|
213
|
+
fragment.safe_filename? allow_hidden: allow_hidden
|
214
|
+
end.map(&:!).any?
|
168
215
|
|
169
216
|
! unsafe
|
170
217
|
end
|
171
218
|
|
219
|
+
def is_directory_path?
|
220
|
+
File.directory? self
|
221
|
+
end
|
222
|
+
|
223
|
+
def is_file_path?
|
224
|
+
File.file? self
|
225
|
+
end
|
226
|
+
|
227
|
+
def file_join *args
|
228
|
+
unsafe = args.reject(&:safe_path?)
|
229
|
+
|
230
|
+
return File.join self, *args if unsafe.blank?
|
231
|
+
|
232
|
+
frags = unsafe.first(3).stringify_collection grave: true
|
233
|
+
count = unsafe.count
|
234
|
+
|
235
|
+
message = \
|
236
|
+
"#{count} unsafe path fragments including: #{frags}"
|
237
|
+
|
238
|
+
fail ArgumentError, message
|
239
|
+
end
|
240
|
+
|
172
241
|
def hex?
|
173
242
|
return false if self.blank?
|
174
243
|
match_non_hex_digits = /\H/
|
@@ -225,6 +294,17 @@ module VV
|
|
225
294
|
t: 1000_000_000_000 }.stringify_keys
|
226
295
|
end
|
227
296
|
|
297
|
+
def unstyle
|
298
|
+
self.gsub( /\e\[+\d+m/, empty_string )
|
299
|
+
.gsub( /\e\[((\d+)+\;)+\d+m/, empty_string )
|
300
|
+
end
|
301
|
+
alias_method :unstyled, :unstyle
|
302
|
+
|
303
|
+
def unstyle!
|
304
|
+
self.gsub!( /\e\[+\d+m/, empty_string )
|
305
|
+
self.gsub!( /\e\[((\d+)+\;)+\dm/, empty_string )
|
306
|
+
end
|
307
|
+
|
228
308
|
def style *args
|
229
309
|
color = bold = underline = italic = nil
|
230
310
|
|
@@ -273,6 +353,10 @@ module VV
|
|
273
353
|
"@#{self}"
|
274
354
|
end
|
275
355
|
|
356
|
+
def insta_sym
|
357
|
+
self.insta.to_sym
|
358
|
+
end
|
359
|
+
|
276
360
|
def to position
|
277
361
|
self[0..position]
|
278
362
|
end
|
@@ -381,5 +465,47 @@ module VV
|
|
381
465
|
self[9]
|
382
466
|
end
|
383
467
|
|
468
|
+
def cli_print width: 80,
|
469
|
+
padding: 0,
|
470
|
+
position: 0,
|
471
|
+
hard_wrap: false
|
472
|
+
|
473
|
+
raise NotImplemented if hard_wrap
|
474
|
+
raise NotImplemented if self.includes? newline
|
475
|
+
|
476
|
+
pad_length = padding - position
|
477
|
+
position += pad_length
|
478
|
+
print pad_length.spaces
|
479
|
+
|
480
|
+
unstyled_length = self.unstyled.length
|
481
|
+
remaining_length = width - position
|
482
|
+
if unstyled_length <= remaining_length
|
483
|
+
print self
|
484
|
+
position += unstyled_length
|
485
|
+
return position
|
486
|
+
end
|
487
|
+
|
488
|
+
space_index = self[0..remaining_length].rindex(" ")
|
489
|
+
space_index ||= self.index(" ")
|
490
|
+
|
491
|
+
if space_index
|
492
|
+
sub = self[0..space_index]
|
493
|
+
print sub
|
494
|
+
puts
|
495
|
+
position = 0
|
496
|
+
start = space_index + 1
|
497
|
+
return self[start..-1].cli_print width: width,
|
498
|
+
padding: padding,
|
499
|
+
position: position,
|
500
|
+
hard_wrap: hard_wrap
|
501
|
+
else
|
502
|
+
print self
|
503
|
+
puts
|
504
|
+
position = 0
|
505
|
+
end
|
506
|
+
|
507
|
+
return position
|
508
|
+
end
|
509
|
+
|
384
510
|
end
|
385
511
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module VV
|
2
|
+
module SymbolMethods
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def vv_included?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def insta
|
15
|
+
self.to_s.insta
|
16
|
+
end
|
17
|
+
|
18
|
+
def insta_sym
|
19
|
+
self.to_s.insta_sym
|
20
|
+
end
|
21
|
+
|
22
|
+
def plural? *args, **kwargs
|
23
|
+
self.to_s.plural?( *args, **kwargs )
|
24
|
+
end
|
25
|
+
|
26
|
+
def singular? *args, **kwargs
|
27
|
+
self.to_s.singular?( *args, **kwargs )
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/vv/utility/automate.rb
CHANGED
@@ -73,11 +73,89 @@ module VV
|
|
73
73
|
|
74
74
|
def push( name: )
|
75
75
|
puts %x{ TAG_VERSION=$(./bin/version)
|
76
|
+
git push origin HEAD && \\
|
77
|
+
gem push $(readlink -f #{name}-*.gem | sort | tail -n 1) && \\
|
76
78
|
git push origin v${TAG_VERSION}
|
77
|
-
|
78
|
-
}
|
79
|
+
}
|
79
80
|
end
|
80
81
|
module_function :push
|
81
82
|
|
83
|
+
def run command: nil, annoying_command: nil
|
84
|
+
command ||= "rake"
|
85
|
+
annoying_command ||= command
|
86
|
+
args = ARGV.to_a
|
87
|
+
|
88
|
+
# breaking out of binding.pry is hard without exec
|
89
|
+
exec command if args.empty?
|
90
|
+
|
91
|
+
return annoying_command(args, command: annoying_command)
|
92
|
+
end
|
93
|
+
module_function :run
|
94
|
+
|
95
|
+
def annoying_command args, command: nil
|
96
|
+
command ||= "rake"
|
97
|
+
|
98
|
+
test_dir = File.join File.expand_path(Dir.pwd), "test"
|
99
|
+
temp_test_dir = "temp_test_dir_#{Random.identifier 6}"
|
100
|
+
|
101
|
+
message = "Cannot find test directory `#{test_dir}`."
|
102
|
+
fail message unless test_dir.is_directory_path?
|
103
|
+
|
104
|
+
fail "Unexpected arg count" if args.size > 2
|
105
|
+
|
106
|
+
helper_file = "test_helper.rb"
|
107
|
+
file, line_number = args
|
108
|
+
|
109
|
+
if line_number.nil?
|
110
|
+
file, line_number = file.split(String.colon)[0..1]
|
111
|
+
end
|
112
|
+
|
113
|
+
File.rename_directory test_dir, temp_test_dir
|
114
|
+
sleep 0.01
|
115
|
+
File.make_directory test_dir
|
116
|
+
|
117
|
+
path = file.split(File.separator).last
|
118
|
+
new_filename = temp_test_dir.file_join path
|
119
|
+
helper_file = temp_test_dir.file_join helper_file
|
120
|
+
File.copy_into new_filename, test_dir
|
121
|
+
File.copy_into helper_file, test_dir
|
122
|
+
|
123
|
+
if line_number
|
124
|
+
line_number = line_number.to_i!
|
125
|
+
i = j = line_number - 1
|
126
|
+
lines = File.vv_readlines test_dir.file_join(path)
|
127
|
+
|
128
|
+
while i > 0
|
129
|
+
line = lines[i]
|
130
|
+
break if line.start_with? " def "
|
131
|
+
i -= 1
|
132
|
+
end
|
133
|
+
|
134
|
+
while j < lines.count
|
135
|
+
line = lines[j]
|
136
|
+
break if line.start_with? " end"
|
137
|
+
j += 1
|
138
|
+
end
|
139
|
+
content = (lines[0..3] + lines[i..j] + lines[-2..-1] ).join("\n")
|
140
|
+
content += "\n"
|
141
|
+
|
142
|
+
File.write test_dir.file_join(path), content
|
143
|
+
end
|
144
|
+
|
145
|
+
full_command = \
|
146
|
+
[ command,
|
147
|
+
"rm -r #{test_dir}",
|
148
|
+
"mv #{temp_test_dir} #{test_dir}" ].join(" && ")
|
149
|
+
|
150
|
+
exec full_command
|
151
|
+
|
152
|
+
# The below shouldn't run in normal operation, since
|
153
|
+
# exec will replace the current process
|
154
|
+
ensure
|
155
|
+
File.remove_directory test_dir
|
156
|
+
File.rename_directory temp_test_dir, test_dir
|
157
|
+
end
|
158
|
+
module_function :annoying_command
|
159
|
+
|
82
160
|
end
|
83
161
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
class LookupTable
|
2
|
+
|
3
|
+
attr_reader :canonnicals, :aliases, :data
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@canonicals = Hash.new
|
7
|
+
@aliases = Hash.new
|
8
|
+
@data = Hash.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def alias( key:, to: )
|
12
|
+
_ensure_alias_possible key
|
13
|
+
|
14
|
+
@canonicals[key] = to
|
15
|
+
|
16
|
+
@aliases[to] ||= Set.new
|
17
|
+
@aliases[to] << key
|
18
|
+
end
|
19
|
+
|
20
|
+
def []= key, value
|
21
|
+
@data[ self.canonical key ] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def [] key
|
25
|
+
@data[ self.canonical key ]
|
26
|
+
end
|
27
|
+
|
28
|
+
def canonical key
|
29
|
+
@canonicals[key] || key
|
30
|
+
end
|
31
|
+
|
32
|
+
def canonical_keys
|
33
|
+
@data.keys + (@aliases.keys - @data.keys)
|
34
|
+
end
|
35
|
+
|
36
|
+
def include? key
|
37
|
+
@data.include?( self.canonical key )
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_h
|
41
|
+
keys = self.canonical_keys
|
42
|
+
|
43
|
+
keys.inject({}) {|acc, key|
|
44
|
+
data = @data[key] || {}
|
45
|
+
aliases = @aliases[key] || {}
|
46
|
+
acc[key] = { data: data, aliases: aliases.to_a }
|
47
|
+
acc
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Make Object class method called `delegate_missing`?
|
52
|
+
def method_missing(method, *args, **kwargs, &block)
|
53
|
+
super
|
54
|
+
rescue NoMethodError
|
55
|
+
begin
|
56
|
+
if args.size > 0 && kwargs.size > 0
|
57
|
+
return self.to_h.public_send method, *args, **kwargs, &block
|
58
|
+
elsif args.size > 0
|
59
|
+
return self.to_h.public_send method, *args, &block
|
60
|
+
elsif kwargs.size > 0
|
61
|
+
return self.to_h.public_send method, **kwargs, &block
|
62
|
+
else
|
63
|
+
return self.to_h.public_send method, &block
|
64
|
+
end
|
65
|
+
rescue NoMethodError
|
66
|
+
end
|
67
|
+
raise
|
68
|
+
end
|
69
|
+
|
70
|
+
def lookup_canonical key
|
71
|
+
return key if self.canonical_keys.include? key
|
72
|
+
|
73
|
+
@canonicals[key]
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def _ensure_alias_possible key
|
79
|
+
return if @aliases[key].blank?
|
80
|
+
aliases = @aliases[key]
|
81
|
+
count = aliases.count
|
82
|
+
message = \
|
83
|
+
"Cannot alias `#{key}` because #{count} others currently alias it."
|
84
|
+
fail message
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/lib/vv/version.rb
CHANGED
data/lib/vv.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
require 'securerandom'
|
2
|
+
require 'set'
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'bigdecimal/util'
|
5
|
+
require 'fileutils'
|
2
6
|
|
3
7
|
require_relative "vv/gem_methods"
|
4
8
|
|
@@ -6,6 +10,10 @@ Gem.require_files "vv/*.rb"
|
|
6
10
|
Gem.require_files "vv/style/*.rb"
|
7
11
|
Gem.require_files "vv/utility/*.rb"
|
8
12
|
|
13
|
+
class Symbol
|
14
|
+
include VV::SymbolMethods
|
15
|
+
end
|
16
|
+
|
9
17
|
class String
|
10
18
|
include VV::StringMethods
|
11
19
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Aysan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-02-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -63,10 +63,12 @@ files:
|
|
63
63
|
- README.markdown
|
64
64
|
- lib/vv.rb
|
65
65
|
- lib/vv/array_methods.rb
|
66
|
+
- lib/vv/cli.rb
|
66
67
|
- lib/vv/false_methods.rb
|
67
68
|
- lib/vv/file_methods.rb
|
68
69
|
- lib/vv/gem_methods.rb
|
69
70
|
- lib/vv/hash_methods.rb
|
71
|
+
- lib/vv/integer_methods.rb
|
70
72
|
- lib/vv/nil_methods.rb
|
71
73
|
- lib/vv/object_methods.rb
|
72
74
|
- lib/vv/random_methods.rb
|
@@ -77,8 +79,10 @@ files:
|
|
77
79
|
- lib/vv/style/format.rb
|
78
80
|
- lib/vv/style/italic.rb
|
79
81
|
- lib/vv/style/underline.rb
|
82
|
+
- lib/vv/symbol_methods.rb
|
80
83
|
- lib/vv/true_methods.rb
|
81
84
|
- lib/vv/utility/automate.rb
|
85
|
+
- lib/vv/utility/lookup_table.rb
|
82
86
|
- lib/vv/version.rb
|
83
87
|
homepage: https://github.com/zachaysan/vv
|
84
88
|
licenses:
|