fsorg 0.0.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 +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: []
|