简单命令(Simple Commands)

管道(Pipelines)

pipeline(管道)是一个或多个命令的序列,用字符 | 分隔。管道的命令格式如下:

1
command [ | command2 ... ]

管道的特点如下:

  • 管道是一个由“标准输入输出”链接起来的进程集合;
  • 管道中的每个命令都作为单独的进程来执行(即在一个子 shell 中启动);
  • 每一个进程的输出(stdout)被直接作为下一个进程的输入(stdin);
  • 管道命令不处理 standard error output(stderr);
  • 管道的符号为:|

管道的处理流程如下图:

pipe

重定向(Redirection)

在命令执行前,它的输入和输出可能被 redirected (重定向),该功能可以用于如下场景:

  • 屏幕输出的信息很重要,而且需要将它存下来时;
  • 一些运行命令的可能已知错误信息,想以 2> /dev/null 将它丢掉;
  • 一些系统的例行命令(例如写在 /etc/crontab)的运行结果,需要存下来时;
  • 错误信息与正确信息需要分别输出时。

例子:

快速创建带内容的文件

1
2
$ echo "hello world" > /tmp/file
$ cat /tmp/file

将已知错误信息丢弃

1
$ find /tmp/ -name 2> /dev/null

描述符(Descriptor Number)

描述符 描述
0 标准输入(stdin)
1 标准输出(stdout)
2 标准错误输出(stderr)

操作符(Operator)

操作符 描述
< 重定向输入(Redirecting Input)
> 重定向输出(Redirecting Output),与 1> 等价
>> 追加到重定向输出(Appending Redirected Output)
2> 重定向错误输出(Redirecting Error)
2>> 追加到重定向错误输出(Appending Redirected Error)
&> 重定向标准输出和标准错误输出(Redirecting Standard Output and Standard Error)。 推荐使用,它与 >word 2>&1 在语义上等价
>& 同上,但不推荐使用
2>&1 将标准错误输出重定向到标准输出

序列(Lists)

list(序列)是一个或多个管道,用操作符 ;, &, &&, 或 || 分隔的序列, 并且可以选择用 ;, &, 或 \新行符结束。

操作符 例子 描述
&& command1 && command2 一个 AND 序列。command2 只有在 command1 返回 0 时才被执行
\ \ command1 \ \ command2 一个 OR 序列。command2 只有在 command1 返回非 0 状态时才被执行
; command1; command2 结束一个序列。不考虑命令的退出状态,连续执行命令
\ command\ 结束一个序列
& command1 & 如果一个命令是由 & 结束的, shell 将在后台的子 shell 中执行这个命令
  • AND 和 OR 序列的返回状态是序列中最后执行的命令的返回状态。
  • 这些序列操作符中, && 和 || 优先级相同, ; 和 & 优先级相同。

退出状态(Exit Status)

  • 从 shell 的角度看,一个命令退出状态是 0 意味着成功退出。 非零状态值表明失败。
  • 如果没有找到命令,为执行它而创建的子进程返回 127。
  • 如果找到了命令但是文件不可执行,返回状态是 126。
  • 如果命令由于扩展或重定向错误而失败,退出状态大于零。

使用场景

在某些情况下,很多命令我想要一次输入去运行,有两种方法:

  1. Shell Script
  2. 使用序列

例如,一串无人值守源代码形式安装的命令如下:

1
$ ./configure && make && make install

复合命令(Compound Commands)

compound command(复合命令)是如下情况之一:

  • (list)
  • { list; }
  • ((expression))
  • [[ expression ]]

  • if list; then list; [ elif list; then list; ] ... [ else list; ] fi

  • case word in [ [(] pattern [ | pattern ]

  • while list; do list; done

  • until list; do list; done
  • for name [ in word ] ; do list ; done
  • for (( expr1 ; expr2 ; expr3 )) ; do list ; done

  • select name [ in word ] ; do list ; done

算术求值(Arithmetic Evaluation)

条件表达式(Conditional Expressions)

GNU/CoreUtils 是一组类 Unix 操作系统所需的基础软件包。它包含三组命令,常用的命令如 catlsrm。学习 GNU/Linux 的第一步,就是要熟悉软件包下常用的命令。下面分别介绍这三组常用的命令:

File utilities

Basic operations

命令 描述 备注
cp Copy files and directories cp -rp 备份目录。
-r 递归复制目录,否则提示“略过目录‘xxx’”。
-p 保留源文件或目录的属性(包括属主、属组、权限、修改时间等)。
-f 强制覆盖。
mv Move (rename) files
rm Remove files or directories rm -rf 强制递归删除文件或目录。
-r 递归删除,将指定目录下的所有文件及子目录一并处理。
-f 强制删除文件或目录。
ln Create a link to a file ln -s TARGET LINK_NAME 创建软链接。
mkdir Create a directory -p 递归创建目录。
rmdir Remove empty directories -p 递归删除空目录,如果目录非空会删除失败并提示:rmdir: failed to remove 'xxx': Directory not empty

Directory listing

命令 描述 备注
ls List directory contents -l 查看详细信息。
-a 显示隐藏文件。
-d 仅列出目录本身,而不是列出目录内的文件。
-h 将文件容量以人类较易读的方式(如GB、KB等)列出来。
-t 按时间排序显示,默认为新的排在前面。
-S 按文件容量大小排序,而不是用文件名。
dir List directory contents briefly Exactly like ls -C -b
vdir List directory contents verbosely Exactly like ls -l -b

Changing file attributes

命令 描述 备注
chown Change file owner and group chown -R owner:group /there/is/a/file
-R 递归修改,常用于一次性更改某一目录内所有的文件、目录。目标属主必须在 /etc/passwd
chgrp Change group ownership -R 递归修改,目标属组必须在 /etc/group
chmod Change access permissions
touch Change file timestamps 改变文件访问和修改时间,也可用于快速创建一个文件。

Disk usage

命令 描述 备注
df Show disk free space on file systems -h 以 K,M,G 为单位,更易读的方式显示。
-i list inode information instead of block usage
du Show disk usage on file systems -h 以 K,M,G 为单位,更易读的方式显示。
-s, --summarize 汇总显示(等于 --max-depth=0
-d, --max-depth=N 显示第 N 层子目录各自的大小,常用于找出最占空间的目录。例如:du --max-depth=1 -h ./
--exclude=PATTERN Exclude files that match PATTERN.
stat Return data about an inode
truncate Shrink or extend the size of a file to the specified size -s 参数指定一个大小:K, M, G, T, P, E, Z, Y

Text utilities

Output of entire files

命令 描述 备注
cat Concatenates and prints files on the standard output 常用于连接并输出多个文件的内容。
tac Concatenates and prints files on the standard output in reverse 常用于反向连接并输出多个文件的内容。
nl Numbers lines of files -b 指定行号的方式,主要有 a t两种:
-b a 无论是否是空行,同样列出行号。
-b t 默认值,不列出空行行号。
base64 base64 encode/decode data and print to standard output

Output of parts of files

命令 描述 备注
head Output the first part of files 默认输出 10 行
tail Output the last part of files -n 输出倒数 n 行(默认输出 10 行)
-f 不停读取输出文件的最新内容,常用于实时监视日志输出,用 Ctrl+C 来终止。
split Split a file into pieces 用于按行、按大小分割文件
csplit Split a file into context-determined pieces

Operating on sorted files

命令 描述 备注
sort Sort text files 详见本文
shuf Shuffling text
uniq Uniquify files 详见本文

Operating on fields

命令 描述 备注
cut Print selected parts of lines 详见本文
paste Merge lines of files 合并多个文件的所有行
join Joins lines of two files on a common field 合并两个文件中相同位置的行

Operating on characters

命令 描述 备注
tr Translate or delete characters 详见本文
expand Convert tabs to spaces
unexpand Convert spaces to tabs

Summarizing files

命令 描述 备注
wc Print the number of bytes, words, and lines in files 详见本文
sum
cksum
md5sum
sha1sum
sha256sum
sha512sum

Shell utilities

User information

命令 描述 备注
id Print user identity 显示当前用户的信息(uid、gid、groups)
logname Print current login name
whoami Print effective user ID
groups Print group names a user is in
users Print login names of users currently logged in
who Print who is currently logged in

System context

命令 描述 备注
date Print or set system date and time date +%Y-%m-%d 2016-12-28
arch Print machine hardware name
nproc Print the number of available processors
uname Print system information
hostname Print or set system name
hostid Print numeric host identifier
uptime Print system uptime and load 常用于查看系统负载

Working context

命令 描述 备注
pwd Print working directory 显示当前所在目录
stty Print or change terminal characteristics
tty Print file name of terminal on standard input
printenv Print all or some environment variables

Modified command invocation

命令 描述 备注
nohup Run a command immune to hangups
timeout Run a command with a time limit
env Run a command in a modified environment

Process control

命令 描述 备注
kill Send a signal to processes

Delaying

命令 描述 备注
sleep Delay for a specified time

Redirection

命令 描述 备注
tee Redirect output to multiple files or processes 详见本文

Conditions

命令 描述 备注
false Do nothing, unsuccessfully
true Do nothing, successfully
test Check file types and compare values
expr Evaluate expressions

Printing text

命令 描述 备注
echo Print a line of text -n 不输出末尾换行符
-e 开启转义字符,例如:反斜杠 、换行符 \n
printf Format and print data
yes Print a string until interrupted 使用管道自动输入“y”进行文件强制覆盖,方法:`yes cp 源文件 目的文件`

Numeric operations

命令 描述 备注
seq Print numeric sequences
numfmt Reformat numbers 常用于格式化数字

File name manipulation

命令 描述 备注
basename Strip directory and suffix from a file name 截取出文件名
dirname Strip last file name component 截取出目录名

参考

GSLB(Global Server Load Balance,全局负载均衡)作为 CDN 系统架构中最核心的部分,负责流量调度。本文站在服务提供方的视角,做一些技术总结。

GSLB 横向对比

下表是三种常见的实现方式对比:

比较项 基于 DNS 解析方式 基于 HTTP 重定向方式 基于 IP 路由方式
性能 本地 DNS 服务器和用户终端 DNS 缓存能力使 GSLB 的负载得到有效分担 GSLB 处理压力大,容易成为系统性能的瓶颈 借助 IP 网络设备完成负载均衡,没有单点性能瓶颈
准确度 定位准确度取决于本地 DNS 覆盖范围,用户的本地 DNS 设置错误会造成定位不准确 在对用户 IP 地址数据进行有效维护的前提下,定位准确且精度高 就近性调度准确,但对设备健康性等动态信息响应会有延迟
效率 效率约等于 DNS 系统本身处理效率 依靠服务器做处理,对硬件资源的要求高 效率约等于 IP 设备本身效率
扩展性 扩展性和通用性好 扩展性较差,需对各种应用协议进行定制开发 通用性好,但适用范围有限
商用性 在 Web 加速领域使用较多 国内流媒体 CDN 应用较多 尚无商用案例

其中,基于 DNS 解析方式的 GSLB 有两个注意点:

准确度

本地 DNS 服务器(英文:Local DNS Server,缩写:LDNS)是用户所在局域网或 ISP 网络中使用的域名服务器,定位准确度就取决于它了。因为当用户在浏览器里访问某个域名时,浏览器会首先向 LDNS 发起查询,LDNS 再代为向整个 DNS 域名系统发起查询,直到找到解析结果。域名解析流程详见本文

如果 LDNS 设置不当,例如没有使用当前 ISP 提供的当地 LDNS,如 8.8.8.8,这种实现方式可能会误判用户的位置,从而将用户误导到错误的 CDN 缓存节点,造成加速效果差的问题。

缓存

DNS 的查询机制给使用它的互联网应用带来额外的时延,有时时延还比较大,为了解决问题,引入了“缓存”机制。缓存是指 DNS 查询结果在 LDNS 中缓存,当其它主机向它发起查询请求时,它就直接向主机返回缓存中能够找到的结果,直到数据过期。

在基于 DNS 解析方式下无论采用何种工作方式,都会有一些请求不会到达 GSLB,这是 DNS 系统本身的缓存机制在起作用。当用户请求的域名在本地 DNS 或本机(客户端浏览器)得到了解析结果,这些请求就不会达到 GSLB。Cache 更新时间越短,用户请求到达 GSLB 的几率越大。由于 DNS 的缓存机制屏蔽掉相当一部分用户请求,从而大大减轻了 GSLB 处理压力,使得系统抗流量冲击能力显著提升,这也是很多商业 CDN 选择 DNS 机制做全局负载均衡的原因之一。但弊端在于,如果在 DNS 缓存刷新间隔之内系统发生影响用户服务的变化,比如某个节点故障,某个链路拥塞等,用户依然会被调度到故障点去。

智能 DNS 实现浅析

基于 DNS 解析方式的 GSLB 的实现关键,就在于使 DNS “智能化”。简单来说,就是通过建立 IP 地址访问列表,判断用户的访问来源,以确定其访问节点的位置。下面浅析如何实现智能 DNS:

IP 地址收集策略

由于基于 DNS 解析方式的 CDN 使用 LDNS 进行寻址,因此我们只需要收集互联网上 DNS 服务器的 IP 地址。这样一来,收集的数量就会大大降低。为了更进一步缩小范围,一般使用 IP 地址加子网掩码的形式,如 123.175.0.0/16。在 IP 地址列表文件,就这么一行,却可以囊括很多 DNS 服务器。

IP 地址收集方法

除了可以跟第三方购买 IP 地址段之外,这里重点介绍下如何自行收集 IP 地址段。

ICANN

ICANN —— 一个负责 IP 地址分配以及域名管理的机构,与之关联的五个 RIR 机构负责替 ICANN 分配与登记部分区域的 IP 地址段:

RIR Region
AFRINIC Africa region
APNIC Asia and Pacific region
ARIN Canada, many Caribbean and North Atlantic islands, and the United States
LACNIC Latin America and parts of the Caribbean
RIPE NCC Europe, the Middle East and parts of Central Asia

可见,亚太地区的 IP 地址由 APNIC 分配,访问这里可以知道在何处得到 IP 地址分配的有用信息。进入 FTP ,阅读 README 以了解该下载哪个文件以及文件的格式。下载 delegated-apnic-latest 文件,过滤出分配给中国大陆(CN)的 IP 地址。

然后可以通过 CNNIC IP 地址注册信息查询系统查询这个地址段属于哪个运营商,但一次只能查询一个地址段,根本无法手工完成所有地址段的查询,因此推荐在 Linux 下使用 whois 工具以遍历的方式逐个查询,然后按关键字归类、去重、排序,按运营商产生几个独立的文件。如果各 IP 地址租用方未能按统一的标准在 APNIC 提交注册信息则需要特殊处理。

IP 地址列表使用

最后,将每个 IP 地址列表文件关联一个 Bind 的视图 View。定义视图的目的在于,当有来自某个文件所列 IP 范围内的客户发起查询请求时,使用本视图的区文件进行域名解析。通俗的说,就是让某个运营商线路的用户,去访问某个运营商机房的服务器。

工作、生活中常与域名打交道,尤其是近期需要为公司的系统上 CDN 服务,因此对 CDN 的核心 DNS 做了一番重温、总结。

DNS 是什么?

DNS 是互联网的一项基础服务,它将人类易记的域名解析为不易记的 IP 地址,使人更方便的访问互联网。

DNS 的结构?

域名系统(DNS)是一个多层级、分布式的系统,就如同一个树状结构:

1
                    +---+                                 
                    | . |   Root nameserver               
                    +-+-+                                 
                      |                                   
  +-------+-------+---+---+-------+-------+               
  |       |       |       |       |       |               
+-+-+   +-+-+   +-+-+   +-+-+   +-+-+   +-+-+    
|com|   |net|   |org|   |gov|   |cn |   |...|   Top-level domain
+---+   +---+   +-+-+   +---+   +---+   +---+             
                  |                                       
             +----+----+                     
             |wikipedia|   First-level domain                     
             +----+----+                                  
                  |                                       
          +---------------+                               
          |       |       |                               
        +-+-+   +-+-+   +-+--+                            
        |www|   |ftp|   |mail|   Resource record          
        +---+   +---+   +----+

域名系统(DNS)的每一级只知道直接下级的位置,而无法获得跨级的位置,因此在域名解析的时候,需要自上而下、逐级查询。这种机制虽然看似低效,却能够提供分布式、高容错的服务,避免让域名系统(DNS)成为一个集中式的单点系统。

DNS 如何解析域名?

对某个域名发起第一次解析请求时,负责处理递归查询的本地 DNS 服务器要发送好几次查询:

A DNS recursor consults three name servers to resolve the address www.wikipedia.org.

域名解析的时候,需要自上而下、逐级查询:先查根域,再查顶级域,再查一级域名,最终定位到 IP 地址。dig 命令加 +trace 参数可以追踪整个域名解析过程,从中了解经过的每一级 nameserver,其结果简化如下:

1
$ dig +trace www.wikipedia.org

;; global options: +cmd
.                    NS      a.root-servers.net.
.                    NS      b.root-servers.net.
.                    NS      c.root-servers.net.
.                    NS      d.root-servers.net.
.                    NS      e.root-servers.net.
.                    NS      f.root-servers.net.
.                    NS      g.root-servers.net.
.                    NS      h.root-servers.net.
.                    NS      i.root-servers.net.
.                    NS      j.root-servers.net.
.                    NS      k.root-servers.net.
.                    NS      l.root-servers.net.
.                    NS      m.root-servers.net.
;; Received 315 bytes from 202.96.128.166#53(202.96.128.166) in 642 ms

org.                 NS      a0.org.afilias-nst.info.
org.                 NS      a2.org.afilias-nst.info.
org.                 NS      b0.org.afilias-nst.org.
org.                 NS      b2.org.afilias-nst.org.
org.                 NS      c0.org.afilias-nst.info.
org.                 NS      d0.org.afilias-nst.org.
;; Received 691 bytes from 199.7.83.42#53(l.root-servers.net) in 408 ms

wikipedia.org.       NS      ns0.wikimedia.org.
wikipedia.org.       NS      ns1.wikimedia.org.
wikipedia.org.       NS      ns2.wikimedia.org.
;; Received 651 bytes from 199.19.53.1#53(c0.org.afilias-nst.info) in 1155 ms

www.wikipedia.org.   A       198.35.26.96
;; Received 90 bytes from 91.198.174.239#53(ns2.wikimedia.org) in 365 ms

下面分别介绍每一级 nameserver:

根域名(Root nameserver)

. 代表的根域名服务器(Root nameserver),是 DNS 中最高级别的域名服务器(nameserver),负责返回顶级域名的权威域名服务器(authoritative nameserver)的地址。

早期的域名必须以英文句号“.”结尾,当用户访问 www.wikipedia.org 的 HTTP 服务时必须在地址栏中输入:http://www.wikipedia.org.,这样 DNS 才能够进行域名解析。如今 DNS 服务器已经可以自动补上结尾的句号。

目前全球共有 13 组根域名服务器,以英文字母 A 到 M 依序编号,域名格式为“字母.root-servers.net”。编号相同的根域名服务器使用同一个 IP,数百台根域名服务器总共只使用 13 个 IP,因此可以抵抗针对其所进行的分布式拒绝服务攻击(DDoS)。这些根域名服务器的运行软件皆为 BINDNSD

顶级域名(Top-level domain)

顶级域名(Top-level domain, TLD)主要分为两类:

顶级域名的数量仍在不断增长中,除了英文字母的域名,还不断新增各种语系的域名,如中文域名。

一级域名(First-level domain)

组织或个人通过域名代理服务商(如 GoDaddy、万网)进行注册的域名。根据需要还可以自行在一级域名下新增二级、三级等子域名。

资源记录(Resource record)

域名系统中,一般一个域(DNS zone)通过一个 zone 文件保存该域的相关配置信息。zone 文件包含了域名和 IP 地址等资源之间的映射,以资源记录(Resource recerd, RR)的文本形式进行组织。这里列举了所有的资源记录类型。

以域名 example.com 为例,其 zone 文件简化如下:

name ttl record class record type record data comment
example.com. 1h IN NS ns ns.example.com is a nameserver for example.com
ns 1h IN A 192.0.2.2 IPv4 address for ns.example.com
example.com. 1h IN A 192.0.2.1 IPv4 address for example.com
www 1h IN CNAME example.com. www.example.com is an alias for example.com

总结

综上,注册域名、管理资源记录都是站长最常见的操作。至于域名解析就交给本地 DNS 服务器代为处理,一般用户都无需操心。

Vim

使用 Vim 也有好几年了,虽然这款“编辑器之神”的学习曲线非常陡峭,但一旦上手将会极大提高文本编辑效率,因此值得投入精力学习。

本文我将会从三个方面总结 Vim 的知识。

模式

Vim 效率之高的秘密,就在于它拥有多种“模式”。如果你已经习惯了 Windows 下的编辑器,这些模式在一开始会很违反你的使用直觉。因此学习 Vim 的第一件事,就是要习惯这些模式之间的切换。

Vim 共具有 6 种基本模式和 5 种派生模式,下面只介绍最常用的 4 个基本模式:

普通模式

Vim 启动后的默认模式。这正好和许多新用户期待的操作方式相反,因为大多数编辑器的默认模式为插入模式(就是一打开编辑器就可以开始码字)。

Vim 强大的编辑能力中很大部分是来自于其普通模式的命令(及组合)。在普通模式下,用户可以执行一般的编辑器命令,比如移动光标,删除文本等等。如果进一步学习各种各样的文本间移动/跳转命令和其它编辑命令,并且能够灵活组合使用的话,能够比那些没有模式的编辑器更加高效的进行文本编辑。

下面介绍普通模式下几类常用的快捷键:

移动命令

跨行移动

快捷键 说明
hjkl VIM allows using the cursor keys in order to move around. However, for a pure VIM experience you should stick to using ‘h’, ‘j’, ‘k’ and ‘l’. It’s considered more efficient since you don’t have to move your hand from the home row when you’re typing.
gg 到第一行
G 到最后一行
nG 到第 n 行
% 匹配括号移动,包括 () {} [](需要先把光标先移到括号上)
* 匹配光标当前所在的单词(# 反向)

当前行移动

快捷键 说明
0 到行头($ 反向)
^ 到本行第一个非 blank 字符的位置(所谓 blank 字符就是空格、tab、换行、回车等)
w 到下一个单词的开头(b 反向)
e 到下一个单词的结尾
f Find next character(F 反向)
fi 到字符 i 处
4fi 到第四个字符 i 处
t Find before character(T 反向)

Vim 当前行移动

编辑命令

替换命令

快捷键 说明
r Replace current character
When you need to replace only one character under your cursor, without changing to insert mode, use r.

剪切/复制/粘贴

快捷键 说明
x Cut current character
d dd Cut current line
dt Cut till …
y yy Copy current line (yank)
yt Copy till …
p Paste

缩进/补全

快捷键 说明
<< 左缩进
>> 右缩进
= 自动缩进
Ctrl + p 在 Insert 模式下,自动补全…
全文格式化

从别的编辑器里粘贴到 vim 里的代码经常由于不正常的缩进变得格式混乱,可以使用如下命令:

自动缩进当前行: ==

全文格式化:gg=G ,即:

  1. gg - Goto the beginning of the file
  2. = - apply indentation
  3. G - till end of file

重复命令

快捷键 说明
. 重复执行上一个命令
n<command> 重复执行某个命令 n 次
<start position><command><end position> 对某段起止文本执行某个命令,例如:d(删除)、y(复制)、v(选择)、gU(变大写)、gu(变小写)

插入模式

在这个模式中,大多数按键都会向文本缓冲中插入文本。大多数新用户希望文本编辑器在编辑过程中一直保持这个模式。

使用以下快捷键进入插入模式:

当前行插入

快捷键 说明
i Switch to insert mode on current character
a Switch to insert mode after current character
I Switch to insert mode on first visible character of the current line
A Switch to insert mode on last visible character of the current line

另起一行插入

快捷键 说明
o Switch to insert mode after current line
O Switch to insert mode before current line

可视模式

这个模式与普通模式比较相似。但是移动命令会扩大高亮的文本区域。高亮区域可以是字符、行或者是一块文本。当执行一个非移动命令(例如复制、删除)时,命令会被执行到这块高亮的区域上。

使用以下快捷键进入可视模式:

快捷键 说明
Ctrl + v Switch to visual block mode
v Switch to visual character mode
V Switch to visual line mode

命令行模式

在命令行模式中可以输入命令。在命令执行完后,Vim 返回到命令行模式之前的模式,通常是普通模式。

使用以下快捷键进入命令行模式:

快捷键 说明
: 执行命令:
:h 帮助文档,例如查看 f 命令的帮助::h f
! 过滤命令
/? 搜索字符串

全局替换

:%s/vivian/sky/g 替换每一行中所有 vivian 为 sky

配置

使用 Vim 年月较久后总会定制一套个性化的 Vim 配置,例如截取一段常用的 ~/.vimrc 配置:

1
set number                  " 显示行号
set cursorline              " 突出显示当前行
set ruler                   " 打开状态栏标尺
set shiftwidth=4            " 设定 << 和 >> 命令缩进时的宽度为 4
set softtabstop=4           " 使得按退格键时可以一次删掉 4 个空格
set tabstop=4               " 设定 tab 长度为 4
set nowrapscan              " 禁止在搜索到文件两端时重新搜索
set incsearch               " 输入搜索内容时就显示搜索结果
set hlsearch                " 高亮显示搜索结果
syntax on                   " 程序语法开关
inoremap jj <ESC>           " 重映射 ESCAPE 键
" 定义缩写:ab [缩写] [要替换的文字]
ab asap as soon as possible

另外注意,Vim 的操作记录会写入 ~/.viminfo

GVim

GVim 是 Windows 版的 Vim,因为有了标准的 Windows 风格的图形界面,所以叫 G(Graphical)Vim。

GVim 的多标签切换:

快捷键 说明
:tabnew 新建标签页
:tabs 显示已打开标签页的列表
:tabc 关闭当前标签页
:tabn 移动到下一个标签页
:tabp 移动到上一个标签页
:tabfirst 移动到第一个标签页
:tablast 移动到最后一个标签页

字符集配置参考 这里,其它小技巧参考 这里

参考

上文

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起。这种问题只能由人来裁决,解决冲突的办法无非是从冲突中二者选其一或者由你亲自整合到一起。

你完全可以手工编辑处理冲突,或者推荐使用图形化的外部合并与比较工具(mergetool)。

mergetool 是什么?

Merge tool is a GUI that steps you through each conflict, and you get to choose how to merge. Sometimes it requires a bit of hand editing afterwards, but usually it’s enough by itself. It is much better than doing the whole thing by hand certainly.

mergetool 如何选择?

mergetool 需自行选择安装。选择很多,例如:meld, opendiff, kdiff3, tkdiff, xxdiff, tortoisemerge, gvimdiff, diffuse, ecmerge, p4merge, araxis, vimdiff, emerge …

推荐使用 meld,一款优秀的可视化 diff 和代码合并工具(merge tool),支持特性如下:

  • 跨平台,支持 Linux/Unix、 OS X、Windows,多种便捷的安装方式
  • 跨工具,支持多种版本控制系统(VCS),如 Git、SVN、Mercurial …
  • 支持双方或三方文件、目录对比
  • GUI 界面好看 :)

meld 如何使用?

安装好 meld ,还需进行如下配置:

用于 git diff

首先配置好 git :

1
$ git config --global diff.external ~/meld.sh

然后准备编写 meld.sh 包装脚本:

1
$ vim ~/meld.sh

默认情况下, git diff 会传递 7 个参数给该包装脚本:

1
path old-file old-hex old-mode new-file new-hex new-mode

但我们仅仅只需要 old-filenew-file 参数,因此需要用包装脚本来传递它们。脚本内容如下:

1
2
#!/bin/sh
meld $2 $5

如果对涉及到的参数感兴趣,可以在脚本补充一段 echo $0 $*

最后对于 Linux/Unix、OS X,还需要增加脚本的可执行权限:

1
$ chmod +x ~/meld.sh

以上配置好后,就可以调用图形化工具愉快的使用 git diff 了。

用于 git mergetool

首先配置好 git :

1
$ git config --global merge.tool meld

如果合并的时候出现如下冲突:

1
CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.

使用如下命令:

1
$ git mergetool

就可以调用图形化工具愉快的解决冲突了。

在解决了所有文件的所有冲突后,运行 git add 将把它们标记为已解决状态即可(一旦暂存,就表示冲突已经解决)。

参考

如果工作目录的本地代码做了修改但尚未提交,pull 拉取远程仓库的新提交时,往往会提示冲突:

1
2
3
4
$ git pull
error: Your local changes to the following files would be overwritten by merge:
/there/is/a/conflict/file
Please, commit your changes or stash them before you can merge.

如上所示,有 commitstash 两种处理方法。针对本地代码的完成情况我们需要作出选择。

代码已完成

如果确认本地代码已经完成无误,可以先将本地代码 commit 到本地仓库。再次 pull 拉取远程仓库时,如无冲突,Git 会自动产生一次“合并”提交:

1
2
3
4
5
6
7
$ git lg
* e6f4e18 - Merge branch 'master' of origin (1 minutes ago)
|\
* | abfa93b - 本地仓库的提交 (2 minutes ago)
| * 1f1c21d - 远程仓库的提交 (3 minutes ago)
|/
* 17ef24c - 基准版本 (4 minutes ago)

这是因为 pull 的默认策略是“fetch + merge”。如果本地仓库的提交一直不 push 到远程仓库,极端情况下每一次 pull 都可能会产生一次“合并”提交,这会造成祖先图谱(graph)无谓的复杂。此时推荐使用 rebase 避免本地仓库无谓的合并节点:

1
$ git pull --rebase

代码未完成

但如果本地代码仍未完成,此时推荐使用 stash 命令暂存修改,避免将未完成的功能代码 commit 到本地仓库,污染仓库。

暂存当前修改

第一步,stash 暂存当前修改:

1
2
$ git stash save "填写你的备注"
Saved working directory and index state ......
1
2
3
$ git status
On branch master
nothing to commit, working directory clean

可以看到暂存后工作目录一干二净。这是因为 stash 命令可以将“修改过的被追踪的文件(modified tracked files)”和“暂存的变更(staged changes)”暂存到临时堆栈中,并将工作目录还原干净,以便后续的操作。

Stashing takes the dirty state of your working directory – that is, your modified tracked files and staged changes – and saves it on a stack of unfinished changes that you can reapply at any time.

拉取远程仓库

第二步,继续 pull 拉取远程仓库并进行自动合并:

1
$ git pull

重新还原暂存

第三步,stash pop 重新还原暂存修改:

1
$ git stash pop

处理冲突

最后一步,如果还原后产生冲突,需要手工或使用 mergetool 进行处理。处理完毕后,使用 add 标明冲突已解决:

1
$ git add .

参考

Git-工具-储藏(Stashing)

本文介绍一个生僻但推荐使用的命令 rebase(衍合)。

使用场景

衍合的两个使用场景:

  1. 整理提交历史
  2. 生成干净补丁

整理提交历史

开发过程中,常常需要定期将最新的远程分支拉取(pull)到本地分支,保持本地代码最新(up to date)。如果拉取频繁,pull 默认的 merge 行为会造成祖先图谱(ancestry graph)无谓的复杂:

1
$ git pull origin master    // pull = fetch + merge

*   ab900eb - 三方合并版本(注意这里!)
|\
| * 756ba83 - 本地分支提交的版本
* | 915fe84 - 先被推送到远程分支的版本
|/
*   e7ce3f8 - 基准版本(共同祖先)

此时推荐使用 rebase 命令!其产生的祖先图谱如下,非常简洁:

1
*   dc6baf3 - 本地分支提交的版本(注意这个提交被改写了!)
|
*   915fe84 - 先被推送到远程分支的版本
|
*   e7ce3f8 - 基准版本(共同祖先)

可见,这个神奇的命令功能类似 merge ,但它避免了上述无谓的合并节点,从而产生一个更为整洁的提交历史。如果视察一个衍合过的分支历史,仿佛所有的提交都是在一根时间轴上先后进行的,尽管实际上它们原本是同时并行发生的。这么做的好处是,非常便于项目管理人员按时间轴进行代码审查

命令用法

可以在 pull 时主动加上 --rebase 参数:

1
$ git pull --rebase origin master

甚至推荐将 rebase 设为 pull 命令的默认行为,从而应用于所有新建分支:

1
$ git config --global branch.autosetuprebase always

注意,对于应用上述命令前已存在的分支(例如 master),需要补充执行如下配置:

1
$ git config branch.master.rebase true

命令原理

下面进一步介绍rebase 命令的原理:

  1. 把本地分支从上一次 pull 之后的变更暂存起来;
  2. 恢复到上一次 pull 时的情况;
  3. 合并远程分支的提交;
  4. 最后再逐一合并刚暂存下来的本地提交(相当于重放一遍)。

生成干净补丁

另一个使用衍合的目的,是想要得到一个能在远程分支上干净应用的补丁 — 比如某些项目你不是维护者,但想帮点忙的话,最好用衍合:先在自己的一个独立分支中进行开发,当准备向主项目提交补丁的时候,根据最新的 origin/master 进行一次衍合操作然后再提交,这样维护者就不需要做任何整合工作(实际上是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址作一次快进合并,或者直接采纳你提交的补丁。

使用风险

注意,衍合必须遵守的准则:一旦本地分支中的提交(commit)已经被推送到远程仓库,就千万不要对该分支进行衍合操作。如果把衍合当成一种在推送(push)代码之前整理提交历史的手段,而且仅仅衍合那些尚未推送的本地提交,就没问题。如果衍合那些已经推送的提交,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。

参考

快进式合并

当我们 git pull 拉取代码时,背后实际上是进行了一次“快进式合并”:

1
2
3
4
5
6
7
8
$ git fetch origin master:master
$ git merge origin/master

* 756ba83 - 特性分支版本2(SHA-1 不变)
|
* 915fe84 - 特性分支版本1(SHA-1 不变)
|
* e7ce3f8 - master 基准版本(祖先)

那么,究竟什么是“快进式合并(fast-forward merge)”?如果顺着一个分支走下去可以直接到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

快进式合并一般只用于同一分支内的代码合并。

非快进式合并

作为对比,如果加上 --no-ff 参数进行“非快进式合并(no-fast-forward merge)”,其祖先图谱如下:

1
2
3
4
5
6
7
8
9
$ git checkout master
$ git merge --no-ff feature-test

* ab900eb - 合并版本(注意这里!)
|\
| * 756ba83 - 特性分支版本2
| * 915fe84 - 特性分支版本1
|/
* e7ce3f8 - master 基准版本(祖先)

可见,合并后保留有分支历史痕迹,能看得出来曾经做过分支合并。为了保证版本演进的清晰,当分支合并回主干时(如上),建议都加上 --no-ff 参数。

参考

checkout 命令可以用于三种场景:

  • 切换分支
  • 创建分支
  • 撤销修改

本文只介绍第三种场景。

例子

如果我们想要撤销一个文件的本地修改,自然可以手工编辑恢复,但这样做实在是吃力不讨好。 checkout 命令可以帮助我们:

只撤销本地修改

修改文件后,使用 status 命令查看一下文件状态:

1
2
3
4
5
6
$ git status
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: /there/is/a/modified/file

Git 提示我们,对于未 add 进暂存区的文件,可以使用 git checkout -- <file> 快速撤销本地修改。

同时撤销本地和暂存区修改

那么,对于已 add 进暂存区的文件,如何撤销本地修改?还是先使用 status 命令查看一下文件状态:

1
2
3
4
5
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: /there/is/a/modified/file

先取消暂存修改

Git 提示我们,可以使用 reset 命令取消暂存:

1
$ git reset /there/is/a/modified/file

取消暂存后,文件状态就回到了跟“例1”一样了:

1
2
3
4
5
6
$ git status
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: /there/is/a/modified/file

再撤销本地修改

这时按提示使用 checkout 即可:

1
$ git checkout -- /there/is/a/modified/file

这时工作目录就干净了:

1
2
$ git status
nothing to commit, working directory clean

可以看到,结合使用 resetcheckout 命令,可以撤销 index 和 working tree 的修改。

一步到位

那么有更便捷的、一步到位的办法吗?有,指定提交即可:

1
2
3
4
5
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: /there/is/a/modified/file
1
$ git checkout HEAD -- /there/is/a/modified/file
1
2
$ git status
nothing to commit, working directory clean

那么 checkout 命令的全貌究竟是怎样的呢?

checkout 命令格式

checkout 命令的格式及描述如下:

1
2
3
git checkout [<tree-ish>] [--] <paths>...

Updates the named paths in the working tree from the index file (default) or from a named <tree-ish> (most often a commit, tag or branch)
  • 默认使用 index 暂存区的内容覆盖本地修改,如果不指定 <tree-ish> 参数。
  • 或者可以使用指定的提交、标记、分支版本覆盖本地修改。
  • 为了避免文件路径 <paths><tree-ish> 同名而发生冲突,在 <paths> 前用 -- 作为分隔。

checkoutreset

还记得在《git reset 命令回退版本》中介绍的 reset 命令吗?它与 checkout 命令之间有什么区别与关系?

区别

在这里介绍 reset 命令的另一种形式:

1
2
3
git reset [<tree-ish>] [--] <paths>...

This form copy entries from <tree-ish> to the index for all <paths>. (It does not affect the working tree or the current branch.)

checkout 命令的参数一模一样,区别是什么?

命令 操作目标 描述
checkout 工作目录(working tree) 用于撤销本地修改
reset 暂存区(index) 只用于覆盖暂存区

因此 git reset <paths> 等于 git add <paths> 的逆向操作。

如果企图用 reset 命令覆盖工作目录,是会报错的:

1
$ git reset --hard /there/is/a/modified/file
fatal: Cannot do hard reset with paths.

关系

After running git reset <paths> to update the index entry, you can use git checkout -- <paths> to check the contents out of the index to the working tree.

Alternatively, using git checkout [<tree-ish>] [--] <paths> and specifying a commit, you can copy the contents of a path out of a commit to the index and to the working tree in one go.

参考

git checkout
git reset
Git 教程 - 撤销修改