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 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: