Qeuroal's Blog

静幽正治

教程

  • 本人学习的总原则: 了解功能, 现用现学

    以熟练实用为目标, 以查漏补缺为方法, 达到究竟涅槃不可思议的程度

    以下的顺序即为学习顺序, 分为看过和感觉不错的

学习顺序推荐

  1. learn vim

    • zh

      不全(第25-29章是没有的)

    • en

  2. Vim实用技巧必知必会(极客时间吴咏炜)

  3. Vim参考手册 by Bram Molenaar, 译者: Willis

    一句话: 布莱姆·米勒(荷兰语:Bram Moolenaar,1961年—2023年8月3日)是一名荷兰程序员,在自由软件界素有盛名。他是Vim软件的最初开发者,软件版本管理者,被推崇为终身仁慈独裁者.

感觉不错的

  • 控制台运行 vimtutor 这是 Vim 官方实操教程

  • vim实操教程

    只能当作入门吧!

  • 简明 Vim 练级攻略

    左耳听风值得信赖, 很不错的入门教程

  • Vim Galore

    更新频繁,Vim 进阶必读. 确实东西很多!!!

  • 每日一Vim

    共 30 篇,说是比较全, 但是还达不到系统的程度, 可以用来拓展知识面, 而不应该当作系统知识学习

  • Open Vim

    交互式 Vim 教程

  • QuickRef.ME/vim

    Vim cheatsheet

计数

  • 移动技术
  • d
  • p P
  • 宏命令
  • :normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a

插件

g

first

更多帮助

关于Vim帮助手册有一个额外的小技巧:假设您想了解关于Ctrl-p在插入模式时的功能。如果您仅仅查找:h CTRL-P,您将被导航到普通模式下关于Ctrl-P的介绍页面,这并不是您想要的信息。在这种情况下,改为查找:h i_CTRL-P。添加的i_表示插入模式。注意它属于哪个模式。

Vimrc

配置文件

激活这一配置(:source %)

您的vimrc文件中添加set nocompatible。如果启用compatible选项,许多Vim特有的功能将会被禁止

start

安装

从Vim的官方网站或官方仓库可以获得下载链接:

退出Vim

  • :q

在Vim的好几种模式下都可以通过按<Esc>键切回普通模式

保存文件

  • :w
  • :w filename
  • :wq
  • :q!

帮助

如果您需要查询一些信息,只需要输入:h后接关键词,然后按<Tab>

打开文件

  • 单个文件

    1
    vim hello1.txt
  • 多个文件

    1
    vim hello1.txt hello2.txt hello3.txt

参数

当前版本

终端

1
vim --version
  • ‘+’表示支持的特性
  • ‘-‘表示不支持的特性

内部实现

1
:version

替换

在Vim中,您可以使用:s命令(substitue的缩写)替换文本。如果您想打开hello.txt后立即将所有的”pancake”替换成”bagel”,在终端中:

1
vim +%s/pancake/bagel/g hello.txt

该命令可以被叠加,在终端中:

1
vim +%s/pancake/bagel/g +%s/bagel/egg/g +%s/egg/donut/g hello.txt

Vim会将所有”pancake” 实例替换为”bagel”,然后将所有”bagel”替换为”egg”,然后将所有”egg”替换为”donut”(在后面的章节中您将学到如何替换)。

您同样可以使用c标志来代替+语法,在终端中:

1
2
vim -c %s/pancake/bagel/g hello.txt
vim -c %s/pancake/bagel/g -c %s/bagel/egg/g -c %s/egg/donut/g hello.txt

打开多个窗口

使用oO选项使Vim打开后分别显示为水平或垂直分割的窗口

若想将Vim打开为2个水平分割的窗口,在终端中运行:

1
vim -o2

若想将Vim打开为5个水平分割的窗口,在终端中运行:

1
vim -o5

若想将Vim打开为5个水平分割的窗口,并使前两个窗口显示hello1.txthello2.txt的内容,在终端中运行:

1
vim -o5 hello1.txt hello2.txt

若想将Vim打开为2个垂直分割的窗口、5个垂直分割的窗口、5个垂直分割窗口并显示2个文件,在终端中分别运行以下命令:

1
2
3
vim -O2
vim -O5
vim -O5 hello1.txt hello2.txt

挂起

如果您编辑时想将Vim挂起,您可以按下Ctrl-z。同样,您也可以使用:stop:suspend命令达到相同的效果。若想从挂起状态返回,在终端中运行fg命令。

聪明的启动Vim

您可以向vim命令传递不同的选项(option)和标志(flag),就像其他终端命令一样。其中一个选项是命令行命令(+{cmd}-c cmd)。当您读完本教程学到更多命令后,看看您是否能将相应命令应用到Vim的启动中。同样,作为一个终端命令,您可以将vim命令和其他终端命令联合起来。比如,您可以将ls命令的输出重定向到Vim中编辑,命令是ls -l | vim -

缓冲区(Buffers),窗口(Windows)和选项卡(Tabs)

确保您的vimrc文件中开启了set hidden

Buffers

buffer就是内存中的一块空间,您可以在这里写入或编辑文本。当您在Vim中打开一个文件时,文件的数据就与一个buffer绑定。当您在Vim中打开3个文件,您就有3个buffers。

运行:buffers命令可以查看所有的buffers(另外,您也可以使用:ls:files命令)

遍历所有buffers

  • :bnext 切换至下一个buffer(:bprevious切换至前一个buffer)。
  • :buffer + 文件名。(按下<Tab>键Vim会自动补全文件名)。
  • :buffer + n, n是buffer的编号。比如,输入:buffer 2将使您切换到buffer #2。
  • 按下Ctrl-O将跳转至跳转列表中旧的位置,对应的,按下Ctrl-I将跳转至跳转列表中新的位置。这并不是属于buffer的特有方法,但它可以用来在不同的buffers中跳转。我将在第5章详细讲述关于跳转的知识。
  • 按下Ctrl-^跳转至先前编辑过的buffer。

删除buffer

  • :bdelete
  • 接收一个buffer编号(:bdelete 3将删除buffer #3)一个文件名(:bdelete然后按<Tab>自动补全文件名)。

仅保留当前 buffer

%bd | e#

退出Vim

顺带说一句,如果您已经打开了多个buffers,您可以使用quit -all来关闭所有的buffers:

1
:qall

如果您想关闭所有buffers但不保存,仅需要在后面加!(叹号)就行了:

1
:qall!

若要保存所有buffers然后退出,请运行:

1
:wqall

Windows

一个window就是在buffer上的一个视口

在Vim中,您同样可以拥有多个窗口。

先前我说过,您看到的是file1.js的buffer。但这个说法并不完整,现在这句话得更正一下,您看到的是file1.js 的buffer通过 一个窗口 显示出来。窗口就是您查看的buffer所使用的视口。

如果您想在窗口之间导航,使用这些快捷键:

1
2
3
4
Ctrl-W H    移动光标到左边的窗口
Ctrl-W J 移动光标到下面的窗口
Ctrl-W K 移动光标到上面的窗口
Ctrl-W L 移动光标到右边的窗口

多窗口

  • split: 垂直排列

    1
    :split file2.js
  • vsplit: 水平排列

    1
    :split file2.js
  • 多个窗口显示同一个buffer

    1
    :buffer file2.js
1
2
3
:vsplit filename    垂直分割当前窗口,并在新窗口中打开名为filename的文件。
:split filename 水平分割当前窗口,并在新窗口中打开名为filename的文件。
:new filename 创建一个新窗口并打开名为filename的文件。

普通命令

1
2
3
4
Ctrl-W V    打开一个新的垂直分割的窗口
Ctrl-W S 打开一个新的水平分割的窗口
Ctrl-W C 关闭一个窗口
Ctrl-W O 除了当前窗口,关闭所有其他的窗口

关闭窗口

您可以按Ctrl-W C或输入:quit。当您关闭一个窗口后,buffers仍然会在列表中。

上下窗口最大化

1
ctrl+w+shift+_

上下窗口对等大小

1
ctrl+w+=

左右窗口最大化

1
ctrl+w+shift+|

左右窗口对等

1
ctrl+w+=

上下窗口大小微调

1
ctrl+w+-
1
ctrl+w+[num]+-
1
ctrl+w+shift+[+]
1
ctrl+w+[num]+shift+[+]

左右窗口大小微调

1
ctrl+w+shift+>
1
ctrl+w+[num]+shift+>
1
ctrl+w+shift+<
1
ctrl+w+[num]+shift+<

窗口对调

1
ctrl+w+x

窗口必须是对等的才行

窗口轮转

1
ctrl+w+r/R

窗口必须是对等的才行

移动所在窗口位置

1
ctrl+w+HJKL

不同窗口跳转

1
crtl+w+hjkl
1
crtl+w、ctrl+w

Tabs

Tabs就是windows的集合

功能

拥有多个tabs的好处是,您可以在不同的tab中使用不同的窗口布局。也许,您想让您的第1个tab包含3个垂直分割的窗口,然后让第2个tab为水平分割和垂直分割混合的窗口布局。tab是完成这件工作的完美工具!

tab启动

  • 导航

    1
    2
    3
    4
    5
    6
    :tabnew file.txt    在tab中打开一个文件
    :tabclose 关闭当前tab
    :tabnext 切换至下一个tab
    :tabprevious 切换至前一个tab
    :tablast 切换至最后一个tab
    :tabfirst 切换至第一个tab
  • gt切换到下一个标签页(对应的,可以用gT切换到前一个标签页)。您也可以传递一个数字作为参数给gt,这个数字是tab的编号。若想切换到第3个tab,输入3gt

  • 启动

    1
    vim -p file1.js file2.js file3.js

三维移动

窗口

就像在笛卡尔坐标系的二维平面上沿着X-Y轴移动

使用Ctrl-W H/J/K/L移动到上面、右侧、下面、以及左侧的窗口

buffer

在笛卡尔坐标系的Z轴上穿梭,想象您的buffer文件在Z轴上呈线性排列

使用:bnextbprevious在Z轴上一次一个buffer地遍历。您也可以使用:buffer 文件名/buffer编号在Z轴上跳转到任意坐标

总结

结合window和buffer的移动,您可以在 三维空间 中移动

可以使用window导航命令移动到上面、右侧、下面、或左侧的窗口(X-Y平面导航)

因为每个window都可能包含了多个buffers,您可以使用buffer移动命令向前、向后移动(Z轴导航)

使用

  • 首先,对于==某个特定任务==,我先使用buffers存储所有需要的文件。Vim就算打开很多buffer,速度一般也不会减慢。另外打开多个buffers并不会使我的屏幕变得拥挤。我始终只会看到1个buffer(假设我只有1个window),这可以让我注意力集中在1个屏幕上。当我需要使用其他文件时,可以快速切换至对应文件的buffer。
  • 当==比对文件、读文档、或追踪代码流==时,我使用==多窗口来一次查看多个buffers==。我尽量保持屏幕上的==窗口数不超过3个==,因为超过3个屏幕将变得拥挤(我使用的是小型笔记本)。当相应工作完成后,我就关掉多余的窗口。窗口越少可以使注意力更集中。
  • 我使用tmuxwindows来代替tabs。通常一次使用多个tmux窗口。比如,一个tmux窗口用来写客户端代码,一个用来写后台代码。

打开和搜索文件

打开和编辑文件

Vim中打开一个文件使用:edit

1
:edit file.txt
  • 如果file.txt已经存在,就会打开file.txtbuffer。
  • 如果file.txt不存在,会创建一个新buffer名为file.txt

注意

  • :edit命令支持使用<Tab>进行自动补全

  • :edit可以接收通配符参数。*匹配当前目录下的任意文件

    • :edit *.yml<Tab>: Vim将列出当前目录下所有.yml文件供您选择。

    • 可以使用**进行递归的搜索

      • 查找当前项目文件夹下所有*.md文件,但您不知道在哪个目录,您可以这样做

        1
        :edit **/*.md<Tab>
  • :edit可以用于运行netrw(Vim的内置文件浏览器)

    • 使用:给:edit一个目录参数而不是文件名就行了

    • 示例

      1
      2
      :edit .
      :edit test/unit/

使用find命令搜索文件

搜索文件

使用:find命令搜索文件

1
2
:find package.json
:find app/controllers/users_controller.rb

:find命令同样支持自动补全

1
2
:find p<Tab>                " to find package.json
:find a<Tab>c<Tab>u<Tab> " to find app/controllers/users_controller.rb

Find 和 Path

区别

  • :find命令根据path选项配置的路径查找文件
  • :edit不会

查看一下您的path是什么

1
:set path?

默认情况下,您的path内容很可能是这样的:

1
path=.,/usr/include,,
  • . 意思是在当前文件所在目录下搜索。(译者注:注意不是命令行输入pwd返回的当前目录,而是 当前所打开的文件 所在的目录)
  • , means to search in the current directory.(译者注:此处貌似作者有点小错误,逗号,应该是表示路径之间的分割符。连续的两个,,(两个逗号之间为空)才表示当前目录)
  • /usr/include 表示在C编译器头文件目录下搜索。

app/controllers/添加到当前path选项。以下是操作步骤

1
:set path+=app/controllers/

现在您的路径已经更新,当您输入:find u<Tab>时,Vim将会在app/controllers/目录内搜索所有以”u”开头的文件。

如果您有一个嵌套的目录controllers/,比如app/controllers/account/users_controller.rb,Vim就找不到users_controllers了。您必须改为添加:set path+=app/controllers/**

将整个项目文件夹添加到path中,这样当您按<Tab>,Vim将在所有文件夹内搜索您要找的文件,

1
:set path+=$PWD/**
  • 建议仅仅将您最常访问的文件/目录添加到path
  • 您可以将set path+={您需要添加的目录}添加到您的vimrc文件中。更新path仅花费几秒钟,但可以为您的工作节省很多时间。

使用Grep命令在文件中搜索

如果您想在文件内部查找(搜索文件中的词句),您可以使用grep。Vim有两个方法可以完成这个工作:

  • 内置grep (:vim。没错,就是:vim,它是:vimgrep的简写)。
  • 外部grep (:grep)。

内置grep

:vim有以下语法:

1
:vim /pattern/ file
  • /pattern/ 是您要搜索的内容的正则表达式。
  • file 是文件参数。您可以传入多个参数。Vim将在文件中搜索所有匹配正则表达式的内容。类似于:find,您可以传入*和**通配符。

示例

比如,要在app/controllers/目录下所有ruby文件(.rb)中,查找所有的”breakfast”字符串:

1
:vim /breakfast/ app/controllers/**/*.rb

输入上面的命令后,您将会被导航到第一个结果。Vim的vim搜索命令使用quickfix进行处理。要查看所有搜索结果,运行:copen会打开一个quickfix窗口。下面有一些有用的quickfix命令,可以让您提高效率:

1
2
3
4
5
6
:copen        打开quickfix窗口
:cclose 关闭quickfix窗口
:cnext 跳到下一个错误
:cprevious 跳到前一个错误
:colder 跳到旧的错误列表
:cnewer 跳到新的错误列表

缺点:慢,如果匹配结果数量较多时系统速度会变慢。这是因为Vim将每一个搜索结果读入内存。Vim加载每一个匹配的文件就像它们被编辑一样。如果Vim查到大量文件,它将消耗很多内存

外置grep

默认情况下,它使用终端命令grep。要想在app/controllers/目录中搜索字符串”lunch”,您可以这样做:

1
:grep -R "lunch" app/controllers/

注意这里不是使用/pattern/,它遵循的是终端grep的语法"pattern",它同样使用’quickfix’来显示所有的匹配结果。

Vim使用grepprg变量来决定运行:grep时,应该使用哪个外部程序。所以您并不是非得使用终端的grep命令。稍后我将为您演示如何改变外部grep程序的默认值。

用Netrw浏览文件

NERDTree 是一个很好的选择

Fzf插件

第4章 Vim 语法

刚接触Vim时很容易被Vim许多复杂的命令吓到,如果你看到一个Vim的用户使用gUfV1GdG,你可能不能立刻想到这些命令是在做什么。这一章中,我将把Vim命令的结构拆分成一个简单的语法规则进行讲解。

这一章将是本书中最重要的一章,一旦你理解了Vim命令的语法结构,你将能够和Vim”说话”。注意,在这一章中当我讨论Vim语言时,我讨论并不是
Vimscript(Vim自带的插件编写和自定义设置的语言),这里我讨论的是Vim中normal模式的下的命令的通用规则。

如何学习一门语言

我并不是一个英语为母语的人,当我13岁移民到美国时我学习的英语,我会通过做三件事情建立我的语言能力:

  1. 学习语法规则
  2. 扩展我的词汇量
  3. 练习,练习,练习

同样的,为了说好Vim语言,你需要学习语法规则,增加词汇量,并且不断练习直到你可以把执行命令变成肌肉记忆。

语法规则

你只需要知道一个Vim语言的语法规则:

1
verb + noun # 动词 + 名词

这就类似与在英语中的祈使句:

  • “Eat(verb) a donut(noun)”
  • “Kick(verb) a ball(noun)”
  • “Learn(verb) the Vim Editor(noun)”

现在你需要的就是用Vim中基本的动词和名字来建立你的词汇表

名词(动作 Motion)

我们这里将 动作 Motion 作为名词, 动作Motion用来在Vim中到处移动。下面列出了一些常见的动作的例子:

1
2
3
4
5
6
7
h	左
j 下
k 上
l 右
w 向前移动到下一个单词的开头
} 跳转到下一个段落
$ 跳转到当前行的末尾

在之后的章节你将学习更多的关于动作的内容,所以如果你不理解上面这些动作也不必担心。

动词(操作符 Operator)

根据:h operator,Vim共有16个操作符,然而根据我的经验,学习这3个操作符在80%的情况下就已经够用了

1
2
3
y	yank(复制)
d delete(删除)
c change 删除文本,将删除的文本存到寄存器中,进入插入模式

顺带说一句,当你yank一段文本后,您可以使用p将它粘贴到光标后,或使用P粘贴到光标前。

动词(操作符 Operator)和名词(动作 motions)的结合

现在你已经知道了基本的动词和名词,我们来用一下我们的语法规则,动词和名词的结合!假设你有下面这段文本:

1
const learn = "Vim";
  • 复制当前位置到行尾的所有内容:y$
  • 删除当前位置到下一个单词的开头:dw
  • 修改当前位置到这个段落的结尾:c}

动作 motions也接受数字作为参数(这个部分我将在下个章节展开),如果你需要向上移动3行,你可以用3k代替按3次k,数字可应用在Vim语法中。

  • 向左拷贝2个字符:y2h
  • 删除后两个单词:d2w
  • 修改后面两行:c2j

目前,你也许需要想很久才能完成一个简单的命令,不过我刚开始时也是这样,我也经历过类似的挣扎的阶段但是不久我的速度就快了起来,你也一样。唯一途径就是重复、重复再重复。

作为补充,行级的 操作符 operations (作用在整行中的操作符)在文本编辑中和其他的 操作符 一样,Vim允许你通过按两次 操作符使它执行行级的操作,例如ddyycc来执行删除,复制或修改整个行。您可以使用其他operations试一下(比如gUgU)。

666!从这可以看出Vim命令的一种执行模式。但是到目前为止还没有结束,Vim有另一种类型的名词:文本对象(text object)

更多名词(文本对象 Text Objects)

想象一下你现在正在某个被括号包围的文本中例如(hello Vim),你现在想要删掉括号中的所有内容,你会怎样快速的完成它?是否有一种方法能够把括号中内容作为整体删除呢?

答案是有的。文本通常是结构化的,特别是代码中,文本经常被放置在小括号、中括号、大括号、引号等当中。Vim提供了一种处理这种结构的文本对象的方法。

文本对象可以被 操作符 operations 使用,这里有两类文本对象:

1
2
i + object  内部文本对象
a + object 外部文本对象

内部文本对象选中的部分不包含包围文本对象的空白或括号等,外部文本对象则包括了包围内容的空白或括号等对象。外部对象总是比内部对象选中的内容更多。如果你的光标位于一对括号内部,例如(hello Vim)中:

  • 删除括号内部的内容但保留括号:di(
  • 删除括号以及内部的内容:da(

让我们看一些别的例子,假设你有这样一段Javascript的函数,你的光标停留在”Hello”中的”H”上:

1
2
3
4
const hello = function() {
console.log("Hello Vim");
return true;
}
  • 删除整个”Hello Vim”:di(
  • 删除整个函数(被{}包含):di{
  • 删除”Hello”这个词:diw

文本对象很强大因为你可以在同一个位置指向不同的内容,可以删除一对小括号中的文本,也可以是当前大括号中的函数体,也可以是当前单词。这一点也很好记忆,当你看到di(di{diw时,你也可以很好的意识到他们表示的是什么:小括号,大括号,单词。

让我们来看最后一个例子。假设你有这样一些html的标签的文本:

1
2
3
4
5
<div>
<h1>Header1</h1>
<p>Paragraph1</p>
<p>Paragraph2</p>
</div>

如果你的光标位于”Header1”文本上:

  • 删除”Header1”:dit
  • 删除<h1>Header1</h1>dat

如果你的光标在”div”文本上:

  • 删除h1和所有p标签的行:dit
  • 删除所有文本:dat
  • 删除”div”:di<

下面列举的一些通常见到的文本对象:

1
2
3
4
5
6
7
8
9
10
11
w     一个单词
p 一个段落
s 一个句子
(或) 一对()
{或} 一对{}
[或] 一对[]
<或> 一对<>
t XML标签
" 一对""
' 一对''
` 一对``

你可以通过:h text-objects了解更多

结合性和语法

在学习Vim的语法之后,让我们来讨论一下Vim中的结合性以及为什么在文本编辑器中这是一个强大的功能。

结合性意味着你有很多可以组合起来完成更复杂命令的普通命令,就像你在编程中可以通过一些简单的抽象建立更复杂的抽象,在Vim中你可以通过简单的命令的组合执行更复杂的命令。Vim语法正是Vim中命令的可结合性的体现。

Vim的结合性最强大之处体现在它和外部程序结合时,Vim有一个 过滤操作符!可以用外部程序过滤我们的文本。假设你有下面这段混乱的文本并且你想把它用tab格式化的更好看的一些:

1
2
3
4
Id|Name|Cuteness
01|Puppy|Very
02|Kitten|Ok
03|Bunny|Ok

这件事情通过Vim命令不太容易完成,但是你可以通过终端提供的命令column很快的完成它,当你的光标位于”Id”上时,运行!}column -t -s "|",你的文本就变得整齐了许多:

1
2
3
4
Id  Name    Cuteness
01 Puppy Very
02 Kitten Ok
03 Bunny Ok

让我们分解一下上面那条命令,动词是!(过滤操作符),名词是}(到下一个段落)。过滤操作符!接受终端命令作为另一个参数,因此我把column -t -s "|"传给它。我不想详细描述column是如何工作的,但是总之它格式化了文本。

假设你不止想格式化你的文本,还想只展示Ok结尾的行,你知道awk命令可以做这件事情,那么你可以这样做:

1
!}column -t -s "|" | awk 'NR > 1 && /Ok/{print $0}'

结果如下:

1
2
02  Kitten  Ok
03 Bunny Ok

666!管道竟然在Vim中也能起作用。

这就是Vim的结合性的强大之处。你知道的动词 操作符,名词 动作,终端命令越多,你组建复杂操作的能力成倍增长。

换句话说,假设你只知道四个动作w, $, }, G和删除操作符(d),你可以做8件事:按四种方式移动(w, $, }, G)和删除4种文本对象(dw, d$, d}, dG)。如果有一天你学习了小写变大写的操作符(gU),你的Vim工具箱中多的不是1种工具,而是4种:gUw, gU$, gU}, gUG。现在你的Vim工具箱中就有12种工具了。如果你知道10个动作和5个操作符,那么你就有60种工具(50个操作+10个移动)。另外,行号动作(nG)给你了n动作,其中n是你文件中的行数(例如前往第5行,5G)。搜索动作(/)实际上给你带来无限数量的动作因为你可以搜索任何内容。你知道多少终端命令,外部命令操作符(!)就给你了多少种过滤工具。使用Vim这种能够组合的工具,所有你知道的东西都可以被串起来完成更复杂的操作。你知道的越多,你就越强大。

这种具有结合性的行为也正符合Unix的哲学:一个命令做好一件事动作只需要做一件事:前往X。操作符只需要做一件事:完成Y。通过结合一个操作符和一个动作,你就获得了YX:在X上完成Y。

甚至,动作操作符都是可拓展的,你可以自己创造动作操作符去丰富你的Vim工具箱,Vim-textobj-user插件允许你创建自己的文本对象,同时包含有一系列定义好的文本对象

另外,如果你不知道我刚才使用的columnawk命令也没有关系,重要的是Vim可以和终端命令很好的结合起来。

聪明地学习语法

你刚刚学完Vim唯一的语法规则:

1
verb + noun

我学Vim中最大的”AHA moment”之一是当我刚学完大写命令(gU)时,想要把一个单词变成大写,我本能的运行了gUiW,它居然成功了,我光标所在的单词都大写了。我正是从那是开始理解Vim的。我希望你也会在不久之后有你自己的”AHA moment”,如果之前没有的话。

这一章的目标是向你展现Vim中的verb+noun模式,因此之后你就可以像学习一门新的语言一样渐进的学习Vim而不是死记每个命令的组合。

学习这种模式并且理解其中的含义,这是聪明的学习方式。

第5章 在文件中移动

一开始,通过键盘移动会让你感觉特别慢特别不自在,但是不要放弃!一旦你习惯了它,比起鼠标你可以更快的在文件中去到任何地方。

这一章,你将学习必要的移动以及如何高效的使用它们。 记住,这一章所讲的并不是Vim的全部移动命令(motions),我们的目标是介绍有用的移动来快速提高效率。 如果你需要学习更多的移动命令,查看:h motion.txt

字符导航

最基本的移动单元是上下左右移动一个字符。

1
2
3
4
h   左
j 下
k 上
l 右

你也可以通过方向键进行移动,如果你只是初学者,使用任何你觉得最舒服的方法都没有关系。

我更喜欢hjkl因为我的右手可以保持在键盘上的默认姿势,这样做可以让我更快的敲到周围的键。 为了习惯它,我实际上在刚开始的时候通过~/.vimrc关闭了方向键:

1
2
3
4
noremap <Up> <NOP>
noremap <Down> <NOP>
noremap <Left> <NOP>
noremap <Right> <NOP>

也有一些插件可以帮助改掉这个坏习惯,其中有一个叫vim-hardtime。 让我感到惊讶的是,我只用了几天就习惯了使用hjkl

另外,如果你想知道为什么Vim使用hjkl进行移动,这实际上是因为Bill Joy写VI用的Lear-Siegler ADM-3A终端没有方向键,而是把hjkl当做方向键

如果你想移动到附近的某个地方,比如从一个单词的一个部分移动到另一个部分,我会使用hl。 如果我需要在可见的范围内上下移动几行,我会使用jk。 如果我想去更远的地方,我倾向于使用其他移动命令。

相对行号

我觉得设置numberrelativenumber非常有用,你可以在~/.vimrc中设置:

1
set relativenumber number

==这将会展示当前行号和其他行相对当前行的行号==。

为什么这个功能有用呢?这个功能能够帮助我知道我离我的目标位置差了多少行,有了它我可以很轻松的知道我的目标行在我下方12行,因此我可以使用12j去前往。 否则,如果我在69行,我的目标是81行,我需要去计算81-69=12行,这太费劲了,当我需要去一个地方时,我需要思考的部分越少越好。

这是一个100%的个人偏好,你可以尝试relativenumber/norelativenumbernumber/nonumber 然后选择自己觉得最有用的。

对移动计数

在继续之前,让我们讨论一下”计数”参数。 一个移动(motion)可以接受一个数字前缀作为参数,上面我提到的你可以通过12j向下移动12行,其中12j中的12就是计数数字。

你使用带计数的移动的语法如下:

1
[计数] + 移动

你可以把这个应用到所有移动上,如果你想向右移动9个字符,你可以使用9l来代替按9次l。 当你学到了更多的动作时,你都可以试试给定计数参数。

单词导航

我们现在移动一个更长的单元:单词(word)。 你可以通过w移动到下一个单词的开始,通过e移动到下一个单词的结尾,通过b移动到上一个单词的开始,通过ge移动到前一个单词的结尾。

另外,为了和上面说的单词(word)做个区分,还有一种移动的单元:词组(WORD)。 你可以通过W移动到下一个词组的开始,通过E移动到下一个词组的结尾,通过B移动到前一个词组的开头,通过gE移动到前一个词组的结尾。 为了方便记忆,所以我们选择了词组和单词这两个词,相似但有些区分。

1
2
3
4
5
6
7
8
w		移动到下一个单词的开头
W 移动到下一个词组的开头
e 移动到下一个单词的结尾
E 移动到下一个词组的结尾
b 移动到前一个单词的开头
B 移动到前一个词组的开头
ge 移动到前一个单词的结尾
gE 移动到前一个词组的结尾

词组和单词到底有什么相同和不同呢?单词和词组都按照非空字符被分割,一个单词指的是一个只包含a-zA-Z0-9字符串,一个词组指的是一个包含除了空字符(包括空格,tab,EOL)以外的字符的字符串。 你可以通过:h word:h WORD了解更多。

例如,假如你有下面这段内容:

1
const hello = "world";

当你光标位于这行的开头时,你可以通过l走到行尾,但是你需要按21下,使用w,你需要6下,使用W只需要4下。 单词和词组都是短距离移动的很好的选择。

然而,之后你可以通过当前行导航只按一次从c移动到;

当前行导航

当你在进行编辑的时候,你经常需要水平地在一行中移动,你可以通过0跳到本行第一个字符,通过$跳到本行最后一个字符。 另外,你可以使用^跳到本行第一个非空字符,通过g_跳到本行最后一个非空字符。 如果你想去当前行的第n列,你可以使用n|

1
2
3
4
5
0		跳到本行第一个字符
^ 跳到本行第一个非空字符
g_ 跳到本行最后一个非空字符
$ 跳到本行最后一个字符
n| 跳到本行第n列

你也可以在本行通过ft进行行内搜索,ft的区别在于f会停在第一个匹配的字母上,t会停在第一个匹配的字母前。 因此如果你想要搜索并停留在”h”上,使用fh。 如果你想搜索第一个”h”并停留在它的前一个字母上,可以使用th。 如果你想去下一个行内匹配的位置,使用;,如果你想去前一个行内匹配的位置,使用,

FTft对应的向后搜索版本。如果想向前搜索”h”,可以使用Fh,使用;,保持相同的搜索方向搜索下一个匹配的字母。 注意,;不是总是向后搜索,;表示的是上一次搜索的方向,因此如果你使用的F,那么使用;时将会向前搜索使用,时向后搜索。

1
2
3
4
5
6
f   在同一行向后搜索第一个匹配
F 在同一行向前搜索第一个匹配
t 在同一行向后搜索第一个匹配,并停在匹配前
T 在同一行向前搜索第一个匹配,并停在匹配前
; 在同一行重复最近一次搜索
, 在同一行向相反方向重复最近一次搜索

回到上一个例子:

1
const hello = "world";

当你的光标位于行的开头时,你可以通过按一次键$去往行尾的最后一个字符”;”。 如果想去往”world”中的”w”,你可以使用fw。 一个建议是,==在行内目标附近通过寻找重复出现最少的字母例如”j”,”x”,”z”来前往行中的该位置更快==。

句子和段落导航

接下来两个移动的单元是句子和段落。

首先我们来聊聊句子。 一个句子的定义是以.!?和跟着的一个换行符或空格,tab结尾的。 你可以通过)(跳到下一个和上一个句子。

1
2
(   跳到前一个句子
) 跳到下一个句子

让我们来看一些例子,你觉得哪些字段是句子哪些不是? 可以尝试在Vim中用()感受一下。

1
2
3
4
I am a sentence. I am another sentence because I end with a period. I am still a sentence when ending with an exclamation point! What about question mark? I am not quite a sentence because of the hyphen - and neither semicolon ; nor colon :

There is an empty line above me.

另外,如果你的Vim中遇到了无法将一个以.结尾的字段并且后面跟着一个空行的这种情况判断为一个句子的问题,你可能处于compatible的模式。 运行:set nocompatible可以修复。 在Vi中,一个句子是以两个空格结尾的,你应该总是保持的nocompatible的设置。

接下来,我们将讨论什么是段落。 一个段落可以从一个空行之后开始,也可以从段落选项(paragraphs)中”字符对”所指定的段落宏的每个集合开始。

1
2
{   跳转到上一个段落
} 跳转到下一个段落

如果你不知道什么是段落宏,不用担心,重要的是一个段落总是以一个空行开始和结尾, 在大多数时候总是对的。

我们来看这个例子。 你可以尝试着使用}{进行导航,也可以试一试()这样的句子导航。

1
2
3
4
5
6
7
8
Hello. How are you? I am great, thanks!
Vim is awesome.
It may not easy to learn it at first...- but we are in this together. Good luck!

Hello again.

Try to move around with ), (, }, and {. Feel how they work.
You got this.

你可以通过:h setence:h paragraph了解更多。

匹配导航

程序员经常编辑含有代码的文件,这种文件内容会包含大量的小括号,中括号和大括号,并且可能会把你搞迷糊你当前到底在哪对括号里。 许多编程语言都用到了小括号,中括号和大括号,你可能会迷失于其中。 如果你在它们中的某一对括号中,你可以通过%跳到其中一个括号或另一个上(如果存在)。 你也可以通过这种方法弄清你是否各个括号都成对匹配了。

1
%    Navigate to another match, usually works for (), [], {}

我们来看一段Scheme代码示例因为它用了大量的小括号。 你可以在括号中用%移动

1
2
3
4
5
6
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else
(+ (fib (- n 1)) (fib (- n 2)))
)))

我个人喜欢使用类似vim-rainbow这样的可视化指示插件来作为%的补充。 通过:h %了解更多。

行号导航

你可以通过nG调到行号为n的行,例如如果你想跳到第7行,你可以使用7G,跳到第一行使用gg1G,跳到最后一行使用G

有时你不知道你想去的位置的具体行号,但是知道它大概在整个文件的70%左右的位置,你可以使用70%跳过去,可以使用50%跳到文件的中间。

1
2
3
4
gg      跳转到第一行
G 跳转到最后一行
nG 跳转到第n行
n% 跳到文件的n%

另外,如果你想看文件总行数,可以用CTRL-g查看。

窗格导航

为了移动到当前窗格的顶部,中间,底部,你可以使用HML

你也可以给HL传一个数字前缀。 如果你输入10H你会跳转到窗格顶部往下数10行的位置,如果你输入3L,你会跳转到距离当前窗格的底部一行向上数3行的位置。

1
2
3
4
5
H   跳转到屏幕的顶部
M 跳转到屏幕的中间
L 跳转到屏幕的底部
nH 跳转到距离顶部n行的位置
nL 跳转到距离底部n行的位置

滚动

在文件中滚动,你有三种速度可以选择: 滚动一整页(CTRL-F/CTRL-B),滚动半页(CTRL-D/CTRL-U),滚动一行CTRL-E/CTRL-Y)。

1
2
3
4
5
6
Ctrl-e    向下滚动一行
Ctrl-d 向下滚动半屏
Ctrl-f 向下滚动一屏
Ctrl-y 向上滚动一行
Ctrl-u 向上滚动半屏
Ctrl-b 向上滚动一屏

你也可以相对当前行进行滚动

1
2
3
zt    将当前行置于屏幕顶部附近
zz 将当前行置于屏幕中央
zb 将当前行置于屏幕底部

搜索导航

通常,你已经知道这个文件中有一个字段,你可以通过搜索导航非常快速的定位你的目标。 你可以通过/向下搜索,也可以通过?向上搜索一个字段。 你可以通过n重复最近一次搜索,N向反方向重复最近一次搜索。

1
2
3
4
/   向后搜索一个匹配
? 向前搜素一个匹配
n 重复上一次搜索(和上一次方向相同)
N 重复上一次搜索(和上一次方向相反)

假设你有一下文本:

1
2
3
4
5
let one = 1;
let two = 2;
one = "01";
one = "one";
let onetwo = 12;

你可以通过/let搜索”let”,然后通过n快速的重复搜索下一个”let”,如果需要向相反方向搜索,可以使用N。 如果你用?let搜索,会得到一个向前的搜索,这时你使用n,它会继续向前搜索,就和?的方向一致。(N将会向后搜索”let”)。

你可以通过:set hlsearch设置搜索高亮。 这样,当你搜索/let,它将高亮文件中所有匹配的字段。 另外,如果你通过:set incsearch设置了增量搜索,它将在你输入时不断匹配的输入的内容。 默认情况下,匹配的字段会一直高亮到你搜索另一个字段,这有时候很烦人,如果你希望取消高亮,可以使用:nohlsearch。 因为我经常使用这个功能,所以我会设置一个映射:

1
nnoremap <esc><esc> :noh<return><esc>

你可以通过*快速的向前搜索光标下的文本,通过#快速向后搜索光标下的文本。 如果你的光标位于一个字符串”one”上,按下*相当于/\<one\>
/\<one\>中的\<\>表示整词匹配,使得一个更长的包含”one”的单词不会被匹配上,也就是说它会匹配”one”,但不会匹配”onetwo”。 如果你的光标在”one”上并且你想向后搜索完全或部分匹配的单词,例如”one”和”onetwo”,你可以用g*替代*

1
2
3
4
*   向后查找光标所在的完整单词
# 向前查找光标所在的完整单词
g* 向后搜索光标所在的单词
g# 向前搜索光标所在的单词

位置标记

你可以通过标记保存当前位置并在之后回到这个位置,就像文本编辑中的书签。 你可以通过mx设置一个标记,其中x可以是a-zA-Z。 有两种办法能回到标记的位置: 用 `x精确回到(行和列),或者用'x回到行级位置。

1
2
3
ma    用a标签标记一个位置
`a 精确回到a标签的位置(行和列)
'a 跳转到a标签的行

a-z的标签和A-Z的标签存在一个区别,小写字母是局部标签,大写字母是全局标签(也称文件标记)。

我们首先说说局部标记。 每个buffer可以有自己的一套局部标记,如果打开了两个文件,我可以在第一个文件中设置标记”a”(ma),然后在另一个文件中设置另一个标记”a”(ma)。

不像你可以在每个buffer中设置一套局部标签,你只能设置一套全局标签。 如果你在myFile.txt中设置了标签mA,下一次你在另一个文件中设置mA时,A标签的位置会被覆盖。 全局标签有一个好处就是,即使你在不同的项目中,你也可以跳转到任何一个全局标签上,全局标签可以帮助你在文件间切换。

使用:marks查看所有标签,你也许会注意到除了a-zA-Z以外还有别的标签,其中有一些例如:

1
2
3
4
5
6
7
''   在当前buffer中跳转回到上一次跳转前的最后一行
`` 在当前buffer中跳转回到上一次跳转前的最后一个位置
`[ 跳转到上一次修改或拷贝的文本的开头
`] 跳转到上一次修改或拷贝的文本的结尾
`< 跳转到最近一次可视模式下选择的部分的开头
`> 跳转到最近一次可视模式下选择的部分的结尾
`0 跳转到退出Vim前编辑的最后一个文件

除了上面列举的,还有更多标记,我不会在这一一列举因为我觉得它们很少用到,不过如果你很好奇,你可以通过: marks查看。

跳转

最后,我们聊聊Vim中的跳转你通过任意的移动可以在不同文件中或者同一个的文件的不同部分间跳转。 然而并不是所有的移动都被认为是一个跳转。 使用j向下移动一行就不被看做一个跳转,即使你使用10j向下移动10行,也不是一个跳转。 但是你通过10G去往第10行被算作一个跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'   跳转到标记的行
` 跳转到标记的位置(行和列)
G 跳转到行
/ 向后搜索
? 向前搜索
n 重复上一次搜索,相同方向
N 重复上一次搜索,相反方向
% 查找匹配
( 跳转上一个句子
) 跳转下一个句子
{ 跳转上一个段落
} 跳转下一个段落
L 跳转到当前屏幕的最后一行
M 跳转到当前屏幕的中间
H 跳转到当前屏幕的第一行
[[ 跳转到上一个小节
]] 跳转到下一个小节
:s 替换
:tag 跳转到tag定义

我不建议你把上面这个列表记下来,一个大致的规则是,任何大于一个单词或超过当前行导航的移动都可能是一个跳转。 Vim保留了你移动前位置的记录,你可以通过:jumps查看这个列表,如果想了解更多,可以查看:h jump-motions

为什么跳转有用呢? 因为你可以在跳转列表中通过Ctrl-oCtrl-i在记录之间向上或向下跳转到对应位置。 你可以在不同文件中进行跳转,这将是我之后会讲的部分。

聪明地学习导航

如果你是Vim的新手,这有很多值得你学,我不期望任何人能够立刻记住每样知识点,做到不用思考就能执行这需要一些时间。

我想,最好的开始的办法就是从一些少量的必要的移动开始记。 我推荐你从h,j,k,l,w,b,G,/,?,n开始,不断地重复这10个移动知道形成肌肉记忆,这花不了多少时间。

为了让你更擅长导航,我有两个建议:

  1. 注意重复的动作。 如果你发现你自己在重复的使用l,你可以去找一个方法让你前进的更快,然后你会发现你可以用w在单词间移动。 如果你发现你自己的重复的使用w,你可以看看是否有一种方法能让你直接到行尾,然后你会想到可以用$。 如果你可以口语化的表达你的需求,Vim中大概就会有一种方法去完成它。
  2. 当你学习任何一个新的移动时,多需要花一定的时间直到你可以不经过思考直接完成它。

最后,为了提高效率你不需要知道所有的Vim的命令,大多数Vim用户也都不知道,你只需要学习当下能够帮助你完成任务的命令。

慢慢来,导航技巧是Vim中很重要的技巧,每天学一点并且把它学好。

第6章 输入模式

输入模式是大部分文本编辑器的默认模式,在这个模式下,所敲即所得。

尽管如此,这并不代表输入模式没什么好学的。Vim的输入模式包含许多有用功能。在这一章节中,你将能够学到如何利用Vim输入模式中的特性来提升你的输入效率。

进入输入模式的方法

我们有很多方式从普通模式进入输入模式,下面列举出了其中的一些方法:

1
2
3
4
5
6
7
8
9
10
i    从光标之前的位置开始输入文本
I 从当前行第一个非空字符之前的位置之前开始输入文本
a 在光标之后的位置追加文本
A 在当前行的末尾追加文本
o 在光标位置下方新起一行并开始输入文本
O 在光标位置的上方新起一行并开始输入文本
s 删除当前光标位置的字符并开始输入文本
S 删除当前行并开始输入文本
gi 从当前缓冲区上次结束输入模式的地方开始输入文本
gI 在当前行的第一列的位置开始输入文本

值得注意的是这些命令的小写/大写模式,每一个小写命令都有一个与之对应的大写命令。如果你是初学者,不用担心记不住以上整个命令列表,可以从 ia两条命令开始,这两条命令足够在入门阶段使用了,之后再逐渐地掌握更多其他的命令。

退出输入模式的方法

下面列出了一些从输入模式退出到普通模式的方法:

1
2
3
<esc>     退出输入模式进入普通模式
Ctrl-[ 退出输入模式进入普通模式
Ctrl-c 与 Ctrl-[ 和 <esc>功能相同, 但是不检查缩写

我发现 esc键在键盘上太远了,很难够到,所以我在我的机器上将 caps lock 映射成了esc键。 如果你搜索Bill Joy(Vi的作者)的ADM-3A 键盘, 你会发现esc键并不是像现在流行的键盘布局一样在键盘的最左上方,而是在q键的左边,所以我认为将caps lock 映射成esc键是合理的。

另一个Vim用户中常见的习惯是用以下的配置方法在输入模式中把esc映射到jj或者jk

1
2
inoremap jj <esc>
inoremap jk <esc>

重复输入模式

你可以在进入输入模式之前传递一个计数参数. 比如:

1
10i

如果你输入“hello world!”然后退出输入模式, Vim将重复这段文本10次。这个方法对任意一种进入输入模式的方式都有效(如:10I, 11a, 12o

在输入模式中删除大块文本

当你输入过程中出现一些输入错误时,一直重复地用backspace来删除的话会非常地繁琐。更为合理的做法是切换到普通模式并使用d来删除错误。或者,你能用以下命令在==输入模式下就删除一个或者多个字符==:

1
2
3
Ctrl-h    删除一个字符
Ctrl-w 删除一个单词
Ctrl-u 删除一整行

此外,这些快捷键也支持在 命令行模式 和 Ex模式 中使用(命令行模式和Ex模式将会在之后的章节中介绍)

用寄存器进行输入

寄存器就像是内存里的暂存器一样,可供存储和取出文本。在输入模式下,可以使用快捷键Ctrl-r加上寄存器的标识来从任何有标识的寄存器输入文本。有很多标识可供使用,但是在这一章节中你只需要知道以(a-z)命名的寄存器是可以使用的就足够了。

让我们在一个具体的例子中展示寄存器的用法,首先你需要复制一个单词到寄存器a中,这一步可以用以下这条命令来完成:

1
"ayiw
  • "a 告诉Vim你下一个动作的目标地址是寄存器a
  • yiw 复制一个内词(inner word),可以回顾Vim语法章节查看具体语法。

现在寄存器a存放着你刚复制的单词。在输入模式中,使用以下的快捷键来粘贴存放在寄存器a中文本:

1
Ctrl-r a

Vim中存在很多种类型的寄存器,我会在后面的章节中介绍更多他们的细节。

页面滚动

你知道在输入模式中也是可以进行页面滚动的吗?在输入模式下,如果你使用快捷键Ctrl-x进入Ctrl-x子模式,你可以进行一些额外操作,页面滚动正是其中之一。

1
2
Ctrl-x Ctrl-y    向上滚动页面
Ctrl-x Ctrl-e 向下滚动页面

自动补全

Vim在进入Ctrl-x子模式后(和页面滚动一样),有一个自带的自动补全功能。尽管它不如intellisense或者其他的语言服务器协议(LSP)一样好用,但是也算是一个锦上添花的内置功能了。

下面列出了一些适合入门时学习的自动补全命令:

1
2
3
4
Ctrl-x Ctrl-l	   补全一整行
Ctrl-x Ctrl-n 从当前文件中补全文本
Ctrl-x Ctrl-i 从引用(include)的文件中补全文本
Ctrl-x Ctrl-f 补全一个文件名

当你触发自动补全时,Vim会显示一个选项弹窗,可以使用Ctrl-nCtrl-p来分别向上和向下浏览选项。

Vim也提供了两条不需要进入Ctrl-x模式就能使用的命令:

1
2
Ctrl-n             使用下一个匹配的单词进行补全
Ctrl-p 使用上一个匹配的单词进行补全

通常Vim会关注所有缓冲区(buffer)中的文本作为自动补全的文本来源。如果你打开了一个缓冲区,其中一行是”Chocolate donuts are the best”:

  • 当你输入”Choco”然后使用快捷键Ctrl-x Ctrl-l, Vim会进行匹配并输出这一整行的文本。
  • 当你输入”Choco”然后使用快捷键Ctrl-p,Vim会进行匹配并输出”Chocolate”这个单词。

Vim的自动补全是一个相当大的话题,以上只是冰山一角,想要进一步学习的话可以使用:h ins-completion命令进行查看。

执行普通模式下的命令

你知道Vim可以在输入模式下执行普通模式的命令吗?

在输入模式下, 如果你按下Ctrl-o,你就会进入到insert-normal(输入-普通)子模式。如果你关注一下左下角的模式指示器,通常你将看到-- INSERT -- ,但是按下Ctrl-o后就会变为-- (insert) --。 在这一模式下,你可以执行一条普通模式的命令,比如你可以做以下这些事:

设置居中以及跳转

1
2
3
Ctrl-o zz       居中窗口
Ctrl-o H/M/L 跳转到窗口的顶部/中部/底部
Ctrl-o 'a 跳转到标志'a处

重复文本

1
Ctrl-o 100ihello    输入 "hello" 100 次

执行终端命令

1
2
Ctrl-o !! curl https://google.com    运行curl命令
Ctrl-o !! pwd 运行pwd命令

快速删除

1
2
Ctrl-o dtz    从当前位置开始删除文本,直到遇到字母"z"
Ctrl-o D 从当前位置开始删除文本,直到行末

聪明地学习输入模式

如果你和我一样是从其他文本编辑器转到Vim的,你或许也会觉得一直待在输入模式下很有诱惑力,但是我强烈反对你在没有输入文本时,却仍然待在输入模式下。应该养成当你的双手没有在输入时,就退出到普通模式的好习惯。

当你需要进行输入时,先问问自己将要输入的文本是否已经存在。如果存在的话,试着复制或者移动这段文本而不是手动输入它。再问问自己是不是非得进入输入模式,试试能不能尽可能地使用自动补全来进行输入。尽量避免重复输入同一个单词。

第7章 点命令

在编辑文本时,我们应该尽可能地避免重复的动作。在这一章节中,你将会学习如何使用点命令来重放上一个修改操作。点命令是最简单的命令,然而又是减少重复操作最为有用的命令。

用法

正如这个命令的名字一样,你可以通过按下.键来使用点命令。

比如,如果你想将下面文本中的所有”let“替换为”const”:

1
2
3
let one = "1";
let two = "2";
let three = "3";
  • 首先,使用/let来进行匹配。
  • 接着,使用cwconst<esc>来将”let”替换成”const”。
  • 第三步,使用n来找到下一个匹配的位置。
  • 最后,使用点命令(.)来重复之前的操作。
  • 持续地使用n . n .直到每一个匹配的词都被替换。

在这个例子里面,点命令重复的是cwconst<esc>这一串命令,它能够帮你将需要8次输入的命令简化到只需要敲击一次键盘。

什么才算是修改操作?

如果你查看点命令的定义的话(:h .),文档中说点命令会重复上一个修改操作,那么什么才算是一个修改操作呢?

当你使用普通模式下的命令来更新(添加,修改或者删除)当前缓冲区中的内容时,你就是在执行一个修改操作了。其中的例外是使用命令行命令进行的修改(以开头的命令),这些命令不算作修改操作。

在第一个例子中,你看到的cwconst<esc>就是一个修改操作。现在假设你有以下这么一个句子:

1
pancake, potatoes, fruit-juice,

我们来删除从这行开始的位置到第一个逗号出现的位置。你可以使用df,来完成这个操作,使用.来重复两次直到你将整个句子删除。

让我们再来试试另一个例子:

1
pancake, potatoes, fruit-juice,

这一次你只需要删除所有的逗号,不包括逗号前面的词。我们可以使用f,来找到第一个逗号,再使用x来删除光标下的字符。然后使用用.来重复两次,很简单对不对?等等!这样做行不通(只会重复删除光标下的一个字符,而不是删除逗号)!为什么会这样呢?

在Vim里,修改操作是不包括移动(motions)的,因为移动(motions)不会更新缓冲区的内容。当你运行f,x,你实际上是在执行两个独立的操作:f,命令只移动光标,而x更新缓冲区的内容,只有后者算作修改动作。和之前例子中的df,进行一下对比的话,你会发现df,中的f,告诉删除操作d哪里需要删除,是整个删除命令df,的一部分。

让我们想想办法完成这个任务。在你运行f,并执行x来删除第一个逗号后,使用;来继续匹配f的下一个目标(下一个逗号)。之后再使用.来重复修改操作,删除光标下的字符。重复; . ; .直到所有的逗号都被删除。完整的命令即为f,x;.;.

再来试试下一个例子:

1
2
3
pancake
potatoes
fruit-juice

我们的目标是给每一行的结尾加上逗号。从第一行开始,我们执行命令A,<esc>j来给结尾加上逗号并移动到下一行。现在我们知道了j是不算作修改操作的,只有A,算作修改操作。你可以使用j . j . 来移动并重复修改操作。完整的命令是A,<esc>j

从你按下输入命令(A)开始到你退出输入模式()之间的所有输入都算作是一整个修改操作。

重复多行修改操作

假设你有如下的文本:

1
2
3
4
5
6
7
8
9
10
let one = "1";
let two = "2";
let three = "3";
const foo = "bar";
let four = "4";
let five = "5";
let six = "6";
let seven = "7";
let eight = "8";
let nine = "9";

你的目标是删除除了含有”foo”那一行以外的所有行。首先,使用d2j删除前三行。之后跳过”foo”这一行,在其下一行使用点命令两次来删除剩下的六行。完整的命令是d2jj..

这里的修改操作是d2j2j不是一个移动(motion)操作,而是整个删除命令的一部分。

我们再来看看下一个例子:

1
2
3
4
zlet zzone = "1";
zlet zztwo = "2";
zlet zzthree = "3";
let four = "4";

我们的目标是删除所有的’z’。从第一行第一个字符开始,首先,在块可视化模式下使用Ctrl-vjj来选中前三行的第一个’z’字母。如果你对块可视化模式不熟悉的话也不用担心,我会在下一章节中进行介绍。在选中前三行的第一个’z’后,使用d来删除它们。接着用w移动到下一个z字母上,使用..重复两次之前选中加删除的动作。完整的命令为Ctrl-vjjdw..

你删除一列上的三个’z‘的操作(Ctrl-vjjd)被看做一整个修改操作。可视化模式中的选择操作可以用来选中多行,作为修改动作的一部分。

在修改中包含移动操作

让我们来重新回顾一下本章中的第一个例子。这个例子中我们使用了/letcwconst<esc>紧接着n . n .将下面的文本中的’let’都替换成了’const’。

1
2
3
let one = "1";
let two = "2";
let three = "3";

其实还有更快的方法来完成整个操作。当你使用/let搜索后,执行cgnconst<Esc>,然后. . .

gn是一个移动并选择的动作,它向前搜索和上一个搜索的模式(本例中为/let)匹配的位置,并且 自动对匹配的文本进行可视化模式下的选取。想要对下一个匹配的位置进行替换的话,你不再需要先移动在重复修改操作(n . n .),而是简单地使用. .就能完成。你不需要再进行移动操作了,因为找到下一个匹配的位置并进行选中成为了修改操作的一部分了。

当你在编辑文本时,应该时刻关注像gn命令这种能一下子做好几件事的移动操作。

(译者在这里研究了一会,并做了不少实验,总结规律是:单独的motion(第4章中所说的名词)不算修改操作,而opeartor(动词)+motion(名词)时(请回顾第4章),motion被视为一个完整的修改操作中的一部分。再看一个例子,看看/命令是如何被包含在一个修改操作中的:

1
2
3
4
5
6
7
8
a
b
foo
c
d
foo
e
f

假设你的光标在第一行的a上,执行命令d/foo<Esc>,Vim会删除a,b。然后.,Vim会删除foo, c, d,再按.,Vim什么也不做,因为后面没有”foo”了。在这个例子中,/foo是一个motion(名词),是Vim语法(动词+名词:operator + motion)的一部分,前面的d则是动词。d/foo<Esc>这条命令的功能是:从当前光标所在位置开始删除,直到遇到”foo”为止。后面的点命令就重复这个功能,第二次按.之所以Vim什么也不做,是因为找不到下一个匹配了,所以这条命令就失效了。

聪明地学习点命令

点命令的强大之处在于使用仅仅1次键盘敲击代替好几次敲击。对于x这种只需一次敲击键盘就能完成的修改操作来说,点命令或许不会带来什么收益。但是如果你的上一个修改操作是像cgnconst<esc>这种复杂命令的话,使用点命令来替代就有非常可观的收益了。

在进行编辑时,思考一下你正将进行的操作是否是可以重复的。举个例子,如果我需要删除接下来的三个单词,是使用d3w更划算,还是dw再使用.两次更划算?之后还会不会再进行删除操作?如果是这样的话,使用dw好几次确实比d3w更加合理,因为dw更加有复用性。在编辑时应该养成“修改操作驱动”的观念。

点命令非常简单但又功能强大,帮助你开始自动化处理简单的任务。在后续的章节中,你将会学习到如何使用Vim的宏命令来自动化处理更多复杂的操作。但是首先,还是让我们来学习一下如何使用寄存器来存取文本吧。

第8章 寄存器

学习Vim中的寄存器就像第一次学习线性代数一样,除非你学习了他们,否则你会觉得自己根本不需要它们。

你可能已经在复制或删除文本并用pP粘贴它们到别处的时候使用过Vim的寄存器了。但是,你知道Vim总共有10种不同类型的寄存器吗?如果正确地使用Vim寄存器,将帮助您从重复的输入中解放出来。

在这一章节中,我会介绍Vim的所有寄存器类型,以及如何有效地使用它们。

寄存器的10种类型

下面是Vim所拥有的10种寄存器类型:

  1. 匿名寄存器("").
  2. 编号寄存器("0-9).
  3. 小删除寄存器 ("-).
  4. 命名寄存器 ("a-z).
  5. 只读寄存器 (":, "., and "%).
  6. Buffer交替文件寄存器 ("#).
  7. 表达式寄存器 ("=).
  8. 选取和拖放寄存器("* and "+).
  9. 黑洞寄存器 ("_).
  10. 搜索模式寄存器 ("/).

寄存器命令

要使用寄存器,您必须先使用命令将内容存储到寄存器,以下是一些存值到寄存器中的操作:

1
2
3
y    复制
c 删除文本并进入输入模式
d 删除文本

其实还有更多的寄存器写入操作(比如sx),但是上面列出的是最常用的一些。根据经验看来,如果一个操作删除了文本,那么很有可能这个操作将移除的文本存入寄存器中了。

想要从寄存器中取出(粘贴)文本,你可以用以下的命令:

1
2
p    在光标位置之后粘贴文本
P 在光标位置之前粘贴文本

pP都可以接受计数和一个寄存器标志作为参数。比如,想要把最近复制的文本粘贴10次的话可以用10p。想粘贴寄存器”a”中的文本,可以用"ap。想将寄存器“a”中的文本粘贴10次的话,可以使用10"ap。注意,从技术层面讲,命令p实际上表示的是”put”(放置),而不是”paste”(粘贴),使用粘贴只是因为它更符合传统习惯。

从某个特定寄存器中读取文本的通用语法是"x,其中x是这个寄存器的标志。

在输入模式中使用寄存器

在这一章节中你学到的东西在输入模式中也同样适用。想要获取寄存器”a”中的文本,通常可以使用"ap来进行。不过当你在输入模式下时,你需要运行Ctrl-r a。在输入模式下使用寄存器的语法是:

1
Ctrl-r x

其中x是寄存器标志。既然你现在已经知道如何存储和访问寄存器了,让我们学点更深入的吧。

匿名寄存器("")

想从匿名寄存器中获取文本,可以使用""p。 匿名寄存器默认存储着你最近一次复制,修改或删除的文本。如果再进行另一次复制,修改或删除,Vim会自动替换匿名寄存器中的文本。匿名寄存器和电脑上粘贴板的功能很接近。

默认情况下,p(或者P)是和匿名寄存器相关联的(从现在起我将使用p而不是""p来指代匿名寄存器)。

编号寄存器("0-9)

编号寄存器会自动以升序来进行填充。一共有两种不同的编号寄存器:复制寄存器(0)和其他编号寄存器(1-9)。让我们先来讨论复制寄存器。

复制寄存器 ("0)

如果你使用yy来复制一整行文本,事实上Vim会将文本存放两个寄存器中:

  1. 匿名寄存器 (p).
  2. 复制寄存器 ("0p).

在你又复制其他不同的文本后,Vim会自动替换匿名寄存器和复制寄存器(0)中的内容。其他的任何操作都不会被存放在0号寄存器中。这可以为你提供方便,因为除非你再进行另一次复制,否则你已经复制的内容会一直在寄存器中,无论你进行多少次修改和删除。

比如,如果你:

  1. 复制一整行 (yy)
  2. 删除一整行(dd)
  3. 再删除另一行 (dd)

复制寄存器中的文本仍然是第一步中复制的文本。

如果你:

  1. 复制一整行 (yy)
  2. 删除一整行 (dd)
  3. 复制另一行 (yy)

复制寄存器中的内容则是第三步中复制的内容。

还有一个小技巧,在输入模式下,你可以使用Ctrl-r 0快速地粘贴你刚才复制的内容。

编号寄存器 ("1-9)

当你修改或者删除至少一整行的文本时,这部分文本会按时间顺序被存储在1-9号编号寄存器中。(==编号越小时间距离越近==)

比如,你有以下这些文本:

1
2
3
line three
line two
line one

当你的光标在文本“line three”上时,使用dd来一行一行地删除这些文本。在所有文本都已经删除后,1号寄存器中的内容应该是”line one”(时间上最近的文本), 2号寄存器则包含”line two”(时间上第二近的文本),3号寄存器中则包含”line three”(最早删除的文本)。普通模式下可以使用"1p来获取1号寄存器中的内容。

==编号寄存器的编号在使用点命令时会自动增加==。比如,如果你的1号编号寄存器("1)中的内容为”line one”, 2号寄存器("2)为”line two”, 三号寄存器("3)”line three”,你可以使用以下的技巧来连续地粘贴他们:

  • 使用"1p来粘贴1号寄存器中的内容。
  • 使用. (点命令)来粘贴2号寄存器("2)中的内容。
  • 使用. (点命令)来粘贴3号寄存器("3)中的内容。

在连续地使用点命令时,Vim会自动的增加编号寄存器的编号。这个技巧对于所有的编号寄存器都适用。如果你从5号寄存器开始("5P), 点命令.会执行"6P,再次使用.则会执行"7P,等等。

小型的删除比如单词删除(dw)或者单词修改(cw)不会被存储在编号寄存器中,它们被存储在小删除寄存器("-)中,我将在接下来的一小节讨论小删除寄存器。

小删除寄存器("-)

==不足一行的修改或者删除都不会被存储在0-9号编号寄存器中,而是会被存储在小删除寄存器 ("-)中。==

比如:

  1. 删除一个单词 (diw)
  2. 删除一行文本 (dd)
  3. 删除一行文本 (dd)

"-p 会给你第一步中删除的单词。

另一个例子:

  1. 删除一个单词(diw)
  2. 删除一行文本 (dd)
  3. 删除一个单词 (diw)

"-p 会给出第三步中删除的单词。类似地, "1p 会给出第二步中删除的一整行文本。不幸的是我们没有办法获取第一步中删除的单词,因为小删除寄存器只能存储一个文本。然而,如果你想保存第一步中删除的文本,你可以使用命名寄存器来完成。

命名寄存器 ("a-z)

命名寄存器是Vim中用法最丰富的寄存器。a-z命名寄存器可以存储复制的,修改的和被删除的文本。不像之前介绍的3种寄存器一样,它们会自动将文本存储到寄存器中,你需要显式地告诉Vim你要使用命名寄存器,你拥有完整的控制权。

为了复制一个单词到寄存器”a”中,你可以使用命令"ayiw

  • "a告诉Vim下一个动作(删除/修改/复制)会被存储在寄存器”a”中
  • yiw复制这个单词

为了从寄存器”a”中获取文本,可以使用命令"ap。你可以使用以26个字母命名的寄存器来存储26个不同的文本。

有时你可能会想要往已有内容的命名寄存器中继续添加内容,这种情况下,你可以追加文本而不是全部重来。你可以使用大写版本的命名寄存器来进行文本的追加。比如,假设你的”a”寄存器中已经存有文本”Hello”,如果你想继续添加”world”到寄存器”a”中,你可以先找到文本”world”然后使用"Aiw来进行复制,即可完成追加。

只读寄存器(":, "., "%)

Vim有三个只读寄存器:.,:%,它们的用法非常简单:

1
2
3
.    存储上一个输入的文本
: 存储上一次执行的命令
% 存储当前文件的文件名

如果你写入”Hello Vim”,之后再运行".p就会打印出文本”Hello Vim”。如果你想要获得当前文件的文件名,可以运行命令"%p。如果你运行命令:s/foo/bar/g,再运行":p的话则会打印出文本”s/foo/bar/g”。

Buffer交替文件寄存器 ("#)

在Vim中,#通常代表交替文件。交替文件指的是你上一个打开的文件,想要插入交替文件的名字的话,可以使用命令"#p

表达式寄存器 ("=)

Vim有一个表达式寄存器,"=,用于计算表达式的结果。

你可以使用以下命令计算数学表达式1+1的值:

1
"=1+1<Enter>p

在这里,你在告诉Vim你正在使用表达式寄存器"=,你的表达式是(1+1),你还需要输入p来得到结果。正如之前所提到的,你也可以在输入模式中访问寄存器。想要在输入模式中计算数学表达式的值,你可以使用:

1
Ctrl-r =1+1

你可以使用@来从任何寄存器中获取表达式并用表达式寄存器计算其值。如果你希望从寄存器”a”中获取文本:

1
"=@a

之后输入<enter>,再输入p。类似地,想在输入模式中得到寄存器”a”中的值可以使用:

1
Ctrl-r =@a

表达式是Vim中非常宏大的一个话题,所以我只会在这里介绍一些基础知识,我将会在之后的VimScript章节中进一步讲解更多关于表达式的细节。

选取和拖放寄存器 ("*, "+)

你难道不觉得有些时候你需要从某些外部的程序中复制一些文本并粘贴到Vim中吗,或者反过来操作?有了Vim的选取和拖放寄存器你就能办到。Vim有两个选取寄存器:quotestar ("*) 和 quoteplus ("+)。你可以用它们来访问从外部程序中复制的文本。

如果你在运行一个外部程序(比如Chrome浏览器),然后你使用Ctrl-c(或者Cmd-c,取决于你的操作系统)复制了一部分文本,通常你是没有办法在Vim里使用p来粘贴这部分文本的。但是,Vim的两个寄存器"+"*都是和你系统的粘贴板相连接的,所以你可以使用"+p"*p来粘贴这些文本。反过来,如果你使用"+yiw或者"*yiw在Vim中复制了一些文本,你可以使用Ctrl-v(或者Cmd-v)。值得注意的是这个方法只在你的Vim开启了+clipboard选项时才有用,可以在命令行中运行vim --version查看这一选项。如果你看见-clipboard的话,则需要安装一下支持Vim粘贴板的配置。

你也许会想如果"*"+能办到的事完全相同,那为什么Vim需要两个不同的寄存器呢?一些机器使用的是X11窗口系统,这一系统有3个类型的选项:首选,次选和粘贴板。如果你的机器使用的是X11的话,Vim使用的是quotestar ("*)寄存器作为X11的首选选项,并使用 quoteplus ("+)作为粘贴板选项。这只在你的Vim配置里开启了xterm_clipboard 选项时才有效(vim --version中的+xterm_clipboard)。如果你的的Vim配置中没有 xterm_clipboard也不是什么大问题。这只是意味着quotestarquoteplus两个寄存器是可以互相替代的。

我发觉使用=*p或者=+p的话比较麻烦,为了使Vim仅使用p就能粘贴从外部程序复制的文本,你可以在你的vimrc配置文件中加入下面一行:

1
set clipboard=unnamed

现在当我从外部程序中复制文本时,我可以使用匿名寄存器p来进行粘贴。我也可以在Vim中复制文本后在外部程序中使用Ctrl-v来粘贴。如果你的Vim开启了 +xterm_clipboard设置,你或许会想同时也使用unnamedunnamedplus的粘贴板选项。

黑洞寄存器 ("_)

你每次删除或修改文本的时候,这部分文本都会自动保存在Vim的寄存器中。有些时候你并不希望把什么东西都往寄存器里存,这该怎么办到呢?

你可以使用黑洞寄存器("_)。想要删除一行并且不将其存储在任何寄存器中时,可以使用"_dd命令.

它是和 /dev/null 类似的寄存器。

搜索模式寄存器 ("/)

为了粘贴你的上一个搜索询问(/?),你可以使用搜索模式寄存器("/)。使用命令 "/p就能粘贴上一个搜索的条目。

查看所有的寄存器

你可以使用:register命令来查看你的所有寄存器。如果你只想查看”a”,”1”和”-“寄存器的内容的话则可以使用命令:register a 1 -

有一个Vim的插件叫做 vim-peekaboo ,可以让你查看到寄存器的内容,在普通模式下输入"@ 即可,或者在输入模式中输入Ctrl-r。我发现这个插件相当的有用,因为大多数时候我是记不住我的寄存器中的内容的。值得一试!

执行寄存器

命名寄存器不只可以用来存放文本,你还可以借助@来执行宏命令。我会在下一章节中介绍宏命令。

注意,因为宏命令时存储在Vim寄存器中的,使用宏时可能会覆盖存储的内容。如果你将文本”Hello Vim”存放在寄存器”a”中,并且之后你在同一个寄存器里记录了一个宏命令 (qa{macro-commands}q),那么这个宏命令将会覆盖之前存储的文本”Hello Vim”(你可以使用@a来执行寄存器中存储的宏命令)。

清除寄存器

从技术上来说,我们没有必要来清除任何寄存器,因为你下一个使用来存储文本的寄存器会自动覆盖该寄存器中之前的内容。然而,你可以通过记录一个空的宏命令来快速地清除任何命名寄存器。比如,如果你运行qaq,Vim就会在寄存器”a”中记录一个空的宏命令。

还有一种方法就是运行命令:call setreg('a','hello register a'),其中’a’代表的就是寄存器”a”。而”hello register a”就是你想存储的内容。

还有一种清除寄存器的方法就是使用表达式:let @a = ''来将寄存器”a 的值设为空的字符串。

获取寄存器中的内容

你可以使用:put命令来粘贴任何寄存器的内容。比如,如果你运行命令:put a,Vim就会打印出寄存器”a”的内容,这和"ap非常像,唯一的区别在于在普通模式下命令p在当前光标位置之后打印寄存器的内容,而:put新起一行来打印寄存器的内容。

因为:put是一个命令行命令,您可以传一个地址给它。:10put a将会在当前光标下数10行,然后插入新行,内容为寄存器a中的内容。

一个很酷的技巧是将黑洞寄存器("_)传给:put命令。因为黑洞寄存器不保存任何值,:put _命令将插入一个新的空白行。您可将这个与全局命令联合起来,插入多个空行。比如,要在所有以文本”end”结尾的行下插入空行,使用:g/end/put _。在后面您将了解关于全局命令的知识。

聪明地学习寄存器

恭喜你成功地坚持到了最后!这一章有非常多的内容需要消化。如果你感觉被新的知识淹没,你要知道你并不孤单,当我最初开始学习Vim寄存器时也有这种感觉。

我并不认为你必须现在就记得所有的知识点。为了提高我们的生产效率,你可以从使用以下三类寄存器开始:

  1. 匿名寄存器("").
  2. 命名寄存器 ("a-z).
  3. 编号寄存器 ("0-9).

既然匿名寄存器是默认和pP,你只需要学习两个寄存器:命名寄存器和编号寄存器。之后如果你需要用到其他的寄存器时你再逐渐地学习其他寄存器的用法,不用急,慢慢来。

普通人的短期记忆都是有极限的,大概每次只能记住5-7个信息。这就是为什么在我的日常编辑中,我只用3到7个命名寄存器的原因,我没有办法记住整整26个寄存器的内容。我通常从寄存器”a”开始用,之后用寄存器”b”,以字母表升序的顺序来使用。尝试一下各种方法,看看哪种最适合你。

Vim寄存器非常强大,合理使用的话能够避免你输入数不清的重复文本。但是现在,是时候学习一下宏命令了。

第9章 宏命令

在编辑文件的时候,你会发现有时候你在反复地做一些相同的动作。如果你仅做一次,并在需要的时候调用这些动作岂不是会更好吗。通过 Vim 的宏命令,你可以将一些动作记录到 Vim 寄存器。

在本章中,你将会学习到如何通过宏命令自动完成一些普通的任务(另外,看你的文件在自动编辑是一件很酷的事情)。

基本宏命令

宏命令的基本语法如下:

1
2
qa                     开始记录动作到寄存器 a
q (while recording) 停止记录

你可以使用小写字母 (a-z)去存储宏命令。并通过如下的命令去调用:

1
2
@a    Execute macro from register a
@@ Execute the last executed macros

假设你有如下的文本,你打算将每一行中的所有字母都变为大写。

1
2
3
4
5
hello
vim
macros
are
awesome

将你的光标移动到 “hello” 栏的行首,并执行:

1
qa0gU$jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • gU$ 将从光标到行尾的字母变为大写。
  • j 移动到下一行。
  • q 停止记录。

调用 @a 去执行该宏命令。就像其他的宏命令一样,你也可以为该命令加一个计数。例如,你可以通过 3@a 去执行 a 命令3次。你也可以执行 3@@ 去执行上一次执行过的宏命令3次。

安全保护

在执行遇到错误的时候,宏命令会自动停止。假如你有如下文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

你想将每一行的第一个词变为大写,你可以使用如下的宏命令:

1
qa0W~jq

上面命令的分解如下:

  • qa 开始记录一个宏定义并存储在 a 寄存器。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 将光标选中的单词变为大写。
  • j 移动到下一行。
  • q 停止记录。

我喜欢对宏命令进行超过所需次数的调用,所以我通常使用 99@a 命令去执行该宏命令99次。使用该命令,Vim并不会真正执行这个宏99次,当 Vim 到达最后一行执行j时,它会发现无法再向下了,然后会抛出一个错误,并终止宏命令的执行。

实际上,遇到错误自动停止运行是一个很好的特性。否则,Vim 会继续执行该命令99次,尽管它已经执行到最后一行了。

命令行执行宏

在正常模式执行 @a 并不是宏命令调用的唯一方式。你也可以在命令行执行 :normal @a:normal 会将任何用户添加的参数作为命令去执行。例如添加 @a,和在 normal mode 执行 @a 的效果是一样的。

:normal 命令也支持范围参数。你可以在选择的范围内去执行宏命令。如果你只想在第二行和第三行执行宏命令,你可以执行 :2,3 normal @a

在多个文件中执行宏命令

假如你有多个 .txt 文件,每一个文件包含不同的内容。并且你只想将包含有 “donut” 单词的行的第一个单词变为大写。假设,您的寄存器a中存储的内容是0W~j(就是前面例子中用到的宏命令),那么,您该如何快速完成这个操作呢?

第一个文件:

1
2
3
4
## savory.txt
a. cheddar jalapeno donut
b. mac n cheese donut
c. fried dumpling

第二个文件:

1
2
3
4
## sweet.txt
a. chocolate donut
b. chocolate pancake
c. powdered sugar donut

第三个文件:

1
2
3
## plain.txt
a. wheat bread
b. plain donut

你可以这么做:

  • :args *.txt 查找当前目录下的所有 .txt 文件。
  • :argdo g/donut/normal @a:args 中包含的每一个文件里执行一个全局命令 g/donut/normal @a
  • :argdo update:args 中包含的每一个文件里执行 update 命令,保存修改后的内容。

也许你对全局命令 :g/donut/normal @a 不是很了解,该命令会执行 /donut/搜索命令,然后在所有匹配的行中执行normal @a 命令。我会在后面的章节中介绍全局命令。

递归执行宏命令

你可以递归地执行宏命令,通过在记录宏命令时调用相同的宏寄存器来实现。假如你有如下文本,你希望改变第一个单词的大小写:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

如下命令会递归地执行:

1
qaqqa0W~j@aq

上面命令的分解如下:

  • qaq 记录一个空白的宏命令到 “a” 。把宏命令记录在一个空白的命令中是必须的,因为你不会想将该命令包含有任何其他的东西。
  • qa 开始录入宏命令到寄存器 “a”。
  • 0 移动到行首。
  • W 移动到下一个单词。
  • ~ 改变光标选中的单词的大小写。
  • j 移动到下一行。
  • @a 执行宏命令 “a”。当你记录该宏命令时,@a 应该是空白的,因为你刚刚调用了 qaq
  • q 停止记录。

现在,让我们调用 @a 来查看 Vim 如何递归的调用该宏命令。

宏命令是如何知道何时停止呢?当宏执行到最后一行并尝试 j 命令时,发现已经没有下一行了,就会停止执行。

增添一个已知宏

如果你想在一个已经录制好的宏定义中添加更多的操作,与其重新录入它,不如选择修改它。在寄存器一章中,你学习了如何使用一个已知寄存器的大写字母来想该寄存器中添加内容。同样的,为了在寄存器”a”中添加更多的操作,你也可以使用大写字母”A”。

假设寄存器a中已经存储了这个宏命令:qa0W~q(该宏命令将某行的第二个词组的头一个字母执行改变大小写操作),假设你想在这个基础上添加一些操作命令序列,使得每一行末尾添加一个句点,运行:

1
qAA.<esc>q

分解如下:

  • qA 开始在寄存器 “A” 中记录宏命令。
  • A.<esc> 在行的末尾加上一个句点(这里的A是进入插入模式,不要和宏A搞混淆),然后退出插入模式。
  • q 停止记录宏命令。

现在,当你执行@a时,它不仅将第二个词组的首字母转变大小写,同时还在行尾添加一个句点。

修改一个已知宏

如果想在一个宏的中间添加新的操作该怎么办呢?

假设您在寄存器a中已经存有一个宏命令0W~A.<Esc>,即改变首字母大小写,并在行尾添加句号。如果您想在改变首字母大小写和行尾添加句号之间,在单词”dount”前面加入”deep fried”。(因为唯一比甜甜圈好的东西就是炸甜甜圈)。

我会重新使用上一节使用过的文本:

1
2
3
4
a. chocolate donut
b. mochi donut
c. powdered sugar donut
d. plain donut

首先,让我们通过 :put a 调用一个已经录制好的宏命令(假设你上一节中保存在寄存器a中的宏命令还在):

1
0W~A.^[

^[ 是什么意思呢?不记得了吗,你之前执行过 0W~A.<esc>^[ 是 Vim 的 内部指令,表示 <esc>。通过这些指定的特殊键值组合,Vim 知道这些是内部代码的一些替代。一些常见的内部指令具有类似的替代,例如 <esc><backspace><enter>。还有一些其他的键值组合,但这不是本章的内容。

回到宏命令,在改变大小写之后的键后面(~),让我们添加($)来移动光标到行末,回退一个单词(b),进入插入模式(i),输入”deep fried “ (别忽略”fried “后面的这个空格),之后退出插入模式(<esc>)。

完整的命令如下:

1
0W~$bideep fried <esc>A.^[

这里有一个问题,Vim 不能理解 <esc>。您不能依葫芦画瓢输入”“,所以你需要将<Esc>写成内部代码的形式。在插入模式,在按下<esc>后按下 Ctrl-v,Vim 会打印 ^[。 ==Ctrl-v 是一个插入模式的操作符,可以逐字地插入一个非数字字符==。你的宏命令应该如下:

1
0W~$bideep fried ^[A.^[

为了在寄存器“a”中添加修改后的指令,你可以通过在一个已知命名寄存器中添加一个新条目的方式来实现。在一行的行首,执行 "ay$,使用寄存器 “a”来存储复制的文本。

现在,但你执行 @a 时,你的宏命令会自动改变第一个单词的大小写,在”donut”前面添加”deep fried “,之后在行末添加“.”。

另一个修改宏命令的方式是通过命令行表达式。执行 :let @a=",之后执行 Ctrl-r Ctrl-r a,这会将寄存器“a”的命令逐字打印出来。最后,别忘记在闭合的引号(")。如果你希望在编辑命令行表达式时插入内部码来使用特定的字符,你可以使用 Ctrl-v

拷贝宏

你可以很轻松的将一个寄存器的内容拷贝到另一个寄存器。例如,你可以使用 :let @z = @a 将寄存器”a” 中的命令拷贝到寄存器”z”。 @a 表示寄存器“a”中存储的内容,你现在执行 @z,将会执行和 @a 一样的指令。

我发现对常用的宏命令创建冗余是很有用的。在我的工作流程中,我通常在前7个字母(a-g)上创建宏命令,并且我经常不加思索地把它们替换了。因此,如果我将很有用的宏命令移动到了字母表的末尾,就不用担心我在无意间把他们替换了。

串行宏和并行宏

Vim 可以连续和同时运行宏命令,假设你有如下的文本:

1
2
3
4
5
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

假如你希望把所有的 “FUNC” 字符变为小写,那么宏命令为如下:

1
qa0f{gui{jq

分解如下:

  • qa 开始记录宏命令到 “a” 寄存器。
  • 0移动到第一行。
  • f{ 查找第一个 “{” 字符。
  • gui{ 把括号内的文本(i{)变为小写(gu)。
  • j 移动到下一行。
  • q 停止记录宏命令。

现在,执行 99@a 在剩余的行修改。然而,假如在你的文本里有如下 import 语句会怎么样呢?

1
2
3
4
5
6
import { FUNC1 } from "library1";
import { FUNC2 } from "library2";
import { FUNC3 } from "library3";
import foo from "bar";
import { FUNC4 } from "library4";
import { FUNC5 } from "library5";

执行 99@a,会只在前三行执行。而最后两行不会被执行,因为在执行第四行(包含“foo”)时f{命令会遇到错误而停止,当宏串行执行时就会发生这样的情况。当然,你仍然可以移动到包含(“FUNC4”)的一行,并重新调用该命令。但是假如你希望仅调用一次命令就完成所有操作呢?

你可以并行地执行宏命令。

如本章前面所说,可以使用 :normal 去执行宏命令,(例如: :3,5 normal @a 会在 3-5行执行 a 寄存器中的宏命令)。如果执行 :1,$ normal @a,会在所有除了包含有 “foo” 的行执行,而且它不会出错。

尽管本质上来说,Vim 并不是在并行地执行宏命令,但表面上看,它是并行运行的。 Vim 会独立地在从第一行开始(1,$)每一行执行 @a 。由于 Vim 独立地在每一行执行命令,每一行都不会知道有一行(包含“foo”)会遇到执行错误。

聪明地学习宏命令

你在编辑器里做的很多事都是重复的。为了更好地编辑文件,请乐于发现这些重复性的行为。执行宏命令或者点命令,而不是做相同的动作两次。几乎所有你在 Vim 所作的事情都可以变为宏命令。

刚开始的时候,我发现宏命令时很棘手的,但是请不要放弃。有了足够的练习,你可以找到这种文本自动编辑的快乐。

使用某种助记符去帮助你记住宏命令是很有帮助的。如果你有一个创建函数(function)的宏命令,你可以使用 “f” 寄存器去录制它(qf)。如果你有一个宏命令去操作数字,那么使用寄存器 “n” 去记住它是很好的(qn)。用你想执行的操作时想起的第一个字符给你的宏命令命名。另外,我发现 “q” 是一个很好的宏命令默认寄存器,因为执行 qq 去调用宏命令是很快速而简单的。最后,我喜欢按照字母表的顺序去添加我的宏命令,例如从 qaqb 再到 qc

去寻找最适合你的方法吧。

第10章 撤销

所有人都会犯各种各样的输入错误。因此对于任何一个现代的软件来说,撤销都是一个很基本的功能。 Vim 的撤销系统不仅支持撤销和取消撤销任何修改,而且支持存取不同的文本形态,让你能控制你输入的所有文本。在本章中,你将会学会如何执行撤销和 取消撤销文本,浏览撤销分支,反复撤销, 以及浏览改动时间线。

撤销(undo),重做和行撤销(UNDO)

对于一个基本的 undo 操作,你可以执行 u 或者 :undo

假设你有如下文本(注意”one”下面有一个空行):

1
2
one

然后添加另一个文本:

1
2
one
two

如果你执行 u,Vim 会删除 “two”。

Vim 是如何知道应该恢复多少修改呢? 答案是,Vim每次仅恢复一次修改,这有点类似于点命令的操作(和 点命令不同之处在于,命令行命令也会被算作一次修改)。

要取消上一次的撤销,可以执行 Ctrl-r 或者 :redo。例如上面的例子中,当你执行撤销来删除 “two” 以后,你可以执行 Ctrl-r 来恢复被删除掉的文本。

Vim 也有另一个命令 U 可以实现 行撤销 (UNDO) 的功能,执行这个命令会撤销所有最新的修改。

那么,Uu 的区别是什么呢?首先,U 会删除 最近修改的行中所有的 的修改,而 u 一次仅删除一次修改。 其次,执行u 不会被算作一次修改操作,而执行 U 则会被算作一次修改。

让我们会的之前的例子:

1
2
one
two

修改第二行的内容为 “three” (ciwthree<esc>):

1
2
one
three

再次修改第二行的例子为 “four” (ciwfour<esc>):

1
2
one
four

此时,如果你按下 u,你会看到 “three”。如果你再次按下 u,你会看到 “two”。然而,在第二行仍为 “four” 的时候,如果你按下 U,你会看到

1
2
one

执行 U 会跳过中间所有修改,直接恢复到文件最初的状态(第二行为空)。另外,由于 UNO 实际上是执行了一个新的修改,因此你可以 UNDO 执行过的 UNDO。 执行 U 后 再次执行 U 会撤销 自己。假如你连续执行 U,那么你将看到第二行的文本不停地出现和消失。

就我个人而言,我几乎不会使用 U,因为很难记住文本最初的样子。(我几乎不使用它)

Vim 可以通过变量 undolevels 来选择最多可执行 undo 的次数。你可以通过 :echo &undolevels 来查看当前的配置。我一般设置为 1000。如果你也想设置为 1000 的话,你可以执行 :set undolevels=1000。不用担心,你可以设置它为任何一个你想设置的值。

断点插入操作

在上文中我提到,u 每次恢复一个修改,类似于点命令。在每次进入 插入模式和退出插入模式之间的任何修改都被定义为一次修改。

如果你执行 ione two three<esc> 之后,按下 u,Vim 会同时删除 “one two three”,因为这是一笔修改。如果你每次只输入较短的文本,那这是可接受的;可假设你在一次插入模式中输入了大量的文本,而后退出了插入模式,可很快你意识到这中间有部分错误。此时,如果你按下 u,你会丢失上一次输入的所有内容。 因此,假设你按下 u 只删除你上一次输入的一部分文本岂不是会更好。

幸运的是,你可以拆分它。当你在插入模式时,按下 Ctrl-G u 会生成一个断点。例如,如果你执行 ione <Ctrl-G u>two <Ctrl-G u>three<esc>,之后你按下u,你仅会失去文本 “three”,再次执行 u,会删除 “two”。当你想要输入一长段内容时,应该有选择性地执行断点插入操作。==在每一句话的末尾,两个段落的中间,或者每一行代码结束时插入断点是一个很好的选择,这可以帮助你快速从错误中恢复出来。==

在插入模式中,执行删除操作时插入断点也非常有用。例如通过 Ctrl-W 删除光标前的单词时,以及 Ctrl-U删除光标前的所有文本时。一个朋友建议我使用如下的映射:

1
2
inoremap <c-u> <c-g>u<c-u>
inoremap <c-w> <c-g>u<c-w>

通过上述命令,你可以很轻松地恢复被删除的文本。

撤销树

Vim 将每一次修改存储在一个撤销树中。你打开一个空白文件,然后添加一段新文本:

1
2
one

再插入一段新文本:

1
2
one
two

undo一次:

1
2
one

插入一段不同的话:

1
2
one
three

再次 undo

1
2
one

再次插入另一段话:

1
2
one
four

现在如果你执行 undo,您将丢失刚刚添加的文本 “four” :

1
2
one

如果你再次执行 undo 操作:

1

文本 “one” 也会丢失。对于大部分编辑器来说,找回文本 “two” 和 “three” 都是不可能的事情,但是对于 Vim 来说却不是这样。执行 g+,你会得到:

1
2
one

再次执行 g+ ,你将会看到一位老朋友:

1
2
one
two

让我们继续执行 g+:

1
2
one
three

再一次执行 g+ :

1
2
one
four

在 Vim 中,你每一次执行 u 去做一次修改时,Vim都会通过创建一个”撤销分支”来保存之前的文本内容。在本例中,你输入”two”后, 执行 u,然后输入”three”,你就创建了一个叶子分支,保存了含有”two”的文本状态。此时,撤销树已经包含了至少两个叶子节点,主节点包含文本”three”(最新),而另一undo分支节点包含文本“two”。假如你执行了另一次撤销操作并且输入了”four”,那么此时会生成三个节点,一个主节点包含文本”four”, 以及另外两个节点分别存储了”three”和”two”。

为了在几个不同的节点状态间进行切换,你可以执行 g+ 去获取一个较新的状态,以及执行 g- 去获取一个教旧的状态。 uCtrl-Rg+, 和 g- 之间的区别是,u and Ctrl-R 只可以在 main 节点之间进行切换,而g+g- 可以在 所有 节点之间进行切换。

Undo 树并不可以很轻松地可视化。我发现一个插件 vim-mundo 对于理解 undo 树很有帮助。花点时间去与它玩耍吧。

保存撤销记录

当你通过 Vim 打开一个文件,并且立即按下 u,Vim 很可能会显示 “Already at oldest change” 的警告。

要想从最近的一次编辑工作中(在vim关闭文件再打开,算做一次新的编辑工作),在撤销历史中回滚,可以通过 :wundo命令使Vim 保存一份你的 undo 历史记录。

创建一个文件 mynumbers.txt. 输入:

1
one

插入另一行文件 (确保你要么退出并重新进入插入模式,要么创建了断点):

1
2
one
two

插入新的一行:

1
2
3
one
two
three

现在,创建你的撤销记录文件。 语法为 :wundo myundofile。 如果你需要覆盖一个已存在的文件,在 wundo 之后添加 !.

1
:wundo! mynumbers.undo

退出 Vim。

此时,在目录下,应该有mynumbers.txtmynumbers.undo 两个文件。再次打开 mynumbers.txt 文件并且按下 u,这是没有响应的。因为自打开文件后,你没有执行任何的修改。现在,通过执行 :rundo 来加载 undo 历史。

1
:rundo mynumbers.undo

此时,如果你按下 u,Vim 会删除 “three”。再次按下 u可以删除 “two”。这就好像你从来没有关闭过 Vim 一样。

如果你想要自动加载 undo 历史文件,你可以通过在你的 .vimrc 文件中添加如下代码:

1
2
set undodir=~/.vim/undo_dir
set undofile

我认为将所有的 undo 文件集中保存在一个文件夹中最好,例如在 ~/.vim 目录下。 undo_dir 是随意的。 set undofile 告诉 Vim 打开 undofile 这个特性,因为该特性默认是关闭的。现在,无论你何时保存,Vim 都会自动创建和保存撤销的历史记录(在使用undo_dir目录前,请确保你已经创建了它)。

时间旅行

是谁说时间旅行不存在。 Vim 可以通过 :earlier 命令将文本恢复为之前的状态。

假如有如下文本:

1
2
one

之后你输入了另一行:

1
2
one
two

如果你输入 “two” 的时间少于10秒,那么你可以通过如下命令恢复到 “two” 还没被输入前的状态:

1
:earlier 10s

你可以使用 :undolist 去查看之前所做的修改。 :earlier 可以加上分钟 (m), 小时 (h), and 天 (d) 作为参数。

1
2
3
4
5
:earlier 10s    恢复到10秒前的状态
:earlier 10m 恢复到10分钟前的状态
:earlier 10h 恢复到10小时前的状态
:earlier 10d 恢复到10天前的状态

另外,它同样接受一个计数作为参数,告诉vim恢复到老状态的次数。比如,如果运行:earlier 2,Vim将恢复到2次修改前的状态。功能类似于执行g-两次。同样,你可以运行:earlier 10f命令告诉vim恢复到10次保存前的状态。

这些参数同样作用于:earlier命令的对应版本::later

1
2
3
4
5
6
:later 10s    恢复到10秒后的状态
:later 10m 恢复到10分钟后的状
:later 10h 恢复到10小时后的状
:later 10d 恢复到10天后的状态
:later 10 恢复到新状态10次
:later 10f 恢复到10次保存后的状态

聪明地学习撤销操作

uCtrl-R 是两个不可缺少的 Vim 参数。请先学会它们。在我的工作流中,我并不使用 UNDO,然而我认为承认它存在是很好的。下一步,学会如何使用:earlier:later,以及时间参数。在这之后,请花些时间理解 undo 树。 插件 vim-mundo 对我的帮助很大。单独输入本章中展示的文本,并且查看撤销树的每一次改变。一旦你掌握它,你看待撤销系统的眼光一定不同。

在本章之前,你学习了如何在项目内查找任何文本,配合撤销,你可以在时间维度上查找任何一个文本。你现在可以通过位置和写入时间找到任何一个你想找的文本。你已经对 Vim 无所不能了。

第11章 可视模式

高亮显示文本块并对其进行更改,是很多文本编辑器中的常见功能。 Vim也可以使用可视模式实现这一功能。在本章中,您将学习如何使用可视模式来有效地处理文本块。

三种可视模式

Vim有三种可视模式,分别是:

1
2
3
v         逐字符可视模式
V 逐行可视模式
Ctrl-v 逐块可视模式

如果您有下列文字:

1
2
3
one
two
three

逐字符可视模式用于选择单个字符。在第一行的第一个字符上按v。然后使用j跳转至下一行。它高亮显示从”one”到光标位置的所有文本。现在,如果您按gU,Vim将高亮显示的字符转为大写。

逐行可视模式适用于整行。按V并观看Vim选择光标的所在行。就像逐字符可视模式一样,如果您运行gU,Vim将高亮显示的字符转为大写。

逐块可视模式适用于行和列。与其他两种模式相比,它为您提供了更大的移动自由度。按Ctrl-V,Vim像逐字符可视模式一样高亮显示光标下的字符,但向下移动时,除非光标已经在行尾,否则不会高亮显示光标上方的整行,它跳转至下一行时高亮显示尽可能少的字符。尝试用h/j/k/l移动,并观察光标的移动。

在Vim窗口的左下方,您会看到显示-- VISUAL ---- VISUAL LINE ---- VISUAL BLOCK --以提示您所处的可视模式。

当您处于可视模式时,可以通过按vVCtrl-V键切换到另一种可视模式。例如,如果您处于逐行可视模式,并且想要切换为逐块可视模式,请运行Ctrl-V。试试吧!

有三种退出可视模式的方法:escCtrl-C和与当前可视模式相同的键。后者的意思是,如果您当前处于逐行可视模式(V),则可以通过再次按V退出它。如果您处于字符可视模式,则可以通过按v退出它。如果您处于逐块可视模式,请按Ctrl-V

实际上,还有另一种进入可视模式的方式:

1
gv    转到上一个可视模式

它将在与上次相同的高亮显示的文本块上启动相同的可视模式。

可视模式导航

在可视模式下,您可以使用Vim动作(motion)扩展高亮显示的文本块。

让我们使用之前使用的相同文本:

1
2
3
one
two
three

这次让我们从”two”行开始。按v进入字符可视模式(这里的方括号[]表示高亮显示的字符):

1
2
3
one
[t]wo
three

j,Vim将高亮显示从”two”行到”three”行的第一个字符的所有文本。

1
2
3
one
[two
t]hree

假设您刚刚意识到还需要高亮显示”one”行,因此按k。令您沮丧的是,它现在排除了”three”高亮。

1
2
3
one
[t]wo
three

有没有一种方法可以自由地扩展视觉选择范围,以向您想要的任何方向发展?

答案是肯定的。让我们先恢复光标到高亮显示”two”和”three”行的位置。

1
2
3
one
[two
t]hree <-- 光标

高亮区域跟随光标移动。如果要将其向上扩展到行”one”,则需要将光标移动到”two”,现在您的光标在”three”行上。这时可以用oO切换光标位置。

1
2
3
one
[two <-- 光标
t]hree

现在,当您按k时,它不再缩小选择,而是向上扩展。

1
2
3
[one
two
t]hree

在可视模式中使用oO,光标会在高亮选择区域的开头和结尾跳转,以便与您扩展高亮区域。

可视模式语法

可视模式与普通模式使用相同的操作符(operations)。

例如,如果您有以下文字,然后您想在可视模式中删除前两行:

1
2
3
one
two
three

用逐行可视模式(V)高亮显示”one”和”two”行:

1
2
3
[one
two]
three

按下d键将删除选择,类似于普通模式。请注意,与普通模式的语法规则有所不同,动词+名词不适用可视模式。虽然存在相同的动词(d),但在可视模式下没有名词。可视模式下的语法规则是名词+动词(反过来了),其中名词是高亮显示的文本。首先选择文本块,然后进行操作。

在普通模式下,有一些命令不需要名词(motion),例如x删除光标下方的单个字符,还有r替换光标下方的字符(rx将当前光标下的字符替换为x)。在可视模式下,这些命令现在将应用于整个高亮显示的文本,而不是单个字符。回到高亮显示的文本:

1
2
3
[one
two]
three

运行x会删除所有高亮显示的文本。

您可以使用此行为在markdown文本中快速创建标题。假设您需要快速下面的文本转换为一级markdown标题(“===”):

1
Chapter One

首先,您使用yy复制文本,然后使用p粘贴文本:

1
2
Chapter One
Chapter One

现在转到第二行,以逐行可视模式选择它:

1
2
Chapter One
[Chapter One]

在markdown中,您可以通过在文本下方添加一系列=来创建标题,因此您可以通过运行r=来替换整个高亮显示的文本:

1
2
Chapter One
===========

要了解有关可视模式下的运算符的更多信息,请查看:h visual-operators

可视模式和Ex命令

您可以有选择地在高亮显示的文本块上应用Ex命令。如果您具有以下表达式,并想将前两行的”const”替换为”let”:

1
2
3
const one = "one";
const two = "two";
const three = "three";

任意 可视模式高亮显示前两行,然后运行替换命令:s/const/let/g

1
2
3
let one = "one";
let two = "two";
const three = "three";

请注意,我说过您可以使用 任何 可视模式执行此操作。您不必高亮显示整个行即可在该行上运行Ex命令。==只要您在每行上至少选择一个字符,就会应用Ex命令。==

跨多行编辑

您可以使用逐块可视模式在Vim中跨多行编辑文本。如果需要在每行末尾添加分号:

1
2
3
const one = "one"
const two = "two"
const three = "three"

将光标放在第一行上:

  • 进入逐块可视模式,并向下两行(Ctrl-V jj)。
  • 高亮显示到行尾($)。
  • 按(A) ,然后键入”;”。
  • 退出可视模式(esc)。

您应该看到在每一行后面附加的 “;”。666! 有两种方法可以从逐块可视模式进入输入模式:可以使用A在光标后输入文本,也可以使用I在光标前输入文本。请勿将它们与普通模式下的AI混淆。(普通模式中,A表示在行尾添加内容,I表示在行首非空字符前插入内容)。

另外,您也可以使用:normal命令在多行添加内容:
-高亮显示所有3行(vjj)。
-输入:normal! A;

记住,:normal命令执行普通模式命令。您可以指示它运行A;在该行的末尾添加文本”;”。

递增数字

==Vim有Ctrl-XCtrl-A命令来减少和增加数字==。与可视模式一起使用时,可以跨多行递增数字。

如果您具有以下HTML元素:

1
2
3
4
5
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>
<div id="app-1"></div>

有多个具有相同名称的id是一个不好的做法,因此让我们对其进行递增以使其唯一:

  • 将光标移动到 第二行的 “1”。
  • 启动逐块可视模式,并向下移动3行(Ctrl-V 3j)。这高亮显示剩余的”1”,现在除了第一行,所有的”1”应该已经高亮。
  • 运行g Ctrl-A

您应该看到以下结果:

1
2
3
4
5
<div id="app-1"></div>
<div id="app-2"></div>
<div id="app-3"></div>
<div id="app-4"></div>
<div id="app-5"></div>

g Ctrl-A在多行上递增数字。 Ctrl-X/Ctrl-A也可以增加字母。如果您运行:

1
:set nrformats+=alpha

nrformats选项指示Vim将哪个基数视为Ctrl-ACtrl-X递增和递减的“数字”。通过添加alpha,现在将字母字符视为数字。如果您具有以下HTML元素:

1
2
3
4
5
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>
<div id="app-a"></div>

将光标放在第二个”app-a”上。使用与上述相同的技术(Ctrl-V 3j 然后 g Ctrl-A)增加ID。

1
2
3
4
5
<div id="app-a"></div>
<div id="app-b"></div>
<div id="app-c"></div>
<div id="app-d"></div>
<div id="app-e"></div>

选择最后一个可视模式区域

前面章节中我提到了gv可以快速高亮显示上一个可视模式选择的内容。您还可以使用以下两个特殊标记转到最后一个可视模式的开始和结束位置:

1
2
'<    转到上一个可视模式高亮显示的第一个位置(行)(译者注,英文原版中'<'前面的符号是`,但这应该是一个错误,应该是单引号')
'> 转到上一个可视模式高亮显示的最后位置(行)

之前,我提到过您可以在高亮显示的文本上有选择地执行Ex命令,例如::s/const/let/g。当您这样做时,您应该看到以下内容:

1
:'<,'>s/const/let/g

您实际上是在使用('<, '>) 标记作为范围来执行 s/const/let/g命令。这太有趣了!

您随时可以随时编辑这些标记。比如,如果您需要从高亮显示的文本的开头到文件的末尾进行替换,则只需将命令行更改为:

1
:'<,$s/const/let/g

从插入模式进入可视模式

您也可以从插入模式进入可视模式。在插入模式下进入字符可视模式:

1
Ctrl-O v

回想一下,在插入模式下运行Ctrl-O可以使您执行普通模式命令。在普通模式命令挂起模式下,运行v进入逐字可视模式。请注意,在屏幕的左下方,它显示为--(insert) VISUAL--。该技巧适用于任何可视模式运算符:vV,和Ctrl-V

选择模式

Vim具有类似于可视模式的模式,称为选择模式。与可视模式一样,它也具有三种不同的模式:

1
2
3
gh         逐字符选择模式
gH 逐行选择模式
gCtrl-h 逐块选择模式

选择模式比Vim的可视模式更接近常规编辑器的文本高亮显示行为。

在常规编辑器中,高亮显示文本块并键入字母(例如字母”y”)后,它将删除高亮显示的文本并插入字母”y”。如果您使用逐行选择模式(gH)高亮显示一行文本并键入”y”,它将删除高亮显示的文本并插入字母”y”,这与常规文本编辑器非常相似。

将此行为与可视模式进行对比:如果您使用逐行可视模式(V)高亮显示一行文本并键入”y”,则高亮显示的文本不会被删除且被字母”y”代替,而是仅将其复制(yank)。在选择模式中,你不能执行对高亮文本执行普通模式的命令。

我个人从未使用过选择模式,但是很高兴知道它的存在。

以聪明的方式学习可视模式

可视模式是Vim高亮显示文本的过程。

如果发现使用可视模式操作的频率比正常模式操作的频率高得多,请当心。==我认为这是一种反模式。运行可视模式操作所需的击键次数要多于普通模式下的击键次数。==假设您需要删除一个内部单词(inner word,请回顾前面的文本对象),如果可以只用三个按键(diw),为什么要使用四个按键viwd(先v进入可视模式,然后iw高亮一个内部单词,最后d删除)呢?前者更为直接和简洁。当然,有时使用可视模式是合适的,但总的来说,更倾向于直接的方法。

第12章 搜索和替换

本章涵盖两个独立但相关的概念:搜索和替代。很多时候,您得基于文本的共同模式搜索大量的内容。通过学习如何在搜索和替换中使用正则表达式而不是字面字符串,您将能够快速定位任何文本。

附带说明一下,在本章中,当谈论搜索时,我将主要使用/。您使用/进行的所有操作也可以使用?完成。

智能区分大小写

尝试匹配搜索词的大小写可能会很棘手。如果要搜索文本”Learn Vim”,则很容易把字母的大小写输错,从而得到错误的搜索结果。如果可以匹配任何情况,会不会更轻松,更安全?这是选项ignorecase闪亮的地方。只需在 vimrc 中添加set ignorecase,所有搜索词就不区分大小写。现在,您不必再执行/Learn Vim了。 /learn vim将起作用。

但是,有时您需要搜索特定大小写的短语。一种方法是用 set noignorecase 关闭ignorecase选项,但是每次需要搜索区分大小写的短语时,都得反复地打开和关闭这个选项。

为避免反复开关ignorecase选项,Vim 有一个smartcase选项。您可以将ignorecasesmartcase选项结合起来,当您输入的搜索词全部是小写时,进行大小写不敏感搜索;而当搜索词 至少有1个大写字母时,进行大小写敏感搜索。

在您的 vimrc 中,添加:

1
set ignorecase smartcase

如果您有这些文字:

1
2
3
hello
HELLO
Hello
  • /hello 匹配”hello”,”HELLO”和”Hello”。
  • /HELLO 仅匹配”HELLO”。
  • /Hello 仅匹配”Hello”。

有一个缺点。因为现在当您执行/hello时,Vim 将进行大小写不敏感搜索,那如果只需要搜索小写字符串怎么办?您可以在搜索词前使用\C模式来告诉 Vim,后续搜索词将区分大小写。如果执行/\Chello,它将严格匹配”hello”,而不是”HELLO”或”Hello”。

一行中的第一个和最后一个字符

您可以使用^匹配行中的第一个字符,并使用$匹配行中的最后一个字符。

如果您有以下文字:

1
hello hello

您可以使用/^hello来定位第一个”hello”。 ‘^’后面的字符必须是一行中的第一个字符。 要定位最后一个”hello”,请运行/hello$。 ‘$‘ 之前的字符必须是一行中的最后一个字符。

如果您有以下文字:

1
hello hello friend

运行/hello$将匹配不到任何内容,因为”friend”是该行的最后一项,而不是”hello”。

重复搜索

您可以使用//重复上一个搜索。如果您只是搜索/hello,则运行//等同于运行/hello。此快捷键可以为您节省一些按键操作,尤其是在您刚搜索了一个很长的字符串的情况下。另外,回想一下前面的章节,您还可以使用nN分别以相同方向和相反方向重复上一次搜索。

如果您想快速回忆起 第n个最近使用的搜索字怎么办?您可以先按/,然后按up/down方向键(或Ctrl-N/Ctrl-P),快速遍历搜索历史,直到找到所需的搜索词。要查看所有搜索历史,可以运行:history /

在搜索过程中到达文件末尾时,Vim 会抛出一个错误:"搜索到达底部,未找到匹配项:{your-search}"("Search hit the BOTTOM without match for: {your-search}")。有时这个特性能成为一个安全守卫,可以防止过度搜索,但是有时您又想将搜索重新循环到顶部。您可以使用set wrapscan选项使 Vim 在到达文件末尾时回到文件顶部进行搜索。要关闭此功能,请执行set nowrapscan

使用候选词搜索

一次搜索多个单词属于日常操作。 如果您需要搜索”hello vim”或”hola vim”,而不是”salve vim”或”bonjour vim”,则可以使用|或运算符。

给予这样一段文本:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

要同时匹配”hello”和”hola”,可以执行/hello\|hola。 您必须使用(\)转义(|)或运算符,否则 Vim 将按字面意义搜索字符串”|”。

如果您不想每次都输入\|,则可以在搜索开始时使用magic语法(\v):/\vhello|hola。 我不会在本章中详细介绍magic,但是有了\v,您就不必再转义特殊字符了。 要了解有关\v的更多信息,请随时查看:h \v

设置模式匹配的开始位置和结束位置

也许您需要搜索的文本是复合词的一部分。如果您有这些文字:

1
2
3
4
11vim22
vim22
11vim
vim

如果您仅需要选择以”11”开始、以”22”结束的”vim”,您可以使用\zs(开始匹配)和\ze(结束匹配)运算符。 执行:

1
/11\zsvim\ze22

Vim仍然会匹配整个模式”11vim22”,但是仅高亮显示介于\zs\ze之间的内容。 另一个例子:

1
2
foobar
foobaz

如果需要在”foobaz”中搜索”foo”,而不是在”foobar”中搜索,请运行:

1
/foo\zebaz

搜索字符组

到目前为止,您所有的搜索字都是字面内容。在现实生活中,您可能必须使用通用模式来查找文本。最基本的模式是字符组[ ]

如果您需要搜索任何数字,则可能不想每一次都输入/0\|1\|2\|3\|4\|5\|6\|7\|8\|9\|0。相反,请使用/[0-9]来匹配一位数字。 0-9表达式表示 Vim 尝试匹配的数字范围是 0-9,因此,如果要查找 1 到 5 之间的数字,请使用/[1-5]

数字不是 Vim 可以查找的唯一数据类型。您也可以执行/[a-z]来搜索小写字母,而/[A-Z]来搜索大写字母。

您可以将这些范围组合在一起。如果您需要搜索数字 0-9 以及从 a 到 f(十六进制)的小写字母和大写字母,可以执行/[0-9a-fA-F]

要进行否定搜索,可以在字符范围括号内添加^。要搜索非数字,请运行/[^0-9],Vim会匹配任何字符,只要它不是数字即可。请注意,范==围括号内的脱符号(^)与行首位置符号(例如:/^hello)不同==。如果插入号在一对方括号之外,并且是搜索词中的第一个字符,则表示”一行中的第一个字符”。如果插入符号在一对方括号内,并且是方括号内的第一个字符,则表示否定搜索运算符。 /^abc匹配行中的第一个”abc”,而/[^abc]匹配除”a”,”b”或”c”以外的任何字符。

搜索重复字符

如果需要在此文本中搜索两位数:

1
2
3
1aa
11a
111

您可以使用/[0-9][0-9]来匹配两位数字字符,但是该方法难以扩展。 如果您需要匹配二十个数字怎么办? 打字 20 次[[0-9]]并不是一种有趣的体验。 这就是为什么您需要一个count参数。

您可以将count传递给您的搜索。 它具有以下语法:

1
{n,m}

顺便说一句,当在 Vim 中使用它们时,这些count周围的花括号需要被转义。 count 运算符放在您要递增的单个字符之后。

这是count语法的四种不同变体:

  • {n}是精确匹配。 /[0-9]\{2\}匹配两个数字:”11”,以及”111”中的”11”。
  • {n,m}是范围匹配。 /[0-9]\{2,3\}匹配 2 到 3 位数字:”11”和”111”。
  • {,m}是上限匹配。 /[0-9]\{,3\}匹配最多 3 个数字:”1”,”11”和”111”。
  • {n,}是下限匹配。 /[0-9]\{2,\}匹配最少 2 个或多个数字:”11”和”111”。

计数参数\{0,\}(零或多个)和\{1,\}(一个或多个)是最常见的搜索模式,Vim 为它们提供了特殊的操作符:*++需要被转义,而* 可以正常运行而无需转义)。 如果执行/[0-9]*,功能与/[0-9]\{0,\}相同。 它搜索零个或多个数字,会匹配””,”1”,”123”。 顺便说一句,它也将匹配非数字,例如”a”,因为在技术上,字母”a”中的数字个数为零。 在使用”*“之前,请仔细考虑。 如果执行/[0-9]\+,则与/[0-9]\{1,\}相同。 它搜索一个或多个数字,将匹配”1”和”12”。

预定义的字符组

Vim 为常见字符组(例如数字和字母)提供了简写。 我不会在这里逐一介绍,但可以在:h /character-classes中找到完整列表。 下面是有用的部分:

1
2
3
4
5
6
7
\d    数字[0-9]
\D 非数字[^ 0-9]
\s 空格字符(空格和制表符)
\S 非空白字符(除空格和制表符外的所有字符)
\w 单词字符[0-9A-Za-z_]
\l 小写字母[a-z]
\u 大写字符[A-Z]

您可以像使用普通字符组一样使用它们。 要搜索任何一位数字,可以使用/\d以获得更简洁的语法,而不使用/[0-9]

搜索示例:在一对相似字符之间捕获文本

如果要搜索由双引号引起来的短语:

1
"Vim is awesome!"

运行这个:

1
`/"[^"]\+"`

让我们分解一下:

  • " 是字面双引号。它匹配第一个双引号。
  • [^"] 表示除双引号外的任何字符,只要不是双引号,它就与任何字母数字和空格字符匹配。
  • \+表示一个或多个。因为它的前面是[^"],因此 Vim 查找一个或多个不是双引号的字符。
  • " 是字面双引号。它与右双引号匹配。

当看到第一个"时,它开始模式捕获。Vim 在一行中看到第二个双引号时,它匹配第二个"模式并停止模式捕获。同时,两个双引号之间的所有非双引号字符都被[^"]\+ 模式捕获,在这个例子中是短语”Vim is awesome!”。这是一个通用模式(其实就是正则表达式)用于捕获 由一对类似的定界符包围的短语

  • 要捕获被单引号包围的短语,你可以使用/'[^']\+'
  • 要捕获为0包围的短语,你可以使用/0[^0]\+0

搜索示例:捕获电话号码

如果要匹配以连字符(-)分隔的美国电话号码,例如123-456-7890,则可以使用:

1
/\d\{3\}-\d\{3\}-\d\{4\}

美国电话号码的组成是:首先是三位数字,其后是另外三位数字,最后是另外四位数字。 让我们分解一下:

  • \d\{3\}与精确重复三次的数字匹配
  • -是字面的连字符

为==避免转义,可使用\v==:

1
/\v\d{3}-\d{3}-\d{4}

此模式还可用于捕获任何重复的数字,例如 IP 地址和邮政编码。

这涵盖了本章的搜索部分。 现在开始讲替换。

基本替换

Vim 的替代命令是一个有用的命令,用于快速查找和替换任何模式。 替换语法为:

1
:s/{old-pattern}/{new-pattern}/

让我们从一个基本用法开始。 如果您有以下文字:

1
vim is good

让我们用”awesome”代替”good”,因为 Vim 很棒。 运行:s/good/awesome/.您应该看到:

1
vim is awesome

重复最后一次替换

您可以使用普通模式命令&或运行:s来重复最后一个替代命令。 如果您刚刚运行:s/good/awesome/,则运行&:s将会重复执行。

另外,在本章前面,我提到您可以使用//来重复先前的搜索模式。 此技巧可用于替代命令。 如果/good是最近被替换的单词,那么将第一个替换模式参数留为空白,例如在:s//awesome/中,则与运行:s/good/awesome/相同。

替换范围

就像许多 Ex 命令一样,您可以将范围参数传递给替换命令。 语法为:

1
:[range]s/old/new/

如果您有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要将第3行到第5行中的”let”替换为”const”,您可以执行以下操作:

1
:3,5s/let/const/

下面是一些你可以使用的范围参数的变体:

  • :,3s/let/const/ - 如果逗号前没有给出任何内容,则表示当前行。 从当前行替换到第 3 行。
  • :1,s/let/const/ - 如果逗号后没有给出任何内容,它也代表当前行。 从第 1 行替换到当前行。
  • :3s/let/const/ - 如果仅给出一个值作为范围(不带逗号),则仅在该行进行替换。

在 Vim 中,%通常表示整个文件。 如果运行:%s/let/const/,它将在所有行上进行替换。请记住这个范围参数语法,在后面章节中很多命令行命令都遵循这个语法。

模式匹配

接下来的几节将介绍基本的正则表达式。 丰富的模式知识对于掌握替换命令至关重要。

如果您具有以下表达式:

1
2
3
4
5
let one = 1;
let two = 2;
let three = 3;
let four = 4;
let five = 5;

要在数字周围添加一对双引号:

1
:%s/\d/"\0"/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

让我们分解一下命令:

  • :%s 定位整个文件以执行替换。
  • \d 是 Vim 的预定义数字范围简写(类似使用[0-9])。
  • "\0" 双引号是双引号的字面值。 \0是一个特殊字符,代表”整个匹配的模式”。 此处匹配的模式是单个数字\d

另外,&也同样代表”整个匹配的模式”,就像\0一样。 :s/\d/"&"/也可以。

让我们考虑另一个例子。 给出以下表达式,您需要将所有的”let”和变量名交换位置:

1
2
3
4
5
one let = "1";
two let = "2";
three let = "3";
four let = "4";
five let = "5";

为此,请运行:

1
:%s/\(\w\+\) \(\w\+\)/\2 \1/

上面的命令包含太多的反斜杠,很难阅读。 使用\v运算符更方便:

1
:%s/\v(\w+) (\w+)/\2 \1/

结果:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

太好了! 让我们分解该命令:

  • :%s 定位文件中的所有行以执行替换操作
  • (\w+) (\w+)对模式进行分组。\w是 Vim 预定义的单词字符范围简写([0-9A-Za-z_])之一。 包围\w()将匹配的单词字符进行分组。 请注意两个分组之间的空间。 (\w+) (\w+) 捕获两个分组。 在第一行上,第一组捕获”let”,第二组捕获”one”。(英文版中,作者写成了:第一组捕获”one”,第二组捕获”two”,可能是作者不小心的错误)。
  • \2 \1 以相反的顺序返回捕获的组。 \2包含捕获的字符串”let”,而\1包含字符串”one”。 使\2 \1返回字符串”let one”。

回想一下,\0代表整个匹配的模式。 您可以==使用( )将匹配的字符串分成较小的组==。 每个组都由\1, \2, \3等表示。

让我们再举一个例子来巩固这一匹配分组的概念。 如果您有以下数字:

1
2
3
123
456
789

要颠倒顺序,请运行:

1
:%s/\v(\d)(\d)(\d)/\3\2\1/

结果是:

1
2
3
321
654
987

每个(\d)都匹配一个数字并创建一个分组。 在第一行上,第一个(\d)的值为”1”,第二个(\d)的值为”2”,第三个(\d)的值为”3”。 它们存储在变量\1\2\3中。 在替换的后半部分,新模式\3\2\1在第一行上产生”321”值。

相反,如果您运行下面的命令:

1
:%s/\v(\d\d)(\d)/\2\1/

您将获得不同的结果:

1
2
3
312
645
978

这是因为您现在只有两个组。 被(\d\d)捕获的第一组存储在\1内,其值为”12”。 由(\d)捕获的第二组存储在\2内部,其值为”3”。 然后,\2\1返回”312”。

替换标志

如果您有以下句子:

1
chocolate pancake, strawberry pancake, blueberry pancake

要将所有 pancakes 替换为 donut,您不能只运行:

1
:s/pancake/donut

上面的命令将仅替换第一个匹配项,返回的结果是:

1
chocolate donut, strawberry pancake, blueberry pancake

有两种解决方法。 一,您可以再运行两次替代命令。 二,您可以向其传递全局(g)标志来替换一行中的所有匹配项。

让我们谈谈全局标志。 运行:

1
:s/pancake/donut/g

Vim 迅速将所有”pancake”替换为”donut”。 全局命令是替代命令接受的几个标志之一。 您在替代命令的末尾传递标志。 这是有用的标志的列表:

1
2
3
4
5
6
&    重用上一个替代命令中的标志。 必须作为第一个标志传递。
g 替换行中的所有匹配项。
c 要求替代确认。
e 防止替换失败时显示错误消息。
i 执行不区分大小写的替换
I 执行区分大小写的替换

我上面没有列出更多标志。 要了解所有标志,请查看:h s_flags

顺便说一句,重复替换命令(&:s)不保留标志。 运行&只会重复:s/pancake/donut/而没有g。 要使用所有标志快速重复最后一个替代命令,请运行:&&

更改定界符

如果您需要用长路径替换 URL:

1
https://mysite.com/a/b/c/d/e

要用单词”hello”代替它,请运行:

1
:s/https:\/\/mysite.com\/a\/b\/c\/d\/e/hello/

但是,很难说出哪些正斜杠(/)是替换模式的一部分,哪些是分隔符。 您可以使用任何单字节字符(除字母,数字或"|\之外的字符)来更改定界符。让我们将它们替换为+。上面的替换命令可以重写为 :

1
:s+https:\/\/mysite.com\/a\/b\/c\/d\/e+hello+

现在,更容易看到分隔符在哪里。

特殊替换

您还可以修改要替换的文本的大小写。 给出以下表达式,您的任务是将所有变量名比如 “one”, “two”, “three”等,改成大写:

1
2
3
4
5
let one = "1";
let two = "2";
let three = "3";
let four = "4";
let five = "5";

请运行:

1
%s/\v(\w+) (\w+)/\1 \U\2/

你会得到:

1
2
3
4
5
let ONE = "1";
let TWO = "2";
let THREE = "3";
let FOUR = "4";
let FIVE = "5";

这是该命令的细分:

  • (\w+) (\w+)捕获前两个匹配的分组,例如”let”和”one”。
  • \1返回第一个组的值”let”
  • \U\2大写(\U)第二组(\2)。

该命令的窍门是表达式\U\2\U将后面跟着的字符变为大写。

让我们再举一个例子。 假设您正在编写 Vim 书籍,并且需要将一行中每个单词的首字母大写。

1
vim is the greatest text editor in the whole galaxy

您可以运行:

1
:s/\<./\U&/g

结果:

1
Vim Is The Greatest Text Editor In The Whole Galaxy

细目如下:

  • :s 替换当前行
  • \<. 由两部分组成:\<匹配单词的开头,.匹配任何字符。 \<运算符使后面跟着的字符表示单词的第一个字符。 由于.是下一个字符,因此它将匹配任意单词的第一个字符。
  • \U& 将后续符号子序列&大写。 回想一下,&(或\0)代表整个匹配。 这里它匹配单词的第一个字符。
  • g全局标志。 没有它,此命令将仅替换第一个匹配项。 您需要替换此行上的每个匹配项。

要了解替换的特殊替换符号(如\u\U)的更多信息,请查看:h sub-replace-special

候选模式

有时您需要同时匹配多个模式。 如果您有以下问候:

1
2
3
4
hello vim
hola vim
salve vim
bonjour vim

您仅需在包含单词”hello”或”hola”的行上用”friend”代替”vim”。回想一想本章前面的知识点,你可以使用| 来分隔可选的模式:

1
:%s/\v(hello|hola) vim)/\1 friend/g

结果:

1
2
3
4
hello friend
hola friend
salve vim
bonjour vim

这是细分:

  • %s 在文件的每一行上运行替代命令。
  • (hello|hola) 匹配*“hello”或”hola”,并将其视为一个组。
  • vim 是字面意思”vim”。
  • \1 是第一个匹配组,它是文本”hello”或”hola”。
  • friend 是字面的“朋友”。

指定替换模式的开始位置和结束位置

回想一下,您可以使用\zs\ze来指定一个匹配的开始位置和结束位置。这个技术在替换操作中同样有效,如果你有以下文本:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotcake

要想将”hotcake”中的”cake”替换为”dog”,得到”hotdog”:

1
:%s/hot\zscake/dog/g

结果是:

1
2
3
chocolate pancake
strawberry sweetcake
blueberry hotdog

贪婪与非贪婪

您可以使用下面技巧,在某行中替换第n个匹配:

1
One Mississippi, two Mississippi, three Mississippi, four Mississippi, five Mississippi.

要想将第3个”Mississippi”替换为 “Arkansas”,运行:

1
:s/\v(.{-}\zsMississippi){3}/Arkansas/g

命令分解:

  • :s/ 替换命令。
  • \v 魔术关键字,使您不必转义特殊字符。
  • . 匹配任意单个字符。
  • {-} 表示使用非贪婪模式匹配前面的0个或多个字符。
  • \zsMississippi 使得从”Mississippi”开始捕获匹配。
  • (...){3} 查找第3个匹配

在本章前面的内容中,你已经看到过{3}这样的语法。在本例中,=={3}将精确匹配第3个匹配==。这里的新技巧是{-}。它表示进行非贪婪匹配。它会找到符合给定模式的==最短的匹配==。在本例中,(.{-}Mississippi)匹配以任意字符开始、数量最少的”Mississippi”。对比(.*Mississippi),后者会找到符合给定模式的最长匹配。

如果您使用(.{-}Mississippi),你会得到5个匹配:”One Mississippi”, “Two Mississippi”,等。如果您使用(.*Mississippi),您只会得到1个匹配:最后一个 “Mississippi”。*表示贪婪匹配,而{-}表示非贪婪匹配。要想了解更多,可以查看 :h /\{-:h non-greedy

让我们看一个简单的例子。如果您有以下字符串:

1
abc1de1

用贪婪模式匹配 “abc1de1” :

1
/a.*1

用非贪婪模式匹配 “abc1”:

1
/a.\{-}1

因此,如果您需要将最长的匹配转为大写(贪婪模式),运行:

1
:s/a.*1/\U&/g

会得到:

1
ABC1DE1

如果您需要将最短的匹配转为大写(非贪婪模式),运行:

1
:s/a.\{-}1/\U&/g

会得到:

1
ABC1de1

如果您是第一次接触贪婪模式与非贪婪模式这两个概念,可能会把你绕晕。围绕不同的组合去实验,知道您明白这两个概念。

跨多个文件替换

最后,让我们学习如何在多个文件中替换短语。对于本节,假设您有两个文件: food.txtanimal.txt.

food.txt内:

1
2
3
corn dog
hot dog
chili dog

animal.txt内:

1
2
3
large dog
medium dog
small dog

假设您的目录结构如下所示:

1
2
├ food.txt
├ animal.txt

首先,用:args同时捕获”food.txt”和”animal.txt”到参数列表中。回顾前面的章节,:args可用于创建文件名列表。在 Vim 中有几种方法可以做到这一点,其中一种方法是在Vim内部运行:

1
:args *.txt                  捕获当前位置的所有txt文件

测试一下,当您运行:args时,您应该会看到:

1
[food.txt] animal.txt

现在,所有的相关文件都已经存储在参数列表中,您可以用 :argdo 命令跨多文件替换,运行:

1
:argdo %s/dog/chicken/

这条命令对所有:args列表中的文件执行替换操作。最终,存储修改的文件:

1
:argdo update

:args:argdo 是两个有用的工具,用于跨多文件执行命令行命令。可以用其他命令结合尝试一下!

用宏跨多个文件替换

另外,您也可以用宏跨多个文件运行替代命令。执行:

1
2
3
4
5
6
:args *.txt
qq
:%s/dog/chicken/g
:wnext
q
99@q

以下是步骤的细分:

  • :args *.txt 会将相关文件列出到:args列表中。
  • qq 启动”q”寄存器中的宏。
  • :%s/dog/chicken/g在当前文件的所有行上用”chicken”替换”dog”。
  • :wnext 写入(保存)文件,然后转到args列表中的下一个文件。就像同时运行:w:next一样。
  • q 停止宏录制。
  • 99@q 执行宏九十九次。 Vim 遇到第一个错误后,它将停止执行宏,因此 Vim 实际上不会执行该宏九十九次。

以聪明的方式学习搜索和替换

良好的搜索能力是编辑的必要技能。掌握搜索功能使您可以利用正则表达式的灵活性来搜索文件中的任何模式。花些时间学习这些。要想掌握正则表达式,您必须在实践中去不断地使用它。我曾经读过一本关于正则表达式的书,却没有真正去做,后来我几乎忘了读的所有东西。主动编码是掌握任何技能的最佳方法。

一种提高模式匹配技能的好方法是,每当您需要搜索一个模式串时(例如”hello 123”),不要直接查询文字的字面值(/hello 123),去尝试使用模式串来搜索它(比如/\v(\l+) (\d+))。这些正则表达式概念中的许多不仅在使用 Vim 时,也适用于常规编程。

既然您已经了解了 Vim 中的高级搜索和替换,现在让我们学习功能最丰富的命令之一,即全局命令。

总结

转义

  • 使用候选词搜索
  • {n,m}
  • 特殊的操作符:*++需要被转义,而* 可以正常运行而无需转义)

% 和 g

  • 将文档每行==第一个匹配==上的 <old string> 替换为 <new string>

    1
    :%s/<old string>/<new string>/
  • 将文档中==所有==的 <old string> 替换为 <new string>

    1
    :%s/<old string>/<new string>/g

设置tab位4个空格

修改配置

如果要修改全局Vim的配置

1
vim /etc/vim/vimrc1

但是不建议这么做,可以只修改当前用户的Vim配置

1
vim ~/.vimrc1

在配置文件中添加以下参数

  1. 第一种配置参数

    1
    2
    set ts=4
    set sw=412
  2. 第二种配置参数

    1
    2
    3
    set ts=4
    set expandtab
    set autoindent123

建议使用第二种,具有兼容性

修改已保存文件

Tab替换为空格

1
2
3
:set ts=4
:set expandtab
:%retab!123

空格替换为Tab

1
2
3
:set ts=4
:set noexpandtab
:%retab!123

加感叹号!是用于处理非空白字符之后的Tab,即所有的Tab

若不加感叹号!,则只处理行首的Tab修改配置


如果要修改全局Vim的配置

1
vim /etc/vim/vimrc1

但是不建议这么做,可以只修改当前用户的Vim配置

1
vim ~/.vimrc1

在配置文件中添加以下参数

  1. 第一种配置参数

    1
    2
    set ts=4
    set sw=412
  2. 第二种配置参数

    1
    2
    3
    set ts=4
    set expandtab
    set autoindent123

建议使用第二种,具有兼容性

修改已保存文件

Tab替换为空格

1
2
3
:set ts=4
:set expandtab
:%retab!123

空格替换为Tab

1
2
3
:set ts=4
:set noexpandtab
:%retab!123

加感叹号!是用于处理非空白字符之后的Tab,即所有的Tab

若不加感叹号!,则只处理行首的Tab

基础

认识 Latex

命令与环境

构成

反斜杠加上命令名称,再加上花括号内的参数

1
\documentclass{ctexart}

保留字符

符号 说明
# 自定义命令时,用于标明参数序号。
$ 数学环境命令符。
% 注释符,其后的该行命令都会视为注释。如果在行末添加这个命令,可以防止 LaTeX 在行末插入一些奇怪的空白符。
^ 数学环境中的上标命令符。
& 表格环境中的跳列符。
_ 数学环境中的下标命令符。
{ 与 } 用于标记命令的必选参数,或者标记某一部分命令使其成为一个整体。
\ 用于开始 LaTeX 命令。

导言区

1
2
3
4
\documentclass[option]{doc-class}
\begin{document}
...
\end{document}

在语句 \begin{document} 之前的内容称为==导言区==,导言区即模板定义

doc-class

doc-class(文档类) 类别
article 科学期刊、演示文稿、短报告、邀请函
proc 基于 article 的会议论文集
report 多章节的长报告、博士论文、短篇书
book 书籍
slides 幻灯片,使用了大号 Scans Serif 字体

此外还有 beamer 宏包定义的 beamer 文档类,其常用于创建幻灯片。

options (可选项)

options 取值
字体 默认 10 pt,可选 11 pt 和 12 pt
页面方向 默认 portrait (竖向),可选 landscape (横向)
纸张尺寸 默认 letterpaper ,可选 a4paper 、b5paper 等
分栏 默认 onecolumn ,可选 twocolumn
双面打印 有 oneside/twoside 两个选项,用于排版奇偶页, article/report 默认单面
章节分页 有 openright/openany 两个选项,决定在奇数页或任意页开启新页。注意,article 是没有 chapter (章)命令的,默认任意页
公式对齐 默认居中;可改为左对齐 fleqn ;默认编号居右,可改为左对齐leqno
草稿选项 默认 final ,可改为 draft ,使行溢出的部分显示为黑块

宏包

导言区最常见的是宏包的加载工作,命令形如:

1
\usepackage{package}

宏包是指一系列已经制作好的功能“模块”,在你需要使用一些非原生 LaTeX 功能时,只需调用这些宏包就可以了

学习宏包

按 Win+R 组合键调出运行对话框,输入 texdoc 加上宏包名称即可打开宏包 pdf 帮助文档,例如:texdoc xeCJK

文件输出

pdf

后缀 说明
.sty 宏包文件。
.cls 文档类文件。
.aux 用于存储交叉引用信息的文件,因此,在更新交叉引用(公式编号、大纲级别)后,需要编译两次才能正常显示。
.log 日志,记录上次编译的信息。
.toc 目录文件。
.lof 图形目录。
.lot 表格目录。
.idx 如果文档中包含索引,该文件用于存储索引信息。
.ind 索引记录文件。
.ilg 索引日志文件。
.bib 参考文献数据文件。
.bbl 生成的参考文献记录。
.bst 模板。
.blg 日志。
.out hyperref 宏包生成的 pdf 书签记录。

异常处理

删除文件夹下除了 tex 以外的文件再编译

注意

在某些独占程序打开了以上的文件时(比如用Acrobat 打开了 pdf 文件),编译也可能出现错误。请在编译时确保关闭这些独占程序。

标点与强调

英文符号 |、<、>、+、= 一般用于数学环境

  • 在文本中使用,请在它们两侧加上 $
  • 大于小于号:你应该使用 \textgreater\textless 命令

引号

  • 左单引号是重音符 `
  • 右单引号是常用的引号符
  • 左双引号就是连续两个重音符
  • 英文下的引号嵌套需要借助 \thinspace 命令分隔

省略号

英文的省略号使用 \ldots 这个命令

强调:粗与斜

\emph{text}

  • 中文:强调文本
  • 细纹:斜体

下划线与删除线

  • LaTeX 原生提供的 \underline 命令简直烂得可以
  • ==建议==你使用 ulem 宏包下的 uline 命令,它还支持换行文本
命令 说明
\uline{} 下划线
\uuline{} 双下划线
\dashuline{} 虚下划线
\dotuline{} 点下划线
\uwave{} 波浪线
\sout{} 删除线
\xout{} 斜删除线

注意

ulem 宏包重定义了 \emph 命令:

  • 强调命令会对内容加下划线

  • 嵌套使用两次强调命令则会对内容加双下划线

通过宏包的 normalem 选项可以取消这个更改:\usepackage[normalem]{ulem}

其他

  • 角度符号或者温度符号
  • 欧元符号可能需要用到 textcomp 宏包支持的 \texteuro 命令
  • 千位分隔位
    • 不想它在中间断行,就在外侧再加上一个 \mbox 命令
  • 注音符号
  • 音标:
    • tipa 宏包
    • numprint 宏包
    • siunitx 宏包
  • hologo 宏包可以输出许多TEX 家族标志

格式控制

符号说明

符号 说明
pt point,磅。
pc pica,1 pc = 12 pt,四号字。
in inch,英寸,1 in = 72.27 pt。
bp bigpoint,大点,1 bp = in。
cm centimeter,厘米,1 cm = in。
mm millimeter,毫米,1 mm = cm。
sp scaled point,TEX 的基本长度单位,1 sp = pt。
em 当前字号下,大写字母M的宽度。
ex 当前字号下,小写字母x的高度。

长度宏

说明
\textwidth 页面上文字的总宽度,即叶宽减去两侧边距
\linewidth 当前行允许的行宽

可变长度

  • 5 pt plus 3 pt minus 2 pt:表示一个能收缩到 3 pt 也能伸长到 8 pt 的长度
  • 直接使用倍数:1.5\parindent

特殊空格

\hspace\vspace

空格与换行

  • 多个空格、换行会被视为一个
  • 禁止 LaTeX 在某个空格处的换行,将空格用 ˜ 命令替代即可,比如: Fig.˜ 8

换行

  • 会自动换行
  • 分段:两个回车 (用于正文换行)
  • 空白段落(空白行)
    • 两个回车 + \mbox{} + 两个回车
    • \par: 生成一个带缩进的新段
  • 强制换行: \\
    • 缺点:下一行段首缩进会消失

段落间距离

  • 默认: 0pt plus 1pt
  • 控制命令 \parskip\setlength{\parskip}{0pt plus 1pt}
  • 首字下沉:lettrine 宏包

分页

  • \newpage 命令开始新的一页
  • \clearpage 命令清空浮动体队列,并开始新的一页
  • \cleardoublepage 命令清空浮动体队列,并在偶数页开始新的一页
  • 连续新开两页:在 \newpage 中间加上一个空的箱子(\mbox{}),\newpage\mbox{}\newpage

缩进、对齐与行距

缩进

  • 宏包:借助 indentfirst 宏包
  • 设置距离:使用 \setlength\parindent{2em} 这样的命令来设置缩进距离
  • 段首强制取消缩进:在段首使用 \noindent 命令

英文的每节第一段的段首==允许没有缩进==

对齐

默认使用两端对齐的排版方式

设置

可以使用 flushleft 、flushright 、center 这三种环境来构造居左、居中、居右三种效果。

特殊的 \centering 命令常常用在环境内部(或者一对花括号内部),以实现居中的效果。

尽量用 center 环境代替这个老旧的命令。类似地还有 \raggedleft 实现居右, \raggedright 实现居左

字体与颜色

中西文“斜体”

中文

汉字没有加斜体

汉字字体里面也不一定有加粗体的设计

英文

  • 加斜是指某种字族的 Italy 字系
  • 斜体,是指 Slant 字族

使用

  • 行文中表强调时使用的是前者;
  • 在 MS Word 等软件中看到的倾斜字母 I ,也代表前者。

原生字体命令

字族

命令 描述
\rmfamily 把字体设置为 Roman 罗马字族
\sffamily 把字体设置为 Sans Serif 无衬线字族
\ttfamily 把字体设置为 Typewritter 等宽字族

字系

命令 描述
\bfseries 粗体 BoldSeries 字系属性
\mdseries 中粗体 MiddleSerie 字系属性

字形

命令 描述
\upshape 竖直字形
\slshape 斜体字形
\itshape 强调体字形
\scshape 小号大写字形

临时改变字体

临时改变字体,使用 \textrm\textbf 这类命令

注意

  • 字族、字系、字形三种命令是互相独立的,可以任意组合使用

  • 这种复合字体的效果有时候无法达到(因为没有对应的设计)

    LaTeX 会针对这种情况给出警告,但仍可以编译,只是效果达不到预期。

自定义命令

在文中多次使用某种字体变换,可以将其自定义成一个命令

字号

默认的“标准”字号

  • 在documentclass 的选项中设置的 12 pt(如果你设置了的话)

相对字号命令

ctex 宏包

ctex 宏包的 \zihao 命令的参数 08 以及 -0-8 表示初号到八号、小初号到小八号

日常使用的小四号为 12 pt,五号为 10.5 pt

设置特殊的字号

1
\fontsize{font-size}{line-height}{\selectfont <text>}
  • font-size: 填数字,单位pt
  • line-height: 填 \baselineskip,表示行与行之间的基线间距(即行距),默认是 1.2 倍文字高。

默认字体

全文的默认字体使用 \rmfamily 族的字体

通过重定义的方式更改它,使 \rmfamily\textrm 命令都指向新的字体,甚至把默认字体改为 sf/tt 字族

1
2
3
4
5
6
\renewcommand{\rmdefault}{font-name}
% 默认字体改为sf字族,也可以用 \ttdefault
\renewcommand{\familydefault}{\sfdefault}
\renewcommand{\sfdefault}{font-name}
% 如果你排版 CJK文档,还需要更改CJK的默认字体
\renewcommand{\CJKfamilydefault}{\CJKsfdefault}

西文字体

预包含西文字体

使用

引言区定义

1
2
3
\newcommand{\myfont}[2]{{\fontfamily{#1}\selectfont #2}}
% 可更改默认字体,同理可改 sfdefault
\renewcommand{\rmdefault}{ptm}

正文中

1
Let's change font to \myfont{ppl}{Palatino}!

本地安装字体

使用 fontspec 宏包

一般使用 fontspec 宏包来选择本地安装 的字体。注意,该宏包可能会明显增加编译时间

1
2
3
\usepackage{fontspec}
\newfontfamily{\lucida}{Lucida Calligraphy}
\lucida{This is Lucida Calligraphy}

该宏包的 \setmathrm/sf/tt\setboldmathrm 命令可以更改数学环境中调用的字体。

简单加载 txtfont 宏包

将西文字体设置为Roman 体,同时设置好数学字体。

简单字体宏包还有cmbright ,以及提供 Palatino 字体的 pxfonts ,前者提供的 CM Bright 与 TeX 默认字体 Computer Modern 协调得不错

中文支持与CJK字体

ctex 宏包

直接定义了新的中文文档类 ctexart 、ctexrep 与 ctexbook ,以及 ctexbeamer 幻灯文档类。

xeCJK 宏包

在使用 时,如果你使用 ctex 文档类,它会在底层调用 xeCJK 宏包,所以你无须再显式地加载它。当然你也可以使用原生文档类,然后逐一汉化参数内容。

示例

在引言区

1
2
3
\usepackege[slantfont,boldfont]{xeCJK}
\xeCJKsetup{CJKMath=true}
\setCJKmainfont[BoldFont=SimHei]{Simsun}

颜色

使用 xcolor 宏包

1
2
\usepackage{xcolor}
\definecolor{keywordcolor}{RGB}{34,34,250}

指定颜色的text

1
{\color{color-name}{text}}

调色

1
2
3
4
5
6
% 70%红色
{\textcolor{red!70}}
% 50蓝,20黑,30白
{\textcolor{blue!50!black!20!white!30!}{50蓝,20黑,30白}}
% 黄色互补色
{\textcolor{-yellow}{黄色互补色}}

引用与注释

标签与引用

没看懂

脚注、边注和尾注

没看懂

摘要

没看懂

参考文献

natbib 宏包

正式排版:封面、大纲与目录

封面

封面的内容在导言区进行定义,==一般写在所有宏包、自定义命令之后==

命令如下:

1
2
3
\title{Learning Latex}
\author{Qeuroal
\date{text}

使用封面

document 环境内的第一行写上 \maketitle ,这样就能产生一个简易的封面。

1
2
3
4
5
\begin{document}
\maketitle


\end{document}

标题页的脚注用 \thanks 命令完成。

注意

  • \title 和 \author ==是必须定义的==
  • date
    • \date 如果省略:会自动以编译当天的日期为准,格式形如: January 1, 1970。
    • 不想显示日期,可以写 \date{}

大纲与章节

命令 大纲级别
\part 部分,这个大纲不会打断 chapter 的编号。
\chapter 章,article 的文档类不包含本大纲级别。
\section 节。
\subsection 次节,默认 report/book 文档类中本级别及以下的大纲不进行编号,也不纳入目录。
\subsubsection 小节,默认 article 文档类中本级别及以下的大纲不进行编号,也不纳入目录。
\paragraph 段,极少使用。
\subparagraph 次段,极少使用。

计数器与列表

计数器

LaTeX 中的自动编号都借助内部的计数器来完成,计数器包括下面几种:

  • 章节 :part、chapter、section、subsection、subsubsection、paragraph 与 subparagraph。
  • 编号列表 :enumi、enumii、enumiii 与 enumiv。
  • 公式和图表 :equation、figure 与 table。
  • 其他 :page、footnote 与 mpfootnote

  • mpfootnote 命令用于实现 minipage 环境的脚注

使用

通过 \the 接上计数器名称来调用计数器,比如 \thechapter

如果只是输出计数器的数值,可以指定数值的形式,如阿拉伯数字、大小写英文字母,或大小写罗马数字。

常用的命令:

  • \arabic{counter-name}
  • \Alph
  • \alph
  • \Roman
  • \roman
  • % ctex文档类还支持\chinese

图、表格

没看懂

页面设置

借助 geometry 宏包

优秀的讲解

类与对象

表述

类描述了一系列具有相同特征(即数据元素)和行为(即功能方法)的对象
一个上佳的方法是将对象想象成“服务提供者” 。你的程序本身也是为用户提供服务的,它通过使用其他对象提供的服务来做到这一点。所以,你的任务是创建(更好的情况是,从已有的库中找到)一些提供对应服务以解决问题的对象。

内存方面

创建一个类即描述了其对象的外观和行为。直到使用 new 关键字时,你才会真正创建一个对象,以及为该对象分配内存,并使得其方法可以被调用。

消息

调用方法

调用方法的行为被描述为 “==向一个对象发送一条消息==”,如:

1
int x = a.f();
  • f() 代表消息

  • a 代表对象

面向对象编程

面向对象编程描述为“==向对象发送消息==”

接口与类的关系

是抽象与更抽象的关系。

比如,定义人这个类,但是在人之外还有各种动物,或者说,人就是高级动物。但是有一个问题是,人有学历之类的属性,而其他动物一定要有学历这一属性吗?答案是否。人有自己的行为动作,那么其他动物是否也有相同或者类似的行为动作呢?答案是是的。那么我们是不是就可以设计一个动物类,而不添加任何属性呢?那么这个类就可以称作接口了。

引用

引用的作用是关联对象。

引用未必会关联某个对象,如:String s

引用的个人理解

引用就是一个地址或者指针,通过地址来操作相关对象

创建对象

new

使用 new 关键字来创建对象: 我要一个这种类型的对象

1
String s = new String9("asdf");

初始化

数组

  • 放置对象的数组:初始化为 null
  • 放置基本类型的数组:初始化为 0

对象

基本类型 默认值
boolean false
char \u0000(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
引用 null
调用引用的方法会==得到异常==,但仍然可以打印一个 null 引用而不会抛出异常

初始化顺序

  1. 自动初始化
  2. 构造初始化

静态数据

  • 字段是基本类型:基本类型的标准初始值
  • 字段是对象引用:默认初始值为 null

初始化引用

  1. 在定义对象时。这意味着它们将始终在调用构造器之前被初始化。
  2. 在该类的构造器中。
  3. 在对象实际使用之前。这通常称为延迟初始化(lazy initialization) 。在对象创建成本高昂且不需要每次都创建的情况下,它可以减少开销。
  4. 使用实例初始化

示例

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// reuse/Bath.java
// 使用组合进行构造器初始化
class Soap {
private String s;

Soap() {
System.out.println("Soap()");
s = "Constructed";
}

@Override
public String toString() {
return s;
}
}

public class Bath {
// 在定义时初始化
private String s1 = "Happy", s2 = "Happy", s3, s4;
private Soap castile;
private int i;
private float toy;

public Bath() {
System.out.println("Inside Bath()");
// 构造初始化
s3 = "Joy";
toy = 3.14f;
castile = new Soap();
}

// 实例初始化
{
i = 47;
}

@Override
public String toString() {
// 延迟初始化
if (s4 == null)
s4 = "Joy";
return "s1 = " + s1 + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" + "i = " + i + "\n"
+ "toy = " + toy + "\n" + "castile = " + castile;
}

public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
}

与C不同点

同名变量

1
2
3
4
5
6
{
int x = 12;
{
int x = 96; // 语法错误
}
}

Java 编译器会提示说,变量 x 已经定义过了。

==类似 C 和 C++ 那样在外围的作用域中“隐藏”变量的方式在 Java 中是不被允许的==

  • 文件中必须存在一个与该文件同名的类(如果你没有这么做,则编译器会报错)

入口

如果你需要创建一个能够独立运行的程序,那么与文件同名的类中还必须包含一个程序启动的入口方法。这个特殊的方法叫作 main(),具有以下的格式和返回值:

1
public static void main(String[] args)

编译

  1. javac <filename>.java
  2. java <filename>

赋值

基本类型

基本类型存储了实际的值,而非只想一个对象的引用,所以在为其赋值的时候,你直接将一个地方的==内容复制到了另一个地方==。

对象赋值

当操作一个对象的时候,我们==真正操作的是这个对象的引用==。当“将一个对象赋值给另一个对象”时,你是将这个引用从一个地方复制到了另一个地方。这意味着对对对象而言,将两个变量同时指向同一个对象。

方法调用中的别名

将一个对象作为参数传递给方法时,也会产生别名

格式化处理

符号 含义
%n 换行
%e 表示以科学记数法显示结果

整数值对象

创建方式

  • 自动转化为 Integer

    通过对 Integer.valueOf() 的==自动调用==来完成的

    1
    2
    int value = 0;
    Integer i = value;
  • 使用标准的对象创建语法 new。 这是以前创建 “包装/装箱” Integer 对象的首选方法

    1
    2
    int value = 0;
    Integer r = new Integer(value);
  • 从 Java 9 开始,valueOf() 优于 [2]。如果尝试在 Java 9 中使用方式 [2],你将收到警告,并被建议使用 [3] 代替。很难确定 [3] 是否的确优于 [1],不过 [1] 看起来更简洁

    1
    2
    int value = 0;
    Integer v = Integer.valueOf(value);
  • 基本类型 int 也可以当作整数值对象使用

    1
    int value = 0;

equal()

  • equals() 方法的默认行为是比较引用。
  • 如果只想比较内容,你必须重写 equals() 方法
  • 大多数标准库会重写 equals() 方法来比较对象的内容而不是它们的引用

boolean

  • 把 int 类型当作 boolean 类型并==不合法==
  • 如果在应该使用字符串的地方使用了布尔值,布尔值会自动转换成合适的文本格式
  • boolean 类型是有限制的:
    • 我们只能赋予它 true 和 false 值,并测试它是真还是假
    • 不能将 boolean 值相加,或对 boolean 值执行其他任何运算

字面值

  • 大写(或小写)的字符 L 表示 long 类型(不过使用小写的 l 容易让人迷惑,因为它看起来就像数字 1)

  • 大写(或小写)的字符 F 表示 float 类型

  • 大写(或小写)的字符 D 表示 double 类型

  • Java 7 引入了==二进制字面量==,通过前缀 0b0B 来表示,它适用于所有整数类型。

  • 可以在数字字面量里使用下划线,这样更易于阅读

    如:341_435_936.445_667

    规则:

    • 只能使用单个下划线,不能连续使用多个;
    • 数字的开头或结尾不能有下划线;
    • FDL 这样的后缀周围不能有下划线;
    • 在二进制或十六进制标识符 bx 的周围不能有下划线。
  • e 表示 “10 的幂次”

    e 大小写都可以,含义相同

  • 编译器一般会将指数作为 double 类型处理,所以如果没有尾部的 f, 我们会收到一条出错提示, 告诉我们必须将 double 类型转换成 float 类型

布尔类型位运算

  • 可以对它执行按位“与” 、按位“或”和按位“异或”运算,但==不能执行按位“非”==
  • 对于布尔值,按位操作符和逻辑操作符具有相同的效果,但它们==不会“短路”==
  • 针对布尔值的按位运算还比逻辑操作符多了一个“异或”运算

移位操作

  • 移位操作符也操纵二进制位,它们==只能用来处理基本类型里的整数类型==。
  • 左移位操作符(==<<==)会将操作符左侧的操作数向左移动,移动的位数在操作符右侧指定(==低位补 0==)
  • “有符号”的右移位操作符(==>>==)则按照操作符右侧指定的位数将操作符左侧的操作数向右移动。“有符号”的右移位操作符使用了“符号扩展” :
    • 如果符号为正,则在==高位插入 0==
    • 否则在==高位插入 1==
  • 种“无符号”的右移位操作符(==>>>==) ,它使用“零扩展”: 无论符号为正还是为负,都在==高位插入 0==
  • 无符号”右移位操作符结合赋值操作符可能会遇到一个问题:如果对 byte 或 short 值进行移位运算,得到的可能不是正确的结果。它们会先被提升为 int 类型,进行右移操作,然后在被赋回给原来的变量时==被截断==, 这时得到==结果是 -1==

字符串操作符 + 和 +=

如果表达式以一个字符串开头,则其后的所有操作数都必须是字符串类型的(编译器会自动把双引号里的字符序列转换成字符串)

字符串转换并不取决于先后顺序

类型转换

自动转换

Java 中无法自动将 int 类型转为 boolean 类型

强制转换

既可以对数值进行类型转换, 也可以对变量进行类型转换

窄化转型

有可能面临信息丢失的危险。窄化转型就是说,将能容纳更多信息的数据类型==转换成无法容纳那么多信息的数据类型==。此时,编译器会要求我们进行强制类型转换,意在提醒我们: “这可能是一个危险的操作,如果的确要这么做,你必须显式地进行类型转换。”

宽化转型

不必显式地进行类型转换, 因为新类型可以容纳比原来的类型更多的信息, 而不会造成任何信息的丢失

截尾与舍入

在执行窄化转型时,会出现截尾与舍入

处理

  • float 或 double 转型为整型值时: 截尾

    java.lang.Math 中的 round(): 舍入

提升

  • 如果对小于 int 类型的基本数据类型(即 char、byte 或者 short)执行算术运算或按位运算,运算执行前这些值就会被自动提升为 int,结果也是 int 类型。
  • 如果要把结果赋值给较小的类型,就==必须使用强制类型转换==(由于把值赋给了较小的类型,可能会出现信息丢失) 。
  • 表达式里出现的最大的数据类型决定了表达式最终结果的数据类型

不允许转换

  • boolean

    Java 可以把任何基本类型转换成别的基本类型,但 boolean 除外,它不允许进行任何类型的转换处理。

  • “类”类型(class type)也不允许进行类型转换

    将一种类型转换成另一种类型需要采用特殊的方法:对象可以在它的类型所属族群里进行类型转换,但不能把它转换成外部的类型。

Java 没有 sizeof()

逗号操作符

逗号操作符不是 ==逗号分隔符==

  • 逗号分隔符:分割生命和函数的不同参数

  • 逗号操作符:Java 里==唯一==用到逗号操作符的地方就是 for 循环的控制表达式。

    • 使用地方:初始化和步进部分,这些语句会按先后顺序执行
    • 注意:可以在for语句里定义多个变量,但它们必须是 ==相同的类型==

循环

for-in

用于数组和容器,也叫做 foreach 语法或 for-in 语法

说明

for-in 会==自动为你生成每一项元素==,这样你就不需要创建 int 变量来对这个元素构成的序列进行计数。

使用

任何反悔了数组的方法都可以使用 for-in

range()

range() 方法可以让我们在更多的地方使用 for-in 语法,从而提高了可读性

总结

不仅方便代码的编写,更重要的是让代码更容易阅读,它表明了你打算做什么(获取数组中的每一个元素) , 作者更提倡使用 for-in 语法

无限循环

  • for(;;)
  • while(true)

goto

Java中没有goto

标签

与goto使用了相同的机制

说明

标签是以冒号结尾的标识符

1
label:

用法

在 Java 中,放置标签的==唯一地方==是正好在迭代语句之前。 “正好”的意思就是,==不要在标签和迭代之间插入任何语句==

break 和 continue 通常只会中断当前循环,但和标签一起用时,它们可以中断这个嵌套的循环,直接跳转到标签所在的位置

注意

  • 使用标签的==唯一理由==就是你用到了嵌套循环,而且你需要使用 break 或 continue 来跳出多层的嵌套。

switch

语法

1
2
3
4
5
6
7
8
9
switch(integral-selector) {
case integral-value1 : statement; break;
case integral-value2 : statement; break;
case integral-value3 : statement; break;
case integral-value4 : statement; break;
case integral-value5 : statement; break;
// ...
default: statement;
}
  • 整数选择器(integral-selector)是一个能生成整数值的表达式

    • int

    • char

    • 字符串

    • enum

初始化

静态块

Java 允许在一个类里将多个静态初始化语句放在一个特殊的“静态子句”里(有时称为静态块)

1
2
3
4
5
6
public class Spoon {
static int i;
static {
i = 47;
}
}

只执行一次:第一次创建该类的对象时,或第一次访问该类的静态成员时(即使从未创建过该类的对象) 。

实例初始化

Java 提供了一种称为实例初始化(instance initialization)的类似语法,==用于初始化每个对象的非静态变量==。

可以用来保证无论调用哪个显式的构造器,某些操作都会发生

实例初始化子句在==构造器之前执行==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Mugs {
Mug mug1;
Mug mug2;

// 实例初始化
{
mug1 = new Mug(1);
mug2 = new Mug(2);
}

Mugs() {
System.out.println("Mugs()");
}

Mugs(int i) {
System.out.println("Mugs(int)");
}
}

数组

定义

  • 方法1

    1
    int[] a1;
  • 方法2

    1
    int a1[];

注意

  • 编译器不允许指定数组的大小

  • 所拥有的只是对数组的引用(你已经为该引用分配了足够的存储空间) ,并没有为数组对象本身分配任何空间

  • 创建了一个非基本类型数组,其实就是创建了一个引用数组

    即便使用了 new 来创建数组之后,它还只是一个引用数组,直到通过自动装箱为数组里的每个引用本身初始化了一个Integer 对象之后,这个数组的初始化才真正结束

初始化

特殊的初始化方式

只能在创建数组的地方出现,编译器负责存储的分配(相当于使用 new)

1
int[] a1 = {1, 2, 3, 4, 5}

默认

自动初始化为空值(对于数值类型和 char 是 0, 对于 boolean 则是 false)

花括号包围列表

使用花括号保卫列表来初始化对象数组

  • 方法1:局限性更大,它只能用在定义数组的时候

    1
    Integer[] a = {1,  2, 3, };
  • 方法2: 在任何地方使用第二种形式(定义了 b) ,甚至在方法调用中也可以

    1
    Integer[] b = new Integer[]{ 1,  2, 3,  };

注意

  • 初始值列表的最后一个逗号都是可选的(此功能可以让维护长列表更容易)

运算

赋值

可以将一个数组赋值给另一个数组

1
a2 = a1

其实真正做的知识复制了一个引用

查询数组有多少个元素

成员变量: length

可变参数列表

如果列表中没有任何内容,则它会转变成一个大小为零的数组

函数

printBinaryInt() 和 printBinaryLong()

分别接受 int 类型和 long 类型的参数,然后输出其二进制格式

%n

Java 用 %n 来实现的功能,它会根据程序运行的平台生成合适的换行符,不过这仅会在使用 System.out.printf()System.out.format() 时起作用。对于 System.out.println(),你仍然必须使用 \n;如果使用了 %n, println() 只会输出 %n 而不是将其当作换行符。

toBinaryString()

使用 Integer 和 Long 类的静态方法 toBinaryString() 可以很容易实现获取二进制

如果将比较小的类型传递给Integer.toBinaryString() 方法,则该类型会自动被转换为 int 类型

Integer.toString()

1
2
// Integer.toString() 的简化版:
System.out.println("" + x);

Array.toString(a)

1
2
int[] a = new int[10];
System.out.println(Array.toString(a));

getClass()

生成一个对象的类,当打印这个类时,你会看到一个表示该类类型的编码字符串

[ 表示这是后面紧随的类型的数组

ordinal()

表示特定 enum 常量的声明顺序

一个静态的values() 方法

它按照声明顺序生成一个 enum 常量值的数组

自动类型推断

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
32
33
34
35
36
// housekeeping/TypeInference.java 
// {NewFeature} 从 JDK 11 开始
class Plumbus {
}

public class TypeInference {
void method() {
// 显式类型:
String hello1 = "Hello";
// 类型推断:
var hello = "Hello!";
// 用户定义的类型也起作用:
Plumbus pb1 = new Plumbus();
var pb2 = new Plumbus();
}

// 静态方法里也可以启用:
static void staticMethod() {
var hello = "Hello!";
var pb2 = new Plumbus();
}
}

class NoInference {
String field1 = "Field initialization";
// var field2 = "Can't do this";

// void method() {
// var noInitializer; // No inference data
// var aNull = null; // No inference data //
// }

// var inferReturnType() {
// return "Can't infer return type"; //
// }
}

static

static 修饰的字段或方法(也称“类数据 class data” 和 “类方法class method”),只服务于类,而非特定的对象。这些数据或方法只会占用同一块内存空间,即会被由该类创建所有的对象所共享。

注意

调用 static 方法并不依赖于是否提前创建对象。正因为如此,我们也==不能在没有具体对象的情况下, 使用 static 方法直接调用非 static 成员或方法==(因为非 static 成员和方法必须基于特定的对象才能 运行) 。

调用方法

  • 通过对象调用: <对象>.<static 成员>

  • 通过类名调用: <类名>.<static 成员> (==推荐使用该方法==)

    注意:==非 static 成员则不能这样使用==

生成方式

  • static 字段

    • 是基于类创建的
    • 即便你没有创建对象,也可以调用该方法
  • 非 static 字段

    • 是基于对象创建的

见下

访问控制

  • 每个编译单元(文件)都只能有一个public类,但可以根据需要拥有任意数量的包访问权限的类
    • public 类的名称必须与包含编译单元的文件名完全匹配,包括大小写
    • 编译单元里可以没有 public 类。这时你可以随意命名文件
  • 类访问权限, 只有两种选择: 包访问权限和 public
    • 若防止对该类的访问,可以将其所有的构造器都设为 private

Math

函数 功能
random() 生成一个范围为0~1(包括 0,但不包括 1)的 double 值

java.lang.Character 包装器类

函数 功能
isLowerCase() 检测出相关字符是否为小写字母

问题

包与库的关系

不清楚

toString()

每个非基本类型的对象都有一个 toString() 方法

它会在一些特殊情况下被调用,比如当编译器需要一个字符串,但有的是一个对象时。

注解

注解 @Override 用在 toString() 方法上

@Override 是可选的

final

final 数据

  • 定义常量

空白 final

final 执行赋值的操作只能发生在两个地方

  • 在字段定义处使用表达式进行赋值
  • 在每个构造器中

final 参数

在方法内部不能更改参数引用指向的内容

可以读取这个参数, 但不能修改它

final 方法

原因

  • 在方法上放置一个“锁” ,这样就可以防止继承类通过重写来改变该方法的含义
  • 效率:编译器可以将任何对该方法的调用转换为内联调用 (现在可以不用考虑这个因素了,主要考虑 “锁” 的因素)

final 和 private

类中的任何 private 方法都是隐式的 final

final 类

将整个类定义为 final 时(通过在其定义前加上 final 关键字) ,就阻止了该类的所有继承

综述

提升学习能力

学习资料

社会的进步是一部信息存储和传播方式变革的历史

时间不变信息爆炸的应对策略

  • 放弃治疗
  • 加大投入
  • ==提升效率==

学习过程

提升科研能力

什么是科研

  • 解决未知的问题
  • 发现新事物或新规律
  • 创造新事物

科研特性

  • 只认第一,不认第二
  • 科学研究需要探索未知,面对新事物(恐惧感)
  • 需要解决新问题
  • 科研需要大量的重复
  • 科研会经历很多次失败

读研意义

  • 全球视野,以及发现需求的眼光
  • 面对未知事务的勇气,以及愈挫愈勇的韧性
  • 解决问题的English
  • 在持续努力和练习的过程中形成习惯
  • 任何成功的人,都是优秀的研究者

好的选题从哪里来

  • 知己知彼:你对一个领域的了解程度会直接影响你的选题
  • 现实需求:真正来自现实生活的需求最有生命力(洞察力)
  • 长期思考:有的问题需要多年的斯奎
  • 宽广胸怀:一个人的出发点直接决定了选题可能得高度
  • 好的选题非一日之功

提升创新能力

==新?==

==创新?==: 省时省钱爽

学习策略

专注

不可能什么都去学

要“偏科”

向“牛人”学习

  • 思维方式
  • 一本好书
  • 一门好课
  • 一个好资源

思考什么方式学习最高效

善假于物

投资时间

  • 花一定的时间去学习去提高我们的效率,从而节省我们的时间
  • 辛苦很可能是因为无知,要减少自己的无效时间(这就是==低效==)
  • 最怕还不是低效的行为,而是认为这就是世界的运转方式,而意识不到自己的低效,因此,要有==刀刃向内的勇气==,多问问自己,多查查资料,看看有没有更好更快的方法
  • 两类不知道知识
    • 我知道自己不知道某个知识
    • 我根本不知道自己不知道某个知识
      • 见常人所不能见
      • 多看,多了解
      • 被动
      • 做一个复杂、耗时的事情,要思考有没有方法能够变得更简单(尤其是重复的)

总结

流程化+工具+投资时间

建立分享意识

  • 分享是高效、积极、主动的学习
  • 分享是学习的动力所在(输出带动输入)
  • 分享可以为自己带来更多的机会(怀才不遇)
  • 分享可以建立自己的品牌,扩大人脉资源
  • 分享是对知识的主动传播
  • 分享可以检验需求!身边每个牛叉的人都是积极的分享者

费曼学习法:把别人讲明白

汇总

  • 有所学,有所不学的==专注意识==
  • 学习精华内容的==(牛人)意识==
  • 善假于物的==工具意识==
  • 提升效率的==时间投资==意识
  • 传播知识共同成长的==分享意识==

当一项技能不能形成习惯,就无法给我们的生命带来改变

学习与搜索

学习与搜索

课堂三问

  • ==[过去]==这个工具出现的背景是什么?这个工具是怎么发展的?
  • ==[现在]==如何使用这个工具?对我有何价值?
  • ==[未来]==未来这个工具还会怎样发展?有没有更好的解决方案?

什么是学习

把别人的变成自己的

搜索引擎

什么是需求

  • 需求就是客服种种不便,消灭抱怨
  • 每一个抱怨的背后都隐藏着一个未被满足的需求,每一个需求的背后必然隐藏着一个不可忽视的市场

为什么你愿意花钱

快速学习

读论文方法推荐

  • 思维导图梳理

流程化

  • 减少流程间的切换次数
  • 专注在一件事情上:在相对一定的时间中,保持专注

方法

  1. 信息检索

    思考如何准确检索到与主题密切相关的信息;注意检索式,信息来源,时效性等等

  2. 信息收集

    可以通过为知笔记快速收集信息,(信息同步速度会受网络状态影响)

  3. 信息筛选

    通过星标阅读来整理信息

  4. 信息整理

    结合星标、笔记和思维导图来整理信息

  5. 信息输出

    总结成文字,提交作业

科研入门及十大信息源

规划

什么是科研

略(上有)

科研目的

更加爽

科研从哪里开始

科研从需求开始,需求在哪里?

掌握信息,要对领域有一个比较深的了解

十大信息源

  1. 专利
  2. 会议文献
  3. 期刊
  4. 科技报告
  5. 学位论文
  6. 科技档案
  7. 图书
  8. 产品资料
  9. 标准
  10. 政府出版物

  • 来自产品的方法往往比文献中的方法更加可靠
  • 时间出现顺序从上至下

快速调研

处于不同段位检索的方法

  • 检索与目标要紧密结合
  • 入门阶段:快 (体现快速学习能力)
  • 解决问题阶段:广 和 准 (快速解决问题、广阔思路)
  • 选题阶段要: 广 深 新 (要求高)

如何快速了解导师(实验室)的研究方向?

  • 导师的在研课题的基金申请书
  • 自然基金系统,主动查询摘要
  • 实验室网页
  • 实验室的毕业论文
  • 实验室近年发表的论文
  • 师兄师姐
  • 直接和导师沟通

文献数据库及其利用

问题

去哪里查

如何检索

如何管理

如何阅读

WOS 数据库

新文章 or 学科发展相对较慢的论文的重要性

  • 看使用次数

使用

文献检索的前中后阶段

  • 检索前;检索目的、数据库选择、检索策略
  • 检索中:结果的浏览、分析鱼信息输出
  • 检索后:管理、阅读、整理

论文管理工具

==需求==就是做某件事觉得不爽

==创新==就是解决遇到的不爽

快速定位关键文献

困惑

文献很多

选择重要文献研读之

文献帅选的策略鱼调研目的相关

  • 跟踪进展:
  • 特定参考
  • 全面调研
    • 综述
    • 专著
    • 教材 or 教科书 (宽泛)
    • 专家

获取重要资料

  • 专家推荐
  • 文献检索
  • 文献分析

引文分析软件

下载的数据,文献之间没有关联,那么说明检索是不太合适的。如果检索是合适的,那么这些文献之间会相互引用

高效调研

信息检索

  1. 我在哪里可以检索到信息,如何选择关键词

    • 信息源

      • 十大信息源

        • 会议文献
        • 期刊
        • 科技报告
        • 学位论文
        • 图书
      • 网站搜索

      • 科学新闻:简洁易懂

      • 课题申请指南 (新的前沿)

      • 社群

      • 微信公众号

      • 科研项目数据库

      • 博客

      • 面对面交流

      • 实地考察

      • 内部档案

    • 选词

      • 宽泛检索,逐步凝练;通过网页介绍发现关键词
        • 网页
        • 知网
        • 知网指数分析功能
        • 百度学术
      • 关键词分析
        • endnote
      • histcite分析(作图+cited reference)
    • 判断关键词是否宽泛

      • 根据cited references来判断
      • 根据histcite作图中的关联情况来判断
  2. 如何利用数据库和其他信息源

筛选

二分法

标题 -> 摘要 -> 全文

工具

  • zetero

  • histcite

    链接

  • web of science

    • ==登录时不要使用科学上网==
    • 数据库:==Web of Science 核心合集==
    • 记录内容:==全纪录与引用的参考文献==
  • endnote

pd 配置

共享文件夹

选择 整个 $HOME 目录吧

应用程序

打印机

启动顺序

启动顺序 -> 高级 -> BIOS:EFI 64位

CD/DVD

隔离 linux 与 mac

不建议打开

关闭3d加速

安装

从 Live USB 启动 Arch Linux

为 Arch Linux 创建实时 USB 后,关闭计算机,将 USB 插入其中,然后启动系统。

请记住,在某些情况下,您可能无法从启用安全启动的实时 USB 启动。如果您是这种情况,请先访问 BIOS 并禁用安全启动。

Arch Linux 安装有两种可能的选项——传统模式和 UEFI 模式。UEFI(统一可扩展固件接口)模式更新,大多数现代硬件仅支持 UEFI 安装。因此,本 Arch Linux 安装指南将使用 UEFI 模式。

正确启动到 Arch ISO 后,您应该会看到类似于下图所示的内容。

选择Arch Linux install medium (x86_64, UEFI)选项并点击Enter

安装程序解压并加载 Linux 内核后, 您将自动跳转到具有 root 权限的终端。

验证与 Internet 的连接

首先,检查互联网连接。要检查互联网连接,只需 ping 一个网站,如下例所示。

如果你收到一个错误信息,请检查你的互联网连接或路由器。

对磁盘进行分区

我们 Arch Linux 安装指南的下一步是对硬盘进行分区。如果您不熟悉诸如 fdisk 或之类的分区工具,那么您很可能会在这里遇到最大的麻烦cfdisk。不过别担心,这很容易,你会看到。

首先列出您的磁盘:

1
fdisk -l

出于本指南的目的,我们创建一个虚拟机,其中包含一个由 /dev/sda。更改/dev/sda为您的设备名称。

对于基本分区,我们需要创建以下分区布局:

  • /dev/sda1: EFI 系统分区,大小为 512 MB,FAT32 格式。这为存储引导加载程序和引导所需的其他文件提供了空间。
  • /dev/sda2交换分区,4GB 大小。交换空间用于将虚拟内存扩展到已安装的物理内存 (RAM) 之外,或用于挂起磁盘支持。
  • /dev/sda3Linux分区,剩余可用磁盘空间大小,EXT4格式。这是存储我们的 Arch Linux 操作系统、文件和其他信息的根 (/) 分区。

创建 EFI 系统分区

现在让我们通过cfdisk 对机器硬盘驱动器运行命令来实际开始创建磁盘布局分区表 。

1
cfdisk /dev/sda

选择 GPT 标签类型并点击Enter

然后 从底部菜单中选择 Free Space 并点击 New。您可以使用Tab或 箭头键浏览菜单选项。

以 MB ( 1024M) 为单位键入分区大小,然后按Enter键。

/dev/sda1分区仍然被选中的情况下,从底部菜单中选择Type并选择EFI System分区类型。

现在,您已完成 EFI 系统分区的配置。

创建交换分区

j 键下移选项

现在让我们使用相同的过程创建 Swap 分区。再次选择剩余的Free space和 并点击New

以 GB ( 4G) 为单位键入分区大小,然后按Enter键。

/dev/sda2分区仍然被选中的情况下,从底部菜单中选择Type并选择Linux swap分区类型。

现在,您已经完成了 Swap 分区的配置。

创建根分区

最后,您需要创建根 ( /) 分区。再次选择剩余的Free space并点击New

对于 ( /) 大小,保留默认大小值。这意味着,所有剩余的可用空间。按Enter键。

/dev/sda3分区仍然被选中的情况下,从底部菜单中选择Type并选择Linux filesystem分区类型。

现在,您已经完成了根分区的配置。

将更改写入磁盘

接下来,您需要保存所做的更改。选择Write从底部菜单和命中Enter

键入yes并按下Enter键。

我们到此结束。选择Quit并按下Enter即可。

创建文件系统

现在您已准备好磁盘分区,是时候在其上创建文件系统了。但是让我们首先通过运行来查看分区表摘要:

1
fdisk -l

/dev/sda磁盘应该有三个分区(/dev/sda1dev/sda2,和/dev/sda3)类似于上面所示的那些。

前面步骤中分区的创建只是在硬盘提供的存储空间上划出边界,并指定了每条边界线之间的空间类型。现在,是时候用所需的文件系统格式化分区了。

我们必须在这里创建 3 个文件系统,所以让我们开始吧。

对于 EFI 分区类型,创建一个 FAT32 文件系统。

1
mkfs.fat -F32 /dev/sda1

准备交换分区:

1
2
mkswap /dev/sda2
swapon /dev/sda2

对于根分区,创建一个 ext4 文件系统:

1
mkfs.ext4 /dev/sda3

安装 Arch Linux

修改镜像源

进入/etc/pacman.d目录,修改mirrorlist文件,默认情况下,mirrorlist文件里面包含了许多源地址,这样会导致下载程序包速度很慢,我们只需要启用中国的源地址就可以:

1
2
3
cd /etc/pacman.d
cp mirrorlist mirrorlist.backup
vim mirrorlist

修改镜像源

  • 全部下载, 打开China

    1
    2
    3
    # curl -o /etc/pacman.d/mirrorlist https://archlinux.org/mirrorlist/all/
    # vim /etc/pacman.d/mirrorlist
    # 找到 China, 删除 China 下的列表中的 '#'
  • 只下载China的

    1
    # curl -o /etc/pacman.d/mirrorlist https://archlinux.org/mirrorlist/\?country\=CN

同步 pacman 存储库

首先同步 pacman 存储库,以便您可以下载和安装软件:

1
pacman -Syy

挂载根分区 & 安装

我们必须先将根分区 ( /dev/sda3)挂载到 /mnt目录中,然后才能执行所有的安装。

1
mount /dev/sda3 /mnt

安装根分区后,是时候安装所有必要的包了。使用该 pacstrap 命令安装 Arch Linux 所需的软件包。

1
pacstrap /mnt base base-devel linux linux-firmware sudo nano net-tools

下载和安装这些软件包需要一些时间。现在我们可以开始配置我们的系统了。

注: 若不放心,可以多运行几遍 pacstrap /mnt base linux linux-firmware sudo nano

配置已安装的 Arch 系统

fstab文件

生成一下fstab文件,让系统明白自己是谁和自己在哪。

1
genfstab -U /mnt >> /mnt/etc/fstab

然后可以执行以下命令看看fstab文件是否生成正确。

1
cat /mnt/etc/fstab

arch-chroot

以root身份进入我们的新系统,这次是进来是为了配置系统的。

1
arch-chroot /mnt

先告诉系统现在的时间是基于哪个时区的。

1
2
3
4
5
# 设定时区
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# 设置硬件时钟
hwclock -w

设置语言

先安装个vim,当然也可以装nano,看哪个用得顺手。

1
2
3
4
5
# vim
pacman -S vim

# nano
pacman -S nano

编辑/etc/locale.gen

1
2
3
4
5
# vim 方式
vim /etc/locale.gen

# nano方式
nano /etc/locale.gen

把以下几行开头的#符号去掉。

1
2
3
en_US.UTF-8 UTF-8
en_GB.UTF-8 UTF-8 // 这行看是否需要
zh_CN.UTF-8 UTF-8

编辑完保存退出,然后执行下面的指令。

1
locale-gen

现在来编辑一下local.conf

1
2
3
4
5
6
7
8
# vim 方式
vim /etc/locale.conf

# nano 方式
nano /etc/locale.conf

##### 编辑以下内容后保存退出
LANG=en_US.UTF-8

配置网络

创建编辑/etc/hostname,自己定义一个主机名,例如:

1
localhost

然后编辑/etc/hosts,内容如下:

1
2
3
127.0.0.1    localhost
::1 localhost
127.0.1.1 localhost

执行以下命令去应用网络服务自启动:

1
2
systemctl enable systemd-networkd
systemctl enable systemd-resolved
1
2
3
4
5
6
7
pacman -S dhcpcd networkmanager

systemctl enable dhcpcd
systemctl enable NetworkManager

使用无线网络的话,则需安装以下几个软件包(未验证)
pacman -S iw wpa_supplicant dialog (有线网络忽略这行)

配置以太网:(可选)

编辑/etc/systemd/network/20-wired.network,内容如下:

1
2
3
4
5
[Match]
Name=ens33

[Network]
DHCP=yes

虽然没啥用,vmware 约等于静态ip了

修改ROOT密码

1
passwd

在 Arch Linux 上安装 GRUB 引导程序

现在我们安装引导加载程序,以便 Arch 在重新启动后启动。Linux 发行版和 Arch Linux 的默认引导加载程序也由 GRUB 包表示。

安装 GRUB 引导加载程序和 EFI 引导管理器包:

1
pacman -S grub efibootmgr os-prober mtools

然后创建挂载点 /dev/sda1并挂载它。

1
2
mkdir /boot/efi
mount /dev/sda1 /boot/efi

现在让我们安装我们的引导加载程序。

1
grub-install –-target=x86_64-efi –-bootloader-id=grub_uefi

输出

1
2
Installing for x86_64-efi platform.
Installation finished. No error reported.

最后,生成 /boot/grub/grub.cfg 文件。

1
grub-mkconfig -o /boot/grub/grub.cfg

创建一个普通用户帐户

用户帐户的创建方法也会自动为用户创建主目录。另外,我们可以给这个账号sudo权限。写比如我们的是Qeuroal

1
useradd -m -G wheel Qeuroal

请务必为新用户设置密码:

1
passwd Qeuroal

接下来,sudo为新创建的用户启用权限:

1
2
3
4
// 使用vim编辑visudo
EDITOR=vim visudo


向下滚动屏幕并找到以下行:

1
2
3
# %wheel ALL=(ALL) ALL
// 在文件中添加如下内容
用户名 ALL=(ALL) ALL

通过删除#符号取消注释。

Arch Linux 安装完成

恭喜!现在我们已经完成了 Arch Linux 的安装,所以我们现在将从终端退出并卸载我们的根分区并重新启动到我们新安装的 Arch Linux 系统

1
2
3
exit
umount -R /mnt
reboot

parallels tools

安装KDE桌面

集显驱动安装

1
2
3
4
5
lspci | grep -e VGA -e 3D	//查看显卡类型

pacman -Ss xf86-video //查询所有开源驱动

sudo pacman -S xf86-video-intel //安装intel集显驱动

安装Xorg

Xorg (简体中文) - ArchWiki (archlinux.org)

1
pacman -S xorg

安装sddm

1
2
3
pacman -S sddm

systemctl enable sddm // 设置开机自启

安装Plasma桌面

  • kde-applications:KDE 的全套应用(软件太多了,很多用不到,在此不安装)
  • konsole:终端
1
pacman -S plasma konsole

重启

1
reboot

How to Install Parallels Tools on Manjaro

Steps required to install Paralells Tools on Manjaro Linux

Steps

  1. Install Manjaro Linux on a VM (it’s recommended to take a snapshot before installing Parallels Tools)

  2. Update the OS

    1
    pacman -Syu
  3. Install the latest LTS kernel and kernel headers (See this)

    Note: To install the headers, run uname -r and look at the first two numbers to figure out the package name. For example if the output of uname -r is 4.19.32-1-MANJARO the kernel headers package name for your kernel would be linux419-headers.

  4. Install base-devel package group and DKMS

    1
    pacman -S base-devel dkms
  5. Connect the Parallels Tools image and run the installer script

    1
    2
    3
    mkdir /mnt/cdrom
    mount /dev/cdrom /mnt/cdrom
    /mnt/cdrom/install

安装

安装 zsh

安装

1
2
3
4
5
6
7
8
# macOS
brew install zsh zsh-completions

# Ubuntu
sudo apt-get install zsh

# CentOS
sudo yum -y install zsh

切换

1
chsh -s /bin/zsh

安装zsh框架

选择其中一个安装即可

安装 oh-my-zsh

1
2
3
4
5
# mac
bash -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# ubuntu
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

安装 zim

  1. 安装 zim

    1
    curl -fsSL https://raw.githubusercontent.com/zimfw/install/master/install.zsh | zsh
  2. 编辑 Zsh 配置文件 ~/.zimrc

    1
    vim ~/.zimrc
  3. 在文件最后加入下面的一行文字,以添加 powerlevel10k 模块,然后退出。

    1
    zmodule romkatv/powerlevel10k
  4. 安装 powerlevel10k 模块,在终端输入如下命令即可。

    1
    zimfw install

    安装之后,powerlevel10k会让你进行配置。

    在此时配置时,很多图标符号看不到,因为 powerlevel10k 中包含许多特殊图标符号,需要与之兼容的字体。

安装 powerlevel10K 主题

安装主题

方法1 (推荐)

1
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git $ZSH_CUSTOM/themes/powerlevel10k

方法2

1
2
cd ./.oh-my-zsh/themes
git clone https://github.com/romkatv/powerlevel10k.git

配置主题

~/.zshrc 设置如下内容, 以使用p10k主题

1
ZSH_THEME="powerlevel10k/powerlevel10k"

插件

语法高亮插件

1
cd ~/.oh-my-zsh/custom/plugins/ && git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

自动补全

1
cd ~/.oh-my-zsh/custom/plugins/ && git clone https://github.com/zsh-users/zsh-autosuggestions

自动跳转

1
2
3
4
5
# mac
brew install autojump

# ubuntu
sudo apt install autojump

zshrc 配置插件

oh-my-zsh框架

接下来 ~/.zshrc 在插件配置处添加下载的这两个插件名

1
2
3
4
5
6
plugins=(
git
zsh-syntax-highlighting
zsh-autosuggestions
autojump
)

另外, 时间戳改一下格式

1
HIST_STAMPS="yyyy-mm-dd"

zim框架

  • mac

    1
    [[ -s /usr/local/etc/profile.d/autojump.sh  ]] && . /usr/local/etc/profile.d/autojump.sh
  • linux

    • ubuntu

      1
      [[ -s /usr/share/autojump/autojump.sh ]] && . /usr/share/autojump/autojump.sh
    • archlinux

      1
      [[ -s /usr/share/autojump/autojump.bash ]] && . /usr/share/autojump/autojump.bash

      [!ERROR] /usr/share/autojump/autojump.bash:34: command not found: complete
      .zshrc 文件中添加以下命令

      1
      2
      autoload bashcompinit
      bashcompinit

安装依赖字体

运行 p10k configure 向导模式进行p10k的主题定制

完成后会生成一个配置文件 ~/.p10k.zsh,并且在 ~/.zshrc 中自动加入了

下载字体

推荐 JetBrainsMono Nerd Font

网站

  • Nert Fonts 下载

    推荐 JetBrainsMono Nerd Font, 点击下载(v3.0.2)

  • Nert Fonts

  • Nerd Font Github

  • Hack Nerd Font Github

  • Caskaydia Cove Nerd Font

  • MesloLGS NF 官网地址

    1
    2
    3
    4
    5
    mkdir fonts
    curl -o ./fonts/MesloLGS\ NF\ Regular.ttf https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/MesloLGS%20NF%20Regular.ttf
    curl -o ./fonts/MesloLGS\ NF\ Bold.ttf https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/MesloLGS%20NF%20Bold.ttf
    curl -o ./fonts/MesloLGS\ NF\ Italic.ttf https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/MesloLGS%20NF%20Italic.ttf
    curl -o ./fonts/MesloLGS\ NF\ Bold\ Italic.ttf https://raw.githubusercontent.com/romkatv/powerlevel10k-media/master/MesloLGS%20NF%20Bold%20Italic.ttf

安装字体

  1. linux

    1
    bash ./install.sh -of
  2. manually

    1. Unzip the archive (解压)
    2. install the font (安装)
      • macOS
        Select all font files in the folder and ==double-click== the “Install Font” button.
      • Windows
        Select all font files in the folder, right-click any of them, then pick “==Install==” from the menu.
      • Linux
        Unpack fonts to ~/.local/share/fonts (or /usr/share/fonts, to install fonts system-wide) and ==fc-cache -f -v==
    3. Restart your software and choose the font

设置字体

Profiles -> Open Profiles -> Edit Profiles -> Text修改字体。红色矩形框勾选,字体其实有好几个,至于用哪个Powerline结尾的都行,只是大小和间距略有不同。

快捷键

快捷键 功能
⌃ + u 清空当前行
⌃ + a 移动到行首
⌃ + e 移动到行尾
⌃ + f 向前移动 (front) 前面
⌃ + b 向后移动 ( before )在某某之前
⌃ + p 上一条命令 ( prev )
⌃ + n 下一条命令 ( next )
⌃ + r 搜索历史命令
⌃ + y 召回最近用命令删除的文字
⌃ + h 删除光标之前的字符
⌃ + d 删除光标所指的字符
⌃ + w 删除光标之前的单词
⌃ + k 删除从光标到行尾的内容
⌃ + t 交换光标和之前的字符
⌘ + Click 可以打开文件,文件夹和链接
⌘ + n 新建窗口
⌘ + t 新建标签页
⌘ + w 关闭当前页
⌘ + 数字&⌘ + 方向键 切换标签页
⌥⌘ + 数字 切换窗口
⌘ + enter 切换全屏
⌘ + d 左右分屏
⇧⌘ + d 上下分屏
⌘ + ; 自动补全历史记录
⇧⌘ + h 自动补全剪贴板历史
⌥⌘ + e 查找所有来定位某个标签页
⌘ + r&⌃ + l 清屏
⌘ + / 显示光标位置
⌥⌘ + b 历史回放
⌘ + f 查找,然后用tab和⇧ + tab可以向右和向左补全,补全之后的内容会被自动复制, 还可以用⌥ + enter将查找结果输入终端

QAs

  • warning: completion was already initialized before completion module. Will call compinit again. See https://github.com/zimfw/zimfw/wiki/Troubleshooting#completion-is-not-working

    • archlinux 运行命令: echo 'skip_global_init=1' >> ~/.zshenv
    • ubuntu 运行命令: echo 'skip_global_compinit=1' >> ~/.zshenv
  • autojump (linux): ImportError: No module named autojump_argparse

    修改 /usr/bin/autojump 文件首行 #!/usr/bin/env python#!/usr/bin/python3, 这两个有哪个 python 程序就写哪个路径.

重要

使用已经写好的脚本安装,github地址

Readme上有使用方法

安装 vimmars

1
2
3
git clone https://github.com/Qeuroal/vimmars.git
cd ~/vimmars
bash ./install.sh

安装 ycm

全部语言的安装

1
2
cd ~/.vim/plugged/YouCompleteMe
python3 ./install.py --all

c/c++

1
2
cd ~/.vim/plugged/YouCompleteMe
python3 ./install.py --clang-completer

推荐几个 Vim 配置方案

推荐另外几个出色的 Vim 教程

图片

自动命令

事件

类别 事件 触发条件
读取 BufNewFile 编辑一个新文件时
读取 BufReadPre 读入新缓冲区之前
读取 BufRead, BufReadPost 读入新缓冲区之后
读取 BufReadCmd 开始编辑新缓冲区之前
读取 FileReadPre 使用:read命令读入文件之前
读取 FileReadPost 使用:read命令读入文件之后
读取 StdinReadPre 由标准输入设备读入缓冲区之前
读取 StdinReadPost 由标准输入设备读入缓冲区之后
写入 BufWrite, BufWritePre 将整个缓冲区写入文件时
写入 BufWritePost 将整个缓冲区写入文件之后
写入 BufWriteCmd 将整个缓冲区写入文件之前
缓冲区 BufAdd, BufCreate 将缓冲区加入缓冲区列表之后
缓冲区 BufDelete 从缓冲区列表中移除缓冲区之前
缓冲区 BufEnter 进入缓冲区之后
缓冲区 BufLeave 离开缓冲区之前
缓冲区 BufWinEnter 在窗口中显示缓冲区之后
缓冲区 BufWinLeave 从窗口中关闭缓冲区之前
缓冲区 BufNew 创建缓冲区之后
缓冲区 BufUnload 卸载缓冲区之前
选项 FileType 设置’filetype’选项之后
选项 Syntax 设置’syntax’选项之后
选项 EncodingChanged ‘encoding’选项改变之后触发命令;
选项 OptionSet 设置任何选项之后
启动退出 VimEnter Vim启动并载入初始化文件之后
启动退出 GUIEnter 启动GUI之后
启动退出 VimLeavePre 改写viminfo文件之前,退出Vim之前
启动退出 VimLeave 改写viminfo文件之后,退出Vim之前
其他 FileChangedShell 当文件的最后修改时间等属性发生改变时
其他 InsertEnter 进入插入模式时
其他 InsertLeave 离开插入模式时
其他 FocusGained Vim成为当前窗口时
其他 FocusLost Vim不再是当前窗口时
其他 WinEnter 进入窗口时
其他 WinLeave 离开窗口时
其他 CursorMoved 在常规模式下移动光标时
其他 CursorMovedI 在插入模式下移动光标时
其他 CursorHold 当超过’updatetime’所指定时间用户没有输入时
其他 vimResized 窗口尺寸变化之后

示例

操作 事件
启动Vim并创建默认窗口 BufWinEnter
创建默认缓冲区 BufEnter
:edit a.txt VimEnter
创建新缓冲区 BufNew
将新缓冲区加入到缓冲区列表之中 BufAdd
退出默认缓冲区 BufLeave
退出默认窗口 BufWinLeave
将默认缓冲区从缓冲区列表之中移除 BufUnload
删除默认缓冲区 BufDelete
将a.txt文件读入新缓冲区 BufReadCmd
激活新缓冲区 BufEnter
激活新窗口 BufWinEnter
进入插入模式 InsertEnter
输入文本 CursorMovedI
退出插入模式 InsertLeave
:wq BufWriteCmd
退出新窗口 BufWinLeave
将新缓冲区从缓冲区列表之中移除 BufUnload
准备退出Vim VimLeavePre
退出Vim VimLeave

命令模式常用快捷键

  • G 移动到文件的最后一行。
  • nG n为数字,移动到文件的第n行。
  • gf   打开以光标所在字符串为文件名的文件
  • gg   移动到文件的第一行。
  • N[Enter]  向下移动N行。
  • h   向左移动光标(
  • j   向下移动光标(
  • k   向上移动光标(
  • l(小L) 向右移动光标(
  • 20j  向下移动20行
  • +Enter  把光标移至下一行第一个非空白字符
  • -  把光标移至上一行第一个非空白字符
  • ^ 把光标移至本行第一个非空白字符
  • 0或Home 移动光标至行首
  • $或End 移动光标至行尾
  • w   向右移动一个单词,光标停在下一个单词开头
  • W 向右移动一个单词,光标移动时忽略一些标点
  • b   向左移动一个单词,光标停在下一个单词开头
  • B   向左移动一个单词,光标移动时忽略一些标点
  • e   向右移动一个单词,光标停在下一个单词末尾
  • E   向右移动一个单词,光标移动时忽略一些标点
  • ge   向左移动一个单词,光标停在下一个单词末尾
  • gE   向左移动一个单词,光标移动时忽略一些标点
  • H   移动光标至屏幕的最上方那一行行首
  • M   移动光标至屏幕的最中间那一行行首
  • L   移动光标至屏幕的最下方那一行行首
  • ( 移动光标至上一句
  • ) 移动光标至下一句
  • { 移动光标至上一段
  • } 移动光标至上一段
  • ctrl+b  屏幕向上移动一页
  • ctrl+f  屏幕向下移动一页
  • ctrl+d  屏幕向上移动半页
  • ctrl+u  屏幕向下移动半页
  • ctrl+e  屏幕向下移动一行
  • n% 到文件n%的位置,如1%,50%, 100%之类
  • zz 将当前行移动到屏幕中央
  • zt 将当前行移动到屏幕顶端
  • zb 将当前行移动到屏幕底端
  • dd   剪切当前行(不是删除,因为此时在缓存内保存了一份剪切的字符串)。
  • d$   删除光标位置到该行最后一个字符(包括光标字元)。
  • d0   删除光标字元至行首字符(不包括光标字元)。
  • dG   删除光标所在行至最后一行所有字符(包括光标所在行)。
  • d1G  删除光标所在行(包括光标所在行)与首行间所有字符。
  • ndd  删除光标所在行(包括)向下的n行。
  • dw   删除光标所在字符至下一个单词间所有字符(包括下一个单词前面的空格)。
  • daw  剪切光标所在位置的单词。
  • das 剪切光标所在位置的句子。
  • x   向后删除一个字符。
  • nx   向后删除n个字符。
  • yy   复制当前行。
  • nyy  复制n行(包含当前行)。
  • y1G  复制第一行至当前行所有字符。
  • yG   复制当前行到最后一行所有字符。
  • y0   复制当前行行首字符至光标字符间所有字符。
  • y$   复制当前行光标字符到行尾字符间所有字符。
  • yaw  复制光标所在位置的单词。
  • yas  复制光标所在位置的句子。
  • ZZ   保存更改并退出。
  • p   将已经复制的数据粘贴到光标的下一行(若是使用y0,y$复制的行的一部分,则粘贴时,会粘贴到光标所在行的光标字符后面)。
  • P   将已经复制的数据粘贴到光标的上一行(若是使用y0,y$复制的行的一部分,则粘贴时,会粘贴到光标所在行的光标字符前面)。
  • u   复原上一操作(撤消)。
  • ctrl+R  重做上一操作。
  • .   点号,重做上一操作。
  • J   将当前行与下一行合并为一行。
  • gJ   将当前行与下一行合并为一行,不过合并后不留空格。
  • << 段落向左缩进一个shiftwidth
  • >> 段落向右缩进一个shiftwidth
  • gq 对选中的长文字进行重排
  • o   当前行下插入空行,并进入插入模式。
  • O   当前行上插入空行,并进入插入模式。
  • i   进入插入模式,从当前光标所在处插入。
  • I   进入插入模式,从当前行第一个非空格处开始插入。
  • a   进入插入模式,从当前光标所在处下一个字符开始插入。
  • A  进入插入模式,从当前光标所在行最后一个字符开始插入。
  • 8i=<ESC> 重复输入小技巧,先按数字8,再按i进入插入模式,再按=或其他字符,如=-,再按Esc退出键,再按一次键,就会出现8=-组成的字符串。如=-=-=-=-=-=-=-=-
  • ESC健  返回命令模式。

编辑模式

在命令模式下按ioa等键可进入插入模式(编辑模式)。

  • o  当前行下插入空行,并进入插入模式。
  • O  当前行上插入空行,并进入插入模式。
  • i  进入插入模式,从当前光标所在处插入。
  • I  进入插入模式,从当前行第一个非空格处开始插入。
  • a  进入插入模式,从当前光标所在处下一个字符开始插入。
  • A  进入插入模式,从当前光标所在行最后一个字符开始插入。

另外按字母s也可以进入插入模式:

  • s  小s,删除光标字符,并进入编辑模式,并在光标所在位置开始编辑。
  • S  大S,删除光标所在行,并进入编辑模式,并在行首开始编辑。
  • ns 删除包含光标字符在内的n个字符,并进入编辑模式。
  • nS 大S,删除光标所在行在内及其后的n-1行,并进入编辑模式。

扩展命令模式常用操作键

  • :r filename 在当前位置插入另一个文件的内容
  • :[n]r filename 在当前文件第n行插入另一个文件的内容
  • :r !date 在光标处插入当前日期与日期,同理,:r !command可以将其他shell命令的输出插入到当前文档
  • :r !cat filename|head -n N 在当前文件光标的下一行插入文件filename的前N行
  • :w 保存
  • :q  离开vim
  • :q!  强制离开
  • :wq  保存后离开
  • :w [filename]    将当前文本另存为filename
  • :n1,n2 w [filename]  将n1至n2行内容另存为filename
  • :ce(nter) 本行文字居中
  • :le(ft) 本行文字靠左
  • :ri(ght) 本行文字靠右
  • /word          向下查找关键字word。
  • ?word          向上查找关键字word。
  • :n1,n2s/word1/word2/g  在第n1行与第n2行之间查找word1字符串,并直接替换为word2。
  • :1,$s/word1/word2/g   在第1行最后一行之间查找word1字符串,并直接替换为word2。
  • :1,$s/word1/word2/gc    在第1行最后一行之间查找word1字符串,并替换为word2(替换时询问用户是否替换)。
  • :.,+5s/^/#/g        将当前行及下面5行标记为注释(包括当前行,共6行)。
  • :6,9 de          删除第6至第9行。
  • :2,8 co 10        复制第2行至第8行内容至第10行后面(从下一行开始)
  • :set nu          显示行号。
  • :set nonu        不显示行号。
  • :set hlsearch        高亮度搜索显示
  • :set nohlsearch      不高亮度搜索显示
  • :set autoindent/noautoindent  自动缩进
  • :set ruler/noruler      设置右下角状态栏说明
  • :set showmode/noshowmode  设置是否显示左下角(-INSERT-)状态列
  • :set backspace=2      =2时,退格键可删除任意字符,=0或=1时只能删除刚输入的字符。
  • :set          显示与系统预设值不同的设定参数。
  • :set all          显示所有的环境参数值
  • :syntax on/off      设置语法高亮
  • :set bg=dark/light      设置背景(background)为暗色/亮色
  • :set tabstop=4       设置Tab宽度
  • :set shiftwidth=4      设置每一级缩进的宽度
  • :Sex 水平分割一个窗口,浏览文件系统
  • :Vex 垂直分割一个窗口,浏览文件系统

vimrc常用环境变量设置

  • set hlsearch    “设置高亮度反白
  • set backspace=2    “可随时用退格键删除
  • set ruler      “可显示最后一行的状态
  • set showmode    “显示状态
  • set nu      “显示行号
  • set bg=dark    “显示不同的底色色调
  • set softtabstop=4 “统一缩进为4
  • set tabstop=4    “设置Tab宽度
  • set shiftwidth=4    “设置每一级缩进的宽度
  • set fileencodings=utf-8,gbk,gb18030,gk2312
  • syntax on      “语法高亮
  • set showcmd    “输入的命令显示出来,看的清楚些
  • set clipboard+=unnamed    “共享剪贴板
  • set cursorline    “突出显示当前行
  • set noeb      “去掉输入错误的提示声音
  • set confirm      “在处理未保存或只读文件的时候,弹出确认
  • set autoindent    “设置自动缩进
  • set cindent “设置自动缩进
  • set expandtab  “用空格代替制表符
  • set smarttab    “在行和段开始处使用制表符
  • set laststatus=2    “总是显示状态栏

~/.vimrc配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
set hlsearch
set backspace=2
set ruler
set showmode
set nu
set bg=dark
set softtabstop=4
set shiftwidth=4
set fileencodings=utf-8,gbk,gb18030,gk2312
syntax on
set showcmd
set clipboard+=unnamed
set cursorline
set confirm
set autoindent
set cindent
set expandtab
set laststatus=2

可视模式

  • 可视模式(Visual Black),也即区块选择模式,此时可以对列字符进行操作。
  • 在命令模式下,按vVctrl+v可以进入到可视模式。
  • v    进入可视模式,并进行单字符(反白)选择。
  • V    进入可视模式,并进行行(反白)选择。
  • ctrl+v  进入可视模式,并进行区块选择,以长方形的方式(反白)选择字符。
  • y    将反白的地方复制下来。
  • d    删除反白区域。
  • 进入可视模式后,可以按方向键或h/j/k/l(向左、向下、向上、向右)进行反白区域选择。

多视窗功能

  • 输入:sp进入多视窗模式,此时,可以对文本进行前后对照或不同档案间进行对照。
  • :sp 同一文件两个视窗,进行前后对照。
  • :sp [filename] 两个文件进行对照。

在多视窗模式下:

  • ctrl+w 可依次在多个视窗间进行上下循环切换。
  • ctrl+w+k 可依次在多个视窗间进行从下到上切换(先按ctrl+w,松开后,再按k)。
  • ctrl+w+j 可依次在多个视窗间进行从上到下切换(先按ctrl+w,松开后,再按j)。
  • ctrl+w+q 关闭当前视窗下面的视窗(先按ctrl+w,松开后,再按q)。

GDB

GDB(GNU symbolic Debugger)是Linux系统下的强大的调试工具,可以用来调试ada, c, c++, asm, minimal, d, fortran, objective-c, go, java,pascal 等多种语言。

我们以调试go代码为示例来介绍GDB的使用。源码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func add(a, b int) int {
sum := 0
sum = a + b
return sum
}
func main() {
sum := add(10, 20)
fmt.Println(sum)
}

构建二进制应用:

1
go build -gcflags="-N -l" -o test main.go

启动调试

1
2
gdb ./test
gdb --args ./test arg1 arg2 # 指定参数启动

进入gdb调试界面之后,执行run命令运行程序。若程序已经运行,我们可以attach该程序的进程id进行调试:

1
2
$ gdb
(gdb) attach 1785

当执行attach命令的时候,GDB首先会在当前工作目录下查找进程的可执行程序,如果没有找到,接着会用源代码文件搜索路径。我们也可以用file命令来加载可执行文件。

或者通过命令设置进程id:

1
2
gdb test 1785 
gdb test --pid 1785

若已运行的进程不含调试信息,我们可以使用同样代码编译出一个带调试信息的版本,然后使用file和attach命令进行运行调试。

1
2
3
4
$ gdb
(gdb) file test
Reading symbols from test...done.
(gdb) attach 1785

可视化窗口

GDB也支持多窗口图形启动运行,一个窗口显示源码信息,一个窗口显示调试信息:

1
gdb test -tui

GDB支持在运行过程中使用Crtl+X+A组合键进入多窗口图形界面, GDB支持的快捷操作有:

1
2
3
Crtl+X+A // 多窗口与单窗口界面切换
Ctrl + X + 2 // 显示两个窗口
Ctrl + X + 1 // 显示一个窗口

运行程序

通过run命令运行程序:

1
(gdb) run

指定命令行参数运行:

1
(gdb) run arg1 arg2

或者通过set命令设置命令行参数:

1
2
(gdb) set args arg1 arg2
(gdb) run

除了run命令外,我们也可以使用start命令运行程序。start命令会在在main函数的第一条语句前面停下来。

1
(gdb) start

断点设置、查看、删除、禁用

设置断点

GDB中是通过break命令来设置断点(BreakPoint),break可以简写成b

  • break function

    在指定函数出设置断点,设置断点后程序会在进入指定函数时停住

  • breshak linenum

    在指定行号处设置断点

  • break +offset/-offset

    在当前行号的前面或后面的offset行处设置断点。offset为自然数

  • break filename:linenum

    在源文件filename的linenum行处设置断点

  • break filename:function

    在源文件filename的function函数的入口处设置断点

  • break \*address

    在程序运行的内存地址处设置断点

  • break

    break命令没有参数时,表示在下一条指令处停住。

  • break … if

    …可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break if i=100,表示当i为100时停住程序

查看断点

我们可以通过info命令查看断点:

1
2
(gdb) info breakpoint # 查看所有断点
(gdb) info breakpoint 3 # 查看3号断点

删除断点

删除断点是通过delete命令删除的,delete命令可以简写成d

1
(gdb) delete 3 # 删除3号断点

断点启用与禁用

1
2
(gdb) disable 3 # 禁用3号断点
(gdb) enable 3 # 启用3号断点

调试

单步执行

next用于单步执行,会一行行执行代码,运到函数时候,不会进入到函数内部,跳过该函数,但会执行该函数,即step over。可以简写成n

1
(gdb) next

单步进入

step用于单步进入执行,跟next命令类似,但是遇到函数时候,会进入到函数内部一步步执行,即step into。可以简写成s

1
(gdb) step

step相关的命令stepi,用于每次执行每次执行一条机器指令。可以简写成si

继续执行到下一个断点

continue命令会继续执行程序,直到再次遇到断点处。可以简写成c:

1
2
(gdb) continue
(gdb) continue 3 # 跳过3个断点

继续运行到指定位置

until命令可以帮助我们实现运行到某一行停住,可以简写成u

1
(gdb) until 5

跳过执行

skip命令可以在step时跳过一些不想关注的函数或者某个文件的代码:

1
2
(gdb) skip function add   # step时跳过add函数
(gdb) info skip # 查看skip列表

其他相关命令:

  • skip delete [num] 删除skip
  • skip enable [num] 启动skip
  • skip disable [num] 关闭skip

注意: 当不带skip号时候,是针对所有skip进行设置。

执行完成当前函数

finish命令用来将当前函数执行完成,并打印函数返回时的堆栈地址、返回值、参数值等信息,即step out

1
(gdb) finish

查看源码

GDB中的list命令用来显示源码信息。list命令可以简写成l

  • list

    从第一行开始显示源码,继续输入list,可列出后面的源码

  • list linenum

    列出linenum行附近的源码

  • list function

    列出函数function的代码

  • list filename:linenum

    列出文件filename文件中,linenum行出的代码

  • list filename:function

    列出文件filename中,函数function的代码

  • list +offset/-offset

    列出在当前行号的前面或后面的offset行附近的代码。offset为自然数。

  • list +/-

    列出当前行后面或者前面的代码

  • list linenum1, linenum2

    列出行linenum1和linenum2之间的代码

查看信息

info命令用来显示信息,可以简写成i

  • info files

    显示当前的debug文件,包含程序入口地址,内存分段布局位置信息等

  • info breakpoints

    显示当前设置的断点列表

  • info registers

    显示当前寄存器的值,可以简写成i r。指定寄存器名称,可以查看具体寄存器信息:i r rsp

    如: 查看 rax 寄存器的信息: info reg rax

  • info all-registers

    显示所有寄存器的值。GDB提供四个标准寄存器:pc是程序计数器寄存器,sp是堆栈指针。fp用于记录当前堆栈帧的指针,ps用于记录处理器状态的寄存器,GDB会处理好不同架构系统寄存器不一致问题,比如对于amd64架构,pc对应就是rip寄存器。

    引用寄存器内容是将寄存器名前置$符作为变量来用。比如$pc就是程序计数器寄存器值。

  • info args

    显示当前函数参数

  • info locals

    显示当前局部变量

  • info frame

    查看当前栈帧的详细信息,包括rip信息,正在运行的指令所在文件位置

  • info variables

    查看程序中的变量符号

  • info functions

    查看程序中的函数符号

  • info functions regexp

    通过正则匹配来查看程序中的函数符号

  • info goroutines

    显示当前执行的goroutine列表,带*的表示当前执行的。注意需要加载go runtime支持。

  • info stack

    查看栈信息

  • info proc mappings

    可以简写成i proc m。用来查看应用内存映射

  • info proc [procid]

    显示进程信息

  • info proc status

    显示进程相关信息,包括user id和group id;进程内有多少线程;虚拟内存的使用;挂起的信号,阻塞的信号,忽略的信号;TTY;消耗的系统和用户时间;堆栈大小;nice值

  • info display

  • info watchpoints

    列出当前所设置了的所有观察点

  • info line [linenum]

    查看第linenum的代码指令地址信息,不带linenum时,显示的是当前位置的指令地址信息

  • info source

    显示此源代码的源代码语言

  • info sources

    显示程序中所有有调试信息的源文件名,一共显示两个列表:一个是其符号信息已经读过的,一个是还未读取过的

  • info types

    显示程序中所有类型符号

  • info types regexp

    通过正则匹配来查看程序中的类型符号

其他类似命令有:

  • show args

    查看命令行参数

  • show environment [envname]

    查看环境变量信息

  • show paths

    查看程序的运行路径

  • whatis var1

    显示变量var1类型

  • ptype var1

    显示变量var1类型,若是var1结构体类型,会显示该结构体定义信息。

查看调用栈

通过where可以查看调用栈信息:

1
2
3
4
5
6
(gdb) where
#0 _rt0_amd64 ()
at /usr/lib/go/src/runtime/asm_amd64.s:15
#1 0x0000000000000001 in ?? ()
#2 0x00007fffffffdd2c in ?? ()
#3 0x0000000000000000 in ?? ()

设置观察点

通过watch命令,可以设置观察点。当观察点的变量发生变化时,程序会停下来。可以简写成wa

1
(gdb) watch sum

查看汇编代码

我们可以通过开启disassemble-next-line自动显示汇编代码。

1
(gdb) set disassemble-next-line on

当面我们可以查看指定函数的汇编代码:

1
(gdb) disassemble main.main

disassemble可以简写成disas。我们也可以将源代码和汇编代码一一映射起来后,查看

1
(gdb) disas /m main.main

GDB默认显示汇编指令格式是AT&T格式,我们可以改成intel格式:

1
(gdb) set disassembly-flavor intel

自动显示变量值

display命令支持自动显示变量值功能。当进行next或者step等调试操作时候,GDB会自动显示display所设置的变量或者地址的值信息。

display命令格式:

1
2
3
display <expr>
display /<fmt> <expr>
display /<fmt> <addr>
  • expr是一个表达式
  • fmt表示显示的格式
  • addr表示内存地址

其他相关命令:

  • undisplay [num]: 不显示
  • delete display [num]: 删除
  • disable display [num]: 关闭自动显示
  • enable display [num]: 开启自动显示
  • info display: 查看display信息

注意: 当不带display号时候,是针对所有display进行设置。

显示将要执行的汇编指令

我们可以通过display命令,实现当程序停止时,查看将要执行的汇编指令:

1
2
(gdb) display /i $pc # 为了让每次执行都能看到汇编代码
(gdb) display /3i $pc # 一次性显示3条指令

==> 指的那一行就是正准备执行的代码, 但还没有执行的

取消显示可以用undisplay命令进行操作。

查看backtrace信息

backtrace命令用来查看栈帧信息。可以简写成bt

1
2
3
4
(gdb) backtrace # 显示当前函数的栈帧以及局部变量信息
(gdb) backtrace full # 显示各个函数的栈帧以及局部变量值
(gdb) backtrace full n # 从内向外显示n个栈桢,及其局部变量
(gdb) backtrace full -n # 从外向内显示n个栈桢,及其局部变量

切换栈帧信息

frame命令可以切换栈帧信息:

1
(gdb) frame n # 其中n是层数,最内层的函数帧为第0帧

其他相关命令:

  • info frame: 查看栈帧列表

调试多线程

GDB中有一组命令能够辅助多线程的调试:

  • info threads

    显示当前可调式的所有线程,线程 ID 前有 “*” 表示当前被调试的线程。

  • thread threadid

    切换线程到线程threadid

  • set scheduler-locking [on|off|step]

    多线程环境下,会存在多个线程运行,这会影响调试某个线程的结果,这个命令可以设置调试的时候多个线程的运行情况,on 表示只有当前调试的线程会继续执行,off 表示不屏蔽任何线程,所有线程都可以执行,step 表示在单步执行时,只有当前线程会执行。

  • thread apply [threadid] [all] args

    对线程列表执行命令。比如通过thread apply all bt full可以查看所有线程的局部变量信息。

查看运行时变量

print命令可以用来查看变量的值。print命令可以简写成pprint命令格式如下:

1
print [</format>] <expr>

format用来设置显示变量的格式,可选的。可用值有如下:

  • x 按十六进制格式显示变量
  • d 按十进制格式显示变量
  • u 按十六进制格式显示无符号整型
  • o 按八进制格式显示变量
  • t 按二进制格式显示变量
  • a 按十六进制格式显示变量
  • c 按字符格式显示变量
  • f 按浮点数格式显示变量
  • z 按十六进制格式显示变量,左侧填充零

expr可以是一个变量,也可以是表达式,也可以是寄存器:

1
2
3
4
5
6
(gdb) p var1 # 打印变量var1
(gdb) p &var1 # 打印变量var1地址
(gdb) p $rsp # 打印rsp寄存器地址
(gdb) p $rsp + 8 # 打印rsp加8后的地址信息
(gdb) p 0xc000068fd0 # 打印0xc000068fd0转换成10进制格式
(gdb) p /x 824634150864 # 打印824634150864转换成16进制格式

print也支持查看连续内存,@操作符用于查看连续内存,@的左边是第一个内存的地址的值,@的右边则想查看内存的长度。

例如对于如下代码:int arr[] = {2, 4, 6, 8, 10};,可以通过如下命令查看arr前三个单元的数据:

1
2
(gdb) p *arr@3
$2 = {2, 4, 6}

查看内存中的值

examine命令用来查看内存地址中的值,可以简写成xexamine命令的语法如下所示:

examine /<n/f/u>

  • n 表示显示字段的长度,也就是说从当前地址向后显示几个地址的内容。
  • f 表示显示的格式
    • d 数字 decimal
    • u 无符号数字 unsigned decimal
    • s 字符串 string
    • c 字符 char
    • u 无符号整数 unsigned integer
    • t 二进制 binary
    • o 八进制格式 octal
    • x 十六进制格式 hex
    • f 浮点数格式 float
    • i 指令 instruction
    • a 地址 address
    • z 十六进制格式,左侧填充零 hex, zero padded on the left
  • u 表示从当前地址往后请求的字节数,默认是4个bytes
    • b 一个字节 byte
    • h 两个字节 halfword
    • w 四个字节 word
    • g 八个字节 giantword

示例:

1
2
3
4
5
6
7
8
9
(gdb) x/10c 0x4005d4 # 打印前10个字符
(gdb) x/16xb a # 以16进制格式打印数组前a16个byte的值
(gdb) x/16ub a # 以无符号10进制格式打印数组a前16个byte的值
(gdb) x/16tb a # 以2进制格式打印数组前16个abyte的值
(gdb) x/16xw a # 以16进制格式打印数组a前16个word(4个byte)的值
(gdb) x $rsp # 打印rsp寄存器执行的地址的值
(gdb) x $rsp + 8 # 打印rsp加8后的地址指向的值
(gdb) x 0xc000068fd0 # 打印内存0xc000068fd0指向的值
(gdb) x/5i schedule # 打印函数schedule前5条指令

修改变量或寄存器值

set命令支持修改变量以及寄存器的值:

1
2
3
(gdb) set var var1=123 # 设置变量var1值为123
(gdb) set var $rax=123 # 设置寄存器值为123
(gdb) set environment envname1=123 # 设置环境变量envname1值为123

查看命令帮助信息

help命令支持查看GDB命令帮助信息。

1
2
(gdb) help status # 查看所有命令使用示例
(gdb) help x # 查看x命令使用帮助

搜索源文件

search命令支持在当前文件中使用正则表达式搜索内容。search等效于forward-search命令,是从当前位置向前搜索,可以简写成foreverse-search命令功能跟forward-search恰好相反,其可以简写成rev

1
2
(gdb) search func add # 从当前位置向前搜索add方法
(gdb) rev func add # 从当前为向后搜索add方法

执行shell命令

我们可以通过shell指令来执行shell命令。

1
2
(gdb) shell cat /proc/27889/maps # 查看进程27889的内存映射。若想查看当前进程id,可以使用info proc命令获取
(gdb) shell ls -alh

GDB对go runtime支持

  • runtime.Breakpoint():触发调试器断点。
  • runtime/debug.PrintStack():显示调试堆栈。
  • log:适合替代 print显示调试信息

进一步阅读

配置环境

安装 Hexo

接下来就需要安装 Hexo 了,这是一个博客框架,Hexo 官方还提供了一个命令行工具,用于快速创建项目、页面、编译、部署 Hexo 博客,所以在这之前我们需要先安装 Hexo 的命令行工具。 命令如下:

1
$ npm install -g hexo-cli

信息提前说明

配置文件说明

在 Hexo 中有两份主要的配置文件,其名称都是 _config.yml。 其中,一份位于*站点根目录下,主要包含 Hexo 本身的配置;另一份位于主题目录*下,这份配置由主题作者提供,主要用于配置主题相关的选项。

为了描述方便,在以下说明中,将前者称为 站点配置文件, 后者称为 主题配置文件

初始化 hexo

  1. 创建文件夹

    1
    $ mkdir io_github_qeuroal
  2. 初始化

    1
    2
    $ hexo init
    $ mkdir ~/Desktop/hexo && ls -al > ~/Desktop/hexo/hexo_init.info
  3. 将 Hexo 编译生成 HTML 代码

    1
    $ hexo generate
  4. 本地运行

    1
    $ hexo server
  5. 站点配置文件

    • 修改根目录下的 _config.yml 文件,找到 Site 区域,这里面可以配置站点标题 title、副标题 subtitle 等内容、关键字 keywords 等内容.

      例:

      1
      2
      3
      4
      5
      6
      # Site
      title: NightTeam
      subtitle: 一个专注技术的组织
      description: 涉猎的主要编程语言为 Python、Rust、C++、Go,领域涵盖爬虫、深度学习、服务研发和对象存储等。
      keywords: "Python, Rust, C++, Go, 爬虫, 深度学习, 服务研发, 对象存储"
      author: NightTeam
    • language 的字段设置为 zh-CN

配置主题

Pure 主题

使用 pure, 教程见这里的 《Hexo搭建个人博客并部署到Github》和《Hexo博客主题pure使用说明》

下载主题

  1. 下载

    1
    $ git clone https://github.com/cofess/hexo-theme-pure.git themes/pure
  2. 启用主题

    修改 站点配置文件

    1
    theme: pure

更新主题 (可选, 暂未使用过)

1
2
$ cd themes/pure
$ git pull

主题配置

导航菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 导航菜单
menu:
Home: .
Archives: archives # 归档
Categories: categories # 分类
Tags: tags # 标签
Repository: repository # github repositories
Books: books # 书单
Links: links # 友链
About: about # 关于

# 导航菜单图标(font awesome)
menu_icons:
enable: true # 是否启用菜单图标
home: fa-dashboard
archives: fa-delicious
categories: fa-folder
tags: fa-tags
repository: fa-code
books: fa-leanpub
links: fa-gg
about: fa-coffee

主题配置

1
2
3
4
5
6
7
8
9
10
11
# config
config:
skin: theme-black # 主题颜色 theme-blue theme-green theme-purple
layout: main-center # 布局方式 main-left main-center main-right
toc: true # 是否开启文章章节目录导航
menu_highlight: false # 是否开启当前菜单高亮显示
thumbnail: false # enable posts thumbnail, options: true, false
excerpt_link: Read More
#New
isNewTabLinks: true #是否链接打开新标签页
autoUnfold: true # 默认展开文章目录

页面显示

1
2
3
4
5
6
7
8
9
# Pagination
pagination:
number: false
prev:
alwayShow: true
next:
alwayShow: true
midSize: 2 # 当前页码左右到省略号显示的页码数,默认2,表现为 1 ... 4 5 6 7 8 ... 10
showContent: false # 页面文章小于2篇时显示文章内容

大图显示

1
2
# Fancybox
fancybox: false

捐赠

直接更改照片文件即可

修改为自己的名字

  • cofess<your name>

修改 example.com

全局搜索 example.com 将其替换成网站网址 Qeuroal.top

个人链接

1
2
3
4
5
6
links:
Github: https://github.com/Qeuroal
Blog: http://blog.Qeuroal.com
微博: http://weibo.com/Qeuroal
# 花瓣: http://huaban.com/Qeuroal
# Behance: https://www.behance.net/Qeuroal

个人标签

1
2
3
# My Personal Labels
labels:
- 后端

搜索 (未处理)

主题内置三种站内搜索方式:insight、swiftype、baidu

1
2
3
4
5
# Search
search:
insight: true # you need to install `hexo-generator-json-content` before using Insight Search
swiftype: # enter swiftype install key here
baidu: false # you need to disable other search engines to use Baidu search

文章启用目录索引

1
2
3
4
5
6
title: 文章标题
categories:
- 文章分类
tags:
- 文章标签
toc: true # 是否启用内容索引

分享

支持weibo,qq,qzone,wechat,tencent,douban,diandian,facebook,twitter,google,linkedin

1
2
3
4
5
6
# Share
# weibo,qq,qzone,wechat,tencent,douban,diandian,facebook,twitter,google,linkedin
share:
enable: true # 是否启用分享
sites: weibo,qq,wechat,facebook,twitter # PC端显示的分享图标
mobile_sites: weibo,qq,qzone # 移动端显示的分享图标

分类, 标签, 仓库, about等菜单的配置

/theme/pure/_source 下的所有文件复制到 /source

友情链接

复制theme/pure/_source/ 目录下links文件夹到blog path/source/ 目录下

在 hexo 目录下的 source 文件夹内创建一个名为 _data(禁止改名)的文件夹。

然后在文件内创建一个名为 links.yml 的文件,在其中添加相关数据即可。

单个友情链接的格式为:

1
2
3
4
Name:
link: http://example.com
avatar: http://example.com/avatar.png
desc: "这是一个描述"

添加多个友情链接,我们只需要根据上面的格式重复填写即可。

数学公式

Hexo默认使用 “hexo-renderer-marked” 引擎渲染网页,该引擎会把一些特殊的 markdown 符号转换为相应的 html 标签

解决方案

解决方案有很多,可以网上搜下,为了节省大家的时间,这里只提供亲身测试过的方法。

更换 Hexo 的 markdown 渲染引擎,hexo-renderer-markdown-it-plus 引擎替换默认的渲染引擎 hexo-renderer-marked 即可。

安装hexo-renderer-markdown-it-plus插件
1
2
npm un hexo-renderer-marked --save
npm i hexo-renderer-markdown-it-plus --save
配置

安装插件后,如果未正常渲染LaTeX数学公式,在博客配置文件_config.yml中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
markdown_it_plus:
highlight: true
html: true
xhtmlOut: true
breaks: true
langPrefix:
linkify: true
typographer:
quotes: “”‘’
plugins:
- plugin:
name: markdown-it-katex
enable: true
- plugin:
name: markdown-it-mark
enable: false
文章启用mathjax
1
2
title: Hello World
mathjax: true

文章简介内容不全显式

1
showContent: false # 页面文章小于2篇时显示文章内容

删除部分文件

  • themes/pure/_config.yml.example
  • themes/pure/README.cn.md
  • themes/pure/README.md

关闭订阅

  • 1
    2
    # rss
    # rss: /atom.xml
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    social:
    links:
    github: https://github.com/Qeuroal
    weibo: http://weibo.com/Qeuroal
    twitter: https://twitter.com/Qeuroal
    # facebook: /
    # dribbble: /
    behance: https://www.behance.net/Qeuroal
    # rss: atom.xml

更改头像

1
2
3
profile:
# avatar: images/avatar.jpg
avatar: images/avatar.png

更改个人简介

文件: /source/about/index.md

安装插件

hexo-deployer-git (must, common)

安装
1
$ npm install hexo-deployer-git --save
站点配置(根目录) config.yml

_config.yml

1
2
3
4
5
6
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: git
repo: {git repo ssh address}
branch: master

hexo-generator-feed (normal, 但不推荐订阅)

Github:https://github.com/hexojs/hexo-generator-feed

简介

RSS的生成插件,你可以在配置显示你站点的RSS,文件路径\atom.xml。

安装:
1
$ npm install hexo-generator-feed --save
配置:

在博客配置文件_config.yml中添加

1
2
3
4
5
6
feed:
type: atom
path: atom.xml
limit: 20
hub:
content:
  • type - Feed type. (atom/rss2)
  • path - Feed path. (Default: atom.xml/rss2.xml)
  • limit - Maximum number of posts in the feed (Use 0 or false to show all posts)
  • hub - URL of the PubSubHubbub hubs (Leave it empty if you don’t use it)
  • content - (optional) set to ‘true’ to include the contents of the entire post in the feed.

hexo-generator-json-content (must)

Github

https://github.com/alexbruno/hexo-generator-json-content

简介

用于生成静态站点数据,提供搜索功能的数据源。

安装
1
$ npm install hexo-generator-json-content --save
配置

在博客配置文件_config.yml中添加

1
2
3
4
5
6
7
jsonContent:
ignore:
- path/to/a/page
- url/to/one/post
- an-entire-category
- specific.file
- .ext # a file extension

在github上用hexo搭建了自己的博客,用typora写完一篇博文,满怀欣喜地deploy之后发现图片加载失败…真的很吐血。去网上一搜索,嗯,好像不少人都遇到了这个问题,解决方法看似一大堆实则无效或过时不匹配,绝大多数资料都是安装hexo-asset-image的插件,亲测对于markdown图片路径没有用啊!比出现问题更折磨人的是遇上一堆错误的解决方法…

我想要寻找在本地和网页上都能显示的办法,终于发现了一款插件hexo-image-link,是将markdown图片路径转换为asset_img语法,使得图片能够同时显示在typora和hexo上。

1
{% asset_img "local-image.png" "image file label" %} -> {% asset_img label local-image.png %}
具体步骤
  • 修改_config.yml中的post_asset_folder: true

  • 安装hexo-image-link

    1
    $ npm install hexo-image-link --save
  • 修改 站点配置文件post_asset_folder: true

  • 如果 npm下载比较慢的话,尝试 cnpm下载

  • 安装

    1
    2
    $ npm install -g cnpm --registry=https://registry.npm.taobao.org
    $ cnpm install hexo-image-link --save
  • 修改md文件中的图片路径

hexo-autonofollow (bad)

Github:https://github.com/liuzc/hexo-autonofollow

简介:自动为站外链接添加nofollow属性

安装:

1
$ npm install hexo-autonofollow --save

配置:

在博客配置文件_config.yml中添加

1
2
3
4
5
nofollow:
enable: true
exclude:
- exclude1.com
- exclude2.com
  • enable - 是否启用
  • exclude - 排除域名

配置主题无意义的组件

豆瓣书单 (关闭, 毫无意义)

复制theme/pure/_source/ 目录下books文件夹到blog path/source/ 目录下

1
2
3
4
5
# douban 豆瓣书单
douban:
user: *** # 豆瓣用户名
start: 0 # 从哪一条记录开始
count: 100 # 获取豆瓣书单数据条数
评论 (关闭, 鸡肋)
1
2
comment:
type: # 启用哪种评论系统, 不填即关闭

Next 主题

下载主题

  1. 最新版本:

    Download the Latest Master Branch

  2. 稳定版本

    Download the Latest Release Version

  3. 指定版本

    Download the Specific Release Version

    In rare cases useful, but not recommended.
    You must define version. Let’s take v8.0.0 as an example. Replace it with any version from tags list.

  4. git 下载

    1
    2
    $ cd io_github_qeuroal
    $ git clone https://github.com/next-theme/hexo-theme-next themes/next

教程: Installation

主题配置

修改 theme/next/_config.yml

主题配置文件

1
2
3
4
5
#Schemes
#scheme: Muse
#scheme: Mist
scheme: Pisces
#scheme: Gemini
  1. 将 about、tags、categories 前的 #号去掉,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    menu:
    home: / || fa fa-home
    about: /about/ || fa fa-user
    tags: /tags/ || fa fa-tags
    categories: /categories/ || fa fa-th
    archives: /archives/ || fa fa-archive
    #schedule: /schedule/ || fa fa-calendar
    #sitemap: /sitemap.xml || fa fa-sitemap
    #commonweal: /404/ || fa fa-heartbeat
  2. 新建相关页面

    在博客根目录下执行下列命令

    1
    2
    3
    4
    $ hexo new page "about"
    $ hexo new page "tags"
    $ hexo new page "categories"
    $ hexo new page 404
  3. 修改生成页面的配置

    1
    2
    3
    4
    source/about/index.md
    source/tags/index.md
    source/categories/index.md
    source/404/index.md
  4. 404 相关

    front-matter 中添加 permalink: /404,表示指定该页面固定链接为 http://“主页”/404.html

    1
    2
    3
    4
    5
    6
    7
    ---
    title: 404 Not Found:该页无法显示
    toc: false
    comments: false
    permalink: /404
    ---
    <script type="text/javascript" src="//www.qq.com/404/search_children.js" charset="utf-8" homePageUrl="<%- config.url %>" homePageName="回到我的主页"></script>

设置左上角或右上角 github 图标

主题配置文件,启用 github-banner 如下:

1
2
3
4
github_banner:
enable: true
permalink: https://https://github.com/Qeuroal
title: Follow me on GitHub

设置侧栏阅读进度百分比

编辑站点配置文件,修改 back2top 部分如下

1
2
3
4
copyback2top:
enable: true
sidebar: true
scrollpercent: true

设置网页底部信息

查看主题配置文件,修改 footer 配置如下:

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
footer:
# Specify the year when the site was setup. If not defined, current year will be used.
#since: 2021

# Icon between year and copyright info.
icon:
# Icon name in Font Awesome. See: https://fontawesome.com/icons
name: fa fa-heart
# If you want to animate the icon, set it to true.
animated: false
# Change the color of icon, using Hex Code.
color: "#ff0000"

# If not defined, `author` from Hexo `_config.yml` will be used.
# Set to `false` to disable the copyright statement.
copyright:

# Powered by Hexo & NexT
powered: true

# Beian ICP and gongan information for Chinese users. See: https://beian.miit.gov.cn, http://www.beian.gov.cn
beian:
enable: false
icp:
# The digit in the num of gongan beian.
gongan_id:
# The full num of gongan beian.
gongan_num:
# The icon for gongan beian. See: http://www.beian.gov.cn/portal/download
gongan_icon_url:

图片懒加载设置

在主题配置文件中启用 lazyload

1
copylazyload: true

设置代码块复制和代码高亮

在主题配置文件中修改 codeblock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
codeblock:
# Code Highlight theme
# All available themes: https://theme-next.js.org/highlight/
theme:
light: atom-one-dark
dark: stackoverflow-dark
prism:
light: prism
dark: prism-dark
# Add copy button on codeblock
copy_button:
enable: true
# Available values: default | flat | mac
style:
# Fold code block
fold:
enable: false
height: 500

修改文章底部标签样式

在主题配置文件中修改

1
copytag_icon: true

网站icon

1
2
3
4
5
6
favicon:
small: /images/favicon.png
medium: /images/favicon.png
apple_touch_icon: /images/avatar.jpg
safari_pinned_tab: /images/avatar.jpg
#android_manifest: /manifest.json

侧边栏头像

1
2
3
4
5
6
7
8
# Sidebar Avatar
avatar:
# Replace the default image and set the url here.
url: /images/avatar.png
# If true, the avatar will be displayed in circle.
rounded: true
# If true, the avatar will be rotated with the cursor.
rotated: true

侧边栏社交链接

修改主题配置文件

1
2
3
4
5
6
7
8
9
10
social:
GitHub: https://github.com/Qeuroal || fab fa-github
#E-Mail: mailto:yourname@gmail.com || fa fa-envelope
#Weibo: https://weibo.com/yourname || fab fa-weibo
#Twitter: https://twitter.com/yourname || fab fa-twitter
#FB Page: https://www.facebook.com/yourname || fab fa-facebook
#StackOverflow: https://stackoverflow.com/yourname || fab fa-stack-overflow
#YouTube: https://youtube.com/yourname || fab fa-youtube
#Instagram: https://instagram.com/yourname || fab fa-instagram
#Skype: skype:yourname?call|chat || fab fa-skype

在文章底部增加版权信息

编辑 主题配置文件,修改如下配置:

1
2
3
4
5
6
7
8
9
10
creative_commons:
# Available values: by | by-nc | by-nc-nd | by-nc-sa | by-nd | by-sa | cc-zero
license: by-nc-sa
# Available values: big | small
size: small
sidebar: false
post: true
# You can set a language value if you prefer a translated version of CC license, e.g. deed.zh
# CC licenses are available in 39 languages, you can find the specific and correct abbreviation you need on https://creativecommons.org
language:

开启缓存

1
2
3
# Allow to cache content generation. Introduced in NexT v6.0.0.
cache:
enable: true

压缩

1
2
# Remove unnecessary files after hexo generate.
minify: true

侧边栏显示分类条数

1
2
# Posts / Categories / Tags in sidebar.
site_state: true

友链

1
2
3
4
5
6
7
8
9
10
# Blog rolls 友链
links_settings:
icon: fa fa-link # 设置icon
title: 友情链接 # 设置标题
# Available values: block | inline
layout: block # css样式,是一行一条,还是自动换行

links:
友链1: http://yoursite.com
#... 依次追加即可

文章toc

1
2
3
4
5
6
toc:
enable: true # 是否开启
number: true # 自动显示文章编号
wrap: false # 溢出是否换行
expand_all: true # 展开所有还是手风琴
max_depth: 4 # 显示最大深度

顶部元信息

1
2
3
4
5
6
7
post_meta:
item_text: true # 使用中文显示
created_at: true
updated_at:
enable: true
another_day: true
categories: true

首页摘要

1
2
excerpt_description: false  # 关闭自动提取
read_more_btn: trues # 显示阅读全文
  1. 手动控制

    使用<!--more-->控制

  2. 使用 front-matter 中添加 descriptionexcerpt 字段

    1
    2
    3
    4
    ---
    description: your excerpt
    excerpt: your excerpt
    ---

安装插件

hexo-deployer-git (must, common)

  1. 安装

    1
    $ npm install hexo-deployer-git --save
  2. 站点配置(根目录) config.yml

    _config.yml

    1
    2
    3
    4
    5
    6
    # Deployment
    ## Docs: https://hexo.io/docs/deployment.html
    deploy:
    type: git
    repo: {git repo ssh address}
    branch: master

在github上用hexo搭建了自己的博客,用typora写完一篇博文,满怀欣喜地deploy之后发现图片加载失败…真的很吐血。去网上一搜索,嗯,好像不少人都遇到了这个问题,解决方法看似一大堆实则无效或过时不匹配,绝大多数资料都是安装hexo-asset-image的插件,亲测对于markdown图片路径没有用啊!比出现问题更折磨人的是遇上一堆错误的解决方法…

我想要寻找在本地和网页上都能显示的办法,终于发现了一款插件hexo-image-link,是将markdown图片路径转换为asset_img语法,使得图片能够同时显示在typora和hexo上。

1
{% asset_img "local-image.png" "image file label" %} -> {% asset_img label local-image.png %}
具体步骤
  • 修改_config.yml中的post_asset_folder: true

  • 安装hexo-image-link

    1
    $ npm install hexo-image-link --save
  • 修改 站点配置文件post_asset_folder: true

注:

  • 如果 npm下载比较慢的话,尝试 cnpm下载

  • 安装

    1
    2
    $ npm install -g cnpm --registry=https://registry.npm.taobao.org
    $ cnpm install hexo-image-link --save
  • 修改md文件中的图片路径

hexo-generator-searchdb

  1. 安装 exo-generator-searchdb 这个插件

    1
    $ npm install hexo-generator-searchdb --save
  2. 配置 主题配置文件: _config.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Local Search
    # Dependencies: https://github.com/next-theme/hexo-generator-searchdb
    local_search:
    enable: true
    # If auto, trigger search by changing input.
    # If manual, trigger search by pressing enter key or search button.
    trigger: auto
    # Show top n results per article, show all results by setting to -1
    top_n_per_article: 1
    # Unescape html strings to the readable one.
    unescape: false
    # Preload the search data when the page loads.
    preload: false

部署

  1. 配置站点配置文件 _config.yml

    1
    2
    3
    4
    5
    6
    # Deployment
    ## Docs: https://hexo.io/docs/deployment.html
    deploy:
    type: git
    repo: {git repo ssh address}
    branch: master
  2. 部署

    1
    $ hexo deploy

自定义域名

添加 CNAME 文件

/source 文件夹下添加 CNAME 文件, 如下所示:

hexo 常用命令

1
2
3
4
5
6
7
$ hexo new "postName" # 新建文章
$ hexo new page "pageName" # 新建页面
$ hexo generate # 生成静态页面至public目录
$ hexo server # 开启预览访问端口(默认端口4000,'ctrl + c'关闭server)
$ hexo deploy # 将.deploy目录部署到GitHub
$ hexo help # 查看帮助
$ hexo version # 查看Hexo的版本

git

删除远程仓库里的某个文件/文件夹

在git中可以用git rm命令删除文件(删除远程仓库文件)

1
2
3
4
5
6
git clone 仓库地址
git add .
step1: git rm 文件 //本地中该文件会被删除
step2: git rm -r 文件夹 //删除文件夹
step3: git commit -m '删除某个文件'
step4: git push (origin master)

上面的方法会把对应的本地文件也删除掉,如果不想把本地文件删除,只把缓存区中的对应部分删除,则加上 --cached

1
2
git rm --cached 文件 //本地中该文件不会被删除
git rm -r --cached 文件夹 //删除文件夹

git add . 后面执行上面的命令,再推送到 github 远程仓库上的时候,仓库里面对应的文件/文件夹就会被删除

1
2
3
4
5
6
git clone 仓库地址
git add .
step1: git rm --cached 文件 //本地中该文件不会被删除
step2: git rm -r --cached 文件夹 //删除文件夹
step3: git commit -m '删除某个文件'
step4: git push (origin master)

git commit后,如何撤销commit

修改了本地的代码,然后使用:

1
2
git add file
git commit -m '修改原因'

复制

执行commit后,还没执行push时,想要撤销这次的commit,该怎么办?

解决方案: 使用命令:

1
git reset --soft HEAD^

复制

这样就成功撤销了commit,如果想要连着add也撤销的话,–soft改为–hard(删除工作空间的改动代码)。

1
2
HEAD^ 表示上一个版本,即上一次的commit,也可以写成HEAD~1
如果进行两次的commit,想要都撤回,可以使用HEAD~2

复制

1
2
--soft
不删除工作空间的改动代码 ,撤销commit,不撤销git add file

复制

1
2
--hard
删除工作空间的改动代码,撤销commit且撤销add

复制

另外一点,如果commit注释写错了,先要改一下注释,有其他方法也能实现,如:

1
2
git commit --amend
这时候会进入vim编辑器,修改完成你要的注释后保存即可。

撤销 git add . 并保留修改的方法

执行完 git add .

文件退出暂存区,但是修改保留:

1
git reset --mixed

撤销所有的已经 add 的文件:

1
git reset HEAD .

撤销某个文件或文件夹:

1
git reset HEAD  -filename

另外:可以用 git status Git 会告诉你可以通过那个命令来执行操作。

补充

可以用 git status Git 会告诉你可以通过那个命令来执行操作。

push 失败

case 1

OpenSSL SSL_read: Connection was reset, errno 10054的话就把代理关了

删除未跟踪文件

1
2
# 删除 untracked files
git clean -f
1
2
# 连 untracked 的目录也一起删掉
git clean -fd
1
2
# 连 gitignore 的untrack 文件/目录也一起删掉 (慎用,一般这个是用来删掉编译出来的 .o之类的文件用的)
git clean -xfd
1
2
3
4
# 在用上述 git clean 前,墙裂建议加上 -n 参数来先看看会删掉哪些文件,防止重要文件被误删
git clean -nxfd
git clean -nf
git clean -nfd

多终端同步

新建私有仓库

1
2
3
4
5
6
7
echo "# Readme" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M master
git remote add origin <your https repo address>
git push -u origin master

文件夹说明

文件夹 说明 是否需要上传github
node_modules hexo需要的模块,就是一些基础的npm安装模块,比如一些美化插件,在执行npm install的时候会重新生成 不需要
themes 主题文件 ==需要==
public hexo g命令执行后生成的静态页面文件 不需要
packages.json 记录了hexo需要的包的信息,之后换电脑了npm根据这个信息来安装hexo环境 ==需要==
_config.yml 全局配置文件,这个不用多说了吧 ==需要==
.gitignore hexo生成的默认的.gitignore模块 ==需要==
scaffolds 文章的模板 ==需要==
.deploy_git hexo g自动生成的 不需要

.gitignore内容

1
2
3
4
5
6
7
.DS_Store
Thumbs.db
db.json
*.log
node_modules/
public/
.deploy*/

删除.git文件

删除所有主题的 .git 文件,如:rm -rf ./theme/pure/.git

查找 .git 文件

在==根目录==下执行以下命令

1
find . -name ".git*"

上传

1
2
3
git add .
git commit -m "<yout commit statement>"
git push

新电脑上的操作

  1. 下载

    1
    git clone <your repo>
  2. 安装依赖

    1
    npm install

    npm install其实就是读取了 packages.json 里面的信息,自动安装依赖,有的小伙伴可能只执行npm install就行了,不过按照上面的三步是最稳妥的

  3. 仓库更新

    为了保证同步,推荐先合并更新再进行博客的编写

    • pull (不推荐)

      1
      git pull
    • fetch

      1
      2
      git fetch --all    #将远程git仓库上最新的内容拉取到本地,将本地库所关联的远程库更新至最新
      git reset --hard origin/master #强制将本地内容指向刚刚同步git云端内容

      reset 对所拉取的文件不做任何处理,此处不用 pull 是因为本地尚有许多文件,使用 pull 会有一些版本冲突,解决起来也麻烦,而本地的文件都是初始化生成的文件,较拉取的库里面的文件而言基本无用,所以直接丢弃。

  4. 部署

    1
    2
    hexo clean && hexo g && hexo s
    hexo d

更新脚本

1

QAs

Error: Cannot find module ‘hexo-util’

  • 方法1

    1. rm -rf node_modules
    2. npm install hexo-util
  • 方法2

    For those who also encounter this issue, please check your NPM version.

    • > 3: Still not work. Please remove node_modules directory and reinstall using npm install.
    • < 3: Please add hexo-util explicitly via npm install –save-dev hexo-util to you site package deps.

    reference: https://github.com/iissnan/hexo-theme-next/issues/1490

基本概念

简介

模型训练5要素

  • 数据:包括数据读取,数据清洗,进行数据划分和数据预处理,比如读取图片如何预处理及数据增强。
  • 模型:包括构建模型模块,组织复杂网络,初始化网络参数,定义网络层。
  • 损失函数:包括创建损失函数,设置损失函数超参数,根据不同任务选择合适的损失函数。
  • 优化器:包括根据梯度使用某种优化器更新参数,管理模型参数,管理多个参数组实现不同学习率,调整学习率。
  • 迭代训练:组织上面 4 个模块进行反复训练。包括观察训练效果,绘制 Loss/Accuracy 曲线,用 TensorBoard 进行可视化分析

张量

概念

张量

官方定义

A tensor is the primary data structure used by neural networks.

官方定义2

A PyTorch Tensor is conceptually identical to a numpy array: a Tensor is an n-dimensional array, and PyTorch provides many functions for operating on these Tensors.

通俗理解

多维数组(Tensors and nd-arrays are the same thing! So tensors are multidimensional arrays or nd-arrays for short.)

Indexes required Computer science Mathematics
n nd-array nd-tensor
  • A scalar is a $0$ dimensional tensor
  • A vector is a $1$ dimensional tensor
  • A matrix is a $2$ dimensional tensor
  • A nd-array is an $n$ dimensional tensor

拓展

We often see this kind of thing where different areas of study use different words for the same concept.

索引

obvious: 访问一个多维数组, 需要几个索引

即维数,或者说在张量中访问一个元素,需要的索引数

A tensor’s rank tells us how many indexes are needed to refer to a specific element within the tensor.

An axis of a tensor is a specific dimension of a tensor.

tensor 属性

torch.dtype

Data type dtype CPU tensor GPU tensor
32-bit floating point torch.float32 torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.float64 torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point torch.float16 torch.HalfTensor torch.cuda.HalfTensor
8-bit integer (unsigned) torch.uint8 torch.ByteTensor torch.cuda.ByteTensor
8-bit integer (signed) torch.int8 torch.CharTensor torch.cuda.CharTensor
16-bit integer (signed) torch.int16 torch.ShortTensor torch.cuda.ShortTensor
32-bit integer (signed) torch.int32 torch.IntTensor torch.cuda.IntTensor
64-bit integer (signed) torch.int64 torch.LongTensor torch.cuda.LongTensor

torch.device

类型

  • CPU
  • GPU

指定GPU

1
device = torch.device('cuda:0')

注意

One thing to keep in mind about using multiple devices is that tensor operations between tensors must happen between tensors that ==exists on the same device==.

torch.layout

应该就是步长==?????==

总览

  • data: 被包装的 Tensor。
  • grad: data 的梯度。
  • grad_fn: 创建 Tensor 所使用的 Function,是自动求导的关键,因为根据所记录的函数才能计算出导数。
  • requires_grad: 指示是否需要梯度,并不是所有的张量都需要计算梯度。
  • is_leaf: 指示是否叶子节点(张量),叶子节点的概念在计算图中会用到,后面详细介绍。
  • dtype: 张量的数据类型,如 torch.FloatTensor,torch.cuda.FloatTensor。
  • shape: 张量的形状。如 (64, 3, 224, 224)
  • device: 张量所在设备 (CPU/GPU),GPU 是加速计算的关键

构建tensor的方法

直接构造tensor

  • torch.Tensor(data)
  • torch.tensor(data)
  • torch.as_tensor(data)
  • torch.from_numpy(data)

根据数值构造tensor

  • torch.eye(k): 生成 k 阶的单位矩阵

  • torch.zeros(shape): 生成元素值都为 0, 形状为 shape 的张量

  • torch.zeros_like(input, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format): 根据 input 形状创建全 0 张量

  • torch.ones(shape): 生成元素值都为 1, 形状为 shape 的张量

  • torch.rand(shape): 生成元素值随机, 形状为 shape 的张量

  • torch.full()

    1
    torch.full(size, fill_value, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • size: 张量的形状,如 (3,3)
    • fill_value: 张量中每一个元素的值
  • torch.full_like()

    1
    torch.full_like(input, fill_value, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor
    • input: the size of input will determine size of the output tensor.
    • fill_value: the number to fill the output tensor with.
  • torch.arange(): 创建等差的 1 维张量。注意区间为 $[start, end)$

    1
    torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • start: 数列起始值
    • end: 数列结束值,开区间,取不到结束值
    • step: 数列公差,默认为 1
  • torch.linspace() : 创建均分的 1 维张量。数值区间为 $[start, end]$

    1
    torch.linspace(start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • start: 数列起始值
    • end: 数列结束值
    • steps: 数列长度 (元素个数)
  • touch.logspace() : 创建对数均分的 1 维张量, 数值区间为 $[start, end]$, 底为 base

    1
    torch.logspace(start, end, steps=100, base=10.0, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • start: 数列起始值
    • end: 数列结束值
    • steps: 数列长度 (元素个数)
    • base: 对数函数的底,默认为 10

根据概率构造tensor

  • torch.normal(mean, std, *, generator=None, out=None): 生成正态分布 (高斯分布)

    • mean: 均值
    • std: 标准差

    4种模式

    • mean 为标量, std 为标量, 则==需要设置 size==

      如: torch.normal(0., 1., size=(4,))

    • mean 为标量,std 为张量

    • mean 为张量,std 为标量

    • mean 为张量,std 为张量

  • torch.randn()torch.randn_like(): 生成标准正态分布。

    1
    torch.randn(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • size: 张量的形状
  • torch.rand()torch.rand_like(): 在区间 $[0, 1)$ 上生成均匀分布

    1
    torch.rand(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
  • torch.randint()torch.randint_like(): 在区间 $[low, high)$ 上生成整数均匀分布

    1
    randint(low=0, high, size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • size: 张量的形状
  • torch.randperm() :生成从 0 到 n-1 的随机排列。常用于生成索引。

    1
    torch.randperm(n, out=None, dtype=torch.int64, layout=torch.strided, device=None, requires_grad=False)
    • n: 张量的长度
  • torch.bernoulli()

    1
    torch.bernoulli(input, *, generator=None, out=None)

    功能:以 input 为概率,生成伯努利分布 (0-1 分布,两点分布)

    • input: 概率值

使用data的构造方法的不同

输出不同对比

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data = np.array([1, 2, 3], dtype=np.int32)
print("numpy: ", data, "; dtype: ", data.dtype)

# torch.Tensor(data)
tensor1 = torch.Tensor(data)
print("tensor1: ", tensor1, "; dtype: ", tensor1.dtype)

# torch.tensor(data)
tensor2 = torch.tensor(data)
print("tensor2: ", tensor2, "; dtype: ", tensor2.dtype)

# torch.as_tensor(data)
tensor3 = torch.as_tensor(data)
print("tensor3: ", tensor3, "; dtype: ", tensor3.dtype)

# torch.from_numpy(data)
tensor4 = torch.from_numpy(data)
print("tensor4: ", tensor4, "; dtype: ", tensor4.dtype)

output

1
2
3
4
5
numpy:  [1 2 3] ; dtype:  int32
tensor1: tensor([1., 2., 3.]) ; dtype: torch.float32
tensor2: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor3: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor4: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32

数据类型对比

torch.Tensor(data)

数据 数据类型
tensor([1., 2., 3.]) torch.float32

torch.tensor(data)

数据 数据类型
tensor([1, 2, 3], dtype=torch.int32) torch.int32

torch.as_tensor(data)

数据 数据类型
tensor([1, 2, 3], dtype=torch.int32) torch.int32

torch.from_numpy(data)

数据 数据类型
tensor([1, 2, 3], dtype=torch.int32) torch.int32

本质的不同

torch.Tensor() 和 torch.tensor()

  • torch.Tensor(data)

    这是构造函数

  • torch.tensor(data)

    • 这是工厂函数

      You can think of the torch.tensor() function as a factory that builds tensors given some parameter inputs.

    • 相比于 torch.Tensor(data) 更好

      the factory function torch.tensor() has better documentation and more configuration options, so it gets the winning spot at the moment.

dtype 的对比

  • ``torch.Tensor()`

    使用的是默认类型

    注意: 这个函数中的dtype也而==不可以==显式的设置, 如: torch.Tensor(data, dtype=torch.int32)

  • other(torch.tensor(), torch.as_tensor(), torch.from_numpy())

    使用的是推断类型(根据传入的原始数据, 自动推断出元素的类型)

    注意: 这些函数中的dtype也而可以显式的设置, 如: torch.tensor(data, dtype=torch.float32)

内存对比: copy vs share

代码

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
data = np.array([1, 2, 3], dtype=np.int32)
print("numpy: ", data, "; dtype: ", data.dtype)

## create tensor data
# torch.Tensor(data)
tensor1 = torch.Tensor(data)
# torch.tensor(data)
tensor2 = torch.tensor(data)
# torch.as_tensor(data)
tensor3 = torch.as_tensor(data)
# torch.from_numpy(data)
tensor4 = torch.from_numpy(data)

## show different
print("old: ")
print("\ttensor1: ", tensor1, "; dtype: ", tensor1.dtype)
print("\ttensor2: ", tensor2, "; dtype: ", tensor2.dtype)
print("\ttensor3: ", tensor3, "; dtype: ", tensor3.dtype)
print("\ttensor4: ", tensor4, "; dtype: ", tensor4.dtype)

# 更改数据
data[0] = 0

print("new:")
print("\ttensor1: ", tensor1, "; dtype: ", tensor1.dtype)
print("\ttensor2: ", tensor2, "; dtype: ", tensor2.dtype)
print("\ttensor3: ", tensor3, "; dtype: ", tensor3.dtype)
print("\ttensor4: ", tensor4, "; dtype: ", tensor4.dtype)

out:

1
2
3
4
5
6
7
8
9
10
old: 
tensor1: tensor([1., 2., 3.]) ; dtype: torch.float32
tensor2: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor3: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor4: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
new:
tensor1: tensor([1., 2., 3.]) ; dtype: torch.float32
tensor2: tensor([1, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor3: tensor([0, 2, 3], dtype=torch.int32) ; dtype: torch.int32
tensor4: tensor([0, 2, 3], dtype=torch.int32) ; dtype: torch.int32

总结

Share Data Copy Data
torch.as_tensor() torch.tensor()
torch.from_numpy() torch.Tensor()
  • This sharing just means that the actual data in memory exists in a single place
  • Sharing data is more efficient and uses less memory than copying data because the data is not written to two locations in memory.

torch.as_tensor() 和 torch.from_numpy() 的选择

  • The torch.from_numpy() function only accepts numpy.ndarrays
  • the torch.as_tensor() function accepts a wide variety of array-like objects including other PyTorch tensors

张量操作

拼接

torch.cat()

将张量按照 dim 维度进行拼接

1
torch.cat(tensors, dim=0, out=None)
  • tensors: 张量序列
  • dim: 要拼接的维度

torch.stack()

将张量在新创建的 dim 维度上进行拼接

1
torch.stack(tensors, dim=0, out=None)
  • tensors: 张量序列
  • dim: 要拼接的维度

意义

使用stack可以保留两个信息:[1. 序列] 和 [2. 张量矩阵] 信息,属于【==扩张==再拼接】的函数

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 # 假设是时间步T1
T1 = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 假设是时间步T2
T2 = torch.tensor([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])

print(torch.stack((T1,T2),dim=0).shape)
print(torch.stack((T1,T2),dim=1).shape)
print(torch.stack((T1,T2),dim=2).shape)
print(torch.stack((T1,T2),dim=3).shape)


#################### outputs: #############
torch.Size([2, 3, 3])
torch.Size([3, 2, 3])
torch.Size([3, 3, 2])
'选择的dim>len(outputs),所以报错'

切分

torch.chunk()

1
torch.chunk(input, chunks, dim=0)

功能:将张量按照维度 dim 进行平均切分。若不能整除,则最后一份张量小于其他张量。

  • input: 要切分的张量
  • chunks: 要切分的份数
  • dim: 要切分的维度

torch.split()

1
torch.split(tensor, split_size_or_sections, dim=0)

功能:将张量按照维度 dim 进行平均切分。可以指定每一个分量的切分长度。

  • tensor: 要切分的张量
  • split_size_or_sections:
    • 为 int 时,表示每一份的长度,如果不能被整除,则最后一份张量小于其他张量;
    • 为 list 时,按照 list 元素作为每一个分量的长度切分。如果 list 元素之和不等于切分维度 (dim) 的值,就会报错。
  • dim: 要切分的维度

索引

torch.index_select()

1
torch.index_select(input, dim, index, out=None)

功能:在维度 dim 上,按照 index 索引取出数据拼接为张量返回。

  • input: 要索引的张量
  • dim: 要索引的维度
  • index: 要索引数据的序号

torch.mask_select()

1
torch.masked_select(input, mask, out=None)

功能:按照 mask 中的 True 进行索引拼接得到一维张量返回。

  • 要索引的张量
  • mask: 与 input 同形状的布尔类型张量

t.le() t.ge()

1
t.le(5)

变换

torch.reshape()

1
torch.reshape(input, shape)

功能:变换张量的形状。当张量在内存中是连续时,返回的张量和原来的张量共享数据内存,改变一个变量时,另一个变量也会被改变。

  • input: 要变换的张量
  • shape: 新张量的形状
    • -1 表示这个维度是根据其他维度计算得出的

torch.transpose()

1
torch.transpose(input, dim0, dim1)

功能:交换张量的两个维度。常用于图像的变换,比如把 $chw$ 变换为 $hwc$。

  • input: 要交换的变量
  • dim0: 要交换的第一个维度
  • dim1: 要交换的第二个维度

torch.t()

功能:2 维张量转置,对于 2 维矩阵而言,等价于torch.transpose(input, 0, 1)

torch.squeeze()

1
torch.squeeze(input, dim=None, out=None)

功能:压缩长度为 1 的维度。

  • dim: 若为 None,则移除所有长度为 1 的维度;若指定维度,则==当且仅当该维度长度为 1 时可以移除==。

torch.unsqueeze()

1
torch.unsqueeze(input, dim)

功能:根据 dim 扩展维度,长度为 1。

数学运算

torch.add()

1
2
torch.add(input, other, out=None)
torch.add(input, other, *, alpha=1, out=None)

功能:逐元素计算 input + alpha * other。因为在深度学习中经常用到先乘后加的操作。

  • input: 第一个张量
  • alpha: 乘项因子
  • other: 第二个张量

torch.addcdiv()

1
torch.addcdiv(input, tensor1, tensor2, *, value=1, out=None)

计算公式: $out_i = input_i + value \times \frac{tensor1_i}{tensor2_i}$

torch.addcmul()

1
torch.addcmul(input, tensor1, tensor2, *, value=1, out=None)

计算公式: $out_i = input_i + value \times tensor1_i \times tensor2_i$

bridge with Numpy

Tensor to numpy array

1
tensor.numpy()

函数

见这里

获取torch默认类型函数

1
torch.get_default_dtype()

suffix “_”

在函数的后面加上后缀 _, 将会改变作用的数据

示例

1
2
3
4
5
6
7
8
9
10
11
# source tensor
tensor = torch.eye(5)
print(tensor)

# tensor after using add()
tensor.add(5)
print(tensor)

# tensor after using add_()
tensor.add_(5)
print(tensor)

out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tensor([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
tensor([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])
tensor([[6., 5., 5., 5., 5.],
[5., 6., 5., 5., 5.],
[5., 5., 6., 5., 5.],
[5., 5., 5., 6., 5.],
[5., 5., 5., 5., 6.]])

自动求导(autograd)

只要搭建好前向计算图,利用 torch.autograd 自动求导得到所有张量的梯度

torch.autograd.backward()

1
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)

功能

  • 自动求取梯度

参数

  • tensors: 用于求导的张量,如 loss

  • retain_graph: 保存计算图。PyTorch 采用动态图机制,默认每次反向传播之后都会释放计算图。这里设置为 True 可以不释放计算图。

    y.backward() 方法调用的是 torch.autograd.backward(self, gradient, retain_graph, create_graph)。但是在第二次执行 y.backward() 时会出错。因为 PyTorch 默认是每次求取梯度之后不保存计算图的,因此第二次求导梯度时,计算图已经不存在了。在第一次求梯度时使用 y.backward(retain_graph=True) 即可。

  • create_graph: 创建导数计算图,用于高阶求导

  • grad_tensors: 多梯度权重。当有多个 loss 混合需要计算梯度时,设置每个 loss 的权重。

retain_grad 参数

反向传播结束之后仍然需要保留非叶子节点的梯度

grad_tensors 参数

给 loss 设置权重

torch.autograd.grad()

1
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)

功能

求取梯度

参数

  • outputs: 用于求导的张量,如 loss
  • inputs: 需要梯度的张量
  • create_graph: 创建导数计算图,用于高阶求导
  • retain_graph:保存计算图
  • grad_outputs: 多梯度权重计算

返回值

返回结果是一个 tunple,需要取出第 0 个元素才是真正的梯度。

注意点

  • 在每次反向传播求导时,计算的梯度不会自动清零。如果进行多次迭代计算梯度而没有清零,那么梯度会在前一次的基础上叠加。

    故使用 w.grad.zero() 将梯度清零

  • 依赖与叶子节点的节点, requires_grad 属性默认为 True

  • 叶子节点不可以执行 inplace 操作

    1
    2
    3
    4
    5
    6
    7
    ## inplace 操作: 改变后的值和原来的值内存地址是同一个
    a += x
    a.add_(x)

    ## 非inplace 操作: 改变后的值和原来的值内存地址不是同一个
    a = a + x
    a.add(x)

    举例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    print("非 inplace 操作")
    a = torch.ones((1, ))
    print(id(a), a)
    # 非 inplace 操作,内存地址不一样
    a = a + torch.ones((1, ))
    print(id(a), a)

    print("inplace 操作")
    a = torch.ones((1, ))
    print(id(a), a)
    # inplace 操作,内存地址一样
    a += torch.ones((1, ))
    print(id(a), a)

    结果自己跑一下嘛

    问题

    如果在反向传播之前 inplace 改变了叶子的值, 再执行 backward() 会报错

逻辑回归

概念

二分类模型

模型表达式 $y=f(z)=\frac{1}{1+e^{-z}}$,其中 $z=WX+b$。$f(z)$ 称为 sigmoid 函数,也被称为 Logistic 函数

分类原则

逻辑回归是在线性回归的基础上加入了一个 sigmoid 函数,这是为了更好地描述置信度,把输入映射到 (0,1) 区间中,符合概率取值。

训练步骤

  1. 导入模型

  2. 计算误差 loss

  3. 后向传播 (call loss.backward())

  4. 加载优化器 optimizer: 注册模型中的所有参数

  5. 梯度下降 (call optim.step())

    各参数的梯度保存在 .grad 属性中

反向传播

To backpropagate the error all we have to do is to loss.backward(). You need to clear the existing gradients though, ==else gradients will be accumulated to existing gradients.==

模型构建总结

步骤

  1. 加载数据
  2. 定义神经网络
  3. 定义损失函数
  4. 训练网络
  5. 测试网络

PyTorch 构建模型需要 5 大步骤:

  • 数据:包括数据读取,数据清洗,进行数据划分和数据预处理,比如读取图片如何预处理及数据增强。
  • 模型:包括构建模型模块,组织复杂网络,初始化网络参数,定义网络层。
  • 损失函数:包括创建损失函数,设置损失函数超参数,根据不同任务选择合适的损失函数。
  • 优化器:包括根据梯度使用某种优化器更新参数,管理模型参数,管理多个参数组实现不同学习率,调整学习率。
  • 迭代训练:组织上面 4 个模块进行反复训练。包括观察训练效果,绘制 Loss/ Accuracy 曲线,用 TensorBoard 进行可视化分析。

数据

数据模块

细分

  • 数据收集: 样本和标签
  • 数据划分: 训练集, 验证集和测试集
  • 数据读取:对应于PyTorch 的 DataLoader。其中 DataLoader 包括 Sampler 和 DataSet。Sampler 的功能是生成索引, DataSet 是根据生成的索引读取样本以及标签。
  • 数据预处理:对应于 PyTorch 的 transforms

考虑

  • Who created the dataset?
  • How was the dataset created?
  • What transformations were used?
  • What intent does the dataset have?
  • Possible unintentional consequences?
  • Is the dataset biased?
  • Are there ethical issues with the dataset?

DataLoader

torch.utils.data.DataLoader()

1
torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, multiprocessing_context=None)

功能

构建可迭代的装载器

参数

  • dataset: Dataset 类,决定数据从哪里读取以及如何读取
  • batchsize: 批大小
  • num_works: 是否多进程读取数据
  • sheuffle: 每个 epoch 是否乱序
  • drop_last: 当样本数不能被 batchsize 整除时,是否舍弃最后一批数据

Epoch, I’t’eration, Batchsize

  • Epoch: ==所有训练样本==都已经输入到模型中,称为一个 Epoch
  • Iteration: ==一批样本==输入到模型中,称为一个 Iteration
  • Batchsize: ==批大小==,决定一个 iteration 有多少样本,也决定了一个 Epoch 有多少个 Iteration

举例: 假设样本总数有 80,设置 Batchsize 为 8,则共有 $80 \div 8=10$ 个 Iteration。这里 $1 Epoch = 10 Iteration$。

torch.utils.data.Dataset

功能

Dataset 是抽象类,所有自定义的 Dataset 都需要继承该类,并且重写__getitem()__方法和__len__()方法

  • __getitem()__方法的作用是接收一个索引,返回索引对应的样本和标签,这是我们自己需要实现的逻辑
  • __len__()方法是返回所有样本的数量

数据读取

  • 读取哪些数据:每个 Iteration 读取一个 Batchsize 大小的数据,每个 Iteration 应该读取哪些数据。
  • 从哪里读取数据:如何找到硬盘中的数据,应该在哪里设置文件路径参数
  • 如何读取数据:不同的文件需要使用不同的读取方法和库。

步骤

  1. 划分数据集为: 训练集, 验证集和测试集, 比例为 $8 : 1 : 1$, 并构造路径
  2. 实现 get_img_info()__getitem__(self, index), __len__() 函数
  3. 构建模型

DataSet

MNIST 数据集

损失函数

均方误差 MSE

$MSE = \frac{1}{m}\sum_{i = 1}^{m}{(y_i-\hat{y_i})^2}$

  • $y_i$ 是预测值
  • $\hat{y_i}$ 是真实值
0%