1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2025-11-06 22:44:18 -05:00

Add alacritty_terminal option to escape pty args

This adds a new `escape_args` option to `tty::Options` on Windows, which
can be used to automatically escape all arguments passed to the shell.

While useful to automatically make most arguments work on Windows, there
are some scenarios where it is not possible for users to properly
specify arguments with this option enabled (e.g.: `cmd /c`). An option
should always be present to disable this option when used.

The implementation is based on the `Command` code in Rust's STD lib.
This commit is contained in:
feeiyu 2025-08-03 00:39:00 +08:00 committed by GitHub
parent f07622e908
commit 84377a45a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 122 additions and 5 deletions

View file

@ -200,6 +200,8 @@ impl From<TerminalOptions> for PtyOptions {
shell: options.command().map(Into::into),
drain_on_exit: options.hold,
env: HashMap::new(),
#[cfg(target_os = "windows")]
escape_args: false,
}
}
}

View file

@ -132,7 +132,14 @@ impl UiConfig {
let shell = self.terminal.shell.clone().or_else(|| self.shell.clone()).map(Into::into);
let working_directory =
self.working_directory.clone().or_else(|| self.general.working_directory.clone());
PtyOptions { working_directory, shell, drain_on_exit: false, env: HashMap::new() }
PtyOptions {
working_directory,
shell,
drain_on_exit: false,
env: HashMap::new(),
#[cfg(target_os = "windows")]
escape_args: false,
}
}
#[inline]

View file

@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 0.25.1-dev
### Added
- New `escape_args` field on `tty::Options` for Windows shell argument escaping control
### Changed
- Pass `-q` to `login` on macOS if `~/.hushlogin` is present

View file

@ -33,6 +33,13 @@ pub struct Options {
/// Extra environment variables.
pub env: HashMap<String, String>,
/// Specifies whether the Windows shell arguments should be escaped.
///
/// - When `true`: Arguments will be escaped according to the standard C runtime rules.
/// - When `false`: Arguments will be passed raw without additional escaping.
#[cfg(target_os = "windows")]
pub escape_args: bool,
}
/// Shell options.

View file

@ -125,14 +125,52 @@ impl OnResize for Pty {
}
}
// Modified per stdlib implementation.
// https://github.com/rust-lang/rust/blob/6707bf0f59485cf054ac1095725df43220e4be20/library/std/src/sys/args/windows.rs#L174
fn push_escaped_arg(cmd: &mut String, arg: &str) {
let arg_bytes = arg.as_bytes();
let quote = arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty();
if quote {
cmd.push('"');
}
let mut backslashes: usize = 0;
for x in arg.chars() {
if x == '\\' {
backslashes += 1;
} else {
if x == '"' {
// Add n+1 backslashes to total 2n+1 before internal '"'.
cmd.extend((0..=backslashes).map(|_| '\\'));
}
backslashes = 0;
}
cmd.push(x);
}
if quote {
// Add n backslashes to total 2n before ending '"'.
cmd.extend((0..backslashes).map(|_| '\\'));
cmd.push('"');
}
}
fn cmdline(config: &Options) -> String {
let default_shell = Shell::new("powershell".to_owned(), Vec::new());
let shell = config.shell.as_ref().unwrap_or(&default_shell);
once(shell.program.as_str())
.chain(shell.args.iter().map(|s| s.as_str()))
.collect::<Vec<_>>()
.join(" ")
let mut cmd = String::new();
cmd.push_str(&shell.program);
for arg in &shell.args {
cmd.push(' ');
if config.escape_args {
push_escaped_arg(&mut cmd, arg);
} else {
cmd.push_str(arg)
}
}
cmd
}
/// Converts the string slice into a Windows-standard representation for "W"-
@ -140,3 +178,62 @@ fn cmdline(config: &Options) -> String {
pub fn win32_string<S: AsRef<OsStr> + ?Sized>(value: &S) -> Vec<u16> {
OsStr::new(value).encode_wide().chain(once(0)).collect()
}
#[cfg(test)]
mod test {
use crate::tty::windows::{cmdline, push_escaped_arg};
use crate::tty::{Options, Shell};
#[test]
fn test_escape() {
let test_set = vec![
// Basic cases - no escaping needed
("abc", "abc"),
// Cases requiring quotes (space/tab)
("", "\"\""),
(" ", "\" \""),
("ab c", "\"ab c\""),
("ab\tc", "\"ab\tc\""),
// Cases with backslashes only (no spaces, no quotes) - no quotes added
("ab\\c", "ab\\c"),
// Cases with quotes only (no spaces) - quotes escaped but no outer quotes
("ab\"c", "ab\\\"c"),
("\"", "\\\""),
("a\"b\"c", "a\\\"b\\\"c"),
// Cases requiring both quotes and escaping (contains spaces)
("ab \"c", "\"ab \\\"c\""),
("a \"b\" c", "\"a \\\"b\\\" c\""),
// Complex real-world cases
("C:\\Program Files\\", "\"C:\\Program Files\\\\\""),
("C:\\Program Files\\a.txt", "\"C:\\Program Files\\a.txt\""),
(
r#"sh -c "cd /home/user; ARG='abc' \""'${SHELL:-sh}" -i -c '"'echo hello'""#,
r#""sh -c \"cd /home/user; ARG='abc' \\\"\"'${SHELL:-sh}\" -i -c '\"'echo hello'\"""#,
),
];
for (input, expected) in test_set {
let mut escaped_arg = String::new();
push_escaped_arg(&mut escaped_arg, input);
assert_eq!(escaped_arg, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_cmdline() {
let mut options = Options {
shell: Some(Shell {
program: "echo".to_string(),
args: vec!["hello world".to_string()],
}),
working_directory: None,
drain_on_exit: true,
env: Default::default(),
escape_args: false,
};
assert_eq!(cmdline(&options), "echo hello world");
options.escape_args = true;
assert_eq!(cmdline(&options), "echo \"hello world\"");
}
}