Missing Semester Notes - Command-line Environment

2020-05-20

继续介绍提升shell下工作效率的方法。我们之前都集中于如何执行各种命令,这节课我们将看到如何同时运行多个进程并跟踪他们的状态。我们也会学几个命令和方法,通过别名和配置文件的形式。这些都能帮助你节省时间。比如在你的所有机器上部署一样的配置文件而避免冗长的命令。你将看到怎么通过SSH使用远程机器。

原文链接:https://missing.csail.mit.edu/2020/command-line/

任务控制

在一些情况下你需要打断正在执行的任务,比如你跑了一个命令之后发现一时半会儿跑不完(find发现不知道还要等多久才能结束)。这时候你可能会直接C-c掐掉任务了。这是怎么做到的,而为何有时候任务掐不掉呢?

杀掉一个进程

你的Shell正在用一种叫“信号”的UNIX通信机制域进程交换信息。当进程收到一个信号后它会停止它的执行,处理信号并根据其做出相应改变。因为这个原因,信号是一种软中断。

在上述例子中,当按下C-c时,命令行会送一个SIGINT信号给进程。

这里有个最简单的用Python写的例子来捕获且忽略SIGINT信号。要终止这个进程需要通过C-\发送SIGQUIT信号。

#!/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

如下是两次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

虽然SIGINTSIGQUIT都是有关命令请求的,而一个更加普遍的要求进程退出的是SIGTERM信号。为了发送这个信号我们可以使用kill命令,格式是`kill -TERM ```

暂停与转入后台进程

信号能做的事情不只是杀掉进程。比如,SIGSTOP能暂停的一个进程。在命令行中,C-z将会使Shell发送一个SIGTSTP信号,short for Terminal Stop(也就是命令行版本的SIGSTOP)。

我们可以继续在前台或后台执行暂停的任务,使用fg或者bg

jobs命令可以列举出与当前命令行有关的所有未完成的任务。你可以使用他们的pid引用它们(或是用pgrep找出它们)。更直观的,你可以用百分号后跟任务号(jobs中会显示)来引用任务。引用最后一次的后台任务可以使用特殊参数$!

另一件需要知道的事就是&后缀命令可以将整条命令运行在后台中(虽然它还是会继续用你当前的STDOUT,有点烦人)。

对于一个已经在执行的任务,C-z然后bg可以把它扔到后台去执行。需要注意,这样的后台进程仍然是你Shell的子进程,如果你关了Shell还是会被关掉(这回发出另一个信号SIGHUP)。为了避免这个事情的发生,你可以使用nohup(一个忽略SIGHUP的包装)跑你的命令,或者用disown在进程已经在运行的情况下。你也可以用下一节介绍的终端复用器来作为替代。

下面是这些命令的例子

$ sleep 1000
^Z
[1]  + 18653 suspended  sleep 1000

$ nohup sleep 2000 &
[2] 18745
appending output to nohup.out

$ jobs
[1]  + suspended  sleep 1000
[2]  - running    nohup sleep 2000

$ bg %1
[1]  - 18653 continued  sleep 1000

$ jobs
[1]  - running    sleep 1000
[2]  + running    nohup sleep 2000

$ kill -STOP %1
[1]  + 18653 suspended (signal)  sleep 1000

$ jobs
[1]  + suspended (signal)  sleep 1000
[2]  - running    nohup sleep 2000

$ kill -SIGHUP %1
[1]  + 18653 hangup     sleep 1000

$ jobs
[2]  + running    nohup sleep 2000

$ kill -SIGHUP %2

$ jobs
[2]  + running    nohup sleep 2000

$ kill %2
[2]  + 18745 terminated  nohup sleep 2000

$ jobs

SIGKILL是特别的信号,因为它不能被进程捕获且总是直接立即终止进程。它会产生比较严重的副作用比如产生孤儿进程。

你可以在这篇文章中详细了解信号,或者man signal或者kill -t

终端复用器

当你在用命令行界面的时候总是想同时做不止一件事。比如你可能想在用编辑器的同时跑程序。虽然你可以开多个窗口,但用复用器会更时尚。

例如tmux允许你在使用网格和分页在一个窗口中复用多个终端,这样你可以与多个Shell会话交互。进一步,多路复用器可以让你的命令行会话与窗口脱钩,并在之后的某个需要用的时刻重新附加到窗口上。这点在你使用远程终端的时候不需要用nohup与类似的东西。

最流行的复用器是tmux,这是一个高度可配置且支持大量快捷键的复用器。它的热键是C-b + x,先用Ctrl+b进入命令等待,再键入命令。

tmux对象有如下的层次:

  • 会话 - 一个有着一个或多个窗口的独立工作区
    • tmux 开启一个新的会话
    • tmux new -s NAME 开启一个名为NAME的新会话
    • tmux ls 列出当前会话
    • 在会话中<C-b> d可以脱钩当前会话
    • tmux a 附加到上一次会话中,可以用-t指定会话
  • 窗口 - 等价于浏览器中的分页,他们是同一会话中视觉上的分离部分
    • <C-b> c 创建新的窗口。关闭它只需要<C-d>
    • <C-b> N 去第N号窗口,注意到这里的N是数字
    • <C-b> p 去前一个窗口
    • <C-b> n 去下一个窗口
    • <C-b> , 重命名当前窗口
    • <C-b> w 列出现有的窗口
  • 格子 - 例如Vim中的分页,让你在同一个显示器页面中显示多个shell
    • <C-b> " 水平分割当前格子
    • <C-b> % 垂直分割当前格子
    • <C-b> <direction> 按方向移动到下一个格子
    • <C-b> z 对当前格子进行放缩
    • <C-b> [ 开始回滚。按空格开始选择,按回车复制选择
    • <C-b> <space> 循环切换格子

更多的命令看这个快速指南,这个有更多细节包括原始的screen命令。你可以试试熟悉一下screen毕竟在UNIX机器上它更广泛。

别名

打老长的一串命令太无聊了。所以许多Shell支持别名(aliasing)。一个shell的别名是另一个命令的短名称,并会被自动替换。在bash中别名长这样:

alias alias_name="command_to_alias arg1 arg2"

主要到等于号前后都没有空格,因为alias是一个Shell中的单参数的命令

别名有许多方便的特性:

# Make shorthands for common flags
alias ll="ls -lh"

# Save a lot of typing for common commands
alias gs="git status"
alias gc="git commit"
alias v="vim"

# Save you from mistyping
alias sl=ls

# Overwrite existing commands for better defaults
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 can be composed
alias la="ls -A"
alias lla="la -l"

# To ignore an alias run it prepended with \
\ls
# Or disable an alias altogether with unalias
unalias la

# To get an alias definition just call it with alias
alias ll
# Will print ll='ls -lh'

需要注意的是别名默认不会被会话持久化,所以你需要写入文件中,比如.bashrc.zshrc,我们下一节会介绍

Dot文件

许多文件的配置都以纯文本形式存储在Dot文件中,因为以.开头的文件会在默认的ls中被隐藏,例如.vimrc

Shell也是这样的,在启动的时候,你的Shell将会从许多文件中读取它的配置。不同的Shell在登录或者交互时都是非常复杂的。关于这个问题,这里有一篇非常好的文章。

bash来说,编辑.bashrc.bash_profile在绝大多数系统上都适用。这里你可以包含你想在启动时运行的命令,例如别名或者PATH环境变量。事实上,许多程序会要求你在Shell中用export PATH="$PATH:/path/to/program/bin"这种命令来保证它们的二进制文件能被找到。

其他的一些用Dot文件配置的例子

  • bash - ~/.bashrc, ~/.bash_profile
  • git - ~/.gitconfig
  • vim - ~/.vimrc~/.vim文件夹
  • ssh - ~/.ssh/config
  • tmux - ~/.tmux.conf

你要如何组织你的Dot文件呢?它们应该老老实实呆在自己的文件夹下并处于版本控制管理中,再使用符号链接放到它们该出现的位置。这样的话有如下好处:

  • 方便安装:如果你到新机器上,只需要花几分钟便可以得到原配置
  • 便携:你的工具可以在任何地方都以同样的方式运行
  • 同步性:你可以在任意一处更新你的文件并在所有地方保持同步
  • 跟踪变化:你可能需要在你整个程序员生涯中维护只属于你自己的Dot文件,所以版本历史对于长期项目是非常关键的

怎么写Dot文件?根据不同工具的文档或者man页面。另一种方法就是看看别人blog上的配置文件。你可以在github上找到一吨的Dot文件仓库,最流行的是这个 (只是提醒你不要瞎逼拷别人的配置),这里是个对于这个主题来说非常好的资源。

便携性

一个痛点是Dot文件可能在几个机器之间不能通用。比如不同的操作系统或不同的Shell。也有的时候你只想单独给一台机器配置一些特别的东西。

这有一些技巧。如果配置文件支持的话,用等号或者if条件去写特殊机器的配置。比如你的Shell可能会有如下的东西:

if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi

# Check before using shell-specific features
if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi

# You can also make it machine-specific
if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi

如果配置文件支持的话,可以搞一下includes,例如~/.gitconfig就有如下设置

[include]
    path = ~/.gitconfig_local

然后再每一台机器上~/.gitconfig_local可以包含机器独有的设置。你甚至可以为不同种类的机器开不同的库去跟踪它们。

这样做也方便你让不同的程序共享一份配置。比如你想让bashzsh共享同样的别名,你可以写一个.aliase然后在两边自己的配置文件中都写如下的内容:

# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
    source ~/.aliases
fi

远程机器

现在程序员每天的工作中越来越离不开远程机器了。如果你需要用远程服务器去部署后端或者你需要服务器来进行高性能需求的计算,你将会用到SSH。与大多数工具一样,它是一个高度可配置的东西,所以值得在这里一学。

ssh上别的机器只需要执行类似ssh [email protected]的命令。这条是指以foo的用户名连接bar.mit.edu(当然你也可以用IP)。之后我们可以看到如何使用ssh配置文件使得你只需要执行ssh bar这样的命令。

执行命令

一个经常被忽视的特性是ssh是支持直接运行命令的。比如ssh foobar@server ls将会直接在远程端执行ls。这个特性在管道中也适用,所以ssh foobar@server ls | grep PATTERN将会把远程端ls的结果送进本地的grep,而ls | ssh foobar@server grep PATTERN将会把本地ls出来的结果送到远程机器上去grep

SSH 密钥

基于密钥的认证将会利用基于公钥的密码学系统在客户端和服务端进行认证而不用暴露私钥。这就是为什么你可以不用每次都手动输入密码。然而,你的私钥(一般在~/.ssh/id_rsa,最近是~/.ssh/id_ed25519)的效用等价于你的密码,所以,你懂的。

密钥生成

为了生成密钥对你可以用ssh-keygen

ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519

你需要选择一个密码短语来避免有人把你的私钥偷走了。用ssh-agentgpg-agent来避免你每次都要输入密码短语。

如果你的密钥已经用来推github库了,你需要参考下这里。为了验证你的密码短语,你可以跑一下ssh-keygen -y -f /path/to/key

基于密钥的认证

ssh会看.ssh/authorized_keys去判断哪个客户端的连接可以放进来。把你的公钥拷出去可以用

cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'

一种简单的解决方案是

ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote

用SSH拷贝文件

有许多方法可以通过SSH拷贝文件

  • ssh+tee,最简单的就是用STDIN和管道来传输文件cat localfile | ssh remote_server tee serverfiletee是一个把STDIN输出到文件的东西
  • scp当要拷贝文件夹或者大量文件的时候,scp是一种安全又便捷的方法,它能够很简单的传输你的文件夹。语法是scp path/to/local_file remote_host:path/to/remote_file
  • rsync是一种scp的升级,可以探测到本地与远程文件中的异同避免重复拷贝。它对符号链接、权限有着更细粒度控制,例如带--partial标志的文件 可以从之前中断的传输中回复。rsync有着与scp非常相似的语法。

端口转发

在许多场景下,你需要跑软件并监听特定的端口。当在本机跑这种软件的时候,直接输入localhost:PORT或者127.0.0.1:PORT,但如果你在远程机器上跑这种软件的时候要怎么办呢?

这里就要用到端口转发的特性了。有两种转发,一种是本地转发,一种是远程转发(也就是正向和反向都支持,原文有图,这里不赘述),这里有篇文章介绍这个。

本地转发最长用的场景就是,你在远程机器上跑了一个服务,你想通过访问本地端口来访问它。如例如,你跑了个jupyter notebook在远程机器上并监听了8888端口,然后你希望通过访问本地9999端口来访问这个远程端口。那么命令就是ssh -L 9999:localhost:8888 foobar@remote_server然后使用localhost:9999来访问就完事了。

SSH配置文件

以上看来连接一个服务器可能要打老长一串命令。所以创建别名是非常有必要的。

alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@remote_server

然而,有一个更好的替代就是直接使用SSH配置文件

Host vm
    User foobar
    HostName 172.16.174.141
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888

# Configs can also take wildcards
Host *.mit.edu
    User foobaz

配置文件的另一个优势是通过~/.ssh/config文件而不是别名,所以这让其他的程序例如scp,rsync,mosh等也可以读取这个配置然后转换成自己的配置。

注意到这个文件~/.ssh/config可以被认为是一个Dot文件,一般来说也可以和你自己的Dot文件库放在一起。然而,如果你的Dot文件库是公开的,你的ip和配置啥的就漏了,怕不是要被人D出屎来。

SSH服务器的配置文件在/etc/ssh/sshd_config,你可以在这里搞些事情,比如关掉密码登录(只留下key登录),改变ssh端口,打开X11转发等等。你可以对每个用户做出不同的配置。

一个小点

一个常见的痛点时当你连上一个远程机器之后,你会因为关机或者睡眠什么的掉线。另外,如果SSH连接有很大的延迟的话也是贼tm烦人的。Mosh是一个移动端的shell,允许漫游连接,并提供智能的本地输出。

Shells 与框架

bash实在是用的太广泛的,而且它是大部分系统的默认框架。然而它不是唯一的选择。例如zsh就是bash的一个超集并提供了很多开箱即用的特性

  • 更加聪明的文件名代换,**
  • 内联匹配/通配符展开
  • 拼写校正
  • 更好的tab补全与选择
  • 路径展开(cd /u/lo/b会被展开成cd /usr/local/bin

框架(Frameworks)可以提升你的Shell使用体验。一些流行的框架比如preztooh-my-zsh,还有一些专门针对特定功能的比如zsh-syntax-highlightingzsh-history-substring-search。例如fish这样的Shell就默认包含了很多用户友好的特性。比如说:

  • 正确的提示
  • 命令行语法高亮
  • 历史命令字串搜索
  • 基于man页面的flag补全
  • 更智能的自动命令补全
  • 提升了主题

不过用这些框架可能会让你的Shell变慢一些,尤其是在它们没有被优化好的时候。你可以把你不需要的特性关掉来提速。

终端模拟器

你值得花点时间来研究就要配置出什么命令行,毕竟你要天天用。有许多模拟器能帮你。

有如下这些地方你值得花时间配置好:

  • 字体
  • 配色
  • 键盘快捷键
  • 分页支持
  • 滚屏配置
  • 性能(有些命令行支持GPU加速)
Missing Semesternotesshell

Missing Semester Notes - Version Control (git)

Missing Semester Notes - Data Wrangling