miga-base 1.1.3.6 → 1.2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 529121bd6f55dfeb21559b77af71a4537db91cda1fe2fdfc9bdee9adfcaaa962
4
- data.tar.gz: 8b892eb88019c4c5d0ec654c722142004867665892a38cf96233afc0b1f30aef
3
+ metadata.gz: afb7c816474f73c547c7f54982293fe72c24d7829184801ab51fc848adf3d9e3
4
+ data.tar.gz: 06d51ebc47e96c28072c8415321014535ea7576d9e6f1c381e3ca62dc880df15
5
5
  SHA512:
6
- metadata.gz: ab6397d8b74d163cac4afa9a8e79776b91c1c4175da947d669bcc9ae441d04a0345f9c31feffbbb6349e5d9375ccd61b6396740763f3d96d0606a9f2312d9ad0
7
- data.tar.gz: db534b553aca1ecbcee679edaf33b7cdea972e648b507b2a2e7841f8865f7572e3d498714672b8f49b21babda9f5db311bb580a4e8f90e70470a6fff13f51e6e
6
+ metadata.gz: a4f5279fdba54ad1f8bcbce86cb84a7643b473b7742686e8e9992f8c4af7c982d5776a5a06775b10df67b61de90c4a32f915b51a17a9b11a223474574e7f8159
7
+ data.tar.gz: bcd547a1dc5337fbf036ba8e2df6de6db4c4fb34979afeb30ce6c1a692e76166b5412457fb3cc778e03cb601a192af0c8d33709b2e5d7233508dfdbd1c5156ed
@@ -179,7 +179,7 @@ class MiGA::Cli::Action::Browse < MiGA::Cli::Action
179
179
  str
180
180
  .to_s.unmiga_name
181
181
  .sub(/^./, &:upcase)
182
- .gsub(/(Aai|Ani|Ogs|Cds|Ssu| db$| ssu )/, &:upcase)
182
+ .gsub(/(Aai|Ani|Ogs|Cds|Ssu|Rds|ani95|aai90| db$| ssu )/, &:upcase)
183
183
  .sub(/Haai/, 'hAAI')
184
184
  .sub(/Mytaxa/, 'MyTaxa')
185
185
  .sub(/ pvalue$/, ' p-value')
@@ -38,9 +38,13 @@ class MiGA::Cli::Action::Daemon < MiGA::Cli::Action
38
38
  'Path to the list of execution hostnames'
39
39
  ) { |v| cli[:nodelist] = v }
40
40
  opt.on(
41
- '--ppn INT',
41
+ '--ppn INT', Integer,
42
42
  'Maximum number of cores to use in a single job'
43
- ) { |v| cli[:ppn] = v.to_i }
43
+ ) { |v| cli[:ppn] = v }
44
+ opt.on(
45
+ '--ppn-project INT', Integer,
46
+ 'Maximum number of cores to use in project-wide tasks'
47
+ ) { |v| cli[:ppn_project] = v }
44
48
  opt.on(
45
49
  '--json PATH',
46
50
  'Path to a custom daemon definition in json format'
@@ -81,7 +85,7 @@ class MiGA::Cli::Action::Daemon < MiGA::Cli::Action
81
85
  # Configure and run daemon
82
86
  p = cli.load_project
83
87
  d = MiGA::Daemon.new(p, cli[:json])
84
- dopts = %i[latency maxjobs nodelist ppn shutdown_when_done]
88
+ dopts = %i[latency maxjobs nodelist ppn ppn_project shutdown_when_done]
85
89
  dopts.each { |k| d.runopts(k, cli[k]) }
86
90
  d.show_log! if cli[:show_log]
87
91
  d.daemon(cli.operation, cli[:daemon_opts])
@@ -73,14 +73,14 @@ class MiGA::Cli::Action::DerepWf < MiGA::Cli::Action
73
73
  c_f = r.file_path(:clades_gsp) or raise 'Result incomplete: run failed'
74
74
  clades = File.readlines(c_f).map { |i| i.chomp.split("\t") }
75
75
  rep = representatives(p)
76
- File.open(File.expand_path('genomospecies.tsv', cli[:outdir]), 'w') do |fh|
76
+ File.open(File.join(cli[:outdir], 'genomospecies.tsv'), 'w') do |fh|
77
77
  fh.puts "Clade\tRepresentative\tMembers"
78
78
  clades.each_with_index do |i, k|
79
79
  fh.puts ["gsp_#{k + 1}", rep[k], i.join(',')].join("\t")
80
80
  end
81
81
  end
82
82
  if cli[:collection]
83
- dir = File.expand_path('representatives', cli[:outdir])
83
+ dir = File.join(cli[:outdir], 'representatives')
84
84
  FileUtils.mkdir_p(dir)
85
85
  rep.each do |i|
86
86
  f = p.dataset(i).result(:assembly).file_path(:largecontigs)
@@ -91,12 +91,12 @@ class MiGA::Cli::Action::DerepWf < MiGA::Cli::Action
91
91
 
92
92
  def representatives(p)
93
93
  cli.say 'Identifying representatives'
94
- f = File.expand_path('representatives.txt', cli[:outdir])
94
+ f = File.join(cli[:outdir], 'representatives.txt')
95
95
  if cli[:criterion] == :medoids
96
96
  FileUtils.cp(p.result(:clade_finding).file_path(:medoids_gsp), f)
97
97
  else
98
- src = File.expand_path('utils/representatives.rb', MiGA::MiGA.root_path)
99
- `ruby '#{src}' '#{p.path}' | cut -f 2 > '#{f}'`
98
+ src = File.join(MiGA::MiGA.root_path, 'utils/representatives.rb')
99
+ MiGA::MiGA.run_cmd("ruby '#{src}' '#{p.path}' | cut -f 2", stdout: f)
100
100
  end
101
101
  File.readlines(f).map(&:chomp)
102
102
  end
@@ -236,8 +236,7 @@ class MiGA::Cli::Action::Doctor < MiGA::Cli::Action
236
236
  file = res.file_path(f) or next
237
237
  if file !~ /\.gz/
238
238
  cli.say " > Gzipping #{d.name} #{f} "
239
- cmdo = `gzip -9 '#{file}'`.chomp
240
- warn(cmdo) unless cmdo.empty?
239
+ run_cmd(['gzip', '-9', file])
241
240
  changed = true
242
241
  end
243
242
  end
@@ -267,8 +266,9 @@ class MiGA::Cli::Action::Doctor < MiGA::Cli::Action
267
266
  next if Dir["#{dir}/*.faa"].empty?
268
267
 
269
268
  cli.say " > Fixing #{d.name}"
270
- cmdo = `cd '#{dir}' && tar -zcf proteins.tar.gz *.faa && rm *.faa`.chomp
271
- warn(cmdo) unless cmdo.empty?
269
+ run_cmd <<~CMD
270
+ cd #{dir.shellescape} && tar -zcf proteins.tar.gz *.faa && rm *.faa
271
+ CMD
272
272
  end
273
273
  end
274
274
 
@@ -292,10 +292,11 @@ class MiGA::Cli::Action::Doctor < MiGA::Cli::Action
292
292
  fix = false
293
293
  unless dir.nil?
294
294
  if Dir.exist? dir
295
- cmdo = `cd '#{dir}/..' \
295
+ run_cmd <<~CMD
296
+ cd #{dir.shellescape}/.. \
296
297
  && tar -zcf '#{d.name}.reg.tar.gz' '#{d.name}.reg' \
297
- && rm -r '#{d.name}.reg'`.chomp
298
- warn(cmdo) unless cmdo.empty?
298
+ && rm -r '#{d.name}.reg'
299
+ CMD
299
300
  end
300
301
  fix = true
301
302
  end
@@ -335,4 +336,12 @@ class MiGA::Cli::Action::Doctor < MiGA::Cli::Action
335
336
  # TODO: Find different 95%ANI clusters with genomes from the same species
336
337
  # TODO: Find AAI values too high or too low for each LCA rank
337
338
  end
339
+
340
+ ##
341
+ # Run command +cmd+ with options +opts+
342
+ def run_cmd(cmd, opts = {})
343
+ opts = { return: :output, err2out: true, raise: false }.merge(opts)
344
+ cmdo = MiGA::MiGA.run_cmd(cmd, opts).chomp
345
+ warn(cmdo) unless cmdo.empty?
346
+ end
338
347
  end
@@ -190,7 +190,11 @@ class MiGA::Cli::Action::GetDb < MiGA::Cli::Action
190
190
 
191
191
  def unarchive(file)
192
192
  cli.say "Unarchiving #{file}"
193
- `cd "#{cli[:local]}" && tar -zxf "#{file}" && rm "#{file}"`
193
+ MiGA::MiGA.run_cmd <<~CMD
194
+ cd #{cli[:local].shellescape} \
195
+ && tar -zxf #{file.shellescape} \
196
+ && rm #{file.shellescape}
197
+ CMD
194
198
  end
195
199
 
196
200
  def register_database(manif, db, ver)
@@ -107,7 +107,12 @@ module MiGA::Cli::Action::Init::FilesHelper
107
107
  MiGA::MiGA.download_file_ftp(
108
108
  :miga_dist, arch, File.join(miga_db, arch)
109
109
  ) { |n, size| cli.advance("#{arch}:", n, size) }
110
- `cd '#{miga_db}' && tar zxf '#{arch}' && rm '#{arch}'`
110
+ cmd = <<~CMD
111
+ cd #{miga_db.shellescape} \
112
+ && tar zxf #{arch.shellescape} \
113
+ && rm #{arch.shellescape}
114
+ CMD
115
+ run_cmd(cmd, source: nil)
111
116
  cli.puts
112
117
  end
113
118
  end
@@ -83,14 +83,16 @@ class MiGA::Cli::Action::Init < MiGA::Cli::Action
83
83
  def empty_action
84
84
  end
85
85
 
86
- def run_cmd(cli, cmd)
87
- `. "#{cli[:config]}" && #{cmd}`
86
+ def run_cmd(cli, cmd, opts = {})
87
+ opts = { return: :output, source: cli[:config] }.merge(opts)
88
+ MiGA::MiGA.run_cmd(cmd, opts)
88
89
  end
89
90
 
90
- def run_r_cmd(cli, paths, cmd)
91
+ def run_r_cmd(cli, paths, cmd, opts = {})
91
92
  run_cmd(
92
93
  cli,
93
- "echo #{cmd.shellescape} | #{paths['R'].shellescape} --vanilla -q 2>&1"
94
+ "echo #{cmd.shellescape} | #{paths['R'].shellescape} --vanilla -q",
95
+ { err2out: true, stdout: '/dev/null' }.merge(opts)
94
96
  )
95
97
  end
96
98
 
@@ -159,7 +161,7 @@ class MiGA::Cli::Action::Init < MiGA::Cli::Action
159
161
  def find_software(exec)
160
162
  path = nil
161
163
  loop do
162
- d_path = File.dirname(run_cmd(cli, "which #{exec.shellescape}"))
164
+ d_path = File.dirname(run_cmd(cli, ['which', exec], raise: false))
163
165
  if cli[:ask] || d_path == '.'
164
166
  path = cli.ask_user('Where can I find it?', d_path, nil, true)
165
167
  else
@@ -172,7 +174,7 @@ class MiGA::Cli::Action::Init < MiGA::Cli::Action
172
174
  end
173
175
  break
174
176
  end
175
- cli.print "I cannot find #{exec} "
177
+ cli.print "I cannot find #{exec}. "
176
178
  end
177
179
  path
178
180
  end
@@ -208,19 +210,21 @@ class MiGA::Cli::Action::Init < MiGA::Cli::Action
208
210
  end
209
211
 
210
212
  def test_library(cli, paths, language, pkg)
211
- case language
212
- when :r
213
- run_r_cmd(cli, paths, "library('#{pkg}')")
214
- when :ruby
215
- x = "#{paths['ruby'].shellescape} -r #{pkg.shellescape} -e '' 2>/dev/null"
216
- run_cmd(cli, x)
217
- when :python
218
- x = "#{paths['python3'].shellescape} -c 'import #{pkg}' 2>/dev/null"
219
- run_cmd(cli, x)
220
- else
221
- raise "Unrecognized language: #{language}"
222
- end
223
- $?.success?
213
+ opts = { raise: false, return: :status, stderr: '/dev/null' }
214
+ status =
215
+ case language
216
+ when :r
217
+ run_r_cmd(cli, paths, "library('#{pkg}')", opts)
218
+ when :ruby
219
+ x = [paths['ruby'], '-r', pkg, '-e', '']
220
+ run_cmd(cli, x, opts)
221
+ when :python
222
+ x = [paths['python3'], '-c', "import #{pkg}"]
223
+ run_cmd(cli, x, opts)
224
+ else
225
+ raise "Unrecognized language: #{language}"
226
+ end
227
+ status.success?
224
228
  end
225
229
 
226
230
  def install_library(cli, paths, language, pkg)
@@ -232,14 +236,14 @@ class MiGA::Cli::Action::Init < MiGA::Cli::Action
232
236
  # This hackey mess is meant to ensure the test and installation are done
233
237
  # on the configuration Ruby, not on the Ruby currently executing the
234
238
  # init action
235
- gem_cmd = "Gem::GemRunner.new.run %w(install --user #{pkg})"
236
- x = "#{paths['ruby'].shellescape} -r rubygems -r rubygems/gem_runner \
237
- -e #{gem_cmd.shellescape} 2>&1"
238
- run_cmd(cli, x)
239
+ x = [
240
+ paths['ruby'], '-r', 'rubygems', '-r', 'rubygems/gem_runner',
241
+ '-e', "Gem::GemRunner.new.run %w(install --user #{pkg})"
242
+ ]
243
+ run_cmd(cli, x, err2out: true)
239
244
  when :python
240
- x = "#{paths['python3'].shellescape} \
241
- -m pip install --user #{pkg.shellescape} 2>&1"
242
- run_cmd(cli, x)
245
+ x = [paths['python3'], '-m', 'pip', 'install', '--user', pkg]
246
+ run_cmd(cli, x, err2out: true)
243
247
  else
244
248
  raise "Unrecognized language: #{language}"
245
249
  end
@@ -1,5 +1,4 @@
1
- # @package MiGA
2
- # @license Artistic-2.0
1
+ # frozen_string_literal: true
3
2
 
4
3
  require 'miga/cli/action'
5
4
 
@@ -42,33 +41,53 @@ class MiGA::Cli::Action::Ls < MiGA::Cli::Action
42
41
  '-s', '--silent',
43
42
  'No output and exit with non-zero status if the dataset list is empty'
44
43
  ) { |v| cli[:silent] = v }
44
+ opt.on(
45
+ '--exec CMD',
46
+ 'Command to execute per dataset, with the following token variables:',
47
+ '~ {{dataset}}: Name of the dataset',
48
+ '~ {{project}}: Path to the project'
49
+ ) { |v| cli[:exec] = v }
45
50
  end
46
51
  end
47
52
 
48
53
  def perform
49
54
  ds = cli.load_and_filter_datasets(cli[:silent])
55
+ p = cli.load_project
50
56
  exit(ds.empty? ? 1 : 0) if cli[:silent]
51
- if !cli[:datum].nil?
57
+
58
+ head = nil
59
+ fun = nil
60
+ if cli[:datum]
52
61
  cli[:tabular] = true
53
- format_table(ds, [nil, nil]) { |d| [d.name, d.metadata[cli[:datum]]] }
54
- elsif !cli[:fields].nil?
55
- format_table(ds, [:name] + cli[:fields]) do |d|
56
- [d.name] + cli[:fields].map { |f| d.metadata[f] }
57
- end
62
+ head = [nil, nil]
63
+ fun = proc { |d| [d.name, d.metadata[cli[:datum]]] }
64
+ elsif cli[:fields]
65
+ head = [:name] + cli[:fields]
66
+ fun = proc { |d| [d.name] + cli[:fields].map { |f| d.metadata[f] } }
58
67
  elsif cli[:info]
59
- format_table(ds, Dataset.INFO_FIELDS) { |d| d.info }
68
+ head = Dataset.INFO_FIELDS
69
+ fun = proc(&:info)
60
70
  elsif cli[:processing]
61
- comp = %w[- done queued]
62
- format_table(ds, [:name] + MiGA::Dataset.PREPROCESSING_TASKS) do |d|
63
- [d.name] + d.profile_advance.map { |i| comp[i] }
71
+ head = [:name] + MiGA::Dataset.PREPROCESSING_TASKS
72
+ fun = proc do |d|
73
+ [d.name] + d.profile_advance.map { |i| %w[- done queued][i] }
64
74
  end
65
75
  elsif cli[:taskstatus]
66
- format_table(ds, [:name] + MiGA::Dataset.PREPROCESSING_TASKS) do |d|
67
- [d.name] + d.results_status.values
68
- end
76
+ head = [:name] + MiGA::Dataset.PREPROCESSING_TASKS
77
+ fun = proc { |d| [d.name] + d.results_status.values }
69
78
  else
70
79
  cli[:tabular] = true
71
- format_table(ds, [nil]) { |d| [d.name] }
80
+ head = [nil]
81
+ fun = proc { |d| [d.name] }
82
+ end
83
+
84
+ format_table(ds, head) do |d|
85
+ if cli[:exec]
86
+ MiGA::MiGA.run_cmd(
87
+ cli[:exec].miga_variables(dataset: d.name, project: p.path)
88
+ )
89
+ end
90
+ fun[d]
72
91
  end
73
92
  end
74
93
 
@@ -2,7 +2,6 @@
2
2
  # @license Artistic-2.0
3
3
 
4
4
  require 'miga/cli/action'
5
- require 'shellwords'
6
5
 
7
6
  class MiGA::Cli::Action::Run < MiGA::Cli::Action
8
7
  def parse_cli
@@ -54,9 +53,13 @@ class MiGA::Cli::Action::Run < MiGA::Cli::Action
54
53
 
55
54
  # Prepare command
56
55
  miga = MiGA.root_path
57
- cmd = ["PROJECT=#{p.path.shellescape}",
58
- "RUNTYPE=#{cli[:remote] ? 'ssh' : 'bash'}",
59
- "MIGA=#{miga.shellescape}", "CORES=#{cli[:thr]}"]
56
+ opts = {}
57
+ cmd = [
58
+ "PROJECT=#{p.path.shellescape}",
59
+ "RUNTYPE=#{cli[:remote] ? 'ssh' : 'bash'}",
60
+ "MIGA=#{miga.shellescape}",
61
+ "CORES=#{cli[:thr]}"
62
+ ]
60
63
  obj = cli.load_project_or_dataset
61
64
  klass = obj.class
62
65
  virtual_task = %i[p d maintenance].include?(cli[:result])
@@ -67,13 +70,22 @@ class MiGA::Cli::Action::Run < MiGA::Cli::Action
67
70
 
68
71
  cmd << MiGA.script_path(cli[:result], miga: miga, project: p).shellescape
69
72
  if cli[:remote]
70
- cmd = ['ssh', '-t', '-t', cli[:remote].shellescape,
71
- cmd.join(' ').shellescape]
73
+ cmd = [
74
+ 'ssh', '-t', '-t',
75
+ cli[:remote].shellescape,
76
+ cmd.join(' ').shellescape
77
+ ]
78
+ end
79
+
80
+ if cli[:log]
81
+ opts[:stdout] = cli[:log]
82
+ opts[:err2out] = true
72
83
  end
73
- cmd << ['>', cli[:log].shellescape, '2>&1'] if cli[:log]
74
84
 
75
85
  # Launch
76
- pid = spawn cmd.join(' ')
77
- Process.wait pid
86
+ # note that all elements were carefully escaped in advace, so this has to be
87
+ # passed as String to avoid double-escaping or unintentionally escaping
88
+ # characters such as `=` and `>`
89
+ MiGA::MiGA.run_cmd(cmd.join(' '), opts)
78
90
  end
79
91
  end
@@ -3,7 +3,6 @@
3
3
 
4
4
  require 'miga/cli/action'
5
5
  require 'miga/tax_index'
6
- require 'zlib'
7
6
  require 'tmpdir'
8
7
 
9
8
  class MiGA::Cli::Action::TaxDist < MiGA::Cli::Action
data/lib/miga/cli/base.rb CHANGED
@@ -1,5 +1,4 @@
1
- # @package MiGA
2
- # @license Artistic-2.0
1
+ # frozen_string_literal: true
3
2
 
4
3
  module MiGA::Cli::Base
5
4
  @@TASK_DESC = {
@@ -20,10 +19,10 @@ module MiGA::Cli::Base
20
19
  add: 'Create a dataset in a MiGA project',
21
20
  get: 'Download a dataset from public databases into a MiGA project',
22
21
  ncbi_get: 'Download all genomes in a taxon from NCBI into a MiGA project',
23
- rm: 'Remove a dataset from an MiGA project',
22
+ rm: 'Remove a dataset from a MiGA project',
24
23
  find: 'Find unregistered datasets based on result files',
25
24
  ln: 'Link datasets (including results) from one project to another',
26
- ls: 'List all registered datasets in an MiGA project',
25
+ ls: 'List all registered datasets in a MiGA project',
27
26
  archive: 'Generate a tar-ball with all files from select datasets',
28
27
  # Results
29
28
  add_result: 'Register a result',
@@ -1,6 +1,7 @@
1
- # @package MiGA
2
- # @license Artistic-2.0
1
+ # frozen_string_literal: true
3
2
 
3
+ ##
4
+ # Helper module including functions for CLI options
4
5
  module MiGA::Cli::OptHelper
5
6
  ##
6
7
  # Send MiGA's banner to OptionParser +opt+
@@ -26,6 +27,10 @@ module MiGA::Cli::OptHelper
26
27
  'Accept all defaults as answers'
27
28
  ) { |v| self[:auto] = v }
28
29
  end
30
+ opt.on(
31
+ '--rand-seed INT', Integer,
32
+ 'Set this seed to initialize pseudo-randomness'
33
+ ) { |v| srand(v) }
29
34
  opt.on(
30
35
  '-v', '--verbose',
31
36
  'Print additional information to STDERR'
@@ -61,7 +66,7 @@ module MiGA::Cli::OptHelper
61
66
  # - :result_project To require a type of project result
62
67
  # The options :result, :result_opt, :result_dataset, and :result_project
63
68
  # are mutually exclusive
64
- def opt_object(opt, what = [:project, :dataset])
69
+ def opt_object(opt, what = %i[project dataset])
65
70
  what.each do |w|
66
71
  case w
67
72
  when :project
@@ -86,7 +91,7 @@ module MiGA::Cli::OptHelper
86
91
  when :result, :result_opt
87
92
  opt.on(
88
93
  '-r', '--result STRING',
89
- "#{"(Mandatory) " if w == :result}Name of the result",
94
+ "#{'(Mandatory) ' if w == :result}Name of the result",
90
95
  'Recognized names for dataset-specific results include:',
91
96
  *MiGA::Dataset.RESULT_DIRS.keys.map { |n| " ~ #{n}" },
92
97
  'Recognized names for project-wide results include:',
@@ -118,7 +123,7 @@ module MiGA::Cli::OptHelper
118
123
  # - :active To filter by active (--active) or inactive (--no-active)
119
124
  # - :taxonomy To filter by taxonomy (--taxonomy)
120
125
  # The "k-th" filter (--dataset-k) is always included
121
- def opt_filter_datasets(opt, what = [:ref, :multi, :active, :taxonomy])
126
+ def opt_filter_datasets(opt, what = %i[ref multi active taxonomy])
122
127
  what.each do |w|
123
128
  case w
124
129
  when :ref
@@ -161,6 +166,6 @@ module MiGA::Cli::OptHelper
161
166
  # If +sym+ is nil, +flag+ is used as Symbol
162
167
  def opt_flag(opt, flag, description, sym = nil)
163
168
  sym = flag.to_sym if sym.nil?
164
- opt.on("--#{flag.to_s.gsub('_', '-')}", description) { |v| self[sym] = v }
169
+ opt.on("--#{flag.to_s.tr('_', '-')}", description) { |v| self[sym] = v }
165
170
  end
166
171
  end
@@ -0,0 +1,12 @@
1
+
2
+ module MiGA
3
+ ##
4
+ # A generic MiGA error
5
+ class Error < RuntimeError
6
+ end
7
+
8
+ ##
9
+ # An error with a system call
10
+ class SystemCallError < Error
11
+ end
12
+ end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tempfile'
2
- require 'zlib'
3
4
 
4
5
  ##
5
6
  # General formatting functions shared throughout MiGA.
@@ -38,13 +39,13 @@ module MiGA::Common::Format
38
39
  tmp_path = tmp_fh.path
39
40
  fh = File.open(file, 'r')
40
41
  end
41
- buffer = ''
42
+ buffer = ''.dup
42
43
  fh.each_line do |ln|
43
44
  ln.chomp!
44
45
  if ln =~ /^>\s*(\S+)(.*)/
45
46
  id, df = $1, $2
46
47
  tmp_fh.print buffer.wrap_width(80)
47
- buffer = ''
48
+ buffer = ''.dup
48
49
  tmp_fh.puts ">#{id.gsub(/[^A-Za-z0-9_\|\.]/, '_')}#{df}"
49
50
  else
50
51
  buffer << ln.gsub(/[^A-Za-z\.\-]/, '')
@@ -166,7 +167,7 @@ class String
166
167
  ##
167
168
  # Replace {{variables}} using the +vars+ hash
168
169
  def miga_variables(vars)
169
- o = "#{self}"
170
+ o = self.dup
170
171
  vars.each { |k, v| o.gsub!("{{#{k}}}", v.to_s) }
171
172
  o
172
173
  end
@@ -0,0 +1,93 @@
1
+ require 'shellwords'
2
+
3
+ ##
4
+ # General functions for process (system call) execution
5
+ module MiGA::Common::SystemCall
6
+ ##
7
+ # Execute the command +cmd+ with options +opts+ determined by #run_cmd_opts
8
+ #
9
+ # The command +cmd+ can be:
10
+ # - String: The command is processed as is, without changes
11
+ # - Array: The command is built with +shelljoin+ so each value is escaped
12
+ def run_cmd(cmd, opts = {})
13
+ opts = run_cmd_opts(opts)
14
+ cmd = cmd.shelljoin if cmd.is_a?(Array)
15
+ spawn_opts = {}
16
+ spawn_opts[:out] = opts[:stdout] if opts[:stdout]
17
+ spawn_opts[:err] = opts[:stderr] if opts[:stderr]
18
+ out_io, spawn_opts[:out] = IO.pipe if opts[:return] == :output
19
+ spawn_opts[:err] = [:child, :out] if opts[:err2out] && spawn_opts[:out]
20
+ opts[:source] = MiGA::MiGA.rc_path if opts[:source] == :miga
21
+ if opts[:source] && File.exist?(opts[:source])
22
+ cmd = ". #{opts[:source].shellescape} && #{cmd}"
23
+ end
24
+
25
+ DEBUG "CMD: #{cmd}"
26
+ puts "CMD: #{cmd}" if opts[:show_cmd]
27
+ return if opts[:dry]
28
+
29
+ pid = nil
30
+ error = nil
31
+ begin
32
+ pid = spawn(opts[:env], cmd, spawn_opts)
33
+ Process.wait(pid)
34
+ rescue => e
35
+ error = e
36
+ end
37
+ status = $?
38
+
39
+ if opts[:raise] && !status&.success?
40
+ raise MiGA::SystemCallError.new(
41
+ "Command failed with status " \
42
+ "#{status&.exitstatus}#{' (core dump)' if status&.coredump?}:\n" \
43
+ "#{error&.class}: #{error&.message}\n" \
44
+ "OPT: #{opts}\n" \
45
+ "CMD: #{cmd}"
46
+ )
47
+ end
48
+
49
+ case opts[:return]
50
+ when :status ; status
51
+ when :pid ; pid
52
+ when :error ; error
53
+ when :output
54
+ spawn_opts[:out].close
55
+ output = out_io.read
56
+ out_io.close
57
+ output
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Options for #run_cmd using a Hash +opts+ to modify defaults
63
+ #
64
+ # Supported keys (as Symbol) include:
65
+ # - stdout: Redirect STDOUT to this file
66
+ # - stderr: Redirect STDOUT to this file
67
+ # - dry: Don't run, just send the command to debug (default: false)
68
+ # - return: What should the function return, supported values are
69
+ # +:status+ (Process::Status, default), +:pid+ (Integer, process ID),
70
+ # +:error+ (Error if failed, nil otherwise), +:output+ (String,
71
+ # contents sent to STDOUT)
72
+ # - raise: Raise an exception (MiGA::SystemCallError) in case of failure
73
+ # (default: true)
74
+ # - show_cmd: Print command to the STDOUT (prefixed with CMD: ) to ease
75
+ # debugging (default: false)
76
+ # - err2out: Redirect STDERR to STDOUT
77
+ # - env: Environmental variables as a Hash, keys and values must be strings
78
+ # - source: A file to be sourced before running, or the Symbol +:miga+ to
79
+ # source the MiGA configuration file
80
+ def run_cmd_opts(opts = {})
81
+ {
82
+ stdout: nil,
83
+ stderr: nil,
84
+ dry: false,
85
+ return: :status,
86
+ raise: true,
87
+ show_cmd: false,
88
+ err2out: false,
89
+ env: {},
90
+ source: nil
91
+ }.merge(opts)
92
+ end
93
+ end
data/lib/miga/common.rb CHANGED
@@ -1,14 +1,16 @@
1
- # @package MiGA
2
- # @license Artistic-2.0
1
+ # frozen_string_literal: true
3
2
 
3
+ require 'zlib'
4
+ require 'stringio'
4
5
  require 'miga/version'
5
6
  require 'miga/json'
6
7
  require 'miga/parallel'
7
8
  require 'miga/common/base'
9
+ require 'miga/common/errors'
8
10
  require 'miga/common/path'
9
11
  require 'miga/common/format'
10
12
  require 'miga/common/net'
11
- require 'stringio'
13
+ require 'miga/common/system_call'
12
14
 
13
15
  ##
14
16
  # Generic class used to handle system-wide information and methods, and parent
@@ -19,14 +21,21 @@ class MiGA::MiGA
19
21
  extend MiGA::Common::Path
20
22
  extend MiGA::Common::Format
21
23
  extend MiGA::Common::Net
24
+ extend MiGA::Common::SystemCall
22
25
 
23
26
  ENV['MIGA_HOME'] ||= ENV['HOME']
24
27
 
28
+ ##
29
+ # Path to the +.miga_rc+ file
30
+ def self.rc_path
31
+ File.join(ENV['MIGA_HOME'], '.miga_rc')
32
+ end
33
+
25
34
  ##
26
35
  # Has MiGA been initialized?
27
36
  def self.initialized?
28
- File.exist?(File.expand_path('.miga_rc', ENV['MIGA_HOME'])) &&
29
- File.exist?(File.expand_path('.miga_daemon.json', ENV['MIGA_HOME']))
37
+ File.exist?(rc_path) &&
38
+ File.exist?(File.join(ENV['MIGA_HOME'], '.miga_daemon.json'))
30
39
  end
31
40
 
32
41
  ##
data/lib/miga/daemon.rb CHANGED
@@ -321,7 +321,9 @@ class MiGA::Daemon < MiGA::MiGA
321
321
  def purge!
322
322
  say 'Probing running jobs'
323
323
  @jobs_running.select! do |job|
324
- `#{runopts(:alive).miga_variables(pid: job[:pid])}`.chomp.to_i == 1
324
+ MiGA::MiGA.run_cmd(
325
+ runopts(:alive).miga_variables(pid: job[:pid]), return: :output
326
+ ).chomp.to_i == 1
325
327
  end
326
328
  end
327
329
 
@@ -346,7 +348,7 @@ class MiGA::Daemon < MiGA::MiGA
346
348
  Process.detach(job[:pid]) unless [nil, '', 0].include?(job[:pid])
347
349
  else
348
350
  # Schedule cluster job (qsub, msub, slurm)
349
- job[:pid] = `#{job[:cmd]}`.chomp
351
+ job[:pid] = MiGA::MiGA.run_cmd(job[:cmd], return: :output).chomp
350
352
  end
351
353
 
352
354
  # Check if registered
@@ -1,4 +1,3 @@
1
- require 'zlib'
2
1
  require 'miga/result/base'
3
2
 
4
3
  ##
@@ -198,12 +197,12 @@ module MiGA::Result::Stats
198
197
  file_path(:raw_report)
199
198
 
200
199
  MiGA::MiGA.DEBUG "Fixing essential genes by domain"
201
- scr = "#{MiGA::MiGA.root_path}/utils/domain-ess-genes.rb"
200
+ scr = File.join(MiGA::MiGA.root_path, 'utils', 'domain-ess-genes.rb')
202
201
  rep = file_path(:report)
203
- rc_p = File.expand_path('.miga_rc', ENV['HOME'])
204
- rc = File.exist?(rc_p) ? ". '#{rc_p}' && " : ''
205
- $stderr.print `#{rc} ruby '#{scr}' \
206
- '#{rep}' '#{rep}.domain' '#{tax[:d][0]}'`
202
+ $stderr.print MiGA::MiGA.run_cmd(
203
+ ['ruby', scr, rep, "#{rep}.domain", tax[:d][0]],
204
+ return: :output, err2out: true, source: :miga
205
+ )
207
206
  add_file(:raw_report, "#{source.name}.ess/log")
208
207
  add_file(:report, "#{source.name}.ess/log.domain")
209
208
  end
data/lib/miga/tax_dist.rb CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  require 'miga/common'
5
5
  require 'miga/taxonomy'
6
- require 'zlib'
7
6
 
8
7
  ##
9
8
  # Methods for taxonomy identification based on AAI/ANI values.
data/lib/miga/version.rb CHANGED
@@ -12,7 +12,7 @@ module MiGA
12
12
  # - String indicating release status:
13
13
  # - rc* release candidate, not released as gem
14
14
  # - [0-9]+ stable release, released as gem
15
- VERSION = [1.1, 3, 6].freeze
15
+ VERSION = [1.2, 1, 0].freeze
16
16
 
17
17
  ##
18
18
  # Nickname for the current major.minor version.
@@ -20,7 +20,7 @@ module MiGA
20
20
 
21
21
  ##
22
22
  # Date of the current gem relese.
23
- VERSION_DATE = Date.new(2021, 12, 1)
23
+ VERSION_DATE = Date.new(2021, 12, 12)
24
24
 
25
25
  ##
26
26
  # References of MiGA
data/test/format_test.rb CHANGED
@@ -58,6 +58,9 @@ class FormatTest < Test::Unit::TestCase
58
58
  assert_equal(50.0, o[:gc])
59
59
  assert_equal(5, o[:n50])
60
60
  assert_equal(4.0, o[:med])
61
+ o = MiGA::MiGA.seqs_length(f, :fasta, skew: true)
62
+ assert_equal(-50.0, o[:at_skew])
63
+ assert_equal(-25.0, o[:gc_skew])
61
64
  end
62
65
 
63
66
  def test_seqs_length_fastq
@@ -77,4 +80,22 @@ class FormatTest < Test::Unit::TestCase
77
80
  assert_equal('123 45', tab[2])
78
81
  assert_equal('678 90', tab[3])
79
82
  end
83
+
84
+ def test_miga_name
85
+ assert_not('a-bad-name'.miga_name?)
86
+ assert('a_good_one'.miga_name?)
87
+
88
+ assert('After_it_s_fixed_', 'After it\'s fixed!'.miga_name)
89
+
90
+ assert('A sp.', 'A_sp_'.unmiga_name)
91
+ assert('B str. C', 'B_sp_C'.unmiga_name)
92
+ assert('The X content', 'The_x_content'.unmiga_name)
93
+ end
94
+
95
+ def test_miga_variables
96
+ assert_equal(
97
+ '1 a box!',
98
+ '{{n}} {{my}} {{secret}}!'.miga_variables(my: 'a', secret: :box, n: 1)
99
+ )
100
+ end
80
101
  end
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ class SystemCallTest < Test::Unit::TestCase
4
+ include TestHelper
5
+
6
+ def test_run_cmd_opts
7
+ assert_equal(true, MiGA::MiGA.run_cmd_opts[:raise])
8
+ assert_equal(:status, MiGA::MiGA.run_cmd_opts[:return])
9
+ assert_equal(:pid, MiGA::MiGA.run_cmd_opts(return: :pid)[:return])
10
+ assert_nil(MiGA::MiGA.run_cmd_opts[:stdout])
11
+ end
12
+
13
+ def test_run_cmd_redirection
14
+ f1 = tmpfile('f1')
15
+ MiGA::MiGA.run_cmd('echo 1', stdout: f1)
16
+ assert_equal("1\n", File.read(f1))
17
+
18
+ MiGA::MiGA.run_cmd('echo 2 >&2', stderr: f1)
19
+ assert_equal("2\n", File.read(f1))
20
+
21
+ f2 = tmpfile('with spaces')
22
+ MiGA::MiGA.run_cmd('echo 3', stdout: f2)
23
+ assert_equal("3\n", File.read(f2))
24
+
25
+ MiGA::MiGA.run_cmd(['echo', 4], stdout: f2)
26
+ assert_equal("4\n", File.read(f2))
27
+ end
28
+
29
+ def test_run_cmd_return
30
+ o = MiGA::MiGA.run_cmd('echo 1', stdout: '/dev/null')
31
+ assert(o.is_a? Process::Status)
32
+ assert(o.success?)
33
+
34
+ o = MiGA::MiGA.run_cmd('echo 1', stdout: '/dev/null', return: :pid)
35
+ assert(o.is_a? Integer)
36
+
37
+ o = MiGA::MiGA.run_cmd('echo 1', stdout: '/dev/null', return: :error)
38
+ assert_nil(o)
39
+ end
40
+
41
+ def test_run_cmd_raise
42
+ assert_raise(MiGA::SystemCallError) { MiGA::MiGA.run_cmd('echoes!!!') }
43
+
44
+ o = MiGA::MiGA.run_cmd('echoes!!!', raise: false, return: :status)
45
+ assert_not(o.success?)
46
+
47
+ o = MiGA::MiGA.run_cmd('echoes!!!', raise: false, return: :error)
48
+ assert(o.is_a? Errno::ENOENT)
49
+ end
50
+ end
@@ -1,7 +1,7 @@
1
1
  require 'miga'
2
2
  require 'miga/tax_dist'
3
3
 
4
- class MiGA::DistanceRunner
4
+ class MiGA::DistanceRunner < MiGA::MiGA
5
5
  require_relative 'temporal.rb'
6
6
  require_relative 'database.rb'
7
7
  require_relative 'commands.rb'
@@ -227,7 +227,6 @@ module MiGA::DistanceRunner::Commands
227
227
  end
228
228
 
229
229
  def run_cmd(cmd)
230
- puts "CMD: #{cmd}"
231
- puts `#{cmd} 2>&1`
230
+ MiGA::MiGA.run_cmd(cmd, show_cmd: true, err2out: true)
232
231
  end
233
232
  end
@@ -58,9 +58,9 @@ module MiGA::DistanceRunner::Pipeline
58
58
  end
59
59
  end
60
60
  ds_matrix_fh.close
61
- ref_tree = File.expand_path('utils/ref-tree.R', MiGA::MiGA.root_path)
62
- `"#{ref_tree}" "#{ds_matrix}" "#{out_base}" "#{dataset.name}"`
63
- File.unlink ds_matrix
61
+ ref_tree = File.join(MiGA::MiGA.root_path, 'utils', 'ref-tree.R')
62
+ MiGA::MiGA.run_cmd([ref_tree, ds_matrix, out_base, dataset.name])
63
+ File.unlink(ds_matrix)
64
64
  end
65
65
 
66
66
  # Tests taxonomy
data/utils/find-medoid.R CHANGED
@@ -26,7 +26,6 @@ find_medoids <- function (ani.df, out, clades) {
26
26
  medoids <- c()
27
27
  for(i in cl){
28
28
  lab <- strsplit(i, ",")[[1]]
29
- cat("Clade of:", lab[1], "\n")
30
29
  if(length(lab) == 1) {
31
30
  lab.s <- lab
32
31
  } else {
@@ -44,7 +43,7 @@ find_medoids <- function (ani.df, out, clades) {
44
43
  }
45
44
 
46
45
  #= Main
47
- cat("Finding Medoids")
46
+ cat("Finding Medoids\n")
48
47
  ani <- readRDS(argv[1])
49
48
  find_medoids(ani.df = ani, out = argv[2], clades = argv[3])
50
49
 
@@ -1,5 +1,10 @@
1
1
  require 'zlib'
2
2
  require 'miga'
3
3
 
4
- class MiGA::SubcladeRunner
4
+ class MiGA::SubcladeRunner < MiGA::MiGA
5
+ require_relative 'temporal.rb'
6
+ require_relative 'pipeline.rb'
7
+
8
+ include MiGA::SubcladeRunner::Temporal
9
+ include MiGA::SubcladeRunner::Pipeline
5
10
  end
@@ -29,7 +29,10 @@ module MiGA::SubcladeRunner::Pipeline
29
29
  ofh.close
30
30
  # Cluster genomes
31
31
  if File.size? abc_path
32
- `ogs.mcl.rb -o '#{ogs_file}.tmp' --abc '#{abc_path}' -t '#{opts[:thr]}'`
32
+ run_cmd([
33
+ 'ogs.mcl.rb',
34
+ '-o', "#{ogs_file}.tmp", '--abc', abc_path, '-t', opts[:thr]
35
+ ])
33
36
  File.open(ogs_file, 'w') do |fh|
34
37
  File.foreach("#{ogs_file}.tmp").with_index do |ln, lno|
35
38
  fh.puts(ln) if lno > 0
@@ -45,8 +48,10 @@ module MiGA::SubcladeRunner::Pipeline
45
48
  # Find genomospecies medoids
46
49
  src = File.expand_path('utils/find-medoid.R', MiGA::MiGA.root_path)
47
50
  dir = opts[:gsp_metric] == 'aai' ? '02.aai' : '03.ani'
48
- `Rscript '#{src}' '../../09.distances/#{dir}/miga-project.rds' \
49
- miga-project.gsp-medoids miga-project.gsp-clades`
51
+ run_cmd([
52
+ 'Rscript', src, "../../09.distances/#{dir}/miga-project.rds",
53
+ 'miga-project.gsp-medoids', 'miga-project.gsp-clades'
54
+ ])
50
55
  if File.exist? 'miga-project.gsp-clades.sorted'
51
56
  File.rename 'miga-project.gsp-clades.sorted', 'miga-project.gsp-clades'
52
57
  end
@@ -62,13 +67,15 @@ module MiGA::SubcladeRunner::Pipeline
62
67
  ofh.close
63
68
  end
64
69
 
65
- def subclades metric
70
+ def subclades(metric)
66
71
  src = File.expand_path('utils/subclades.R', MiGA::MiGA.root_path)
67
72
  step = :"#{metric}_distances"
68
73
  metric_res = project.result(step) or raise "Incomplete step #{step}"
69
74
  matrix = metric_res.file_path(:matrix)
70
- `Rscript '#{src}' '#{matrix}' miga-project '#{opts[:thr]}' \
71
- miga-project.gsp-medoids '#{opts[:run_clades] ? 'cluster' : 'empty'}'`
75
+ run_cmd([
76
+ 'Rscript', src, matrix, 'miga-project', opts[:thr],
77
+ 'miga-project.gsp-medoids', opts[:run_clades] ? 'cluster' : 'empty'
78
+ ])
72
79
  if File.exist? 'miga-project.nwk'
73
80
  File.rename('miga-project.nwk', "miga-project.#{metric}.nwk")
74
81
  end
@@ -76,6 +83,10 @@ module MiGA::SubcladeRunner::Pipeline
76
83
 
77
84
  def compile
78
85
  src = File.expand_path('utils/subclades-compile.rb', MiGA::MiGA.root_path)
79
- `ruby '#{src}' '.' 'miga-project.class'`
86
+ run_cmd(['ruby', src, '.', 'miga-project.class'])
87
+ end
88
+
89
+ def run_cmd(cmd)
90
+ MiGA::MiGA.run_cmd(cmd, show_cmd: true, err2out: true)
80
91
  end
81
92
  end
@@ -1,12 +1,8 @@
1
+
1
2
  require_relative 'base.rb'
2
- require_relative 'temporal.rb'
3
- require_relative 'pipeline.rb'
4
3
 
5
4
  class MiGA::SubcladeRunner
6
- include MiGA::SubcladeRunner::Temporal
7
- include MiGA::SubcladeRunner::Pipeline
8
-
9
- attr_reader :project, :step, :opts, :home, :tmp
5
+ attr_reader(:project, :step, :opts, :home, :tmp)
10
6
 
11
7
  def initialize(project_path, step, opts_hash = {})
12
8
  @opts = opts_hash
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miga-base
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3.6
4
+ version: 1.2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luis M. Rodriguez-R
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-01 00:00:00.000000000 Z
11
+ date: 2021-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: daemons
@@ -176,10 +176,12 @@ files:
176
176
  - lib/miga/cli/opt_helper.rb
177
177
  - lib/miga/common.rb
178
178
  - lib/miga/common/base.rb
179
+ - lib/miga/common/errors.rb
179
180
  - lib/miga/common/format.rb
180
181
  - lib/miga/common/hooks.rb
181
182
  - lib/miga/common/net.rb
182
183
  - lib/miga/common/path.rb
184
+ - lib/miga/common/system_call.rb
183
185
  - lib/miga/common/with_daemon.rb
184
186
  - lib/miga/common/with_daemon_class.rb
185
187
  - lib/miga/common/with_option.rb
@@ -253,6 +255,7 @@ files:
253
255
  - test/remote_dataset_test.rb
254
256
  - test/result_stats_test.rb
255
257
  - test/result_test.rb
258
+ - test/system_call_test.rb
256
259
  - test/tax_dist_test.rb
257
260
  - test/tax_index_test.rb
258
261
  - test/taxonomy_test.rb