fsorg 0.0.0 → 0.2.1

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 +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
  - - ">="