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 #[command(about = "Common transformation of FASTA/Q")]
15 Seq(SeqArgs),
16
17 #[command(about = "Random Sampling by given seed and fraction")]
18 Sample(SampleArgs),
19
20 #[command(
21 about = "Report the stats of sequence length (Output: #seq, #bases, avg_size, min_size, med_size, max_size, N50)"
22 )]
23 Size(SizeArgs),
24
25 #[command(
26 about = "Report stats for sequence and quality by position (Output: POS, #bases, %A, %C, %G, %T, %N, avgQ, errQ, ...)",
27 long_about = "\x1b[1mFqchk\n\x1b[0m\
28 Parses FASTQ data with quality threshold and computes per-position statistics.\n\n\
29 \x1b[1;4mOutput columns:\x1b[0m\n\
30 (1) POS: Position in the read\n\
31 (2) #bases: Number of bases at this position\n\
32 (3) %A, %C, %G, %T, %N: Percentage of each nucleotide\n\
33 (4) avgQ: Average quality score (Q₁ + Q₂ + ... + Qₙ) / N\n\
34 (5) errQ: Estimated error rate -10 * log₁₀((P₁ + P₂ + ... + Pₙ) / N)\n\
35 (6-7) %low, %high: Percentage of the nucleotide that the quality scores below or above the threshold, respectively. (When q_threshold > 0)\n\
36 (6-) %QX: Percentage of the nucleotide that the quality scores is X (When q_threshold = 0)\n\n\
37 \x1b[1;4mNote:\x1b[0m\n\
38 Some tools treat quality scores less than 3 (Q < 3) as 3 to avoid instability in downstream metrics. \
39 For example, Q = 0 yields an error probability P = 1.0, Q = 1 gives P ≈ 0.794, and Q = 2 gives P ≈ 0.630. \
40 These low Q-scores can heavily skew error rate calculations (e.g., errQ), which is why they are often floored to 3. \
41 However, this adjustment can lead to results that are inconsistent with the original definition. \
42 Therefore, this tool preserves the original quality scores as-is."
43 )]
44 Fqchk(FqchkArgs),
45
46 #[command(
47 about = "Report the nucleotide composition of FASTA/Q (Output: #A, #C, #G, #T, #2, #3, #4, #CG, #GC)",
48 long_about = "\x1b[1mComp\n\x1b[0m\
49 Report the nucleotide composition of FASTA/Q w/o masked sequences with a BED file.\n\n\
50 \x1b[1;4mOutput columns:\x1b[0m\n\
51 (1) #seq: Number of reads\n\
52 (2) #bases: Number of bases\n\
53 (3-6) #A, #C, #G, #T: Number of each nucleotide\n\
54 (7) #2: Number of R, Y, S, W, K, M\n\
55 (8) #3: Number of B, D, H, V\n\
56 (9) #4: Number of N\n\
57 (10) #CG: Number of CG on the template strand\n\
58 (11) #GC: Number of GC on the template strand"
59 )]
60 Comp(CompArgs),
61
62 #[command(
63 about = "Trims low-quality bases from a FASTQ data based on a quality threshold Q.",
64 long_about = "\x1b[1mQCTrim\n\x1b[0m\
65 Trims low-quality bases from a FASTQ data based on a quality threshold Q.\n\n\
66 \x1b[1;4mThe algorithm:\n\x1b[0m\
67 (1) Scan the read from 5’ to 3’ to find the first base with quality >= Q. This is the starting position of the trimmed read.\n\
68 (2) Compute a running score from 5’ to 3’ \n\tscore(i) = score(i-1) + quality(i) - Q \n where the initial score is score(start) = quality(start) - Q.\n\
69 (3) Find out the maximun score. This is the end of the trimmed read.\n\
70 (4) If all base quality are less than Q, the read is discarded.\n\
71 (5) If the trimmed read is shorter than min_len, we first extend it from the 3’ end. If still too short, extend form the 5’ end until min_len is reached or no more bases are available.\
72 \n\n\x1b[1;4mNotes:\n\x1b[0m\
73 Quality trimming is no longer necessary in most modern sequencing pipelines. Its usefulness depends on the sequencing technology and the goals of your downstream analysis."
74 )]
75 Qctrim(QCTrimArgs),
76}
77
78#[derive(Args)]
79pub struct FqchkArgs {
80 pub in_fq: String,
82 #[arg(short, long)]
83 pub quality_value: Option<u8>,
85 #[arg(short, long)]
86 pub ascii_base: Option<u8>,
88}
89
90#[derive(Args)]
91pub struct QCTrimArgs {
92 pub in_fq: String,
94 #[arg(short, long)]
95 pub q_thershold: Option<u8>,
97 #[arg(short, long)]
98 pub min_length: Option<usize>,
100 #[arg(short, long)]
101 pub ascii_base: Option<u8>,
103}
104
105#[derive(Args)]
106#[command(group(
107 ArgGroup::new("exclusive_group")
108 .args(["in_fq", "in_fa"])
109 .required(true)
110 .multiple(false)
111))]
112pub struct SizeArgs {
113 #[arg(short = 'I', long)]
114 pub in_fq: Option<String>,
116 #[arg(short = 'A', long)]
117 pub in_fa: Option<String>,
119}
120
121#[derive(Args)]
122#[command(group(
123 ArgGroup::new("exclusive_group")
124 .args(["in_fq", "in_fa"])
125 .required(true)
126 .multiple(false)
127))]
128pub struct CompArgs {
129 #[arg(short = 'I', long)]
130 pub in_fq: Option<String>,
132 #[arg(short = 'A', long)]
133 pub in_fa: Option<String>,
135 #[arg(short = 'u', long)]
136 pub exclude_masked: bool,
138 #[arg(short = 'r', long)]
139 pub in_bed: Option<String>,
141}
142
143#[derive(Args)]
144#[command(group(
145 ArgGroup::new("exclusive_group")
146 .args(["in_fq", "in_fa"])
147 .required(true)
148 .multiple(false)
149))]
150pub struct SampleArgs {
151 #[arg(short = 'I', long)]
152 pub in_fq: Option<String>,
154 #[arg(short = 'A', long)]
155 pub in_fa: Option<String>,
157 #[arg(short = 's', long)]
158 pub random_seed: Option<usize>,
160 #[arg(short = 'f', long, value_parser = validate_ratio)]
161 pub sample_fraction: Option<f64>,
163}
164
165#[derive(Args)]
166#[command(group(
167 ArgGroup::new("exclusive_group")
168 .args(["in_fq", "in_fa"])
169 .required(true)
170 .multiple(false)
171))]
172pub struct SeqArgs {
173 #[arg(short = 'I', long)]
174 pub in_fq: Option<String>,
176 #[arg(short = 'A', long)]
177 pub in_fa: Option<String>,
179
180 #[arg(short = 'L', long)]
181 pub mini_seq_length: Option<usize>,
183 #[arg(short = 'N', long)]
184 pub drop_ambigous_seq: bool,
186 #[arg(short = '1', long)]
187 pub output_odd: bool,
189 #[arg(short = '2', long)]
190 pub output_even: bool,
192
193 #[arg(short = 'r', long)]
194 pub reverse_complement: bool,
196 #[arg(short = 'R', long)]
197 pub both_complement: bool,
199 #[arg(long)]
200 pub output_fasta: bool,
202 #[arg(short = 'C', long)]
203 pub trim_header: bool,
205 #[arg(short = 'l', long)]
206 pub line_len: Option<usize>,
208
209 #[arg(short = 'Q', long)]
210 pub ascii_bases: Option<u8>,
212 #[arg(long)]
213 pub output_qual_33: bool,
215 #[arg(long)]
216 pub q_low: Option<u8>,
218 #[arg(long)]
219 pub q_high: Option<u8>,
221 #[arg(short = 'F', long)]
222 pub fake_fastq_quality: Option<char>,
224
225 #[arg(short = 'U', long)]
226 pub uppercases: bool,
230 #[arg(short = 'x', long)]
231 pub lowercases_to_char: bool,
233
234 #[arg(long)]
235 pub mask_char: Option<char>,
237 #[arg(short = 'M', long)]
238 pub mask_regions: Option<String>,
240 #[arg(long)]
241 pub mask_complement_region: bool,
243}
244pub fn valiation_seq_args(args: &SeqArgs) -> Result<(), std::io::Error> {
246 let mut errors = Vec::new();
247 if args.output_even && args.output_odd {
248 errors.push("--output-even-reads and --output-odd-reads can not be used together.");
249 }
250 if args.mask_complement_region && args.mask_regions.is_none() {
251 errors.push("--mask-complment-region requires --mask-regions.");
252 }
253 if args.lowercases_to_char && args.mask_char.is_none() {
254 errors.push("--lowercases-to-char requires --mask-char.");
255 }
256 if args.output_fasta && (args.output_qual_33 || args.fake_fastq_quality.is_some()) {
257 errors
258 .push("--output-fasta can not be used with --output-qual-33 or --fake-fastq-quality.");
259 }
260 if args.output_qual_33 && args.fake_fastq_quality.is_some() {
261 errors.push("--output-qual-33 and --fake-fastq-quality can not be used together.");
262 }
263 if args.reverse_complement && args.both_complement {
264 errors.push("--reverse-complement and --both-complement can not be used together.");
265 }
266 if !errors.is_empty() {
267 for error in errors {
268 eprintln!("{} {}", "error:".red().bold(), error);
269 }
270 std::process::exit(1);
271 }
272 Ok(())
273}
274
275fn validate_ratio(s: &str) -> Result<f64, String> {
276 let val: f64 = s
277 .parse()
278 .map_err(|_| "Must be a valid floating-point number".to_string())?;
279 if (0.0..=1.0).contains(&val) {
280 Ok(val)
281 } else {
282 Err("Value must be between 0.0 and 1.0 (inclusive)".to_string())
283 }
284}