Skip to main content

zsh/
clone.rs

1//! Clone module - port of Modules/clone.c
2//!
3//! Provides the clone builtin to start a forked instance of the shell on a new terminal.
4
5use std::io;
6
7/// Clone the current shell to a new terminal
8#[cfg(unix)]
9pub fn clone_shell(tty_path: &str) -> io::Result<u32> {
10    use std::ffi::CString;
11    use std::os::unix::io::RawFd;
12
13    let tty_c = CString::new(tty_path)
14        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid tty path"))?;
15
16    let ttyfd: RawFd = unsafe { libc::open(tty_c.as_ptr(), libc::O_RDWR | libc::O_NOCTTY) };
17
18    if ttyfd < 0 {
19        return Err(io::Error::last_os_error());
20    }
21
22    let pid = unsafe { libc::fork() };
23
24    match pid {
25        -1 => {
26            unsafe { libc::close(ttyfd) };
27            Err(io::Error::last_os_error())
28        }
29        0 => {
30            unsafe {
31                if libc::setsid() == -1 {
32                    eprintln!(
33                        "clone: failed to create new session: {}",
34                        io::Error::last_os_error()
35                    );
36                }
37
38                libc::dup2(ttyfd, 0);
39                libc::dup2(ttyfd, 1);
40                libc::dup2(ttyfd, 2);
41
42                if ttyfd > 2 {
43                    libc::close(ttyfd);
44                }
45
46                let cttyfd = libc::open(tty_c.as_ptr(), libc::O_RDWR);
47                if cttyfd >= 0 {
48                    #[cfg(any(target_os = "linux", target_os = "macos"))]
49                    {
50                        libc::ioctl(cttyfd, libc::TIOCSCTTY as libc::c_ulong, 0);
51                    }
52                    libc::close(cttyfd);
53                }
54            }
55
56            Ok(0)
57        }
58        child_pid => {
59            unsafe { libc::close(ttyfd) };
60            Ok(child_pid as u32)
61        }
62    }
63}
64
65#[cfg(not(unix))]
66pub fn clone_shell(_tty_path: &str) -> io::Result<u32> {
67    Err(io::Error::new(
68        io::ErrorKind::Unsupported,
69        "clone not supported",
70    ))
71}
72
73/// Execute clone builtin
74pub fn builtin_clone(args: &[&str]) -> (i32, String, Option<u32>) {
75    if args.is_empty() {
76        return (1, "clone: terminal required\n".to_string(), None);
77    }
78
79    match clone_shell(args[0]) {
80        Ok(pid) => (0, String::new(), Some(pid)),
81        Err(e) => (1, format!("clone: {}: {}\n", args[0], e), None),
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_builtin_clone_no_args() {
91        let (status, _, _) = builtin_clone(&[]);
92        assert_eq!(status, 1);
93    }
94
95    #[test]
96    fn test_builtin_clone_invalid_tty() {
97        let (status, output, _) = builtin_clone(&["/nonexistent/tty"]);
98        assert_eq!(status, 1);
99        assert!(output.contains("clone"));
100    }
101}