Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cmd.exe (experimental) #567

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d630c7c
Add support for `cmd.exe` (experimental)
mataha May 10, 2023
2972aac
Mark template tests with `cfg` directives
mataha May 11, 2023
2fdde1c
Update `dunce` to 1.0.4
mataha Jun 5, 2023
bc947f4
Normalize path prefix on Windows
mataha Jun 5, 2023
1d17390
Add tests for path prefix normalization
mataha Jun 5, 2023
8f7e5bd
Merge branch 'main' into feat/cmd.exe
mataha Jun 5, 2023
3d79691
Make the test run
mataha Jun 5, 2023
70321ca
Fix rustfmt errors
mataha Jun 5, 2023
a0c6535
Make percent characters work in all contexts
mataha Jun 6, 2023
690c0c4
Disable delayed expansion in `cmd init`
mataha Jun 6, 2023
52bcfab
Fix whitespace suppression in `cmd.exe` template
mataha Jun 6, 2023
f3b7ffe
Make sure `OLDPWD` is not set
mataha Jun 7, 2023
8201b8f
Don't run with Command Extensions disabled
mataha Jun 7, 2023
3888c84
Make the Command Extensions requirement more clear
mataha Jun 11, 2023
a2e7f62
Don't hardcode command names
mataha Jun 11, 2023
069cd75
Fall back to being `cd` if directory exists
mataha Jun 11, 2023
f2acb8e
Quote external command macros directly
mataha Jun 11, 2023
f1ad7e5
Sanitize trailing backslash when in root directory
mataha Jun 11, 2023
bde3ea8
Merge branch 'main' into feat/cmd.exe
mataha Jun 11, 2023
51b1185
Quit if Command Extensions are disabled
mataha Jun 11, 2023
efa8d1a
Tighten the check for command-line context
mataha Jun 17, 2023
6eadea5
Initialize hook only if it's a `pwd` hook
mataha Jun 17, 2023
4b62839
Add common shorthands as command aliases
mataha Jun 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Support for `cmd.exe` (experimental).

### Added

- Short option `-a` for `zoxide query --all`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bincode = "1.3.1"
clap = { version = "4.0.0", features = ["derive"] }
color-print = "0.3.4"
dirs = "5.0.0"
dunce = "1.0.1"
dunce = "1.0.4"
fastrand = "1.7.0"
glob = "0.3.0"
ouroboros = "0.15.5"
Expand Down
2 changes: 1 addition & 1 deletion contrib/completions/_zoxide

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contrib/completions/zoxide.bash

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contrib/completions/zoxide.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/cmd/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pub enum InitHook {
#[derive(ValueEnum, Clone, Debug)]
pub enum InitShell {
Bash,
Cmd,
Elvish,
Fish,
Nushell,
Expand Down
3 changes: 2 additions & 1 deletion src/cmd/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use askama::Template;
use crate::cmd::{Init, InitShell, Run};
use crate::config;
use crate::error::BrokenPipeHandler;
use crate::shell::{Bash, Elvish, Fish, Nushell, Opts, Posix, Powershell, Xonsh, Zsh};
use crate::shell::{Bash, Cmd, Elvish, Fish, Nushell, Opts, Posix, Powershell, Xonsh, Zsh};

impl Run for Init {
fn run(&self) -> Result<()> {
Expand All @@ -17,6 +17,7 @@ impl Run for Init {

let source = match self.shell {
InitShell::Bash => Bash(opts).render(),
InitShell::Cmd => Cmd(opts).render(),
InitShell::Elvish => Elvish(opts).render(),
InitShell::Fish => Fish(opts).render(),
InitShell::Nushell => Nushell(opts).render(),
Expand Down
36 changes: 35 additions & 1 deletion src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ macro_rules! make_template {
}

make_template!(Bash, "bash.txt");
make_template!(Cmd, "cmd.txt");
make_template!(Elvish, "elvish.txt");
make_template!(Fish, "fish.txt");
make_template!(Nushell, "nushell.txt");
Expand Down Expand Up @@ -53,6 +54,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn bash_bash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
Expand All @@ -65,6 +67,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn bash_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
Expand All @@ -79,6 +82,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn bash_shfmt(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Bash(&opts).render().unwrap();
Expand All @@ -94,6 +98,22 @@ mod tests {
}

#[apply(opts)]
mataha marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(windows)]
fn cmd_cmd(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't run as #[cfg(feature = "nix-dev")] is in effect. Should I pull it to a separate module?

let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Cmd(&opts).render().unwrap();

Command::new("cmd.exe")
.args(["/a", "/d", "/e:on", "/q", "/v:off", "/k", "@doskey", "/macros:cmd.exe"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}

#[apply(opts)]
#[cfg(unix)]
fn elvish_elvish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = String::default();
Expand All @@ -114,6 +134,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn fish_fish(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Fish(&opts).render().unwrap();
Expand All @@ -131,6 +152,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn fish_fishindent(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Fish(&opts).render().unwrap();
Expand All @@ -149,6 +171,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn nushell_nushell(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Nushell(&opts).render().unwrap();
Expand All @@ -169,6 +192,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn posix_bash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
Expand All @@ -184,6 +208,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn posix_dash(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
Expand All @@ -196,7 +221,8 @@ mod tests {
}

#[apply(opts)]
fn posix_shellcheck_(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
#[cfg(unix)]
fn posix_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();

Expand All @@ -210,6 +236,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn posix_shfmt(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Posix(&opts).render().unwrap();
Expand All @@ -225,6 +252,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn powershell_pwsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = "Set-StrictMode -Version latest\n".to_string();
Expand All @@ -239,6 +267,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn xonsh_black(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
Expand All @@ -253,6 +282,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn xonsh_mypy(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap();
Expand All @@ -261,6 +291,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn xonsh_pylint(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
Expand All @@ -275,6 +306,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn xonsh_xonsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap();
Expand All @@ -292,6 +324,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn zsh_shellcheck(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
Expand All @@ -307,6 +340,7 @@ mod tests {
}

#[apply(opts)]
#[cfg(unix)]
fn zsh_zsh(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
Expand Down
98 changes: 83 additions & 15 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,44 +291,77 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
}
}

fn get_drive_path(drive_letter: u8) -> PathBuf {
format!(r"{}:\", drive_letter as char).into()
fn get_drive_prefix_path(drive_letter: u8) -> PathBuf {
format!(r"{}:\", patch_drive_letter(drive_letter)).into()
}

fn get_drive_relative(drive_letter: u8) -> Result<PathBuf> {
fn get_drive_relative_path(drive_letter: u8) -> Result<PathBuf> {
let path = current_dir()?;
if Some(drive_letter) == get_drive_letter(&path) {
return Ok(path);
return Ok(patch_drive_prefix(path));
}

if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) {
return Ok(path.into());
if let Some(path) = env::var_os(format!("={}:", patch_drive_letter(drive_letter))) {
return Ok(patch_drive_prefix(path.into()));
}

let path = get_drive_path(drive_letter);
let path = get_drive_prefix_path(drive_letter);
Ok(path)
}

#[inline(always)]
fn patch_drive_letter(drive_letter: u8) -> char {
drive_letter.to_ascii_uppercase() as char
}

// https://github.com/rust-lang/rust-analyzer/pull/14689
fn patch_drive_prefix(path: PathBuf) -> PathBuf {
let mut components = path.components();

match components.next() {
Some(Component::Prefix(prefix)) => {
let prefix = match prefix.kind() {
Prefix::Disk(drive_letter) => {
format!(r"{}:", patch_drive_letter(drive_letter))
}
Prefix::VerbatimDisk(drive_letter) => {
format!(r"\\?\{}:", patch_drive_letter(drive_letter))
}
_ => return path,
};

let mut path = PathBuf::default();
path.push(prefix);
path.extend(components);
path
}
_ => path,
}
}

match components.peek() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive_letter) => {
let disk = components.next().unwrap();
components.next();
if components.peek() == Some(&Component::RootDir) {
let root = components.next().unwrap();
stack.push(disk);
stack.push(root);
components.next();
base_path = get_drive_prefix_path(drive_letter);
} else {
base_path = get_drive_relative(drive_letter)?;
stack.extend(base_path.components());
base_path = get_drive_relative_path(drive_letter)?;
}

stack.extend(base_path.components());
}
Prefix::VerbatimDisk(drive_letter) => {
components.next();
if components.peek() == Some(&Component::RootDir) {
components.next();
base_path = get_drive_prefix_path(drive_letter);
} else {
// Verbatim prefix without a root component? Likely not a legal path
bail!("illegal path: {}", path.display());
}

base_path = get_drive_path(drive_letter);
stack.extend(base_path.components());
}
_ => bail!("invalid path: {}", path.display()),
Expand All @@ -340,7 +373,7 @@ pub fn resolve_path(path: impl AsRef<Path>) -> Result<PathBuf> {
let drive_letter = get_drive_letter(&current_dir).with_context(|| {
format!("could not get drive letter: {}", current_dir.display())
})?;
base_path = get_drive_path(drive_letter);
base_path = get_drive_prefix_path(drive_letter);
stack.extend(base_path.components());
}
_ => {
Expand Down Expand Up @@ -377,3 +410,38 @@ pub fn to_lowercase(s: impl AsRef<str>) -> String {
let s = s.as_ref();
if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() }
}

#[cfg(test)]
#[cfg(windows)]
mod tests_win {
use std::path::PathBuf;

use rstest::rstest;

#[rstest]
#[case(r"c:\", r"C:\")]
#[case(r"C:\", r"C:\")]
#[case(r"c:\\.", r"C:\")]
#[case(r"c:\..", r"C:\")]
#[case(r"C:\.\.", r"C:\")]
#[case(r"\\?\C:\", r"C:\")]
#[case(r"\\?\c:\", r"C:\")]
#[case(r"\\?\C:\\\", r"C:\")]
#[case(r"\\?\c:\\.\", r"C:\")]
#[case(r"c:\Windows", r"C:\Windows")]
#[case(r"C:\WINDOWS", r"C:\WINDOWS")]
#[case(r"c:\\\Windows\.", r"C:\Windows")]
#[case(r"C:\$WinREAgent", r"C:\$WinREAgent")]
#[case(r"\\?\c:\\Windows\\.", r"C:\Windows")]
#[case(r"\\?\c:\..\.\windows", r"C:\windows")]
#[case(r"c:\Windows\System32\.", r"C:\Windows\System32")]
#[case(r"c:\WINDOWS\..\..\Windows", r"C:\Windows")]
#[case(r"c:\Windows\..\.\.\..\Temp\..\tmp", r"C:\tmp")]
#[case(r"c:\.\Windows\..\..\Program Files", r"C:\Program Files")]
#[case(r"\\?\C:\\$WinREAgent\\..\Program Files\.", r"C:\Program Files")]
fn resolve_path(#[case] absolute_path: &str, #[case] normalized_form: &str) {
let path = PathBuf::from(absolute_path);
let resolved_path = super::resolve_path(path).unwrap();
assert_eq!(resolved_path.to_str().unwrap(), normalized_form);
}
}