第五讲 命令行环境

课程视频地址:https://www.bilibili.com/video/BV1Dy4y1a7BW

课程讲义地址:https://missing-semester-cn.github.io/2020/command-line/

本机学习使用平台:wsl1 + ubuntu20.04

任务控制

我们来看如何结束进程、暂停进程和切换进程。

you can see the difference-between-job-task-and-process here.

最简单的方法——快捷键

  • <ctrl + c>:发送SIGINT信号,中断进程;
  • <ctrl + z>:发送SIGTSTP信号,暂停进程;
  • <ctrl + \>:发送SIGQUIT信号,退出进程;(和SIGINT差不多)

优雅的方法——kill命令

SIGKILL 是一个特殊的信号,它不能被进程捕获并且它会马上结束该进程。不过这样做会有一些副作用,例如留下孤儿进程。

SIGTERM 则是一个更加通用的、也更加优雅地退出信号。

为了发出这个信号我们需要使用 kill 命令, 它的语法是: kill -TERM <PID>或 %{{job ID}}

关于kill的信号选择我们可以通过kill -l来查看。

PID的获取

  • jobs -l来查看job的详细信息(包括PID)

  • pgrep <name>来直接获取进程的PID。

后台进程

  • jobs指令查看进程。jobs 命令会列出当前终端会话中尚未完成的全部任务,可以使用百分号 + 任务编号(jobs 会打印任务编号)来选取该任务。如果要选择最近的一个任务,可以使用 $! 这一特殊参数。

  • 命令中的 & 后缀可以让命令在直接在后台运行,不过它此时还是会使用 shell 的标准输出,这种情况可以使用 shell 重定向处理。

  • 使用 fgbg 命令恢复暂停的工作。它们分别表示在前台继续或在后台继续。

    • fg指令使最近转进后台的 job 转回前台,用fg %{{job ID}}来将特定的 job 转回前台执行。
    • bg指令使最近暂停的 job 转回后台,用bg %{{job ID}}来将特定的 job 转回后台执行。
  • 让已经在运行的进程转到后台运行,您可以键入Ctrl-Z ,然后紧接着再输入bg

注意,后台的进程仍然是您的终端进程的子进程,一旦您关闭终端(会发送另外一个信号SIGHUP),这些后台的进程也会终止。为了防止这种情况发生,您可以使用 nohup (一个用来忽略 SIGHUP 的封装) 来运行程序。针对已经运行的程序,可以使用disown

演示例子

$ sleep 1000
# output: ^Z
# output: [1]  + 18653 suspended  sleep 1000
$ nohup sleep 2000 &
# output: [2] 18745
appending output to nohup.out
$ jobs
# output: [1]  + suspended  sleep 1000
# output: [2]  - running    nohup sleep 2000
$ bg %1
# output: [1]  - 18653 continued  sleep 1000
$ jobs
# output: [1]  - running    sleep 1000
# output: [2]  + running    nohup sleep 2000
$ kill -STOP %1
# output: [1]  + 18653 suspended (signal)  sleep 1000
$ jobs
# output: [1]  + suspended (signal)  sleep 1000
# output: [2]  - running    nohup sleep 2000
$ kill -SIGHUP %1
# output: [1]  + 18653 hangup     sleep 1000
$ jobs
# output: [2]  + running    nohup sleep 2000
$ kill -SIGHUP %2
$ jobs
# output: [2]  + running    nohup sleep 2000
$ kill %2
# output: [2]  + 18745 terminated  nohup sleep 2000
$ jobs

终端多路复用 TMUX

会话 Session

每个会话都是一个独立的工作区,其中包含一个或多个窗口。

  • tmux 开始一个新的会话;
  • tmux new -s NAME 以指定名称开始一个新的会话;
  • tmux ls 列出当前所有会话;
    • tmux 中输入 <C-b> d ,将当前会话分离;
  • tmux a 重新连接最后一个会话。您也可以通过 -t 来指定具体的会话;
  • tmux kill-session -t <name>关闭指定会话 ;

窗口 Window

相当于编辑器或是浏览器中的标签页,从视觉上将一个会话分割为多个部分。

  • <C-b> c 创建一个新的窗口,使用 <C-d>关闭;
  • <C-b> N 跳转到第 N 个窗口,注意每个窗口都是有编号的;
  • <C-b> p 切换到前一个窗口;
  • <C-b> n 切换到下一个窗口;
  • <C-b> , 重命名当前窗口;
  • <C-b> w 列出当前所有窗口;

面板 Pane

像 vim 中的分屏一样,面板使我们可以在一个屏幕里显示多个 shell。

  • <C-b> " 水平分割;
  • <C-b> % 垂直分割;
  • <C-b> <方向> 切换到指定方向的面板,<方向> 指的是键盘上的方向键;
    • 可以在tmux里面的命令模式(按<C-b> : 打开),输入set -g mouse on,就可以启用鼠标点击和滚轮了;
  • <C-b> z 切换当前面板的缩放;
  • <C-b> <空格> 在不同的面板排布间切换;
  • <C-b> [ 开始往回卷动屏幕。您可以按下空格键来开始选择,回车键复制选中的部分(按q退出);
  • <C-b> x 关闭当前的面板/窗口;

别名 Alias

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

配置文件(Dotfiles)

很多程序的配置都是通过纯文本格式的被称作点文件的配置文件来完成的(之所以称为点文件,是因为它们的文件名以 . 开头,例如 ~/.vimrc。也正因为此,它们默认是隐藏文件,ls并不会显示它们)。

shell 的配置也是通过这类文件完成的。在启动时,您的 shell 程序会读取很多文件以加载其配置项。根据 shell 本身的不同,您从登录开始还是以交互的方式完成这一过程可能会有很大的不同。关于这一话题,这里 有非常好的资源。

对于 bash来说,在大多数系统下,您可以通过编辑 .bashrc.bash_profile 来进行配置。在文件中您可以添加需要在启动时执行的命令,例如上文我们讲到过的别名,或者是您的环境变量。

一些其他的工具也可以通过点文件进行配置:

  • bash - ~/.bashrc, ~/.bash_profile
  • git - ~/.gitconfig
  • vim - ~/.vimrc~/.vim 目录
  • ssh - ~/.ssh/config
  • tmux - ~/.tmux.conf

配置文件中需要放些什么?您可以通过在线文档和帮助手册了解所使用工具的设置项。另一个方法是在网上搜索有关特定程序的文章,作者们在文章中会分享他们的配置。还有一种方法就是直接浏览其他人的配置文件:您可以在这里找到无数的dotfiles 仓库 —— 其中最受欢迎的那些可以在这里找到(我们建议您不要直接复制别人的配置)。这里 也有一些非常有用的资源。

本课程的老师们也在 GitHub 上开源了他们的配置文件: Anish, Jon, Jose.

配置文件的一个常见的痛点是它可能并不能在多种设备上生效。例如,如果您在不同设备上使用的操作系统或者 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 来包含与该设备相关的特定配置。您甚至应该创建一个单独的代码仓库来管理这些与设备相关的配置。(相当于对文件进行映射,你不必要对本地的点文件做太多的写入。)

如果您希望在不同的程序之间共享某些配置,该方法也适用。例如,如果您想要在 bashzsh 中同时启用一些别名,您可以把它们写在 .aliases 里,然后在这两个 shell 里应用:

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

一些遇到的麻烦

为什么在WSL ubuntu的环境下,执行笨笨py代码过程中按ctrl+\没有发出SIGQUIT信号?

The Missing Semester – 第五讲 学习笔记-小白菜博客

实际上为:WSL其实收到了SIGQUIT的信号,但没有实现对这个信号的处理,并不是信号没有发出。

可以看这些链接帮助理解:https://github.com/microsoft/WSL/issues/169,https://www.zhihu.com/question/338541555

课后习题

任务控制

  1. 我们可以使用类似 ps aux | grep 这样的命令来获取任务的 pid ,然后您可以基于pid 来结束这些进程。但我们其实有更好的方法来做这件事。在终端中执行 sleep 10000 这个任务。然后用 Ctrl-Z 将其切换到后台并使用 bg来继续允许它。现在,使用 pgrep 来查找 pid 并使用 pkill 结束进程而不需要手动输入pid。(提示:: 使用 -af 标记)。

    -a, --list-full	#List the full command line as well as the process ID.  (pgrep only.)  
    -f, --full		#The pattern is normally only matched against the process name.  When -f is set, the full command line is used. 
    
    # 可以用pkill直接全沙掉
    gfcat030@DESKTOP-KUSC3EH:~$ jobs                                                                         [1]   Running                 sleep 200 &                                                                 [2]-  Running                 sleep 2000 &                                                                [3]+  Running                 sleep 3000 &                                                               gfcat030@DESKTOP-KUSC3EH:~$ pkill sleep                                                                   [1]   Terminated              sleep 200                                                                    [2]-  Terminated              sleep 2000                                                                 [3]+  Terminated              sleep 3000 
    # 这样操作
    gfcat030@DESKTOP-KUSC3EH:~$ sleep 10000
    # press Ctril-Z
    # output: [1]  + 29705 suspended  sleep 10000
    gfcat030@DESKTOP-KUSC3EH:~$ bg %1
    # output: [1]  + 29705 continued  sleep 10000
    gfcat030@DESKTOP-KUSC3EH:~$ pgrep -af "sleep"
    # output: 29705
    gfcat030@DESKTOP-KUSC3EH:~$ pkill -af "sleep"
    # output: [1]  + 29705 terminated  sleep 10000
    
    
  2. 如果您希望某个进程结束后再开始另外一个进程, 应该如何实现呢?在这个练习中,我们使用 sleep 60 & 作为先执行的程序。一种方法是使用 wait 命令。尝试启动这个休眠命令,然后待其结束后再执行 ls 命令。

    但是,如果我们在不同的 bash 会话中进行操作,则上述方法就不起作用了。因为 wait 只能对子进程起作用。之前我们没有提过的一个特性是,kill 命令成功退出时其状态码为 0 ,其他状态则是非0。kill -0 则不会发送信号,但是会在进程不存在时返回一个不为0的状态码。请编写一个 bash 函数 pidwait ,它接受一个 pid 作为输入参数,然后一直等待直到该进程结束。您需要使用 sleep 来避免浪费 CPU 性能。

    # 用wait可以这样做
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ sleep 60 &
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ pgrep "sleep" | wait && ls
    #用脚本就这样
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ cat pidw.sh
    #!usr/bin/env bash                                                                                       dwait() {
    	while kill -0 $1 2>/dev/null  # catch stderr to /dev/null
    	do 
    		sleep 1  
    	done   
    	ls 
    }
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ source pidwait.sh
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ sleep 60 &
    gfcat030@DESKTOP-KUSC3EH:~/missingsem$ pidwait $(pgrep "sleep")
    

终端多路复用

请完成这个 tmux 教程 参考这些步骤来学习如何自定义 tmux

别名

  1. 创建一个 dc 别名,它的功能是当我们错误的将 cd 输入为 dc 时也能正确执行。

    gfcat030@DESKTOP-KUSC3EH:~$ alias dc=cd 
    
  2. 执行 history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 来获取您最常用的十条命令,尝试为它们创建别名。注意:这个命令只在 Bash 中生效,如果您使用 ZSH,使用history 1 替换 history

配置文件

让我们帮助您进一步学习配置文件:

  1. 为您的配置文件新建一个文件夹,并设置好版本控制
  2. 在其中添加至少一个配置文件,比如说您的 shell,在其中包含一些自定义设置(可以从设置 $PS1 开始)。
  3. 建立一种在新设备进行快速安装配置的方法(无需手动操作)。最简单的方法是写一个 shell 脚本对每个文件使用 ln -s,也可以使用专用工具
  4. 在新的虚拟机上测试该安装脚本。
  5. 将您现有的所有配置文件移动到项目仓库里。
  6. 将项目发布到GitHub。