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.
- checksums.yaml +5 -5
- data/bin/central +53 -8
- data/lib/central.rb +166 -16
- metadata +4 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b1df8c743134a9baddb4d6bd454b6bd00e417dde4812533c3d2f4c218debe64a
|
4
|
+
data.tar.gz: 702aa1bdf779cad2ef1fc32ce4ef3a9354d8f0a81f059f94029a33bdb0a31f7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f6c2e18e8f0428bbc390c29472892c5878a47ee8fc271ff2d2da50d3665da28c7cfb61944b12558021a22947892e008388111c59409988ef3e27a4450d865db
|
7
|
+
data.tar.gz: '08ade7d82161a2fe545b30e21428e3c627f3d052cfd35fc40d1718004e4881d16034eb1cfde2ed0ae5c603f5b70eae56a4ce840c199ffe74705405eac1313e82'
|
data/bin/central
CHANGED
@@ -7,15 +7,60 @@
|
|
7
7
|
# -------------------------------------------------------------------------
|
8
8
|
|
9
9
|
require 'central'
|
10
|
+
require 'optparse'
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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)
|
data/lib/central.rb
CHANGED
@@ -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
|
-
|
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
|
118
|
+
stdout += ch
|
89
119
|
unless silent
|
90
|
-
stdout_line
|
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
|
136
|
+
stderr += ch
|
107
137
|
unless silent
|
108
|
-
stderr_line
|
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
|
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
|
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
|
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
|
-
(
|
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
|
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.
|
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:
|
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.
|
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
|
-
|
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
|