fsorg 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/fsorg +4 -0
  3. data/lib/fsorg.rb +391 -0
  4. 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "fsorg"
3
+
4
+
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: []