prick 0.19.0 → 0.20.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -6
  3. data/README.md +7 -7
  4. data/Rakefile +3 -1
  5. data/TODO +13 -11
  6. data/bin/console +2 -1
  7. data/doc/build-yml.txt +14 -0
  8. data/exe/prick +237 -19
  9. data/lib/builder/batch.rb +147 -0
  10. data/lib/builder/builder.rb +122 -0
  11. data/lib/builder/node.rb +189 -0
  12. data/lib/builder/node_pool.rb +105 -0
  13. data/lib/builder/parser.rb +120 -0
  14. data/lib/local/command.rb +193 -0
  15. data/lib/{prick → local}/git.rb +148 -22
  16. data/lib/local/timer.rb +98 -0
  17. data/lib/prick/constants.rb +54 -66
  18. data/lib/prick/diff.rb +28 -18
  19. data/lib/prick/prick_version.rb +161 -0
  20. data/lib/prick/state.rb +80 -165
  21. data/lib/prick/version.rb +2 -163
  22. data/lib/prick.rb +38 -24
  23. data/lib/share/init/.gitignore +10 -0
  24. data/lib/share/init/.prick-context +2 -0
  25. data/lib/share/init/.rspec +3 -0
  26. data/{share/schema/schema/public → lib/share/init/migration}/.keep +0 -0
  27. data/lib/share/init/prick.yml +6 -0
  28. data/lib/share/init/schema/.keep +0 -0
  29. data/lib/share/init/schema/build.yml +2 -0
  30. data/lib/share/init/schema/prick/.keep +0 -0
  31. data/lib/share/init/schema/prick/build.yml +5 -0
  32. data/lib/share/init/schema/prick/data.sql +6 -0
  33. data/{share/schema → lib/share/init}/schema/prick/tables.sql +2 -3
  34. data/lib/share/init/schema/public/.keep +0 -0
  35. data/lib/share/init/spec/prick_helper.rb +1 -0
  36. data/lib/share/init/spec/prick_spec.rb +6 -0
  37. data/lib/share/init/spec/spec_helper.rb +50 -0
  38. data/lib/share/migrate/migration/build.yml +4 -0
  39. data/lib/share/migrate/migration/diff.after-tables.sql +0 -0
  40. data/lib/share/migrate/migration/diff.before-tables.sql +0 -0
  41. data/lib/share/migrate/migration/diff.tables.sql +0 -0
  42. data/lib/subcommand/prick-build.rb +55 -0
  43. data/lib/subcommand/prick-create.rb +78 -0
  44. data/lib/subcommand/prick-drop.rb +25 -0
  45. data/lib/subcommand/prick-fox.rb +62 -0
  46. data/lib/subcommand/prick-init.rb +46 -0
  47. data/lib/subcommand/prick-make.rb +202 -0
  48. data/lib/subcommand/prick-migrate.rb +37 -0
  49. data/lib/subcommand/prick-release.rb +23 -0
  50. data/lib/subcommand/prick-setup.rb +20 -0
  51. data/lib/subcommand/prick-teardown.rb +18 -0
  52. data/prick.gemspec +23 -31
  53. metadata +69 -78
  54. data/.gitignore +0 -29
  55. data/.travis.yml +0 -7
  56. data/doc/create_release.txt +0 -17
  57. data/doc/flow.txt +0 -98
  58. data/doc/migra +0 -1
  59. data/doc/migrations.txt +0 -172
  60. data/doc/notes.txt +0 -116
  61. data/doc/prick.txt +0 -114
  62. data/doc/sh.prick +0 -316
  63. data/lib/ext/algorithm.rb +0 -14
  64. data/lib/ext/fileutils.rb +0 -26
  65. data/lib/ext/forward_method.rb +0 -18
  66. data/lib/ext/pg.rb +0 -18
  67. data/lib/ext/shortest_path.rb +0 -44
  68. data/lib/prick/archive.rb +0 -124
  69. data/lib/prick/branch.rb +0 -265
  70. data/lib/prick/builder.rb +0 -246
  71. data/lib/prick/cache.rb +0 -34
  72. data/lib/prick/command.rb +0 -104
  73. data/lib/prick/database.rb +0 -82
  74. data/lib/prick/dsort.rb +0 -151
  75. data/lib/prick/ensure.rb +0 -119
  76. data/lib/prick/exceptions.rb +0 -25
  77. data/lib/prick/head.rb +0 -189
  78. data/lib/prick/migration.rb +0 -70
  79. data/lib/prick/program.rb +0 -287
  80. data/lib/prick/project.rb +0 -626
  81. data/lib/prick/rdbms.rb +0 -137
  82. data/lib/prick/schema.rb +0 -27
  83. data/lib/prick/share.rb +0 -64
  84. data/libexec/strip-comments +0 -33
  85. data/make_releases +0 -72
  86. data/make_schema +0 -10
  87. data/share/diff/diff.after-tables.sql +0 -4
  88. data/share/diff/diff.before-tables.sql +0 -4
  89. data/share/diff/diff.tables.sql +0 -8
  90. data/share/features/diff.sql +0 -2
  91. data/share/features/feature/diff.sql +0 -2
  92. data/share/features/feature/migrate.sql +0 -2
  93. data/share/features/features.sql +0 -2
  94. data/share/features/features.yml +0 -2
  95. data/share/features/migrations.sql +0 -4
  96. data/share/gitignore +0 -2
  97. data/share/migration/diff.tables.sql +0 -8
  98. data/share/migration/features.yml +0 -6
  99. data/share/migration/migrate.sql +0 -3
  100. data/share/migration/migrate.yml +0 -8
  101. data/share/migration/tables.sql +0 -3
  102. data/share/schema/build.yml +0 -14
  103. data/share/schema/schema/build.yml +0 -3
  104. data/share/schema/schema/prick/build.yml +0 -14
  105. data/share/schema/schema/prick/data.sql +0 -7
  106. data/share/schema/schema/prick/schema.sql +0 -3
  107. data/share/schema/schema/public/build.yml +0 -13
  108. data/share/schema/schema.sql +0 -3
  109. data/test_assorted +0 -192
  110. data/test_feature +0 -112
  111. data/test_refactor +0 -34
  112. data/test_single_dev +0 -83
@@ -0,0 +1,189 @@
1
+ module Prick
2
+ module Build
3
+ class Node
4
+ attr_reader :parent
5
+ forward_to :parent, :conn
6
+ attr_reader :phase # :init, :decl, :seed, :term or nil (for BuildNode)
7
+ attr_reader :kind # :sql, :exe, :fox, :yml, :inline, :module
8
+ attr_reader :path
9
+ attr_reader :args # only defined for :exe (String)
10
+
11
+ def name() @name = File.basename(path) end
12
+
13
+ def schema() @schema ||= parent&.schema || "public" end
14
+ def schema=(s) @schema = s end
15
+
16
+ attr_reader :source
17
+
18
+ def source
19
+ @source ||= read_source
20
+ end
21
+
22
+ def prefix_lines() 0 end
23
+
24
+ def source_lines()
25
+ return @source_lines if @source_lines
26
+ source
27
+ @source_lines
28
+ end
29
+
30
+ def lines() prefix_lines + source_lines end
31
+
32
+ def initialize(parent, phase, kind, path, args = nil)
33
+ constrain parent, BuildNode, NilClass
34
+ constrain phase, :init, :decl, :seed, :term, nil
35
+ constrain kind, :sql, :exe, :fox, :yml, :inline, :module
36
+ constrain path, String, NilClass
37
+ @parent, @phase, @kind, @path = parent, phase, kind, path
38
+ @args = args&.empty? ? nil : args
39
+ @schema = nil
40
+ @source_lines = nil
41
+ end
42
+
43
+ def to_s() [path, args].compact.join(" ") end
44
+ def inspect() to_s end
45
+ def dump() puts "#{inspect} (#{schema})" end
46
+
47
+ protected
48
+ def read_source
49
+ @source_lines = 0
50
+ @source = []
51
+ end
52
+ end
53
+
54
+ class SqlNode < Node
55
+ def initialize(parent, phase, path)
56
+ super(parent, phase, :sql, path)
57
+ end
58
+
59
+ protected
60
+ def read_source
61
+ file = File.read(path)
62
+ @source_lines = 1 + 1 + file.count("\n")
63
+ ["set search_path to #{schema};\n", file]
64
+ end
65
+ end
66
+
67
+ class FoxNode < Node
68
+ def initialize(parent, phase, path)
69
+ super(parent, phase, :fox, path)
70
+ end
71
+ end
72
+
73
+ # Note that #path refers to the build file
74
+ class InlineNode < Node
75
+ attr_reader :stmts
76
+
77
+ def initialize(parent, phase, path, *stmts)
78
+ super(parent, phase, :inline, path)
79
+ @stmts = Array(stmts).flatten
80
+ end
81
+
82
+ def inspect() "#@path \"#{@stmts.join(";")}\"" end
83
+
84
+ protected
85
+ def read_source
86
+ @source_lines = @stmts.size
87
+ @stmts
88
+ end
89
+ end
90
+
91
+ class ModuleNode < Node
92
+ attr_reader :klass
93
+ attr_reader :command
94
+ def object() self.class.objects[klass] end
95
+
96
+ def initialize(parent, phase, path, klass, command, args = nil)
97
+ constrain klass, Symbol, String
98
+ constrain command, Symbol, String
99
+
100
+ super(parent, phase, :module, path, args)
101
+
102
+ @klass = klass.to_sym
103
+ @command = command.to_sym
104
+
105
+ if !object
106
+ Kernel.class_eval File.read(path)
107
+ self.class.objects[@klass] = eval(klass.to_s).new(conn)
108
+ end
109
+ end
110
+
111
+ def call()
112
+ object.send(@command, *args)
113
+ end
114
+
115
+ def inspect() "#{path} #{klass}##{command}" end
116
+
117
+ private
118
+ # Map from klass name (a Symbol) to object
119
+ @@objects = {}
120
+ def self.objects() @@objects end
121
+ end
122
+
123
+ class ExeNode < Node
124
+ # Using a pipe instead of just executing the command shaves off some
125
+ # deciseconds spent starting up bash. It expects the process to read
126
+ # database/username from standard input
127
+ attr_reader :pipe
128
+
129
+ def initialize(parent, phase, path, args = nil)
130
+ super(parent, phase, :exe, path, args)
131
+ @pipe = Command::Pipe.new(to_s, stderr: nil)
132
+ end
133
+
134
+ def inspect() "#{path}(#{args.join(", ")})" end
135
+
136
+ protected
137
+ def read_source
138
+ pipe.puts [conn.name, conn.user]
139
+ sql = pipe.wait
140
+ @source_lines = 1 + 1 + sql.count("\n")
141
+ ["set search_path to #{schema};\n", sql]
142
+ end
143
+ end
144
+
145
+ class BuildNode < Node
146
+ def nodes() @nodes ||= init_nodes + decl_nodes + seed_nodes + term_nodes end
147
+
148
+ attr_reader :decl_nodes
149
+ attr_reader :init_nodes
150
+ attr_reader :term_nodes
151
+ attr_reader :seed_nodes
152
+
153
+ def initialize(parent, path)
154
+ super(parent, nil, :yml, path)
155
+ @decl_nodes = []
156
+ @init_nodes = []
157
+ @term_nodes = []
158
+ @seed_nodes = []
159
+ end
160
+
161
+ def inspect() to_s end
162
+
163
+ def dump
164
+ puts "BuildNode #{path}"
165
+ indent {
166
+ puts "schema: #{schema}" if schema
167
+ decl_nodes.each(&:dump)
168
+ for kind in [:init, :term, :seed]
169
+ kind_nodes = self.send("#{kind}_nodes".to_sym)
170
+ if !kind_nodes.empty?
171
+ puts "#{kind.upcase}:"
172
+ indent { kind_nodes.each(&:dump) }
173
+ end
174
+ end
175
+ }
176
+ end
177
+ end
178
+
179
+ class RootBuildNode < BuildNode
180
+ attr_reader :conn
181
+
182
+ def initialize(conn, path)
183
+ @conn = conn
184
+ super(nil, path)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
@@ -0,0 +1,105 @@
1
+ module Prick
2
+ module Build
3
+ class NodePool
4
+ def schemas() @schemas.keys end
5
+ def before_schema(s) schemas.take_while { |schema| schema != s } end
6
+ def after_schema(s) schemas.reverse.take_while { |schema| schema != s }.reverse end
7
+
8
+ attr_reader :nodes
9
+
10
+ attr_reader :init_nodes
11
+ attr_reader :decl_nodes
12
+ attr_reader :seed_nodes
13
+ attr_reader :term_nodes
14
+
15
+ # attr_reader :setup_nodes
16
+ # attr_reader :teardown_nodes
17
+
18
+ def fox_seed_nodes() seed_nodes.select { |node| node.kind == :fox } end
19
+ def sql_seed_nodes() seed_nodes.select { |node| node.kind == :sql } end
20
+
21
+ def initialize()
22
+ self.clear
23
+ end
24
+
25
+ def add(*nodes)
26
+ nodes = Array(nodes).flatten
27
+ @nodes.concat(nodes)
28
+ nodes.each { |node|
29
+ @schemas[node.schema] += 1
30
+ @kind_nodes[node.phase]&.append(node)
31
+ }
32
+ self
33
+ end
34
+
35
+ def delete(*nodes)
36
+ # puts "#delete(*nodes)"
37
+ nodes = Array(nodes).flatten
38
+ nodes.each { |node|
39
+ delete_node(node)
40
+ kind_nodes = @kind_nodes[node.phase] and kind_nodes.delete_at(kind_nodes.index(node))
41
+ }
42
+ nodes.last
43
+ end
44
+
45
+ def delete_if(phase = nil, &block)
46
+ candidates = @kind_nodes[phase] || @nodes
47
+ delete(candidates.select { |node| yield(node) })
48
+ end
49
+
50
+ def delete_schema(*schemas, exclude: [])
51
+ schemas = Array(schemas).flatten
52
+ delete_if { |node|
53
+ schemas.include?(node.schema) && !exclude.include?(node.phase) && !exclude.include?(node.kind)
54
+ }
55
+ end
56
+
57
+ def clear(*phases)
58
+ phases = Array(phases).flatten
59
+ if !phases.empty?
60
+ for phase in phases
61
+ nodes = @kind_nodes[phase]
62
+ nodes.each { |node| delete_node(node) }
63
+ @kind_nodes[phase].clear
64
+ end
65
+ else
66
+ @schemas = Hash.new(0) # map from schema name to number of nodes
67
+ @nodes = []
68
+ @init_nodes = []
69
+ @decl_nodes = []
70
+ @seed_nodes = []
71
+ @term_nodes = []
72
+ @kind_nodes = {
73
+ decl: @decl_nodes,
74
+ init: @init_nodes,
75
+ seed: @seed_nodes,
76
+ term: @term_nodes,
77
+ yml: nil
78
+ }
79
+ end
80
+ end
81
+
82
+ def dump
83
+ puts "NodePool, #{nodes.size} nodes"
84
+ indent {
85
+ puts "init_nodes:"
86
+ indent { @init_nodes.each &:dump }
87
+ puts "decl_nodes:"
88
+ indent { @decl_nodes.each &:dump }
89
+ puts "seed_nodes:"
90
+ indent { @seed_nodes.each &:dump }
91
+ puts "term_nodes:"
92
+ indent { @term_nodes.each &:dump }
93
+ }
94
+ end
95
+
96
+ private
97
+ def delete_node(node)
98
+ # puts "#delete_node"
99
+ @nodes.delete_at(@nodes.index(node))
100
+ @schemas[node.schema] -= 1
101
+ @schemas.delete(node.schema) if @schemas[node.schema] == 0
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,120 @@
1
+ module Prick
2
+ module Build
3
+ class Parser
4
+ def self.parse(conn, dir)
5
+ Parser.new(conn).parse(dir).unit
6
+ # [parser.unit, parser.schemas]
7
+ end
8
+
9
+ attr_reader :conn
10
+ attr_reader :dir
11
+ attr_reader :unit
12
+ # attr_reader :schemas
13
+
14
+ def initialize(conn)
15
+ @conn = conn
16
+ end
17
+
18
+ def parse(dir)
19
+ @dir = dir
20
+ # @schemas = {}
21
+ parse_directory(nil, dir)
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ # First build unit is a RootBuildNode, the rest are regular BuildNode objects
28
+ def make_build_unit(parent, path)
29
+ if @unit
30
+ BuildNode.new(parent, path)
31
+ else
32
+ @unit = RootBuildNode.new(conn, path)
33
+ end
34
+ end
35
+
36
+ def parse_directory(parent, dir)
37
+ build_file = "#{dir}/build.yml".sub(/\/\//, "/")
38
+ if File.exist? build_file
39
+ parse_build_file(parent, dir, build_file)
40
+ else
41
+ raise Error, "Can't find build.yml in #{dir}"
42
+ end
43
+ end
44
+
45
+ def parse_build_file(parent, dir, path)
46
+ unit = make_build_unit(parent, path)
47
+ entries = YAML.load(File.read(path)) || []
48
+ entries.each { |entry|
49
+ if entry.is_a?(Hash)
50
+ entry.each { |key, value|
51
+ if key == "schema"
52
+ unit.schema = value
53
+ # @schemas[unit.schema = value] = true
54
+ else
55
+ case key
56
+ when "init"; unit.init_nodes
57
+ when "term"; unit.term_nodes
58
+ when "seed"; unit.seed_nodes
59
+ else
60
+ raise Error, "Illegal key in #{unit.path}: #{key}"
61
+ end.concat(Array(value).map { |value| parse_entry(unit, key.to_sym, dir, value) })
62
+ end
63
+ }
64
+ else
65
+ node = parse_entry(unit, :decl, dir, entry)
66
+ if node.kind == :fox
67
+ unit.seed_nodes << node
68
+ else
69
+ unit.decl_nodes << node
70
+ end
71
+ end
72
+ }
73
+ unit
74
+ end
75
+
76
+ def parse_entry(unit, phase, dir, entry)
77
+ if entry.is_a?(Hash)
78
+ entry.size == 1 or raise Error, "sql and module are single-line values"
79
+ key, value = entry.first
80
+ case key
81
+ when "sql"; InlineNode.new(unit, phase, unit.path, value)
82
+ when "call";
83
+ args = value.split(/\s+/)
84
+ args.size >= 3 or raise "Illegal number of arguments: #{value}"
85
+ file, klass, command, args = *args
86
+ ModuleNode.new(unit, phase, "#{dir}/#{file}", klass, command, args)
87
+ else
88
+ raise Error, "Illegal key: #{key}"
89
+ end
90
+ else
91
+ name = entry
92
+ name.sub!(/\/$/, "")
93
+ if name =~ /^(\S+)\s+(.*)$/ # exe
94
+ file = $1
95
+ args = $2
96
+ else
97
+ file = name
98
+ end
99
+ path = "#{dir}/#{file}"
100
+ if File.directory? path
101
+ parse_directory(unit, path)
102
+ elsif File.executable? path
103
+ ExeNode.new(unit, phase, path, args)
104
+ elsif File.file? path
105
+ case path
106
+ when /\.sql$/
107
+ SqlNode.new(unit, phase, path)
108
+ when /\.fox$/
109
+ FoxNode.new(unit, :seed, path)
110
+ else
111
+ raise Error, "Unrecognized file type #{File.basename(path)} in #{unit.dir}"
112
+ end
113
+ else
114
+ raise Error, "Can't find #{name} in #{dir} from #{unit}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,193 @@
1
+ require 'fcntl'
2
+
3
+ require 'forward_to'
4
+
5
+ include ForwardTo
6
+
7
+ module Command
8
+ class Error < RuntimeError
9
+ attr_reader :cmd
10
+ attr_reader :status
11
+ attr_reader :stdin
12
+ attr_reader :stdout
13
+ attr_reader :stderr
14
+
15
+ def initialize(cmd, status, stdin, stdout, stderr)
16
+ super(stderr.join("\n"))
17
+ @cmd = cmd
18
+ @status = status
19
+ @stdin = stdin
20
+ @stdout = stdout
21
+ @stderr = stderr
22
+ end
23
+ end
24
+
25
+ class Pipe
26
+ attr_reader :cmd
27
+ attr_reader :status
28
+
29
+ def writer() @pw[1] end
30
+ def reader() @pr[0] end
31
+ def error() @pe[0] end
32
+
33
+ forward_to :reader, :read, :gets
34
+ forward_to :writer, :write, :puts
35
+
36
+ # Remaining output not consumed by #get/#read or through #reader or #error
37
+ attr_reader :stdout
38
+ attr_reader :stdin
39
+
40
+ def initialize(cmd, stderr: nil, fail: true)
41
+ @cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
42
+ @stderr = stderr
43
+ @fail = fail
44
+
45
+ @pw = IO::pipe # pipe[0] for read, pipe[1] for write
46
+ @pr = IO::pipe
47
+ @pe = IO::pipe
48
+
49
+ STDOUT.flush
50
+
51
+ @pid = fork {
52
+ @pw[1].close
53
+ @pr[0].close
54
+ @pe[0].close
55
+
56
+ STDIN.reopen(@pw[0])
57
+ @pw[0].close
58
+
59
+ STDOUT.reopen(@pr[1])
60
+ @pr[1].close
61
+
62
+ STDERR.reopen(@pe[1])
63
+ @pe[1].close
64
+
65
+ exec(cmd)
66
+ }
67
+
68
+ @pw[0].close
69
+ @pr[1].close
70
+ @pe[1].close
71
+ end
72
+
73
+ def wait
74
+ @pw[1].close
75
+ @status = Process.waitpid2(@pid)[1].exitstatus
76
+
77
+ out = @pr[0].readlines.map(&:chomp)
78
+ err = @pe[0].readlines.map(&:chomp)
79
+
80
+ @pr[0].close
81
+ @pe[0].close
82
+
83
+ raise Command::Error.new(@cmd, @status, nil, out, err) if @status != 0 && @fail == true
84
+
85
+ case @stderr
86
+ when true; [out, err]
87
+ when false; out
88
+ when nil
89
+ $stderr.puts err
90
+ out
91
+ end
92
+ end
93
+ end
94
+
95
+ # Execute the shell command 'cmd' and return standard-output as an array of
96
+ # strings. If :stdin is a string or an array of lines if will be fed to the
97
+ # command on standard-input, if it is a IO object that IO object is piped to
98
+ # the command
99
+ #
100
+ # By default #command pass through standard-error but if :stderr is true,
101
+ # #command will return a tuple of standard-output, standard-error lines. If
102
+ # :stderr is false, standard-error is ignored and is the same as adding
103
+ # "2>/dev/null" to the command
104
+ #
105
+ # #command raises a Command::Error exception if the command return with an
106
+ # exit code != 0 unless :fail is false. In that case the the exit code can be
107
+ # fetched from Command::status
108
+ #
109
+ def command(cmd, stdin: nil, stderr: nil, fail: true)
110
+ cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
111
+
112
+ pw = IO::pipe # pipe[0] for read, pipe[1] for write
113
+ pr = IO::pipe
114
+ pe = IO::pipe
115
+
116
+ STDOUT.flush
117
+
118
+ pid = fork {
119
+ pw[1].close
120
+ pr[0].close
121
+ pe[0].close
122
+
123
+ STDIN.reopen(pw[0])
124
+ pw[0].close
125
+
126
+ STDOUT.reopen(pr[1])
127
+ pr[1].close
128
+
129
+ STDERR.reopen(pe[1])
130
+ pe[1].close
131
+
132
+ exec(cmd)
133
+ }
134
+
135
+ pw[0].close
136
+ pr[1].close
137
+ pe[1].close
138
+
139
+ if stdin
140
+ case stdin
141
+ when IO; pw[1].write(stdin.read)
142
+ when String; pw[1].write(stdin)
143
+ when Array; pw[1].write(stdin.join("\n") + "\n")
144
+ end
145
+ pw[1].flush
146
+ pw[1].close
147
+ end
148
+
149
+ @status = Process.waitpid2(pid)[1].exitstatus
150
+
151
+ out = pr[0].readlines.map(&:chomp)
152
+ err = pe[0].readlines.map(&:chomp)
153
+
154
+ pw[1].close if !stdin
155
+ pr[0].close
156
+ pe[0].close
157
+
158
+ if @status != 0
159
+ @exception = Command::Error.new(cmd, @status, stdin, out, err)
160
+ raise @exception if fail
161
+ else
162
+ @exception = nil
163
+ end
164
+
165
+ case stderr
166
+ when true; [out, err]
167
+ when false; out
168
+ when nil
169
+ $stderr.puts err
170
+ out
171
+ end
172
+ end
173
+
174
+ # Exit status of the last command
175
+ def status() @status end
176
+
177
+ # Stored exception when #command is called with :fail true
178
+ def exception() @exception end
179
+
180
+ # Like command but returns true if the command exited with the expected
181
+ # status. Note that it suppresses standard-error by default
182
+ #
183
+ def command?(cmd, expect: 0, stdin: nil, stderr: false)
184
+ command(cmd, stdin: stdin, stderr: stderr, fail: false)
185
+ @status == expect
186
+ end
187
+
188
+ module_function :command
189
+ module_function :status
190
+ module_function :exception
191
+ module_function :command?
192
+ end
193
+