Missing Semester Notes - Command-line Environment
source link: https://www.saltyfish.win/posts/missing-semester-notes-04/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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
虽然SIGINT
与SIGQUIT
都是有关命令请求的,而一个更加普遍的要求进程退出的是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
可以包含机器独有的设置。你甚至可以为不同种类的机器开不同的库去跟踪它们。
这样做也方便你让不同的程序共享一份配置。比如你想让bash
和zsh
共享同样的别名,你可以写一个.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-agent或gpg-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 serverfile
。tee是一个把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使用体验。一些流行的框架比如prezto或oh-my-zsh,还有一些专门针对特定功能的比如zsh-syntax-highlighting或zsh-history-substring-search。例如fish这样的Shell就默认包含了很多用户友好的特性。比如说:
- 正确的提示
- 命令行语法高亮
- 历史命令字串搜索
- 基于man页面的flag补全
- 更智能的自动命令补全
- 提升了主题
不过用这些框架可能会让你的Shell变慢一些,尤其是在它们没有被优化好的时候。你可以把你不需要的特性关掉来提速。
终端模拟器
你值得花点时间来研究就要配置出什么命令行,毕竟你要天天用。有许多模拟器能帮你。
有如下这些地方你值得花时间配置好:
- 键盘快捷键
- 性能(有些命令行支持GPU加速)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK