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
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
-