central 0.2.4 → 0.3.0

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 +45 -9
  3. data/lib/central.rb +327 -218
  4. metadata +4 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 59a223e19b875e1e3d7dc50f70f60697816d47e3
4
- data.tar.gz: 8037442fffcaaccbe55bf1209dae265890432836
2
+ SHA256:
3
+ metadata.gz: 9ac8e8ea400a706c511823313cf3832bc3fde937077e8388e0b79e03ccb6f2e3
4
+ data.tar.gz: 8174b7da6cadf72d7c3aa3c5a1ed11fe87ed11413029e7de71712c307746e0ac
5
5
  SHA512:
6
- metadata.gz: 1ef58f6d6da2bf0f3148677cc86d5cf5779c4d2fa766102c1dfd5cd4ac5aad643d7e55ed78a575b83a6e2d38735bfefdc0bb8a6eebe5935dde1ec4326bf03fa6
7
- data.tar.gz: 025ab8f80c6e4cc29038ff1cacb276206527da789865732539d64887b23b590007c9debe723ab7224ed02de28ac705be2a331cdeaf04ae568ed7a1ade5392858
6
+ metadata.gz: 270220f138029359a37a99d8b79d22a9484d05fd8a0a01efea438031f17b10a3497c98a845a98ba9ef2f36838557f81b2a00f3e318e8ee2eb93cbbf4a5c2223d
7
+ data.tar.gz: 6d6b11e171213305af47dcc4dd351c8f1abe4ab841f37ca870d549ed6251bff677766dc49ec7263baa33c34282a416061b8c5d7cf85b0166fa839d209f38f916
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # encoding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  # -------------------------------------------------------------------------
5
5
  # # central - dot files manager licensed under LGPLv3 (see LICENSE file) |
@@ -7,15 +7,51 @@
7
7
  # -------------------------------------------------------------------------
8
8
 
9
9
  require 'central'
10
+ require 'optparse'
10
11
 
11
- if ARGV.length > 0
12
- if ARGV[0] == '-v' || ARGV[0] == '--version' || ARGV[0] == '-version'
13
- puts "central v0.2.4"
14
- exit 0
12
+ VERSION = 'v0.3.0'
13
+
14
+ options = {:color => true}
15
+ OptionParser.new do |opts|
16
+ opts.banner = %{central #{VERSION} - created by Dmitry Geurkov (d.geurkov@gmail.com) and licensed under LGPLv3
17
+
18
+ Usage: central [options] [directory|configuration.rb, ...]
19
+
20
+ Description: if directory is specified it will use configuration.rb file inside that directory
21
+ if no [directory|configuration.rb] is specified
22
+ it will use configuration.rb in current working directory
23
+
24
+ Options:}
25
+ opts.on("-i", "--irb", "Start interactive IRB REPL") do |_|
26
+ options[:irb] = true
27
+ end
28
+
29
+ opts.on("-m", "--monitor", "Monitor erb files for changes and reprocess them automatically") do |_|
30
+ options[:monitor] = true
15
31
  end
16
- if ARGV[0] == '-h' || ARGV[0] == '--help' || ARGV[0] == '-help'
17
- puts "central [path/to/configuration.rb]"
18
- exit 0
32
+
33
+ opts.on("-n", "--no-color", "Do not colorize output") do |_|
34
+ options[:color] = false
19
35
  end
36
+
37
+ opts.on("-v", "--version", "Print version") do |_|
38
+ puts "central #{VERSION}"
39
+ exit
40
+ end
41
+ end.parse!
42
+
43
+ if options[:irb]
44
+ require 'irb'
45
+ $colored = true
46
+ $colored = options[:color] if options[:color]
47
+ info "Welcome to central #{VERSION} IRB repl, interactive playground for your dotcentral configuration"
48
+ info "Please ignore warning below as central has source function, and IRB tries to create alias for irb_source"
49
+ info "In order to use IRB's source functionality just call it without alias as irb_source"
50
+ ARGV.clear
51
+ IRB.start(__FILE__)
52
+ elsif options[:monitor]
53
+ run_central(ARGV,options[:color])
54
+ run_monitors
55
+ else
56
+ run_central(ARGV,options[:color])
20
57
  end
21
- central(ARGV)
@@ -1,4 +1,5 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  # -------------------------------------------------------------------------
3
4
  # # central - dot files manager licensed under LGPLv3 (see LICENSE file) |
4
5
  # # written in Ruby by Dmitry Geurkov (d.geurkov@gmail.com) |
@@ -8,6 +9,42 @@ require 'erb'
8
9
  require 'socket'
9
10
  require 'open3'
10
11
  require 'fileutils'
12
+ require 'digest'
13
+
14
+ # cli colors
15
+ $colored = true
16
+ COLOR_RED = 31
17
+ COLOR_GREEN = 32
18
+
19
+ # monitors
20
+ $monitors = {}
21
+
22
+ # putsc, puts with color
23
+ def color(message, color)
24
+ if $colored
25
+ "\e[#{color}m#{message}\e[0m"
26
+ else
27
+ message
28
+ end
29
+ end
30
+
31
+ # info
32
+ def info(message, param = nil)
33
+ puts color(message, COLOR_GREEN) +
34
+ (param.nil? ? '' : ': ' + param)
35
+ end
36
+
37
+ # error
38
+ def error(message, param = nil)
39
+ puts color(message, COLOR_RED) +
40
+ (param.nil? ? '' : ': ' + param)
41
+ end
42
+
43
+ # fail, print message to stderr and exit with 1
44
+ def fail(message, param = nil)
45
+ error message, param
46
+ exit 1
47
+ end
11
48
 
12
49
  # get hostname
13
50
  def hostname
@@ -17,114 +54,110 @@ end
17
54
  # get operating system
18
55
  def os
19
56
  if RUBY_PLATFORM.include?('linux')
20
- return 'linux'
57
+ 'linux'
21
58
  elsif RUBY_PLATFORM.include?('darwin')
22
- return 'osx'
59
+ 'osx'
23
60
  elsif RUBY_PLATFORM.include?('freebsd')
24
- return 'freebsd'
61
+ 'freebsd'
25
62
  elsif RUBY_PLATFORM.include?('solaris')
26
- return 'solaris'
63
+ 'solaris'
27
64
  end
28
65
  end
29
66
 
30
67
  def linux?
31
- return os == 'linux'
68
+ os == 'linux'
32
69
  end
33
70
 
34
71
  def osx?
35
- return os == 'osx'
72
+ os == 'osx'
73
+ end
74
+
75
+ def macos?
76
+ os == 'osx'
36
77
  end
37
78
 
38
79
  def freebsd?
39
- return os == 'freebsd'
80
+ os == 'freebsd'
40
81
  end
41
82
 
42
83
  def solaris?
43
- return os == 'solaris'
44
- end
45
-
46
- # run shell command and get output, optionaly can print command running if verbose and if not silent will also print to stdout and stderr
47
- $shell_return_value = nil
48
- def shell(command,verbose: false, silent: true)
49
- puts "Executing: #{command}" if verbose
50
- stdout = ''
51
- stdout_line = ''
52
- stderr = ''
53
- stderr_line = ''
54
- Open3.popen3(command) do |i,o,e,t|
84
+ os == 'solaris'
85
+ end
86
+
87
+ # run shell command and get output, optionaly can print command running
88
+ # if verbose and if not silent will also print to stdout and stderr
89
+ def shell(command, verbose: false, silent: true)
90
+ info 'Executing', command if verbose
91
+ exit_code = nil
92
+ stdout = String.new
93
+ stdout_line = String.new
94
+ stderr = String.new
95
+ stderr_line = String.new
96
+ Open3.popen3(command) do |_, o, e, t|
55
97
  stdout_open = true
56
98
  stderr_open = true
57
- while stdout_open or stderr_open
99
+ while stdout_open || stderr_open
58
100
  if stdout_open
59
101
  begin
60
102
  ch = o.read_nonblock(1)
61
- stdout.insert(-1,ch)
103
+ stdout.insert(-1, ch)
62
104
  unless silent
63
- stdout_line.insert(-1,ch)
105
+ stdout_line.insert(-1, ch)
64
106
  if ch == "\n"
65
107
  STDOUT.puts stdout_line
66
108
  stdout_line = ''
67
109
  end
68
110
  end
69
111
  rescue IO::WaitReadable
70
- IO.select([o],nil,nil,0.1) unless stderr_open
112
+ IO.select([o], nil, nil, 0.01) unless stderr_open
71
113
  rescue EOFError
72
114
  stdout_open = false
73
115
  end
74
116
  end
75
- if stderr_open
76
- begin
77
- ch = e.read_nonblock(1)
78
- stderr.insert(-1,ch)
79
- unless silent
80
- stderr_line.insert(-1,ch)
81
- if ch == "\n"
82
- STDERR.puts stderr_line
83
- stderr_line = ''
84
- end
117
+ next unless stderr_open
118
+
119
+ begin
120
+ ch = e.read_nonblock(1)
121
+ stderr.insert(-1, ch)
122
+ unless silent
123
+ stderr_line.insert(-1, ch)
124
+ if ch == "\n"
125
+ STDERR.puts stderr_line
126
+ stderr_line = ''
85
127
  end
86
- rescue IO::WaitReadable
87
- IO.select([e],nil,nil,0.1) unless stdout_open
88
- rescue EOFError
89
- stderr_open = false
90
128
  end
129
+ rescue IO::WaitReadable
130
+ IO.select([e], nil, nil, 0.01) unless stdout_open
131
+ rescue EOFError
132
+ stderr_open = false
91
133
  end
92
134
  end
93
- $shell_return_value = t.value
94
- end
95
- if stderr == ''
96
- return stdout
97
- else
98
- return stdout, stderr
135
+ exit_code = t.value
99
136
  end
137
+ [exit_code, stdout, stderr]
100
138
  end
101
139
 
102
140
  # run shell command with sudo prefix, acts same as shell
103
- def sudo(command,verbose:,silent:)
104
- return shell('sudo '+command, verbose: verbose, silent: silent)
141
+ def sudo(command, verbose:, silent:)
142
+ shell('sudo ' + command, verbose: verbose, silent: silent)
105
143
  end
106
144
 
107
145
  # function used to check that system has all required tools installed
108
- def check_tool(name,check)
109
- begin
110
- output = shell(check+' 2>&1').downcase
111
- if output == '' or output.include?('command not found')
112
- STDERR.puts "#{name} not found, please install it to use central"
113
- exit 1
114
- end
115
- rescue Errno::ENOENT
116
- STDERR.puts "#{name} not found, please install it to use central"
117
- exit 1
146
+ def check_tool(name, check)
147
+ _, output, = shell(check + ' 2>&1')
148
+ if output == '' || output.downcase.include?('command not found')
149
+ fail "#{name} not found, please install it to use central"
118
150
  end
151
+ rescue Errno::ENOENT
152
+ fail "#{name} not found, please install it to use central"
119
153
  end
120
154
 
121
- check_tool('file','file --version')
122
- check_tool('grep','grep --version')
123
- check_tool('ln','ln --version')
124
- check_tool('readlink','readlink --version')
125
- check_tool('git','git --version')
126
- check_tool('curl','curl --version')
127
-
155
+ check_tool('file', 'file --version')
156
+ check_tool('grep', 'grep --version')
157
+ check_tool('ln', 'ln --version')
158
+ check_tool('readlink', 'readlink --version')
159
+ check_tool('git', 'git --version')
160
+ check_tool('curl', 'curl --version')
128
161
 
129
162
  # current working directory
130
163
  def pwd
@@ -133,7 +166,7 @@ end
133
166
 
134
167
  # absolute path
135
168
  def abs(path)
136
- path = File.absolute_path(File.expand_path(path))
169
+ File.absolute_path(File.expand_path(path))
137
170
  end
138
171
 
139
172
  # change current working directory
@@ -147,13 +180,45 @@ def file_exists?(path)
147
180
  File.file?(path) && File.readable?(path)
148
181
  end
149
182
 
183
+ # get file size
184
+ def file_size(path)
185
+ File.new(abs(path)).size
186
+ end
187
+
188
+ # get file creation time
189
+ def file_ctime(path)
190
+ File.new(abs(path)).ctime
191
+ end
192
+
193
+ # get file modification time
194
+ def file_mtime(path)
195
+ File.new(abs(path)).mtime
196
+ end
197
+
198
+ # get directory entries
199
+ def dir_entries(path)
200
+ Dir.entries(abs(path)).select { |f| f != '.' && f != '..' }
201
+ end
202
+
150
203
  # check if directory exists
151
204
  def dir_exists?(path)
152
205
  path = abs(path)
153
206
  Dir.exist?(path)
154
207
  end
155
208
 
156
- # get directory name of a file
209
+ # get file name of a path, optionally strip suffix if needed
210
+ def file_name(path, strip_suffix: '')
211
+ File.basename(abs(path), strip_suffix)
212
+ end
213
+
214
+ # get file suffix of a path
215
+ def file_suffix(path)
216
+ path = file_name(path)
217
+ suffix_index = path =~ /\.[^.]+$/
218
+ return path[suffix_index, path.size - suffix_index] if suffix_index
219
+ end
220
+
221
+ # get directory name of a path
157
222
  def file_dir(path)
158
223
  File.dirname(abs(path))
159
224
  end
@@ -165,80 +230,75 @@ end
165
230
 
166
231
  # get full path of symlink
167
232
  def symlink_path(symlink)
168
- shell("readlink \"#{abs(symlink)}\" 2>&1").strip
233
+ _, out, = shell("readlink \"#{abs(symlink)}\" 2>&1")
234
+ out.strip
235
+ end
236
+
237
+ # calculate SHA2 digest for a file
238
+ def sha2(file)
239
+ Digest::SHA256.hexdigest read(file)
169
240
  end
170
241
 
171
242
  # make directory including intermediate directories
172
243
  def mkdir(path)
173
244
  path = abs(path)
174
- unless dir_exists?(path)
175
- out = shell("mkdir -p \"#{path}\" 2>&1")
176
- unless $shell_return_value.success?
177
- STDERR.puts out
178
- STDERR.puts "Couldn't create directory: #{path}"
179
- exit 1
180
- end
181
- puts "Created directory: #{path}"
245
+ return if dir_exists?(path)
246
+
247
+ exit_code, out, = shell("mkdir -p \"#{path}\" 2>&1")
248
+ unless exit_code.success?
249
+ error out
250
+ fail "Couldn't create directory", path
182
251
  end
252
+ info 'Created directory', path
183
253
  end
184
254
 
185
255
  # remove file/directory
186
- def rm(path,recursive: false)
256
+ def rm(path, recursive: false)
187
257
  path = abs(path)
188
- if recursive
189
- recursive = '-R '
190
- else
191
- recursive = ''
192
- end
258
+ recursive = recursive ? '-R ' : ''
193
259
  is_dir = dir_exists?(path)
194
260
  is_symlink = symlink?(path)
195
- out = shell("rm #{recursive}-f \"#{path}\" 2>&1")
196
- unless $shell_return_value.success?
197
- STDERR.puts out
198
- STDERR.puts "Couldn't remove path: #{path}"
199
- exit 1
261
+ exit_code, out, = shell("rm #{recursive}-f \"#{path}\" 2>&1")
262
+ unless exit_code.success?
263
+ error out
264
+ fail "Couldn't remove path", path
200
265
  end
201
266
  if is_dir
202
- puts "Removed directory: #{path}"
267
+ info 'Removed directory', path
203
268
  elsif is_symlink
204
- puts "Removed symlink: #{path}"
269
+ info 'Removed symlink', path
205
270
  else
206
- puts "Removed file: #{path}"
271
+ info 'Removed file', path
207
272
  end
208
273
  end
209
274
 
210
275
  # remove directory recursively
211
276
  def rmdir(path)
212
- rm(path,recursive: true)
277
+ rm(path, recursive: true)
213
278
  end
214
279
 
215
280
  # touch file
216
281
  def touch(path)
217
282
  path = abs(path)
218
- unless file_exists?(path)
219
- out = shell("touch \"#{path}\" 2>&1")
220
- unless $shell_return_value.success?
221
- STDERR.puts out
222
- STDERR.puts "Couldn't touch file: #{path}"
223
- exit 1
224
- end
225
- puts "Touched file: #{path}"
283
+ return if file_exists?(path)
284
+
285
+ exit_code, out, = shell("touch \"#{path}\" 2>&1")
286
+ unless exit_code.success?
287
+ error out
288
+ fail "Couldn't touch file", path
226
289
  end
290
+ info 'Touched file', path
227
291
  end
228
292
 
229
293
  # change file permissions
230
- def chmod(path,permissions,recursive: false)
294
+ def chmod(path, permissions, recursive: false)
231
295
  path = abs(path)
232
- if recursive
233
- recursive = '-R '
234
- else
235
- recursive = ''
236
- end
296
+ recursive = recursive ? '-R ' : ''
237
297
  shell("chmod #{recursive}#{permissions} \"#{path}\"")
238
298
  end
239
299
 
240
300
  # symlink path
241
- def symlink(from,to)
301
+ def symlink(from, to)
242
302
  from = abs(from)
243
303
  to = abs(to)
244
304
  if symlink?(from)
@@ -247,187 +307,216 @@ def symlink(from,to)
247
307
  symlink from, to
248
308
  end
249
309
  elsif file_exists?(from)
250
- STDERR.puts "File #{from} exists in place of symlink..."
251
- exit 1
310
+ fail "File #{from} exists in place of symlink..."
252
311
  elsif dir_exists?(from)
253
- STDERR.puts "Directory #{from} exists in place of symlink..."
254
- exit 1
312
+ fail "Directory #{from} exists in place of symlink..."
255
313
  else
256
- out = shell("ln -s \"#{to}\" \"#{from}\" 2>&1")
257
- unless $shell_return_value.success?
258
- STDERR.puts out
259
- STDERR.puts "Couldn't create symlink: #{from} → #{to}"
260
- exit 1
314
+ exit_code, out, = shell("ln -s \"#{to}\" \"#{from}\" 2>&1")
315
+ unless exit_code.success?
316
+ error out
317
+ fail "Couldn't create symlink", "#{from} → #{to}"
261
318
  end
262
- puts "Created symlink: #{from} → #{to}"
319
+ info 'Created symlink', "#{from} → #{to}"
263
320
  end
264
321
  end
265
322
 
266
323
  # git clone url into a path
267
- def git(url,path,branch: nil,silent: false,depth: nil)
324
+ def git(url, path, branch: nil, silent: true, depth: nil)
268
325
  path = abs(path)
269
326
  if dir_exists?(path) && dir_exists?("#{path}/.git")
270
- cwd = pwd()
327
+ cwd = pwd
271
328
  chdir path
272
329
  out = nil
273
330
  if branch
274
- out = shell('git fetch 2>&1',{:silent => silent})
275
- if out.size > 0
276
- puts out if silent
277
- end
278
- out = shell("git checkout #{branch} 2>&1",{:silent => silent})
331
+ _, out, = shell('git fetch 2>&1', silent: silent)
332
+ puts out if silent && out.size.positive?
333
+ _, out, = shell("git checkout #{branch} 2>&1", silent: silent)
279
334
  unless out.downcase.include? 'is now at'
280
335
  puts out if silent
281
336
  end
282
- out = shell("git pull origin #{branch} 2>&1",{:silent => silent})
337
+ _, out, = shell("git pull origin #{branch} 2>&1", silent: silent)
283
338
  else
284
- out = shell('git pull 2>&1',{:silent => silent})
339
+ _, out, = shell('git pull 2>&1', silent: silent)
285
340
  end
286
- unless out.downcase.include? "already up-to-date"
341
+ unless out.downcase.include? 'already up'
287
342
  puts out if silent
288
- puts "Git repository pulled: #{url} → #{path}"
343
+ info 'Git repository pulled', "#{url} → #{path}"
289
344
  end
290
345
  chdir cwd
291
346
  else
292
- if branch
293
- branch = "-b #{branch} "
294
- else
295
- branch = ''
296
- end
297
- if depth
298
- depth = '--depth '+depth.to_s+' '
299
- else
300
- depth = ''
301
- end
302
- out = shell("git clone #{depth}#{branch}#{url} \"#{path}\" 2>&1",{:silent => silent})
347
+ branch = branch ? "-b #{branch} " : ''
348
+ depth = depth ? "--depth #{depth} " : ''
349
+ _, out, = shell("git clone #{depth}#{branch}#{url} \"#{path}\" 2>&1",
350
+ silent: silent)
303
351
  puts out if silent
304
- puts "Git repository cloned: #{url} → #{path}"
352
+ info 'Git repository cloned', "#{url} → #{path}"
305
353
  end
306
354
  end
307
355
 
308
356
  # download url into a path using curl
309
- def curl(url,path, verbose: false)
357
+ def curl(url, path, content_length_check: false, verbose: false)
310
358
  path = abs(path)
311
- puts "Downloading #{url} → #{path}"
312
- output = shell("curl -s -S \"#{url}\"",verbose: verbose, silent: true)
313
- unless $shell_return_value.success?
314
- STDERR.puts output
315
- STDERR.puts "Couldn't download file from #{url}..."
316
- exit 1
359
+ if content_length_check and file_exists?(path)
360
+ content_length = curl_headers(url, verbose: verbose)['content-length'].to_i
361
+ return if file_size(path) == content_length
362
+ end
363
+ info 'Downloading', "#{url} #{path}"
364
+ exit_code, output, = shell("curl -s -S \"#{url}\"",
365
+ verbose: verbose, silent: true)
366
+ unless exit_code.success?
367
+ error output
368
+ fail "Couldn't download file from", url
317
369
  end
318
- File.write(path,output)
319
- puts "Downloaded #{url} → #{path}"
370
+ File.write(path, output)
371
+ info 'Downloaded', "#{url} → #{path}"
372
+ end
373
+
374
+ # get url response headers as Hash using curl
375
+ def curl_headers(url, method: 'HEAD', verbose: false)
376
+ exit_code, output, = shell("curl -I -X #{method} -s -S \"#{url}\"",
377
+ verbose: verbose, silent: true)
378
+ unless exit_code.success?
379
+ error output
380
+ fail "Couldn't get headers from", url
381
+ end
382
+ headers = {}
383
+ output.scan(/^(?!HTTP)([^:]+):(.*)$/).each do |m|
384
+ headers[m[0].strip.downcase] = m[1].sub("\r","").strip
385
+ end
386
+ headers
320
387
  end
321
388
 
322
389
  # read content of a file
323
390
  def read(file)
324
391
  file = abs(file)
325
- if file_exists?(file)
326
- return File.read(file)
327
- else
328
- STDERR.puts "Couldn't read file #{file}..."
329
- exit 1
330
- end
392
+ return File.read(file) if file_exists?(file)
393
+
394
+ fail "Couldn't read file", file
331
395
  end
332
396
 
333
397
  # write content into a file
334
- def write(file,content)
398
+ def write(file, content)
335
399
  file = abs(file)
336
- File.write(file,content)
400
+ File.write(file, content)
337
401
  end
338
402
 
339
403
  # source file in sh/bash/zsh script
340
- def source(file,source)
404
+ def source(file, source)
341
405
  file = abs(file)
342
406
  source = abs(source)
343
407
  source_line = "source \"#{source}\""
344
- out = shell("grep -Fx '#{source_line}' \"#{file}\"")
345
- if out == ""
346
- shell("echo '#{source_line}' >> \"#{file}\"")
347
- puts "Added source #{source} line to #{file}"
348
- end
408
+ _, out, = shell("grep -Fx '#{source_line}' \"#{file}\"")
409
+ return unless out == ''
410
+
411
+ shell("echo '#{source_line}' >> \"#{file}\"")
412
+ info 'Added source', "#{source} line to #{file}"
349
413
  end
350
414
 
351
415
  # list directory content
352
- def ls(path,dotfiles: false, grep: '', dir: true, file: true)
416
+ def ls(path, dotfiles: false, grep: '', dir: true, file: true)
353
417
  path = abs(path)
354
- if dotfiles
355
- dotfiles = '-a '
356
- else
357
- dotfiles = ''
358
- end
418
+ dotfiles = dotfiles ? '-a ' : ''
359
419
  command = "ls -1 #{dotfiles}\"#{path}\" 2>&1"
360
- if grep.length > 0
361
- command += " | grep #{grep}"
362
- end
363
- output = shell(command)
420
+ command += " | grep #{grep}" unless grep.empty?
421
+
422
+ _, output, = shell(command)
364
423
  if output.downcase.end_with?('no such file or directory')
365
- STDERR.puts "Couldn't ls directory #{path}..."
366
- exit 1
424
+ fail "Couldn't ls directory", path
367
425
  end
426
+
368
427
  ls = output.split("\n")
369
- unless dir
370
- ls = ls.keep_if {|f| !File.directory?("#{path}/#{f}") }
371
- end
372
- unless file
373
- ls = ls.keep_if {|f| !File.file?("#{path}/#{f}") }
374
- end
375
- return ls
428
+ ls = ls.keep_if { |f| !File.directory?("#{path}/#{f}") } unless dir
429
+ ls = ls.keep_if { |f| !File.file?("#{path}/#{f}") } unless file
430
+ ls
431
+ end
432
+
433
+ # compare_file
434
+ def compare_file(from, to)
435
+ from = abs(from)
436
+ to = abs(to)
437
+ FileUtils.compare_file(from, to)
438
+ end
439
+
440
+ # copy_file
441
+ def copy_file(from, to)
442
+ from = abs(from)
443
+ to = abs(to)
444
+ fail "Couldn't access file", from unless file_exists?(from)
445
+
446
+ return if file_exists?(to) && FileUtils.compare_file(from, to)
447
+
448
+ FileUtils.copy_file(from, to)
449
+ info 'Copied file', "#{from} → #{to}"
376
450
  end
377
451
 
378
452
  # copy
379
- def copy(from,to)
453
+ def copy(from, to)
380
454
  from = abs(from)
381
455
  to = abs(to)
382
456
  if dir_exists?(from)
383
- (Dir.entries(from).select { |f| f != "." and f != ".." }).each do |f|
457
+ dir_entries(from).each do |f|
384
458
  FileUtils.mkdir_p(to)
385
- copy("#{from}/#{f}","#{to}/#{f}")
459
+ copy("#{from}/#{f}", "#{to}/#{f}")
386
460
  end
387
461
  else
388
- unless file_exists?(from)
389
- STDERR.puts "Couldn't access file #{from}..."
390
- exit 1
462
+ copy_file(from, to)
463
+ end
464
+ end
465
+
466
+ # mirror
467
+ def mirror(from, to)
468
+ from = abs(from)
469
+ to = abs(to)
470
+ if dir_exists?(from)
471
+ from_entries = dir_entries(from)
472
+ if dir_exists?(to)
473
+ dir_entries(to).each do |f|
474
+ rm("#{to}/#{f}", recursive: true) unless from_entries.include?(f)
475
+ end
391
476
  end
392
- if !file_exists?(to) or !FileUtils.compare_file(from,to)
393
- FileUtils.copy_file(from,to)
394
- puts "Copied file: #{from} #{to}"
477
+ from_entries.each do |f|
478
+ FileUtils.mkdir_p(to)
479
+ copy("#{from}/#{f}", "#{to}/#{f}")
395
480
  end
481
+ else
482
+ copy_file(from, to)
396
483
  end
397
484
  end
398
485
 
399
486
  # process erb template into an output_file
400
- def erb(file,output_file = nil)
487
+ def erb(file, output_file = nil, monitor = true)
401
488
  file = abs(file)
402
- if output_file == nil
403
- if file.end_with?('.erb')
404
- output_file = file[0...-4]
405
- else
406
- output_file = file+'.out'
407
- end
408
- end
409
- if file_exists?(file)
410
- output = ERB.new(File.read(file)).result
411
- if File.exist?(output_file) && File.read(output_file) == output
412
- return
413
- end
414
- File.write(output_file,output)
415
- puts "Processed erb #{file} → #{output_file}"
416
- else
417
- STDERR.puts "Couldn't process erb file #{file}..."
418
- exit 1
489
+ fail 'No erb file found', file unless file_exists?(file)
490
+
491
+ if output_file.nil?
492
+ output_file = file.end_with?('.erb') ? file[0...-4] : file + '.out'
419
493
  end
494
+
495
+ $monitors[file] = proc { erb(file, output_file, false) } if monitor
496
+
497
+ out = ERB.new(File.read(file)).result
498
+ return if File.exist?(output_file) && File.read(output_file) == out
499
+
500
+ File.write(output_file, out)
501
+ info 'Processed erb', "#{file} → #{output_file}"
502
+ end
503
+
504
+ # monitor file for changes and execute proc if file changed
505
+ def monitor(file, &block)
506
+ file = abs(file)
507
+ fail 'No file found', file unless file_exists?(file)
508
+
509
+ $monitors[file] = block
420
510
  end
421
511
 
422
512
  # run configuration.rb file
423
513
  def run(file)
424
- cwd = pwd()
514
+ cwd = pwd
425
515
  file = abs(file)
426
- unless file_exists?(file)
427
- STDERR.puts "No configuration file: #{file} found"
428
- exit 1
429
- end
430
- puts "Running configuration: "+file
516
+ file = File.join(file,'configuration.rb') if not file_exists?(file) and dir_exists?(file)
517
+ fail 'No configuration file found', file unless file_exists?(file)
518
+
519
+ info 'Running configuration', file
431
520
  file_cwd = file_dir(file)
432
521
  chdir file_cwd
433
522
  load file
@@ -436,15 +525,14 @@ end
436
525
 
437
526
  # run configuration.rb file only if it exists
438
527
  def run_if_exists(file)
439
- if file_exists?(file)
440
- run file
441
- end
528
+ run file if file_exists?(file)
442
529
  end
443
530
 
444
531
  # run central configuration
445
- def central(configurations)
446
- if configurations.instance_of?(Array) && configurations.length > 0
447
- configurations.each {|configuration| run configuration }
532
+ def run_central(configurations, colored = true)
533
+ $colored = colored
534
+ if configurations.instance_of?(Array) && !configurations.empty?
535
+ configurations.each { |configuration| run configuration }
448
536
  elsif configurations.instance_of?(String)
449
537
  run configurations
450
538
  else
@@ -452,3 +540,24 @@ def central(configurations)
452
540
  end
453
541
  end
454
542
 
543
+ # run monitors
544
+ def run_monitors
545
+ info 'Monitoring files for changes (press Ctrl-C to stop)'
546
+ file_mtimes = {}
547
+ $monitors.keys.each { |f| file_mtimes[f] = File.mtime(f) }
548
+ loop do
549
+ $monitors.keys.each do |f|
550
+ file_mtime = File.mtime(f)
551
+ next if file_mtime == file_mtimes[f]
552
+
553
+ info 'File modified', f
554
+ $monitors[f].call
555
+ file_mtimes[f] = file_mtime
556
+ end
557
+ begin
558
+ sleep(0.5)
559
+ rescue Interrupt
560
+ exit 0
561
+ end
562
+ end
563
+ 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.4
4
+ version: 0.3.0
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-19 00:00:00.000000000 Z
11
+ date: 2021-01-14 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: '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