prick 0.19.0 → 0.20.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -4
  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 +32 -21
  53. metadata +95 -76
  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
+