ripe 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -5
- data/Guardfile +2 -4
- data/README.md +1 -0
- data/bin/ripe +1 -59
- data/lib/ripe.rb +8 -1
- data/lib/ripe/blocks.rb +13 -0
- data/lib/ripe/blocks/block.rb +142 -0
- data/lib/ripe/blocks/liquid_block.rb +48 -0
- data/lib/ripe/blocks/multi_block.rb +71 -0
- data/lib/ripe/blocks/parallel_block.rb +29 -0
- data/lib/ripe/blocks/serial_block.rb +61 -0
- data/lib/ripe/blocks/working_block.rb +101 -0
- data/lib/ripe/cli.rb +121 -0
- data/lib/ripe/cli/helper.rb +31 -0
- data/lib/ripe/db.rb +7 -0
- data/lib/ripe/db/task.rb +42 -0
- data/lib/ripe/db/task_migration.rb +33 -0
- data/lib/ripe/db/worker.rb +64 -0
- data/lib/ripe/db/worker_migration.rb +41 -0
- data/lib/ripe/dsl.rb +2 -4
- data/lib/ripe/dsl/task_dsl.rb +4 -2
- data/lib/ripe/dsl/workflow_dsl.rb +5 -0
- data/lib/ripe/library.rb +34 -45
- data/lib/ripe/repo.rb +24 -23
- data/lib/ripe/version.rb +1 -1
- data/lib/ripe/worker_controller.rb +72 -144
- data/lib/ripe/worker_controller/preparer.rb +172 -0
- data/lib/ripe/worker_controller/syncer.rb +118 -0
- data/spec/cli_spec.rb +14 -0
- data/spec/library_spec.rb +18 -18
- data/spec/spec_helper.rb +2 -0
- data/spec/testpack.rb +16 -5
- data/spec/testpack/.ripe/meta.db +0 -0
- data/spec/testpack/.ripe/tasks/bar.sh +3 -0
- data/spec/testpack/{ripe → .ripe}/tasks/foo.sh +0 -0
- data/spec/testpack/.ripe/workers/1/1.sh +16 -0
- data/spec/testpack/.ripe/workers/1/2.sh +16 -0
- data/spec/testpack/.ripe/workers/1/job.sh +54 -0
- data/spec/testpack/.ripe/workers/2/3.sh +16 -0
- data/spec/testpack/.ripe/workers/2/4.sh +16 -0
- data/spec/testpack/.ripe/workers/2/job.sh +54 -0
- data/spec/testpack/.ripe/workers/3/5.sh +16 -0
- data/spec/testpack/.ripe/workers/3/6.sh +16 -0
- data/spec/testpack/.ripe/workers/3/job.sh +54 -0
- data/spec/testpack/.ripe/workflows/foobar.rb +23 -0
- data/spec/testpack/{case/Sample1 → Sample1}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample1 → Sample1}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample1 → Sample1}/foo_output.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample2 → Sample2}/foo_output.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/bar_output.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/foo_input.txt +0 -0
- data/spec/testpack/{case/Sample3 → Sample3}/foo_output.txt +0 -0
- data/spec/worker_controller_spec.rb +143 -0
- metadata +66 -40
- data/lib/ripe/block.rb +0 -41
- data/lib/ripe/liquid_block.rb +0 -17
- data/lib/ripe/multi_block.rb +0 -35
- data/lib/ripe/parallel_block.rb +0 -13
- data/lib/ripe/serial_block.rb +0 -37
- data/lib/ripe/task.rb +0 -21
- data/lib/ripe/task_migration.rb +0 -18
- data/lib/ripe/worker.rb +0 -44
- data/lib/ripe/worker_migration.rb +0 -26
- data/lib/ripe/working_block.rb +0 -41
- data/spec/block_spec.rb +0 -7
- data/spec/ripe_spec.rb +0 -7
- data/spec/testpack/ripe/tasks/bar.sh +0 -3
- data/spec/testpack/ripe/workflows/foobar.rb +0 -23
@@ -0,0 +1,172 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
class WorkerController
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class controls worker preparation from a given workflow, list of
|
7
|
+
# samples and parameters. It applies the workflow to each of the specified
|
8
|
+
# samples.
|
9
|
+
#
|
10
|
+
# @attr workers [Array<Worker>] workers prepared in current batch
|
11
|
+
#
|
12
|
+
# @see Ripe::DSL::WorkflowDSL#describe
|
13
|
+
# @see Ripe::WorkerController#prepare
|
14
|
+
|
15
|
+
class Preparer
|
16
|
+
|
17
|
+
attr_accessor :workers
|
18
|
+
|
19
|
+
##
|
20
|
+
# Prepare workers by applying the workflow callback and its parameters to
|
21
|
+
# each sample.
|
22
|
+
#
|
23
|
+
# @param workflow [String] the name of a workflow to apply on the sample
|
24
|
+
# list
|
25
|
+
# @param samples [Array<String>] list of samples to apply the callback to
|
26
|
+
# @param params [Hash<Symbol, String>] a list of worker-wide parameters
|
27
|
+
|
28
|
+
def initialize(workflow, samples, params = {})
|
29
|
+
# Extract callback and params from input
|
30
|
+
callback, params = load_workflow(workflow, params)
|
31
|
+
|
32
|
+
if ![:patch, :force, :depend].include?(params[:mode].to_sym)
|
33
|
+
abort "Invalid mode #{params[:mode]}."
|
34
|
+
end
|
35
|
+
|
36
|
+
# Apply the workflow to each sample
|
37
|
+
sample_blocks = prepare_sample_blocks(samples, callback, params)
|
38
|
+
|
39
|
+
# Split samples into groups of +:group_num+ samples and produce a
|
40
|
+
# worker from each of these groups.
|
41
|
+
@workers = sample_blocks.each_slice(params[:group_num].to_i).map do |worker_blocks|
|
42
|
+
prepare_worker(worker_blocks, params)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Load a workflow and return its +callback+ and +params+ components.
|
48
|
+
#
|
49
|
+
# @param workflow [String] the name of a workflow
|
50
|
+
# @param params [Hash<Symbol, String>] a list of worker-wide parameters
|
51
|
+
# @return [Proc, Hash<Symbol, String>] a list containing the workflow callback
|
52
|
+
# and default params
|
53
|
+
|
54
|
+
def load_workflow(workflow, params)
|
55
|
+
filename = Library.find(:workflow, workflow)
|
56
|
+
abort "Could not find workflow #{workflow}." if filename == nil
|
57
|
+
require_relative filename
|
58
|
+
|
59
|
+
# Imports +$workflow+ from the workflow component. This is a dirty
|
60
|
+
# hack to help make the +DSL::WorkflowDSL+ more convenient for the
|
61
|
+
# end user.
|
62
|
+
|
63
|
+
params = {
|
64
|
+
wd: Dir.pwd,
|
65
|
+
mode: :patch,
|
66
|
+
group_num: 1,
|
67
|
+
}.merge($workflow.params.merge(params))
|
68
|
+
|
69
|
+
[$workflow.callback, params]
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Apply the workflow (callback) to each sample, producing a single root
|
74
|
+
# block per sample.
|
75
|
+
#
|
76
|
+
# @param samples [Array<String>] a list of samples
|
77
|
+
# @param callback [Proc] workflow callback to be applied to each sample
|
78
|
+
# @param params [Hash] a list of worker-wide parameters
|
79
|
+
# @return [Hash] a +{sample => block}+ hash
|
80
|
+
|
81
|
+
def prepare_sample_blocks(samples, callback, params)
|
82
|
+
sample_blocks = samples.map do |sample|
|
83
|
+
block = callback.call(sample, params).prune(params[:mode].to_sym == :force,
|
84
|
+
params[:mode].to_sym == :depend)
|
85
|
+
if block != nil
|
86
|
+
puts "Preparing sample #{sample}"
|
87
|
+
{sample => block}
|
88
|
+
else
|
89
|
+
puts "Nothing to do for sample #{sample}"
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Produce a {sample => block} hash
|
95
|
+
sample_blocks.compact.inject(&:merge)
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Prepare a worker from a group of sample blocks.
|
100
|
+
#
|
101
|
+
# @param worker_sample_blocks [Hash] a list containing as many elements
|
102
|
+
# as there are samples in the group, with each element containing
|
103
|
+
# +[String, Blocks::Block]+
|
104
|
+
# @param params [Hash] worker-level parameter list
|
105
|
+
# @return [DB::Worker] worker
|
106
|
+
|
107
|
+
def prepare_worker(worker_sample_blocks, params)
|
108
|
+
worker = DB::Worker.create(handle: params[:handle])
|
109
|
+
worker_blocks = prepare_worker_blocks(worker_sample_blocks, worker)
|
110
|
+
|
111
|
+
# Combine all grouped sample blocks into a single worker block
|
112
|
+
|
113
|
+
params = params.merge({
|
114
|
+
name: worker.id,
|
115
|
+
stdout: worker.stdout,
|
116
|
+
stderr: worker.stderr,
|
117
|
+
command: Blocks::SerialBlock.new(*worker_blocks).command,
|
118
|
+
})
|
119
|
+
|
120
|
+
worker_block = Blocks::LiquidBlock.new("#{PATH}/share/moab.sh", params)
|
121
|
+
File.open(worker.sh, 'w') { |f| f.write(worker_block.command) }
|
122
|
+
|
123
|
+
worker.update({
|
124
|
+
status: :prepared,
|
125
|
+
ppn: params[:ppn],
|
126
|
+
queue: params[:queue],
|
127
|
+
walltime: params[:walltime],
|
128
|
+
})
|
129
|
+
|
130
|
+
worker
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Organize worker blocks into tasks and prepare them.
|
135
|
+
#
|
136
|
+
# @param worker_sample_blocks [Array<Hash<String, Blocks::Block>>] a list
|
137
|
+
# containing as many elements as there are samples in the group
|
138
|
+
# @param worker [DB::Worker] worker
|
139
|
+
# @return [Array<Blocks::Block>] a list of all the prepared blocks for a
|
140
|
+
# worker
|
141
|
+
|
142
|
+
def prepare_worker_blocks(worker_sample_blocks, worker)
|
143
|
+
worker_sample_blocks.map do |sample, block|
|
144
|
+
# Preorder traversal of blocks -- assign incremental numbers starting from
|
145
|
+
# 1 to each node as it is being traversed, as well as producing the job
|
146
|
+
# file for each task.
|
147
|
+
post_var_assign = lambda do |subblock|
|
148
|
+
if subblock.blocks.length == 0
|
149
|
+
# This section is only called when the subblock is actually a working
|
150
|
+
# block (a leaf in the block arborescence).
|
151
|
+
task = worker.tasks.create({
|
152
|
+
sample: sample,
|
153
|
+
block: subblock.id,
|
154
|
+
})
|
155
|
+
|
156
|
+
File.open(task.sh, 'w') { |f| f.write(subblock.command) }
|
157
|
+
subblock.vars.merge!(log: task.log)
|
158
|
+
else
|
159
|
+
subblock.blocks.each(&post_var_assign)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
post_var_assign.call(block)
|
164
|
+
block
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Ripe
|
2
|
+
|
3
|
+
class WorkerController
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class controls worker syncing with the compute cluster queue.
|
7
|
+
#
|
8
|
+
# @attr_reader running_jobs [Array<Hash<Symbol, String>>] a list of running
|
9
|
+
# jobs as well as certain parameters (+moab_id+, +time+ and +status).
|
10
|
+
# @attr_reader completed_jobs [Array<DB::Worker>] a list of completed
|
11
|
+
# workers
|
12
|
+
# @attr_reader workers [Array<DB::Worker>] list of workers that have been
|
13
|
+
# updated (or completed)
|
14
|
+
#
|
15
|
+
# @see Ripe::WorkerController#sync
|
16
|
+
|
17
|
+
class Syncer
|
18
|
+
|
19
|
+
attr_reader :running_jobs, :completed_jobs, :workers
|
20
|
+
|
21
|
+
##
|
22
|
+
# Synchronize the status of jobs with the internal list of workers.
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@workers = []
|
26
|
+
|
27
|
+
fetch_running_jobs
|
28
|
+
update_running_workers
|
29
|
+
fetch_completed_jobs
|
30
|
+
update_completed_workers
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Fetch status for all running jobs.
|
35
|
+
#
|
36
|
+
# @return [void]
|
37
|
+
|
38
|
+
def fetch_running_jobs
|
39
|
+
lists = {idle: '-i', blocked: '-b', active: '-r'}
|
40
|
+
lists = lists.map do |status, op|
|
41
|
+
showq = `showq -u $(whoami) #{op} | grep $(whoami)`.split("\n")
|
42
|
+
showq.map do |job|
|
43
|
+
{
|
44
|
+
moab_id: job[/^([0-9]+) /, 1],
|
45
|
+
time: job[/ ([0-9]{1,2}(\:[0-9]{2})+) /, 1],
|
46
|
+
status: status,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@running_jobs = lists.inject(&:+)
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Update the status of running workers from the running jobs.
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
|
58
|
+
def update_running_workers
|
59
|
+
@workers += @running_jobs.map do |job|
|
60
|
+
worker = DB::Worker.find_by(moab_id: job[:moab_id])
|
61
|
+
if worker
|
62
|
+
worker.update(time: job[:time])
|
63
|
+
unless ['cancelled', job[:status]].include?(worker.status)
|
64
|
+
checkjob = `checkjob #{job[:moab_id]}`
|
65
|
+
worker.update({
|
66
|
+
host: checkjob[/Allocated Nodes:\n\[(.*):[0-9]+\]\n/, 1],
|
67
|
+
# Queued jobs that appear become either idle, blocked or active
|
68
|
+
status: job[:status],
|
69
|
+
})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
worker
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Fetch a list of completed workers from the running jobs: these are jobs
|
78
|
+
# that were previously marked as active, blocked or idle that can no
|
79
|
+
# be found on the compute cluster queue.
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
|
83
|
+
def fetch_completed_jobs
|
84
|
+
running_job_ids = @running_jobs.map { |job| job[:moab_id] }
|
85
|
+
|
86
|
+
running_workers = DB::Worker.where('status in (:statuses)',
|
87
|
+
:statuses => ['active', 'idle', 'blocked'])
|
88
|
+
|
89
|
+
@completed_workers = running_workers.select do |worker|
|
90
|
+
!running_job_ids.include?(worker.moab_id) &&
|
91
|
+
worker.status != 'cancelled'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Update the status of completed workers from the running jobs.
|
97
|
+
#
|
98
|
+
# @return [void]
|
99
|
+
|
100
|
+
def update_completed_workers
|
101
|
+
@workers += @completed_workers.map do |worker|
|
102
|
+
stdout = (File.exists?(worker.stdout)) ? File.new(worker.stdout).readlines.join : ""
|
103
|
+
worker.update({
|
104
|
+
cpu_used: stdout[/Resources:[ \t]*cput=([0-9]{1,2}(\:[0-9]{2})+),/, 1],
|
105
|
+
exit_code: stdout[/Exit code:[ \t]*(.*)$/, 1],
|
106
|
+
host: stdout[/Nodes:[ \t]*(.*)$/, 1],
|
107
|
+
memory_used: stdout[/Resources:.*,mem=([0-9]*[a-zA-Z]*),/, 1],
|
108
|
+
time: stdout[/Resources:.*,walltime=([0-9]{1,2}(\:[0-9]{2})+)$/, 1],
|
109
|
+
status: :completed,
|
110
|
+
})
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CLI do
|
4
|
+
describe '::Helper#parse_cli_opts' do
|
5
|
+
it 'parses string options into hash options' do
|
6
|
+
string_opts = 'a=1,b=2,c=3'
|
7
|
+
test_hash_opts = CLI::Helper.parse_cli_opts(string_opts)
|
8
|
+
|
9
|
+
ref_hash_opts = {a: '1', b: '2', c: '3'}
|
10
|
+
|
11
|
+
expect(test_hash_opts).to eql ref_hash_opts
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/spec/library_spec.rb
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe Library do
|
4
4
|
context 'when RIPELIB env is empty' do
|
5
|
-
before
|
5
|
+
before :each do
|
6
6
|
ENV['RIPELIB'] = ''
|
7
|
-
@library =
|
7
|
+
@library = Library
|
8
8
|
end
|
9
9
|
|
10
10
|
it 'looks in the working directory' do
|
11
|
-
expect(@library.paths).to eql ["#{Dir.pwd}/#{
|
11
|
+
expect(@library.paths).to eql ["#{Dir.pwd}/#{Repo::REPOSITORY_PATH}"]
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'cannot resolve components of the test library' do
|
15
|
-
expect(@library.
|
16
|
-
expect(@library.
|
17
|
-
expect(@library.
|
15
|
+
expect(@library.find(:task, 'foo')).to eql nil
|
16
|
+
expect(@library.find(:task, 'bar')).to eql nil
|
17
|
+
expect(@library.find(:workflow, 'foobar')).to eql nil
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
context 'when RIPELIB contains the test library' do
|
22
|
-
before
|
23
|
-
@test =
|
24
|
-
ENV['RIPELIB'] = @test.
|
25
|
-
@library =
|
22
|
+
before :each do
|
23
|
+
@test = TestPack.new
|
24
|
+
ENV['RIPELIB'] = @test.lib_path
|
25
|
+
@library = Library
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'looks in two directories' do
|
@@ -32,22 +32,22 @@ describe Ripe::Library do
|
|
32
32
|
it 'looks in the working directory first' do
|
33
33
|
# It looks in the working directory, and then in the directory
|
34
34
|
# specified in RIPELIB.
|
35
|
-
expect(@library.paths[0]).to eql "#{Dir.pwd}/#{
|
36
|
-
expect(@library.paths[1]).to eql @test.
|
35
|
+
expect(@library.paths[0]).to eql "#{Dir.pwd}/#{Repo::REPOSITORY_PATH}"
|
36
|
+
expect(@library.paths[1]).to eql @test.lib_path
|
37
37
|
end
|
38
38
|
|
39
39
|
it 'resolves task components of the test library' do
|
40
|
-
expect(@library.
|
41
|
-
expect(@library.
|
40
|
+
expect(@library.find(:task, 'foo')).to eql @test.tasks['foo']
|
41
|
+
expect(@library.find(:task, 'bar')).to eql @test.tasks['bar']
|
42
42
|
end
|
43
43
|
|
44
44
|
it 'resolves workflows components of the test library' do
|
45
|
-
expect(@library.
|
45
|
+
expect(@library.find(:workflow, 'foobar')).to eql @test.workflows['foobar']
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'cannot resolve non-existing componenets' do
|
49
|
-
expect(@library.
|
50
|
-
expect(@library.
|
49
|
+
expect(@library.find(:task, 'other')).to eql nil
|
50
|
+
expect(@library.find(:workflow, 'other')).to eql nil
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/testpack.rb
CHANGED
@@ -1,16 +1,27 @@
|
|
1
1
|
module Ripe
|
2
2
|
class TestPack
|
3
|
-
attr_reader :path, :tasks, :workflows
|
3
|
+
attr_reader :path, :lib_path, :tasks, :workflows, :samples, :steps
|
4
4
|
|
5
5
|
def initialize
|
6
|
-
@path = "#{
|
6
|
+
@path = "#{PATH}/spec/testpack"
|
7
|
+
@lib_path = "#{@path}/#{Repo::REPOSITORY_PATH}"
|
7
8
|
@tasks = {
|
8
|
-
'foo' => "#{@
|
9
|
-
'bar' => "#{@
|
9
|
+
'foo' => "#{@lib_path}/tasks/foo.sh",
|
10
|
+
'bar' => "#{@lib_path}/tasks/bar.sh",
|
10
11
|
}
|
11
12
|
@workflows = {
|
12
|
-
'foobar' => "#{@
|
13
|
+
'foobar' => "#{@lib_path}/workflows/foobar.rb",
|
13
14
|
}
|
15
|
+
@samples = [
|
16
|
+
'Sample1',
|
17
|
+
'Sample2',
|
18
|
+
'Sample3'
|
19
|
+
]
|
20
|
+
@steps = [
|
21
|
+
'foo_input.txt',
|
22
|
+
'foo_output.txt',
|
23
|
+
'bar_output.txt'
|
24
|
+
]
|
14
25
|
end
|
15
26
|
end
|
16
27
|
end
|
Binary file
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
# <foo.sh>
|
3
|
+
|
4
|
+
INPUT_FOO="Sample1/foo_input.txt"
|
5
|
+
FOO_MESSAGE="For You"
|
6
|
+
OUTPUT_FOO="Sample1/foo_output.txt"
|
7
|
+
|
8
|
+
exec 1>"$LOG" 2>&1
|
9
|
+
|
10
|
+
# Foo is certainly one of the most important prerequisites to Bar.
|
11
|
+
|
12
|
+
echo "$(cat "$INPUT_FOO") $FOO_MESSAGE" > "$OUTPUT_FOO"
|
13
|
+
|
14
|
+
echo "##.DONE.##"
|
15
|
+
|
16
|
+
# </foo.sh>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
# <bar.sh>
|
3
|
+
|
4
|
+
INPUT_BAR="Sample1/foo_input.txt"
|
5
|
+
BAR_MESSAGE="Bar"
|
6
|
+
OUTPUT_BAR="Sample1/bar_output.txt"
|
7
|
+
|
8
|
+
exec 1>"$LOG" 2>&1
|
9
|
+
|
10
|
+
# Bar is the most important consequence of Foo.
|
11
|
+
|
12
|
+
echo "$(cut -d' ' -f1 "$INPUT_BAR") $BAR_MESSAGE" > "$OUTPUT_BAR"
|
13
|
+
|
14
|
+
echo "##.DONE.##"
|
15
|
+
|
16
|
+
# </bar.sh>
|