diff --git a/Cargo.lock b/Cargo.lock index 27da609..66ce4ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,27 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "difflib" version = "0.4.0" @@ -712,18 +733,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -994,6 +1025,7 @@ dependencies = [ "clap_complete_fig", "clap_complete_nushell", "color-print", + "csv", "dirs", "dunce", "fastrand", @@ -1003,6 +1035,7 @@ dependencies = [ "rstest", "rstest_reuse", "serde", + "serde_json", "tempfile", "which", ] diff --git a/Cargo.toml b/Cargo.toml index d137115..16f3ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,10 @@ dirs = "6.0.0" dunce = "1.0.1" fastrand = "2.0.0" glob = "0.3.0" +csv = "1.2.0" ouroboros = "0.18.3" serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.96" [target.'cfg(unix)'.dependencies] nix = { version = "0.30.1", default-features = false, features = [ diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 97e654f..655998f 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -94,6 +94,18 @@ _arguments "${_arguments_options[@]}" : \ ;; esac ;; +(export) +_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' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ +&& ret=0 +;; (import) _arguments "${_arguments_options[@]}" : \ '--from=[Application to import from]:FROM:(autojump z)' \ @@ -155,6 +167,7 @@ _zoxide_commands() { local commands; commands=( 'add:Add a new directory or increment its rank' \ 'edit:Edit the database' \ +'export:Export entries from the database' \ 'import:Import entries from another application' \ 'init:Generate shell configuration' \ 'query:Search for a directory in the database' \ @@ -197,6 +210,11 @@ _zoxide__edit__reload_commands() { local commands; commands=() _describe -t commands 'zoxide edit reload commands' commands "$@" } +(( $+functions[_zoxide__export_commands] )) || +_zoxide__export_commands() { + local commands; commands=() + _describe -t commands 'zoxide export commands' commands "$@" +} (( $+functions[_zoxide__import_commands] )) || _zoxide__import_commands() { local commands; commands=() diff --git a/contrib/completions/_zoxide.ps1 b/contrib/completions/_zoxide.ps1 index bb47d3a..1f70e8d 100644 --- a/contrib/completions/_zoxide.ps1 +++ b/contrib/completions/_zoxide.ps1 @@ -27,6 +27,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database') + [CompletionResult]::new('export', 'export', [CompletionResultType]::ParameterValue, 'Export entries from the database') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') @@ -81,6 +82,17 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } + '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('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') + [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') + break + } 'zoxide;import' { [CompletionResult]::new('--from', '--from', [CompletionResultType]::ParameterName, 'Application to import from') [CompletionResult]::new('--merge', '--merge', [CompletionResultType]::ParameterName, 'Merge into existing database') diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 82b174e..c068c7f 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -22,6 +22,9 @@ _zoxide() { zoxide,edit) cmd="zoxide__edit" ;; + zoxide,export) + cmd="zoxide__export" + ;; zoxide,import) cmd="zoxide__import" ;; @@ -53,7 +56,7 @@ _zoxide() { case "${cmd}" in zoxide) - opts="-h -V --help --version add edit import init query remove" + opts="-h -V --help --version add edit export import init query remove" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -158,6 +161,58 @@ _zoxide() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + zoxide__export) + opts="-f -o -h -V --format --out --help --version" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --format) + COMPREPLY=($(compgen -W "json csv" -- "${cur}")) + return 0 + ;; + -f) + COMPREPLY=($(compgen -W "json csv" -- "${cur}")) + return 0 + ;; + --out) + local oldifs + if [ -n "${IFS+x}" ]; then + oldifs="$IFS" + fi + IFS=$'\n' + COMPREPLY=($(compgen -f "${cur}")) + if [ -n "${oldifs+x}" ]; then + IFS="$oldifs" + fi + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o filenames + fi + return 0 + ;; + -o) + local oldifs + if [ -n "${IFS+x}" ]; then + oldifs="$IFS" + fi + IFS=$'\n' + COMPREPLY=($(compgen -f "${cur}")) + if [ -n "${oldifs+x}" ]; then + IFS="$oldifs" + fi + if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then + compopt -o filenames + fi + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; zoxide__import) opts="-h -V --from --merge --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/contrib/completions/zoxide.elv b/contrib/completions/zoxide.elv index 93c57af..6fac0cf 100644 --- a/contrib/completions/zoxide.elv +++ b/contrib/completions/zoxide.elv @@ -24,6 +24,7 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand --version 'Print version' cand add 'Add a new directory or increment its rank' cand edit 'Edit the database' + cand export 'Export entries from the database' cand import 'Import entries from another application' cand init 'Generate shell configuration' cand query 'Search for a directory in the database' @@ -71,6 +72,16 @@ set edit:completion:arg-completer[zoxide] = {|@words| cand -V 'Print version' cand --version 'Print version' } + &'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 -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' + } &'zoxide;import'= { cand --from 'Application to import from' cand --merge 'Merge into existing database' diff --git a/contrib/completions/zoxide.fish b/contrib/completions/zoxide.fish index 3a0bfe7..8c8e396 100644 --- a/contrib/completions/zoxide.fish +++ b/contrib/completions/zoxide.fish @@ -28,6 +28,7 @@ complete -c zoxide -n "__fish_zoxide_needs_command" -s h -l help -d 'Print help' complete -c zoxide -n "__fish_zoxide_needs_command" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "add" -d 'Add a new directory or increment its rank' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "edit" -d 'Edit the database' +complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "export" -d 'Export entries from the database' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "import" -d 'Import entries from another application' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_zoxide_needs_command" -f -a "query" -d 'Search for a directory in the database' @@ -49,6 +50,11 @@ 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 increment" -s V -l version -d 'Print version' complete -c zoxide -n "__fish_zoxide_using_subcommand edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help' 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 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'' z\t''" complete -c zoxide -n "__fish_zoxide_using_subcommand import" -l merge -d 'Merge into existing database' diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 642908e..a3916d1 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -43,6 +43,18 @@ module completions { --version(-V) # Print version ] + def "nu-complete zoxide export format" [] { + [ "json" "csv" ] + } + + # 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) + --help(-h) # Print help + --version(-V) # Print version + ] + def "nu-complete zoxide import from" [] { [ "autojump" "z" ] } diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 1e0d404..e8bae34 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -111,6 +111,42 @@ const completion: Fig.Spec = { }, ], }, + { + name: "export", + description: "Export entries from the database", + options: [ + { + name: ["-f", "--format"], + description: "Output format (json or csv)", + isRepeatable: true, + args: { + name: "format", + suggestions: [ + "json", + "csv", + ], + }, + }, + { + name: ["-o", "--out"], + description: "Output file path (default: stdout)", + isRepeatable: true, + args: { + name: "out", + isOptional: true, + template: "filepaths", + }, + }, + { + name: ["-h", "--help"], + description: "Print help", + }, + { + name: ["-V", "--version"], + description: "Print version", + }, + ], + }, { name: "import", description: "Import entries from another application", diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 7359786..f2d6a18 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -43,6 +43,7 @@ https://github.com/ajeetdsouza/zoxide pub enum Cmd { Add(Add), Edit(Edit), + Export(Export), Import(Import), Init(Init), Query(Query), @@ -88,6 +89,28 @@ pub enum EditCommand { Reload, } +/// Export entries from the database +#[derive(Debug, Parser)] +#[clap( + author, + help_template = HelpTemplate, +)] +pub struct Export { + /// Output format (json or csv) + #[clap(value_enum, long, short)] + pub format: ExportFormat, + + /// Output file path (default: stdout) + #[clap(long, short, value_hint = ValueHint::FilePath)] + pub out: Option, +} + +#[derive(ValueEnum, Clone, Debug)] +pub enum ExportFormat { + Json, + Csv, +} + /// Import entries from another application #[derive(Debug, Parser)] #[clap( diff --git a/src/cmd/export.rs b/src/cmd/export.rs new file mode 100644 index 0000000..430cae9 --- /dev/null +++ b/src/cmd/export.rs @@ -0,0 +1,56 @@ +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +use anyhow::{Context, Result}; + +use crate::cmd::{Export, ExportFormat, Run}; +use crate::db::Database; + +impl Run for Export { + fn run(&self) -> Result<()> { + let db = Database::open()?; + let dirs = db.dirs(); + + let output = match self.format { + ExportFormat::Json => serde_json::to_string(dirs) + .context("could not serialize to JSON")?, + ExportFormat::Csv => { + let mut wtr = csv::Writer::from_writer(Vec::new()); + for dir in dirs { + wtr.write_record([&*dir.path, &dir.rank.to_string(), &dir.last_accessed.to_string()]) + .context("could not write CSV record")?; + } + wtr.flush().context("could not flush CSV writer")?; + String::from_utf8(wtr.into_inner().context("could not get CSV bytes")?) + .context("CSV output is not valid UTF-8")? + } + }; + + 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")?; + } + } + + Ok(()) + } +} + +fn write_to_file(path: impl AsRef, content: &str) -> Result<()> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent) + .with_context(|| format!("could not create directory: {}", parent.display()))?; + } + } + fs::write(path, content) + .with_context(|| format!("could not write to file: {}", path.display()))?; + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 5c17474..a068cac 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,6 +1,7 @@ mod add; mod cmd; mod edit; +mod export; mod import; mod init; mod query; @@ -19,6 +20,7 @@ impl Run for Cmd { match self { Cmd::Add(cmd) => cmd.run(), Cmd::Edit(cmd) => cmd.run(), + Cmd::Export(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(),