seqtk_rs/
sub_cli.rs

1use clap::{ArgGroup, Args, Parser, Subcommand};
2use colored::*;
3
4#[derive(Parser)]
5#[command(version, about, long_about = None)]
6#[command(propagate_version = true)]
7pub struct Cli {
8    #[command(subcommand)]
9    pub command: Commands,
10}
11
12#[derive(Subcommand)]
13pub enum Commands {
14    /// Common transformation of FASTA/Q
15    Seq(SeqArgs),
16
17    /// Random Sampling by given seed and fraction
18    Sample(SampleArgs),
19    /// Report the stats of sequence length (Output: #seq, #bases, avg_size, min_size, med_size, max_size, N50)
20    Size(SizeArgs),
21    /// Report stats for sequence and quality by position (Output: POS, #bases, %A, %C, %G, %T, %N, avgQ, errQ, ...)
22    Fqchk(FqchkArgs),
23    /// Report the nucleotide composition of FASTA/Q (Output: #A, #C, #G, #T, #2, #3, #4, #CG, #GC)
24    Comp(CompArgs),
25    // /// Trim sequence
26    // Trim(TrimArgs),
27}
28
29#[derive(Args)]
30/// sub_cli 3
31pub struct FqchkArgs {
32    /// input fastq path
33    pub in_fq: String,
34    #[arg(short, long)]
35    /// output tsv path
36    pub out: Option<String>,
37    #[arg(short, long)]
38    /// quality value [default: 0]
39    pub quality_value: Option<u8>,
40    #[arg(short, long)]
41    /// ascii value [default: 33]
42    pub ascii_base: Option<u8>,
43}
44
45// #[derive(Args)]
46// pub struct TrimArgs {
47//     /// fastq path
48//     pub in_fq: String,
49//     #[arg(short, long)]
50//     /// Error rate threshold [default: 0.05]
51//     pub error_thershold: Option<f64>,
52// }
53
54#[derive(Args)]
55#[command(group(
56    ArgGroup::new("exclusive_group")
57        .args(["in_fq", "in_fa"])
58        .required(true)
59        .multiple(false)
60))]
61pub struct SizeArgs {
62    #[arg(short = 'I', long)]
63    /// Input fastq path
64    pub in_fq: Option<String>,
65    #[arg(short = 'A', long)]
66    /// Input fasta path
67    pub in_fa: Option<String>,
68}
69
70#[derive(Args)]
71#[command(group(
72    ArgGroup::new("exclusive_group")
73        .args(["in_fq", "in_fa"])
74        .required(true)
75        .multiple(false)
76))]
77pub struct CompArgs {
78    #[arg(short = 'I', long)]
79    /// Input fastq path
80    pub in_fq: Option<String>,
81    #[arg(short = 'A', long)]
82    /// Input fasta path
83    pub in_fa: Option<String>,
84    #[arg(short = 'u', long)]
85    /// Only report unmasked bases
86    pub exclude_masked: bool,
87    #[arg(short = 'r', long)]
88    /// Report bases that overlap with the regions specified in the BED (0-based) file [default: null]
89    pub in_bed: Option<String>,
90}
91
92#[derive(Args)]
93#[command(group(
94    ArgGroup::new("exclusive_group")
95        .args(["in_fq", "in_fa"])
96        .required(true)
97        .multiple(false)
98))]
99pub struct SampleArgs {
100    #[arg(short = 'I', long)]
101    /// Input fastq path
102    pub in_fq: Option<String>,
103    #[arg(short = 'A', long)]
104    /// Input fasta path
105    pub in_fa: Option<String>,
106    #[arg(short = 's', long)]
107    /// Set the seed for the random number generator. This value ensures reproducibility of the sampling process. (This option takes effect only when used in conjunction with --sample-fraction / -f.) [default: 4]
108    pub random_seed: Option<usize>,
109    #[arg(short = 'f', long, value_parser = validate_ratio)]
110    /// Specify the fraction of the total dataset to sample. The value is a FLOAT between 0 and 1. For example, a value of 0.1 will sample 10% of the data.
111    pub sample_fraction: Option<f64>,
112}
113
114#[derive(Args)]
115#[command(group(
116    ArgGroup::new("exclusive_group")
117        .args(["in_fq", "in_fa"])
118        .required(true)
119        .multiple(false)
120))]
121pub struct SeqArgs {
122    #[arg(short = 'I', long)]
123    /// Input fastq path
124    pub in_fq: Option<String>,
125    #[arg(short = 'A', long)]
126    /// Input fasta path
127    pub in_fa: Option<String>,
128
129    #[arg(short = 'L', long)]
130    /// Remove sequences shorter than MINI_SEQ_LENGTH. [default: 0]
131    pub mini_seq_length: Option<usize>,
132    #[arg(short = 'N', long)]
133    /// drop sequences containing ambiguous bases 'N'
134    pub drop_ambigous_seq: bool,
135    #[arg(short = '1', long)]
136    /// Output only the reads from odd-numbered (2n-1) records
137    pub output_odd: bool,
138    #[arg(short = '2', long)]
139    /// Output only the reads from even-numbered (2n) records
140    pub output_even: bool,
141
142    #[arg(short = 'r', long)]
143    /// reverse complement
144    pub reverse_complement: bool,
145    #[arg(short = 'R', long)]
146    /// output both forward and reverse complement
147    pub both_complement: bool,
148    #[arg(long)]
149    /// force output format to FASTA (discard quality)
150    pub output_fasta: bool,
151    #[arg(short = 'C', long)]
152    /// drop comments at the header lines (only keep the first word before first space)
153    pub trim_header: bool,
154    #[arg(short = 'l', long)]
155    /// Number of characters per line for sequences and their corresponding quality values [default: all on a single line]
156    pub line_len: Option<usize>,
157
158    #[arg(short = 'Q', long)]
159    /// The quality scores are represented as chars with ASCII values equal to the score plus a base offset ASCII_BASES [default: 33]
160    pub ascii_bases: Option<u8>,
161    #[arg(long)]
162    /// Output the quality score to an offset of 33 (Effective only when --ascii-bases is not 33)
163    pub output_qual_33: bool,
164    #[arg(long)]
165    /// Mask bases with a quality score lower than Q_LOW [default: 0]
166    pub q_low: Option<u8>,
167    #[arg(long)]
168    /// Mask bases with a quality score higher than Q_HIGH [default: 255]
169    pub q_high: Option<u8>,
170    #[arg(short = 'F', long)]
171    /// Generate fake quality values using the specified CHAR.
172    pub fake_fastq_quality: Option<char>,
173
174    #[arg(short = 'U', long)]
175    /// Converts all bases in the sequences to uppercase. When used in conjunction with other masking options
176    /// (e.g., --q-low, --q-high, --mask-regions, --mask-char, etc.),
177    /// the program first converts the sequences to uppercase and then applies the other masking operations.
178    pub uppercases: bool,
179    #[arg(short = 'x', long)]
180    /// Convert all lowercases to --mark-char
181    pub lowercases_to_char: bool,
182
183    #[arg(long)]
184    /// Mask bases by converting them to MASK_CHAR [default: convert to lowercase]
185    pub mask_char: Option<char>,
186    #[arg(short = 'M', long)]
187    /// Mask bases that overlap with the regions specified in the BED (0-based) file [default: null]
188    pub mask_regions: Option<String>,
189    #[arg(long)]
190    /// Mask bases that do NOT overlap with the region specified in the BED (effective with --mask-regions / -M)
191    pub mask_complement_region: bool,
192}
193/// Validate seq arguments.
194pub fn valiation_seq_args(args: &SeqArgs) -> Result<(), std::io::Error> {
195    let mut errors = Vec::new();
196    if args.output_even && args.output_odd {
197        errors.push("--output-even-reads and --output-odd-reads can not be used together.");
198    }
199    if args.mask_complement_region && args.mask_regions.is_none() {
200        errors.push("--mask-complment-region requires --mask-regions.");
201    }
202    if args.lowercases_to_char && args.mask_char.is_none() {
203        errors.push("--lowercases-to-char requires --mask-char.");
204    }
205    if args.output_fasta && (args.output_qual_33 || args.fake_fastq_quality.is_some()) {
206        errors
207            .push("--output-fasta can not be used with --output-qual-33 or --fake-fastq-quality.");
208    }
209    if args.output_qual_33 && args.fake_fastq_quality.is_some() {
210        errors.push("--output-qual-33 and --fake-fastq-quality can not be used together.");
211    }
212    if args.reverse_complement && args.both_complement {
213        errors.push("--reverse-complement and --both-complement can not be used together.");
214    }
215    if !errors.is_empty() {
216        for error in errors {
217            eprintln!("{} {}", "error:".red().bold(), error);
218        }
219        std::process::exit(1);
220    }
221    Ok(())
222}
223
224fn validate_ratio(s: &str) -> Result<f64, String> {
225    let val: f64 = s
226        .parse()
227        .map_err(|_| "Must be a valid floating-point number".to_string())?;
228    if (0.0..=1.0).contains(&val) {
229        Ok(val)
230    } else {
231        Err("Value must be between 0.0 and 1.0 (inclusive)".to_string())
232    }
233}