fsorg 0.1.0 → 0.2.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 +4 -4
  2. data/bin/fsorg +29 -0
  3. data/lib/fsorg.rb +153 -63
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c27599263e9d7099988d7e22a4bf25025b65f1cc1add20f9ffe0b8c83c2f38fd
4
- data.tar.gz: 41a9c30f0d6c97eba90440fadb536ebbb3f1c27df23990ee7afb6582fb76816e
3
+ metadata.gz: 8de7035273a930e84018355538d364b68016be1d6dc015269e00867a6cb0b72b
4
+ data.tar.gz: 167a486eaa81c1315c9dab96cf5c1536277a871d40a20c3f8d5dded4d13e8e0f
5
5
  SHA512:
6
- metadata.gz: 8732ef572aa0947e6169839e74dee898e2fbcf42dcfe6a01506a969f8754b942fed18874ec9dab0cb2777b9cae1b5ba61b1638737804bece0a0bb4e0a838607d
7
- data.tar.gz: b9cd12b4acf2303ba5df9b122bea206712e749578c33c0b992ac2999be99f2025393106a4cd00b42119de0875f3d71e55cb1d31fc7a999f08410b7134fb0e80b
6
+ metadata.gz: eb062a7c0e9722ff75aee01f2703bd29b0caa54668c093bceca3e4b865940da4251c79d06b01e74456730332f586c541325033e33d1e12be5623a03fca2df9c8
7
+ data.tar.gz: 5cf687f4913525c92a17ba1c094e9b27fcdf2641fdf8aecbe35655c2b75d6b8b321b251fce5c72591b16a3f15b121dcf148872e5d64cd35568b9f1660e06c0c4
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,12 @@ 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
+ @to_write = [] # [ { :path, :content, :permissions } ]
24
25
  end
25
26
 
26
- def relative_to_root(path)
27
- File.join(@root_directory, path)
27
+ def from_relative_to_root(path)
28
+ @root_directory / path
28
29
  end
29
30
 
30
31
  def depth_change(line)
@@ -38,35 +39,15 @@ class Fsorg
38
39
  change
39
40
  end
40
41
 
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
42
  def preprocess
63
43
  process_front_matter
44
+ strip_comments
64
45
  desugar
65
46
  process_includes
66
47
  # TODO process_see
67
48
  process_for
68
49
  process_if
69
- turn_writes_into_runs
50
+ store_writes
70
51
  turn_puts_into_runs
71
52
  ask_missing_variables
72
53
  process_root
@@ -103,15 +84,40 @@ class Fsorg
103
84
 
104
85
  def desugar
105
86
  output = []
87
+ inside_write_directive_shorthand = false
106
88
  @document.lines(chomp: true).each_with_index do |line, index|
107
89
  @current_line = index + 1
108
- if /^\}(\s*ELSE\s*\{)$/.match line
90
+ if inside_write_directive_shorthand
91
+ if line.strip == "]"
92
+ inside_write_directive_shorthand = false
93
+ output << "}"
94
+ else
95
+ output << line
96
+ end
97
+ elsif /^\}(\s*ELSE\s*\{)$/.match line.strip
109
98
  output << "}"
110
99
  output << $~[1]
100
+ elsif /^FILE\s+(?<filename>.+?)$/ =~ line.strip
101
+ output << "RUN touch #{filename.shellescape}"
111
102
  elsif /^(\s*[^{]+?\{)([^{]+?)\}$/.match line.strip
112
103
  output << $~[1]
113
104
  output << $~[2]
114
105
  output << "}"
106
+ elsif /^(?<filename>.+?)\s*(\s+\((?<permissions>#{PERMISSIONS_PATTERN.to_s})\))?\s*\[$/.match line.strip
107
+ output << "WRITE #{$~[:filename]} " + ($~[:permissions] ? "MODE #{$~[:permissions]}" : "") + " {"
108
+ inside_write_directive_shorthand = true
109
+ else
110
+ output << line
111
+ end
112
+ end
113
+ @document = output.join "\n"
114
+ end
115
+
116
+ def strip_comments
117
+ output = []
118
+ @document.lines(chomp: true).each_with_index do |line, index|
119
+ if /^(?<content>.*)#\s.*$/ =~ line
120
+ output << content
115
121
  else
116
122
  output << line
117
123
  end
@@ -139,35 +145,23 @@ class Fsorg
139
145
  @document = output.join "\n"
140
146
  end
141
147
 
142
- def turn_writes_into_runs
148
+ def store_writes
143
149
  output = []
144
- inside_write_directive = false
145
- compact = nil
146
- current_content = []
147
- destination = nil
148
- permissions = nil
150
+ current = {}
151
+ inside_write_directive = -> { !current.keys.empty? }
149
152
 
150
153
  @document.lines(chomp: true).each_with_index do |line, index|
151
154
  @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}" : "")
155
+ if inside_write_directive.()
156
+ if line.strip == "}"
157
+ @to_write << current
158
+ current = {}
158
159
  else
159
- current_content << line
160
+ current[:content] += line + "\n"
160
161
  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
162
+ elsif /^WRITE(\s+INTO)?\s+(?<destination>.+?)(?:\s+MODE\s+(?<permissions>.+?))?\s*\{$/.match line.strip
163
+ current = $~.named_captures.transform_keys(&:to_sym)
164
+ current[:content] = ""
171
165
  else
172
166
  output << line
173
167
  end
@@ -181,8 +175,8 @@ class Fsorg
181
175
  @document.lines(chomp: true).each_with_index do |line, index|
182
176
  @current_line = index + 1
183
177
  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}"
178
+ output << "RUN install -D \"$DOCUMENT_DIR/#{$~[:source]}\" #{($~[:destination] || $~[:source]).shellescape}" + (if $~[:permissions]
179
+ " -m #{$~[:permissions].shellescape}"
186
180
  else
187
181
  ""
188
182
  end)
@@ -298,7 +292,7 @@ class Fsorg
298
292
  end
299
293
 
300
294
  def ask_missing_variables
301
- @document.scan /\{\{(?<variable>[^}]+?)\}\}/ do |variable|
295
+ (@document + @to_write.map { |f| f[:content] }.join(" ")).scan /\{\{(?<variable>[^}]+?)\}\}/ do |variable|
302
296
  unless @data.include? variable[0].to_sym
303
297
  @data[variable[0].to_sym] = :ask
304
298
  end
@@ -344,29 +338,117 @@ class Fsorg
344
338
  @data[:HERE]
345
339
  end
346
340
 
347
- def walk
341
+ def execute_writes(dry_run, quiet)
342
+ @to_write.each do |future_file|
343
+ do_write future_file, dry_run, quiet
344
+ end
345
+ end
346
+
347
+ def walk(dry_run, quiet, verbose)
348
348
  current_path = [@root_directory]
349
349
  current_path_as_pathname = -> { current_path.reduce(Pathname.new "") { |path, fragment| path.join fragment } }
350
350
  @data[:HERE] = @root_directory
351
351
 
352
+ if verbose
353
+ puts "Data is #{@data.except :HERE}".light_black
354
+ end
355
+
352
356
  @document.lines(chomp: true).each_with_index do |line, index|
353
357
  @current_line = index + 1
354
-
358
+ @current_depth = current_path.length - 1
359
+
355
360
  @data.each do |key, value|
356
361
  line = line.gsub "{{#{key}}}", value.to_s
357
362
  end
358
363
 
359
- if /^RUN\s+(?<command>.+?)$/ =~ line.strip
360
- puts "run ".colorize(:cyan) + command + " at ".colorize(:blue) + current_location.to_s.colorize(:blue)
364
+ if /^(?<leaf>.+?)\s+\{/ =~ line.strip
365
+ current_path << leaf
366
+ @data[:HERE] = current_path_as_pathname.()
367
+ if verbose
368
+ puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
369
+ end
370
+ do_mkpath current_location, dry_run, quiet
361
371
  elsif line.strip == "}"
362
372
  current_path.pop
363
- elsif /^(?<leaf>.+?)\s+\{/ =~ line.strip
364
- current_path << leaf
365
373
  @data[:HERE] = current_path_as_pathname.()
366
- puts "mk ".colorize(:cyan) + current_location.to_s
374
+ if verbose
375
+ puts "currenly #{current_path.map { |fragment| fragment.to_s }.join " -> "}".light_black
376
+ end
377
+ elsif /^RUN\s+(?<command>.+?)$/ =~ line.strip
378
+ environment = {
379
+ "FSORG_ROOT" => @root_directory.to_s,
380
+ "HERE" => @data[:HERE].relative_path_from(@root_directory).to_s,
381
+ "CWD" => Dir.pwd,
382
+ "DOCUMENT_DIR" => @document_path.parent.to_s,
383
+ }
384
+ do_run command, current_location, environment, dry_run, quiet, verbose
385
+ end
386
+ end
387
+
388
+ @current_depth = 0
389
+ # TODO do writes alongside other operations
390
+ puts ("Writing files " + "─" * 40).light_black
391
+ execute_writes dry_run, quiet
392
+ end
393
+
394
+ def do_mkpath(path, dry_run, quiet)
395
+ unless quiet
396
+ puts "#{" " * @current_depth}+ ".cyan.bold + path.relative_path_from(@root_directory).to_s
397
+ end
398
+ unless dry_run
399
+ path.mkpath
400
+ end
401
+ end
402
+
403
+ def do_write(future_file, dry_run, quiet)
404
+ dest = from_relative_to_root future_file[:destination]
405
+ do_mkpath dest.parent, dry_run, quiet
406
+
407
+ unless quiet
408
+ puts "> ".cyan.bold + dest.relative_path_from(@root_directory).to_s + (future_file[:permissions] ? " mode #{future_file[:permissions]}".yellow : "")
409
+ end
410
+ unless dry_run
411
+ dest.write replace_data future_file[:content]
412
+ # Not using dest.chmod as the syntax for permissions is more than just integers,
413
+ # and matches in fact the exact syntax of chmod's argument, per the manpage, chmod(1) (line "Each MODE is of the form…")
414
+ `chmod #{future_file[:permissions]} #{dest}` if future_file[:permissions]
415
+ end
416
+ end
417
+
418
+ def replace_data(content)
419
+ content.gsub /\{\{(?<variable>[^}]+?)\}\}/ do |interpolation|
420
+ variable = interpolation[2..-3]
421
+ @data[variable.to_sym]
422
+ end
423
+ end
424
+
425
+ def do_run(command, inside, environment, dry_run, quiet, verbose)
426
+ indentation = " " * @current_depth
427
+ unless quiet
428
+ puts "#{indentation}$ ".cyan.bold + command + (verbose ? " at #{inside.relative_path_from(@root_directory)}".light_blue + " with ".light_black + (format_environment_hash environment) : "")
429
+ end
430
+ unless dry_run
431
+ stdout, stdout_w = IO.pipe
432
+ stderr, stderr_w = IO.pipe
433
+
434
+ system environment, command, { :chdir => inside.to_s, :out => stdout_w, :err => stderr_w }
435
+ stdout_w.close
436
+ stderr_w.close
437
+
438
+ stdout.read.each_line(chomp: true) do |line|
439
+ puts " " + indentation + line
440
+ end
441
+ stderr.read.each_line(chomp: true) do |line|
442
+ puts " " + indentation + line.red
367
443
  end
368
444
  end
369
445
  end
446
+
447
+ def format_environment_hash(environment)
448
+ "{ ".light_black + (environment.map do |key, value|
449
+ "$#{key}".red + "=".light_black + "\"#{value}\"".green
450
+ end.join ", ".light_black) + " }".light_black
451
+ end
370
452
  end
371
453
 
372
454
  def deindent(text)
@@ -386,6 +468,14 @@ def deindent(text)
386
468
  end.join "\n"
387
469
  end
388
470
 
389
- fsorg = Fsorg.from_command_line
390
- fsorg.preprocess
391
- fsorg.walk
471
+ def capture_output
472
+ old_stdout = $stdout
473
+ old_stderr = $stderr
474
+ $stdout = StringIO.new
475
+ $stderr = StringIO.new
476
+ yield
477
+ return $stdout.string, $stderr.string
478
+ ensure
479
+ $stdout = old_stdout
480
+ $stderr = old_stderr
481
+ 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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ewen Le Bihan