miga-base 1.1.3.6 → 1.2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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