central 0.2.5 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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