cl-magic 0.4.0 → 1.2.0

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.
data/lib/cl/magic/cl-dk CHANGED
@@ -10,101 +10,18 @@ require 'cl/magic/common/common_options.rb'
10
10
  require 'cl/magic/common/logging.rb'
11
11
  require 'cl/magic/common/parse_and_pick.rb'
12
12
 
13
- @logger = get_logger()
14
-
15
- #
16
- # Util Methods
17
- #
18
-
19
- def get_base_compose_hash()
20
- cmd = "cd #{@working_dir} && docker compose config 2> /dev/null"
21
- return YAML.load(`#{cmd}`)
22
- end
23
-
24
- def get_save_parts_filepath()
25
- return File.join(@working_dir, ".cl-dk-parts.yml")
26
- end
13
+ require_relative 'dk/help_printer'
14
+ require_relative 'dk/yaml_arg_munger'
15
+ require_relative 'dk/world_settings'
16
+ require_relative 'dk/parts_merger'
27
17
 
28
- def get_base_compose_parts_and_make_hashes()
29
- compose_hash = get_base_compose_hash()
30
- dk_parts_hash = {}
31
- dk_make_hash = {}
32
- if compose_hash
33
- compose_hash = merge_world_files(compose_hash, show_help=ARGV.include?("--help"))
34
- dk_parts_hash = compose_hash['x-dk-parts'] ? compose_hash.delete('x-dk-parts') : {}
35
- dk_make_hash = compose_hash['x-dk-make'] ? compose_hash.delete('x-dk-make') : {}
36
- end
37
- return compose_hash, dk_parts_hash, dk_make_hash
38
- end
39
-
40
- # world
41
-
42
- def get_repo_basename()
43
- command = "cd #{@working_dir} && basename $(git remote get-url origin 2> /dev/null) .git"
44
- repo_basename = TTY::Command.new(:printer => :null).run(command).out.gsub('.git', '').strip.chomp
45
- if repo_basename==".git" or repo_basename==""
46
- return File.basename(@working_dir)
47
- end
48
- return repo_basename
49
- end
50
-
51
- def get_world_settings_filepath()
52
- return File.join(".cl-dk-world.yml")
53
- end
54
-
55
- def get_world_settings_hash()
56
- filepath = get_world_settings_filepath()
57
- return File.exist?(filepath) ? YAML.load_file(filepath) : {}
58
- end
59
-
60
- def get_world_path_from_settings()
61
- world_settings = get_world_settings_hash()
62
- if world_settings.key?(:world_path) and world_settings.key?(:context)
63
- return File.join(world_settings[:world_path], world_settings[:context])
64
- end
65
- return ""
66
- end
67
-
68
- def save_world_settings(world_settings_hash)
69
- filepath = get_world_settings_filepath()
70
- tempfile = File.new(filepath, 'w')
71
- tempfile.write(world_settings_hash.to_yaml)
72
- tempfile.close
73
- end
74
-
75
- def get_world_project_path()
76
- repo_basename = get_repo_basename()
77
- world_path = get_world_path_from_settings()
78
- return File.join(world_path, repo_basename) if world_path and repo_basename
79
- return nil
80
- end
18
+ @logger = get_logger()
81
19
 
82
20
  #
83
21
  # Help Methods
84
22
  #
85
23
 
86
- def print_dk_help_line(key, help)
87
- if $stdout.isatty
88
- if help.nil?
89
- @logger.puts("#{key.ljust(15, ' ')} ???no help???")
90
- else
91
- key = key.ljust(15, ' ')
92
- help_parts = help.split(";")
93
-
94
- # first line
95
- @logger.puts(key, help_parts.shift)
96
-
97
- # following lines
98
- padding = "".ljust(15, ' ')
99
- help_parts.each do |p|
100
- @logger.puts(padding, p)
101
- end
102
- @logger.puts("") if help.end_with?(";")
103
- end
104
- end
105
- end
106
-
107
- def print_dk_help(dk_parts_hash, dk_make_hash, args)
24
+ def try_print_dk_help(dk_parts_hash, dk_make_hash, args)
108
25
  no_args = args.empty?()
109
26
  asked_for_help = args.include?('--help')
110
27
  has_dk_commands = dk_parts_hash.keys.any?
@@ -115,15 +32,9 @@ def print_dk_help(dk_parts_hash, dk_make_hash, args)
115
32
  puts ""
116
33
  puts "Run docker compose while munging yamls in sophisticated ways."
117
34
  puts ""
118
- if get_repo_basename
119
- puts "PROJ INFO"
120
- puts " - Repo basename: #{get_repo_basename}"
121
- puts " - World filepath: #{get_world_project_path()}"
122
- puts ""
123
- end
124
35
  puts "PROJ PARTS"
125
36
  dk_parts_hash.keys.each do |key|
126
- print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
37
+ @help_printer.print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
127
38
  end
128
39
  puts ""
129
40
  puts "YML TOKENS"
@@ -133,208 +44,10 @@ def print_dk_help(dk_parts_hash, dk_make_hash, args)
133
44
  puts " - <dk-project-path> absolute filepath to world/project directory"
134
45
  puts " - <dk-working-path> absolute filepath to location dk command was run from"
135
46
  puts ""
136
- puts "ADDITIONAL TURNKEY COMMANDS"
137
- puts " - dk set-world sets the location of the world directory"
138
- puts " - dk make turnkey commands for a project"
139
- puts " - dk set-parts save parts so they are automatically applied to commands"
140
- puts ""
141
47
  puts "-------------------------"
142
48
  end
143
49
  end
144
50
 
145
- def print_make_help(dk_parts_hash, dk_make_hash)
146
- if $stdout.isatty
147
- puts ""
148
- puts "Usage: dk [DK_PARTS] make COMMAND"
149
- puts ""
150
- puts "Make commands designed to make your developer experience more turnkey"
151
- puts ""
152
- puts "Parts:"
153
- dk_parts_hash.keys.each do |key|
154
- print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
155
- end
156
- puts ""
157
- puts "Commands:"
158
- dk_make_hash.keys.each do |key|
159
- print_dk_help_line(key, dk_make_hash[key].fetch('help'))
160
- end
161
- puts ""
162
- else
163
- dk_make_hash.keys.each do |key|
164
- puts key
165
- end
166
- end
167
- end
168
-
169
- def print_set_world_help()
170
- puts ""
171
- puts "Usage: dk set-world PATH"
172
- puts ""
173
- puts "Set the folder that contains dk-world configuration"
174
- puts ""
175
- end
176
-
177
- def print_set_parts_help()
178
- compose_hash, dk_parts_hash, dk_make_hash = get_base_compose_parts_and_make_hashes()
179
-
180
- # print help
181
- puts ""
182
- puts "Usage: dk [DK_PARTS] set-parts"
183
- puts ""
184
- puts "Set parts for a project and apply to every 'dk' command"
185
- puts ""
186
- if dk_parts_hash.keys.any?
187
- puts "Parts:"
188
- dk_parts_hash.keys.each do |key|
189
- print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
190
- end
191
- puts ""
192
- end
193
- end
194
-
195
- #
196
- # Yaml Merging
197
- #
198
-
199
- class ::Hash
200
- def dk_merge(second)
201
- merger = proc { |_, v1, v2|
202
- if Hash === v1 && Hash === v2
203
- v1.merge(v2, &merger)
204
- else
205
- if Array === v1 && Array === v2
206
- if v2.first=="<dk-replace>"
207
- v2[1..-1] # everything but the first item
208
- else
209
- v1 | v2 # union arrays
210
- end
211
- else
212
- if [:undefined, nil, :nil].include?(v2)
213
- v1
214
- else
215
- v2
216
- end
217
- end
218
- end
219
- }
220
- merge(second.to_h, &merger)
221
- end
222
- def dk_reject!(&blk)
223
- self.each do |k, v|
224
- v.dk_reject!(&blk) if v.is_a?(Hash)
225
- self.delete(k) if blk.call(k, v)
226
- end
227
- end
228
- end
229
-
230
- def dk_merge_and_remove(compose_hash, yml_hash)
231
- # remove help & merge
232
- clean_yml = yml_hash.clone
233
- clean_yml.delete('help')
234
- return compose_hash.dk_merge(clean_yml).dk_reject! { |k, v| v=='<dk-remove>' }
235
- end
236
-
237
- def merge_world_files(compose_hash, show_help=false)
238
- dk_proj_path = get_world_project_path()
239
- if dk_proj_path
240
- print_dk_help_line("dk-project-path", "#{dk_proj_path}") if show_help and $stdout.isatty
241
-
242
- Dir.glob("#{dk_proj_path}/*.yml").sort.each do |filepath|
243
- print_dk_help_line("dk-world", "#{filepath}") if show_help and $stdout.isatty
244
-
245
- # read file and replace
246
- contents = File.read(filepath)
247
- contents.gsub!('<dk-world-path>', get_world_path_from_settings())
248
- contents.gsub!('<dk-project-path>', dk_proj_path)
249
- contents.gsub!('<dk-working-path>', @working_dir)
250
-
251
- # yml merge
252
- yml_hash = YAML.load(contents)
253
- compose_hash = dk_merge_and_remove(compose_hash, yml_hash)
254
- end
255
- end
256
- return compose_hash
257
- end
258
-
259
- def get_saved_parts(dk_parts_hash)
260
- saved_part_keys = []
261
- filepath = get_save_parts_filepath()
262
-
263
- # merge
264
- if File.exist?(filepath)
265
- saved_parts = YAML.load_file(filepath)
266
- saved_parts.each do |potential_part_key|
267
- dk_part = dk_parts_hash.fetch(potential_part_key, nil) # yml detail
268
- if dk_part
269
- saved_part_keys << potential_part_key
270
- else
271
- @logger.error "invalid saved part: #{potential_part_key}"
272
- exit 1
273
- end
274
- end
275
- end
276
- return saved_part_keys
277
- end
278
-
279
- def merge_parts_save_and_prep_args(compose_hash, dk_parts_hash, dk_make_hash, saved_part_keys)
280
- tempfile = File.new(File.join(@working_dir, ".cl-dk.yml"), 'w')
281
- args = ARGV.clone
282
-
283
- # dk help
284
- print_dk_help(dk_parts_hash, dk_make_hash, args)
285
-
286
- def print_and_merge_part(part_key, dk_part, compose_hash)
287
-
288
- # print
289
- if $stdout.isatty
290
- help_str = dk_part.fetch('help')
291
- print_dk_help_line("#{part_key}", "#{help_str ? '- ' + help_str : ''}") if dk_part.keys.any?
292
- end
293
- # merge
294
- return dk_merge_and_remove(compose_hash, dk_part)
295
- end
296
-
297
- # saved parts
298
- saved_part_keys.each do |potential_part_key|
299
- dk_part = dk_parts_hash.fetch(potential_part_key, nil) # yml detail
300
- compose_hash = print_and_merge_part(potential_part_key, dk_part, compose_hash) if dk_part
301
- end
302
-
303
- # arg parts
304
- selected_part_keys = []
305
- while true
306
- potential_part_key = args.first
307
- dk_part = dk_parts_hash.fetch(potential_part_key, nil) # yml detail
308
- if dk_part
309
-
310
- # unless already applied
311
- unless saved_part_keys.include?(potential_part_key)
312
- compose_hash = print_and_merge_part(potential_part_key, dk_part, compose_hash)
313
- end
314
- selected_part_keys << potential_part_key
315
- args.shift # remove part arg
316
- else
317
- break
318
- end
319
- end
320
- @logger.puts "" if $stdout.isatty # tailing line break
321
- tempfile.write(compose_hash.to_yaml) # write it to the tempfile
322
-
323
- # clone args
324
- args = args.clone
325
-
326
- # remove existing '-f' flag, if needed
327
- file_flag_index = args.index('-f')
328
- if file_flag_index==0
329
- args.delete_at(file_flag_index)
330
- args.delete_at(file_flag_index)
331
- end
332
- args.unshift('-f', tempfile.path) # add new '-f' flag
333
-
334
- tempfile.close
335
- return args, selected_part_keys
336
- end
337
-
338
51
  #
339
52
  # Run: DK
340
53
  #
@@ -345,147 +58,29 @@ def run_dk(compose_args)
345
58
  end
346
59
 
347
60
  #
348
- # Run: SET WORLD
61
+ # Features
349
62
  #
350
63
 
351
- def run_dk_set_world()
352
- world_settings = get_world_settings_hash()
64
+ def do_work()
353
65
 
354
- # world path
355
- world_path = ARGV[1]
356
- if world_path.nil?
357
- print_set_world_help
358
- exit
359
- else
360
- if world_path.start_with?("/") or world_path.start_with?("~")
361
- world_path = File.expand_path(world_path)
362
- else
363
- world_path = File.expand_path(File.join(@working_dir, world_path))
364
- end
365
- end
66
+ # utils
67
+ @help_printer = HelpPrinter.new(@logger)
68
+ @world_settings = WorldSettings.new(@working_dir)
69
+ @yaml_arg_munger = YamlArgMunger.new(@working_dir, @world_settings)
70
+ @parts_merger = PartsMerger.new(@working_dir, @yaml_arg_munger, @help_printer, @logger)
366
71
 
367
- if TTY::Prompt.new.yes?("Set world to: #{world_path}?")
368
- world_settings[:world_path] = world_path
369
- save_world_settings(world_settings)
370
- run_dk_set_context()
72
+ # world files
73
+ compose_hash, dk_parts_hash, dk_make_hash = @yaml_arg_munger.get_base_compose_parts_and_make_hashes()
74
+ if compose_hash
75
+ try_print_dk_help(dk_parts_hash, dk_make_hash, ARGV.clone)
76
+ compose_hash, selected_parts, shifted_args = @parts_merger.merge_parts(compose_hash, dk_parts_hash, ARGV.clone)
77
+ final_args = @yaml_arg_munger.save_yaml_and_adjust_args(compose_hash, shifted_args)
78
+ run_dk(final_args)
371
79
  else
372
- exit
373
- end
374
-
375
- end
376
-
377
- def run_dk_set_context()
378
- world_settings = get_world_settings_hash()
379
- unless world_settings[:world_path] and File.directory?(world_settings[:world_path])
380
- @logger.error "no world path set."
80
+ @logger.error "no docker configuration found"
381
81
  exit 1
382
82
  end
383
83
 
384
- # read folder from world path (aka. context)
385
- all_dir = Dir.chdir(world_settings[:world_path]) do
386
- Dir.glob('*').select { |f| File.directory? f and f != "common" }
387
- end
388
- selected_dir = pick_single_result(all_dir.collect {|f| [f]}, "select context").first
389
- world_settings[:context] = selected_dir
390
- save_world_settings(world_settings)
391
-
392
- @logger.puts "world set!"
393
- exit
394
- end
395
-
396
- #
397
- # Run: SET PARTS
398
- #
399
-
400
- def run_dk_set_parts()
401
- parts = ARGV[1..]
402
- filepath = get_save_parts_filepath()
403
- if parts.any?
404
- tempfile = File.new(filepath, 'w')
405
- tempfile.write(ARGV[1..].to_yaml) # write it to the tempfile
406
- tempfile.close
407
- else
408
- if File.exist?(filepath)
409
- File.delete(filepath)
410
- @logger.info "parts cleared"
411
- else
412
- print_set_parts_help()
413
- end
414
- end
415
- exit
416
- end
417
-
418
- #
419
- # Run: MAKE
420
- #
421
-
422
- def run_dk_make(compose_args, dk_make_hash, dk_parts_hash, selected_part_keys)
423
- # print help?
424
- no_make_command = (compose_args.count == 3)
425
- if no_make_command
426
- print_make_help(dk_parts_hash, dk_make_hash)
427
- else
428
-
429
- #
430
- # supports running multiple commands in a row
431
- # ex. dk make down up
432
- #
433
-
434
- make_commands = compose_args[3..]
435
- make_commands.each_with_index do |key, i|
436
-
437
- if not dk_make_hash.has_key?(key)
438
- @logger.error "#{key} - command not found"
439
- exit 1
440
- else
441
-
442
- all_commands = dk_make_hash[key]["commands"]
443
-
444
- #
445
- # all commands run in a subprocess except the last one, so
446
- # the last command can run in the forground and be
447
- # interactive (ex. dk make down up bash)
448
- #
449
-
450
- # construct last command
451
- last_cmd = if make_commands.length == i+1
452
- all_commands.pop
453
- else
454
- nil
455
- end
456
-
457
- # run background commands
458
- all_commands.each do |c|
459
- cmd = prep_make_command(c, selected_part_keys)
460
- `#{cmd}` # run in sub-process
461
- end
462
-
463
- # run last command in foreground
464
- if last_cmd
465
- cmd = prep_make_command(last_cmd, selected_part_keys)
466
- exec(cmd)
467
- end
468
- end
469
- end
470
-
471
- end
472
- end
473
-
474
- def prep_make_command(c, selected_part_keys)
475
- c = interpolate_parts_into_command(c, selected_part_keys)
476
-
477
- # logging
478
- @logger.puts "" if $stdout.isatty
479
- @logger.wait(c)
480
- cmd = "cd #{@working_dir} && #{c}"
481
- end
482
-
483
- def interpolate_parts_into_command(cmd, selected_part_keys)
484
- if selected_part_keys.any?
485
- parts = selected_part_keys.join(' ')
486
- return cmd.gsub("cl dk", "cl dk #{parts}")
487
- end
488
- return cmd
489
84
  end
490
85
 
491
86
  #
@@ -494,32 +89,4 @@ end
494
89
 
495
90
  @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
496
91
 
497
- # SET commands first
498
- case ARGV.first
499
- when "set-world"
500
- run_dk_set_world()
501
- when "set-context"
502
- run_dk_set_context()
503
- when "set-parts"
504
- run_dk_set_parts()
505
- else
506
- compose_hash, dk_parts_hash, dk_make_hash = get_base_compose_parts_and_make_hashes()
507
- if compose_hash
508
-
509
- # parts & args
510
- saved_part_keys = get_saved_parts(dk_parts_hash)
511
- compose_args, selected_part_keys = merge_parts_save_and_prep_args(compose_hash, dk_parts_hash, dk_make_hash, saved_part_keys)
512
-
513
- # sub-command
514
- case compose_args[2]
515
- when "make"
516
- all_part_keys = selected_part_keys + saved_part_keys
517
- run_dk_make(compose_args, dk_make_hash, dk_parts_hash, all_part_keys)
518
- else
519
- run_dk(compose_args)
520
- end
521
- else
522
- @logger.error "no docker configuration found"
523
- exit 1
524
- end
525
- end
92
+ do_work()
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env ruby
2
+ # cli for your compose projects
3
+ require 'optparse'
4
+ require 'optparse/subcommand'
5
+
6
+ require 'cl/magic/common/common_options.rb'
7
+ require 'cl/magic/common/logging.rb'
8
+
9
+ require_relative 'dk/help_printer'
10
+ require_relative 'dk/yaml_arg_munger'
11
+ require_relative 'dk/world_settings'
12
+ require_relative 'dk/parts_merger'
13
+
14
+ @logger = get_logger()
15
+ @cl_cmd_name = File.basename(__FILE__).split('-').join(' ')
16
+
17
+ def print_make_help(dk_parts_hash, dk_make_hash)
18
+ if $stdout.isatty
19
+ puts ""
20
+ puts "Usage: dk make [DK_PARTS] COMMAND"
21
+ puts ""
22
+ puts "Make commands designed to make your developer experience more turnkey"
23
+ puts ""
24
+ puts "Parts:"
25
+ dk_parts_hash.keys.each do |key|
26
+ @help_printer.print_dk_help_line(key, dk_parts_hash[key].fetch('help'))
27
+ end
28
+ puts ""
29
+ puts "Commands:"
30
+ dk_make_hash.keys.each do |key|
31
+ @help_printer.print_dk_help_line(key, dk_make_hash[key].fetch('help'))
32
+ end
33
+ puts ""
34
+ else
35
+ dk_make_hash.keys.each do |key|
36
+ puts key
37
+ end
38
+ end
39
+ end
40
+
41
+ def run_dk_make(final_args, dk_make_hash, dk_parts_hash, selected_parts)
42
+
43
+ # print help?
44
+ no_make_command = (final_args.count == 2)
45
+ if no_make_command
46
+ print_make_help(dk_parts_hash, dk_make_hash)
47
+ else
48
+
49
+ #
50
+ # supports running multiple commands in a row
51
+ # ex. dk make down up
52
+ #
53
+
54
+ make_commands = final_args[2..]
55
+ make_commands.each_with_index do |key, i|
56
+
57
+ if not dk_make_hash.has_key?(key)
58
+ @logger.error "#{key} - command not found"
59
+ exit 1
60
+ else
61
+
62
+ all_commands = dk_make_hash[key]["commands"]
63
+
64
+ #
65
+ # all commands run in a subprocess except the last one, so
66
+ # the last command can run in the forground and be
67
+ # interactive (ex. dk make down up bash)
68
+ #
69
+
70
+ # construct last command
71
+ last_cmd = if make_commands.length == i+1
72
+ all_commands.pop
73
+ else
74
+ nil
75
+ end
76
+
77
+ # run background commands
78
+ all_commands.each do |c|
79
+ cmd = prep_make_command(c, selected_parts)
80
+ `#{cmd}` # run in sub-process
81
+ end
82
+
83
+ # run last command in foreground
84
+ if last_cmd
85
+ cmd = prep_make_command(last_cmd, selected_parts)
86
+ exec(cmd)
87
+ end
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+
94
+ def prep_make_command(c, selected_parts)
95
+ # logging
96
+ @logger.puts "" if $stdout.isatty
97
+ @logger.wait(c.gsub("cl dk", "dk"))
98
+
99
+ # run command
100
+ c = interpolate_parts_into_command(c, selected_parts)
101
+ cmd = "cd #{@working_dir} && #{c}"
102
+ end
103
+
104
+ def interpolate_parts_into_command(cmd, selected_parts)
105
+ cmd = "cl #{cmd}" if cmd.start_with?("dk")
106
+
107
+ # subcommands: don't need interpolated parts
108
+ ['parts', 'make', 'make-world', 'world'].each do |subcommand|
109
+ return cmd if cmd.start_with?("cl dk #{subcommand}")
110
+ end
111
+
112
+ if selected_parts.any?
113
+ parts = selected_parts.join(' ')
114
+ if cmd.include? "cl dk make"
115
+ return cmd.gsub("cl dk make", "cl dk make #{parts}")
116
+ else
117
+ return cmd.gsub("cl dk", "cl dk #{parts}")
118
+ end
119
+ end
120
+ return cmd
121
+ end
122
+
123
+ #
124
+ # Features
125
+ #
126
+
127
+ def do_work()
128
+
129
+ # utils
130
+ @help_printer = HelpPrinter.new(@logger)
131
+ @world_settings = WorldSettings.new(@working_dir)
132
+ @yaml_arg_munger = YamlArgMunger.new(@working_dir, @world_settings)
133
+ @parts_merger = PartsMerger.new(@working_dir, @yaml_arg_munger, @help_printer, @logger)
134
+
135
+ # world files
136
+ compose_hash, dk_parts_hash, dk_make_hash = @yaml_arg_munger.get_base_compose_parts_and_make_hashes()
137
+ if compose_hash
138
+ compose_hash, selected_parts, shifted_args = @parts_merger.merge_parts(compose_hash, dk_parts_hash, ARGV.clone)
139
+ final_args = @yaml_arg_munger.save_yaml_and_adjust_args(compose_hash, shifted_args)
140
+ run_dk_make(final_args, dk_make_hash, dk_parts_hash, selected_parts)
141
+ else
142
+ @logger.error "no docker configuration found"
143
+ exit 1
144
+ end
145
+
146
+ end
147
+
148
+ #
149
+ # Options
150
+ #
151
+
152
+ options = {}
153
+ global_banner = <<DOC
154
+
155
+ Cli for your compose projects
156
+
157
+ Usage: #{@cl_cmd_name} [options]
158
+
159
+ DOC
160
+
161
+ global = OptionParser.new do |g|
162
+ g.banner = global_banner
163
+ add_help_and_verbose(g)
164
+ end
165
+
166
+ #
167
+ # Run
168
+ #
169
+
170
+ @working_dir = ENV['CL_WORKING_DIR'] # passed through cl-magic to here
171
+
172
+ global.parse(ARGV)
173
+
174
+ do_work()