浅谈 Shell 介绍和使用
一、前言
通常,用户控制计算机的方式有图形化界面(GUI,Graphical User Interface)和命令行界面(CLI,Command Line Interface)两种。然而,真正能够控制计算机硬件(如 CPU、内存、显示器等)的只有操作系统内核(Kernel)。因此,图形化界面和命令行只是架设在用户和内核之间的一种桥梁。
由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没必要),因此需要开发一个程序。用户直接使用这个程序,程序的作用就是接收用户的操作(输入),进行简单处理,然后传递给内核,这样用户就可以间接使用操作系统了。Shell 就是这样的一个程序。在用户和内核之间增加一层「代理」,既能简化操作,又能保证内核的安全,何乐而不为呢?
因此,Shell 并不是操作系统内核的一部分。
以上内容部分摘自 Shell 是什么。
二、Shell 的含义
Shell 的英文原意是「外壳」,与之相对的是内核(Kernel)。但具体来说,Shell 这个词有多种含义。
Shell 是一个应用程序,提供一个与用户对话的环境,该环境只有一个命令提示符(通常是
$
或#
),让用户输入命令。这种环境被称为命令行环境(CLI)。Shell 接收到用户输入的命令,将命令传递给操作系统执行,并将结果返回给用户。Shell 是一个命令解析器,解析用户输入的命令,它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 写出各种小程序,又被称为脚本(Script)。这些脚本通过 Shell 解析器解析执行,注意不是编译。
Shell 是一个工具箱,提供了各种小工具,供用户方便地使用操作系统的功能。
以上摘自 Bash 简介。
三、Shell 的种类
Shell 又很多种,只要能给用户提供命令行环境的程序,都可以看作是 Shell。在历史上,主要 Shell 有这些:
- Bourne Shell(sh)
- Bourne Again shell(bash)
- C Shell(csh)
- TENEX C Shell(tcsh)
- Korn shell(ksh)
- Z Shell(zsh)
- Friendly Interactive Shell(fish)
更详细可看网道和 Wikiwand。目前最常用的 Shell 是 bash。
四、Shell 解析器
各操作系统通常会内置多种 Shell 解析器,且有一个默认的 Shell。比如 Linux 操作系统是 bash,Windows 操作系统是 PowerShell,Mac 操作系统是 bash 或 zsh。
以 macOS 为例,不同版本下其内置默认 Shell 如下:
- OS X 10.2 及以下版本默认 Shell 为 csh。
- OS X 10.3 ~ macOS 10.14 版本默认 Shell 为 bash。
- 自 macOS 10.15 起默认 Shell 为 zsh。
关于如何从 bash 切换至 zsh,或者在不修改默认 Shell 的情况下使用其他 Shell,可参考:在 Mac 上将 zsh 用作默认 Shell。
一些命令:
- 查看当前操作系统默认 Shell
$ echo $SHELL /bin/zsh
- 查看当前使用的 Shell(不一定是默认 Shell)
$ ps PID TTY TIME CMD 13766 ttys000 0:00.25 /bin/zsh -l 14879 ttys000 0:04.45 node /usr/local/Cellar/yarn/1.22.19/libexec/bin/yarn.j 14880 ttys000 0:10.75 gulp start --project qualcomm-202206 14886 ttys000 0:00.06 open -W http://localhost:3000 15495 ttys001 0:00.37 /bin/zsh -l 16701 ttys002 0:00.63 -zsh 18509 ttys003 0:00.26 -zsh
一般来说,ps 命令(显示当前系统的进程状态)结果的「倒数第二行」是当前正在使用的 Shell。
- 查看当前操作系统的所有的 Shell
$ cat /etc/shells # List of acceptable shells for chpass(1). # Ftpd will not allow users to connect who are not using # one of these shells. /bin/bash /bin/csh /bin/dash /bin/ksh /bin/sh /bin/tcsh /bin/zsh
- 进入或退出 Shell
当前我的默认 Shell 是 zsh,如果要进入 bash 环境,执行一些命令,然后再退出。举个例子:
$ bash The default interactive shell is now zsh. To update your account to use zsh, please run `chsh -s /bin/zsh`. For more details, please visit https://support.apple.com/kb/HT208050. bash-3.2$ echo $NVM_DIR /Users/frankie/.nvm bash-3.2$ exit exit
其中 bash
命令表示进入 bash Shell 环境,echo $NVM_DIR
表示输出 $NVM_DIR
环境变量,然后执行 exit
命令退出 bash Shell 环境。
五、Shell 脚本
脚本(Script)是指包含一系列命令的文本文件。利用 Shell 解析器便可执行的脚本,被称为 Shell 脚本。
举个例子,一个最简单的 Shell 脚本 hello.sh
:
#!/bin/bash echo "Hello Shell"
- 脚本首行
#!
是一个约定标记(称为 Shebang 行),用于指定执行该脚本的 Shell 解析器,通常是/bin/bash
或/bin/sh
。#!
与解析器之间的「空格」可有可无。- 文件顶部的 Shebang 行是非必需的。若无,则要在执行脚本时手动指定,比如
/bin/sh ./hello.sh
或bash ./hello.sh
。- 若手动指定解析器,那么将会使用所指定的解析器去执行该脚本,而脚本内指定的解析器会被忽略。
- Shell 脚本文件的扩展名是非必需的,甚至可以命名为奇奇怪怪的扩展名,不影响脚本的执行。但是...按照习惯通常会命名为
.sh
。
但需要注意的是,如果 Shell 解析器并没有放在 /bin
,这样脚本叫无法执行了。为了确保稳定性,可以写成这样:
#!/usr/bin/env bash
原因是 env
命令一定是在 /bin/bin
目录下,它返回对应 Shell 解析器的位置。
如果开发过一些 Node 相关工具(比如 vue-cli),你一定看到过这样的 Shebang 行,用于指定解析器为 Node。
#!/usr/bin/env node
也可以观察一下项目的 node_modules/.bin
目录下的所有可执行文件,它们几乎都是以 #!/usr/bin/env node
开头的,其作用是正确地查找对应解析器所在路径,以避免用户没有安装在默认路径下导致无法正常执行脚本的问题。如果你是写 Python 的话,一定见过 #!/usr/bin/env python
...
如果平常想要看下 Node 安装目录的话,可以使用 which
命令,比如:
$ which node /Users/frankie/.nvm/versions/node/v16.15.0/bin/node
六、NPM 脚本与 Shell
在前端项目中,我们通常会在 package.json
中定义一系列的脚本命令,比如:
{ "scripts": { "test": "jest" } }
当我们在终端输入 npm run test
命令时,就可以执行对应的测试脚本。当我们在项目根目录或者用户根目录等路径下直接执行 jest
命令,抛出错误:
$ jest zsh: command not found: jest
原因是执行命令的当前目录或 PATH
(系统环境变量)目录都不存在一个名为 jest
的可执行文件。那么 npm run jest
内部又偷偷做了哪些工作呢,为什么它能准确找到可执行文件 jest
所在的目录呢?
假设我们有一个 my-app
包,如下:
{ "name": "my-app", "version": "1.0.0", "bin": { "myapp": "./cli.js" } }
其中有一个 bin
字段(详见),表示该包有一个名为 myapp
的可执行文件。在执行 npm install my-app
时,除了将包下载至 node_modules
目录下,还会创建一个 cli.js
文件的软链接(symlink)至 node_modules/.bin
目录,该可执行文件的名称就是对应的键名 myapp
。若以 "bin": "./cli.js"
形式配置,名称则为其包名 my-app
。若全局安装,则会被链接至全局目录。
以 jest 为例,可执行文件 node_modules/.bin/jest
其实是 node_modules/jest/bin/jest.js
的软链接,因此真正被执行的是后者。
如果 package.json
中定义的命令是这样:
{ "test": "./node_modules/.bin/jest" }
或这样:
{ "test": "./node_modules/jest/bin/jest.js" }
都非常容易理解,但观感来说,它又长又臭,还丑。那么 { "test": "jest" }
又是如何找到目标可执行文件的呢?
其内在奥秘在于 npm run
命令。当执行此命令时,会创建一个 Shell 子进程,然后在这个 Shell 中执行指定的脚本命令。换句话说,只要是 Shell 可以运行的命令,都可以写在 NPM 脚本里面。
比较特别的是,npm run
创建的 Shell 进程,会将当前目录的 node_modules/.bin
子目录加入到 PATH
环境变量,在执行结束之后,再将 PATH
环境变量恢复原样。
推荐下阮一峰老师的这篇文章:npm scripts 使用指南。
我们来验证下,假设有这样一个项目,且有一个 test.sh
脚本用于输出 PATH
环境变量:
{ "name": "simple-test", "version": "1.0.0", "scripts": { "test": "source ./test.sh" } }
#!/bin/bash # test.sh echo $PATH
其中 source
命令表示执行一个脚本,也可用其简写形式 .
表示,即 . ./test.sh
。我们也常用此命令来刷新环境变量,比如 source ~/.zshrc
,更多可看文章。
下面为了方便对比,执行前后都输出一下 PATH
环境变量的值:
可以发现,在 npm run
执行期间,PATH
环境变量发生了变化,截图标记部分为临时新增的环境变量,其中包括项目所在路径 simple-test/node_modules/.bin
,执行完之后又变为了最初的模样。
至于为什么 simple-test
逐层往上添加 node_modules/.bin
目录,我猜是跟 Node 逐层查找模块有关系,具体没去细究。
因此,我们甚至可以在 bin
字段去声明一些命令:
{ "name": "simple-test", "version": "1.0.0", "scripts": { "test": "source ./test.sh" }, "bin": { "hello": "./hello.sh" } }
然后安装此包后,就可以在 script
中使用 hello
命令了。
七、Shell 配置文件
由于很多高级编程语言,有着丰富的第三方库,因此可选择的配置文件格式有很多,比如 JSON、XML、YAML 等等。对于 Shell 而言,其配置文件多是 key=value
形式的文本文件,等号两边不应有「空格」。
Shell 配置文件本身就是一种特殊的 Shell 脚本,只是没有用 .sh
扩展名而已。前面提到过 Shell 脚本扩展名是非必需的。当 Shell 被启动时,会执行对应 Shell 的配置文件中的命令,通常是配置当前 Shell 的环境,比如 alias
、PATH
等。
Shell 配置文件可分为「系统级别」和「用户级别」。当 Shell 启动时,会先执行系统级别的配置文件(如果存在的话),然后再执行用户级别的配置文件(如果存在的话),因此用户级别的配置文件优先级更高。另外,系统级别的配置对所有用户生效,而用户级别仅对当前用户生效,可以理解为继承关系。
- 系统级别配置文件一般位于
/etc
目录下,比如/etc/profile
、/etc/bashrc
等。- 用户级别配置文件一般位于
~
(用户目录)下,比如~/.profile
、~/.bashrc
、~/.bash_profile
、~/.zshrc
等,通常是隐藏性文件。
以 macOS 为例,通常修改 Shell 配置都是在用户级别的配置文件上操作,比如 ~/.bash_profile
或 ~/.zshrc
,取决于你在使用哪一种 Shell 解析器。
常见 Shell 是如何读取配置的,附一张图,源自 Shell startup scripts:
图中涉及了几个概念 interactive 和 non-interactive、login 和 non-login,分别表示是否为交互式 Shell、是否为登录式 Shell。它们在读取配置文件上会有所区别,比如,非交互式和非登录式的 zsh 不会读取 ~/.zshrc
配置文件。
以下说明,摘自此处:
Login Shell:是指该 Shell 被启动时用于用户登录。
Non-login Shell:是指用户已登录下启动的那些 Shell,被自动执行的 Shell 也属于非登录式 Shell,它们的执行通常与用户登录无关。
Interactive Shell:是指可以让用户通过键盘进行交互的 Shell。 平常在使用的 CLI 都是交互式 Shell。
Non-interactive Shell:是指被自动执行的脚本, 通常不会请求用户输入,输出也一般会存储在日志文件中。
如果在使用 zsh,可阅读下这篇大而全的配置指引:zsh wiki。
八、环境变量与 Shell 变量
本小节内容大部分摘自文章:设置与查看Linux系统中的环境变量。
在 Linux/Unix 系统中,分为「环境变量」和「Shell 变量」两种变量。它们都是区分大小写的,因此 HOME
和 home
是两个不同的变量。
- 环境变量:通常在 Shell 配置文件中以
key=value
形式实现,它在整个系统范围内都可用,并被所有子进程和 Shell 继承。通常以大写形式命名,比如PATH
等。- Shell 变量:专门用于设置或定义它们的 Shell 中的变量,每一种 Shell 解析器都有一组属于自己的内部 Shell 变量。在编写 Shell 脚本时常用于跟踪临时数据。
使用 env
或 printenv
命令(不带其他参数时),可以显示所有环境变量。若查看单个环境变量的值,可以使用 printenv
或 echo
命令。
$ printenv PATH /Users/frankie/.nvm/versions/node/v16.14.0/bin:/usr/local/sbin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin $ echo $PATH /Users/frankie/.nvm/versions/node/v16.14.0/bin:/usr/local/sbin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
echo
命令中的变量名前需要添加前缀 $
,否则当做输出一个字符串。
使用 set
命令,可以显示所有变量(包括环境变量和自定义变量),以及所有的 Shell 函数。
8.1 环境变量
单个值的环境变量,形式看起来是这样的:
KEY=value1
如果变量的值中含有空格,则需要将值放在引号中,比如:KEY="value with spaces"
。
多个值的环境变量,形式是这样的:
KEY=value1:value2:value3
每个值之间以分号 :
作为分隔符。可以理解为类似于高级编程语言中的数组。
8.2 Shell 变量
Shell 变量声明语法如下:
variable=value
等号左边为「变量名」,右边为变量值,同样地,若变量值含有空格,需要将值放在引号中。需要注意的是,在 Shell 中没有数据类型的概念,所有变量值都是字符串。
Shell 变量除了可以在 Shell 脚本文件中声明、使用,也是可以直接在 Shell 会话中操作的,但是该变量的声明周期仅在会话被销毁之前,而且不能被其他新的 Shell 会话环境中读取。若用浏览器来比喻的话,每个 Shell 会话就是两个标签的应用,应用之间的变量是不可共享的。
比如:
$ myname=frankie $ echo $myname frankie
由于用户创建的 Shell 变量,仅可在当前 Shell 中使用,若要传递给子 Shell,需要使用 export 命令。这样输出的变量,对于子 Shell 来说就是环境变量。但注意其他非子 Shell,是不能读取到的。
$ export myname=frankie $ myage=20 $ bash The default interactive shell is now zsh. To update your account to use zsh, please run `chsh -s /bin/zsh`. For more details, please visit https://support.apple.com/kb/HT208050. bash-3.2$ echo $myname frankie bash-3.2$ echo $myage bash-3.2$
其他就不展开了,不然就跑偏了,有兴趣可看阮一峰老师的文章:Bash 变量。
8.3 常见变量
常见环境变量:
变量名 | 含义 |
---|---|
USER | 当前登录用户。 |
PWD | 当前工作目录。 |
OLDPWD | 上一个工作目录。 |
PATH | 系统查找指令时会检查的目录列表。当用户输入一个指令时,系统将会按此目录列表的顺序检索目录,以寻找相应的可执行文件。 |
LANG | 当前的语言和本地化设置,包括字符编码。 |
HOME | 当前用户的主目录。 |
SHELL | 当前系统的默认 Shell。 |
TERM | 终端类型名,即终端仿真器所用的协议。 |
常见的 Shell 变量:
变量名 | 含义 |
---|---|
COLUMNS | 用于设置绘制到屏幕上的输出信息的宽的列数。 |
HOSTNAME | 主机名。 |
PS1 | 命令提示符样式。 |
PS2 | 输入多行命令时,命令提示符样式。 |
8.4 特殊变量
变量名 | 含义 |
---|---|
$? | 上一个命令的退出码,用于判断上一个命令是否执行成功。返回值为 0 表示命令执行成功,否则执行失败。 |
$$ | 当前 Shell 的进程 ID。 |
$_ | 上一个命令的最后一个参数。 |
$! | 最近一个后台执行的异步命令的进程 ID。 |
$0 | 在 CLI 直接执行时,表示当前 Shell 的名称。在 Shell 脚本执行时,表示当前脚本名称。 |
$@ 、$# | $@ 表示脚本的参数值;$# 表示脚本的参数数量。 |
8.5 变量持久化
若不希望每次启动新的 Shell 会话时,都必须重新设置重要的变量,则需要将变量写入配置文件中。以 macOS 为例,最常见的是配置文件是 ~/.bash_profile
或 ~/.zshrc
,具体取决于你的使用了哪一种 Shell 解析器,请参考前面的第七节内容。
比如我在 ~/.zshrc
中配置 nvm 的目录,可以添加这样一行配置:
export NVM_DIR="$HOME/.nvm"
其中 $HOME
本身就是一个环境变量(表示当前用户主目录),假设我的主目录是 /Users/frankie
,那么变量 NVM_DIR
的值就是 /Users/frankie/.nvm
。
平常配置最多的可能是 PATH 变量,假设安装了一个工具 A,然后需要配置该工具的可执行文件 aaa
的路径(假设为 ~/.aaa/bin
),可以这样:
export PATH=$PATH:$HOME/.aaa/bin
如此 /Users/frankie/.aaa/bin
就会被添加至环境变量 PATH 中,这样我们就可以在任何目录下执行 aaa
命令了。
记得配置完,要使用类似 source ~/.zshrc
的命令刷新变量,使其在已启动的 Shell 进程中生效,否则只在新打开的 Shell 进程中生效。因为每次启动一个新的 Shell 进程都会先读取对应的 Shell 配置文件,然后这个刷新命令本质上就是执行了一个 Shell 脚本而已,这些内容在前一节提到过了。
九、常见目录区分
bin
- 是 binary 的缩写,表示可执行二进制文件,主要用于具体应用。sbin
- 是 system binary 的缩写,表示系统级别的可执行二进制文件,主要用于系统管理。etc
- 源自拉丁语 et cetera,是「等等」的意思,用于存放整个文件系统的配置文件。其命名似乎是历史遗留问题,最初用于存放一些零零碎碎的文件。另一种说法是 Editable Text Configuration。usr
- 是 unix system resources 的缩写,它是系统中最重要的目录之一,涵盖了二进制文件、各种文档、各种头文件、x、各种库文件以及诸多程序。好像以前是存放 user 的目录,因此也认为是 user 的缩写,但现在用户目录多是 home。var
- 是 varible 的简写。目录中保存的是未知增长和其内容频繁变动的文件(因此名为变化的)- 更多请看 Linux 目录概览。
/bin:通常是普通用户和超级用户都会用到的必要的命令,例如ls,pwd等等。
/sbin:通常是系统管理员使用的必要的来管理系统的命令,例如shutdown,ifconfig等等。
/usr/bin:通常是一些非必要的,但是普通用户和超级用户都可能使用到的命令,例如gcc,ldd等等。
/usr/sbin:通常是一些非必要的,由系统管理员来使用的管理系统的命令,例如crond,httpd等等。
/usr/local/bin:通常是用户后来安装的软件,可能被普通用户或超级用户使用。
/usr/local/sbin:通常是用户后来安装的软件,一般是用来管理系统的,被系统管理员使用。
从用户权限角度来看,sbin下的命令都是用来管理系统的,所以一般是普通用户无法执行,只有系统管理员可以执行,而bin下的命令则是所有用户都可以执行的。
注:以上所说的并不是绝对的,例如ifconfig在/sbin下,但是普通用户一般具有可执行权限。
小结
- 如果是用户和管理员必备的二进制文件,就会放在/bin;
- 如果是系统管理员必备,但是一般用户根本不会用到的二进制文件,就会放在/sbin;
- 如果不是用户必备的二进制文件,多半会放在/usr/bin;
- 如果不是系统管理员必备的工具,如网络管理命令,多半会放在/usr/sbin;
其他目录
- 主目录:/root、/home/username
- 用户可执行文件:/bin、/usr/bin、/usr/local/bin
- 系统可执行文件:/sbin、/usr/sbin、/usr/local/sbin
- 其他挂载点:/media、/mnt
- 配置:/etc
- 临时文件:/tmp
- 内核和Bootloader:/boot
- 服务器数据:/var、/srv
- 系统信息:/proc、/sys
- 共享库:/lib、/usr/lib、/usr/local/lib
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论