fsorg 0.1.0 → 0.2.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.
- 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
|