fix(export): 移除默认stdout输出并添加CSV表头
将导出命令的输出文件参数改为必填项,不再支持默认输出到stdout 为CSV格式导出添加表头行,包含path、rank和last_accessed字段 更新所有shell补全文件以反映输出文件参数的变化
This commit is contained in:
parent
a128974954
commit
0e5333d0d8
|
|
@ -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]' \
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue