My senior research project was implementing a Unix shell in the Rust language. Here’s a few things I discovered along the way.

This page is not complete! I will continue to update it with more information throughout April and May as I continue writing my final paper on this project.

Goals

Implementation

Execution

This was actually pretty straightforward. When you’re writing a shell in C, normally what you do is use the fork() system call, which creates a child process and returns its process ID (PID). In Rust, we don’t have to bother with as much lower-level stuff because of a struct called std::process::Command, which provides some easy methods for us to just load up whatever command we want and run it.

Executing commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pub fn exec(command: &str, args: Vec<&str>) -> i32 {
    let canon = canonical_path(&command);
    let path = Path::new(&canon);
    if path.is_file() {
        return if path.is_executable() {
            read_from_script(command, args)
        } else {
            println!("{}: permission denied: {}", env!("CARGO_PKG_NAME"), command);
            126
        };
    }
    return match command {
        "cd" => {
            let dest = args.first();
            cd(dest)
        }
        "info" => info(),
        "exit" => exit(0),
        "history" => display_history(),
        _ => {
            let child = Command::new(command).args(args).spawn();
            match child {
                Ok(x) => {
                    let mut y = x;
                    y.wait().unwrap().code().unwrap()
                }
                Err(_) => 127,
            }
        }
    };
}

Changing Directories

One of the most vital commands in any shell is cd, which stands for “change directory”. cd is a builtin, meaning that its functionality is literally written into the code of the shell (unlike the vast majority of programs, which are separate executables).

It just so happens that there’s a function for that, too! std::env::set_current_dir actually changes our directory. We just need to figure out which directory to go to, which is easier said than done because of shortcuts like .. (parent directory) and ~ (home directory). For example, assuming my home directory is /home/shreyas, ~/Music/../Code/senior-research/src/main.rs is the same thing as /home/shreyas/Code/senior-research/src/main.rs, but a program won’t necessarily view them as the same. The following code helps canonicalize paths so that you get taken to the right place.

Determining which directory to travel to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fn canonical_path(x: &&str) -> String {
    let homedir = home::home_dir().unwrap().display().to_string();
    let mut canon = String::from(*x);
    if x.starts_with(&"~") {
        canon = canon.replace("~", &homedir);
    } else if x.contains(&"..") {
        let mut spl = x.split("..").peekable();
        let mut np = Vec::new();
        while let Some(y) = spl.next() {
            let p = match spl.peek() {
                Some(_) => dotdot(PathBuf::from(y)),
                None => String::from(y),
            };
            np.push(p);
        }
        canon = np.join("/");
    }
    canon
}

I discovered some time after this that std::fs::canonicalize exists and does basically what I want, but I haven’t gotten around to replacing canonical_path with it yet.

History

The shell currently tries to open ~/.shell_history on startup, and if it doesn’t find the file, it panics and exits. I need to fix that error handling. For now, just run touch ~/.shell_history and try again.

Interface

The interface of the shell relied heavily on the termion library to interface with the terminal.

The Input Bug

Here’s how it looks now:

What I had to do instead
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
fn main() {
    let mut history = read_history();
    let mut history_pos = history.len();
    let mut beginning_of_line = prompt().len() as u16;
    let mut horiz_pos = beginning_of_line + 1;
    // Set terminal to raw mode to allow reading stdin one key at a time
    let mut stdout = MouseTerminal::from(io::stdout()).into_raw_mode().unwrap();
    write!(stdout, "{}", prompt()).unwrap();
    stdout.lock().flush().unwrap();

    // Use asynchronous stdin
    let mut stdin = termion::async_stdin().keys();

    // Our string
    let mut input = String::new();
    let mut length_of_line = beginning_of_line as u16;

    loop {
        // Read input (if any)
        let next = stdin.next();

        // If a key was pressed
        if let Some(Ok(key)) = next {
            match key {
                // Exit if 'Ctrl+c' is pressed
                termion::event::Key::Ctrl('c') => {
                    input = String::new();
                    write!(stdout, "\r\n").unwrap();
                    write!(stdout, "{}", prompt()).unwrap();
                }
                // Quit everything if 'Ctrl+d' is pressed
                termion::event::Key::Ctrl('d') => {
                    exit(0);
                }
                // Clear screen if 'Ctrl-l' is pressed
                termion::event::Key::Ctrl('l') => {
                    input = String::new();
                    write!(
                        stdout,
                        "{}{}\r",
                        termion::clear::All,
                        termion::cursor::Goto(1, 1)
                    )
                    .unwrap();
                    write!(stdout, "{}", prompt()).unwrap();
                }
                // Return command if 'Enter' is pressed
                termion::event::Key::Char('\n') => {
                    write!(stdout, "\r\n").unwrap();
                    stdout.suspend_raw_mode().unwrap();
                    let time = cmd_time();
                    let (cmd, vec) = separate(&input);
                    let ecode = match cmd {
                        Some(x) => exec(x, vec),
                        None => 0,
                    };
                    if ecode == 127 {
                        eprintln!("{}: command not found...", cmd.unwrap());
                    }
                    if cmd.is_some() && cmd.unwrap().ne("history") {
                        write_history(time, &input);
                        history.push(input);
                        history_pos = history.len();
                    }
                    input = String::new();
                    beginning_of_line = prompt().len() as u16;
                    horiz_pos = beginning_of_line + 1;
                    length_of_line = beginning_of_line as u16;
                    write!(stdout, "{}", prompt()).unwrap();
                    stdout.activate_raw_mode().unwrap();
                }
                termion::event::Key::Backspace => {
                    if input.len() > 0 {
                        input.pop();
                        write!(
                            stdout,
                            "{}{}{}",
                            termion::cursor::Left(1),
                            " ",
                            termion::cursor::Left(1)
                        )
                        .unwrap();
                        length_of_line -= 1;
                        horiz_pos -= 1;
                    }
                }
                termion::event::Key::Left => {
                    if horiz_pos > beginning_of_line + 1 {
                        write!(stdout, "{}", termion::cursor::Left(1)).unwrap();
                        horiz_pos -= 1;
                    }
                }
                termion::event::Key::Right => {
                    if horiz_pos < length_of_line {
                        write!(stdout, "{}", termion::cursor::Right(1)).unwrap();
                        horiz_pos += 1;
                    }
                }
                termion::event::Key::Up => {
                    write!(
                        stdout,
                        "{}{}{}",
                        termion::clear::CurrentLine,
                        termion::cursor::Left(horiz_pos),
                        prompt()
                    )
                    .unwrap();
                    if history_pos > 0 {
                        history_pos -= 1;
                        input = history[history_pos].clone();
                    } else {
                        input = history[0].clone();
                    }
                    length_of_line = beginning_of_line + input.len() as u16;
                    horiz_pos = length_of_line;
                    write!(stdout, "{}", input).unwrap();
                }
                termion::event::Key::Down => {
                    write!(
                        stdout,
                        "{}{}{}",
                        termion::clear::CurrentLine,
                        termion::cursor::Left(horiz_pos),
                        prompt()
                    )
                    .unwrap();
                    if history_pos < history.len() - 1 {
                        history_pos += 1;
                        input = history[history_pos].clone();
                        write!(stdout, "{}", input).unwrap();
                    } else {
                        input = String::new();
                    }
                    length_of_line = beginning_of_line + input.len() as u16;
                    horiz_pos = length_of_line;
                }
                termion::event::Key::Char(x) => {
                    input.push(x);
                    write!(stdout, "{}", x).unwrap();
                    length_of_line += 1;
                    horiz_pos += 1;
                }
                _ => {}
            }
        }
        stdout.lock().flush().unwrap();
        thread::sleep(time::Duration::from_millis(50));
    }
}

Yeah, it’s long.

Usage

The shell has issues with interactive commands. If you run python, most of the characters are captured by the shell instead of passing through to the Python console. Running another shell like bash or zsh causes the shell process to be suspended as soon as you start typing. I don’t really know what causes this behavior.