fsorg 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/fsorg +29 -0
- data/lib/fsorg.rb +153 -63
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8de7035273a930e84018355538d364b68016be1d6dc015269e00867a6cb0b72b
|
4
|
+
data.tar.gz: 167a486eaa81c1315c9dab96cf5c1536277a871d40a20c3f8d5dded4d13e8e0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb062a7c0e9722ff75aee01f2703bd29b0caa54668c093bceca3e4b865940da4251c79d06b01e74456730332f586c541325033e33d1e12be5623a03fca2df9c8
|
7
|
+
data.tar.gz: 5cf687f4913525c92a17ba1c094e9b27fcdf2641fdf8aecbe35655c2b75d6b8b321b251fce5c72591b16a3f15b121dcf148872e5d64cd35568b9f1660e06c0c4
|
data/bin/fsorg
CHANGED
@@ -1,4 +1,33 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require "docopt"
|
3
|
+
require "yaml"
|
2
4
|
require "fsorg"
|
3
5
|
|
6
|
+
args = Docopt.docopt <<-DOC
|
7
|
+
Usage:
|
8
|
+
fsorg [options] <filepath>
|
9
|
+
fsorg [options] <data> <filepath>
|
4
10
|
|
11
|
+
Options:
|
12
|
+
-h --help Show this screen.
|
13
|
+
-r --root=ROOT_DIRECTORY Set the root directory.
|
14
|
+
-n --dry-run Show what would be done, but don't do it.
|
15
|
+
-q --quiet Don't show actions performed.
|
16
|
+
-v --verbose Show details about actions performed.
|
17
|
+
|
18
|
+
Actions performed:
|
19
|
+
Paths are shown relative to the root directory.
|
20
|
+
+ Create a directory
|
21
|
+
$ Run a command
|
22
|
+
> Write a file
|
23
|
+
DOC
|
24
|
+
|
25
|
+
filepath = Pathname.new args["<filepath>"]
|
26
|
+
document = File.new(filepath).read.strip
|
27
|
+
data_raw = args["<data>"] || "{}"
|
28
|
+
data = YAML.load data_raw, symbolize_names: true
|
29
|
+
root_directory = Pathname.new(args["--root"] || "")
|
30
|
+
|
31
|
+
fsorg = Fsorg.new root_directory, data, document, Pathname.new(Dir.pwd).join(filepath)
|
32
|
+
fsorg.preprocess
|
33
|
+
fsorg.walk args["--dry-run"], args["--quiet"], args["--verbose"]
|
data/lib/fsorg.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
require "docopt"
|
2
1
|
require "colorize"
|
3
2
|
require "set"
|
4
3
|
require "shellwords"
|
5
4
|
require "yaml"
|
6
5
|
|
7
|
-
PERMISSIONS_PATTERN = /[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+/
|
6
|
+
PERMISSIONS_PATTERN = /[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=]?[0-7]+/
|
8
7
|
|
9
8
|
def d(thing)
|
10
9
|
p thing
|
@@ -21,10 +20,12 @@ class Fsorg
|
|
21
20
|
@data = { :HERE => @root_directory.to_s }.merge data
|
22
21
|
@document = document
|
23
22
|
@current_line = 0
|
23
|
+
@current_depth = -1
|
24
|
+
@to_write = [] # [ { :path, :content, :permissions } ]
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
-
|
27
|
+
def from_relative_to_root(path)
|
28
|
+
@root_directory / path
|
28
29
|
end
|
29
30
|
|
30
31
|
def depth_change(line)
|
@@ -38,35 +39,15 @@ class Fsorg
|
|
38
39
|
change
|
39
40
|
end
|
40
41
|
|
41
|
-
def self.from_command_line
|
42
|
-
args = Docopt.docopt <<-DOC
|
43
|
-
Usage:
|
44
|
-
fsorg [options] <filepath>
|
45
|
-
fsorg [options] <data> <filepath>
|
46
|
-
|
47
|
-
Options:
|
48
|
-
-h --help Show this screen.
|
49
|
-
-v --version Show version.
|
50
|
-
-r --root=ROOT_DIRECTORY Set the root directory.
|
51
|
-
DOC
|
52
|
-
|
53
|
-
filepath = Pathname.new args["<filepath>"]
|
54
|
-
document = File.new(filepath).read.strip
|
55
|
-
data_raw = args["<data>"] || "{}"
|
56
|
-
data = YAML.load data_raw, symbolize_names: true
|
57
|
-
root_directory = Pathname.new(args["--root"] || "")
|
58
|
-
|
59
|
-
return Fsorg.new root_directory, data, document, Pathname.new(Dir.pwd).join(filepath)
|
60
|
-
end
|
61
|
-
|
62
42
|
def preprocess
|
63
43
|
process_front_matter
|
44
|
+
strip_comments
|
64
45
|
desugar
|
65
46
|
process_includes
|
66
47
|
# TODO process_see
|
67
48
|
process_for
|
68
49
|
process_if
|
69
|
-
|
50
|
+
store_writes
|
70
51
|
turn_puts_into_runs
|
71
52
|
ask_missing_variables
|
72
53
|
process_root
|
@@ -103,15 +84,40 @@ class Fsorg
|
|
103
84
|
|
104
85
|
def desugar
|
105
86
|
output = []
|
87
|
+
inside_write_directive_shorthand = false
|
106
88
|
@document.lines(chomp: true).each_with_index do |line, index|
|
107
89
|
@current_line = index + 1
|
108
|
-
if
|
90
|
+
if inside_write_directive_shorthand
|
91
|
+
if line.strip == "]"
|
92
|
+
inside_write_directive_shorthand = false
|
93
|
+
output << "}"
|
94
|
+
else
|
95
|
+
output << line
|
96
|
+
end
|
97
|
+
elsif /^\}(\s*ELSE\s*\{)$/.match line.strip
|
109
98
|
output << "}"
|
110
99
|
output << $~[1]
|
100
|
+
elsif /^FILE\s+(?<filename>.+?)$/ =~ line.strip
|
101
|
+
output << "RUN touch #{filename.shellescape}"
|
111
102
|
elsif /^(\s*[^{]+?\{)([^{]+?)\}$/.match line.strip
|
112
103
|
output << $~[1]
|
113
104
|
output << $~[2]
|
114
105
|
output << "}"
|
106
|
+
elsif /^(?<filename>.+?)\s*(\s+\((?<permissions>#{PERMISSIONS_PATTERN.to_s})\))?\s*\[$/.match line.strip
|
107
|
+
output << "WRITE #{$~[:filename]} " + ($~[:permissions] ? "MODE #{$~[:permissions]}" : "") + " {"
|
108
|
+
inside_write_directive_shorthand = true
|
109
|
+
else
|
110
|
+
output << line
|
111
|
+
end
|
112
|
+
end
|
113
|
+
@document = output.join "\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
def strip_comments
|
117
|
+
output = []
|
118
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
119
|
+
if /^(?<content>.*)#\s.*$/ =~ line
|
120
|
+
output << content
|
115
121
|
else
|
116
122
|
output << line
|
117
123
|
end
|
@@ -139,35 +145,23 @@ class Fsorg
|
|
139
145
|
@document = output.join "\n"
|
140
146
|
end
|
141
147
|
|
142
|
-
def
|
148
|
+
def store_writes
|
143
149
|
output = []
|
144
|
-
|
145
|
-
|
146
|
-
current_content = []
|
147
|
-
destination = nil
|
148
|
-
permissions = nil
|
150
|
+
current = {}
|
151
|
+
inside_write_directive = -> { !current.keys.empty? }
|
149
152
|
|
150
153
|
@document.lines(chomp: true).each_with_index do |line, index|
|
151
154
|
@current_line = index + 1
|
152
|
-
if inside_write_directive
|
153
|
-
if line.strip ==
|
154
|
-
|
155
|
-
|
156
|
-
content = content.shellescape.gsub('\{\{', "{{").gsub('\}\}', "}}")
|
157
|
-
output << "RUN" + (destination.include?("/") ? "mkdir -p #{Pathname.new(destination.strip).parent.to_s.shellescape}" : "") + "echo -ne #{content} > #{destination.strip.shellescape}" + (permissions ? " && chmod #{permissions} #{destination.strip.shellescape}" : "")
|
155
|
+
if inside_write_directive.()
|
156
|
+
if line.strip == "}"
|
157
|
+
@to_write << current
|
158
|
+
current = {}
|
158
159
|
else
|
159
|
-
|
160
|
+
current[:content] += line + "\n"
|
160
161
|
end
|
161
|
-
elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{
|
162
|
-
|
163
|
-
|
164
|
-
elsif /^(?<destination>.+)(\s*\((?<permissions>#{PERMISSIONS_PATTERN})\))?\s*\[$/ =~ line.strip
|
165
|
-
# Shouldn't be needed, as =~ should assign to destination, but heh, it doesn't work for some reason ¯\_(ツ)_/¯
|
166
|
-
if destination.nil?
|
167
|
-
destination = $~[:destination]
|
168
|
-
end
|
169
|
-
inside_write_directive = true
|
170
|
-
compact = true
|
162
|
+
elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{$/.match line.strip
|
163
|
+
current = $~.named_captures.transform_keys(&:to_sym)
|
164
|
+
current[:content] = ""
|
171
165
|
else
|
172
166
|
output << line
|
173
167
|
end
|
@@ -181,8 +175,8 @@ class Fsorg
|
|
181
175
|
@document.lines(chomp: true).each_with_index do |line, index|
|
182
176
|
@current_line = index + 1
|
183
177
|
if /^PUT\s+(?<source>.+?)(\s+AS\s+(?<destination>.+?))?(\s+MODE\s+(?<permissions>.+?))?$/.match(line.strip)
|
184
|
-
output << "RUN install -D \"$
|
185
|
-
" -
|
178
|
+
output << "RUN install -D \"$DOCUMENT_DIR/#{$~[:source]}\" #{($~[:destination] || $~[:source]).shellescape}" + (if $~[:permissions]
|
179
|
+
" -m #{$~[:permissions].shellescape}"
|
186
180
|
else
|
187
181
|
""
|
188
182
|
end)
|
@@ -298,7 +292,7 @@ class Fsorg
|
|
298
292
|
end
|
299
293
|
|
300
294
|
def ask_missing_variables
|
301
|
-
@document.scan /\{\{(?<variable>[^}]+?)\}\}/ do |variable|
|
295
|
+
(@document + @to_write.map { |f| f[:content] }.join(" ")).scan /\{\{(?<variable>[^}]+?)\}\}/ do |variable|
|
302
296
|
unless @data.include? variable[0].to_sym
|
303
297
|
@data[variable[0].to_sym] = :ask
|
304
298
|
end
|
@@ -344,29 +338,117 @@ class Fsorg
|
|
344
338
|
@data[:HERE]
|
345
339
|
end
|
346
340
|
|
347
|
-
def
|
341
|
+
def execute_writes(dry_run, quiet)
|
342
|
+
@to_write.each do |future_file|
|
343
|
+
do_write future_file, dry_run, quiet
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def walk(dry_run, quiet, verbose)
|
348
348
|
current_path = [@root_directory]
|
349
349
|
current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
|
350
350
|
@data[:HERE] = @root_directory
|
351
351
|
|
352
|
+
if verbose
|
353
|
+
puts "Data is #{@data.except :HERE}".light_black
|
354
|
+
end
|
355
|
+
|
352
356
|
@document.lines(chomp: true).each_with_index do |line, index|
|
353
357
|
@current_line = index + 1
|
354
|
-
|
358
|
+
@current_depth = current_path.length - 1
|
359
|
+
|
355
360
|
@data.each do |key, value|
|
356
361
|
line = line.gsub "{{#{key}}}", value.to_s
|
357
362
|
end
|
358
363
|
|
359
|
-
if /^
|
360
|
-
|
364
|
+
if /^(?<leaf>.+?)\s+\{/ =~ line.strip
|
365
|
+
current_path << leaf
|
366
|
+
@data[:HERE] = current_path_as_pathname.()
|
367
|
+
if verbose
|
368
|
+
puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
|
369
|
+
end
|
370
|
+
do_mkpath current_location, dry_run, quiet
|
361
371
|
elsif line.strip == "}"
|
362
372
|
current_path.pop
|
363
|
-
elsif /^(?<leaf>.+?)\s+\{/ =~ line.strip
|
364
|
-
current_path << leaf
|
365
373
|
@data[:HERE] = current_path_as_pathname.()
|
366
|
-
|
374
|
+
if verbose
|
375
|
+
puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
|
376
|
+
end
|
377
|
+
elsif /^RUN\s+(?<command>.+?)$/ =~ line.strip
|
378
|
+
environment = {
|
379
|
+
"FSORG_ROOT" => @root_directory.to_s,
|
380
|
+
"HERE" => @data[:HERE].relative_path_from(@root_directory).to_s,
|
381
|
+
"CWD" => Dir.pwd,
|
382
|
+
"DOCUMENT_DIR" => @document_path.parent.to_s,
|
383
|
+
}
|
384
|
+
do_run command, current_location, environment, dry_run, quiet, verbose
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
@current_depth = 0
|
389
|
+
# TODO do writes alongside other operations
|
390
|
+
puts ("Writing files " + "─" * 40).light_black
|
391
|
+
execute_writes dry_run, quiet
|
392
|
+
end
|
393
|
+
|
394
|
+
def do_mkpath(path, dry_run, quiet)
|
395
|
+
unless quiet
|
396
|
+
puts "#{" " * @current_depth}+ ".cyan.bold + path.relative_path_from(@root_directory).to_s
|
397
|
+
end
|
398
|
+
unless dry_run
|
399
|
+
path.mkpath
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
def do_write(future_file, dry_run, quiet)
|
404
|
+
dest = from_relative_to_root future_file[:destination]
|
405
|
+
do_mkpath dest.parent, dry_run, quiet
|
406
|
+
|
407
|
+
unless quiet
|
408
|
+
puts "> ".cyan.bold + dest.relative_path_from(@root_directory).to_s + (future_file[:permissions] ? " mode #{future_file[:permissions]}".yellow : "")
|
409
|
+
end
|
410
|
+
unless dry_run
|
411
|
+
dest.write replace_data future_file[:content]
|
412
|
+
# Not using dest.chmod as the syntax for permissions is more than just integers,
|
413
|
+
# and matches in fact the exact syntax of chmod's argument, per the manpage, chmod(1) (line "Each MODE is of the form…")
|
414
|
+
`chmod #{future_file[:permissions]} #{dest}` if future_file[:permissions]
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def replace_data(content)
|
419
|
+
content.gsub /\{\{(?<variable>[^}]+?)\}\}/ do |interpolation|
|
420
|
+
variable = interpolation[2..-3]
|
421
|
+
@data[variable.to_sym]
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def do_run(command, inside, environment, dry_run, quiet, verbose)
|
426
|
+
indentation = " " * @current_depth
|
427
|
+
unless quiet
|
428
|
+
puts "#{indentation}$ ".cyan.bold + command + (verbose ? " at #{inside.relative_path_from(@root_directory)}".light_blue + " with ".light_black + (format_environment_hash environment) : "")
|
429
|
+
end
|
430
|
+
unless dry_run
|
431
|
+
stdout, stdout_w = IO.pipe
|
432
|
+
stderr, stderr_w = IO.pipe
|
433
|
+
|
434
|
+
system environment, command, { :chdir => inside.to_s, :out => stdout_w, :err => stderr_w }
|
435
|
+
stdout_w.close
|
436
|
+
stderr_w.close
|
437
|
+
|
438
|
+
stdout.read.each_line(chomp: true) do |line|
|
439
|
+
puts " " + indentation + line
|
440
|
+
end
|
441
|
+
stderr.read.each_line(chomp: true) do |line|
|
442
|
+
puts " " + indentation + line.red
|
367
443
|
end
|
368
444
|
end
|
369
445
|
end
|
446
|
+
|
447
|
+
def format_environment_hash(environment)
|
448
|
+
"{ ".light_black + (environment.map do |key, value|
|
449
|
+
"$#{key}".red + "=".light_black + "\"#{value}\"".green
|
450
|
+
end.join ", ".light_black) + " }".light_black
|
451
|
+
end
|
370
452
|
end
|
371
453
|
|
372
454
|
def deindent(text)
|
@@ -386,6 +468,14 @@ def deindent(text)
|
|
386
468
|
end.join "\n"
|
387
469
|
end
|
388
470
|
|
389
|
-
|
390
|
-
|
391
|
-
|
471
|
+
def capture_output
|
472
|
+
old_stdout = $stdout
|
473
|
+
old_stderr = $stderr
|
474
|
+
$stdout = StringIO.new
|
475
|
+
$stderr = StringIO.new
|
476
|
+
yield
|
477
|
+
return $stdout.string, $stderr.string
|
478
|
+
ensure
|
479
|
+
$stdout = old_stdout
|
480
|
+
$stderr = old_stderr
|
481
|
+
end
|