Command-line Environment(命令行环境)
一、Job Control(任务控制)
"sleep":它是一个带有参数的命令,这个参数是一个整数,执行这个命令后会休眠,它会在后台睡眠这么多秒。如果我们执行"sleep 20",该进程将会休眠20秒,但如果我们不想等20秒,我们可以输入"Ctrl + C"。
1.1、结束进程
您的 shell 会使用 UNIX 提供的信号机制执行进程间通信。当一个进程接收到信号时,它会停止执行、处理该信号并基于信号传递的信息来改变其执行。就这一点而言,信号是一种软件中断。
在上面的例子中,当我们输入 Ctrl-C
时,shell 会发送一个SIGINT
信号到进程。
下面这个 Python 程序向您展示了捕获信号SIGINT
并忽略它的基本操作,它并不会让程序停止。为了停止这个程序,我们需要使用SIGQUIT
信号,通过输入Ctrl-\
可以发送该信号。
#!/usr/bin/env python
import signal, time
def handler(signum, time):
print("\nI got a SIGINT, but I am not stopping")
signal.signal(signal.SIGINT, handler)
i = 0
while True:
time.sleep(.1)
print("\r{}".format(i), end="")
i += 1
我们先输入了"Ctrl + C"尝试停止程序,可是并没有作用,因为"SIGINT"被程序截获了,然后我输入了"Ctrl + \"这会发送一个不同的信号"SIGQUIT",而这个信号并没有被程序截获。
有一些信号是软件无法捕获的,例如"SIGKILL",如果发送它,它将无论如何都将终止进程的执行。这有时会是有害的,你不应该默认调用它,因为这可能会留下孤儿进程。例如,如果一个进程会启动其他子进程,而你"SIGKILL"它,所有子进程都会继续在那里运行,但它们没有父进程。
如果我们向这个程序发送两次 SIGINT
,然后再发送一次 SIGQUIT
,程序会有什么反应?注意 ^
是我们在终端输入Ctrl
时的表示形式:
$ python sigint.py
24^C
I got a SIGINT, but I am not stopping
26^C
I got a SIGINT, but I am not stopping
30^\[1] 39913 quit python sigint.pyƒ
尽管 SIGINT
和 SIGQUIT
都常常用来发出和终止程序相关的请求。SIGTERM
则是一个更加通用的、也更加优雅地退出信号。为了发出这个信号我们需要使用 kill 命令, 它的语法是: kill -TERM <PID>
。
1.2、暂停和后台执行进程
如果我们启动一个耗时很长的进程,在进程执行中按下"CTRL + Z",我们可以看到终端显示"已暂停",这实际上意味着该进程被发送了一个"SIGSTOP"信号,现在仍在那里,你可以继续执行它,不过只是被暂停了,它依然在后台。
如果我们执行上面的命令,"&" 告诉bash我希望该程序在后台运行,这个程序依然是在运行当中,但不会在提示符旁边显示出来,如果只是运行命令而没有"&"符号,我将无法执行任何操作,除非命令完成或手动终结,否则我将无法使用提示符。
如果我键入"jobs"命令
我会得到一个已暂停的作业,即sleep 1000,和另一个正在运行的作业,即nohup sleep 2000
假设我想继续第一个作业,键入"bg %1","%"后加上我想引用的特定进程,"bg"(background)命令来继续运行后台进程,这相当于向其发送一个继续信号,使其继续运行。
与"bg"对应的还有"fg","fg"用在你想将其恢复到前台并重新连接到标准输出。
如果我想停止这些作业,那么键入"kill"命令,"kill"用于杀死作业,即停止它们。"kill”命令允许你发送任何类型的Unix信号。
假如在这里,我们不完全杀死它,而是发送一个暂停信号,这将再次暂停该进程。
为什么要在远程会话中运行作业之前加上"nohup"?
因为如果我们尝试向第一个作业发送挂起命令,类似于其他信号,第一个作业将会被挂起,这将终止该作业。
但是如果我们向第二个作业发送一个相同的信号,它仍将继续运行,就像"nohup"一样,它将所执行的命令封装起来,忽略任何挂起信号,并使其继续执行。
我们对第二个作业发送"kill -KILL"信号,这会无法阻止它被杀死。
二、终端多路复用
当您在使用命令行时,您通常会希望同时执行多个任务。举例来说,您可以想要同时运行您的编辑器,并在终端的另外一侧执行程序。尽管再打开一个新的终端窗口也能达到目的,使用终端多路复用器则是一种更好的办法。
像 tmux 这类的终端多路复用器可以允许我们基于面板和标签分割出多个终端窗口,这样您便可以同时与多个 shell 会话进行交互。
不仅如此,终端多路复用使我们可以分离当前终端会话并在将来重新连接。
这让您操作远端设备时的工作流大大改善,避免了 nohup
和其他类似技巧的使用。
现在最流行的终端多路器是 tmux。tmux
是一个高度可定制的工具,您可以使用相关快捷键创建多个标签页并在它们间导航。
当我们运行tmux时,它会启动一个会话,虽然我们看不到变化,但是我们可以理解为在另一个shell中,即在我们原本的shell中,我们启动tmux进程,然后tmux启动了一个新的shell。这个tmux进程和原本的shell是分开的。
我们可以用"ls -la"查看当前目录都有什么?
我们还可以运行我们的程序,让它在这里运行,我们使用"Ctrl + b d"来分离会话,使用"tmux a"会重新连接到会话。暂且离开工作的python程序。
tmux
的快捷键需要我们掌握,它们都是类似 <C-b> x
这样的组合,即需要先按下Ctrl+b
,松开后再按下 x
。tmux
中对象的继承结构如下:
- 会话 - 每个会话都是一个独立的工作区,其中包含一个或多个窗口
tmux
开始一个新的会话tmux new -s NAME
以指定名称开始一个新的会话tmux ls
列出当前所有会话- 在
tmux
中输入<C-b> d
,将当前会话分离 tmux a
重新连接最后一个会话。您也可以通过-t
来指定具体的会话
- 窗口 - 相当于编辑器或是浏览器中的标签页,从视觉上将一个会话分割为多个部分
<C-b> c
创建一个新的窗口,使用<C-d>
关闭<C-b> N
跳转到第 N 个窗口,注意每个窗口都是有编号的<C-b> p
切换到前一个窗口<C-b> n
切换到下一个窗口<C-b> ,
重命名当前窗口<C-b> w
列出当前所有窗口
- 面板 - 像 vim 中的分屏一样,面板使我们可以在一个屏幕里显示多个 shell
<C-b> "
水平分割<C-b> %
垂直分割<C-b> <方向>
切换到指定方向的面板,<方向> 指的是键盘上的方向键<C-b> z
切换当前面板的缩放<C-b> [
开始往回卷动屏幕。您可以按下空格键来开始选择,回车键复制选中的部分<C-b> <空格>
在不同的面板排布间切换
扩展阅读: 这里 是一份 tmux
快速入门教程, 而这一篇 文章则更加详细,它包含了 screen
命令。您也许想要掌握 screen 命令,因为在大多数 UNIX 系统中都默认安装有该程序。
三、配置文件(Dotfiles)
3.1、别名
输入一长串包含许多选项的命令会非常麻烦。因此,大多数 shell 都支持设置别名。shell 的别名相当于一个长命令的缩写,shell 会自动将其替换成原本的命令。例如,bash 中的别名语法如下:
alias alias_name="command_to_alias arg1 arg2"
注意, =
两边是没有空格的,因为 alias 是一个 shell 命令,它只接受一个参数。
别名有许多很方便的特性:
# 创建常用命令的缩写
alias ll="ls -lh"
# 能够少输入很多
alias gs="git status"
alias gc="git commit"
alias v="vim"
# 手误打错命令也没关系
alias sl=ls
# 重新定义一些命令行的默认行为
alias mv="mv -i" # -i prompts before overwrite
alias mkdir="mkdir -p" # -p make parent dirs as needed
alias df="df -h" # -h prints human readable format
# 别名可以组合使用
alias la="ls -A"
alias lla="la -l"
# 在忽略某个别名
\ls
# 或者禁用别名
unalias la
# 获取别名的定义
alias ll
# 会打印 ll='ls -lh'
值得注意的是,在默认情况下 shell 并不会保存别名。为了让别名持续生效,您需要将配置放进 shell 的启动文件里,像是.bashrc
或 .zshrc。
很多程序的配置都是通过纯文本格式的被称作点文件的配置文件来完成的(之所以称为点文件,是因为它们的文件名以 .
开头,例如 ~/.vimrc
。也正因为此,它们默认是隐藏文件,ls
并不会显示它们)。
shell 的配置也是通过这类文件完成的。在启动时,您的 shell 程序会读取很多文件以加载其配置项。根据 shell 本身的不同,您从登录开始还是以交互的方式完成这一过程可能会有很大的不同。关于这一话题,这里 有非常好的资源
对于 bash
来说,在大多数系统下,您可以通过编辑 .bashrc
或 .bash_profile
来进行配置。在文件中您可以添加需要在启动时执行的命令,例如上文我们讲到过的别名,或者是您的环境变量。
实际上,很多程序都要求您在 shell 的配置文件中包含一行类似 export PATH="$PATH:/path/to/program/bin"
的命令,这样才能确保这些程序能够被 shell 找到。
还有一些其他的工具也可以通过点文件进行配置:
bash
-~/.bashrc
,~/.bash_profile
git
-~/.gitconfig
vim
-~/.vimrc
和~/.vim
目录ssh
-~/.ssh/config
tmux
-~/.tmux.conf
我们应该如何管理这些配置文件呢,它们应该在它们的文件夹下,并使用版本控制系统进行管理,然后通过脚本将其 符号链接 到需要的地方。这么做有如下好处:
- 安装简单: 如果您登录了一台新的设备,在这台设备上应用您的配置只需要几分钟的时间;
- 可移植性: 您的工具在任何地方都以相同的配置工作
- 同步: 在一处更新配置文件,可以同步到其他所有地方
- 变更追踪: 您可能要在整个程序员生涯中持续维护这些配置文件,而对于长期项目而言,版本历史是非常重要的
配置文件中需要放些什么?您可以通过在线文档和帮助手册了解所使用工具的设置项。另一个方法是在网上搜索有关特定程序的文章,作者们在文章中会分享他们的配置。还有一种方法就是直接浏览其他人的配置文件:您可以在这里找到无数的dotfiles 仓库 —— 其中最受欢迎的那些可以在这里找到(我们建议您不要直接复制别人的配置)。这里 也有一些非常有用的资源。
本课程的老师们也在 GitHub 上开源了他们的配置文件: Anish, Jon, Jose.
在bash中有一些没什么用的提示符,它只会显示bash的名称和版本或者用户名和作用区域,我不想显示它,"PS1"代表一个环境变量,是用于提示符的提示字符串,我们可以修改它使其成为"> "。
我们还可以继续修改此配置,假如想在提示符中显示我们当前工作目录"PS1="\w > "",这就和其他的shell提示符一样了。
3.2、可移植性
配置文件的一个常见的痛点是它可能并不能在多种设备上生效。例如,如果您在不同设备上使用的操作系统或者 shell 是不同的,则配置文件是无法生效的。或者,有时您仅希望特定的配置只在某些设备上生效。
有一些技巧可以轻松达成这些目的。如果配置文件 if 语句,则您可以借助它针对不同的设备编写不同的配置。例如,您的 shell 可以这样做:
if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi
# 使用和 shell 相关的配置时先检查当前 shell 类型
if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi
# 您也可以针对特定的设备进行配置
if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi
如果配置文件支持 include 功能,您也可以多加利用。例如:~/.gitconfig
可以这样编写:
[include]
path = ~/.gitconfig_local
然后我们可以在日常使用的设备上创建配置文件 ~/.gitconfig_local
来包含与该设备相关的特定配置。您甚至应该创建一个单独的代码仓库来管理这些与设备相关的配置。
如果您希望在不同的程序之间共享某些配置,该方法也适用。例如,如果您想要在 bash
和 zsh
中同时启用一些别名,您可以把它们写在 .aliases
里,然后在这两个 shell 里应用:
# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
source ~/.aliases
fi
四、远端设备
对于程序员来说,在他们的日常工作中使用远程服务器已经非常普遍了。如果您需要使用远程服务器来部署后端软件或您需要一些计算能力强大的服务器,您就会用到安全 shell(SSH)。和其他工具一样,SSH 也是可以高度定制的,也值得我们花时间学习它。
通过如下命令,您可以使用 ssh
连接到其他服务器:
ssh foo@bar.mit.edu
这里我们尝试以用户名 foo
登录服务器 bar.mit.edu
。"@"分离了用户名和地址。服务器可以通过 URL 指定(例如bar.mit.edu
),也可以使用 IP 指定(例如foobar@192.168.1.42
)。后面我们会介绍如何修改 ssh 配置文件使我们可以用类似 ssh bar
这样的命令来登录服务器。
4.1、执行命令
ssh
的一个经常被忽视的特性是它可以直接远程执行命令。 ssh foobar@server ls
可以直接在用foobar的命令下执行 ls
命令。 想要配合管道来使用也可以, ssh foobar@server ls | grep PATTERN
会在本地查询远端 ls
的输出而 ls | ssh foobar@server grep PATTERN
会在远端对本地 ls
输出的结果进行查询。
4.2、ssh密钥
基于密钥的验证机制使用了密码学中的公钥,我们只需要向服务器证明客户端持有对应的私钥,而不需要公开其私钥。这样您就可以避免每次登录都输入密码的麻烦了秘密就可以登录。不过,私钥(通常是 ~/.ssh/id_rsa
或者 ~/.ssh/id_ed25519
) 等效于您的密码,所以一定要好好保存它。
4.3、密钥生成
使用 ssh-keygen 命令可以生成一对密钥:
ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519
您可以为密钥设置密码,防止有人持有您的私钥并使用它访问您的服务器。您可以使用 ssh-agent 或 gpg-agent ,这样就不需要每次都输入该密码了。
如果您曾经配置过使用 SSH 密钥推送到 GitHub,那么可能您已经完成了这里 介绍的这些步骤,并且已经有了一个可用的密钥对。要检查您是否持有密码并验证它,您可以运行 ssh-keygen -y -f /path/to/key
.
4.4、基于密钥的认证机制
ssh
会查询 .ssh/authorized_keys
来确认那些用户可以被允许登录。您可以通过下面的命令将一个公钥拷贝到这里:
cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'
如果支持 ssh-copy-id
的话,可以使用下面这种更简单的解决方案:
ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote
4.5、通过 SSH 复制文件
使用 ssh 复制文件有很多方法:
ssh+tee
, 最简单的方法是执行ssh
命令,然后通过这样的方法利用标准输入实现cat localfile | ssh remote_server tee serverfile
。回忆一下,tee 命令会将标准输出写入到一个文件;- scp :当需要拷贝大量的文件或目录时,使用
scp
命令则更加方便,因为它可以方便的遍历相关路径。语法如下:scp path/to/local_file remote_host:path/to/remote_file
;":"分隔它要复制到的路径; - rsync 对
scp
进行了改进,它可以检测本地和远端的文件以防止重复拷贝。它还可以提供一些诸如符号连接、权限管理等精心打磨的功能。甚至还可以基于--partial
标记实现断点续传。rsync
的语法和scp
类似;"-avP"告诉其在可能的情况下保留所有权限,以尝试检查文件是否已经被复制,例如"scp"在复制已经存在的文件时,如果在复制过程中断开连接,"scp"会从头开始复制每个文件,而"rsync"会从停止的地方继续。
4.6、SSH配置
我们已经介绍了很多参数。为它们创建一个别名是个好想法,我们可以这样做:
alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@remote_server
不过,更好的方法是使用 ~/.ssh/config
.
Host vm
User foobar
HostName 172.16.174.141
Port 2222
IdentityFile ~/.ssh/id_ed25519
LocalForward 9999 localhost:8888
# 在配置文件中也可以使用通配符
Host *.mit.edu
User foobaz
这么做的好处是,使用 ~/.ssh/config
文件来创建别名,类似 scp
、rsync
和mosh
的这些命令都可以读取这个配置并将设置转换为对应的命令行选项。
注意,~/.ssh/config
文件也可以被当作配置文件,而且一般情况下也是可以被导入其他配置文件的。不过,如果您将其公开到互联网上,那么其他人都将会看到您的服务器地址、用户名、开放端口等等。这些信息可能会帮助到那些企图攻击您系统的黑客,所以请务必三思。
服务器侧的配置通常放在 /etc/ssh/sshd_config
。您可以在这里配置免密认证、修改 ssh 端口、开启 X11 转发等等。 您也可以为每个用户单独指定配置。
五、Shell & 框架
在 shell 工具和脚本那节课中我们已经介绍了 bash
shell,因为它是目前最通用的 shell,大多数的系统都将其作为默认 shell。但是,它并不是唯一的选项。
例如,zsh
shell 是 bash
的超集并提供了一些方便的功能:
- 智能替换,
**
- 行内替换/通配符扩展
- 拼写纠错
- 更好的 tab 补全和选择
- 路径展开 (
cd /u/lo/b
会被展开为/usr/local/bin
)
框架 也可以改进您的 shell。比较流行的通用框架包括prezto 或 oh-my-zsh。还有一些更精简的框架,它们往往专注于某一个特定功能,例如zsh 语法高亮 或 zsh 历史子串查询。 像 fish 这样的 shell 包含了很多用户友好的功能,其中一些特性包括:
- 向右对齐
- 命令语法高亮
- 历史子串查询
- 基于手册页面的选项补全
- 更智能的自动补全
- 提示符主题
需要注意的是,使用这些框架可能会降低您 shell 的性能,尤其是如果这些框架的代码没有优化或者代码过多。您随时可以测试其性能或禁用某些不常用的功能来实现速度与功能的平衡。
六、终端模拟器
和自定义 shell 一样,花点时间选择适合您的 终端模拟器并进行设置是很有必要的。有许多终端模拟器可供您选择(这里有一些关于它们之间比较的信息)
您会花上很多时间在使用终端上,因此研究一下终端的设置是很有必要的,您可以从下面这些方面来配置您的终端: