macos: quote input strings used for shell commands

When we're building an input string that's explicitly meant to be used
as a shell command, quote (escape) 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.
This commit is contained in:
Jon Parise
2026-02-05 09:21:37 -05:00
parent b30456d9a8
commit 1ff0dd821f
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)
}
}