fix(export): 移除默认stdout输出并添加CSV表头

将导出命令的输出文件参数改为必填项,不再支持默认输出到stdout
为CSV格式导出添加表头行,包含path、rank和last_accessed字段
更新所有shell补全文件以反映输出文件参数的变化
This commit is contained in:
soar0216 2026-04-25 09:27:30 +08:00
parent a128974954
commit 0e5333d0d8
8 changed files with 128 additions and 23 deletions

View File

@ -98,8 +98,8 @@ esac
_arguments "${_arguments_options[@]}" : \
'-f+[Output format (json or csv)]:FORMAT:(json csv)' \
'--format=[Output format (json or csv)]:FORMAT:(json csv)' \
'-o+[Output file path (default\: stdout)]:OUT:_files' \
'--out=[Output file path (default\: stdout)]:OUT:_files' \
'-o+[Output file path]:OUT:_files' \
'--out=[Output file path]:OUT:_files' \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \

View File

@ -85,8 +85,8 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
'zoxide;export' {
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Output format (json or csv)')
[CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output format (json or csv)')
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Output file path (default: stdout)')
[CompletionResult]::new('--out', '--out', [CompletionResultType]::ParameterName, 'Output file path (default: stdout)')
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Output file path')
[CompletionResult]::new('--out', '--out', [CompletionResultType]::ParameterName, 'Output file path')
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')

View File

@ -75,8 +75,8 @@ set edit:completion:arg-completer[zoxide] = {|@words|
&'zoxide;export'= {
cand -f 'Output format (json or csv)'
cand --format 'Output format (json or csv)'
cand -o 'Output file path (default: stdout)'
cand --out 'Output file path (default: stdout)'
cand -o 'Output file path'
cand --out 'Output file path'
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'

View File

@ -52,7 +52,7 @@ complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subc
complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_zoxide_using_subcommand export" -s f -l format -d 'Output format (json or csv)' -r -f -a "json\t''
csv\t''"
complete -c zoxide -n "__fish_zoxide_using_subcommand export" -s o -l out -d 'Output file path (default: stdout)' -r -F
complete -c zoxide -n "__fish_zoxide_using_subcommand export" -s o -l out -d 'Output file path' -r -F
complete -c zoxide -n "__fish_zoxide_using_subcommand export" -s h -l help -d 'Print help'
complete -c zoxide -n "__fish_zoxide_using_subcommand export" -s V -l version -d 'Print version'
complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l from -d 'Application to import from' -r -f -a "autojump\t''

View File

@ -50,7 +50,7 @@ module completions {
# Export entries from the database
export extern "zoxide export" [
--format(-f): string@"nu-complete zoxide export format" # Output format (json or csv)
--out(-o): path # Output file path (default: stdout)
--out(-o): path # Output file path
--help(-h) # Print help
--version(-V) # Print version
]

View File

@ -129,11 +129,10 @@ const completion: Fig.Spec = {
},
{
name: ["-o", "--out"],
description: "Output file path (default: stdout)",
description: "Output file path",
isRepeatable: true,
args: {
name: "out",
isOptional: true,
template: "filepaths",
},
},

View File

@ -100,9 +100,9 @@ pub struct Export {
#[clap(value_enum, long, short)]
pub format: ExportFormat,
/// Output file path (default: stdout)
/// Output file path
#[clap(long, short, value_hint = ValueHint::FilePath)]
pub out: Option<PathBuf>,
pub out: PathBuf,
}
#[derive(ValueEnum, Clone, Debug)]

View File

@ -1,5 +1,4 @@
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use anyhow::{Context, Result};
@ -17,6 +16,8 @@ impl Run for Export {
.context("could not serialize to JSON")?,
ExportFormat::Csv => {
let mut wtr = csv::Writer::from_writer(Vec::new());
wtr.write_record(["path", "rank", "last_accessed"])
.context("could not write CSV header")?;
for dir in dirs {
wtr.write_record([&*dir.path, &dir.rank.to_string(), &dir.last_accessed.to_string()])
.context("could not write CSV record")?;
@ -27,16 +28,8 @@ impl Run for Export {
}
};
match &self.out {
Some(path) => {
write_to_file(path, &output)
.with_context(|| format!("could not write to file: {}", path.display()))?;
}
None => {
writeln!(io::stdout(), "{output}")
.context("could not write to stdout")?;
}
}
write_to_file(&self.out, &output)
.with_context(|| format!("could not write to file: {}", self.out.display()))?;
Ok(())
}
@ -54,3 +47,116 @@ fn write_to_file(path: impl AsRef<Path>, content: &str) -> Result<()> {
.with_context(|| format!("could not write to file: {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Dir;
fn create_test_db() -> tempfile::TempDir {
let data_dir = tempfile::tempdir().unwrap();
let mut db = Database::open_dir(data_dir.path()).unwrap();
for (path, rank, last_accessed) in [
("/home/alice/projects/zoxide", 42.5, 1714000000),
("/home/alice/downloads", 7.0, 1713000000),
(r#"/tmp"quotes,commas""#, 1.0, 1712000000),
] {
db.add_unchecked(path, rank, last_accessed);
}
db.save().unwrap();
data_dir
}
fn set_data_dir_env(data_dir: &tempfile::TempDir) {
unsafe {
std::env::set_var("_ZO_DATA_DIR", data_dir.path());
}
}
#[test]
fn export_json() {
let data_dir = create_test_db();
set_data_dir_env(&data_dir);
let out_file = data_dir.path().join("export.json");
let export = Export {
format: ExportFormat::Json,
out: out_file.clone(),
};
export.run().unwrap();
let content = fs::read_to_string(&out_file).unwrap();
let result: Vec<Dir> = serde_json::from_str(&content).unwrap();
assert_eq!(result.len(), 3);
assert!(result.iter().any(|d| d.path == "/home/alice/projects/zoxide"));
assert!(result.iter().any(|d| d.path == "/home/alice/downloads"));
assert!(result.iter().any(|d| d.path == r#"/tmp"quotes,commas""#));
}
#[test]
fn export_csv() {
let data_dir = create_test_db();
set_data_dir_env(&data_dir);
let out_file = data_dir.path().join("export.csv");
let export = Export {
format: ExportFormat::Csv,
out: out_file.clone(),
};
export.run().unwrap();
let content = fs::read_to_string(&out_file).unwrap();
let mut rdr = csv::Reader::from_reader(content.as_bytes());
let headers = rdr.headers().unwrap();
assert_eq!(headers, ["path", "rank", "last_accessed"].as_slice());
let records: Vec<csv::StringRecord> = rdr.records().map(|r| r.unwrap()).collect();
assert_eq!(records.len(), 3);
let paths: Vec<&str> = records.iter().map(|r| r.get(0).unwrap()).collect();
assert!(paths.contains(&"/home/alice/projects/zoxide"));
assert!(paths.contains(&"/home/alice/downloads"));
assert!(paths.contains(&r#"/tmp"quotes,commas""#));
}
#[test]
fn export_csv_with_special_chars() {
let data_dir = create_test_db();
set_data_dir_env(&data_dir);
let out_file = data_dir.path().join("export.csv");
let export = Export {
format: ExportFormat::Csv,
out: out_file.clone(),
};
export.run().unwrap();
let content = fs::read_to_string(&out_file).unwrap();
assert!(content.contains(r#""""#));
assert!(content.contains(r#"/tmp""#));
let mut rdr = csv::Reader::from_reader(content.as_bytes());
let records: Vec<csv::StringRecord> = rdr.records().map(|r| r.unwrap()).collect();
let special_record = records.iter().find(|r| r.get(0).unwrap().contains("quotes"));
assert!(special_record.is_some());
assert_eq!(special_record.unwrap().get(0).unwrap(), r#"/tmp"quotes,commas""#);
}
#[test]
fn export_creates_parent_directories() {
let data_dir = create_test_db();
set_data_dir_env(&data_dir);
let out_file = data_dir.path().join("nested").join("path").join("export.json");
let export = Export {
format: ExportFormat::Json,
out: out_file.clone(),
};
export.run().unwrap();
assert!(out_file.exists());
}
}