fsorg 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/fsorg +4 -0
- data/lib/fsorg.rb +391 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b9ce898448e087a0796e866079ed09a93621e0b9f6b6b8e81f649a63b7d7946a
|
4
|
+
data.tar.gz: 9b59d3e4832975ac5e9795225bfe59abc779cca86ef8a1fe2e6fa1c931ba2dd9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fd51f3421a42c32328df76734f08f3de123004b71480fc0358592ac154fb9cd8a16486e1545ab2aa8b99c43d43a841d154d1dedf07dab5eaf17903644eb5221d
|
7
|
+
data.tar.gz: ebd92477d65bd0872a88be091bcfb8ce3b98da539ba0895dd67367a9af8d0b6813297070027056c838dd8ddc2a977a4cad42ed57ee504baeaea91a7a69919f48
|
data/bin/fsorg
ADDED
data/lib/fsorg.rb
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
require "docopt"
|
2
|
+
require "colorize"
|
3
|
+
require "set"
|
4
|
+
require "shellwords"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
PERMISSIONS_PATTERN = /[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+/
|
8
|
+
|
9
|
+
def d(thing)
|
10
|
+
p thing
|
11
|
+
return thing
|
12
|
+
end
|
13
|
+
|
14
|
+
class Fsorg
|
15
|
+
attr_accessor :data, :document, :document_path, :root_directory
|
16
|
+
|
17
|
+
def initialize(root_directory, data, document, absolute_path)
|
18
|
+
@root_directory = root_directory
|
19
|
+
@shell = "/usr/bin/env bash"
|
20
|
+
@document_path = absolute_path
|
21
|
+
@data = { :HERE => @root_directory.to_s }.merge data
|
22
|
+
@document = document
|
23
|
+
@current_line = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def relative_to_root(path)
|
27
|
+
File.join(@root_directory, path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def depth_change(line)
|
31
|
+
change = 0
|
32
|
+
if /\}$/ =~ line
|
33
|
+
change -= 1
|
34
|
+
end
|
35
|
+
if /^\{/ =~ line
|
36
|
+
change += 1
|
37
|
+
end
|
38
|
+
change
|
39
|
+
end
|
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
|
+
def preprocess
|
63
|
+
process_front_matter
|
64
|
+
desugar
|
65
|
+
process_includes
|
66
|
+
# TODO process_see
|
67
|
+
process_for
|
68
|
+
process_if
|
69
|
+
turn_writes_into_runs
|
70
|
+
turn_puts_into_runs
|
71
|
+
ask_missing_variables
|
72
|
+
process_root
|
73
|
+
process_shell
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_front_matter
|
77
|
+
unless /^-+$/ =~ @document.lines(chomp: true).first
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
# Remove front matter from document
|
82
|
+
front_matter_raw, rest = "", ""
|
83
|
+
inside_front_matter = false
|
84
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
85
|
+
@current_linecur = index + 1
|
86
|
+
if /^-+$/ =~ line
|
87
|
+
inside_front_matter = !inside_front_matter
|
88
|
+
next
|
89
|
+
end
|
90
|
+
|
91
|
+
if inside_front_matter
|
92
|
+
front_matter_raw += line + "\n"
|
93
|
+
else
|
94
|
+
rest += line + "\n"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
front_matter = YAML.load(front_matter_raw, symbolize_names: true) or {}
|
99
|
+
|
100
|
+
@data = front_matter.merge @data
|
101
|
+
@document = rest
|
102
|
+
end
|
103
|
+
|
104
|
+
def desugar
|
105
|
+
output = []
|
106
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
107
|
+
@current_line = index + 1
|
108
|
+
if /^\}(\s*ELSE\s*\{)$/.match line
|
109
|
+
output << "}"
|
110
|
+
output << $~[1]
|
111
|
+
elsif /^(\s*[^{]+?\{)([^{]+?)\}$/.match line.strip
|
112
|
+
output << $~[1]
|
113
|
+
output << $~[2]
|
114
|
+
output << "}"
|
115
|
+
else
|
116
|
+
output << line
|
117
|
+
end
|
118
|
+
end
|
119
|
+
@document = output.join "\n"
|
120
|
+
end
|
121
|
+
|
122
|
+
def process_includes
|
123
|
+
output = []
|
124
|
+
|
125
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
126
|
+
@current_line = index + 1
|
127
|
+
if line.start_with? "INCLUDE "
|
128
|
+
filepath = @document_path.parent.join(line.sub /^INCLUDE /, "")
|
129
|
+
included_raw = File.new(filepath).read.strip
|
130
|
+
included_fsorg = Fsorg.new(@root_directory, @data, included_raw, filepath)
|
131
|
+
included_fsorg.preprocess
|
132
|
+
@data = included_fsorg.data.merge @data
|
133
|
+
output += included_fsorg.document.lines(chomp: true)
|
134
|
+
else
|
135
|
+
output << line
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
@document = output.join "\n"
|
140
|
+
end
|
141
|
+
|
142
|
+
def turn_writes_into_runs
|
143
|
+
output = []
|
144
|
+
inside_write_directive = false
|
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
|
175
|
+
|
176
|
+
@document = output.join "\n"
|
177
|
+
end
|
178
|
+
|
179
|
+
def turn_puts_into_runs
|
180
|
+
output = []
|
181
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
182
|
+
@current_line = index + 1
|
183
|
+
if /^PUT\s+(?<source>.+?)(\s+AS\s+(?<destination>.+?))?(\s+MODE\s+(?<permissions>.+?))?$/.match(line.strip)
|
184
|
+
output << "RUN install -D \"$FSORG_ROOT/#{$~[:source]}\" #{($~[:destination] || $~[:source]).shellescape}" + (if $~[:permissions]
|
185
|
+
" -D #{$~[:permissions].shellescape}"
|
186
|
+
else
|
187
|
+
""
|
188
|
+
end)
|
189
|
+
else
|
190
|
+
output << line
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
@document = output.join "\n"
|
195
|
+
end
|
196
|
+
|
197
|
+
def process_for
|
198
|
+
output = []
|
199
|
+
inside_for_directive = false
|
200
|
+
body = []
|
201
|
+
args = nil
|
202
|
+
current_depth = 0
|
203
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
204
|
+
@current_line = index + 1
|
205
|
+
if inside_for_directive
|
206
|
+
current_depth += depth_change line.strip
|
207
|
+
if current_depth == 0
|
208
|
+
inside_for_directive = false
|
209
|
+
output += repeat_for_each(args[:iteratee], args[:iterator], body)
|
210
|
+
else
|
211
|
+
body << line
|
212
|
+
end
|
213
|
+
elsif /^FOR\s+(?<iteratee>\w+)\s+IN\s+(?<iterator>.+?)\s*\{$/.match(line.strip)
|
214
|
+
args = $~
|
215
|
+
current_depth = 1
|
216
|
+
body = []
|
217
|
+
inside_for_directive = true
|
218
|
+
else
|
219
|
+
output << line
|
220
|
+
end
|
221
|
+
@document = output.join "\n"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def repeat_for_each(iteratee, iterator, directives)
|
226
|
+
output = []
|
227
|
+
unless data[iterator.to_sym]
|
228
|
+
raise "[#{@document_path}:#{@current_line}]".colorize :red + "Variable '#{iterator}' not found (iterators cannot be asked for interactively). Available variables at this point: #{data.keys.join(", ")}."
|
229
|
+
end
|
230
|
+
unless data[iterator.to_sym].is_a? Array
|
231
|
+
raise "[#{@document_path}:#{@current_line}]".colorize :red + "Cannot iterate over '#{iterator}', which is of type #{data[iterator.to_sym].class}."
|
232
|
+
end
|
233
|
+
data[iterator.to_sym].each do |item|
|
234
|
+
output += directives.map do |directive|
|
235
|
+
directive.gsub "{{#{iteratee}}}", item.to_s
|
236
|
+
end
|
237
|
+
end
|
238
|
+
output
|
239
|
+
end
|
240
|
+
|
241
|
+
def process_if
|
242
|
+
output = []
|
243
|
+
inside_if = false
|
244
|
+
body_if = []
|
245
|
+
inside_else = false
|
246
|
+
body_else = []
|
247
|
+
current_condition = nil
|
248
|
+
current_depth = 0
|
249
|
+
|
250
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
251
|
+
@current_line = index + 1
|
252
|
+
if inside_if
|
253
|
+
current_depth += depth_change line.strip
|
254
|
+
if current_depth == 0
|
255
|
+
inside_if = false
|
256
|
+
inside_else = false
|
257
|
+
output += body_if if evaluates_to_true? current_condition
|
258
|
+
else
|
259
|
+
body_if << line
|
260
|
+
end
|
261
|
+
elsif inside_else
|
262
|
+
current_depth += depth_change line.strip
|
263
|
+
if current_depth == 0
|
264
|
+
inside_else = false
|
265
|
+
inside_if = false
|
266
|
+
output += body_else if evaluates_to_false? current_condition
|
267
|
+
current_condition = nil
|
268
|
+
else
|
269
|
+
body_else << line
|
270
|
+
end
|
271
|
+
elsif /^IF\s+(?<condition>.+?)\s*\{$/.match(line.strip)
|
272
|
+
current_condition = $~[:condition]
|
273
|
+
current_depth = 1
|
274
|
+
inside_if = true
|
275
|
+
elsif /^(\}\s*)?ELSE\s*\{$/.match(line.strip)
|
276
|
+
if current_condition.nil?
|
277
|
+
raise "[#{@document_path}:#{@current_line}] Cannot use ELSE without IF."
|
278
|
+
end
|
279
|
+
inside_else = true
|
280
|
+
current_depth = 1
|
281
|
+
else
|
282
|
+
output << line
|
283
|
+
end
|
284
|
+
@document = output.join "\n"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def evaluates_to_false?(condition)
|
289
|
+
!evaluates_to_true? condition
|
290
|
+
end
|
291
|
+
|
292
|
+
def evaluates_to_true?(condition)
|
293
|
+
unless @data.include? condition.to_sym
|
294
|
+
raise "[#{@document_path}:#{@current_line}] Variable '#{condition}' not found. Available variables at this point: #{data.keys.join(", ")}."
|
295
|
+
end
|
296
|
+
|
297
|
+
@data[condition.to_sym]
|
298
|
+
end
|
299
|
+
|
300
|
+
def ask_missing_variables
|
301
|
+
@document.scan /\{\{(?<variable>[^}]+?)\}\}/ do |variable|
|
302
|
+
unless @data.include? variable[0].to_sym
|
303
|
+
@data[variable[0].to_sym] = :ask
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
@data.each do |key, value|
|
308
|
+
if value == :ask
|
309
|
+
print "#{key}? "
|
310
|
+
@data[key] = YAML.load STDIN.gets.chomp
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def process_root
|
316
|
+
@document = @document.lines(chomp: true).map.with_index do |line, index|
|
317
|
+
@current_line = index + 1
|
318
|
+
if /^ROOT\s+(?<root>.+?)$/.match line.strip
|
319
|
+
@root_directory = if $~[:root].start_with? "/"
|
320
|
+
Pathname.new $~[:root]
|
321
|
+
else
|
322
|
+
@root_directory.join $~[:root]
|
323
|
+
end
|
324
|
+
""
|
325
|
+
else
|
326
|
+
line
|
327
|
+
end
|
328
|
+
end.join "\n"
|
329
|
+
end
|
330
|
+
|
331
|
+
def process_shell
|
332
|
+
@document = @document.lines(chomp: true).map.with_index do |line, index|
|
333
|
+
@current_line = index + 1
|
334
|
+
if /^SHELL\s+(?<shell>.+?)$/.match line.strip
|
335
|
+
@shell = $~[:shell]
|
336
|
+
""
|
337
|
+
else
|
338
|
+
line
|
339
|
+
end
|
340
|
+
end.join "\n"
|
341
|
+
end
|
342
|
+
|
343
|
+
def current_location
|
344
|
+
@data[:HERE]
|
345
|
+
end
|
346
|
+
|
347
|
+
def walk
|
348
|
+
current_path = [@root_directory]
|
349
|
+
current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
|
350
|
+
@data[:HERE] = @root_directory
|
351
|
+
|
352
|
+
@document.lines(chomp: true).each_with_index do |line, index|
|
353
|
+
@current_line = index + 1
|
354
|
+
|
355
|
+
@data.each do |key, value|
|
356
|
+
line = line.gsub "{{#{key}}}", value.to_s
|
357
|
+
end
|
358
|
+
|
359
|
+
if /^RUN\s+(?<command>.+?)$/ =~ line.strip
|
360
|
+
puts "run ".colorize(:cyan) + command + " at ".colorize(:blue) + current_location.to_s.colorize(:blue)
|
361
|
+
elsif line.strip == "}"
|
362
|
+
current_path.pop
|
363
|
+
elsif /^(?<leaf>.+?)\s+\{/ =~ line.strip
|
364
|
+
current_path << leaf
|
365
|
+
@data[:HERE] = current_path_as_pathname.()
|
366
|
+
puts "mk ".colorize(:cyan) + current_location.to_s
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def deindent(text)
|
373
|
+
using_tabs = text.lines(chomp: true).any? { |line| /^\t/ =~ line }
|
374
|
+
indenting_with = using_tabs ? "\t" : " "
|
375
|
+
depth = text.lines.map do |line|
|
376
|
+
count = 0
|
377
|
+
until line[count] != indenting_with
|
378
|
+
count += 1
|
379
|
+
end
|
380
|
+
count
|
381
|
+
end.min
|
382
|
+
pattern = /^#{using_tabs ? '\t' : " "}{#{depth}}/
|
383
|
+
|
384
|
+
text.lines(chomp: true).map do |line|
|
385
|
+
line.sub pattern, ""
|
386
|
+
end.join "\n"
|
387
|
+
end
|
388
|
+
|
389
|
+
fsorg = Fsorg.from_command_line
|
390
|
+
fsorg.preprocess
|
391
|
+
fsorg.walk
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fsorg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ewen Le Bihan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-04-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: hey@ewen.works
|
15
|
+
executables:
|
16
|
+
- fsorg
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/fsorg
|
21
|
+
- lib/fsorg.rb
|
22
|
+
homepage: https://github.com/ewen-lbh/fsorg
|
23
|
+
licenses:
|
24
|
+
- Unlicense
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubygems_version: 3.3.8
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: Create directories from a file that describes them
|
45
|
+
test_files: []
|