rush

repo

4年位前にRustの勉強のためにシェルを作ったのを思い出したので、久々に読んでみようと思います。

cargo run

TinMachine/lilium>/home/lilium/src/github.com/n01e0/rush [master]
💩 ls
Cargo.lock  Cargo.toml  LICENSE  README.md  rusty-tags.vi  src  target

いろいろ書いてあるのはいいんだけど、プロンプトをいい感じにするのはシェルの仕事では無いと思っています。

普通にうんこ出すな。

Cargo.toml

まずびっくりするのがedition = "2018"の表記、懐かしいですね。

あとバージョンを"*"にしている。カス。これはよくないと思います。

lib.rs

pub mod rush;

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn alias_test() {
        let mut shell = rush::Shell::new() ;
        shell.prompt("$").flush();
        let line = vec!["alias", "l=ls"].into_iter().map(|x| x.to_string()).collect::<Vec<String>>();
        shell.exec(line).finish();
        assert!(shell.alias.contains_key("l"));
        assert_eq!(shell.check_alias(vec!["l".to_string()]), vec!["ls".to_string()]);
        assert!(shell.check_alias(vec!["l".to_string()]).len() > 0);
    }
}

ちゃんとテストを書いていて偉いと思います。ところでこの頃ってまだmod.rsじゃなかったっけ

rush.sh

extern crateで始まっててびっくりした。これ最近evcxr以外で書かないな。

extern crate colored;
extern crate dirs;
extern crate hostname;

use colored::*;
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::io::{Write, stdout};
use std::process::{ Command, exit };

pub struct Shell {
    user: String,
    cwd: PathBuf,
    branch: String,
    code: i32,
    prompt: String,
    pub alias: HashMap<String, String>
}

impl Shell {
    pub fn new() -> Shell {
        Shell {
            user: String::new(),
            cwd: PathBuf::new(),
            branch: String::new(),
            code: 0,
            prompt: String::new(),
            alias: HashMap::new(),
        }
    }

    pub fn flush(&mut self) -> &mut Shell {
        self.user = if let Ok(output) = 
            Command::new("whoami").output() {
                let mut name = output.stdout;
                name.retain(|c| *c != 0xa);
                format!("{}/{}>", hostname::get().unwrap().into_string().unwrap(), String::from_utf8(name).unwrap())
        }else{"".to_string()};

        self.cwd = env::current_dir().unwrap();

        self.branch = if let Ok(output) = 
            Command::new("git").args(&["symbolic-ref", "--short", "HEAD"]).output() {
                let mut currnt_branch = output.stdout;
                currnt_branch.retain(|c| *c != 0xa);
                format!(" [{}]", 
                        String::from_utf8(currnt_branch).unwrap_or("".to_string())
                )
        }else{"".to_string()};

        let user = format!("{}", self.user).yellow();
        let cwd = format!("{}", self.cwd.to_str().unwrap()).red();
        let branch = format!("{}", self.branch).yellow();
        let prompt = format!("{} ", self.prompt);
        println!("{}{}{}", user, cwd, branch);
        print!("{}", prompt);
        stdout().flush().unwrap();
        self
    }

    pub fn prompt(&mut self, content: &str) -> &mut Shell {
        self.prompt = content.to_string();
        self
    }

    pub fn exec(&mut self, token: Vec<String>) -> &mut Shell {
        let eof = std::str::from_utf8(&[0]).unwrap();
        let tokens = self.check_alias(token);
        let command = if tokens.len() > 0 {
            &tokens[0]
        } else {
            "clear"
        };
        let args: Option<Vec<String>> = if tokens.len() > 1 {
            Some(tokens[1..].to_vec())
        } else {
            None
        };
        self.code = match command  {
            c if c == eof        => exit(0),
            "exit"               => exit(0),
            "cd"                 => crate::rush::chdir(args),
            "getenv"             => crate::rush::getenv(args),
            "setenv"             => crate::rush::setenv(args),
            "alias"              => self.set_alias(args),
            bin                  => crate::rush::launch(bin, args),
        };
        self
    }

    pub fn finish(&mut self) -> &mut Shell {
        self.cwd = dirs::home_dir().unwrap();
        self
    }

    pub fn check_alias(&self, tokens: Vec<String>) -> Vec<String> {
        let mut ret: Vec<String> = Vec::new();
        for tok in tokens {
            if self.alias.contains_key(&tok) {
                ret.push(self.alias.get(&tok).unwrap_or(&"".to_string()).to_string());
            } else {
                ret.push(tok);
            }
        }
        ret
    }

    fn set_alias(&mut self, args: Option<Vec<String>>) -> i32 {
        match args {
            Some(command) => {
                match command.len() {
                    1 => {
                        let command: Vec<String> = command[0].split('=').collect::<Vec<&str>>().iter().map(|x| x.to_string()).collect();
                        if command.len() == 2 {
                            self.alias.insert(command[0].clone(), command[1].clone());
                        } else {
                            eprintln!("{}", format!("Usage: alias <KEY=VALUE|KEY VALUE>").red());
                        }
                    },
                    2 => {
                        self.alias.insert(command[0].clone(), command[1].clone());
                    },
                    _ => eprintln!("{}", format!("Usage: alias <KEY=VALUE|KEY VALUE>").red()),
                }
            },
            None => eprintln!("{}", format!("Usage: alias <KEY=VALUE|KEY VALUE>").red()), 
        }

        return 0;
    }
}

fn setenv(args: Option<Vec<String>>) -> i32 {
    match args {
        Some(command) => {
            match command.len() {
                1 => {
                    let command: Vec<String> = command[0].split('=').collect::<Vec<&str>>().iter().map(|x| x.to_string()).collect();
                    if command.len() == 2 {
                        env::set_var(&command[0], &command[1]);
                    } else {
                        eprintln!("{}", format!("Usage: setenv <KEY=VALUE|KEY VALUE>").red());
                    }
                },
                2 => env::set_var(&command[0], &command[1]),
                _ => eprintln!("{}", format!("Usage: setenv <KEY=VALUE|KEY VALUE>").red()),
            }
        },
        None => eprintln!("{}", format!("Usage: setenv <KEY=VALUE|KEY VALUE>").red()), 
    }

    return 0;
}

fn getenv(args: Option<Vec<String>>) -> i32 {
    match args {
        Some(keys) => {
            for key in keys {
                match env::var(&key) {
                    Ok(val) => {
                        println!("{}: {:?}", key, val);
                        return 0;
                    },
                    Err(err) => {
                        eprintln!("{}", format!("could not interpret {}: {}", key, err).red());
                        return 1;
                    },
                }
            }
        },
        None => {
            eprintln!("{}", "Usage: getenv <env_key(s)>".red());
            return 1;
        },
    }
    return 0;
}

fn chdir(args: Option<Vec<String>>) -> i32 {
    match args {
        Some(path) => {
            let path = Path::new(&path[0]);
            if let Err(err) = env::set_current_dir(&path) {
                eprintln!("{}", format!("{}", err).red());
                eprintln!("{}", "Usage: cd <dir>".red());
                return 1;
            }
            return 0;
        },
        None => {
            match dirs::home_dir() {
                Some(home) => if let Err(err) = env::set_current_dir(home.as_path()) {
                    eprintln!("{}", format!("{}", err).red());
                    return 1;
                },
                None => eprintln!("{}", "Usage: cd <dir>".red()),
            }
            return 0;
        }
    }
}

fn launch(command: &str, args: Option<Vec<String>>) -> i32 {
    let mut proc = Command::new(command);
    if let Some(args) = args {
        proc.args(&args.iter().map(|x| x.as_str()).collect::<Vec<&str>>());
    }
    match proc.status() {
        Ok(command) => {
            command.code().unwrap()
        },
        Err(err) => {
            eprintln!("{}", format!("{} Command not found: {}", command, err).red());
            1
        }
    }
}

まぁなんかいろいろかいてありますが、anyhow使ってないのがびっくりです。最近はcargo newしたらすぐcargo add anyhow thiserror clapしてる気がするので。

pipeがなかったりとか致命的な機能不足があるけど、シェルの基本的な部分はできていそう。悪くないんじゃないか。

学習用のシェル

新しい言語の学習をする時にツールを作るのをよくやっていたが、シェルに関してもいい題材だと思う。

ネットワークやファイルのIOは無いが、OSの基本的な機能やプロセスの生成等が学べる。

あと言語関係なく普通に勉強としていいと思います。

シェルは別にこれといって特殊なプログラムではないし、OSと密接に紐付いた難しいプログラムでもなくて、入力を受け取ってforkしてexecしてるだけなんだよな。

子プロセスの生成とその管理も学べるし、やっぱり題材としてかなりいいと思います。おすすめです。

おわりに

この記事はn01e0 Advent Calendar 2023の15日目の記事です。

明日はあるかわかりません