fled 0.0.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.
- data/README.md +230 -0
- data/bin/fled +152 -0
- data/lib/dtc/utils/dsldsl.rb +259 -0
- data/lib/dtc/utils/exec.rb +134 -0
- data/lib/dtc/utils/file_visitor.rb +78 -0
- data/lib/dtc/utils/interactive_edit.rb +81 -0
- data/lib/dtc/utils/mini_select.rb +177 -0
- data/lib/dtc/utils.rb +9 -0
- data/lib/fled/file_listing.rb +260 -0
- data/lib/fled.rb +38 -0
- data/tests/helper.rb +90 -0
- data/tests/readme.rb +256 -0
- data/tests/test_operations.rb +229 -0
- metadata +59 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
|
2
|
+
require 'shellwords'
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module DTC
|
6
|
+
module Utils
|
7
|
+
# Execute a program with popen3, pipe data to-from a proc in an async loop
|
8
|
+
#
|
9
|
+
# i = 10
|
10
|
+
# Exec.run("cat",
|
11
|
+
# :input => "Initial input\n",
|
12
|
+
# :select_timeout => 1,
|
13
|
+
# :autoclose_stdin => false
|
14
|
+
# ) do |process, sout, serr, writer|
|
15
|
+
# if writer && (i -= 1) > 0 && i % 2 == 0
|
16
|
+
# puts "Writing"
|
17
|
+
# writer.call("Hello async world!\n", lambda { |*args| puts "Write complete" })
|
18
|
+
# elsif writer && i <= 0
|
19
|
+
# puts "Closing stdin"
|
20
|
+
# writer.call(nil, lambda { |*args| puts "Close complete" })
|
21
|
+
# end
|
22
|
+
# puts "Got #{sout.inspect}" if sout != ""
|
23
|
+
# end
|
24
|
+
class Exec
|
25
|
+
class << self
|
26
|
+
def sys *opts
|
27
|
+
system(opts.flatten.map {|e| Shellwords::shellescape(e.to_s)}.join(" "))
|
28
|
+
raise "External command error" unless $?.success?
|
29
|
+
end
|
30
|
+
def sys_in cwd, *opts
|
31
|
+
Dir.chdir cwd { sys(*opts) }
|
32
|
+
end
|
33
|
+
def rsys *opts
|
34
|
+
res = `#{opts.map {|e| Shellwords::shellescape(e.to_s)}.join(" ")}`
|
35
|
+
$?.success? ? res : nil
|
36
|
+
end
|
37
|
+
def rsys_in cwd, *opts
|
38
|
+
Dir.chdir cwd { rsys(*opts) }
|
39
|
+
end
|
40
|
+
def git *opts
|
41
|
+
sys(*(%w[git] + opts))
|
42
|
+
end
|
43
|
+
def git_in cwd, *opts
|
44
|
+
Dir.chdir(cwd) { git(*opts) }
|
45
|
+
end
|
46
|
+
def rgit *opts
|
47
|
+
rsys(*(%w[git] + opts))
|
48
|
+
end
|
49
|
+
def rgit_in cwd, *opts
|
50
|
+
Dir.chdir(cwd) { return rgit(*opts) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize *cmd
|
55
|
+
options = cmd.last.is_a?(Hash) ? cmd.pop() : {}
|
56
|
+
@input = options.delete(:input)
|
57
|
+
@autoclose_stdin = options.delete(:autoclose_stdin) { true }
|
58
|
+
@cmd = cmd + options.delete(:cmd) { [] }
|
59
|
+
@select_timeout = options.delete(:select_timeout) { 5 }
|
60
|
+
@running = false
|
61
|
+
@ran = false
|
62
|
+
end
|
63
|
+
attr_accessor :input
|
64
|
+
attr_accessor :cmd
|
65
|
+
attr_reader :ran, :running, :exec_time, :stdout, :stderr
|
66
|
+
def run # :yields: exec, new_stdout, new_stderr, write_proc
|
67
|
+
@start_time = Time.new
|
68
|
+
@stdout = ""
|
69
|
+
@stderr = ""
|
70
|
+
@running = true
|
71
|
+
stdin, stdout, stderr = Open3::popen3(*@cmd)
|
72
|
+
begin
|
73
|
+
stdout_read, stderr_read = "", ""
|
74
|
+
MiniSelect.run(@select_timeout) do |select|
|
75
|
+
writing = 0
|
76
|
+
write_proc = lambda do |*args|
|
77
|
+
text, callback = *args
|
78
|
+
if text.nil?
|
79
|
+
write_proc = nil
|
80
|
+
select.close(stdin) do |miniselect, event, file, error|
|
81
|
+
callback.call(miniselect, event, file, error) if callback
|
82
|
+
end
|
83
|
+
else
|
84
|
+
writing += 1
|
85
|
+
select.write(stdin, text) do |miniselect, event, file, error|
|
86
|
+
if event == :error || event == :close
|
87
|
+
writing = 0
|
88
|
+
write_proc = nil
|
89
|
+
else
|
90
|
+
writing -= 1
|
91
|
+
end
|
92
|
+
callback.call(miniselect, event, file, error) if callback
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
select.add_read(stdout) do |miniselect, event, file, data_string|
|
97
|
+
if data_string
|
98
|
+
@stdout << data_string
|
99
|
+
stdout_read << data_string
|
100
|
+
end
|
101
|
+
end
|
102
|
+
select.add_read(stderr) do |miniselect, event, file, data_string|
|
103
|
+
if data_string
|
104
|
+
@stderr << data_string
|
105
|
+
stderr_read << data_string
|
106
|
+
end
|
107
|
+
end
|
108
|
+
select.every_beat do |miniselect|
|
109
|
+
yield(self, stdout_read, stderr_read, write_proc) if block_given?
|
110
|
+
if write_proc && writing == 0 && @autoclose_stdin
|
111
|
+
select.close(stdin)
|
112
|
+
write_proc = nil
|
113
|
+
end
|
114
|
+
stdout_read, stderr_read = "", ""
|
115
|
+
end
|
116
|
+
write_proc.call(@input) if @input
|
117
|
+
end
|
118
|
+
if stdout_read != "" || stderr_read != ""
|
119
|
+
yield(self, stdout_read, stderr_read, nil) if block_given?
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
[stdin, stdout, stderr].each { |f| f.close() if f && !f.closed? }
|
123
|
+
@running = false
|
124
|
+
@ran = true
|
125
|
+
end
|
126
|
+
@exec_time = Time.new - @start_time
|
127
|
+
end
|
128
|
+
# <code>self.new(&block).run(*args)</code>
|
129
|
+
def self.run *args, &block
|
130
|
+
self.new(*args).run(&block)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module DTC
|
2
|
+
module Utils
|
3
|
+
class FileVisitor
|
4
|
+
def depth ; @folders.nil? ? 0 : @folders.count ; end
|
5
|
+
def current_path *args ; File.join(@folders + args) ; end
|
6
|
+
attr_accessor :next_visitor
|
7
|
+
def enter_folder dir
|
8
|
+
return false unless next_visitor ? next_visitor.enter_folder(dir) : true
|
9
|
+
(@folders ||= []) << dir
|
10
|
+
true
|
11
|
+
end
|
12
|
+
def visit_file name, full_path
|
13
|
+
next_visitor.visit_file(name, full_path) if next_visitor
|
14
|
+
end
|
15
|
+
def leave_folder
|
16
|
+
next_visitor.leave_folder if next_visitor
|
17
|
+
@folders.pop
|
18
|
+
end
|
19
|
+
def self.browse path, visitor, max_depth = -1
|
20
|
+
dir = Dir.new(path)
|
21
|
+
return unless visitor.enter_folder path
|
22
|
+
dir.each do |f|
|
23
|
+
full_path = File.join(path, f)
|
24
|
+
next if f == "." || f == ".."
|
25
|
+
if File.directory? full_path
|
26
|
+
self.browse(full_path, visitor, max_depth - 1) unless max_depth == 0
|
27
|
+
else
|
28
|
+
visitor.visit_file f, full_path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
visitor.leave_folder
|
32
|
+
end
|
33
|
+
end
|
34
|
+
class FilteringFileVisitor < FileVisitor
|
35
|
+
def initialize listener, options = {}
|
36
|
+
@excluded = compile_regexp(options[:excluded])
|
37
|
+
@excluded_files = compile_regexp(options[:excluded_files])
|
38
|
+
@excluded_directories = compile_regexp(options[:excluded_directories])
|
39
|
+
@included = compile_regexp(options[:included])
|
40
|
+
@included_files = compile_regexp(options[:included_files])
|
41
|
+
@included_directories = compile_regexp(options[:included_directories])
|
42
|
+
@recurse = options[:max_depth] || -1
|
43
|
+
self.next_visitor = listener
|
44
|
+
end
|
45
|
+
def enter_folder dir
|
46
|
+
return false unless include?(File.basename(dir), false)
|
47
|
+
if (result = super) && !descend?(dir)
|
48
|
+
leave_folder
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
def visit_file name, full_path
|
54
|
+
return false unless include?(name, true)
|
55
|
+
super
|
56
|
+
end
|
57
|
+
protected
|
58
|
+
def compile_regexp(rx_list)
|
59
|
+
return nil if rx_list.nil? || rx_list.reject { |e| e.length == 0 }.empty?
|
60
|
+
Regexp.union(*rx_list.map { |e| /#{e}/i })
|
61
|
+
end
|
62
|
+
def descend?(name)
|
63
|
+
@recurse == -1 || @recurse >= (depth - 1)
|
64
|
+
end
|
65
|
+
def include?(name, is_file)
|
66
|
+
can_include = (@included.nil? || @included.match(name)) &&
|
67
|
+
((is_file && (@included_files.nil? || @included_files.match(name))) ||
|
68
|
+
(!is_file && (@included_directories.nil? || @included_directories.match(name))))
|
69
|
+
if can_include
|
70
|
+
can_include = (@excluded.nil? || !@excluded.match(name)) &&
|
71
|
+
((is_file && (@excluded_files.nil? || !@excluded_files.match(name))) ||
|
72
|
+
(!is_file && (@excluded_directories.nil? || !@excluded_directories.match(name))))
|
73
|
+
end
|
74
|
+
can_include
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'shellwords'
|
4
|
+
begin
|
5
|
+
require 'platform'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
module DTC::Utils
|
10
|
+
# Copied from gem utility_belt (and modified, so get your own copy http://utilitybelt.rubyforge.org)
|
11
|
+
# Giles Bowkett, Greg Brown, and several audience members from Giles' Ruby East presentation.
|
12
|
+
class InteractiveEditor
|
13
|
+
DEBIAN_SENSIBLE_EDITOR = "/usr/bin/sensible-editor"
|
14
|
+
MACOSX_OPEN_CMD = ["open", "--wait-apps", "-e"]
|
15
|
+
WIN_START_CMD = ["start", "-w"]
|
16
|
+
XDG_OPEN = "/usr/bin/xdg-open"
|
17
|
+
def self.sensible_editor
|
18
|
+
return Shellwords::split(ENV["VISUAL"]) if ENV["VISUAL"]
|
19
|
+
return Shellwords::split(ENV["EDITOR"]) if ENV["EDITOR"]
|
20
|
+
if defined?(Platform)
|
21
|
+
return WIN_START_CMD if Platform::IMPL == :mswin
|
22
|
+
return MACOSX_OPEN_CMD if Platform::IMPL == :macosx
|
23
|
+
if Platform::IMPL == :linux
|
24
|
+
if File.executable?(XDG_OPEN)
|
25
|
+
return XDG_OPEN
|
26
|
+
end
|
27
|
+
if File.executable?(DEBIAN_SENSIBLE_EDITOR)
|
28
|
+
return DEBIAN_SENSIBLE_EDITOR
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
raise "Could not determine what editor to use. Please specify (or use platform gem)."
|
33
|
+
end
|
34
|
+
attr_accessor :editor
|
35
|
+
def initialize(editor = InteractiveEditor.sensible_editor, extension = ".yaml")
|
36
|
+
@editor = @editor == "mate" ? ["mate", "-w"] : editor
|
37
|
+
@extension = extension
|
38
|
+
@file = nil
|
39
|
+
end
|
40
|
+
def filename
|
41
|
+
@file ? @file.path : nil
|
42
|
+
end
|
43
|
+
def edit_file_interactively(filename)
|
44
|
+
Exec.sys(@editor, filename)
|
45
|
+
end
|
46
|
+
def edit_interactively(content)
|
47
|
+
unless @file
|
48
|
+
@file = Tempfile.new(["#{File.basename(__FILE__, File.extname(__FILE__))}-edit", @extension])
|
49
|
+
@file << content
|
50
|
+
@file.close
|
51
|
+
end
|
52
|
+
edit_file_interactively(@file.path)
|
53
|
+
IO::read(@file.path)
|
54
|
+
rescue Exception => error
|
55
|
+
@file.unlink
|
56
|
+
@file = nil
|
57
|
+
puts error
|
58
|
+
end
|
59
|
+
def self.edit_file(filename, editor = InteractiveEditor.sensible_editor)
|
60
|
+
editor = InteractiveEditor.new editor
|
61
|
+
editor.edit_file_interactively(filename)
|
62
|
+
rescue Exception => error
|
63
|
+
puts "# !!!" + error.inspect
|
64
|
+
raise
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
def self.edit(content, extension = ".yaml", editor = InteractiveEditor.sensible_editor)
|
68
|
+
InteractiveEditor.new(editor, extension).edit_interactively(content)
|
69
|
+
end
|
70
|
+
def self.edit_in_yaml(object, editor = InteractiveEditor.sensible_editor)
|
71
|
+
input = "# Just empty and save this document to abort !\n" + object.to_yaml
|
72
|
+
editor = InteractiveEditor.new editor
|
73
|
+
res = editor.edit_interactively(input)
|
74
|
+
YAML::load(res)
|
75
|
+
rescue Exception => error
|
76
|
+
puts "# !!!" + error.inspect
|
77
|
+
raise
|
78
|
+
return nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module DTC
|
2
|
+
module Utils
|
3
|
+
# Small select(2) implementation
|
4
|
+
class MiniSelect
|
5
|
+
BUFSIZE = 4096
|
6
|
+
def initialize &block # :yields: miniselect
|
7
|
+
@want_read = {}
|
8
|
+
@want_write = {}
|
9
|
+
@want_close = {}
|
10
|
+
@shutdown = false
|
11
|
+
@stop_when_empty = true
|
12
|
+
yield self if block_given?
|
13
|
+
end
|
14
|
+
attr_reader :shutdown
|
15
|
+
# Default: True. Stops as soon as there are no more watched fds.
|
16
|
+
attr_accessor :stop_when_empty
|
17
|
+
# Shutdown the loop after this step
|
18
|
+
def stop
|
19
|
+
@shutdown = :requested_by_stop
|
20
|
+
end
|
21
|
+
# True if no items will be read or written (which also terminates the loop)
|
22
|
+
def empty?
|
23
|
+
@want_read.empty? && @want_write.empty?
|
24
|
+
end
|
25
|
+
# Add a file to monitor for reading.
|
26
|
+
# When read events occur, provided block is yielded with event <code>:read</code>,
|
27
|
+
# upon close it is yielded with event <code>:close</code>
|
28
|
+
def add_read fd, &cb # :yields: miniselect, event, file, data_string
|
29
|
+
@want_read[fd] = cb
|
30
|
+
end
|
31
|
+
# Write to specified fd in a non-blocking manner
|
32
|
+
#
|
33
|
+
# When write is completed, provided block is yielded with event <code>:done</code>,
|
34
|
+
# upon close it is yielded with event <code>:close</code>
|
35
|
+
def write fd, data, &cb # :yields: miniselect, event, file
|
36
|
+
raise "Cannot write, FD is closed" if fd.closed?
|
37
|
+
raise "Cannot write, FD is marked to close when finished" if @want_close[fd]
|
38
|
+
(@want_write[fd] ||= []) << [data, cb]
|
39
|
+
end
|
40
|
+
# Provided block is called at every select(2) timeout or operation
|
41
|
+
def every_beat &block # :yields: miniselect
|
42
|
+
(@block ||= []) << block
|
43
|
+
end
|
44
|
+
# Provided block is called at every select(2) timeout
|
45
|
+
def every_timeout &block # :yields: miniselect
|
46
|
+
(@timeouts ||= []) << block
|
47
|
+
end
|
48
|
+
# Provided block is called at every read/write error
|
49
|
+
def every_error &block # :yields: miniselect, fd, error
|
50
|
+
(@error_handlers ||= []) << block
|
51
|
+
end
|
52
|
+
# Close the specified file, calling all callbacks as required, closing the
|
53
|
+
# descriptor, and removing them from the miniselect
|
54
|
+
def close_now fd, error = nil
|
55
|
+
event_id = error ? :error : :close
|
56
|
+
Array(@want_read.delete(fd)).each { |cb| cb.call(self, event_id, fd, error) if cb }
|
57
|
+
Array(@want_write.delete(fd)).each { |data, cb| cb.call(self, event_id, fd, error) if cb }
|
58
|
+
Array(@want_close.delete(fd)).each { |cb| cb.call(self, event_id, fd, error) }
|
59
|
+
fd.close unless fd.closed?
|
60
|
+
if error && @error_handlers
|
61
|
+
Array(@error_handlers).each { |cb| cb.call(self, fd, error) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# Write to specified fd in a non-blocking manner
|
65
|
+
#
|
66
|
+
# When write is completed, the file is closed and the provided
|
67
|
+
# block is yielded with event <code>:close</code>.
|
68
|
+
def close fd, &cb # :yields: miniselect, event, file
|
69
|
+
return if fd.closed?
|
70
|
+
@want_close[fd] = cb
|
71
|
+
if Array(@want_read[fd]).empty? && Array(@want_write[fd]).empty?
|
72
|
+
close_now fd
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# Run the select loop, return when it terminates
|
76
|
+
def run timeout = 5
|
77
|
+
while !@shutdown && run_select(timeout)
|
78
|
+
Array(@block).each { |cb| cb.call(self) } if @block
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# <code>self.new(&block).run(*args)</code>
|
82
|
+
def self.run *args, &block
|
83
|
+
self.new(&block).run(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
# Read data from fd without blocking, close on error
|
89
|
+
#
|
90
|
+
# return [is_fd_active, data_read]
|
91
|
+
def read_fd_nonblock fd
|
92
|
+
res = ""
|
93
|
+
can_goon = true
|
94
|
+
error = nil
|
95
|
+
while fd
|
96
|
+
begin
|
97
|
+
buf = fd.read_nonblock(BUFSIZE)
|
98
|
+
res << buf if buf
|
99
|
+
redo if buf && buf.length == BUFSIZE
|
100
|
+
rescue Errno::EAGAIN
|
101
|
+
break
|
102
|
+
rescue EOFError => e
|
103
|
+
can_goon = false
|
104
|
+
break
|
105
|
+
rescue SystemCallError => e
|
106
|
+
can_goon = false
|
107
|
+
error = e
|
108
|
+
break
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@want_read[fd].call(self, :read, fd, res) if res != ""
|
112
|
+
close_now(fd, error) unless can_goon
|
113
|
+
[can_goon, res]
|
114
|
+
end
|
115
|
+
# Write data to fd in a non blocking way, close on error
|
116
|
+
#
|
117
|
+
# return [is_fd_active, unwritten_data]
|
118
|
+
def write_fd_nonblock fd, data, cb
|
119
|
+
len = 0
|
120
|
+
can_goon = true
|
121
|
+
error = nil
|
122
|
+
if fd
|
123
|
+
begin
|
124
|
+
wlen = fd.write_nonblock(data)
|
125
|
+
len += wlen if wlen
|
126
|
+
rescue Errno::EAGAIN
|
127
|
+
rescue SystemCallError => e
|
128
|
+
error = e
|
129
|
+
can_goon = false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
complete = len == data.length
|
133
|
+
if complete
|
134
|
+
pdata, cb = @want_write[fd].shift
|
135
|
+
cb.call(self, :done, fd) if cb
|
136
|
+
elsif len > 0
|
137
|
+
@want_write[fd].first[0] = data[len..-1]
|
138
|
+
end
|
139
|
+
close_now(fd, error) unless can_goon
|
140
|
+
[can_goon, can_goon && complete]
|
141
|
+
end
|
142
|
+
# run one call to select, and resulting fd operations
|
143
|
+
def run_select timeout
|
144
|
+
if @stop_when_empty && empty?
|
145
|
+
@shutdown = :no_active_fd
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
result = select(@want_read.keys, @want_write.keys,
|
149
|
+
(@want_read.keys + @want_write.keys + @want_close.keys).uniq, timeout)
|
150
|
+
if !result
|
151
|
+
Array(@timeouts).each { |cb| cb.call(self) } if @timeouts
|
152
|
+
return true
|
153
|
+
end
|
154
|
+
r, w, e = *result
|
155
|
+
r.each do |readable|
|
156
|
+
can_retry, data = read_fd_nonblock(readable)
|
157
|
+
end
|
158
|
+
w.each do |writable|
|
159
|
+
queue = @want_write[writable]
|
160
|
+
while !queue.empty?
|
161
|
+
can_retry, is_write_complete = write_fd_nonblock(writable, *queue.first)
|
162
|
+
break unless can_retry && is_write_complete
|
163
|
+
end
|
164
|
+
if queue.empty?
|
165
|
+
@want_write.delete(writable)
|
166
|
+
close_now writable if @want_close[writable]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
e.each do |erroredified|
|
170
|
+
puts "#{erroredified} error"
|
171
|
+
close erroredified
|
172
|
+
end
|
173
|
+
true
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/dtc/utils.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
module DTC
|
2
|
+
module Utils
|
3
|
+
autoload :Exec, 'dtc/utils/exec'
|
4
|
+
autoload :InteractiveEditor, 'dtc/utils/interactive_edit'
|
5
|
+
autoload :MiniSelect, 'dtc/utils/mini_select'
|
6
|
+
autoload :DSLDSL, 'dtc/utils/dsldsl'
|
7
|
+
autoload :FileVisitor, 'dtc/utils/file_visitor'
|
8
|
+
end
|
9
|
+
end
|