fsorg 0.1.0 → 0.2.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 +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