技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

深度技术分析:build-your-own-shell

项目背景与痛点

在编程世界中,Shell 经常被视为一个工具箱,是命令行接口中进行各种操作的执行者。然而,对于开发者来说,习得一门新的 Shell 语言不仅需要时间,也常常受限于社区和文档的支持。build-your-own-shell 正是为此而生——它不仅是一个实践项目,也是一种模拟真实世界中构建复杂系统的体验,为开发者提供了一面镜子,让他们能够深入了解底层工作原理。

随着大家对软件研发过程中的透明度和可控制性需求逐渐提高,自己构建 Shell 提供了一个绝佳的机会来增强技术能力和自主权。

核心技术揭秘

build-your-own-shell 的核心在于模拟一个基本的 shell 界面,实现其核心功能,如解析命令行输入、执行外部程序以及处理输入输出重定向。下面我们从技术栈与架构层面深入分析它的工作原理。

技术架构

项目采用了 C 语言来构建,这是一种低级编程语言,可以为您提供直接的硬件控制,使得性能优化和系统级编程变得可能。从架构角度来看,build-your-own-shell 主要分为解析器、命令执行器和输入输出管理器三大模块,如下图所示:

graph TD A[用户输入] --> B{"解析命令"} B -->|"有效命令"| C[执行外部程序] C --> D{"处理 I/O 转向"} D --> E[显示输出] D -->|"错误"| F[返回错误] B -->|"无效命令"| G[显示错误]

关键技术

  • 解析命令:使用简单的字符串处理技术,将输入分解成多个单词,并对每一个单词进行处理。如果是内置命令如 cd、pwd、echo,则在当前 shell 中执行,否则将为外部程序重新分配一个子进程。

  • 执行外部程序:通过 execve 系统调用来执行外部已编译的程序。如果执行成功,则会运行相应的程序,从而显示返回值或结果。

  • 输入输出重定向:

    • 使用文件描述符重定向标准输入/输出。
    • 对于输入重定向,< 符号用于将一个文件的内测读取作为程序的标准输入。
    • 对于输出重定向,> 和 >> 符号分别用于覆盖和附加文件输出到指定文件。

功能亮点与差异

相比于其他开源 Shell,build-your-own-shell 项目虽然只是提供了一个最基本的外包装,但其开放性和模块化设计却使其极具可扩展性。更重要的是,这样的项目非常适合学习和调试,也便于添加自己想要的功能,如增加对特殊命令的支持或者优化性能处理。

应用场景与落地建议

在生产环境中使用build-your-own-shell,则需要考虑以下几个方面:

  • 安全性和稳定性:自定义 Shell 存在配置错误的风险,确保系统的合理防护。
  • 兼容性:考虑不同环境下的兼容性问题。除了基本功能外,应考虑一些高级的 shell 特性,如环境变量扩容或命令行编辑器等。
  • 性能优化:特别关注解析器性能和子进程管理,保证执行效率。

该项目特别适合用于教学练习、底层系统实现研究或者定制化开发场景,尤其是当你想要研究和重新关注操作系统的底层工作原理。

综合评价

build-your-own-shell 作为一款用于学习和实践的项目,其开放性和高度可扩展性为其主要亮点。它为技术爱好者提供了一个强大的学习平台,可以深入研究 Shell 的工作机制和底层编程技术。同时,虽然它提供了无限的学习空间,但对于没有任何编程基础的人来说,这无疑是一项挑战。

优点:

  • 开放化源代码结构,可扩展性强。
  • 易于添加新功能和优化。
  • 切实学习 Shell 设计原理。

缺点:

  • 对于初学者来说,解析与执行一系列复杂命令时可能具有一定的难度。
  • 性能上较高级的 Shell 工具会有一定差距。

简要分析

热度分
599
价值分
324
活跃状态
活跃

语言占比

C
Perl
Raku
Shell
Tcl
Terra

README

All the broken
Too many broken
Shells
In our shellmounds
— Grayceon, Shellmounds

Build Your Own Shell

This is the material for a series of workshops I ran at my workplace on how to write a Unix shell.

The focus is slightly more on building an interactive shell than a scripting-oriented shell, only because I think this is more gratifying, even if it’s less useful.

Be warned that some of the suggestions and discussion make opinionated choices without discussing equally-valid alternatives.

This is a work in progress and there may remain many infelicities. Patches Thoughtfully Considered. Feel free to report issues via Github.

Why write your own shell?

The shell is at the heart of Unix. It’s the glue that makes all the little Unix tools work together so well. Understanding it sheds light on many of Unix’s important ideas, and writing our own is the best path to that understanding.

This workshop has three goals:

  • to give you a better understanding of how Unix processes work;
    • this will make you better at designing and understanding software that runs on Unix;
  • to clarify some common misunderstandings of POSIX shells;
    • this will make you more effective at using and scripting ubiquitous shells like bash;
  • to help you build a working implementation of a shell you can be excited about working on.
    • there are endless personal customizations you can make to your own shell, and can help you think about how you interact with your computer and how it might be different.

(some of this rationale is expanded on in my blog post, Building shells with a grain of salt)

How to use this repository

I’ve tried to break this up into progressive stages that cover mostly orthogonal topics. Each stage contains a description of the facilities that will be discussed, a list of manpages to consult, and a set of tests. I’ve tried to also hint at some functionality that is fun but not necessary for the tests to pass.

In the root of this repository, there is a script called validate; you can run all the tests against your shell-in-progress by specifying the path to your shell’s executable, like this:

$ ./validate ../mysh/mysh

It should tell you what stage you need to implement next. You can also run a stage by itself, or an individual test:

$ ./validate ../mysh/mysh stage_2
$ ./validate ../mysh/mysh stage_3 03

To run the tests, you will need expect, which is usually in a package called expect, and a C compiler. The way the tests are implemented is less robust than one might hope, but should suffice for our pedagogical goals. They are unfortunately somewhat timing sensitive, such that some tests will be flaky. If you encounter a specifically flaky test, please let me know.

The tests assume you will be implementing a vanilla Bourne-flavored shell with some ksh influences. Feel free to experiment with alternate syntax, but if so, you may need to adjust the tests. Except where specifically noted, bash (and ksh) should pass all the tests, so you can “test the tests” that way. (Try ./validate /bin/bash.) Likewise, cat should fail all the tests.

Originally, I targeted plain /bin/sh, but I decided the material in stage 5 was too important. Still, dash will pass everything but stage 5. There are also some other minor compatibility differences with some existing shells; you may run into them if you try them out. Any failure (of a supposedly POSIX shell) that isn’t documented in the comments of a test should be reported as a bug.

Stages

1: fork/exec/wait

In which we discuss the basics of Unix processes, write the simplest possible shell, and then lay the foundations for the rest of the steps.

2: files and pipes

In which we add pipes and fd redirection to our shell.

3: job control and signals

In which we discuss signals and add support for ever-helpful chords like ^C, ^\, and ^Z.

4: quoting and expansion

In which we discuss environments, variables, globbing, and other oft-misunderstood concepts of the shell.

5: interactivity

In which we apply some polish to our shell to make it usable for interactive work.

&: where to go next

In which I prompt you to go further.

Shells written from this workshop

I’ll link to some of the shells that were written as a result of this workshop here shortly, including a couple I wrote to serve as examples of different approaches.

Supplementary Material

Tools

  • The [shtepper] is a great resource for understanding shell execution. You can input an expression and see in excruciating detail how it should be evaluated.

shtepper

Documents

Other Tutorials

I wrote this workshop partially because I felt other tutorials don’t go far enough, but all of these are worth reading, especially if you’re having trouble with a stage they cover:

References

Shells to Examine

  • busybox: C; contains both ash and hush, and test suites.
  • mksh: C; non-interactive tests.
  • rc: C; fairly minimal.
  • zsh: C; extremely maximal.
  • bash: C.
  • fish: C++11; has expect-based interactive tests.
  • Thompson shell: C; the original Unix shell; very minimal.
  • scsh: Scheme and C; intended for scripting.
  • cash: OCaml; based on scsh.
  • eshell: Emacs Lisp.
  • oil: Python and C++; has an extensive test suite.
  • xonsh: Python.
  • oh: Go.
  • yash-rs: Rust.

Although there is an elegant relationship between C and Unix which makes it attractive to write a shell in the former, to minimize frustration I suggest trying a higher-level language first. Ideally the language will have good support for:

  • making POSIX syscalls
  • string manipulation
  • hash tables

Languages that provide a lot of their own infrastructure with regards signals or threads may be much more difficult to use.

C++

http://basepath.com/aup/ex/group__Ux.html

Common Lisp

The most convenient library would be iolib, which you can get through Quicklisp. You’ll need to install libfixposix first. There’s also sb-posix in sbcl for the daring.

Haskell

Java / JVM-based languages

You will probably run into issues related to the JVM, particularly with signals and forking, but as a starting point, you could do worse than loading libc with JNA.

There’s also jtux.

Lua

There are a variety of approaches, but ljsyscall looks promising. luaposix might be sufficient.

OCaml

See also Unix system programming in OCaml, cash.

perl

See perlfunc(3perl); all the functions we want are at hand, usually with the same name.

Python

Although Python provides higher-level abstractions like subprocess, for the purposes of this workshop you probably want to use the functions in os.

Please note an important gotcha for stage 2! Since Python 3.4, fds have defaulted to non-inheritable, which means you’ll need to explicitly os.set_inheritable(fd, True) any file descriptor you intend to pass down to a child.

Racket

The implementation seems a little too heavy to do this conveniently, but see the Scheme section below for alternatives.

Ruby

Process has most of what you need. You can use Shellwords but you decide if it’s cheating or not.

Rust

Although we use few enough calls that you could just create bindings directly, either to libc with the FFI or by directly making syscalls, for just getting something working, the nix-rust library should provide all the necessary facilities.

Scheme

Guile already has all the calls you need; see the POSIX section of the Guile manual. Another approach would be to use something like Chibi Scheme with bindings to libc calls.

Tcl

Although core Tcl doesn’t provide what’s necessary, expect probably does. For example, Tcl doesn’t have a way to exec, but expect provides overlay to do this.

评论

暂无评论