1use std::fmt;
3
4use super::QualFilter;
5use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
6use std::path::PathBuf;
7
8pub const DEFAULT_KMER: usize = 31;
10pub const DEFAULT_PROPORTION_READS: Option<f64> = None;
12pub const DEFAULT_STRAND: bool = false;
14pub const DEFAULT_MINFREQ: f64 = 0.9;
16pub const DEFAULT_AMBIGMISSING: bool = false;
18pub const DEFAULT_REPEATMASK: bool = false;
20pub const DEFAULT_AMBIGMASK: bool = false;
22pub const DEFAULT_CONSTGAPS: bool = false;
24pub const DEFAULT_MINCOUNT: u16 = 5;
26pub const DEFAULT_MINQUAL: u8 = 20;
28pub const DEFAULT_QUALFILTER: QualFilter = QualFilter::Strict;
30pub const DEFAULT_MISSING_SKALO: f32 = 0.1;
32pub const DEFAULT_MAX_PATHDEPTH: usize = 4;
34pub const DEFAULT_MAX_INDEL_KMERS: usize = 2;
36
37#[doc(hidden)]
38fn valid_kmer(s: &str) -> Result<usize, String> {
39 let k: usize = s
40 .parse()
41 .map_err(|_| format!("`{s}` isn't a valid k-mer"))?;
42 if !(5..=63).contains(&k) || k.is_multiple_of(2) {
43 Err("K-mer must be an odd number between 5 and 63 (inclusive)".to_string())
44 } else {
45 Ok(k)
46 }
47}
48
49#[doc(hidden)]
50fn valid_proportion(s: &str) -> Result<f64, String> {
51 let p: f64 = s
52 .parse()
53 .map_err(|_| format!("`{s}` isn't a valid proportion"))?;
54 if !(0.0..=1.0).contains(&p) {
55 Err("K-mer must be between 0 and 1 (inclusive)".to_string())
56 } else {
57 Ok(p)
58 }
59}
60
61#[doc(hidden)]
62fn zero_to_one(s: &str) -> Result<f64, String> {
63 let f: f64 = s
64 .parse()
65 .map_err(|_| format!("`{s}` isn't a valid frequency"))?;
66 if !(0.0..=1.0).contains(&f) {
67 Err("Frequency must be between 0 and 1 (inclusive)".to_string())
68 } else {
69 Ok(f)
70 }
71}
72
73#[doc(hidden)]
74fn valid_cpus(s: &str) -> Result<usize, String> {
75 let threads: usize = s
76 .parse()
77 .map_err(|_| format!("`{s}` isn't a valid number of cores"))?;
78 if threads < 1 {
79 Err("Threads must be one or higher".to_string())
80 } else {
81 Ok(threads)
82 }
83}
84
85pub fn check_threads(threads: usize) {
87 let max_threads = num_cpus::get();
88 if threads > max_threads {
89 log::warn!("{threads} threads is greater than available cores {max_threads}");
90 }
91}
92
93#[doc(hidden)]
94pub fn valid_min_kmer(s: &str) -> Result<ValidMinKmer, String> {
95 match s {
96 s if s.eq(&String::from("auto")) => Ok(ValidMinKmer::Auto),
97 s => {
98 let x: u16 = s.parse().expect("Invalid minimum kmer count");
100 if x.ge(&1) {
101 log::info!("Using provided minimum kmer count of {x}");
102 Ok(ValidMinKmer::Val(x))
103 } else {
104 Err("Minimum kmer count must be >= 1".to_string())
105 }
106 }
107 }
108}
109#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
111pub enum ValidMinKmer {
112 Auto,
114 Val(u16),
116}
117
118#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
120pub enum FileType {
121 Vcf,
123 Aln,
125}
126
127#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
129pub enum FilterType {
130 NoFilter,
132 NoConst,
134 NoAmbig,
136 NoAmbigOrConst,
138}
139
140impl fmt::Display for FilterType {
142 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143 match *self {
144 Self::NoFilter => write!(f, "No filtering"),
145 Self::NoConst => write!(f, "No constant sites"),
146 Self::NoAmbig => write!(f, "No ambiguous sites"),
147 Self::NoAmbigOrConst => write!(f, "No constant sites or ambiguous bases"),
148 }
149 }
150}
151
152#[derive(Parser)]
154#[command(author, version, about, long_about = None)]
155#[command(propagate_version = true)]
156pub struct Args {
157 #[doc(hidden)]
158 #[command(subcommand)]
159 pub command: Commands,
160
161 #[arg(short, long, global = true)]
163 pub verbose: bool,
164}
165
166#[derive(Subcommand)]
168pub enum Commands {
169 #[command(group(
170 ArgGroup::new("input")
171 .required(true)
172 .args(["seq_files", "file_list"]),
173 ))]
174 Build {
176 #[arg(group = "input")]
178 seq_files: Option<Vec<String>>,
179
180 #[arg(short, group = "input")]
182 file_list: Option<String>,
183
184 #[arg(short)]
186 output: String,
187
188 #[arg(short, value_parser = valid_kmer, default_value_t = DEFAULT_KMER)]
190 k: usize,
191
192 #[arg(long, value_parser = valid_proportion)]
194 proportion_reads: Option<f64>,
195
196 #[arg(long, default_value_t = DEFAULT_STRAND)]
198 single_strand: bool,
199
200 #[arg(long, value_parser = valid_min_kmer)]
202 min_count: Option<ValidMinKmer>,
203
204 #[arg(long, default_value_t = DEFAULT_MINQUAL)]
206 min_qual: u8,
207
208 #[arg(long, value_enum, default_value_t = DEFAULT_QUALFILTER)]
210 qual_filter: QualFilter,
211
212 #[arg(long, value_parser = valid_cpus, default_value_t = 1)]
214 threads: usize,
215 },
216 Align {
218 #[arg(required = true)]
220 input: Vec<String>,
221
222 #[arg(short)]
224 output: Option<String>,
225
226 #[arg(short, long, value_parser = zero_to_one, default_value_t = DEFAULT_MINFREQ)]
228 min_freq: f64,
229
230 #[arg(long, default_value_t = DEFAULT_AMBIGMISSING)]
232 filter_ambig_as_missing: bool,
233
234 #[arg(long, value_enum, default_value_t = FilterType::NoConst)]
236 filter: FilterType,
237
238 #[arg(long, default_value_t = DEFAULT_AMBIGMASK)]
240 ambig_mask: bool,
241
242 #[arg(long, default_value_t = DEFAULT_CONSTGAPS)]
244 no_gap_only_sites: bool,
245
246 #[arg(long, value_parser = valid_cpus, default_value_t = 1)]
248 threads: usize,
249 },
250 Map {
252 reference: String,
254
255 input: Vec<String>,
257
258 #[arg(short)]
260 output: Option<String>,
261
262 #[arg(short, long, value_enum, default_value_t = FileType::Aln)]
264 format: FileType,
265
266 #[arg(long, default_value_t = DEFAULT_AMBIGMASK)]
268 ambig_mask: bool,
269
270 #[arg(long, default_value_t = DEFAULT_REPEATMASK)]
272 repeat_mask: bool,
273
274 #[arg(long, value_parser = valid_cpus, default_value_t = 1)]
276 threads: usize,
277 },
278 Distance {
280 skf_file: String,
282
283 #[arg(short)]
285 output: Option<String>,
286
287 #[arg(short, long, value_parser = zero_to_one, default_value_t = 0.0)]
290 min_freq: f64,
291
292 #[arg(long, default_value_t = false)]
294 allow_ambiguous: bool,
295
296 #[arg(long, value_parser = valid_cpus, default_value_t = 1)]
298 threads: usize,
299 },
300 Merge {
302 skf_files: Vec<String>,
304
305 #[arg(short)]
307 output: String,
308 },
309 #[command(group(
310 ArgGroup::new("input")
311 .required(true)
312 .args(["names", "file_list"]),
313 ))]
314 Delete {
316 #[arg(short, long, required = true)]
318 skf_file: String,
319
320 #[arg(short)]
322 output: Option<String>,
323
324 #[arg(short, group = "input")]
326 file_list: Option<String>,
327
328 #[arg(group = "input")]
330 names: Option<Vec<String>>,
331 },
332 Weed {
334 skf_file: String,
336
337 weed_file: Option<String>,
339
340 #[arg(short)]
342 output: Option<String>,
343
344 #[arg(long, default_value_t = false)]
346 reverse: bool,
347
348 #[arg(short, long, value_parser = zero_to_one, default_value_t = DEFAULT_MINFREQ)]
350 min_freq: f64,
351
352 #[arg(long, default_value_t = DEFAULT_AMBIGMISSING)]
354 filter_ambig_as_missing: bool,
355
356 #[arg(long, value_enum, default_value_t = FilterType::NoFilter)]
358 filter: FilterType,
359
360 #[arg(long, default_value_t = DEFAULT_AMBIGMASK)]
362 ambig_mask: bool,
363
364 #[arg(long, default_value_t = DEFAULT_CONSTGAPS)]
366 no_gap_only_sites: bool,
367 },
368 Nk {
370 skf_file: String,
372
373 #[arg(long, default_value_t = false)]
375 full_info: bool,
376 },
377 Cov {
379 fastq_fwd: String,
381
382 fastq_rev: String,
384
385 #[arg(short, value_parser = valid_kmer, default_value_t = DEFAULT_KMER)]
387 k: usize,
388
389 #[arg(long, default_value_t = DEFAULT_STRAND)]
391 single_strand: bool,
392 },
393 Lo {
395 input_skf: String,
397
398 output: String,
400
401 #[arg(short = 'r', long, help_heading = "input")]
403 reference: Option<PathBuf>,
404
405 #[arg(short = 'm', long, default_value_t = DEFAULT_MISSING_SKALO, help_heading = "output")]
407 missing: f32,
408
409 #[arg(
411 short = 'd',
412 long,
413 default_value_t = DEFAULT_MAX_PATHDEPTH,
414 help_heading = "graph traversal"
415 )]
416 depth: usize,
417
418 #[arg(short = 'n', long, default_value_t = DEFAULT_MAX_INDEL_KMERS, help_heading = "other")]
420 indel_kmers: usize,
421
422 #[arg(long, value_parser = valid_cpus, default_value_t = 1, help_heading = "other")]
424 threads: usize,
425 },
426}
427
428pub fn cli_args() -> Args {
430 Args::parse()
431}