Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Building your own shell using Rust (joshmcguigan.com)
216 points by boyter on Nov 18, 2018 | hide | past | favorite | 52 comments


Hi all, author here. I'm not sure of the rules around self promotion so I want to point out that I didn't submit this, but I'm happy to see that others are enjoying it.

I wrote this blog post to document my process of building a simple shell using Rust. I learned a lot about how shells, terminal emulators, and the OS interact by going through this exercise, so I hope this is useful to others as well.

Feel free to reach out if you have any questions.


For future reference, it's fine to submit your own stuff as long as you don't overdo it. For things that can be tried out, there's also a special Show HN category: https://news.ycombinator.com/show


Tooting your own horn is a foundational block of HN and very welcome!


If you have open source projects, then I would like to join


I've linked my GitHub profile below. I really enjoy social coding, so feel free to open pull requests or issues on any of my repos.

https://github.com/JoshMcguigan


I've never used Rust, but looking at the code, and reading https://doc.rust-lang.org/std/process/struct.Command.html , is std::process:Command just farming out to bash? I ask, because the examples don't specify a full path, so that implies something else is doing the PATH search, like bash. Because of this, and the semantics, it looks like it's equivalent to a system() function in other languages.

If you really want job control, a path, environmental variables, and all the things that make a shell a shell, you have to be using exec() or execvp() https://doc.rust-lang.org/std/process/struct.Command.html#me... . Otherwise all you've done is wrap an existing shell that does all the work for you, and that was an immediate fail when we had to a make a shell in my undergrad systems course.

An easy check is the use the command "echo $SHELL". If that spits out anything besides literally "$SHELL", you're farming out.

All that said, Rust's exec() is listed a unix specific, so if you're on Windows, I guess you're stuck.


I'm not confident enough to say this for sure, but I think you are off base.

I created a demo in the Rust playground [0] which shows that there is no implicit environment variable expansion. Also, the source for `std::process:Command` [1] looks to be calling into libc to perform a fork/exec.

That said, this is rather new territory for me, so let me know if I'm missing something here.

[0] : https://play.rust-lang.org/?version=stable&mode=debug&editio... [1] : https://github.com/rust-lang/rust/blob/master/src/libstd/sys...


It will use `execvp` (which automatically reads the PATH variable, no shell required) if you use features that require it (like `before_exec` which runs a block of code between `fork` and `exec), but it generally prefers `posix_spawnp` (Which also uses PATH). The relevant code is here: https://github.com/rust-lang/rust/blob/7d3b9b1640611c52eb949....

All of this is of course Unix specific, on Windows it uses `CreateProcess`.


If you or anyone else would like to file a bug, that’d be great: we should make this more clear in the docs. I’m about to jump on a plane or I’d just do it; a bug will make sure I get back around to it eventually.


Why would the docs need to be updated? Is it assumed that a command would be executing a bash or other shell process?


If something isn’t clear, it deserves an update.

This could be by adding some text to clarify that this doesn’t use a shell, or maybe changing the examples to not imply one is used. It just depends.


The internal implementation of Command is using libc::execvp() on unix and CreateProcessW on windows. In windows case, it's searching the executable in the PATH manually first to match the semantics of unix.


If you look at the examples in the first link you posted, you can see to farm out to an existing shell, they're explicitly invoking "sh". There would be no reason to do that if Command always farmed out to another shell.


If anyone is interested in developing a shell even further, checkout rustyline[0]. It makes it very simple to add keyboard shortcuts (Ctrl-C), completion, and even history. There's also liner[1], which is part of the redox-os project, which also has a lot of the same features.

[0]: https://github.com/kkawakam/rustyline [1]: https://github.com/redox-os/liner


I'd second the recommendation for rustyline. I played around with it a bit while reviewing the rush[0] project, another shell implemented in Rust for learning purposes.

[0]: https://github.com/psinghal20/rush


I second this greatly! Too many people roll their own line editors and always mess up the readline (emacs) shortcuts.


I rolled my own and didn't even bother with emacs shortcuts (got vim ones though, as that's what I'm familiar with).

However I'm not adverse adding emacs shortcuts if anyone can recommend a good guide for which keys do what.


Bash shortcut guides get most of the useful ones. https://github.com/fliptheweb/bash-shortcuts-cheat-sheet


Useful link (thank you) but to be honest I was after a more complete list because nothing breaks usability more than something being familiar but not behaving quite the same exactly. But I'll stop being a lazy arse and Duck Duck Go the answer myself :)


ctrl+c (as well as ctrl+\ and ctrl+z) is a little different from other shortcuts because they're captured by your TTY rather than your running application - though you can catch the interrupt that ctrl+c et al sends.

Lesser known fact: you can also redefine which keys you want to raise SIGINT, SIGQUIT and SIGTSTP with.


An example of a more fully featured shell written in rust is Ion. https://gitlab.redox-os.org/redox-os/ion

It is inspired by fish


I'm currently writing a shell in Rust, and have written a post about it. If you find this interesting you may also find my post interesting.

https://nixpulvis.com/ramblings/2018-10-15-building-a-shell-...


Thanks for sharing, your posts are very well written.

I really like the idea of syntax highlighting before the user presses enter.

Any reason you are writing your own posix parser and readline implementation, rather than using existing Rust crates?


Partially to learn how, partially to maintain a consistent style, and partially to see where we can improve.

As for the POSIX parser specifically, I love grammars, and want to support multiple, and building one in LALRPOP is t too bad. Also I looked at conch-parser, but didn't want to use its AST, and decided to go this route.

I have been using the termion crate, and may have another PR to contribute. I have somewhat ambitious goals for the readline like library though.


As I was thinking about an improved user interface for the shell, it seemed like at some point you'd hit the limit of what is reasonable given the current split implementation between terminal emulator and shell. Do you think you'll get to a point where you'll need to build an all-in-one solution?


It's interesting you mention that. I'm (slightly) involved with the Alacritty project, and generally like the layer of abstraction here. If not only to support the existing status quo, but also as it leaves a lot of the platform specific features for the terminal.

ANSI, and the other standards are large and somewhat unwieldy though, and there are times it would be nice to do away with it all.

As for features, there's a number of things that you could argue for being implemented in either place. One feature I feel is a good fit for the shell for example is saving and being able to recall and/or pipe previous commands, while copy paste might be implemented in both.


Any other recommended article in making your own shell in other language?



[flagged]


Having written a shell in C, shells require a lot of string and buffer manipulation, which I prefer doing in rust for both security and ergonomic reasons.


Security in a shell, what for exactly?


So I can tab-complete files inside a tarball without getting pwned by a malicious filename. So my prompt can show when I'm in a git directory without giving RCE to every script kiddie on the internet. So I can actually read scripts before running them instead of giving up because even the cleanest, best-written sh is by necessity full of underhanded hacks.

Shells are combination development environments, programming languages with primitives and standard libraries, and UIs, and as such need to be exactly as security-minded as any other standard library, IDE, or file explorer.


Read scripts before running them, so you can't do that in BaSH? I do acknowledge the first point you made about a malicious filename though, there should be a safeguard against that, although in all honesty if someone is planting malicious files in your system that you've got bigger issues.


Because shells generally provide you with almost unrestricted access to the system, so it’s pretty important that they are secure?


If you already have access to the shell of a system would you not say that you have already defeated many of the security points of the system you are breaking into?


A shell does not grant you unrestricted access to a system, that's absurd, once an attacker gains access to a shell then they still have gain root.


Because sometimes people write privileged shell scripts, and they should not be vulnerable.


Are you referring to the BaSH vulnerability that has to do with how SHELLOPTS and PS4 environment variables were processed for executables that had setuid? That was an issue, SETUID should not be set for certain programs in the first place, anyway there's a patch for that. How does writing a shell in Rust or any other language for that matter guard against this?


Not segfaulting everywhere?


Definitely.


Because the world already has enough of them, and we need alternatives written in memory safe languages.


The recent C++ revisions are modern (kindof). Coming from C (and expecting a C with classes like stuff like in the 90s) C++ looks totally alien. Can't say its a mess because haven't used it much, just a toy project here and there..


Actually quite a few of modern C++ best practices, were already possible with classical 90's C++.

Namely RAII, type safe strings, vectors, collections, less implicit conversions, reference parameters.

For me, Turbo Vision, Object Windows Library and Visual Components Library were good examples of C++ frameworks making good use of such features.

MFC always was seemed too thin in high level features.

The problem with C++, is that its copy-paste compatibility with C, crucial to win the adoption from C compiler developers, nowadays has become its Achilles heel regarding safety.


[flagged]


Nice way of turning my words around.

The world has enough of shells written in C!


The world has enough shells, seriously, can we stop rebuilding things that work great already.


Being written in an unsafe language open by default to all kinds of security exploits is the antithesis of working great.


How many vulnerabilities does BaSH have? 14, and that's since 1999: https://www.cvedetails.com/vulnerability-list/vendor_id-72/p...

There is no "safe" language, what are you talking about?


It is called Bash, and that is just one example.

Besides any value greater than zero is already too much.

Sure there are plenty of memory safe languages, with bounds checking on vector and string data structures, proper length handling without missing null terminators, pointer null validation on use, where UB is not 200+ entries on the language standard and increasing...


Why not just write it in Rust?


[flagged]


[flagged]


First of all, because it doesn't make any point.

"I am surprised that stuff like this would make it to HN front page. I have checked out the link , it is indeed great but it is not what I should expect to land here."

Why? Is it too simple for HN? Too different from what's posted here? You don't clarify, and make no real contribution to the discussion.

(Besides, you, yourself, call it "great". So why shouldn't it be here?)

Furthermore, the surprise is misplaced. This kind of post features regularly on HN, be it for Rust, Go, etc. There have been similar posts on building a shell, a basic compiler/interpreter, a simple boot loader, and tons of other things besides.

Last, but not least, your last sentence looks like a jab/snark against Rust or against HN-ers voting based on trends ("My take is that Rust is hot nowadays").

Rust is indeed hot, but you can find similar posts all the time on HN for all kinds of languages.


From the HN Guidelines:

  Please don't complain that a submission is inappropriate. If a story is
  spam or off-topic, flag it. Don't feed egregious comments by replying;
  flag them instead. If you flag something, please don't also comment
  that you did.
https://news.ycombinator.com/newsguidelines.html


Probably because your comment did Not contribute to the discussion.


Ok




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: