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