Vec<Vec<String>>ColumnOp--select 最好支持重复参数old=new--drop
如果你想做一个 Rust 版的 tsv/csv 命令行工具,同时支持:
我建议不要把 CLI 参数直接设计成一堆零散参数,而是把核心抽象成 Column Expression / Column Operation。
例如工具叫 biox-tsv:
# 选择列
biox-tsv input.tsv \
--select SNP,CHR,BETA,P
# 重命名
biox-tsv input.tsv \
--rename SNP=variant \
--rename BETA=beta
# 选择 + 重命名
biox-tsv input.tsv \
--select SNP,BETA,P \
--rename SNP=variant \
--rename BETA=beta
# 调整顺序
biox-tsv input.tsv \
--select P,BETA,SNP
# 输出
biox-tsv input.tsv \
--select SNP,BETA,P \
--rename SNP=variant \
-o output.tsv
我比较推荐 --select + --rename 这套 API,因为语义非常直观。
Vec<Vec<String>>例如:
struct Column {
index: usize,
name: String,
}
读取 header 后:
SNP CHR POS BETA SE P
变成:
vec![
Column { index: 0, name: "SNP" },
Column { index: 1, name: "CHR" },
Column { index: 2, name: "POS" },
Column { index: 3, name: "BETA" },
Column { index: 4, name: "SE" },
Column { index: 5, name: "P" },
]
然后 --select 负责产生一个 projection:
struct ColumnSelection {
source_index: usize,
output_name: String,
}
例如:
--select SNP,BETA,P
得到:
vec![
ColumnSelection {
source_index: 0,
output_name: "SNP",
},
ColumnSelection {
source_index: 3,
output_name: "BETA",
},
ColumnSelection {
source_index: 5,
output_name: "P",
},
]
如果再:
--rename SNP=variant
那么最终:
vec![
ColumnSelection {
source_index: 0,
output_name: "variant",
},
ColumnSelection {
source_index: 3,
output_name: "BETA",
},
ColumnSelection {
source_index: 5,
output_name: "P",
},
]
这样 rename 和 select 本身就解耦了。
ColumnOp如果你后面准备把 biox-tsv 做成一个比较完整的 Rust 生信工具,我会进一步设计成:
enum ColumnOp {
Select {
column: ColumnRef,
},
Rename {
from: String,
to: String,
},
Drop {
column: ColumnRef,
},
}
但是实际上 Rename 不一定需要作为独立的执行操作。
可以让:
输入列
↓
解析 column expressions
↓
ColumnPlan
↓
逐行 projection
↓
输出
核心结构:
struct ColumnPlan {
columns: Vec<OutputColumn>,
}
struct OutputColumn {
source_index: usize,
source_name: String,
output_name: String,
}
例如:
SNP CHR POS BETA SE P
执行:
--select SNP,BETA,P
--rename SNP=variant
最终:
ColumnPlan {
columns: vec![
OutputColumn {
source_index: 0,
source_name: "SNP",
output_name: "variant",
},
OutputColumn {
source_index: 3,
source_name: "BETA",
output_name: "BETA",
},
OutputColumn {
source_index: 5,
source_name: "P",
output_name: "P",
},
],
}
这样处理数据的时候非常简单:
for record in reader.records() {
let record = record?;
for column in &plan.columns {
output.write_field(&record[column.source_index])?;
}
}
不会因为 rename 而修改原始数据的列索引。
--select 最好支持重复参数我比较推荐:
--select SNP,CHR
同时允许:
--select SNP --select CHR
甚至:
--select SNP,BETA,P --select SE
最终 CLI parser 合并成:
Vec<String>
例如使用 clap:
#[derive(Parser)]
struct Cli {
input: String,
#[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)]
select: Vec<String>,
#[arg(long, value_delimiter = ',', action = clap::ArgAction::Append)]
rename: Vec<String>,
#[arg(short, long)]
output: Option<String>,
}
不过 rename 最好不要直接用 String,可以定义自己的 parser。
old=newCLI:
--rename SNP=variant
--rename BETA=beta
解析成:
struct Rename {
from: String,
to: String,
}
然后:
impl FromStr for Rename {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let (from, to) = s
.split_once('=')
.ok_or_else(|| anyhow!("rename must be OLD=NEW"))?;
Ok(Self {
from: from.to_string(),
to: to.to_string(),
})
}
}
CLI:
#[arg(long, value_parser = parse_rename)]
rename: Vec<Rename>,
这样用户输入错误时可以直接得到:
error: invalid value 'SNP'
rename must be OLD=NEW
--drop既然已经有 --select,其实:
--drop SE,CHR
也很自然。
例如:
biox-tsv input.tsv \
--drop CHR,POS
但这里有一个设计选择:
--select SNP,BETA,P
或者:
--drop CHR,POS
不能同时使用。
这是我更推荐的第一版设计。
因为:
--select SNP,BETA,P
已经明确表达了最终输出什么。
对于 GWAS 数据特别有用。
比如:
biox-tsv input.tsv \
--select SNP=BETA,P
或者:
--select SNP:variant,BETA:beta,P:pvalue
那么就可以一步完成:
input:
SNP BETA P
output:
variant beta pvalue
CLI:
--select SNP:variant,BETA:beta,P:pvalue
内部直接解析成:
struct ColumnExpr {
source: ColumnRef,
alias: Option<String>,
}
例如:
ColumnExpr {
source: ColumnRef::Name("SNP"),
alias: Some("variant".into()),
}
这实际上比:
--select SNP,BETA,P \
--rename SNP=variant \
--rename BETA=beta
更加适合命令行数据处理。
biox-tsv input.tsv \
--select SNP,BETA,P \
--rename SNP=variant
biox-tsv input.tsv \
--select SNP:variant,BETA:beta,P:pvalue
其中:
SNP:variant
│ │
│ └── output name
└────── input column
以后还可以自然扩展:
--select SNP:variant,BETA:beta,P:pvalue
到:
--select SNP:variant,log(BETA):log_beta
甚至:
--select SNP,CHR,POS,concat(CHR,":",POS):variant
这时候你的工具就从简单的 TSV 操作工具逐渐变成类似 awk / cut + dataframe expression 的东西。
biox-* 项目,我建议的架构结合你前面的 biox-core / biox-tsv / biox-vcf 设计,我会把 列操作放在 biox-tsv,不要放到 biox-core:
biox/
├── crates/
│ ├── biox-core/
│ │
│ ├── biox-tsv/
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── reader.rs
│ │ │ ├── writer.rs
│ │ │ ├── column.rs
│ │ │ ├── plan.rs
│ │ │ └── error.rs
│ │
│ ├── biox-vcf/
│ └── biox-fasta/
│
└── src/
└── main.rs
核心关系:
CLI
│
├── --select
├── --rename
└── --drop
│
▼
ColumnPlan
│
▼
TSV Reader
│
▼
projection
│
▼
TSV Writer
关键点是 ColumnPlan。
不要让 --select、--rename、--drop 分别在数据循环里处理,而是在开始读取数据后,先根据 header 构建一次 ColumnPlan。之后每一行只做整数 index 的访问,这样对几千万行 GWAS TSV 会非常快。