prick 0.19.0 → 0.20.1

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 +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
data/lib/prick/branch.rb DELETED
@@ -1,265 +0,0 @@
1
- require "prick/state.rb"
2
-
3
- # FIXME FIXME FIXME Not used!
4
-
5
-
6
- module Prick
7
- class Branch
8
- # Branch name. It is usually equal to the version but migrations use a
9
- # <base_version>_<version> format instead
10
- attr_reader :name
11
-
12
- # Version of this branch. Note that version and base_version are the same
13
- # for feature branches
14
- attr_reader :version
15
-
16
- # Base version
17
- attr_reader :base_version
18
-
19
- # The release directory. It contains the release's .prick-migration file
20
- attr_reader :directory
21
-
22
- # The Schema object. This is shared by many all branches
23
- attr_reader :schema
24
-
25
- # Migration object. Running the migration object on the base release will
26
- # migrate it to this release
27
- attr_reader :migration
28
-
29
- # Database name
30
- def database() name end
31
-
32
- # Classifiers
33
- def release?() self.is_a?(Release) && !prerelease? end
34
- def prerelease?() self.is_a?(PreRelease) end
35
- def feature?() self.is_a?(Feature) end
36
- def migration?() self.is_a?(MigrationRelease) end
37
-
38
- # Note that `name` can be nil. It defaults to `version.to_s`
39
- def initialize(name, version, base_version, directory, migration)
40
- @name = name || migration.version.to_s
41
- @version = version
42
- @base_version = base_version
43
- @directory = directory
44
- @schema = Schema.new
45
- @migration = migration
46
- end
47
-
48
- def dump
49
- puts "#{self.class}"
50
- indent {
51
- puts "name: #{name}"
52
- puts "version: #{version}"
53
- puts "base_version: #{base_version}"
54
- puts "directory: #{directory}"
55
- print "migration: "
56
- migration.dump
57
- puts "database: #{database}"
58
- }
59
- end
60
-
61
- def self.load(name) raise NotThis end
62
-
63
- # True if the branch exists in git
64
- def exist?() self.class.exist?(name) end
65
-
66
- def create()
67
- !exist? or raise Error, "Can't create branch #{name}, exists already"
68
- Git.create_branch(name)
69
- Git.checkout_branch(name)
70
- prepare if !prepared?
71
- self
72
- end
73
-
74
- # True if the branch exists on disk
75
- def present?() self.class.present?(name) end
76
-
77
- def prepared?() @migration.exist? end
78
- def prepare() @migration.create end
79
-
80
- def include(feature_version) @migration.append_feature(feature_version) end
81
-
82
- def build(database) schema.build(database) end
83
-
84
- # Used to checkout migrations. MigrationReleases checks out the
85
- # corresponding branch while Release and PreRelease checks out a tag
86
- def checkout_release() Git.checkout_branch(version) end
87
-
88
- def migrate(database)
89
- @migration.migrate(database)
90
- end
91
-
92
- def migrate_features(database)
93
- @migration.migrate_features(database)
94
- end
95
-
96
- def self.directory(name) raise NotThis end
97
- def self.exist?(name) Git.branch?(name) || Git.tag?(name) end
98
- def self.present?(name) File.exist?(directory(name)) end
99
-
100
- def <=>(other)
101
- if !self.is_a?(MigrationRelease) && other.is_a?(MigrationRelease)
102
- compare(other.base_version, other.version)
103
- else
104
- compare(other.version, other.base_version)
105
- end
106
- end
107
-
108
- private
109
- def compare(other_version, other_base_version)
110
- r = version <=> other_version
111
- return r if r != 0
112
- if base_version.nil?
113
- other_base_version.nil? ? 0 : -1
114
- elsif other_base_version.nil?
115
- 1
116
- else
117
- base_version <=> other_base_version
118
- end
119
- end
120
- end
121
-
122
- # A user defined branch. User defined branches have no version and only the
123
- # #build method is defined TODO: Make all methods but #build private
124
- class UserBranch < Branch
125
- def initialize(name)
126
- super(name, nil, nil, nil, nil)
127
- end
128
- end
129
-
130
- class AbstractRelease < Branch
131
- def create(schema_version = self.version)
132
- super()
133
- schema.version = schema_version
134
- Git.add schema.version_file
135
- self
136
- end
137
-
138
- def checkout_release() Git.checkout_tag(version) end
139
-
140
- def tag!() Git.create_tag(version) end
141
- end
142
-
143
- class Release < AbstractRelease
144
- def initialize(version, base_version)
145
- !version.zero? || base_version.nil? or raise Internal, "Version 0.0.0 has no base release"
146
- directory = self.class.directory(version.to_s)
147
- migration = ReleaseMigration.new(version, base_version)
148
- super(nil, version, base_version, directory, migration)
149
- end
150
-
151
- def self.load(name)
152
- migration = ReleaseMigration.load(directory(name))
153
- self.new(migration.version, migration.base_version)
154
- end
155
-
156
- def self.directory(name)
157
- File.join(RELEASES_DIR, name)
158
- end
159
- end
160
-
161
- class PreRelease < AbstractRelease
162
- attr_reader :prerelease_version
163
-
164
- def database() version.to_s end
165
-
166
- def initialize(prerelease_version, base_version)
167
- @prerelease_version = prerelease_version
168
- version = prerelease_version.truncate(:pre)
169
- directory = Release.directory(version.to_s)
170
- migration = ReleaseMigration.new(version, base_version)
171
- super(prerelease_version.to_s, version, base_version, directory, migration)
172
- end
173
-
174
- def self.load(name, prerelease_version)
175
- migration = ReleaseMigration.load(directory(name))
176
- self.new(prerelease_version, migration.base_version)
177
- end
178
-
179
- def create()
180
- super(prerelease_version)
181
- end
182
-
183
- def increment
184
- PreRelease.new(prerelease_version.increment(:pre), base_version)
185
- end
186
-
187
- def self.directory(name)
188
- version = Version.new(name)
189
- Release.directory(version.truncate(:pre).to_s)
190
- end
191
- end
192
-
193
- class MigrationRelease < Branch
194
- def database() version.to_s end
195
-
196
- def initialize(version, base_version)
197
- migration = MigrationMigration.new(version, base_version)
198
- super(migration.name, version, base_version, migration.dir, migration)
199
- end
200
-
201
- def self.load(name)
202
- directory = self.directory(name)
203
- migration_state = MigrationState.new(directory).read
204
- self.new(migration_state.version, migration_state.base_version)
205
- end
206
-
207
- def self.directory(name)
208
- File.join(MIGRATIONS_DIR, name)
209
- end
210
- end
211
-
212
- # Feature maintains a migration in the parent release but it is ignored when
213
- # including the feature
214
- class Feature < Branch
215
- attr_reader :feature_name
216
-
217
- def database() base_version.to_s end
218
-
219
- def initialize(feature_name, base_version)
220
- version = Version.new(base_version, feature: feature_name)
221
- migration = FeatureMigration.new(feature_name, base_version)
222
- super(version.to_s, version, base_version, migration.features_dir, migration)
223
- @feature_name = feature_name
224
- end
225
-
226
- def create
227
- super()
228
- schema.version = version
229
- Git.add schema.version_file
230
- self
231
- end
232
-
233
- def include(feature_version) @migration.insert_feature(feature_version) end
234
-
235
- def rebase(base_version)
236
- new_version = Version.new(new_base_version, feature: feature_name)
237
- name = new_version.to_s
238
-
239
- Git.create_branch(name)
240
- Git.checkout_branch(name)
241
-
242
- symlink = self.directory(name)
243
- FileUtils.ln_sr(directory, symlink, force: true)
244
- Git.add(symlink)
245
-
246
- migration.base_version = new_base_version
247
- migration.save
248
-
249
- schema.version = new_version
250
- Git.add ect.schema.version_file
251
- end
252
-
253
- def self.load(name)
254
- directory = self.directory(name)
255
- migration_state = MigrationState.new(directory).read
256
- self.new(migration_state.version.feature, migration_state.base_version)
257
- end
258
-
259
- def self.directory(name)
260
- version = Version.new(name)
261
- File.join(Release.directory(version.truncate(:feature).to_s), version.feature.to_s)
262
- end
263
- end
264
- end
265
-
data/lib/prick/builder.rb DELETED
@@ -1,246 +0,0 @@
1
- require "prick/state.rb"
2
-
3
- require 'indented_io'
4
-
5
- module Prick
6
- # Builder is a procedural object for building schemas and executing
7
- # migrations. Builder use resources that can be executables, SQL files, YAML
8
- # files, FOX files, or directories. A resource is identified by a name (eg.
9
- # 'my-tables') and is used to match a build file. Build files are looked up
10
- # in the following order:
11
- #
12
- # #{name} executable
13
- # #{name}.* executable
14
- # #{name}.yml
15
- # #{name}.sql
16
- # #{name}.fox
17
- # #{name}/
18
- #
19
- # The output from executable objects is expected to be SQL statements that
20
- # are then fed into postgres
21
- #
22
- # When a resource match a directory, the directory can contain a special
23
- # default resource ('build' or 'migrate') that takes over the rest of the
24
- # build process for that directory. Typically, you'll use the 'build.yml' or
25
- # 'migrate.yml' to control the build order of the other resources. If a
26
- # directory doesn't contain a build resource, the resources in the directory
27
- # are built in alphabetic order
28
- #
29
- # Build SQL scripts and executables are executed with search path set to
30
- # containing schema. This doesn't include migration script or executable
31
- #
32
- class Builder
33
- attr_reader :database
34
- attr_reader :directory
35
- attr_reader :default # either "build" or "migrate"
36
- attr_reader :lines
37
-
38
- def state_file() self.class.state_file end
39
- def state_file=(file) self.class.state_file = file end
40
- def self.state_file() @@state_file end
41
- def self.state_file=(file) @@state_file = file end
42
-
43
- def initialize(database, directory, default)
44
- @database = database
45
- @directory = directory
46
- @default = default
47
- @execute = true
48
- @lines = []
49
- @cwd = Dir.getwd # used in error messages
50
- end
51
-
52
- def build(subject = default, execute: true)
53
- puts "build(#{subject.inspect}, execute: #{execute.inspect})" if $verbose > 0
54
- @execute = execute
55
- @lines = []
56
- Dir.chdir(directory) {
57
- if subject
58
- build_subject(subject)
59
- else
60
- build_directory(".")
61
- end
62
- }
63
- @lines.reject! { |l| l =~ /^\s*--/ || l =~ /^\s*$/ }
64
- self
65
- end
66
-
67
- def self.yml_file(directory) raise NotThis end
68
-
69
- protected
70
- def do_path(path, &block)
71
- puts "do_path(#{path.inspect})" if $verbose >= 3
72
- if File.directory?(path)
73
- dir, file = path, nil
74
- else
75
- dir, file = File.split(path)
76
- end
77
- if $verbose >= 2
78
- indent { Dir.chdir(dir) { yield(file) } }
79
- else
80
- Dir.chdir(dir) { yield(file) }
81
- end
82
- end
83
-
84
- def do_sql(path, schema: nil)
85
- puts "do_sql(#{path})" if $verbose >= 2
86
- do_path(path) { |file|
87
- if @execute
88
- begin
89
- Rdbms.exec_file(database.name, file, user: database.user, schema: schema)
90
- rescue Command::Error => ex
91
- $stderr.puts ex.stderr
92
- $stderr.puts "in #{reldir}/#{file}"
93
- exit 1
94
- end
95
- else
96
- @lines += IO.readlines(file).map(&:chomp)
97
- end
98
- }
99
- true
100
- end
101
-
102
- def do_fox(path, schema: nil)
103
- puts "do_fox(#{path.inspect}, #{schema.inspect})" if $verbose >= 2
104
- do_path(path) { |file|
105
- lines = Command.command "fox --state=#{state_file} --write --delete=none #{database.name} #{file}"
106
- if @execute
107
- begin
108
- Rdbms.exec_sql(database.name, lines.join("\n"), user: database.user, schema: schema)
109
- rescue Command::Error => ex
110
- $stderr.puts ex.stderr
111
- $stderr.puts "from #{reldir}/#{file}"
112
- exit 1
113
- end
114
- else
115
- @lines += lines
116
- end
117
- }
118
- end
119
-
120
- def do_exe(path, args = [], schema: nil)
121
- puts "do_exe(#{path.inspect}, #{args.inspect})" if $verbose >= 2
122
- do_path(path) { |file|
123
- cmd = (["./#{file}"] + args).join(" ")
124
- lines = Command.command cmd, stdin: [database.name, database.user], stderr: nil
125
- if @execute
126
- begin
127
- Rdbms.exec_sql(database.name, lines.join("\n"), user: database.user, schema: schema)
128
- rescue Command::Error => ex
129
- $stderr.puts ex.stderr
130
- $stderr.puts "from #{reldir}/#{file}"
131
- exit 1
132
- end
133
- else
134
- @lines += lines
135
- end
136
- }
137
- true
138
- end
139
-
140
- def do_yml(path)
141
- puts "do_yml(#{path})" if $verbose >= 2
142
- do_path(path) { |file|
143
- YAML.load(File.read(file))&.each { |subject| build_subject(subject) }
144
- }
145
- true
146
- end
147
-
148
- # A subject can be both an abstract subject or a concrete file (*.yml, *.sql, or *.fox)
149
- def build_subject(subject)
150
- puts "build_subject(#{subject.inspect}) in #{Dir.getwd}" if $verbose > 0
151
- cmd, *args = subject.split(/\s+/)
152
- if File.file?(cmd) && File.executable?(cmd)
153
- do_exe(cmd, args)
154
- elsif File.file?(subject) && subject.end_with?(".yml")
155
- do_yml(subject)
156
- elsif File.file?(subject) && subject.end_with?(".sql")
157
- do_sql(subject)
158
- elsif File.file?(subject) && subject.end_with?(".fox")
159
- do_fox(subject)
160
- elsif File.exist?(yml_file = "#{subject}.yml")
161
- do_yml(yml_file)
162
- elsif File.exist?(sql_file = "#{subject}.sql")
163
- do_sql(sql_file)
164
- elsif File.directory?(subject)
165
- build_directory(subject)
166
- else
167
- false
168
- end
169
- end
170
-
171
- def build_directory(path)
172
- puts "build_directory(#{path.inspect}) in #{Dir.getwd}" if $verbose >= 2
173
- do_path(path) { |_|
174
- build_subject(@default) || begin
175
- if !Dir["#{default}", "#{default}.yml", "#{default}.sql"].empty?
176
- subjects = [default]
177
- else
178
- candidates = Dir["*"]
179
- exes = candidates.select { |file| File.file?(file) && File.executable?(file) }
180
- ymls = candidates.select { |file| file.end_with?(".yml") }.map { |f| f.sub(/\.yml$/, "") }
181
- sqls = candidates.select { |file| file.end_with?(".sql") }.map { |f| f.sub(/\.sql$/, "") }
182
- foxs = candidates.select { |file| file.end_with?(".fox") }.map { |f| f.sub(/\.fox$/, "") }
183
- dirs = candidates.select { |file| File.directory?(file) }
184
- subjects = (exes + ymls + sqls + foxs + dirs).uniq.sort #.reject { |f| f != "diff" } FIXME ??
185
- end
186
- subjects.inject(false) { |a, s| build_subject(s) || a }
187
- end
188
- }
189
- end
190
-
191
- private
192
- # Return the relative path to the current directory from the directory of
193
- # the time of the instantiation of the Builder object. Used in error
194
- # messages
195
- def reldir
196
- Dir.getwd.sub(/^#{@cwd}\//, "")
197
- end
198
- end
199
-
200
- class MigrationBuilder < Builder
201
- def initialize(database, directory)
202
- super(database, directory, "migrate")
203
- end
204
-
205
- def self.yml_file(directory) File.join(directory, "migrate") + ".yml" end
206
- end
207
-
208
- class SchemaBuilder < Builder
209
- def initialize(database, directory)
210
- super(database, directory, "build")
211
- end
212
-
213
- def build(subject = nil, execute: true)
214
- if subject
215
- @execute = execute
216
- @lines = []
217
- Dir.chdir(directory) {
218
- if File.executable?(subject)
219
- do_exe(subject)
220
- elsif subject.end_with?(".sql")
221
- do_sql(subject)
222
- else
223
- build_subject(subject)
224
- end
225
- }
226
- else
227
- super(execute: execute)
228
- end
229
- self
230
- end
231
-
232
- def self.yml_file(directory) File.join(directory, "build") + ".yml" end
233
-
234
- protected
235
- def do_sql(path)
236
- schema = Dir.getwd =~ /^.*schema\/([^\/]+)(?:\/.*)?$/ ? $1 : "public"
237
- super(path, schema: schema)
238
- end
239
-
240
- def do_exe(path, args = [])
241
- schema = Dir.getwd =~ /^.*schema\/([^\/]+)(?:\/.*)?$/ ? $1 : "public"
242
- super(path, args, schema: schema)
243
- end
244
- end
245
- end
246
-
data/lib/prick/cache.rb DELETED
@@ -1,34 +0,0 @@
1
-
2
- module Prick
3
- class Cache
4
- def initialize
5
- end
6
-
7
- def exist?(version)
8
- File.exist?(file(version))
9
- end
10
-
11
- def list
12
- Dir.glob(File.join(CACHE_DIR, "*.sql.gz"))
13
- end
14
-
15
- def load(database, version)
16
- Rdbms.load(database, file(version))
17
- end
18
-
19
- def save(database, version = database.version)
20
- Rdbms.save(database, file(version))
21
- end
22
-
23
- # Returns list of removed files
24
- def clean
25
- l = list
26
- FileUtils.rm(l)
27
- l
28
- end
29
-
30
- def file(version)
31
- File.join(CACHE_DIR, "#{version}.sql.gz")
32
- end
33
- end
34
- end
data/lib/prick/command.rb DELETED
@@ -1,104 +0,0 @@
1
- require 'fcntl'
2
-
3
- module Command
4
- class Error < RuntimeError
5
- attr_reader :status
6
- attr_reader :stdout
7
- attr_reader :stderr
8
-
9
- def initialize(message, status, stdout, stderr)
10
- super(message)
11
- @status = status
12
- @stdout = stdout
13
- @stderr = stderr
14
- end
15
- end
16
-
17
- # Execute the shell command 'cmd' and return standard output as an array of
18
- # strings. while stderr is passed through unless stderr: is false. If stderr:
19
- # is true, it returns a tuple of [stdout, stderr] instead. The shell command
20
- # is executed with the `errexit` and `pipefail` bash options
21
- #
22
- # The :stdin option is a line or an array of lines that'll be fed to the
23
- # standard input of the command. Default is nil
24
- #
25
- # It raises a Command::Error exception if the command fails unless :fail is
26
- # true. The exit status of the last command is stored in ::status
27
- #
28
- def command(cmd, stdin: nil, stderr: false, fail: true)
29
- cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
30
-
31
- pw = IO::pipe # pipe[0] for read, pipe[1] for write
32
- pr = IO::pipe
33
- pe = IO::pipe
34
-
35
- STDOUT.flush
36
-
37
- pid = fork {
38
- pw[1].close
39
- pr[0].close
40
- pe[0].close
41
-
42
- STDIN.reopen(pw[0])
43
- pw[0].close
44
-
45
- STDOUT.reopen(pr[1])
46
- pr[1].close
47
-
48
- STDERR.reopen(pe[1])
49
- pe[1].close
50
-
51
- exec(cmd)
52
- }
53
-
54
- pw[0].close
55
- pr[1].close
56
- pe[1].close
57
-
58
- if stdin
59
- pw[1].puts(stdin)
60
- pw[1].flush
61
- pw[1].close
62
- end
63
-
64
- @status = Process.waitpid2(pid)[1].exitstatus
65
-
66
- out = pr[0].readlines.collect { |line| line.chop }
67
- err = pe[0].readlines.collect { |line| line.chop }.grep_v(/^NOTICE:/)
68
-
69
- pw[1].close if !stdin
70
- pr[0].close
71
- pe[0].close
72
-
73
- result =
74
- case stderr
75
- when true; [out, err]
76
- when false; out
77
- when NilClass;
78
- $stderr.puts err
79
- out
80
- else
81
- raise Internal, "Unexpected value for :stderr - #{stderr.inspect}"
82
- end
83
-
84
- if @status == 0 || fail == false
85
- result
86
- elsif fail
87
- raise Command::Error.new("\n" + cmd + "\n" + (out + err).join("\n") + "\n", status, out, err)
88
- end
89
- end
90
-
91
- # Exit status of the last command
92
- def status() @status end
93
-
94
- # Like command but returns true if the command exited with the expected status
95
- def command?(cmd, expect: 0)
96
- command(cmd, fail: false)
97
- @status == expect
98
- end
99
-
100
- module_function :command
101
- module_function :status
102
- module_function :command?
103
- end
104
-