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[@]}" : \
|
_arguments "${_arguments_options[@]}" : \
|
||||||
'-f+[Output format (json or csv)]:FORMAT:(json csv)' \
|
'-f+[Output format (json or csv)]:FORMAT:(json csv)' \
|
||||||
'--format=[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' \
|
'-o+[Output file path]:OUT:_files' \
|
||||||
'--out=[Output file path (default\: stdout)]:OUT:_files' \
|
'--out=[Output file path]:OUT:_files' \
|
||||||
'-h[Print help]' \
|
'-h[Print help]' \
|
||||||
'--help[Print help]' \
|
'--help[Print help]' \
|
||||||
'-V[Print version]' \
|
'-V[Print version]' \
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,8 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
|
||||||
'zoxide;export' {
|
'zoxide;export' {
|
||||||
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Output format (json or csv)')
|
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Output format (json or csv)')
|
||||||
[CompletionResult]::new('--format', '--format', [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('-o', '-o', [CompletionResultType]::ParameterName, 'Output file path')
|
||||||
[CompletionResult]::new('--out', '--out', [CompletionResultType]::ParameterName, 'Output file path (default: stdout)')
|
[CompletionResult]::new('--out', '--out', [CompletionResultType]::ParameterName, 'Output file path')
|
||||||
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')
|
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')
|
||||||
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')
|
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')
|
||||||
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
|
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@ set edit:completion:arg-completer[zoxide] = {|@words|
|
||||||
&'zoxide;export'= {
|
&'zoxide;export'= {
|
||||||
cand -f 'Output format (json or csv)'
|
cand -f 'Output format (json or csv)'
|
||||||
cand --format 'Output format (json or csv)'
|
cand --format 'Output format (json or csv)'
|
||||||
cand -o 'Output file path (default: stdout)'
|
cand -o 'Output file path'
|
||||||
cand --out 'Output file path (default: stdout)'
|
cand --out 'Output file path'
|
||||||
cand -h 'Print help'
|
cand -h 'Print help'
|
||||||
cand --help 'Print help'
|
cand --help 'Print help'
|
||||||
cand -V 'Print version'
|
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 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''
|
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''"
|
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 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 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''
|
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 entries from the database
|
||||||
export extern "zoxide export" [
|
export extern "zoxide export" [
|
||||||
--format(-f): string@"nu-complete zoxide export format" # Output format (json or csv)
|
--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
|
--help(-h) # Print help
|
||||||
--version(-V) # Print version
|
--version(-V) # Print version
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,10 @@ const completion: Fig.Spec = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: ["-o", "--out"],
|
name: ["-o", "--out"],
|
||||||
description: "Output file path (default: stdout)",
|
description: "Output file path",
|
||||||
isRepeatable: true,
|
isRepeatable: true,
|
||||||
args: {
|
args: {
|
||||||
name: "out",
|
name: "out",
|
||||||
isOptional: true,
|
|
||||||
template: "filepaths",
|
template: "filepaths",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,9 @@ pub struct Export {
|
||||||
#[clap(value_enum, long, short)]
|
#[clap(value_enum, long, short)]
|
||||||
pub format: ExportFormat,
|
pub format: ExportFormat,
|
||||||
|
|
||||||
/// Output file path (default: stdout)
|
/// Output file path
|
||||||
#[clap(long, short, value_hint = ValueHint::FilePath)]
|
#[clap(long, short, value_hint = ValueHint::FilePath)]
|
||||||
pub out: Option<PathBuf>,
|
pub out: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
#[derive(ValueEnum, Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
@ -17,6 +16,8 @@ impl Run for Export {
|
||||||
.context("could not serialize to JSON")?,
|
.context("could not serialize to JSON")?,
|
||||||
ExportFormat::Csv => {
|
ExportFormat::Csv => {
|
||||||
let mut wtr = csv::Writer::from_writer(Vec::new());
|
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 {
|
for dir in dirs {
|
||||||
wtr.write_record([&*dir.path, &dir.rank.to_string(), &dir.last_accessed.to_string()])
|
wtr.write_record([&*dir.path, &dir.rank.to_string(), &dir.last_accessed.to_string()])
|
||||||
.context("could not write CSV record")?;
|
.context("could not write CSV record")?;
|
||||||
|
|
@ -27,16 +28,8 @@ impl Run for Export {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match &self.out {
|
write_to_file(&self.out, &output)
|
||||||
Some(path) => {
|
.with_context(|| format!("could not write to file: {}", self.out.display()))?;
|
||||||
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")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
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()))?;
|
.with_context(|| format!("could not write to file: {}", path.display()))?;
|
||||||
Ok(())
|
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