fsorg 0.0.0 → 0.2.1

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 +4 -4
  2. data/bin/fsorg +29 -0
  3. data/lib/fsorg.rb +147 -73
  4. metadata +59 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9ce898448e087a0796e866079ed09a93621e0b9f6b6b8e81f649a63b7d7946a
4
- data.tar.gz: 9b59d3e4832975ac5e9795225bfe59abc779cca86ef8a1fe2e6fa1c931ba2dd9
3
+ metadata.gz: ddcf3941d7eb4d9fc41fff221b3b178121564c52a8bfe29aabc2b30a8e0e70d7
4
+ data.tar.gz: a7b50828bdccb1a2afc8ed7911fec2c5ef6f4ebe846d86be774438c7480c8e10
5
5
  SHA512:
6
- metadata.gz: fd51f3421a42c32328df76734f08f3de123004b71480fc0358592ac154fb9cd8a16486e1545ab2aa8b99c43d43a841d154d1dedf07dab5eaf17903644eb5221d
7
- data.tar.gz: ebd92477d65bd0872a88be091bcfb8ce3b98da539ba0895dd67367a9af8d0b6813297070027056c838dd8ddc2a977a4cad42ed57ee504baeaea91a7a69919f48
6
+ metadata.gz: de9ff6a647a0640cac2cc1d7ea297834b70cc9eab702f2be37565acf340cfdaaa9d35353edd6a86979131c18686f348840d7716c597d7516613f1be5e937553f
7
+ data.tar.gz: 238a5c6b2d33689d66675ea1204b0babd74da5513471c8addd1f90263e8073680c464211d16b69bde824d11e49d7e6aa48a35cf6eb3af4f7b78653a8e4754450
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,11 @@ 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
24
  end
25
25
 
26
- def relative_to_root(path)
27
- File.join(@root_directory, path)
26
+ def from_relative_to_root(path)
27
+ @root_directory / path
28
28
  end
29
29
 
30
30
  def depth_change(line)
@@ -38,35 +38,14 @@ class Fsorg
38
38
  change
39
39
  end
40
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
41
  def preprocess
63
42
  process_front_matter
43
+ strip_comments
64
44
  desugar
65
45
  process_includes
66
46
  # TODO process_see
67
47
  process_for
68
48
  process_if
69
- turn_writes_into_runs
70
49
  turn_puts_into_runs
71
50
  ask_missing_variables
72
51
  process_root
@@ -103,15 +82,40 @@ class Fsorg
103
82
 
104
83
  def desugar
105
84
  output = []
85
+ inside_write_directive_shorthand = false
106
86
  @document.lines(chomp: true).each_with_index do |line, index|
107
87
  @current_line = index + 1
108
- if /^\}(\s*ELSE\s*\{)$/.match line
88
+ if inside_write_directive_shorthand
89
+ if line.strip == "]"
90
+ inside_write_directive_shorthand = false
91
+ output << "}"
92
+ else
93
+ output << line
94
+ end
95
+ elsif /^\}(\s*ELSE\s*\{)$/.match line.strip
109
96
  output << "}"
110
97
  output << $~[1]
98
+ elsif /^FILE\s+(?<filename>.+?)$/ =~ line.strip
99
+ output << "RUN touch #{filename.shellescape}"
111
100
  elsif /^(\s*[^{]+?\{)([^{]+?)\}$/.match line.strip
112
101
  output << $~[1]
113
102
  output << $~[2]
114
103
  output << "}"
104
+ elsif /^(?<filename>.+?)\s*(\s+\((?<permissions>#{PERMISSIONS_PATTERN.to_s})\))?\s*\[$/.match line.strip
105
+ output << "WRITE #{$~[:filename]} " + ($~[:permissions] ? "MODE #{$~[:permissions]}" : "") + " {"
106
+ inside_write_directive_shorthand = true
107
+ else
108
+ output << line
109
+ end
110
+ end
111
+ @document = output.join "\n"
112
+ end
113
+
114
+ def strip_comments
115
+ output = []
116
+ @document.lines(chomp: true).each_with_index do |line, index|
117
+ if /^(?<content>.*)#\s.*$/ =~ line
118
+ output << content
115
119
  else
116
120
  output << line
117
121
  end
@@ -128,7 +132,7 @@ class Fsorg
128
132
  filepath = @document_path.parent.join(line.sub /^INCLUDE /, "")
129
133
  included_raw = File.new(filepath).read.strip
130
134
  included_fsorg = Fsorg.new(@root_directory, @data, included_raw, filepath)
131
- included_fsorg.preprocess
135
+ included_fsorg.preprocess
132
136
  @data = included_fsorg.data.merge @data
133
137
  output += included_fsorg.document.lines(chomp: true)
134
138
  else
@@ -139,39 +143,9 @@ class Fsorg
139
143
  @document = output.join "\n"
140
144
  end
141
145
 
142
- def turn_writes_into_runs
146
+ def store_writes
143
147
  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
148
+
175
149
 
176
150
  @document = output.join "\n"
177
151
  end
@@ -181,8 +155,8 @@ class Fsorg
181
155
  @document.lines(chomp: true).each_with_index do |line, index|
182
156
  @current_line = index + 1
183
157
  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}"
158
+ output << "RUN install -D \"$DOCUMENT_DIR/#{$~[:source]}\" #{($~[:destination] || $~[:source]).shellescape}" + (if $~[:permissions]
159
+ " -m #{$~[:permissions].shellescape}"
186
160
  else
187
161
  ""
188
162
  end)
@@ -344,29 +318,121 @@ class Fsorg
344
318
  @data[:HERE]
345
319
  end
346
320
 
347
- def walk
321
+ def walk(dry_run, quiet, verbose)
348
322
  current_path = [@root_directory]
349
- current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
350
323
  @data[:HERE] = @root_directory
324
+ file_to_write = {}
325
+ inside_write_directive = -> { !file_to_write.keys.empty? }
326
+ current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
327
+
328
+ if verbose
329
+ puts "Data is #{@data.except :HERE}".light_black
330
+ end
351
331
 
352
332
  @document.lines(chomp: true).each_with_index do |line, index|
353
333
  @current_line = index + 1
354
-
334
+ @current_depth = current_path.length - 1
335
+
355
336
  @data.each do |key, value|
356
337
  line = line.gsub "{{#{key}}}", value.to_s
357
338
  end
358
339
 
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
340
+ if inside_write_directive.()
341
+ if line.strip == "}"
342
+ do_write file_to_write, dry_run, quiet
343
+ file_to_write = {}
344
+ else
345
+ file_to_write[:content] += line + "\n"
346
+ end
347
+ elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{$/.match line.strip
348
+ file_to_write = $~.named_captures.transform_keys(&:to_sym)
349
+ file_to_write[:content] = ""
363
350
  elsif /^(?<leaf>.+?)\s+\{/ =~ line.strip
364
351
  current_path << leaf
365
352
  @data[:HERE] = current_path_as_pathname.()
366
- puts "mk ".colorize(:cyan) + current_location.to_s
353
+ if verbose
354
+ puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
355
+ end
356
+ do_mkpath current_location, dry_run, quiet
357
+ elsif line.strip == "}"
358
+ current_path.pop
359
+ @data[:HERE] = current_path_as_pathname.()
360
+ if verbose
361
+ puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
362
+ end
363
+ elsif /^RUN\s+(?<command>.+?)$/ =~ line.strip
364
+ environment = {
365
+ "FSORG_ROOT" => @root_directory.to_s,
366
+ "HERE" => @data[:HERE].relative_path_from(@root_directory).to_s,
367
+ "CWD" => Dir.pwd,
368
+ "DOCUMENT_DIR" => @document_path.parent.to_s,
369
+ }
370
+ do_run command, current_location, environment, dry_run, quiet, verbose
371
+ end
372
+ end
373
+ end
374
+
375
+ def do_mkpath(path, dry_run, quiet)
376
+ unless quiet
377
+ puts "#{" " * @current_depth}+ ".cyan.bold + path.relative_path_from(@root_directory).to_s
378
+ end
379
+ unless dry_run
380
+ path.mkpath
381
+ end
382
+ end
383
+
384
+ def do_write(future_file, dry_run, quiet)
385
+ indentation = " " * @current_depth
386
+ dest = from_relative_to_root(current_location) / future_file[:destination]
387
+ unless dest.parent.relative_path_from(@root_directory).to_s == "."
388
+ do_mkpath dest.parent, dry_run, quiet
389
+ end
390
+
391
+ unless quiet
392
+ puts "#{indentation}> ".cyan.bold + dest.relative_path_from(@root_directory).to_s + (future_file[:permissions] ? " mode #{future_file[:permissions]}".yellow : "")
393
+ end
394
+ unless dry_run
395
+ dest.write deindent replace_data future_file[:content]
396
+ # Not using dest.chmod as the syntax for permissions is more than just integers,
397
+ # and matches in fact the exact syntax of chmod's argument, per the manpage, chmod(1) (line "Each MODE is of the form…")
398
+ `chmod #{future_file[:permissions]} #{dest}` if future_file[:permissions]
399
+ end
400
+ end
401
+
402
+ def replace_data(content)
403
+ content.gsub /\{\{(?<variable>[^}]+?)\}\}/ do |interpolation|
404
+ variable = interpolation[2..-3]
405
+ @data[variable.to_sym]
406
+ end
407
+ end
408
+
409
+ def do_run(command, inside, environment, dry_run, quiet, verbose)
410
+ indentation = " " * @current_depth
411
+ unless quiet
412
+ puts "#{indentation}$ ".cyan.bold + command + (verbose ? " at #{inside.relative_path_from(@root_directory)}".light_blue + " with ".light_black + (format_environment_hash environment) : "")
413
+ end
414
+ unless dry_run
415
+ stdout, stdout_w = IO.pipe
416
+ stderr, stderr_w = IO.pipe
417
+
418
+ system environment, command, { :chdir => inside.to_s, :out => stdout_w, :err => stderr_w }
419
+ stdout_w.close
420
+ stderr_w.close
421
+
422
+ stdout.read.each_line(chomp: true) do |line|
423
+ puts " " + indentation + line
424
+ end
425
+ stderr.read.each_line(chomp: true) do |line|
426
+ puts " " + indentation + line.red
367
427
  end
368
428
  end
369
429
  end
430
+
431
+ def format_environment_hash(environment)
432
+ "{ ".light_black + (environment.map do |key, value|
433
+ "$#{key}".red + "=".light_black + "\"#{value}\"".green
434
+ end.join ", ".light_black) + " }".light_black
435
+ end
370
436
  end
371
437
 
372
438
  def deindent(text)
@@ -386,6 +452,14 @@ def deindent(text)
386
452
  end.join "\n"
387
453
  end
388
454
 
389
- fsorg = Fsorg.from_command_line
390
- fsorg.preprocess
391
- fsorg.walk
455
+ def capture_output
456
+ old_stdout = $stdout
457
+ old_stderr = $stderr
458
+ $stdout = StringIO.new
459
+ $stderr = StringIO.new
460
+ yield
461
+ return $stdout.string, $stderr.string
462
+ ensure
463
+ $stdout = old_stdout
464
+ $stderr = old_stderr
465
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fsorg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ewen Le Bihan
@@ -9,7 +9,63 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2022-04-27 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: docopt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: shellwords
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: mustache
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: colorize
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.8.1
13
69
  description:
14
70
  email: hey@ewen.works
15
71
  executables:
@@ -31,7 +87,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
31
87
  requirements:
32
88
  - - ">="
33
89
  - !ruby/object:Gem::Version
34
- version: '0'
90
+ version: 3.0.0
35
91
  required_rubygems_version: !ruby/object:Gem::Requirement
36
92
  requirements:
37
93
  - - ">="