central 0.2.5 → 0.3.1

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.
Files changed (4) hide show
  1. checksums.yaml +5 -5
  2. data/bin/central +53 -8
  3. data/lib/central.rb +166 -16
  4. metadata +4 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2c48d4b6cb13b63e3ba6896942008ae004fad3af
4
- data.tar.gz: 37dc1ea33c1ed5e8521972c46c3d09df02c3aed6
2
+ SHA256:
3
+ metadata.gz: b1df8c743134a9baddb4d6bd454b6bd00e417dde4812533c3d2f4c218debe64a
4
+ data.tar.gz: 702aa1bdf779cad2ef1fc32ce4ef3a9354d8f0a81f059f94029a33bdb0a31f7a
5
5
  SHA512:
6
- metadata.gz: 69a3728b2832dc452ed34d8b88141efe9ea17287fe2555ac5b985e6c89b34122fa0a4037b49701563639fad3442d4894d9a3be671ae449bbded21b78d636dbc9
7
- data.tar.gz: 3773b38f6bc059f9cddfa576017fd67b1628ed7dbf90702618988b2537f59e31e691e57d0c9134689737e17eb918520cc00728f567ceb9da655ba3ab43714deb
6
+ metadata.gz: 7f6c2e18e8f0428bbc390c29472892c5878a47ee8fc271ff2d2da50d3665da28c7cfb61944b12558021a22947892e008388111c59409988ef3e27a4450d865db
7
+ data.tar.gz: '08ade7d82161a2fe545b30e21428e3c627f3d052cfd35fc40d1718004e4881d16034eb1cfde2ed0ae5c603f5b70eae56a4ce840c199ffe74705405eac1313e82'
@@ -7,15 +7,60 @@
7
7
  # -------------------------------------------------------------------------
8
8
 
9
9
  require 'central'
10
+ require 'optparse'
10
11
 
11
- unless ARGV.empty?
12
- if ARGV[0] == '-v' || ARGV[0] == '--version' || ARGV[0] == '-version'
13
- puts 'central v0.2.5'
14
- exit 0
12
+ VERSION = 'v0.3.1'
13
+
14
+ # parse extra options
15
+ ARGV.each do |option|
16
+ if not option.empty? and option.index('-') == 0 and not ['-h', '--help', '-i', '--irb', '-n', '--no-color', '-v', '--version', '-m', '--monitor'].include?(option)
17
+ $options.append(option)
15
18
  end
16
- if ARGV[0] == '-h' || ARGV[0] == '--help' || ARGV[0] == '-help'
17
- puts 'central [path/to/configuration.rb]'
18
- exit 0
19
+ end
20
+ ARGV.delete_if { |option| $options.include?(option) }
21
+
22
+ # parse central options
23
+ options = {:color => true}
24
+ OptionParser.new do |opts|
25
+ opts.banner = %{central #{VERSION} - created by Dmitry Geurkov (d.geurkov@gmail.com) and licensed under LGPLv3
26
+
27
+ Usage: central [options] [directory|configuration.rb, ...]
28
+
29
+ Description: if directory is specified it will use configuration.rb file inside that directory
30
+ if no [directory|configuration.rb] is specified
31
+ it will use configuration.rb in current working directory
32
+
33
+ Options:}
34
+ opts.on("-i", "--irb", "Start interactive IRB REPL") do |_|
35
+ options[:irb] = true
19
36
  end
37
+
38
+ opts.on("-m", "--monitor", "Monitor erb files for changes and reprocess them automatically") do |_|
39
+ options[:monitor] = true
40
+ end
41
+
42
+ opts.on("-n", "--no-color", "Do not colorize output") do |_|
43
+ options[:color] = false
44
+ end
45
+
46
+ opts.on("-v", "--version", "Print version") do |_|
47
+ puts "central #{VERSION}"
48
+ exit
49
+ end
50
+ end.parse!
51
+
52
+ if options[:irb]
53
+ require 'irb'
54
+ $colored = true
55
+ $colored = options[:color] if options[:color]
56
+ info "Welcome to central #{VERSION} IRB repl, interactive playground for your dotcentral configuration"
57
+ info "Please ignore warning below as central has source function, and IRB tries to create alias for irb_source"
58
+ info "In order to use IRB's source functionality just call it without alias as irb_source"
59
+ ARGV.clear
60
+ IRB.start(__FILE__)
61
+ elsif options[:monitor]
62
+ run_central(ARGV,options[:color])
63
+ run_monitors
64
+ else
65
+ run_central(ARGV,options[:color])
20
66
  end
21
- central(ARGV)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # -------------------------------------------------------------------------
2
4
  # # central - dot files manager licensed under LGPLv3 (see LICENSE file) |
3
5
  # # written in Ruby by Dmitry Geurkov (d.geurkov@gmail.com) |
@@ -7,14 +9,38 @@ require 'erb'
7
9
  require 'socket'
8
10
  require 'open3'
9
11
  require 'fileutils'
12
+ require 'digest'
13
+
14
+ # options
15
+ $options = []
16
+
17
+ # get option, returns Array if multiple or nil if none
18
+ def option(opt)
19
+ options = $options.filter { |option| option.index(opt) == 0 }
20
+ if options.size == 0
21
+ return nil
22
+ elsif options.size == 1
23
+ return options[0]
24
+ else
25
+ return options
26
+ end
27
+ end
10
28
 
11
29
  # cli colors
30
+ $colored = true
12
31
  COLOR_RED = 31
13
32
  COLOR_GREEN = 32
14
33
 
34
+ # monitors
35
+ $monitors = {}
36
+
15
37
  # putsc, puts with color
16
38
  def color(message, color)
17
- "\e[#{color}m#{message}\e[0m"
39
+ if $colored
40
+ "\e[#{color}m#{message}\e[0m"
41
+ else
42
+ message
43
+ end
18
44
  end
19
45
 
20
46
  # info
@@ -61,6 +87,10 @@ def osx?
61
87
  os == 'osx'
62
88
  end
63
89
 
90
+ def macos?
91
+ os == 'osx'
92
+ end
93
+
64
94
  def freebsd?
65
95
  os == 'freebsd'
66
96
  end
@@ -74,10 +104,10 @@ end
74
104
  def shell(command, verbose: false, silent: true)
75
105
  info 'Executing', command if verbose
76
106
  exit_code = nil
77
- stdout = ''
78
- stdout_line = ''
79
- stderr = ''
80
- stderr_line = ''
107
+ stdout = String.new
108
+ stdout_line = String.new
109
+ stderr = String.new
110
+ stderr_line = String.new
81
111
  Open3.popen3(command) do |_, o, e, t|
82
112
  stdout_open = true
83
113
  stderr_open = true
@@ -85,9 +115,9 @@ def shell(command, verbose: false, silent: true)
85
115
  if stdout_open
86
116
  begin
87
117
  ch = o.read_nonblock(1)
88
- stdout.insert(-1, ch)
118
+ stdout += ch
89
119
  unless silent
90
- stdout_line.insert(-1, ch)
120
+ stdout_line += ch
91
121
  if ch == "\n"
92
122
  STDOUT.puts stdout_line
93
123
  stdout_line = ''
@@ -103,9 +133,9 @@ def shell(command, verbose: false, silent: true)
103
133
 
104
134
  begin
105
135
  ch = e.read_nonblock(1)
106
- stderr.insert(-1, ch)
136
+ stderr += ch
107
137
  unless silent
108
- stderr_line.insert(-1, ch)
138
+ stderr_line += ch
109
139
  if ch == "\n"
110
140
  STDERR.puts stderr_line
111
141
  stderr_line = ''
@@ -165,13 +195,45 @@ def file_exists?(path)
165
195
  File.file?(path) && File.readable?(path)
166
196
  end
167
197
 
198
+ # get file size
199
+ def file_size(path)
200
+ File.new(abs(path)).size
201
+ end
202
+
203
+ # get file creation time
204
+ def file_ctime(path)
205
+ File.new(abs(path)).ctime
206
+ end
207
+
208
+ # get file modification time
209
+ def file_mtime(path)
210
+ File.new(abs(path)).mtime
211
+ end
212
+
213
+ # get directory entries
214
+ def dir_entries(path)
215
+ Dir.entries(abs(path)).select { |f| f != '.' && f != '..' }
216
+ end
217
+
168
218
  # check if directory exists
169
219
  def dir_exists?(path)
170
220
  path = abs(path)
171
221
  Dir.exist?(path)
172
222
  end
173
223
 
174
- # get directory name of a file
224
+ # get file name of a path, optionally strip suffix if needed
225
+ def file_name(path, strip_suffix: '')
226
+ File.basename(abs(path), strip_suffix)
227
+ end
228
+
229
+ # get file suffix of a path
230
+ def file_suffix(path)
231
+ path = file_name(path)
232
+ suffix_index = path =~ /\.[^.]+$/
233
+ return path[suffix_index, path.size - suffix_index] if suffix_index
234
+ end
235
+
236
+ # get directory name of a path
175
237
  def file_dir(path)
176
238
  File.dirname(abs(path))
177
239
  end
@@ -183,10 +245,15 @@ end
183
245
 
184
246
  # get full path of symlink
185
247
  def symlink_path(symlink)
186
- _, out, = shell("readlink -f \"#{abs(symlink)}\" 2>&1")
248
+ _, out, = shell("readlink \"#{abs(symlink)}\" 2>&1")
187
249
  out.strip
188
250
  end
189
251
 
252
+ # calculate SHA2 digest for a file
253
+ def sha2(file)
254
+ Digest::SHA256.hexdigest read(file)
255
+ end
256
+
190
257
  # make directory including intermediate directories
191
258
  def mkdir(path)
192
259
  path = abs(path)
@@ -286,7 +353,7 @@ def git(url, path, branch: nil, silent: true, depth: nil)
286
353
  else
287
354
  _, out, = shell('git pull 2>&1', silent: silent)
288
355
  end
289
- unless out.downcase.include? 'already up-to-date'
356
+ unless out.downcase.include? 'already up'
290
357
  puts out if silent
291
358
  info 'Git repository pulled', "#{url} → #{path}"
292
359
  end
@@ -302,8 +369,12 @@ def git(url, path, branch: nil, silent: true, depth: nil)
302
369
  end
303
370
 
304
371
  # download url into a path using curl
305
- def curl(url, path, verbose: false)
372
+ def curl(url, path, content_length_check: false, verbose: false)
306
373
  path = abs(path)
374
+ if content_length_check and file_exists?(path)
375
+ content_length = curl_headers(url, verbose: verbose)['content-length'].to_i
376
+ return if file_size(path) == content_length
377
+ end
307
378
  info 'Downloading', "#{url} → #{path}"
308
379
  exit_code, output, = shell("curl -s -S \"#{url}\"",
309
380
  verbose: verbose, silent: true)
@@ -315,6 +386,21 @@ def curl(url, path, verbose: false)
315
386
  info 'Downloaded', "#{url} → #{path}"
316
387
  end
317
388
 
389
+ # get url response headers as Hash using curl
390
+ def curl_headers(url, method: 'HEAD', verbose: false)
391
+ exit_code, output, = shell("curl -I -X #{method} -s -S \"#{url}\"",
392
+ verbose: verbose, silent: true)
393
+ unless exit_code.success?
394
+ error output
395
+ fail "Couldn't get headers from", url
396
+ end
397
+ headers = {}
398
+ output.scan(/^(?!HTTP)([^:]+):(.*)$/).each do |m|
399
+ headers[m[0].strip.downcase] = m[1].sub("\r","").strip
400
+ end
401
+ headers
402
+ end
403
+
318
404
  # read content of a file
319
405
  def read(file)
320
406
  file = abs(file)
@@ -359,8 +445,17 @@ def ls(path, dotfiles: false, grep: '', dir: true, file: true)
359
445
  ls
360
446
  end
361
447
 
448
+ # compare_file
449
+ def compare_file(from, to)
450
+ from = abs(from)
451
+ to = abs(to)
452
+ FileUtils.compare_file(from, to)
453
+ end
454
+
362
455
  # copy_file
363
456
  def copy_file(from, to)
457
+ from = abs(from)
458
+ to = abs(to)
364
459
  fail "Couldn't access file", from unless file_exists?(from)
365
460
 
366
461
  return if file_exists?(to) && FileUtils.compare_file(from, to)
@@ -374,7 +469,27 @@ def copy(from, to)
374
469
  from = abs(from)
375
470
  to = abs(to)
376
471
  if dir_exists?(from)
377
- (Dir.entries(from).select { |f| f != '.' && f != '..' }).each do |f|
472
+ dir_entries(from).each do |f|
473
+ FileUtils.mkdir_p(to)
474
+ copy("#{from}/#{f}", "#{to}/#{f}")
475
+ end
476
+ else
477
+ copy_file(from, to)
478
+ end
479
+ end
480
+
481
+ # mirror
482
+ def mirror(from, to)
483
+ from = abs(from)
484
+ to = abs(to)
485
+ if dir_exists?(from)
486
+ from_entries = dir_entries(from)
487
+ if dir_exists?(to)
488
+ dir_entries(to).each do |f|
489
+ rm("#{to}/#{f}", recursive: true) unless from_entries.include?(f)
490
+ end
491
+ end
492
+ from_entries.each do |f|
378
493
  FileUtils.mkdir_p(to)
379
494
  copy("#{from}/#{f}", "#{to}/#{f}")
380
495
  end
@@ -384,13 +499,16 @@ def copy(from, to)
384
499
  end
385
500
 
386
501
  # process erb template into an output_file
387
- def erb(file, output_file = nil)
502
+ def erb(file, output_file = nil, monitor = true)
388
503
  file = abs(file)
389
504
  fail 'No erb file found', file unless file_exists?(file)
390
505
 
391
506
  if output_file.nil?
392
507
  output_file = file.end_with?('.erb') ? file[0...-4] : file + '.out'
393
508
  end
509
+
510
+ $monitors[file] = proc { erb(file, output_file, false) } if monitor
511
+
394
512
  out = ERB.new(File.read(file)).result
395
513
  return if File.exist?(output_file) && File.read(output_file) == out
396
514
 
@@ -398,10 +516,19 @@ def erb(file, output_file = nil)
398
516
  info 'Processed erb', "#{file} → #{output_file}"
399
517
  end
400
518
 
519
+ # monitor file for changes and execute proc if file changed
520
+ def monitor(file, &block)
521
+ file = abs(file)
522
+ fail 'No file found', file unless file_exists?(file)
523
+
524
+ $monitors[file] = block
525
+ end
526
+
401
527
  # run configuration.rb file
402
528
  def run(file)
403
529
  cwd = pwd
404
530
  file = abs(file)
531
+ file = File.join(file,'configuration.rb') if not file_exists?(file) and dir_exists?(file)
405
532
  fail 'No configuration file found', file unless file_exists?(file)
406
533
 
407
534
  info 'Running configuration', file
@@ -417,7 +544,8 @@ def run_if_exists(file)
417
544
  end
418
545
 
419
546
  # run central configuration
420
- def central(configurations)
547
+ def run_central(configurations, colored = true)
548
+ $colored = colored
421
549
  if configurations.instance_of?(Array) && !configurations.empty?
422
550
  configurations.each { |configuration| run configuration }
423
551
  elsif configurations.instance_of?(String)
@@ -426,3 +554,25 @@ def central(configurations)
426
554
  run 'configuration.rb'
427
555
  end
428
556
  end
557
+
558
+ # run monitors
559
+ def run_monitors
560
+ info 'Monitoring files for changes (press Ctrl-C to stop)'
561
+ file_mtimes = {}
562
+ $monitors.keys.each { |f| file_mtimes[f] = File.mtime(f) }
563
+ loop do
564
+ $monitors.keys.each do |f|
565
+ file_mtime = File.mtime(f)
566
+ next if file_mtime == file_mtimes[f]
567
+
568
+ info 'File modified', f
569
+ $monitors[f].call
570
+ file_mtimes[f] = file_mtime
571
+ end
572
+ begin
573
+ sleep(0.5)
574
+ rescue Interrupt
575
+ exit 0
576
+ end
577
+ end
578
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: central
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Geurkov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-25 00:00:00.000000000 Z
11
+ date: 2021-01-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: central dotfile management system
14
14
  email: d.geurkov@gmail.com
@@ -31,15 +31,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: '2.0'
34
+ version: '2.3'
35
35
  required_rubygems_version: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
40
  requirements: []
41
- rubyforge_project:
42
- rubygems_version: 2.5.2.1
41
+ rubygems_version: 3.2.3
43
42
  signing_key:
44
43
  specification_version: 4
45
44
  summary: central dotfile management