macos: quote input strings used for shell commands (#10583)

When we're building an input string that's explicitly meant to be used
as a shell command, quote it using the same logic as Python's
`shlex.quote` function.

This specifically addresses issues we've seen when open(1)'ing Ghostty
with filename arguments that contain spaces.

See #2633, #3030
This commit is contained in:
Jon Parise
2026-02-05 09:45:00 -05:00
committed by GitHub
4 changed files with 29 additions and 2 deletions

View File

@@ -476,7 +476,7 @@ class AppDelegate: NSObject,
// profile/rc files for the shell, which is super important on macOS
// due to things like Homebrew. Instead, we set the command to
// `<filename>; exit` which is what Terminal and iTerm2 do.
config.initialInput = "\(filename); exit\n"
config.initialInput = "\(filename.shellQuoted()); exit\n"
// For commands executed directly, we want to ensure we wait after exit
// because in most cases scripts don't block on exit and we don't want

View File

@@ -68,7 +68,7 @@ struct NewTerminalIntent: AppIntent {
// We don't run command as "command" and instead use "initialInput" so
// that we can get all the login scripts to setup things like PATH.
if let command {
config.initialInput = "\(command); exit\n"
config.initialInput = "\(command.shellQuoted()); exit\n"
}
// If we were given a working directory then open that directory

View File

@@ -26,4 +26,12 @@ extension String {
return self
}
#endif
private static let shellUnsafe = /[^\w@%+=:,.\/-]/
/// Returns a shell-escaped version of the string, like Python's shlex.quote.
func shellQuoted() -> String {
guard self.isEmpty || self.contains(Self.shellUnsafe) else { return self };
return "'" + self.replacingOccurrences(of: "'", with: #"'"'"'"#) + "'"
}
}

View File

@@ -0,0 +1,19 @@
import Testing
@testable import Ghostty
struct StringExtensionTests {
@Test(arguments: [
("", "''"),
("filename", "filename"),
("abcABC123@%_-+=:,./", "abcABC123@%_-+=:,./"),
("file name", "'file name'"),
("file$name", "'file$name'"),
("file!name", "'file!name'"),
("file\\name", "'file\\name'"),
("it's", "'it'\"'\"'s'"),
("file$'name'", "'file$'\"'\"'name'\"'\"''"),
])
func shellQuoted(input: String, expected: String) {
#expect(input.shellQuoted() == expected)
}
}