fsorg 0.1.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/fsorg +30 -0
- data/lib/fsorg.rb +147 -73
- 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: 68068de4061b604e9b71e8134e9f17dd1d902c0a6e7c269cbfbb0c13d6dd5d24
|
4
|
+
data.tar.gz: bd84015820aedcd7a7510bdc75d17655552159ff22713a0d63f396950e36e6aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61a442489d374f4274df638384f9955323398930f6b002a550e4f288ddfffb68c1d4e596d4eb9f60c5730f57994bb64269325906ae52dec3d93153a92bb9b9d3
|
7
|
+
data.tar.gz: 293cbf041418dcd7b05fc63234ec5de5505c2e38c3efff48322d15525cc6240fb761deb380dcdec1bf51b39e92844852bcf0d656d5b2bdf118dbb1b98f80e156
|
data/bin/fsorg
CHANGED
@@ -1,4 +1,34 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require "docopt"
|
3
|
+
require "pathname"
|
4
|
+
require "yaml"
|
2
5
|
require "fsorg"
|
3
6
|
|
7
|
+
args = Docopt.docopt <<-DOC
|
8
|
+
Usage:
|
9
|
+
fsorg [options] <filepath>
|
10
|
+
fsorg [options] <data> <filepath>
|
4
11
|
|
12
|
+
Options:
|
13
|
+
-h --help Show this screen.
|
14
|
+
-r --root=ROOT_DIRECTORY Set the root directory.
|
15
|
+
-n --dry-run Show what would be done, but don't do it.
|
16
|
+
-q --quiet Don't show actions performed.
|
17
|
+
-v --verbose Show details about actions performed.
|
18
|
+
|
19
|
+
Actions performed:
|
20
|
+
Paths are shown relative to the root directory.
|
21
|
+
+ Create a directory
|
22
|
+
$ Run a command
|
23
|
+
> Write a file
|
24
|
+
DOC
|
25
|
+
|
26
|
+
filepath = Pathname.new args["<filepath>"]
|
27
|
+
document = File.new(filepath).read.strip
|
28
|
+
data_raw = args["<data>"] || "{}"
|
29
|
+
data = YAML.load data_raw, symbolize_names: true
|
30
|
+
root_directory = Pathname.new(args["--root"] || "")
|
31
|
+
|
32
|
+
fsorg = Fsorg.new root_directory, data, document, Pathname.new(Dir.pwd).join(filepath)
|
33
|
+
fsorg.preprocess
|
34
|
+
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,11 @@ 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
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
|
26
|
+
def from_relative_to_root(path)
|
27
|
+
@root_directory / path
|
28
28
|
end
|
29
29
|
|
30
30
|
def depth_change(line)
|
@@ -38,35 +38,14 @@ class Fsorg
|
|
38
38
|
change
|
39
39
|
end
|
40
40
|
|
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
41
|
def preprocess
|
63
42
|
process_front_matter
|
43
|
+
strip_comments
|
64
44
|
desugar
|
65
45
|
process_includes
|
66
46
|
# TODO process_see
|
67
47
|
process_for
|
68
48
|
process_if
|
69
|
-
turn_writes_into_runs
|
70
49
|
turn_puts_into_runs
|
71
50
|
ask_missing_variables
|
72
51
|
process_root
|
@@ -103,15 +82,40 @@ class Fsorg
|
|
103
82
|
|
104
83
|
def desugar
|
105
84
|
output = []
|
85
|
+
inside_write_directive_shorthand = false
|
106
86
|
@document.lines(chomp: true).each_with_index do |line, index|
|
107
87
|
@current_line = index + 1
|
108
|
-
if
|
88
|
+
if inside_write_directive_shorthand
|
89
|
+
if line.strip == "]"
|
90
|
+
inside_write_directive_shorthand = false
|
91
|
+
output << "}"
|
92
|
+
else
|
93
|
+
output << line
|
94
|
+
end
|
95
|
+
elsif /^\}(\s*ELSE\s*\{)$/.match line.strip
|
109
96
|
output << "}"
|
110
97
|
output << $~[1]
|
98
|
+
elsif /^FILE\s+(?<filename>.+?)$/ =~ line.strip
|
99
|
+
output << "RUN touch #{filename.shellescape}"
|
111
100
|
elsif /^(\s*[^{]+?\{)([^{]+?)\}$/.match line.strip
|
112
101
|
output << $~[1]
|
113
102
|
output << $~[2]
|
114
103
|
output << "}"
|
104
|
+
elsif /^(?<filename>.+?)\s*(\s+\((?<permissions>#{PERMISSIONS_PATTERN.to_s})\))?\s*\[$/.match line.strip
|
105
|
+
output << "WRITE #{$~[:filename]} " + ($~[:permissions] ? "MODE #{$~[:permissions]}" : "") + " {"
|
106
|
+
inside_write_directive_shorthand = true
|
107
|
+
else
|
108
|
+
output << line
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@document = output.join "\n"
|
112
|
+
end
|
113
|
+
|
114
|
+
def strip_comments
|
115
|
+
output = []
|
116
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
117
|
+
if /^(?<content>.*)#\s.*$/ =~ line
|
118
|
+
output << content
|
115
119
|
else
|
116
120
|
output << line
|
117
121
|
end
|
@@ -128,7 +132,7 @@ class Fsorg
|
|
128
132
|
filepath = @document_path.parent.join(line.sub /^INCLUDE /, "")
|
129
133
|
included_raw = File.new(filepath).read.strip
|
130
134
|
included_fsorg = Fsorg.new(@root_directory, @data, included_raw, filepath)
|
131
|
-
included_fsorg.preprocess
|
135
|
+
included_fsorg.preprocess
|
132
136
|
@data = included_fsorg.data.merge @data
|
133
137
|
output += included_fsorg.document.lines(chomp: true)
|
134
138
|
else
|
@@ -139,39 +143,9 @@ class Fsorg
|
|
139
143
|
@document = output.join "\n"
|
140
144
|
end
|
141
145
|
|
142
|
-
def
|
146
|
+
def store_writes
|
143
147
|
output = []
|
144
|
-
|
145
|
-
compact = nil
|
146
|
-
current_content = []
|
147
|
-
destination = nil
|
148
|
-
permissions = nil
|
149
|
-
|
150
|
-
@document.lines(chomp: true).each_with_index do |line, index|
|
151
|
-
@current_line = index + 1
|
152
|
-
if inside_write_directive
|
153
|
-
if line.strip == (compact ? "]" : "}")
|
154
|
-
inside_write_directive = false
|
155
|
-
content = deindent(current_content.join("\n")).gsub "\n", '\n'
|
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}" : "")
|
158
|
-
else
|
159
|
-
current_content << line
|
160
|
-
end
|
161
|
-
elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{$/ =~ line.strip
|
162
|
-
inside_write_directive = true
|
163
|
-
compact = false
|
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
|
171
|
-
else
|
172
|
-
output << line
|
173
|
-
end
|
174
|
-
end
|
148
|
+
|
175
149
|
|
176
150
|
@document = output.join "\n"
|
177
151
|
end
|
@@ -181,8 +155,8 @@ class Fsorg
|
|
181
155
|
@document.lines(chomp: true).each_with_index do |line, index|
|
182
156
|
@current_line = index + 1
|
183
157
|
if /^PUT\s+(?<source>.+?)(\s+AS\s+(?<destination>.+?))?(\s+MODE\s+(?<permissions>.+?))?$/.match(line.strip)
|
184
|
-
output << "RUN install -D \"$
|
185
|
-
" -
|
158
|
+
output << "RUN install -D \"$DOCUMENT_DIR/#{$~[:source]}\" #{($~[:destination] || $~[:source]).shellescape}" + (if $~[:permissions]
|
159
|
+
" -m #{$~[:permissions].shellescape}"
|
186
160
|
else
|
187
161
|
""
|
188
162
|
end)
|
@@ -344,29 +318,121 @@ class Fsorg
|
|
344
318
|
@data[:HERE]
|
345
319
|
end
|
346
320
|
|
347
|
-
def walk
|
321
|
+
def walk(dry_run, quiet, verbose)
|
348
322
|
current_path = [@root_directory]
|
349
|
-
current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
|
350
323
|
@data[:HERE] = @root_directory
|
324
|
+
file_to_write = {}
|
325
|
+
inside_write_directive = -> { !file_to_write.keys.empty? }
|
326
|
+
current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
|
327
|
+
|
328
|
+
if verbose
|
329
|
+
puts "Data is #{@data.except :HERE}".light_black
|
330
|
+
end
|
351
331
|
|
352
332
|
@document.lines(chomp: true).each_with_index do |line, index|
|
353
333
|
@current_line = index + 1
|
354
|
-
|
334
|
+
@current_depth = current_path.length - 1
|
335
|
+
|
355
336
|
@data.each do |key, value|
|
356
337
|
line = line.gsub "{{#{key}}}", value.to_s
|
357
338
|
end
|
358
339
|
|
359
|
-
if
|
360
|
-
|
361
|
-
|
362
|
-
|
340
|
+
if inside_write_directive.()
|
341
|
+
if line.strip == "}"
|
342
|
+
do_write file_to_write, dry_run, quiet
|
343
|
+
file_to_write = {}
|
344
|
+
else
|
345
|
+
file_to_write[:content] += line + "\n"
|
346
|
+
end
|
347
|
+
elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{$/.match line.strip
|
348
|
+
file_to_write = $~.named_captures.transform_keys(&:to_sym)
|
349
|
+
file_to_write[:content] = ""
|
363
350
|
elsif /^(?<leaf>.+?)\s+\{/ =~ line.strip
|
364
351
|
current_path << leaf
|
365
352
|
@data[:HERE] = current_path_as_pathname.()
|
366
|
-
|
353
|
+
if verbose
|
354
|
+
puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
|
355
|
+
end
|
356
|
+
do_mkpath current_location, dry_run, quiet
|
357
|
+
elsif line.strip == "}"
|
358
|
+
current_path.pop
|
359
|
+
@data[:HERE] = current_path_as_pathname.()
|
360
|
+
if verbose
|
361
|
+
puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
|
362
|
+
end
|
363
|
+
elsif /^RUN\s+(?<command>.+?)$/ =~ line.strip
|
364
|
+
environment = {
|
365
|
+
"FSORG_ROOT" => @root_directory.to_s,
|
366
|
+
"HERE" => @data[:HERE].relative_path_from(@root_directory).to_s,
|
367
|
+
"CWD" => Dir.pwd,
|
368
|
+
"DOCUMENT_DIR" => @document_path.parent.to_s,
|
369
|
+
}
|
370
|
+
do_run command, current_location, environment, dry_run, quiet, verbose
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def do_mkpath(path, dry_run, quiet)
|
376
|
+
unless quiet
|
377
|
+
puts "#{" " * @current_depth}+ ".cyan.bold + path.relative_path_from(@root_directory).to_s
|
378
|
+
end
|
379
|
+
unless dry_run
|
380
|
+
path.mkpath
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def do_write(future_file, dry_run, quiet)
|
385
|
+
indentation = " " * @current_depth
|
386
|
+
dest = from_relative_to_root(current_location) / future_file[:destination]
|
387
|
+
unless dest.parent.relative_path_from(@root_directory).to_s == "."
|
388
|
+
do_mkpath dest.parent, dry_run, quiet
|
389
|
+
end
|
390
|
+
|
391
|
+
unless quiet
|
392
|
+
puts "#{indentation}> ".cyan.bold + dest.relative_path_from(@root_directory).to_s + (future_file[:permissions] ? " mode #{future_file[:permissions]}".yellow : "")
|
393
|
+
end
|
394
|
+
unless dry_run
|
395
|
+
dest.write deindent replace_data future_file[:content]
|
396
|
+
# Not using dest.chmod as the syntax for permissions is more than just integers,
|
397
|
+
# and matches in fact the exact syntax of chmod's argument, per the manpage, chmod(1) (line "Each MODE is of the form…")
|
398
|
+
`chmod #{future_file[:permissions]} #{dest}` if future_file[:permissions]
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def replace_data(content)
|
403
|
+
content.gsub /\{\{(?<variable>[^}]+?)\}\}/ do |interpolation|
|
404
|
+
variable = interpolation[2..-3]
|
405
|
+
@data[variable.to_sym]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def do_run(command, inside, environment, dry_run, quiet, verbose)
|
410
|
+
indentation = " " * @current_depth
|
411
|
+
unless quiet
|
412
|
+
puts "#{indentation}$ ".cyan.bold + command + (verbose ? " at #{inside.relative_path_from(@root_directory)}".light_blue + " with ".light_black + (format_environment_hash environment) : "")
|
413
|
+
end
|
414
|
+
unless dry_run
|
415
|
+
stdout, stdout_w = IO.pipe
|
416
|
+
stderr, stderr_w = IO.pipe
|
417
|
+
|
418
|
+
system environment, command, { :chdir => inside.to_s, :out => stdout_w, :err => stderr_w }
|
419
|
+
stdout_w.close
|
420
|
+
stderr_w.close
|
421
|
+
|
422
|
+
stdout.read.each_line(chomp: true) do |line|
|
423
|
+
puts " " + indentation + line
|
424
|
+
end
|
425
|
+
stderr.read.each_line(chomp: true) do |line|
|
426
|
+
puts " " + indentation + line.red
|
367
427
|
end
|
368
428
|
end
|
369
429
|
end
|
430
|
+
|
431
|
+
def format_environment_hash(environment)
|
432
|
+
"{ ".light_black + (environment.map do |key, value|
|
433
|
+
"$#{key}".red + "=".light_black + "\"#{value}\"".green
|
434
|
+
end.join ", ".light_black) + " }".light_black
|
435
|
+
end
|
370
436
|
end
|
371
437
|
|
372
438
|
def deindent(text)
|
@@ -386,6 +452,14 @@ def deindent(text)
|
|
386
452
|
end.join "\n"
|
387
453
|
end
|
388
454
|
|
389
|
-
|
390
|
-
|
391
|
-
|
455
|
+
def capture_output
|
456
|
+
old_stdout = $stdout
|
457
|
+
old_stderr = $stderr
|
458
|
+
$stdout = StringIO.new
|
459
|
+
$stderr = StringIO.new
|
460
|
+
yield
|
461
|
+
return $stdout.string, $stderr.string
|
462
|
+
ensure
|
463
|
+
$stdout = old_stdout
|
464
|
+
$stderr = old_stderr
|
465
|
+
end
|