浅谈 Shell 介绍和使用

发布于 2023-05-05 21:15:12 字数 17753 浏览 75 评论 0

配图源自 Freepik

一、前言

通常,用户控制计算机的方式有图形化界面(GUI,Graphical User Interface)和命令行界面(CLI,Command Line Interface)两种。然而,真正能够控制计算机硬件(如 CPU、内存、显示器等)的只有操作系统内核(Kernel)。因此,图形化界面和命令行只是架设在用户和内核之间的一种桥梁。

由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没必要),因此需要开发一个程序。用户直接使用这个程序,程序的作用就是接收用户的操作(输入),进行简单处理,然后传递给内核,这样用户就可以间接使用操作系统了。Shell 就是这样的一个程序。在用户和内核之间增加一层「代理」,既能简化操作,又能保证内核的安全,何乐而不为呢?

因此,Shell 并不是操作系统内核的一部分。

以上内容部分摘自 Shell 是什么

二、Shell 的含义

Shell 的英文原意是「外壳」,与之相对的是内核(Kernel)。但具体来说,Shell 这个词有多种含义。

  1. Shell 是一个应用程序,提供一个与用户对话的环境,该环境只有一个命令提示符(通常是 $#),让用户输入命令。这种环境被称为命令行环境(CLI)。Shell 接收到用户输入的命令,将命令传递给操作系统执行,并将结果返回给用户。

  2. Shell 是一个命令解析器,解析用户输入的命令,它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 写出各种小程序,又被称为脚本(Script)。这些脚本通过 Shell 解析器解析执行,注意不是编译。

  3. 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

一些命令:

  1. 查看当前操作系统默认 Shell
$ echo $SHELL
/bin/zsh
  1. 查看当前使用的 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。

  1. 查看当前操作系统的所有的 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
  1. 进入或退出 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.shbash ./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 的环境,比如 aliasPATH 等。

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 变量」两种变量。它们都是区分大小写的,因此 HOMEhome 是两个不同的变量。

  • 环境变量:通常在 Shell 配置文件中以 key=value 形式实现,它在整个系统范围内都可用,并被所有子进程和 Shell 继承。通常以大写形式命名,比如 PATH 等。
  • Shell 变量:专门用于设置或定义它们的 Shell 中的变量,每一种 Shell 解析器都有一组属于自己的内部 Shell 变量。在编写 Shell 脚本时常用于跟踪临时数据。

使用 envprintenv 命令(不带其他参数时),可以显示所有环境变量。若查看单个环境变量的值,可以使用 printenvecho 命令。

$ 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

棒棒糖

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

懂王

文章 0 评论 0

清秋悲枫

文章 0 评论 0

niceone-tech

文章 0 评论 0

小伙你站住

文章 0 评论 0

刘涛

文章 0 评论 0

南街九尾狐

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文