vv 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cd4725920ce804b99be1c23848495c6e9ca83649fd7ab3a69b8af0d21ec6e48
4
- data.tar.gz: 4b229f3cc218a33e3cc85bab68ecb1662e41d3a6ab8474c80d031ca04873ab64
3
+ metadata.gz: 052ef19db592bebd51a14310cc4eea838dbd10645f43c05f15cb774292222990
4
+ data.tar.gz: 2e23028b168d6cb104a0b631c9b80bcb11696120854fbd603126bc962aa380f9
5
5
  SHA512:
6
- metadata.gz: 70a7280f7746478071e4f19dd6887ccaba0b4e313010cf9b0502955e5670643767b274813570e27f643d6f08183594a7c031796a7e03e89e8d30b943519ad3de
7
- data.tar.gz: 78e143160fbefec25c96fd74201b084634c8a9bd8afbdb49de706e88b9f7e662cd35e9351f63c62004114563f123e3e5c67b00d814d913779da80349350cfbf6
6
+ metadata.gz: a62626e31c21dbe0c5fd426681c2b0007cb4f0a0e844e77a0f1bf5faa9c49edcc7b2da9a4d2e6650220982f95708d3563ffa68090e2371233f6b66f4db780aa9
7
+ data.tar.gz: 998917dbf37b9900ef500517674d42fad1c64cacbd4aab294a13c29c5c795cbda0926cd0efd0106969287a58fa3fe0f5398e2980e83e39e7786120d54e871f9d
@@ -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
@@ -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
@@ -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
@@ -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
@@ -25,12 +25,22 @@ module VV
25
25
  ("A".."Z").to_a
26
26
  end
27
27
 
28
- def letters_and_numbers(capitals: false)
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
- unsafe ||= self.starts_with? period
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 = self.starts_with? separator
166
- unsafe ||= self.ends_with? separator
167
- unsafe ||= self.split(separator).map(&:safe_filename?).map(&:!).any?
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
@@ -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
- gem push $(readlink -f #{name}-*.gem | sort | tail -n 1 )
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
@@ -1,3 +1,3 @@
1
1
  module VV
2
- VERSION = '0.0.8'
2
+ VERSION = '0.0.9'
3
3
  end
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.8
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-01-29 00:00:00.000000000 Z
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: