Shell, Vim和Git的简单知识总结

本文主要是对MIT的课程”The Missing Semester of Your CS Education”中关于Shell,Vim和Git部分的知识进行一个总结。

课程地址:https://missing.csail.mit.edu/

The shell

类似于Python或者Ruby,shell是一个编程环境,它具备变量、条件、循环和函数等。当在shell中执行命令时,实际上是在执行一段shell可以解释的代码。当你要求shell执行某个指令,但是该指令不是shell所了解的编程关键字,那么它会去咨询环境变量$PATH,$PATH包含了shell接到某条指令时,进行程序搜索的路径。which指令则可以查看指令代表具体哪个程序。

1
2
3
4
 echo $PATH                                                        
/opt/homebrew/opt/sqlite/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/opt/openssl@1.1/bin:/usr/local/apache-maven-3.8.1/bin:/usr/local/apache-tomcat-9.0.45/bin
~/p/notes which python3
/usr/bin/python3

bash是被最广泛使用的一种shell,全称”Bourne Again Shell”。

Shell Tools and Scripting

大多数shell都有自己的一套脚本语言,包括变量、控制流和自己的语法等,shell脚本对shell所从事的相关工作进行优化。

Bash语法

bash中为变量赋值的语法是foo=bar$foo访问变量中的值。但是赋值不能写为foo = bar,这样解释器会把foo作为程序调用,并将=bar作为参数。

bash中字符串可通过’’或者””来定义,但是’’定义的字符串为原义字符串,其中包含的变量不会被转义,而””定义的字符串会对包含的变量值进行替换。

1
2
3
4
5
6
7
8
9
~/p/notes foo=bar                                                                     
~/p/notes echo $foo
bar
~/p/notes echo '$foo'
$foo
~/p/notes "$foo"
bar
~/p/notes foo = bar
zsh: command not found: foo

bash也支持函数,并基于参数进行操作,定义函数后,通过source指令使其在shell中被定义。

1
2
3
4
5
6
7
8
~/p/notes cat mcd.sh                                                          
mcd () {
mkdir -p "$1"
cd "$1"
}
~/p/notes source mcd.sh
~/p/notes mcd hello
~/p/notes/hello

bash使用很多特殊变量来表示参数、错误代码和相关变量。

  • $0 脚本名

  • $1$9 所有参数,$1是第一个参数,以此类推

  • $@ 所有参数

  • $# 参数个数

  • $? 前一个命令的返回值

  • $$ 当前脚本的进程识别码

  • !! 完整的上一条命令,包括参数。

    当因为权限不足之行命令失败时,可以使用sudo !!再尝试一次

  • $_ 上一条命令的最后一个参数。

    在交互式shell中,可通过按下Esc之后键入.来获取此值

Shell工具

当我们需要查询某个指令的用法时,可通过man命令来查询,man也就是manual的缩写,其提供了命令的用户手册。

但是man命令的查询结果太过详实,tldr是一个非常好用的替代品,用来查询指令用法。

1
npm install -g tldr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
~/p/notes tldr tar                                                            

tar

Archiving utility.
Often combined with a compression method, such as gzip or bzip2.
More information: https://www.gnu.org/software/tar.

- [c]reate an archive and write it to a [f]ile:
tar cf target.tar file1 file2 file3

- [c]reate a g[z]ipped archive and write it to a [f]ile:
tar czf target.tar.gz file1 file2 file3

- [c]reate a g[z]ipped archive from a directory using relative paths:
tar czf target.tar.gz --directory=path/to/directory .

- E[x]tract a (compressed) archive [f]ile into the current directory [v]erbosely:
tar xvf source.tar[.gz|.bz2|.xz]

- E[x]tract a (compressed) archive [f]ile into the target directory:
tar xf source.tar[.gz|.bz2|.xz] --directory=directory

- [c]reate a compressed archive and write it to a [f]ile, using [a]rchive suffix to determine the compression program:
tar caf target.tar.xz file1 file2 file3

- Lis[t] the contents of a tar [f]ile [v]erbosely:
tar tvf source.tar

- E[x]tract files matching a pattern from an archive [f]ile:
tar xf source.tar --wildcards "*.html"

find命令用来查找文件,由于tldr太好用了,可以直接tldr find来查询用法,就不赘述了。注意find -iname 'PATTERN'不区分大小写。

1
2
3
4
5
~/p/notes  find . -name '*maven*'                                             
./maven2.md
~/p/notes  find . -iname '*maven*'
./maven2.md
./MAVEN.md

grep命令用来查找代码,同样用tldr grep查询其用法即可。-R会递归进入子目录并搜索所有的文本文件。

1
2
3
4
5
6
7
8
~/p grep -R cd notes                                                          
notes/The Missing Semester of Your CS Education.md:~/p/notes cat mcd.sh
notes/The Missing Semester of Your CS Education.md:mcd () {
notes/The Missing Semester of Your CS Education.md: cd "$1"
notes/The Missing Semester of Your CS Education.md:~/p/notes source mcd.sh
notes/The Missing Semester of Your CS Education.md:~/p/notes mcd hello
notes/mcd.sh:mcd () {
notes/mcd.sh: cd "$1"

history命令用来查找历史命令,配合grep命令搜索历史命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~/p history | grep find                                                       
215 find /180942*.*
216 find ~ -name "180942*"
223 find -name "180942*"
224 find -iname "180942*"
225 find ~ iname "180942*"
869 find ~ -iname "docker"
870 sudo find ~ -iname "docker"
3584 find . -name src -type d
3585 find . -name src
3586 tldr find
3657 tar find
3658 tldr find
3668 find . -name '*maven*'
3669 find . -iname '*maven*'

此外,可通过homebrew来安装tree等工具,来展示文件夹目录结构。

Vim

vim作为最流行的基于命令行的编辑器,源于1976年的vi编辑器,是一个多模态的编辑器,并尽量避免鼠标的使用,这也是vim的设计哲学。

编辑模式

在编程的时候,大量时间会花在代码阅读,而不是写代码,vim基于此存在多种编辑模式:

  • Normal
  • Insert (键入i进入)
  • Replace(键入R进入)
  • Visual(键入v进入,键入V进入可视行模式,键入Ctrl+V进入可视块模式)
  • Command(键入:进入)

vim会维护一系列打开的文件,称为“缓存”,每个窗口可以显示一个缓存,一个缓存可以在多个窗口打开。

在Command模式下,可以进行打开、保存、关闭文件等操作:

  • :q 退出(关闭窗口)
  • :w 保存文件
  • :wq 保存文件后退出
  • :e {文件名} 打开要编辑的文件
  • :ls 显示打开的缓存
  • :help {标题} 打开帮助文档
    • :help :w 打开:w命令的帮助文档

在Normal模式下,可以使用命令在文件,也就是缓存中导航:

  • 基本移动:hjkl(左下上右)
  • 词:w(下一个词),b(词首),e(词尾)
  • 行:0(行初),^(第一个非空格字符),$(行尾)
  • 屏幕:H(屏幕首行),M(屏幕中间),L(屏幕底部)
  • 文件:gg(文件头),G(文件尾)
  • 指定行::{行数}
  • 查找:f{字符}t{字符}F{字符}T{字符}
  • 搜索:/{正则表达式}

在Normal模式下,除了进行阅读相关的操作外,还能通过命令对文件进行编辑:

  • O/o 在当前行上/下插入行
  • dw 删除词,d$ 从当前位置删除到行尾,d0 从当前位置删除到行头
  • x 删除字符(等同于dl
  • s替换字符(等同于xi
  • u 撤销
  • y 复制
  • p 粘贴

Git

Git作为版本控制系统的事实标准,已经是多人协同开发最重要的工具。

Git的数据模型

Git将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。文件被称作Blob对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与Blob对象(数据对象)或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树,例如:

1
2
3
4
5
6
7
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

在Git中,每一个提交都被称作为一个快照,历史记录记录是一个由快照组成的有向无环图,因此每个快照都存在一个或者多个父辈(由两个分支merge后的提交),通过HEAD索引确定它的父辈。

1
2
3
4
5
6
7
8
9
10
11
// 文件即数据对象
type blob = array<byte>
// 树可以包含数据或者树
type tree = map<string, tree | blob>
// 每次提交包含父辈,元数据等
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}

Git中的提交是不可改变的,对于提交的修改实际上是创建了一个全新的提交记录,而引用则被更新为指向这些新的提交。

Git中的对象也就是上面所说的集中类型,即blob(数据对象),树或者提交。

1
type object = blob | tree |commit

Git在存储数据时,所有的对象都会基于它们的SHA1哈希进行寻址

1
2
3
4
5
6
7
8
objects = map<string, object>

def store(object):
id = sha1(object)
objects[id] = object

def load(id):
return objects[id]

但是,快照通过它们的sha1哈希值来标记,很不方便实际开发与提交,因此Git通过给这些哈希值命名,并以引用的方式指向提交。例如master可以指向主分支的提交,由于引用可以被更新,指向新的或者历史提交。

Git仓库也就是存储对象和引用的位置,所有的git命令也都对应着对提交树的操作。

Git命令行

  • git help <command>: 获取git命令帮助信息
  • git init: 创建一个新的git仓库,其数据会存放在.git目录下
  • git add <filename>: 添加文件到暂存区
  • git commit:创建一个新的提交
  • git diff <filename>: 显示与暂存区文件的差异
  • git diff <revision> <filename>: 显示某个文件两个版本间的差异
  • git checkout: 更新HEAD和当前的分支
  • git branch <name>: 创建分支
  • git checkout -b <name>: 创建并切换分支
  • git merge <revision>: 合并到当前分支
  • git mergetool: 使用工具来处理合并冲突
  • git rebase: 将一系列补丁变基(rebase)为新的基线
  • git remote: 列出远端
  • git remote add <name> <url>: 添加一个远端
  • git push <remote> <local branch>:<remote branch>: 将对象传送至远端并更新远端引用
  • git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系
  • git fetch: 从远端获取对象/索引
  • git pull: 相当于git fetch; git merge
  • git clone: 从远端下载仓库
  • git commit --amend: 编辑提交的内容或信息
  • git reset HEAD <file>: 恢复暂存的文件
  • git checkout -- <file>: 丢弃修改
  • git stash: 暂时移除工作目录下的修改内容
  • .gitignore: 指定不提交的文件