1use std::path::{Path, PathBuf};
13
14use super::conn::SshDest;
15
16pub const EXIT_CONNECTION_FAILED: u32 = 255;
19
20pub fn binary() -> Option<PathBuf> {
34 pick(on_path(), windows_client(), |p| {
35 !cfg!(windows) || (!is_posix_layer_build(p) && forces_askpass(p))
36 })
37}
38
39pub fn binary_for_user() -> Option<PathBuf> {
43 pick(on_path(), windows_client(), |p| !cfg!(windows) || !is_posix_layer_build(p))
44}
45
46fn on_path() -> impl Iterator<Item = PathBuf> {
47 which::which_all("ssh").into_iter().flatten()
48}
49
50fn windows_client() -> Option<PathBuf> {
52 #[cfg(windows)]
53 {
54 let root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into());
55 Some(Path::new(&root).join(r"System32\OpenSSH\ssh.exe")).filter(|p| p.is_file())
56 }
57 #[cfg(not(windows))]
58 {
59 None
60 }
61}
62
63fn pick(
66 on_path: impl IntoIterator<Item = PathBuf>,
67 windows_client: Option<PathBuf>,
68 preferred: impl Fn(&Path) -> bool,
69) -> Option<PathBuf> {
70 let on_path: Vec<PathBuf> = on_path.into_iter().collect();
71 if let Some(p) = on_path.iter().find(|p| preferred(p)) {
72 return Some(p.clone());
73 }
74 if let Some(w) = windows_client.as_ref().filter(|w| preferred(w)) {
75 return Some(w.clone());
76 }
77 on_path.into_iter().next().or(windows_client)
78}
79
80fn forces_askpass(ssh: &Path) -> bool {
83 static SEEN: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<PathBuf, bool>>> =
84 std::sync::OnceLock::new();
85 let seen = SEEN.get_or_init(Default::default);
86 if let Some(&ok) = seen.lock().unwrap_or_else(|e| e.into_inner()).get(ssh) {
87 return ok;
88 }
89 let mut cmd = std::process::Command::new(ssh);
90 cmd.arg("-V").stdin(std::process::Stdio::null());
91 #[cfg(windows)]
92 {
93 use agentmux_common::win32::NoWindow;
94 cmd.no_window();
95 }
96 let ok = cmd
97 .output()
98 .ok()
99 .and_then(|out| openssh_version(&String::from_utf8_lossy(&out.stderr)))
100 .is_some_and(|v| v >= (8, 4));
101 seen.lock().unwrap_or_else(|e| e.into_inner()).insert(ssh.to_path_buf(), ok);
102 ok
103}
104
105fn openssh_version(banner: &str) -> Option<(u32, u32)> {
108 let rest = &banner[banner.find("OpenSSH")?..];
109 let start = rest.find(|c: char| c.is_ascii_digit())?;
110 let mut parts = rest[start..].split(|c: char| !c.is_ascii_digit());
111 let major = parts.next()?.parse().ok()?;
112 let minor = parts.next()?.parse().ok()?;
113 Some((major, minor))
114}
115
116fn is_posix_layer_build(ssh: &Path) -> bool {
119 cfg!(windows)
120 && ssh
121 .parent()
122 .is_some_and(|dir| dir.join("msys-2.0.dll").is_file() || dir.join("cygwin1.dll").is_file())
123}
124
125pub fn missing_binary_message() -> String {
127 let how = if cfg!(windows) {
128 "install the OpenSSH Client (Settings > System > Optional features)"
129 } else if cfg!(target_os = "macos") {
130 "macOS includes it at /usr/bin/ssh; check that PATH reaches it"
131 } else {
132 "install your distribution's openssh-client package"
133 };
134 format!("SSH terminals need the ssh command, which was not found: {how}")
135}
136
137pub fn launch(
141 dest: &SshDest,
142 cmd: &str,
143 cmd_args: &[String],
144 cwd: &str,
145 control_dir: Option<&Path>,
146) -> Vec<String> {
147 let mut args: Vec<String> = vec!["-tt".into()];
148 let mut opt = |o: String| {
149 args.push("-o".into());
150 args.push(o);
151 };
152 opt("ServerAliveInterval=15".into());
154 opt("ServerAliveCountMax=3".into());
155 let build_info = crate::backend::pane_env::build_info();
161 let set_env = std::iter::once("TERM_PROGRAM=agentmux".to_string())
162 .chain(build_info.iter().map(|(k, v)| format!("{k}={v}")))
163 .collect::<Vec<_>>()
164 .join(" ");
165 opt(format!("SetEnv={set_env}"));
166 if let Some(dir) = control_dir {
167 opt("ControlMaster=auto".into());
168 opt(control_path_option(dir, dest));
169 opt("ControlPersist=10m".into());
170 }
171 push_destination(&mut args, dest);
172 if let Some(remote) = remote_command(cmd, cmd_args, cwd) {
173 args.push(remote);
174 }
175 args
176}
177
178pub fn set_pane_env(args: &mut [String], block_id: &str) {
184 if block_id.is_empty() || !block_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
185 return;
186 }
187 if let Some(set_env) = args.iter_mut().find(|a| a.starts_with("SetEnv=")) {
188 set_env.push_str(&format!(" AGENTMUX_BLOCKID={block_id}"));
189 }
190}
191
192fn push_destination(args: &mut Vec<String>, dest: &SshDest) {
196 if let Some(port) = dest.port {
197 args.push("-p".into());
198 args.push(port.to_string());
199 }
200 args.push("--".into());
201 args.push(dest.destination.clone());
202}
203
204const NAME_HASH_HEX: usize = 8;
206
207#[cfg(any(unix, test))]
210const SOCKET_NAME_MAX: usize = NAME_HASH_HEX + 1 + 40 + 17;
211
212fn control_path_option(dir: &Path, dest: &SshDest) -> String {
219 use sha2::{Digest, Sha256};
220 let name = hex::encode(&Sha256::digest(dest.name().as_bytes())[..NAME_HASH_HEX / 2]);
221 format!("ControlPath={}", dir.join(format!("{name}-%C")).display())
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum Control {
227 Check,
230 Exit,
232}
233
234pub fn control(dest: &SshDest, dir: &Path, op: Control) -> Vec<String> {
237 let word = match op {
238 Control::Check => "check",
239 Control::Exit => "exit",
240 };
241 let mut args = vec!["-o".into(), control_path_option(dir, dest), "-O".into(), word.into()];
242 push_destination(&mut args, dest);
243 args
244}
245
246pub fn launch_exec(
250 dest: &SshDest,
251 cmd: &str,
252 cwd: &str,
253 control_dir: Option<&Path>,
254) -> Vec<String> {
255 let mut args = launch(dest, cmd, &[], cwd, control_dir);
256 args[0] = "-T".into();
257 args
258}
259
260pub fn remote_command(cmd: &str, cmd_args: &[String], cwd: &str) -> Option<String> {
269 let run = if cmd.is_empty() {
270 None
271 } else if cmd_args.is_empty() {
272 Some(cmd.to_string())
274 } else {
275 Some(
276 std::iter::once(cmd)
277 .chain(cmd_args.iter().map(String::as_str))
278 .map(quote)
279 .collect::<Vec<_>>()
280 .join(" "),
281 )
282 };
283 let cd = (!cwd.trim().is_empty()).then(|| format!("cd {}; ", quote_path(cwd)));
286 match (cd, run) {
287 (None, None) => None,
288 (None, Some(run)) => Some(run),
289 (Some(cd), Some(run)) => Some(format!("{cd}{run}")),
290 (Some(cd), None) => Some(format!("{cd}exec \"$SHELL\" -l")),
291 }
292}
293
294pub fn quote_path(path: &str) -> String {
297 if path == "~" {
298 return "~".to_string();
299 }
300 match path.strip_prefix("~/") {
301 Some("") => "~/".to_string(),
302 Some(rest) => format!("~/{}", quote(rest)),
303 None => quote(path),
304 }
305}
306
307pub fn quote(word: &str) -> String {
309 if !word.is_empty()
310 && word
311 .chars()
312 .all(|c| c.is_ascii_alphanumeric() || "-_./=:@%+,".contains(c))
313 {
314 return word.to_string();
315 }
316 format!("'{}'", word.replace('\'', r"'\''"))
317}
318
319pub fn connection_lost(code: u32) -> bool {
323 code == EXIT_CONNECTION_FAILED
324}
325
326pub fn pane_lost_connection(code: Option<u32>, stopped_by_srv: bool) -> bool {
331 !stopped_by_srv && code.is_some_and(connection_lost)
332}
333
334pub fn lost_message(dest: &str) -> String {
336 format!("ssh could not connect to {dest}, or the connection dropped (see the terminal for ssh's own message)")
337}
338
339pub fn lost_note(conn: &str) -> String {
342 format!("No connection to {conn}: it dropped, or could not be made. Press Enter to try again.")
343}
344
345#[cfg(unix)]
358pub fn control_dir(config_home: &Path) -> Option<PathBuf> {
359 let base = std::env::var_os("XDG_RUNTIME_DIR")
360 .map(PathBuf::from)
361 .filter(|d| d.is_absolute())
362 .map(|d| d.join("agentmux"))
363 .unwrap_or_else(|| PathBuf::from(format!("/tmp/agentmux-{}", unsafe { libc::getuid() })));
364 control_dir_in(&base, config_home)
365}
366
367#[cfg(not(unix))]
368pub fn control_dir(_config_home: &Path) -> Option<PathBuf> {
369 None
370}
371
372pub fn shared_control_dir() -> Option<PathBuf> {
376 control_dir(&crate::backend::base::get_mux_config_dir())
377}
378
379#[cfg(unix)]
382fn control_dir_in(base: &Path, config_home: &Path) -> Option<PathBuf> {
383 use sha2::{Digest, Sha256};
384 let channel = hex::encode(&Sha256::digest(config_home.as_os_str().as_encoded_bytes())[..4]);
385 let dir = base.join(format!("ssh-{channel}"));
386 private_dir(base)?;
387 private_dir(&dir)?;
388 socket_fits(&dir).then_some(dir)
389}
390
391#[cfg(unix)]
395fn private_dir(dir: &Path) -> Option<()> {
396 use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
397 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
398 Ok(()) => {}
399 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
400 Err(_) => return None,
401 }
402 let meta = std::fs::symlink_metadata(dir).ok()?;
403 let ours = meta.file_type().is_dir() && meta.uid() == unsafe { libc::getuid() };
404 (ours && meta.permissions().mode() & 0o077 == 0).then_some(())
405}
406
407#[cfg(any(unix, test))]
411fn socket_fits(dir: &Path) -> bool {
412 let sun_path = if cfg!(target_os = "linux") { 108 } else { 104 };
413 dir.as_os_str().len() + 1 + SOCKET_NAME_MAX < sun_path
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
423 fn a_preferred_ssh_comes_first_and_otherwise_the_order_is_the_plain_one() {
424 let p = |s: &str| PathBuf::from(s);
425 let preferred = |path: &Path| path.starts_with("/native");
426 assert_eq!(
427 pick([p("/git/ssh"), p("/native/ssh")], Some(p("/win/ssh")), preferred),
428 Some(p("/native/ssh"))
429 );
430 let win_ok = |path: &Path| path.starts_with("/win");
431 assert_eq!(pick([p("/git/ssh")], Some(p("/win/ssh")), win_ok), Some(p("/win/ssh")));
432 let none = |_: &Path| false;
435 assert_eq!(pick([p("/git/ssh"), p("/old/ssh")], Some(p("/win/ssh")), none), Some(p("/git/ssh")));
436 assert_eq!(pick(Vec::<PathBuf>::new(), Some(p("/win/ssh")), none), Some(p("/win/ssh")));
437 assert_eq!(pick(Vec::<PathBuf>::new(), None, none), None);
438 }
439
440 #[test]
441 fn openssh_versions_are_read_from_the_banner() {
442 assert_eq!(openssh_version("OpenSSH_for_Windows_9.5p2, LibreSSL 3.8.2"), Some((9, 5)));
443 assert_eq!(openssh_version("OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2"), Some((8, 1)));
444 assert_eq!(openssh_version("OpenSSH_10.2p1, OpenSSL 3.5.4 30 Sep 2025"), Some((10, 2)));
445 assert_eq!(openssh_version("Dropbear v2022.83"), None);
446 assert!((8, 1) < (8, 4) && (10, 2) >= (8, 4));
447 }
448
449 #[cfg(windows)]
451 #[test]
452 fn an_msys_or_cygwin_ssh_is_told_from_a_native_one() {
453 let dir = tempfile::tempdir().unwrap();
454 let (msys, cygwin, native) = (dir.path().join("msys"), dir.path().join("cygwin"), dir.path().join("native"));
455 for d in [&msys, &cygwin, &native] {
456 std::fs::create_dir_all(d).unwrap();
457 std::fs::write(d.join("ssh.exe"), b"").unwrap();
458 }
459 std::fs::write(msys.join("msys-2.0.dll"), b"").unwrap();
460 std::fs::write(cygwin.join("cygwin1.dll"), b"").unwrap();
461 assert!(is_posix_layer_build(&msys.join("ssh.exe")));
462 assert!(is_posix_layer_build(&cygwin.join("ssh.exe")));
463 assert!(!is_posix_layer_build(&native.join("ssh.exe")));
464 }
465
466 fn dest(d: &str, port: Option<u16>) -> SshDest {
467 SshDest {
468 destination: d.to_string(),
469 port,
470 }
471 }
472
473 #[test]
474 fn a_plain_pane_is_the_login_shell_with_keepalives() {
475 let args = launch(&dest("asaf@area54", None), "", &[], "", None);
476 assert_eq!(
477 args,
478 [
479 "-tt",
480 "-o",
481 "ServerAliveInterval=15",
482 "-o",
483 "ServerAliveCountMax=3",
484 "-o",
485 build_set_env().as_str(),
486 "--",
487 "asaf@area54"
488 ]
489 );
490 }
491
492 fn build_set_env() -> String {
493 let mut s = "SetEnv=TERM_PROGRAM=agentmux".to_string();
494 for (k, v) in crate::backend::pane_env::build_info() {
495 s.push_str(&format!(" {k}={v}"));
496 }
497 s
498 }
499
500 #[test]
501 fn the_build_rides_along_without_touching_the_remote_command() {
502 let args = launch(&dest("area54", None), "claude", &[], "", None);
503 let set_env = args.iter().find(|a| a.starts_with("SetEnv=")).unwrap();
504 assert!(set_env.starts_with("SetEnv=TERM_PROGRAM=agentmux "));
505 assert!(set_env.contains(&format!("AGENTMUX_VERSION={}", env!("CARGO_PKG_VERSION"))));
506 assert_eq!(args.last().unwrap(), "claude", "durable panes' control commands go through here unchanged");
507 }
508
509 #[test]
510 fn a_plain_pane_names_itself_in_set_env() {
511 let mut args = launch(&dest("area54", None), "", &[], "", None);
512 let before = args.clone();
513 set_pane_env(&mut args, "6f1c2b9e-0a4d-4c3e-9b1a-2f5e7d8c9a01");
514 let set_env = args.iter().find(|a| a.starts_with("SetEnv=")).unwrap();
515 assert!(set_env.ends_with(" AGENTMUX_BLOCKID=6f1c2b9e-0a4d-4c3e-9b1a-2f5e7d8c9a01"), "{set_env}");
516 assert_eq!(args.len(), before.len(), "no argument added, only the SetEnv value");
517 let mut odd = before.clone();
518 set_pane_env(&mut odd, "x y=z");
519 assert_eq!(odd, before, "an id with any other character is left out");
520 }
521
522 #[test]
523 fn a_port_and_connection_sharing_come_before_the_destination() {
524 let args = launch(
525 &dest("area54", Some(2222)),
526 "",
527 &[],
528 "",
529 Some(Path::new("/home/u/.agentmux/ssh")),
530 );
531 let at = |s: &str| args.iter().position(|a| a == s).unwrap();
532 assert!(args.iter().any(|a| a == "ControlMaster=auto"));
533 assert!(args
534 .iter()
535 .any(|a| a.starts_with("ControlPath=") && a.ends_with("%C")));
536 assert_eq!(args[at("-p") + 1], "2222");
537 assert!(at("-p") < at("--") && at("--") + 1 == at("area54"));
538 assert_eq!(args.last().unwrap(), "area54");
539 }
540
541 fn control_path_opt(args: &[String]) -> &str {
542 args.iter().find(|a| a.starts_with("ControlPath=")).expect("a ControlPath")
543 }
544
545 #[test]
551 fn two_aliases_for_one_account_get_their_own_shared_connection() {
552 let dir = Path::new("/tmp/agentmux-501/ssh-1a2b3c4d");
553 let path = |d: &SshDest| control_path_opt(&launch(d, "", &[], "", Some(dir))).to_string();
554 let (key, pw) = (dest("exp-key", None), dest("exp-pw", None));
555 assert_ne!(path(&key), path(&pw));
556 assert_eq!(path(&key), path(&dest("exp-key", None)), "one name, one connection");
557 assert_ne!(path(&dest("u@h", Some(22))), path(&dest("u@h", Some(2222))), "the port is part of the name");
558 assert!(path(&key).ends_with("-%C"), "{}", path(&key));
560 }
561
562 #[test]
565 fn control_asks_the_connection_a_pane_shares() {
566 let dir = Path::new("/tmp/agentmux-501/ssh-1a2b3c4d");
567 let d = dest("tester@area54", Some(2222));
568 let pane = launch(&d, "", &[], "", Some(dir));
569 let check = control(&d, dir, Control::Check);
570 assert_eq!(control_path_opt(&check), control_path_opt(&pane));
571 assert_eq!(&check[check.len() - 6..], ["-O", "check", "-p", "2222", "--", "tester@area54"]);
572 let exit = control(&d, dir, Control::Exit);
573 assert_eq!(&exit[exit.len() - 6..], ["-O", "exit", "-p", "2222", "--", "tester@area54"]);
574 }
575
576 #[test]
580 fn only_ssh_s_own_failure_is_a_lost_connection() {
581 assert!(connection_lost(255));
582 for code in [0, 1, 2, 127, 130] {
583 assert!(!connection_lost(code), "{code}");
584 }
585 let note = lost_note("area54");
586 assert!(note.contains("area54") && note.contains("Press Enter"), "{note}");
587 }
588
589 #[cfg(unix)]
594 fn bind_path_len(dir: &Path) -> usize {
595 dir.as_os_str().len() + 1 + 8 + 1 + 40 + 17
596 }
597
598 #[cfg(unix)]
599 #[test]
600 fn a_deep_config_dir_still_gets_a_socket_path_ssh_can_bind() {
601 let tmp = tempfile::tempdir().unwrap();
605 let deep = tmp.path().join("users-name/.agentmux/channels/local-main-b28b7a-51e8e6a3/config");
606 let dir = control_dir(&deep).expect("a control dir");
607 let max = if cfg!(target_os = "linux") { 108 } else { 104 };
608 assert!(bind_path_len(&dir) < max, "{} bytes: {}", bind_path_len(&dir), dir.display());
609 let _ = std::fs::remove_dir(&dir);
611 }
612
613 #[cfg(unix)]
616 fn short_tempdir() -> tempfile::TempDir {
617 tempfile::Builder::new().prefix("am").tempdir_in("/tmp").unwrap()
618 }
619
620 #[cfg(unix)]
621 #[test]
622 fn each_channel_gets_its_own_owner_only_dir() {
623 use std::os::unix::fs::PermissionsExt;
624 let tmp = short_tempdir();
625 let base = tmp.path().join("b");
626 let (stable, local) = (Path::new("/u/.agentmux/channels/stable/config"), Path::new("/u/.agentmux/channels/local-x/config"));
627 let a = control_dir_in(&base, stable).unwrap();
628 assert_eq!(control_dir_in(&base, stable), Some(a.clone()));
629 assert_ne!(control_dir_in(&base, local), Some(a.clone()));
630 let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
631 assert_eq!((mode(&base), mode(&a)), (0o700, 0o700));
632 }
633
634 #[cfg(unix)]
635 #[test]
636 fn a_base_others_can_enter_or_a_symlink_is_refused() {
637 use std::os::unix::fs::PermissionsExt;
638 let tmp = short_tempdir();
639 let config = Path::new("/u/config");
640 let open = tmp.path().join("open");
641 std::fs::create_dir(&open).unwrap();
642 std::fs::set_permissions(&open, std::fs::Permissions::from_mode(0o755)).unwrap();
643 assert_eq!(control_dir_in(&open, config), None);
644 let real = tmp.path().join("real");
645 std::fs::create_dir(&real).unwrap();
646 std::fs::set_permissions(&real, std::fs::Permissions::from_mode(0o700)).unwrap();
647 let link = tmp.path().join("link");
648 std::os::unix::fs::symlink(&real, &link).unwrap();
649 assert_eq!(control_dir_in(&link, config), None);
650 }
651
652 #[test]
653 fn a_socket_fits_only_below_sun_path() {
654 let sun_path = if cfg!(target_os = "linux") { 108 } else { 104 };
655 let dir = |len: usize| PathBuf::from(format!("/{}", "d".repeat(len - 1)));
656 let longest = sun_path - 1 - (1 + 8 + 1 + 40 + 17);
657 assert!(socket_fits(&dir(longest)));
658 assert!(!socket_fits(&dir(longest + 1)));
659 assert!(!socket_fits(Path::new("/Users/asafebgi/.agentmux/channels/stable/config/ssh")) || cfg!(target_os = "linux"));
661 }
662
663 #[test]
664 fn a_command_and_a_cwd_become_one_remote_command_line() {
665 assert_eq!(remote_command("", &[], ""), None);
666 assert_eq!(
667 remote_command("make test", &[], ""),
668 Some("make test".into())
669 );
670 assert_eq!(
671 remote_command("", &[], "/srv/my app"),
672 Some(r#"cd '/srv/my app'; exec "$SHELL" -l"#.into())
673 );
674 assert_eq!(
675 remote_command("htop", &["-d".into(), "10".into()], "/tmp"),
676 Some("cd /tmp; htop -d 10".into())
677 );
678 assert_eq!(
679 remote_command("echo", &["it's $HOME".into()], ""),
680 Some(r"echo 'it'\''s $HOME'".into())
681 );
682 let args = launch(&dest("h", None), "uptime", &[], "/tmp", None);
683 assert_eq!(&args[args.len() - 3..], ["--", "h", "cd /tmp; uptime"]);
684 }
685
686 #[test]
687 fn a_command_without_a_terminal_is_dash_capital_t() {
688 let args = launch_exec(&dest("area54", Some(22)), "make test", "~/proj", None);
689 assert_eq!(args[0], "-T");
690 assert!(!args.iter().any(|a| a == "-tt"));
691 assert_eq!(
692 &args[args.len() - 3..],
693 ["--", "area54", "cd ~/proj; make test"]
694 );
695 }
696
697 #[test]
698 fn only_a_connection_failure_is_an_error() {
699 assert!(lost_message("area54").contains("could not connect to area54"));
700 assert!(pane_lost_connection(Some(255), false));
701 for code in [Some(0), Some(1), Some(130), None] {
702 assert!(!pane_lost_connection(code, false), "{code:?}");
703 }
704 }
705
706 #[test]
711 fn ssh_ended_by_srv_is_not_a_lost_connection() {
712 assert!(!pane_lost_connection(Some(255), true));
713 }
714
715 #[test]
717 fn a_tilde_cwd_is_the_remote_home() {
718 assert_eq!(quote_path("~"), "~");
719 assert_eq!(quote_path("~/"), "~/");
720 assert_eq!(quote_path("~/my proj"), "~/'my proj'");
721 assert_eq!(quote_path("~/src"), "~/src");
722 assert_eq!(
723 quote_path("/a/~b"),
724 "'/a/~b'",
725 "a tilde inside a path stays literal"
726 );
727 assert_eq!(quote_path("~bob/x"), "'~bob/x'", "only the user's own home");
728 assert_eq!(
729 remote_command("", &[], "~/proj"),
730 Some(r#"cd ~/proj; exec "$SHELL" -l"#.into())
731 );
732 }
733
734 #[test]
735 fn quoting_leaves_plain_words_alone() {
736 assert_eq!(quote("src/main.rs"), "src/main.rs");
737 assert_eq!(quote(""), "''");
738 assert_eq!(quote("a b"), "'a b'");
739 assert_eq!(quote("$(rm -rf ~)"), "'$(rm -rf ~)'");
740 }
741}