Qeuroal's Blog

静幽正治

内存泄露相关

  • valgrind

汇编反汇编相关

  • objdump

debug相关

  • gdb
  • address2line

后台运行

Linux 命令是使用终端与系统交互的好方法。但是,有时可能需要一段时间才能完成手头的任务。这迫使用户等待相当长的时间或完全生成一个新的 shell。

幸运的是,您可以通过一些简单的方法在后台运行 Linux 命令。

&

在你的命令后添加一个 &

运行 Linux 后台命令的最简单方法是在命令后添加与号 (&)。例如,如果从终端启动 gedit 文本编辑器,则在关闭编辑器之前无法使用 shell。但是,当您在命令中添加额外的 & 时,您将能够立即使用 shell。

1
$ gedit &

对于需要长时间执行的任务,&符号可以使当前终端窗口不被占用,这样就能继续在同一个终端上工作,甚至关闭终端窗口也不会影响任务的正常执行。不过需要注意的是,如果要求这个任务输出内容到标准输出中(例如 echols),即便使用了 &,也会等待这些输出任务在前台运行完毕;当使用 &将一个进程放置到后台运行的时候,Bash 会提示这个进程的进程 ID。在 Linux 系统中运行的每一个进程都有一个唯一的进程 ID,我们可以使用进程 ID 来暂停、恢复或者终止对应的进程。

nohup

Linux 中 的nohup命令允许管理员运行不受挂断(Hang Up)信号影响的终端命令。您可以使用 nohup 在后台运行 Linux 命令。

nohup 的一个主要好处是,即使您退出 shell,您的命令也会运行。此外,它会生成执行的日志文件。在当前目录或 $HOME 中查找nohup.out。语法是:

1
nohup 命令

如: 后台运行Nmap 端口扫描

1
$ nohup sudo nmap -sS --top-ports=15 192.168.150.1/24

Q: Within zsh the command &! is a shortcut for disown, i.e. the program won’t get killed upon exiting the invoking shell.

程序退出不被杀死, 同时如果再次登录shell, 那么使用 ps -ef | grep "clash" 会显示clash 的父进程是 1, 这是因为创建的父进程被杀死, 那么由该进程创建的子进程会在父进程被杀死后, 父进程改为 1

1
nohup clash &> /dev/null &!

程序退出会被杀死的进程

1
nohup clash &> /dev/null &

fg和bg

命令由前台转后台

  1. 键入 ctrl+z 以暂停这条命令并返回客户端
  2. 键入 bg 命令让这条shell命令在后台执行
  3. 键入 disown -h 这条命令保证当终端关闭时,Shell脚本不会被杀死

更一般地

  1. ctrl+z将任务暂停,这时屏幕会打印这样一句 [1]+ Stopped ./myserver

  2. 使用 bg %1 将这个任务转入后台运行(1 是刚才暂停任务的job号,步骤1中屏幕提示方括号里面的内容,如果忘了也可以用 jobs 命令查看)

  3. 使用 disown –h %1 指定shell退出时不要发送SIGHUP给任务1(注意任务号前面有个%)

命令由后台转前台

  1. 键入 jobs 查看后台执行的命令
  2. 键入 fg %<d> 将 jobs[d] 切换到前台

disown

使用 disown 将 Linux 命令设置为后台运行

Linux 中 的disown命令可以轻松地在后台运行命令。首先,您需要使用& 运算符在后台发送任务。然后,键入disown以将其与shell分离。

1
$ gedit & disown

disown 的一个主要优点是,与 nohup 一样,当您关闭 shell 或注销时,系统不会终止您的任务。

使用 Tmux 在后台运行 Linux 命令

Tmux 是一个强大的多路复用器,它允许我们在单个窗口中运行多个终端会话。对于不熟悉它的人来说,学习 tmux 是一个很好的选择。Tmux 使在 Linux 中运行后台命令毫不费力。

1
tmux new -d 'ping -c 10 8.8.8.8 > www.itpro.net.cn.log'

当您运行上述tmux命令时,它将在单独的 shell 中执行ping命令并将其保留在后台。您可以使用此方法在后台执行任何 Linux 命令。

能够在后台运行命令使系统管理对管理员来说更有效率。您可以通过多种方式为您的任务做背景。像&和Ctrl + Z这样的 Bash 功能很方便,但系统会在 shell 关闭时终止后台作业。另一方面,即使您注销或终止 shell ,诸如nohupdisown 之类的工具也会使您的命令保持运行。

如果您将程序长时间留在后台,如果编码不当,它们可能会变成僵尸进程。这些过程会显着降低系统速度。因此,请确保每隔一段时间识别并杀死僵尸进程。

查看日志

查看日志常用命令

tail

-n 是显示行号;相当于nl命令;例子如下:

tail -100f test.log 实时监控100行日志

tail -n 10 test.log 查询日志尾部最后10行的日志;

tail -n +10 test.log 查询10行之后的所有日志;

跟tail是相反的,tail是看后多少行日志;例子如下:

head -n 10 test.log 查询日志文件中的头10行日志;

head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;

cat

tac是倒序查看,是cat单词反写;例子如下:

cat -n test.log |grep "debug" 查询关键字的日志

应用场景

应用场景一

按行号查看—过滤出关键字附近的日志

  • cat -n test.log |grep "debug" 得到关键日志的行号
  • cat -n test.log |tail -n +92|head -n 20 选择关键字所在的中间一行. 然后查看这个关键字前10行和后10行的日志:
  • tail -n +92 表示查询92行之后的日志
  • head -n 20 则表示在前面的查询结果里再查前20条记录

应用场景二

根据日期查询日志

  • sed -n '/2014-12-17 16:17:20/,/2014-12-17 16:17:36/p' test.log

特别说明:上面的两个日期必须是日志中打印出来的日志,否则无效;

grep '2014-12-17 16:17:20' test.log 来确定日志中是否有该 时间点

应用场景三

日志内容特别多,打印在屏幕上不方便查看

  • 使用more和less命令,

    如: cat -n test.log |grep "debug" |more 这样就分页打印了,通过点击空格键翻页

  • 使用 >xxx.txt 将其保存到文件中,到时可以拉下这个文件分析

    如:cat -n test.log |grep "debug" >debug.txt

文件颜色所代表的含义

颜色 文件类型标识 含义
浅蓝色 [l] 软链接
白色 [-] 普通文件/硬链接
蓝色 [d] 目录
红色 [l] 某文件被删除后,链接到该文件的软链接的颜色
红色闪烁 表示该文件已被删除,但还存在该文件的软链接
红色 [-] 压缩包
黄色 [c] 字符设备文件,如键盘鼠标
绿色 [-] 可执行文件,可执行的程序
桃红色 [s] 套接字文件
土黄色 [p] 管道文件
灰色文件 表示其它文件

文件属性查看

端口占用情况

1
netstat -tunlp

用于显示 tcp,udp 的端口和进程等相关情况。

netstat 查看端口占用语法格式:

1
netstat -tunlp | grep 端口号
  • -t (tcp) 仅显示tcp相关选项
  • -u (udp)仅显示udp相关选项
  • -n 拒绝显示别名,能显示数字的全部转化为数字
  • -l 仅列出在Listen(监听)的服务状态
  • -p 显示建立相关链接的程序名

例如查看 8000 端口的情况,使用以下命令:

1
2
# netstat -tunlp | grep 8000
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 26993/nodejs

更多命令:

1
2
3
netstat -ntlp   //查看当前所有tcp端口
netstat -ntulp | grep 80 //查看所有80端口使用情况
netstat -ntulp | grep 3306 //查看所有3306端口使用情况

设置开机自启动

shell 启动时自启动

不建议使用: 因为每次启动shell都会启动, 而像clash这样的程序只需要执行一次即可, 并且如果多个用户执行, 会导致后执行clash的shell执行失败.

  1. 打开 ~/.bashrc./.zshrc

  2. 在文件末尾添加又在每次用户登录时执行的命令, 如

    1
    nohup clash &> /dev/null &
  3. source ~/.bashrcsource ~/.zshrc以使更改生效

使用”启动应用程序偏好设置”

(Startup Applications Preferences)

  1. Startup Applications Preferences 的配置信息通常存储在 ~/.config/autostart 目录中的 .desktop 文件中. 每个启动的应用程序都有一个相应的 .desktop 文件,该文件包含有关应用程序启动方式的信息。

添加文件 vim <startup_program>.desktop, 添加如下配置

1
2
3
4
5
6
7
8
9
10
[Desktop Entry]
Type=Application
Exec=<your command>
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name[en_US]=<My Program>
Name=<My Program>
Comment[en_US]=<My startup program>
Comment=<My startup program>

如:

1
2
3
4
5
6
7
8
9
Desktop Entry]
Type=Application
Exec=nohup clash &> /dev/null
Hidden=false
NoDisplay=false
X-GNOME-Autostart-enabled=true
Name[en_US]=clash
Name=clash
Comment[en_US]=clash

Exec 行指定了要在启动时运行的程序的路径

使用 systemctl 方式 (RECOMMNAD)

采用systemctl的方式

强烈建议看看这个文档,里面介绍了一些参数的具体含义.

  1. 准备工作

    1. 写好脚本(假设脚本名为 exec_clash.sh)或将程序放入本地可以运行, 这里以clash为例, 其中, clash位于 /usr/local/bin/clash

      exec_clash.sh 中的 exec_clash 是可根据具体脚本的功能改为相应的名字

    2. 若是脚本, 则赋予脚本运行权限: chmod +x exec_clash.sh

  2. 配置

    1. 编写一个 <clash>.service, 内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      [Unit]
      Description=clash

      [Service]
      Type=simple
      User=qeuroal
      ExecStart=/usr/local/bin/clash
      Restart=always
      RestartSec=30
      StartLimitInterval=0

      [Install]
      WantedBy=multi-user.target graphical.target

      说明

      1. User: 表示以哪个用户启动,不写默认是root. 而用户名也决定了程序启动去哪里读取相应的配置

      2. ExecStart: 就是要执行的语句。

        由于为脚本赋予了x(可执行)权限, 因此, ExecStart后面的内容可以直接写成脚本的绝对路径

      3. Restart=always: 只要不是通过systemctl stop来停止服务,任何情况下都必须要重启服务,默认值为no。还可以配置有条件重启,具体参考上述文档。

      4. RestartSec=5: 重启间隔,比如某次异常后,等待5(s)再进行启动,默认值0.1(s)

      5. StartLimitInterval: 无限次重启,默认是10秒内如果重启超过5次则不再重启,设置为0表示不限次数重启

    2. 拷贝 clash.service/lib/systemd/system, 需要注意权限问题

    3. 修改配置文件后需要重加载配置: sudo systemctl daemon-reload

    4. 设置开机自启: sudo systemctl enable clash.service

      之后就可以直接重启了

    5. 相关操作

      操作 命令
      启动 sudo systemctl start clash.service
      停止 sudo systemctl stop clash.service
      查看状态 sudo systemctl status clash.service
    6. 测试自动重启功能

查看程序启动命令

最佳解决方法

大多数默认应用程序将具有位于/usr/share/applications中的.desktop文件。

要了解在启动其中一个应用程序时将运行的相应终端命令,请打开文件浏览器Nautilus,然后右键单击应用程序的图标以在上下文菜单中选择“属性”。这将为您提供所需的所有详细信息(此处显示将运行gnome-control-center -overview的系统设置)

次佳解决方法

如果通过存储库或dpkg安装了应用程序,则可以使用以下命令:

1
dpkg -l | grep "<application name>"

这将搜索所有已安装的应用程序,并搜索其描述;搜索描述是这里的重要部分,因为描述通常包含应用程序的名称,即使运行它的”command”不包含应用程序名称。

例:
在GNOME中,有一个名为Disk Usage Analyzer的应用程序。但是,从终端运行它的命令不是disk-usage-analyzer。要查找其命令,您可以运行:

1
dpkg -l | grep "disk usage"

输出应包含此条目:

1
2
alaa@aa-lu:~$ dpkg -l | grep "disk usage"
ii baobab 3.6.4-0ubuntu1 i386 GNOME disk usage analyzer

看第二栏。实际运行磁盘使用率分析器的命令是baobab

参考目录

移动隐藏文件

1
mv * .[^.]* <destination_directory>

解释

1、mv命令的最后一个参数是要移动文件的目标位置;
2、第一个 * 匹配除了隐藏文件的所有文件;
3、隐藏文件使用 .[^.]* 匹配
4、匹配隐藏文件用 .[^.]* 为什么不用 .*: .* 会匹配目录 ...
5、.[^.]* 的意思是:以 . 开头,加不是 . 的一个任意字符,再加其他任意字符

参数补充

参数说明:

参数 说明
-b 当目标文件或目录存在时,在执行覆盖前,会为其创建一个备份。
-i 如果指定移动的源目录或文件与目标的目录或文件同名,则会先询问是否覆盖旧文件,输入 y 表示直接覆盖,输入 n 表示取消该操作。
-f 如果指定移动的源目录或文件与目标的目录或文件同名,不会询问,直接覆盖旧文件。
-n 不要覆盖任何已存在的文件或目录。
-u 当源文件比目标文件新或者目标文件不存在时,才执行移动操作。

clash API 使用

教程

首先,阅读Clash的API的文档RESTful - Proxies

GET 获取所有代理

1
/proxies

PUT 切换 Selector 中选中的代理

1
/proxies/<name>

(这边的name可以为节点名称,也可以为Selector。只要在proxies/后直接加上字符串就可以,不需要引号或者:

当前接口只支持切换 Selector 中的代理

Path Parameters Body Parameters
name name
string string
代理Selector名称(大小写敏感) 要切换的代理名字

接着,curl使用指南 curl 的用法指南

最后,使用curl切换节点, 使用/proxies获取所有可用代理,然后curl发送PUT请求解决战斗。

Example

1
curl -X PUT -H "Content-Type: application/json" -d '{"name":"节点名"}' http://localhost:port/proxies/<name>

-H 添加 HTTP 请求的标头Content-Type: application/json,根据链接2,不设置标头为application/json可能会有问题。

-d 参数用于发送 POST 请求的数据体。

最后的网址为clash的external-controller的网址端口,最后Selector为要选择的proxy-groups的名称。

实际指令类似下面这条:

  • 获取命令

    1
    curl -X GET -H "Content-Type: application/json" http://127.0.0.1:9090/proxies -o get.json
  • 更改节点

    1
    curl -X PUT -H "Content-Type: application/json" -d '{"name":"香港-08"}' http://127.0.0.1:9090/proxies/国外流量 -o put.json

所有命令

1
2
3
4
5
curl -X PUT -H "Content-Type: application/json" -d '{"name":"香港-08"}' http://127.0.0.1:9090/proxies/国外流量 -o put.json
curl -X PUT -H "Content-Type: application/json" -d '{"name":"香港-08"}' http://127.0.0.1:9090/proxies/国外流量

curl -X GET -H "Content-Type: application/json" http://127.0.0.1:9090/proxies -o get.json
curl -X GET -H "Content-Type: application/json" http://127.0.0.1:9090/proxies

reference: sakronos

sites

镜像、容器与仓库

通俗解释:镜像就类似操作系统光盘介质,容器相当于通过光盘安装后的系统。

  • 通过光盘(镜像),我们能在不同机器上部署系统(容器),系统内的操作只会保留在当前的系统(容器)中,

  • 如果要升级系统,需要使用到光盘(镜像),但是可能会导致操作系统(容器)的数据丢失。

常用命令

状态查询命令

  • docker stats: 查看 docker 运行情况

  • docker images: 查看所有镜像

  • docker ps 查看当前正在运行的容器,

    • -a 查看所有已经创建的容器
  • docker inspect <container_name>: 分析容器的详细信息

    包含容器所有的配置信息

  • docker top: 显示指定容器的运行进程

  • docker logs: 查看容器运行日志

    后面加上 container_name 或 ID

    OPTIONS:

    -f: 类似于 tail -f

运行管理命令

  • docker exec

    进入容器: docker exec -it <container_name> <bash或/bin/bash>, 类似于 terminal

  • docker run

    1
    docker run --name <container_name> -d -p 9090:80 images:<TAG >
    • -v <宿主机路径>:<容器路径>: 挂载. 可用于防止容器删除后导致数据丢失.
  • del

    在我们构建镜像过程中, 可能会产生 none 的镜像, 这些镜像是没有意义的, 只会白白占用系统资源.

构建镜像

构建一个 docker 镜像是基于另一个 docker 镜像构建的.

Dockerfile

1
FROM java:8
  • FROM XXX: 基于了另外一个镜像

  • ADD:

    与构建上下文路径(context_directory)相匹配

编排管理

docker compose 负责处理容器间的依赖关系, 用于开发测试
docker swarm 支持集群
docker stack 类似于 docker compose, 用于生产环境

docker compose

用于解决容器之间的依赖关系

  • down: stop & del 容器和网络等
  • stop: 仅让容器停止

docker-compose (单机版)

  • version: 对版本进行解析

  • services: 声明 services, 每个 services 对应一个或多个容器. 可以声明多个 services, 可以配置多容器之间的关联.

    • 名字可以随便写

    • image: 声明容器的镜像, 从哪里下载

    • restart: 重启策略

    • ports: 端口映射

    • links: 声明与其他容器间的关联, 自动解析依赖配置, 解耦绑定关系 depends_on: 通过这个字段容器依赖关系, 需要启动 depends_on 的依赖服务后才能够启动当前 services, 但是不是那么可靠. 需重点关注启动顺序, 关注 “ready”(可以对外提供服务了) 和 “running” 状态, 通过 wait-for-it.sh 用来等待服务可用, 才启动当前 services.

    • volumes: 数据挂载

      可以引用系统里面的环境变量, 包括数据卷、日志卷.

    • command: 启动命令

    • deploy(version2): 声明容器对资源的使用限制

      Warning

      在 version3 中使用 --compatibility

      • limits:
      • reservations: 使用的初始资源数
    • envirionment: 声明环境变量

Secure Shell (SSH) 是一个网络协议,它主要被用来加密客户端和服务端之间的连接。 在客户端和服务端的每一次交互都被加密。

这个教程解释了如何在 Ubuntu 机器上启用 SSH。

启用 SSH 将会允许你远程连接到你的系统,并且执行管理员任务。你将能够通过 scpsftp安全传输文件。

在 Ubuntu 上启用 SSH

默认情况下,当 Ubuntu 最初被安装的时候,通过 SSH 进行远程访问是不被允许的。在 Ubuntu 上启用 SSH 非常的简单直接。

以 root 或者其他 sudo 用户身份执行下面的步骤,在你的 Ubuntu 系统上安装并且启用 SSH。

01.使用Ctrl+Alt+T打开终端,并且安装openssh-server软件包:

1
2
sudo apt update
sudo apt install openssh-server

当被提示时,输入你的密码并且按 Enter,继续安装。

02.一旦安装完成,SSH 服务将会被自动启动。你可以验证 SSH 是否正在运行,输入:

1
sudo systemctl status ssh

输出将会告诉你服务正在运行,并且启用开机启动:

1
2
3
4
● ssh.service - OpenBSD Secure Shell server
Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2020-06-01 12:34:00 CEST; 9h ago
...

q返回命令行。

03.Ubuntu 自带一个配置防火墙配置工具,称为 UFW。如果防火墙在你的系统上被启用,请确保打开了 SSH 端口:

1
sudo ufw allow ssh

就这些。现在你可以从任何远程机器上通过 SSH 连接到你的 Ubuntu 系统。Linux 和 macOS 系统默认都安装了 SSH 客户端。想要从一个 Window 机器上连接,使用一个 SSH 客户端,例如:PUTTY

连接到 SSH 服务器

想要在局域网内连接到你的 Ubuntu 机器,以下面的格式输入 ssh 命令,加上用户名和 IP 地址。

1
ssh username@ip_address
1
确保你将`username`修改成你自己的用户名,`ip_address`修改成你安装了 SSH 的 Ubuntu 机器的 IP 地址。

如果你不知道你的 IP 地址,你可以使用ip命令轻易地找到它:

1
ip a

你可以从输出中看到,系统 IP 地址是10.0.2.15

一旦你找到 IP 地址,通过运行下面的ssh 命令登录远程机器:

1
ssh linuxize@10.0.2.15

当你第一次连接时,你将看到下面的信息:

1
2
3
The authenticity of host '10.0.2.15 (10.0.2.15)' can't be established.
ECDSA key fingerprint is SHA256:Vybt22mVXuNuB5unE++yowF7lgA/9/2bLSiO3qmYWBY.
Are you sure you want to continue connecting (yes/no)?

输入yes并且你将会被提示输入你的密码:

1
2
Warning: Permanently added '10.0.2.15' (ECDSA) to the list of known hosts.
linuxize@10.0.2.15's password:

一旦你输入密码,你将会看到默认的 Ubuntu 消息:

1
2
3
4
5
6
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
...

现在你可以登录到你的 Ubuntu 机器上。

连接到 NAT 后面的 SSH

想要通过互联网连接到你的 Ubuntu 机器,你需要知道你的公网 IP 地址,并且配置你的路由器接收端口22的数据,并且发送它到正在运行 SSH 的 Ubuntu 机器。

想要获取你尝试通过 SSH 连接的机器的公网 IP,在这个机器上访问 URL 地址:https://api.ipify.org

当设置端口转发时,每一个路由器都有不同的方式来设置端口转发。你应该参考你的路由器文档,关于如何设置端口转发的信息。简单来讲,你应该输入之前设置的 SSH 端口号 22,以及之前服务器的私有 IP 地址。

一旦你找到 IP 地址,配置你的路由器,输入:

1
ssh username@public_ip_address

如果你将你的机器暴露在互联网中,你最好采取一些安全措施。最基础的一个措施就是配置你的路由器接受一个非标准端口的 SSH 流量,并且转发到你运行 SSH 服务的机器的端口22。

你也可以设置 SSH 密钥公钥验证方式,之后你就可以不使用密码就可以连接到你的 Ubuntu 机器上了。

在 Ubuntu 上禁用 SSH

想要在你的 Ubuntu 系统上禁用 SSH 服务器,简单停止 SSH 即可,输入:

1
sudo systemctl disable --now ssh

稍后,你可以重新启用,输入:

1
sudo systemctl enable --now ssh

SSH登录

SSH 无密登录

step1 客户端生成公私钥

本地客户端生成公私钥:(一路回车默认即可)

1
ssh-keygen

生成之后会在用户的根目录生成一个 “.ssh“的文件夹,进入”.ssh“会生成如下几个文件:

1
cd ~/.ssh

下创建两个密钥:

  1. authorized_keys: 存放远程免密登录的公钥,主要通过这个文件记录多台机器的公钥
  2. id_rsa : 生成的私钥文件
  3. id_rsa.pub : 生成的公钥文件
  4. know_hosts : 已知的主机公钥清单

ps

ssh公钥生效需满足至少下面两个条件:

  • .ssh 目录的权限必须是700
  • .ssh/authorized_keys 文件权限必须是600

step2 上传公钥到服务器

这里测试用的服务器地址为:192.168.32.32

用户为:root

方法1

1
ssh-copy-id -i ~/.ssh/id_rsa.pub <remote_ip>

上面这条命令是写到服务器上的ssh目录下去了

1
2
cd ~/.ssh
vim authorized_keys

可以看到客户端写入到服务器的 id_rsa.pub (公钥)内容。

方法2

使用命令ssh-copy-id <remote_ip>

1
ssh-copy-id root@192.168.32.32

方法3

通过scp将内容写到对方的authorized_keys文件中

1
scp -p ~/.ssh/id_rsa.pub root@<remote_ip>:/root/.ssh/authorized_keys

step3 测试免密登录

客户端通过ssh连接远程服务器,就可以免密登录了。

1
ssh root@192.168.32.32

设置指定用户

step1 配置SSH服务器

编辑SSH服务器配置文件(通常位于/etc/ssh/sshd_config)。

1
$ sudo vim /etc/ssh/sshd_config

确保以下行未注释(没有#):

1
2
PubkeyAuthentication yes
PasswordAuthentication yes

这允许使用SSH密钥和密码进行身份验证。

step2 设置Match

在配置文件的末尾添加Match块,限制只允许用户user1, user2, user3使用密钥登录。您可以这样做:

1
2
Match User user1,user2,user3
PasswordAuthentication no

1
2
3
4
5
6
7
8
Match User user1
PasswordAuthentication no

Match User user2
PasswordAuthentication no

Match User user3
PasswordAuthentication no

这将禁用user1,user2,user3用户的密码登录。其他用户将按照默认设置进行身份验证,允许密码登录。

多个用户也可以单独

step3 重启SSH服务

保存配置文件并重启SSH服务,以使更改生效:

1
$ sudo systemctl restart ssh

现在,user1,user2,user3用户只能使用其SSH密钥进行身份验证,而其他用户仍然可以使用密码进行身份验证。

请确保在执行此操作之前,您已经测试过SSH密钥对的工作方式,并且确保您至少有一种方法可以访问服务器,以防止自身被锁定。

SSH设置环境变量

方法1

~/.ssh/config 中设置如下内容:

1
2
Host *
SetEnv TERM=xterm-256color

它的作用是为所有主机(Host * 表示匹配所有主机)设置一个环境变量 TERM, 其值为 xterm-256color, 具体含义如下:

  • Host *: 这是一个通配符规则,表示该配置适用于所有远程主机。 如果你只想针对特定主机应用配置,可以将 * 替换为主机名或 IP 地址。
  • SetEnv TERM=xterm-256color : SetEnv 是 SSH 配置指令, 用于在连接到远程主机时传递环境变量. TERM=xterm-256color 设置了终端类型为 xterm-256color, 这是一种支持 256 色的终端模拟器类型。这对于需要彩色输出的程序(如 vim、tmux 等)非常重要。

TERM=xterm-256color 的是为了解决远程连接用户时, 键入字符错乱的问题

方法2

ssh 连接时, 使用命令行 -o 选项设置环境变量: ssh -o SetEnv="TERM=xterm-256color" user@hostname

SSH 上传下载

上传

上传本地文件到服务器

格式:scp 要上传的文件路径 用户名@服务器地址:服务器保存路径

例如:把本机 /home/test.txt 文件上传到 192.168.0.101 这台服务器上的 /data/ 目录中

1
scp /home/test.txt root@192.168.0.101:/data/

上传目录到服务器

格式:scp -r 要上传的目录 用户名@服务器地址:服务器的保存目录

例如:把 /home 目录上传到服务器的 /data/ 目录

1
scp -r /home root@192.168.0.101:/data/

下载

从服务器上下载文件

格式:scp 用户名@服务器地址:要下载的文件路径 保存文件的文件夹路径

例如:把 192.168.0.101 上的 /data/test.txt 的文件下载到 /home(本地目录)

1
scp root@192.168.0.101:/data/test.txt /home

从服务器下载整个目录

格式:scp -r 用户名@服务器地址:要下载的服务器目录 保存下载的目录

例如:把 192.168.0.101 上的 /data 目录下载到 /home(本地目录)

1
scp -r root@192.168.0.101:/data  /home/

注:目标服务器要开启写入权限。

访问服务器的 127.0.0.1:port

服务器 port 允许外部 IP 访问时

假设目标服务器的 ip 地址为: 192.168.32.32, port 为 8080

则在本地的浏览器地址栏输入: <ip>:<port>, 这里输入: 192.168.32.32:8080

服务器 port 不允许外部 IP 访问时

  1. 将服务器 port 端口转发到本地 port

    如将服务器 9090 端口转发到本地 19090.

    1
    ssh -L 19090:127.0.0.1:9090 <ip>:<port>
  2. 地址栏输入: localhost:19090127.0.0.1:19090

设置特定用户的sshd_config

1
2
Match User <用户名>
PasswordAuthentication yes

透过跳板机访问服务器

方法1 (简单却暴力)

很多环境都有一台统一登录跳板机,我们需要先登录跳板机,然后再登录自己的目标机器.登录流程如下

1
2
ssh root@跳板机ip 
ssh root@自己的目标机器ip

方法2 (使用端口转发)

  1. 首先通过 ssh 连接跳板机, 并设置端口转发, 如将本地端口 2222 通过跳板机 (132.60.171.142) 转发到服务器 (192.168.2.28) 的 22 端口, ~/.ssh/config 配置如下:

    1
    2
    3
    4
    5
    Host linkrm
    HostName 132.60.171.142
    User qeuroal
    Port 15220
    LocalForward 2222 192.168.2.28:22

    在本地运行命令: ssh linkrm

  2. 通过 ssh 连接本地端口 2222, 访问服务器 (192.168.2.28), ~/.ssh/config 配置如下:

    1
    2
    3
    4
    5
    Host rm
    HostName localhost
    User qeuroal
    Port 2222
    LocalForward 16006 127.0.0.1:6006

    在本地另开一个终端, 并运行命令 ssh rm

方法3 (使用 ProxyCommand)

有一台机器A,欲与机器C建立SSH连接,但由于隔离限制(比如”存在防火墙”)该SSH连接不能直接建立。ssh命令的”ProxyCommand”选项被设计用来解决以上问题。

通过“ProxyCommand”选项,机器A能够灵活使用任意代理机制与机器C上的SSH Server端口建立连接,接着机器A上的SSH Client再与该连接进行数据交互,从而机器A上的SSH Client与机器C上的SSH Server之间建立了与一般“直接SSH连接”不太一样的“间接SSH连接”。不过由于“间接SSH连接”的透明性,逻辑上可认为机器A上的SSH Client与机器C上的SSH Server建立了“直接SSH连接”。
“直接SSH连接”示意图如图1,“间接SSH连接”示意图如图2。

常使用代理机制有两种,接下来进行介绍。

常使用代理机制

代理机制1

ProxyCommand 选项值形式为 ssh -W C:CPort -l USER -i PRIVATE_KEY -p BPort B. 原理是 ssh 命令自提供的代理机制,在机器A上另外单独建立与B的SSH连接(使用-l USER -i PRIVATE_KEY -p BPort B这些参数),该SSH连接的B端侧与机器C上的SSH Server端口(即C:CPort)建立连接,该SSH连接的A端侧与机器A上的SSH Client(即w最终欲建立’间接SSH连接’在机器A上的SSH Client”)建立连接。

假定A上ssh_config配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
Host B
HostName 132.60.171.142
User qeuroal
Port 15220
IdentityFile ~/.ssh/id_dsa

Host C
HostName 192.168.2.28
User qeuroal
Port 22
IdentityFile ~/.ssh/id_rsa
ProxyCommand ssh -W %h:%p B

在A上执行 ssh C 命令,发现A与C成功建立SSH连接。根据以上所述,此时在A上应该有两个SSH进程,一个对应于”A与B的SSH连接”,另外一个对应于”A与C的SSH连接”。在A上执行 ps -ef | grep 'ssh' 命令,得到如下结果,得证。

1
2
7851  7689 15:55 S pts/10   ssh C
7852 7851 15:55 S pts/10 ssh -W C:1046 B

代理机制2

“ProxyCommand”选项值形式为”nc -X 5 -x B:BPort C CPort”,原理:利用”nc”命令,在机器A上使用”nc”命令与代理服务器(即”-x B:BPort”,通过”-X 5”参数来指定与代理服务器的通信协议为”SOCKS4/SOCKS5/HTTPS”)建立代理连接,该代理连接的B端侧与机器C上的SSH Server端口(即”C CPort”)建立连接,该代理连接的A端侧与机器A上的SSH Client(即”最终欲建立‘间接SSH连接’在机器A上的SSH Client”)建立连接。
假定A上ssh_config配置文件内容如下:

1
2
3
4
5
6
Host C
HostName %h
User dsl
Port 1046
IdentityFile ~/.ssh/id_rsa
ProxyCommand nc -X 5 -x B:8989 %h %p

在A上执行ssh C命令,发现A与C成功建立SSH连接。根据以上所述,此时在A上应该有一个NC进程和一个SSH进程,前者对应于”A与B的代理连接”,后者对应于”A与C的SSH连接”。在A上执行ps -ef | grep -e 'ssh' -e 'nc'命令,得到如下结果,得证。

1
2
3
$ ps -ef | grep -e 'ssh' -e 'nc'
8816 8089 16:08 S pts/10 ssh C
8817 8816 16:08 S pts/10 nc -X 5 -x B:8989 C 1046

ssh命令”端口转发”和”ProxyCommand”选项之间的关系

端口转发包括:本地转发,远端转发和动态转发。其中”本地转发”和”远端转发”属于”静态转发”(因为转发目标端口是固定的)。
ProxyCommand选项能够使用基于”动态转发”的代理机制(在外面封装nc命令层),而不能使用基于”静态转发”的代理机制。

参考文献:

常用 SSH 命令参数

参数 功能描述 示例
-L [本地端口]:[目标主机]:[目标端口] 设置本地端口转发。 ssh -L 8080:localhost:80 user@remote_host
-R [远程端口]:[目标主机]:[目标端口] 设置远程端口转发。 ssh -R 9090:localhost:3000 user@remote_host
-D [本地端口] 设置动态端口转发,创建SOCKS代理。 ssh -D 1080 user@remote_host
-p [端口号] 指定SSH连接的端口(默认为22)。 ssh -p 2222 user@remote_host
-v 启用详细模式,显示调试信息,便于排查连接问题。 ssh -v user@remote_host
-C 启用压缩,适用于慢速连接,可能会提高性能。 ssh -C user@remote_host
-i [私钥文件] 指定使用的SSH私钥文件。 ssh -i ~/.ssh/id_rsa user@remote_host
-N 不执行远程命令,仅用于端口转发。 ssh -L 8080:localhost:80 -N user@remote_host
-f 在后台运行SSH会话,通常与-N结合使用。 ssh -f -L 8080:localhost:80 -N user@remote_host
-o [选项] 可以传递特定的SSH配置选项。 ssh -o StrictHostKeyChecking=no user@remote_host

FAQ

ssh 连接 remote host identification has changed

找到.ssh目录,下面有一个known_hosts文件,删除 ~/.ssh/known_hosts 文件,或者如果你可以判断出known_hosts中原ssh服务器的公钥,删去那部分,

Double and random letters when typing on a SSH MacOS -> Ubuntu connection

这里, 设置远程机器的 TERM 变量为 xterm-256color.

Win10 使用注册表一键添加小鹤双拼方案

方法1

新建.txt 文件

1
2
3
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Microsoft\InputMethod\Settings\CHS]
"UserDefinedDoublePinyinScheme0"="小鹤双拼*2*^*iuvdjhcwfg^xmlnpbksqszxkrltvyovt"

方法2

  1. win+r 输入 regedit, 打开注册表

  2. 找到 计算机\HKEY_CURRENT_USER\Software\Microsoft\InputMethod\Settings\CHS

  3. 新建字符串值

    名为 UserDefinedDoublePinyinScheme0

    值为 小鹤双拼*2*^*iuvdjhcwfg^xmlnpbksqszxkrltvyovt

按键映射

互换按键reg文件

交换 esc 和 caps lock 按键

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
"Scancode Map"=hex:00,00,00,00,00,00,00,00,03,00,00,00,3a,00,01,00,01,00,3a,00,00,00,00,00

交换 ctrl 和 caps lock 按键

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
"Scancode Map"=hex:00,00,00,00,00,00,00,00,03,00,00,00,3a,00,1d,00,1d,00,3a,00,00,00,00,00

取消互换按键

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
"Scancode Map"=-

原理

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout\ 下新建一个二进制值项: “Scancode Map”

Scancode Map格式说明

1
2
3
4
5
6
00 00 00 00 00 00 00 00 (固定格式)
XX 00 00 00 (XX为修改的总键数+1的16进制数)
XX XX XX XX (修改后的按键扫描码 (在前) + 原按键的扫描码 (在后))
XX XX XX XX (另一个要替换的按键,同上)
......
00 00 00 00 (固定格式)

示例

1
2
3
4
0000 00 00 00 00 00 00 00 00 	
0008 04 00 00 00 1D 00 5B E0
0010 1D E0 5C E0 5B E0 1D 00
0018 00 00 00 00
  1. 00 00 00 00 00 00 00 00: 固定,照写就好

  2. 04 00 00 00: 04代表要修改三个按键的映射,两个是02,一个是01

  3. 1D 00 5B E0

    • 1D 00 : 是修改后的按键扫描码;
    • 5B E0 : 是原按键的扫描码;

    左Ctrl键的扫描码是1D 00,左win键的扫描码5B E0;
    这四个字节代表的意思是将 左win键 替换成 左Ctrl键。

  4. 1D E0 5C E0: 这四个字节代表的意思是将 右win键 替换成 右Ctrl键。

  5. 5B E0 1D 00: 这四个字节代表的意思是将 左Ctrl键 替换成 左win键。

  6. 00 00 00 00: 固定,照写就好

键位16进制对应表

office 365

Office 365 Professional Plus

Office 365 Home Premium

Office 365 Business

office 2021

Office ProPlus 2021

下载地址

Visio Pro 2021

下载地址

Project Pro 2021

下载地址

vagrant 安装

下载地址

安装

  1. 添加 box 到vagrant

    1
    vagrant box add <box_name> <box_path>
    • <box_name>: 为 box 取的名字,供后续创建虚拟机
    • <box_path>: vagrantbox 所在地址
  2. 查看是否添加成功

    1
    vagrant box list
  3. 创建虚拟机

    创建一个目录, 然后在该目录下执行以下命令

    1
    vagrant init <box_name>

    会生成一个 Vagrantfile 文件

  4. 修改 Vagrantfile 文件配置

  5. 启动虚拟机

    1
    vagrant up
  6. 进入虚拟机

    1
    vagrant ssh
    • 默认用户: vagrant
    • 用户密码: vagrant
  7. 关闭虚拟机

    1
    vagrant halt

PS:

  • 每次启动虚拟机需要使用命令行(cmd)进入 Vagrantfile 文件所在的目录,然后再使用 vagrant up 命令启动虚拟机。
  • 关机同理,使用命令行(cmd)进入 Vagrantfile 文件所在的目录,然后再使用 vagrant halt 命令关闭虚拟机。

virtualbox 安装

下载地址

安装教程

双击打开, 默认安装就可以

Ubuntu镜像地址

清华镜像站

fate 官方测试使用的版本为 18.04

vagrant 系统外部可访问

win10系统下的设置

step1. 设置防火墙入站规则

  1. 进入windows 防火墙

  2. 控制面板 -> 系统和安全 -> Windows Defender 防火墙 -> 高级设置 -> 入站规则 -> 文件和打印机共享(回显请求 - ICMPv4-In)

  3. 右键属性 -> 作用域 -> 远程IP地址 -> 下列 IP 地址 -> 添加

  4. 我使用的是 此IP地址或子网

    目的: 仅允许部分用户使用

    如添加: 10.132.0.0/16

测试是否成功配置

即查看是否可以 ping 通

step2. 设置 vagrantfile 为桥接网络

  1. 查看名称或接口索引

    • 方法1

      1
      netsh interface ipv4 show interfaces

      我的这里是 WLAN

    • 方法2 (未测试)

      1. 打开”控制面板”。您可以在开始菜单中搜索”控制面板”,然后选择相应的结果打开它。
      2. 在控制面板窗口中,将视图设置为”小图标”或”大图标”,以便更轻松找到所需的项目。
      3. 找到并点击”网络和共享中心”。
      4. 在左侧导航栏中,点击”更改适配器设置”,以打开网络连接设置。
      5. 在网络连接设置中,您将看到可用的网络适配器列表。查找您希望用作桥接网络的适配器,例如您的有线网卡或无线网卡。
      6. 右键单击所选适配器,并选择”属性”。
      7. 在适配器属性对话框中,找到并选中”Internet 协议版本 4 (TCP/IPv4)”,然后点击”属性”按钮。
      8. 在”Internet 协议版本 4 (TCP/IPv4) 属性”对话框中,您可以看到适配器的IP地址配置。请注意当前配置的详细信息,以便在后续步骤中进行参考。
      9. 在同一对话框中,点击”高级”按钮。
      10. 在”高级 TCP/IP 设置”对话框中,切换到”IP 设置”选项卡。
      11. 在”IP 设置”选项卡中,您将看到网卡的名称和接口索引。记下名称或接口索引,这将用于Vagrantfile中的网络配置。
  2. 设置 config.vm.network "public_network", bright: "WLAN"

    bright: 就是上面的“查看名称或接口索引 ”

  3. 启动/重启虚拟机: vagrant up/vagrant reload

  4. 查看ip地址: ifconfig, 我这里的是 10.132.22.5

  5. 设置Vigrant: config.vm.network "public_network", bright: "WLAN", ip: "10.132.22.5"

    也可以加入子网掩码, 如 config.vm.network "public_network", bridge: "WLAN", netmask: "255.255.0.0", ip: "10.132.22.5"

step3. 更改 vagrant 密码

passwd

1
2
sudo su
passwd vagrant

PS: 更多信息查看 Ubuntu 处理方法

FAQ

  • 华硕主板打不开virtualbox虚拟机的ubuntu安装系统 (虚拟机VirtualBox异常退出解决办法)
  • vagrant ssh 连接 remote host identification has changed
  • 解决vagrant ssh登录时permission deny的问题

华硕主板打不开virtualbox虚拟机的ubuntu安装系统 (虚拟机VirtualBox异常退出解决办法)

vagrant 启动失败 There was an error while executing VBoxManage, a CLI used by Vagrant for controlli……

进入 BIOS 下, 开启 Intel (VMX) 虚拟化技术

vagrant ssh 连接 remote host identification has changed

找到.ssh目录,下面有一个known_hosts文件,删除 ~/.ssh/known_hosts 文件,或者如果你可以判断出known_hosts中原ssh服务器的公钥,删去那部分,

解决vagrant ssh登录时permission deny的问题

  1. 进入虚拟机

    • 方法1: vagrant ssh

      Vagrantfile 文件夹下打开 cmd, 运行命令 vagrant ssh

    • 方法2: virtualbox

      打开 Vagrantfile 中的 virtualbox 显示

      1
      2
      3
      config.vm.provider "virtualbox" do |vb|      
      vb.gui = true # 启动机器时显示 VirtualBox GUI
      end

      然后运行 vagrant up (虚拟机处于关机状态) 或 vagrant reload (虚拟机处于开机状态)

  2. 更改 /etc/ssh/sshd_config 文件下的 PasswordAuthentication no 改为 PasswordAuthentication yes

  3. 重启虚拟机 vagrant reload

不在一个小局域网,而在同一个大局域网

那就把局域网扩大一些, 可以通过设置子网掩码来设置

  • 临时设置子网掩码

    1. 查看网络: ifconfig

      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
      enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
      inet 10.0.2.15 netmask 255.255.255.0 broadcast 10.0.2.255
      inet6 fe80::3b:7bff:feb7:3b2d prefixlen 64 scopeid 0x20<link>
      ether 02:3b:7b:b7:3b:2d txqueuelen 1000 (Ethernet)
      RX packets 523 bytes 61926 (61.9 KB)
      RX errors 0 dropped 0 overruns 0 frame 0
      TX packets 387 bytes 70310 (70.3 KB)
      TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

      enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
      inet 10.10.32.32 netmask 255.255.255.0 broadcast 10.10.32.255
      inet6 240c:c983:5:2b48::813 prefixlen 128 scopeid 0x0<global>
      inet6 240c:c983:5:2b48:a00:27ff:feab:56e5 prefixlen 64 scopeid 0x0<global>
      inet6 fe80::a00:27ff:feab:56e5 prefixlen 64 scopeid 0x20<link>
      ether 08:00:27:ab:56:e5 txqueuelen 1000 (Ethernet)
      RX packets 1089 bytes 184808 (184.8 KB)
      RX errors 0 dropped 0 overruns 0 frame 0
      TX packets 85 bytes 12017 (12.0 KB)
      TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

      lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
      inet 127.0.0.1 netmask 255.0.0.0
      inet6 ::1 prefixlen 128 scopeid 0x10<host>
      loop txqueuelen 1000 (Local Loopback)
      RX packets 0 bytes 0 (0.0 B)
      RX errors 0 dropped 0 overruns 0 frame 0
      TX packets 0 bytes 0 (0.0 B)
      TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
    2. 设置子网

      1
      sudo ifconfig enp0s8 10.10.32.32 netmask 255.255.0.0

第22章 Vimrc

在先前的章节中,您学习了如何使用Vim。在本章,您将学习如何组织和配置Vimrc。

Vim如何找到Vimrc

对于Vimrc,常见的理解是在根目录下添加一个 .vimrc 点文件(根据您使用的操作系统,文件路径名可能不同)。

实际上,Vim在多个地方查找vimrc文件。下面是Vim检查的路径:

  • $VIMINIT
  • $HOME/.vimrc
  • $HOME/.vim/vimrc
  • $EXINIT
  • $HOME/.exrc
  • $VIMRUNTIME/default.vim

当您启动Vim时,它将在上面列出的6个位置按顺序检查vimrc文件,第一个被找到的vimrc文件将被加载,而其余的将被忽略。

首先,Vim将查找环境变量 $VIMINIT。如果没有找到,Vim将检查 $HOME/.vimrc。如果还没找到,VIm就检查 $HOME/.vim/vimrc。如果Vim找到了vimrc文件,它就停止查找,并使用 $HOME/.vim/vimrc

关于第一个位置,$VIMINIT 是一个环境变量。默认情况下它是未定义的。如果您想将 ~/dotfiles/testvimrc 作为 $VIMINTI 的值,您可以创建一个包含那个vimrc路径的环境变量。当您运行 export VIMINIT='let $MYVIMRC="$HOME/dotfiles/testvimrc" | source $MYVIMRC'后,VIm将使用 ~/dotfiles/testvimrc 作为您的vimrc文件。

第二个位置,$HOME/.vimrc 是很多Vim用户习惯使用的路径。$HOME 大部分情况下是您的根目录(~)。如果您有一个 ~/.vimrc 文件,Vim将使用它作为您的vimrc文件。

第三个,$HOME/.vim/vimrc,位于 ~/.vim 目录中。您可能已经有了一个 ~/.vim 目录用于存放插件、自定义脚本、或视图文件。注意这里的vimrc文件名没有“点”($HOME/.vim/.vimrc 不会被识别,但 $HOME/.vim/vimrc能被识别)。

第四个,$EXINIT 工作方式与 $VIMINIT 类似。

第五个,$HOME/.exrc 工作方式与 $HOME/.vimrc 类似。

第六个,$VIMRUNTIME/defaults.vim 是Vim编译时自带的默认vimrc文件。在我的电脑中,我是使用Homebrew安装的Vim8.2,所以我的路径是(/usr/local/share/vim/vim82)。如果Vim在前5个位置都没有找到vimrc文件,它将使用这个Vim自带的vimrc文件。

在本章剩余部分,我将假设vimrc使用的路径是 ~/.vimrc

应该把什么放在Vimrc中?

我刚开始配置Vimrc时,曾问过一个问题,“我究竟该把什么放在Vimrc文件中?”。

答案是,“任何您想放的东西”。 直接复制粘贴别人的vimrc文件的确是一个诱惑,但您应当抵制这个诱惑。如果您仍然坚持使用别人的vimrc文件,确保您知道这个vimrc干了什么,为什么他/她要用这些设置?以及他/她如何使用这些设置?还有最重要的是,这个vimrc文件是否符合你的实际需要?别人使用并不代表您也要使用。

Vimrc基础内容

简单地说,一个vimrc是以下内容的集合:

  • 插件
  • 设置
  • 自定义函数
  • 自定义命令
  • 键盘映射

当然还有一些上面没有提到的内容,但总体说,已经涵盖了绝大部分使用场景。

插件

在前面的章节中,我曾提到很多不同的插件,比如fzf.vim, vim-mundo, 还有 vim-fugitive.

十年前,管理插件插件是一个噩梦。但随着很多现代插件管理器的开发,现在安装插件可以在几秒内完成。我现在正在使用vim-plug作为我的插件管理器,所以我在本节中将使用它。相关概念和其他流行的插件管理器应该是类似的。我强烈建议您多试试几个插件管理器,比如:

除了上面列出的,还有很多插件管理器,可以随便看看。要想安装 vim-plug,如果您使用的是Unix,运行:

1
curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

要添加新的插件,将您的插件名(比如,Plug 'github-username/repository-name') 放置在 call plug#begin()call plug#end() 之间的行中. 所以,如果您想安装 emmet-vimnerdtree,将下面的片段放到您的vimrc中:

1
2
3
4
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()

然后保存修改,加载当前vimrc (:source %), 然后运行 :PlugInstall 安装插件。

如果以后您想删除不使用的插件,您只需将插件名从 call 代码块之间移除,保存并加载,然后运行 :PlugClean 命令将它从机器上删除。

Vim 8 有自己的内置包管理器。您可以查阅 :h packages 了解更多信息。在后面一章中,我将向您展示如何使用它。

设置

在任意一个vimrc文件中都可以看到大量的 set 选项。 如果您在命令行模式中运行 set 命令,它只是暂时的。当您关闭Vim,设置就会丢失。比如,为了避免您每次运行Vim时都必须在命令行模式运行 :set relativenumber number 命令,您可以将这个命令添加在vimrc中:

1
set relativenumber number

有一些设置需要您赋予一个值,比如 set tabstop=2。想了解一个设置可以接收什么类型的值,可以查看帮助页。

您也可以使用 let 来代替 set(==确保在选项前添加一个 &号==)。使用 let ,您可以使用表达式进行赋值。比如,要想仅当某个路径存在时,才将该路径赋予 'dictionary' 选项:

1
2
3
4
5
let s:english_dict = "/usr/share/dict/words"

if filereadable(s:english_dict)
let &dictionary=s:english_dict
endif

在后面的章节中您将了解关于Vimscript赋值和条件的知识。

要查看Vim中所有可用的选项,查阅 :h E355

自定义函数

Vimrc是一个很好的用来放置自定义函数的地方。在后面的章节中您将学习如何写您自己的Vimscript函数。

自定义命令

您可以使用 command 创建一个自定义命令行命令。

比如,创建一个用于显示今天日期的基本命令 GimmeDate

1
:command! GimmeDate echo call("strftime", ["%F"])

当您运行 :GimmeDate 时,Vim将显示一个类似 “2021-01-1”的日期。

要创建一个可以接收输入的基本命令,您可以使用 <args> 。如果您想向 GimmeDate 传递一个时间/日期格式参数:

1
2
3
4
5
6
7
:command! GimmeDate echo call("strftime", [<args>])

:GimmeDate "%F"
" 2020-01-01

:GimmeDate "%H:%M"
" 11:30

如果您想限定参数的数目,您可以使用 -nargs 标志。-nargs=0 表示没有参数,-nargs=1 表示传递1个参数,-nargs=+ 表示至少1个参数,-nargs=* 表示传递任意数量的参数,-nargs=? 表示传递0个或1个参数。如果您想传递n个参数,使用 -nargs=n(这里 n 是一个任意整数)。

<args> 有两个变体:<f-args><q-args> 。前者用来向Vimscript函数传递参数,后者用来将用户输入自动转换为字符串。

使用 args:

1
2
3
4
5
6
:command! -nargs=1 Hello echo "Hello " . <args>
:Hello "Iggy"
" returns 'Hello Iggy'

:Hello Iggy
" Undefined variable error

使用 q-args:

1
2
3
:command! -nargs=1 Hello echo "Hello " . <q-args>
:Hello Iggy
" returns 'Hello Iggy'

使用 f-args:

1
2
3
4
5
6
7
8
:function! PrintHello(person1, person2)
: echo "Hello " . a:person1 . " and " . a:person2
:endfunction

:command! -nargs=* Hello call PrintHello(<f-args>)

:Hello Iggy1 Iggy2
" returns "Hello Iggy1 and Iggy2"

当您学了关于Vimscript函数的章节后,上面的函数将更有意义。

查阅 :h command:args 了解更多关于command和args的信息。

键盘映射

如果您发现您重复地执行一些相同的复杂操作,那么为这些复杂操作建立一个键盘映射将会很有用:

比如,在我的vimrc文件中有2个键盘映射:

1
2
3
nnoremap <silent> <C-f> :GFiles<CR>

nnoremap <Leader>tn :call ToggleNumber()<CR>

在第一个中,我将 Ctrl-F 映射到 fzf.vim 插件的 :Gfiles 命令(快速搜索Git文件)上。在第二个中,我将 <leader>tn 映射到调用一个自定义函数 ToggleNumber (切换 norelativenumberrelativenumber 选项)。Ctrl-f 映射覆盖了Vim的原生的页面滚动。如果发生冲突,您的映射将会覆盖Vim的设置。因为从几乎从来不用Vim原生的页面滚动功能,所以我认为可以安全地覆盖它。

另外,在 <Leader>tn 中的 “leader” 键到底是什么?

Vim有一个leader键用来辅助键盘映射。比如,我将 <leader>tn 映射为运行 ToggleNumber() 函数。如果没有leader键,我可能会用 tn,但Vim中的 t 已经用做其他功能(”till”搜索导航命令)了。有了leader键,我现在先按定义好的leader键作为开头,然后按 tn,而不用干扰已经存在的命令。您可以设置leader键作为您映射的连续按键的第一个按键。默认Vim使用反斜杠作为leader键(所以 <Leader>tn 会变成 “反斜杠-t-n”)。

我个人喜欢使用空格 <Space> 作为leader键,代替默认的反斜杠。要想改变您的leader键,将下面的文本添加到您的vimrc中:

1
let mapleader = "\<space>"

上面的 nnoremap 命令可以分解为三个部分:

  • n 表示普通模式。
  • nore 表示禁止递归。
  • map 是键盘映射命令。

如果不想使用 nnoremap,您至少也得使用 nmap (nmap <silent> <C-f> :Gfiles<CR>)。但是,最好还是使用禁止递归的版本,这样是为了避免键盘映射时潜在的无限循环风险。

如果您进行键盘映射时不使用禁止递归,下面例子演示了会发生什么。假设您想给 B 添加一个键盘映射,用来在一行的末尾添加一个分号,然后跳回前一个词组(回想一下,B 是Vim普通模式的一个导航命令,用来跳回前一个词组)。

1
nmap B A;<esc>B

当您按下 B …哦豁,Vim开始失控了,开始无止尽的添加;(用 Ctrl-c终止)。为什么会发生这样的情况?因为在键盘映射 A;<esc>B中,这个 B不再是Vim原生的导航命令,它已经被映射到您刚才创建的键盘映射中了。这是您实际上执行的操作序列:

1
A;<esc>A;<esc>A;<esc>A;esc>...

要解决这个问题,您需要指定键盘映射禁止递归:

1
nnoremap B A;<esc>B

现在再按一下 B 试试。这一次它成功地在行尾添加了一个 ;,然后跳回到前一个词组。这个映射中的 B 就表示Vim原生的 B了。

Vim针对不同的模式有不同的键盘映射命令。如果您想创建一个插入模式下的键盘映射 jk,用来退出插入模式:

1
inoremap jk <esc>

其他模式的键盘映射命令有:map(普通、可视、选择、以及操作符等待模式), vmap(可视、选择), smap(选择), xmap(可视), omap(操作符等待模式), map!(插入、命令行), lmap(插入,命令行,Lang-arg模式), cmap(命令行), 还有tmap(终端任务)。在这里我不会详细的讲解它们,要了解更多信息,查阅 :h map.txt

创建最直观、最一致、最易于记忆的键盘映射。

组织管理Vimrc

一段时候键,您的vimrc文件就会变大且复杂得难以阅读。有两种方法让您的vimrc文件保持整洁:

  • 将您的vimrc文件划分为几个文件
  • 折叠您的vimrc文件

划分您的vimrc

您可以使用Vim的 :source 命令将您的vimrc文件划分为多个文件。这个命令可以根据给定的文件参数,读取文件中的命令行命令。

让我们在 ~/.vim 下创建一个子文件夹,取名为 /settings~/.vim/settings)。名字可以取为任意您喜欢的名字。

然后你在这个文件夹下创建4个文件:

  • 第三方插件 (~/.vim/settings/plugins.vim).
  • 通用设置 (~/.vim/settings/configs.vim).
  • 自定义函数 (~/.vim/settings/functions.vim).
  • 键盘映射 (~/.vim/settings/mappings.vim) .

~/.vimrc 里面添加:

1
2
3
4
source $HOME/.vim/settings/plugins.vim
source $HOME/.vim/settings/configs.vim
source $HOME/.vim/settings/functions.vim
source $HOME/.vim/settings/mappings.vim

~/.vim/settings/plugins.vim 里面:

1
2
3
4
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()

~/.vim/settings/configs.vim 里面:

1
2
3
set nocompatible
set relativenumber
set number

~/.vim/settings/functions.vim 里面:

1
2
3
4
5
6
7
function! ToggleNumber()
if(&relativenumber == 1)
set norelativenumber
else
set relativenumber
endif
endfunc

~/.vim/settings/mappings.vim 里面:

1
2
3
inoremap jk <esc>
nnoremap <silent> <C-f> :GFiles<CR>
nnoremap <Leader>tn :call ToggleNumber()<CR>

这样您的vimrc文件依然能够正常工作,但现在它只有4行了。

使用这样的设置,您可以轻易知道到哪去修改配置。如果您要添加一些键盘映射,就将它们添加在 /mappings.vim 文件中。以后,当您的vimrc变大时,您总是可以新建几个子文件来缩小它的大小。比如,如果您想为主题配色创建相关设置,您可以添加 ~/.vim/settings/themes.vim

保持单独的一个Vimrc文件

如果您倾向于保持一个单独的vimrc文件,以使它更加便于携带,您可以使用标志折叠让它保持有序。在vimrc文件的顶部添加一下内容:

1
2
3
4
5
6
" setup folds {{{
augroup filetype_vim
autocmd!
autocmd FileType vim setlocal foldmethod=marker
augroup END
" }}}

Vim能够检测当前buffer所属的文件类型 (:set filetype?). 如果发现属于 vim 类型,您可以使用标志折叠。回想一个标志折叠的用法,它使用 {{{}}} 来指明折叠的开始和结束。

添加 {{{}}} 标志将您的vimrc文件其他部分折叠起来。(别忘了使用 " 对标志进行注释):

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
" setup folds {{{
augroup filetype_vim
autocmd!
autocmd FileType vim setlocal foldmethod=marker
augroup END
" }}}

" plugins {{{
call plug#begin('~/.vim/plugged')
Plug 'mattn/emmet-vim'
Plug 'preservim/nerdtree'
call plug#end()
" }}}

" configs {{{
set nocompatible
set relativenumber
set number
" }}}

" functions {{{
function! ToggleNumber()
if(&relativenumber == 1)
set norelativenumber
else
set relativenumber
endif
endfunc
" }}}

" mappings {{{
inoremap jk <esc>
nnoremap <silent> <C-f> :GFiles<CR>
nnoremap <Leader>tn :call ToggleNumber()<CR>
" }}}

您的vimrc文件将会看起来类似下面:

1
2
3
4
5
6
7
8
9
+-- 6 lines: setup folds -----

+-- 6 lines: plugins ---------

+-- 5 lines: configs ---------

+-- 9 lines: functions -------

+-- 5 lines: mappings --------

启动Vim时加载/不加载Vimrc和插件

如果您要启动Vim时,既不加载Vimrc,也不加载插件,运行:

1
vim -u NONE

如果您要启动Vim时,不加载Vimrc,但加载插件,运行:

1
vim -u NORC

如果您要启动Vim时,加载Vimrc,但不加载插件,运行

1
vim --noplugin

如果您要Vim启动加载一个 其他的 vimrc, 比如 ~/.vimrc-backup, 运行:

1
vim -u ~/.vimrc-backup

聪明地配置Vimrc

Vimrc是定制Vim时的一个重要组件,学习构建您的Vimrc最好是首先阅读他人的vimrc文件,然后逐渐地建立自己的。最好的vimrc并不是谁谁谁使用的,而是最适合您的工作需要和编辑风格的。

第23章 Vim软件包

在前面的章节中,我提到使用第三方插件管理器来安装插件。从Vim 8开始,Vim自带了一个内置的插件管理器,名叫 软件包(packages)。在本章,您将学习如何使用Vim软件包来安装插件。

要看您的Vim编译版本是否能够使用软件包,运行 :version。然后查看是否有 +packages属性。另外,您也可以运行 :echo has('packages')(如果返回1,表示可以使用软件包)。

包目录

在根目录下查看您是否有一个 ~/.vim 文件夹。如果没有就新建一个。在文件夹里面,创建一个子文件夹取名 pack(~/.vim/pack/)。Vim会在这个子文件夹内自动搜索插件。

两种加载方式

Vim软件包有两种加载机制:自动加载和手动加载。

自动加载

要想让Vim启动时自动加载插件,你需要将它们放置在 start/子目录中。路径看起来像这样:

1
~/.vim/pack/*/start/

现在您可能会问,为什么在pack/start/ 之间有一个 * ?这个星号可以是任意名字。让我们将它取为packdemo/

1
~/.vim/pack/packdemo/start/

记住,如果您忽略这一点,用下面的路径代替的话:

1
~/.vim/pack/start/

软件包系统是不会正常工作的。 必须在pack/start/之间添加一个名字才能正常运行。

在这个示例中,让我们尝试安装 [NERDTree](https://github.com/preservim/nThe package system won’t work. It is imperative to put a name between pack/ and start/.erdtree) 插件。用任意方法进入 start/ 目录(cd ~/.vim/pack/packdemo/start/),然后将NERDTree的仓库克隆下来:

1
git clone https://github.com/preservim/nerdtree.git

完成了!您已经完成了安装。下一次您启动Vim,您可以立即执行 NERDTree 命令 :NERDTreeToggle

~/.vim/pack/*/start/ 目录中,您想克隆多少插件仓库就克隆多少。Vim将会自动加载每一个插件。如果您删除了克隆的仓库(rm -rf nerdtree),那么插件就失效了。

手动加载

要想在Vim启动时手动加载插件,您得将相关插件放置在 opt/ 目录中,类似于自动加载,这个路径看起来像这样:

1
~/.vim/pack/*/opt/

让我们继续使用前面的 packdemo/ 这个名字:

1
~/.vim/pack/packdemo/opt/

这一次,让我们安装killersheep 游戏(需要Vim8.2以上版本)。进入opt/ 目录(cd ~/.vim/pack/packdemo/opt/) 然后克隆仓库:

1
git clone https://github.com/vim/killersheep.git

启动Vim。执行游戏的命令是 :KillKillKill。试着运行一下。Vim将会提示这不是一个有效的编辑命令。您需要首先 手动 加载插件,运行:

1
:packadd killersheep

现在再运行一下 :KillKillKill 。命令已经可以使用了。

您可能好奇,“为什么我需要手动加载插件?启动时自动加载岂不是更好?”

很好的问题。有时候有些插件我们并不是所有的时候都在用,比如 KillerSheep 游戏。您可能不会想要加载10个不同的游戏导致Vim启动变慢。但是偶尔当您觉得乏味的时候,您可能想要玩几个游戏,使用手动加载一些非必须的插件。

您也可以使用这个方法有条件的加载插件。可能您同时使用了Neovim和Vim,有一些插件是为NeoVim优化过的。您可以添加类似下列的内容到您的vimrc中:

1
2
3
4
5
if has('nvim')
packadd! neovim-only-plugin
else
packadd! generic-vim-plugin
endif

组织管理软件包

回想一下,要使用Vim的软件包系统必须有以下需求:

1
~/.vim/pack/*/start/

或者:

1
~/.vim/pack/*/opt/

实际上,*星号可以使 任意 名字,这个名字就可以用来管理您的插件。假设您想将您的插件根据类型(颜色、语法、游戏)分组:

1
2
3
~/.vim/pack/colors/
~/.vim/pack/syntax/
~/.vim/pack/games/

您仍然可以使用各个目录下的 start/opt/

1
2
3
4
5
6
7
8
~/.vim/pack/colors/start/
~/.vim/pack/colors/opt/

~/.vim/pack/syntax/start/
~/.vim/pack/syntax/opt/

~/.vim/pack/games/start/
~/.vim/pack/games/opt/

聪明地添加插件

您可能好奇,Vim软件包是否让一些流行的插件管理器,比如 vim-pathogen, vundle.vim, dein.vim, a还有vim-plug面临淘汰?

答案永远是:“看情况而定。”

我仍然使用vim-plug,因为使用它添加、删除、更新插件很容易。如果您使用了很多插件,插件管理器的好处更加明显,因为使用它可以对很多插件进行同时更新。有些插件管理器同时也提供了一些异步功能。

如果您是极简主义者,可以尝试一下Vim软件包。如果您是一名插件重度使用者,您可能需要一个插件管理器。

第24章 Vim Runtime

在前面的章节中,我提到Vim会自动查找一些特殊的路径,比如在~/.vim/ 中的 pack/(第23章) compiler/(第19章)。这些都是Vim runtime路径的例子。

除了上面提到的两个,Vim还有更多runtime路径。在本章,您将学习关于Vim runtime路径的高层次概述。本章的目标是向您展示它们什么时候被调用。知道这些知识能够帮您更进一步理解和定制Vim。

Runtime路径

在一台Unix机器中,其中一个vim runtime路径就是 $HOME/.vim/ (如果您用的是其他操作系统,比如Windows,您的路径可能有所不同)。要查看不同的操作系统有什么样的runtime路径,查阅 :h runtimepath。在本章,我将使用 ~/.vim/ 作为默认的runtime路径。

Plugin脚本

Vim有一个runtime路径 plugin,每次Vim启动时都会执行这个路径中的所有脚本。不要把这个名字 “plugin” 和Vim的外部插件(external plugins,比如NERDTree, fzf.vim, 等)搞混了。

进入 ~/.vim/ 目录,然后创建 plugin/ 子目录。 创建两个文件: donut.vimchocolate.vim

~/.vim/plugin/donut.vim里面:

1
echo "donut!"

~/.vim/plugin/chocolate.vim里面:

1
echo "chocolate!"

现在关闭Vim。下次您启动Vim,您将会看到 "donut!":chocolate! 的显示。此 plugin runtime路径可以用来执行一些初始化脚本。

文件类型检测

在开始之前,为保证检测能正常运行,确保在您的vimrc中至少包含了下列的行:

1
filetype plugin indent on

查阅 :h filetype-overview 了解更多信息。本质上,这条代码开启Vim的文件类型检测。

当您打开一个新的文件,Vim通常知道这个文件是什么类型。如果您有一个文件 hello.rb,运行 :set filetype? 会返回正确的相应 filetype=ruby

Vim知道如何检测 “常见” 的文件类型(Ruby, Python, Javascript, 等)。但如果是一个自定义文件会怎样呢?您需要告诉Vim去检测它,并给它指派一个正确的文件类型。

有两种检测方法:使用文件名和使用文件内容

文件名检测

文件名检测使用一个文件的文件名来检测文件类型。当您打开 hello.rb文件时,Vim依靠扩展名 .rb 知道它是一个Ruby文件。

有两种方法实现文件名检测:一是使用 ftdetect runtime目录,二是使用 filetype.vim runtime文件。我们两个都看一看。

ftdetect/

让我们创建一个古怪(但优雅)的名字,hello.chocodonut。当您打开它后运行 :set filetype? ,因为它的后缀名不是常见的文件名,Vim不知道它是什么类型,会返回 filetype=

您需要指示Vim将所有以 .chocodonut结尾的文件设置为 “chocodonut”类型的文件。在runtime路径根目录(~/.vim/)创建一个子目录,名为 ftdetect/ 。在子目录里面,再创建一个名叫 chocodonut.vim 的文件(~/.vim/ftdetect/chocodonut.vim),在文件里面,添加:

1
autocmd BufNewFile,BufRead *.chocodonut set filetype=chocodonut

当您创建新buffer或打开新buffer时,事件BufNewFileBufRead 就会被触发。 *.chocodonut 意思是只有当新打开的buffer文件名后缀是 .chocodonut 时事件才会被触发。最后,set filetype=chocodonut 命令将文件类型设置为chocodonut类型。

重启Vim。新建一个 hello.chocodonut 文件然后运行 :set filetype?。它将返回 filetype=chocodonut.

好极了!只要您想,您可以将任意多的文件放置在 ftdetect/ 中。以后,如果您想扩展您的 donut 文件类型,你可以添加 ftdetect/strawberrydonut.vim, ftdetect/plaindonut.vim 等等。

在Vim中,实际上有两种方法设置文件类型。其中给一个是您刚刚使用的 set filetype=chocodonut。另一种方法是运行 setfiletype chocodonut。前一个命令 set filetype=chocodonut总是 设置文件类型为chocodonut。 而后者setfiletype chocodonut只有当文件类型尚未设置时,才会将文件类型设置为chocodonut。

文件类型文件

第二种文件类型检测需要你创建一个名为 filetype.vim的文件,并将它放置在根目录(~/.vim/filetype.vim)。在文件内添加一下内容:

1
autocmd BufNewFile,BufRead *.plaindonut set filetype=plaindonut

创建一个名为 hello.plaindonut 的文件。当你打开它后运行 :set filetype? Vim会显示正确的自定义文件类型 filetype=plaindonut

太好了,修改生效了。另外,如果您仔细看看 filetype.vim ,您会发现当您打开hello.plaindonut时,这个文件文件运行了多次。为防止这一点,您可以添加一个守卫,让主脚本只运行一次。更新 filetype.vim:

1
2
3
4
5
6
7
if exists("did_load_filetypes")
finish
endif

augroup donutfiletypedetection
autocmd! BufRead,BufNewFile *.plaindonut setfiletype plaindonut
augroup END

finish 是一个Vim命令,用来停止执行剩余的脚本。表达式"did_load_filetypes"不是 一个Vim内置函数。它实际上是$VIMRUNTIME/filetype.vim 中的一个全局变量。如果您好奇,运行:e $VIMRUNTIME/filetype.vim。您将会发现以下内容:

1
2
3
4
5
if exists("did_load_filetypes")
finish
endif

let did_load_filetypes = 1

当Vim调用这个文件时,它会定义 did_load_filetypes 变量,并将它设置为 1 。在Vim中,1 表示真。你可以试着读完 filetype.vim 剩余的内容,看看您是否能够理解当Vim调用它时干了什么。

文件类型脚本

让我们学习如何基于文件内容检测文件类型。

假设您有一个无扩展名的文件的集合。这些文件唯一相同的地方是,第一行都是以 “donutify” 开头。您现在想给这些文件指派一个 donut 的文件类型。创建新文件,起名为 sugardonut, glazeddonut, 还有 frieddonut(没有扩展名)。在每个文件中,添加下列内容:

1
donutify

当您在sugardonut中运行 :set filetype?,Vim无法知道应该给这个文件指派什么文件类型,会返回 filetype=

在runtime根目录,添加一个 scripts.vim 文件(~/.vim/scripts.vim),在文件中,添加一下内容:

1
2
3
4
5
6
7
if did_filetype()
finish
endif

if getline(1) =~ '^\\<donutify\\>'
setfiletype donut
endif

函数 getline(1) 返回文件第一行的内容。它检查第一行是否以 “donutify” 开头。函数 did_filetype() 是Vim的内置函数,当一个与文件类型相关的事件发生至少一次时,它返回真。它用来做守卫,防止文件类型事件反复运行。

打开文件 sugardonut 然后运行 :set filetype?,Vim现在返回 filetype=donut。如果您打开另外一个donut文件 (glazeddonutfrieddonut),Vim同样会将它们的文件类型定义为 donut 类型。

注意,scripts.vim 仅当Vim打开一个未知文件类型的文件时才会运行。如果Vim打开一个已知文件类型的文件,scripts.vim 将不会运行。

文件类型插件

如果您想让Vim仅当您打开一个 chocodonut 文件时才运行 chocodonut 相关的特殊脚本,而当您打开的是 plaindonut 文件时,Vim就不运行这些脚本。能否做到呢?

您可以使用文件类型插件runtime路径(~/.vim/ftplugin/)来完成这个功能。Vim会在这个目录中查找一个文件,这个文件的文件名与您打开的文件类型一样。创建一个文件,起名为chocodonut.vim (~/.vim/ftplugin/chocodonut.vim):

1
echo "Calling from chocodonut ftplugin"

创建另一个 ftplugin 文件,起名为plaindonut.vim (~/.vim/ftplugin/plaindonut.vim):

1
echo "Calling from plaindonut ftplugin"

现在,每次您打开一个 chocodonut 类型的文件时,Vim会运行 ~/.vim/ftplugin/chocodonut.vim中的脚本。每次您打开 plaindonut 类型的文件时,Vim会运行 ~/.vim/ftplugin/plaindonut.vim 中的脚本。

一个警告:每当一个buffer的文件类型被设置时(比如,set filetype=chocodonut),上述脚本就会运行一次。如果您打开3个不同的 chocodonut 文件,该脚本将运行 总共 3次。

缩进文件

Vim有一个 缩进runtime路径,其工作方式与ftplugin类似,Vim也会在这个目录中查找一个与打开的文件类型名字一样的文件。缩进runtime路径的目的是存储缩进相关的代码。如果您有文件 ~/.vim/indent/chocodonut.vim,它仅当您打开一个 chocodonut 类型的文件时执行。您可以将 chocodonut 文件中缩进相关的代码存储在这里。

颜色

Vim 有一个颜色runtime路径 (~/.vim/colors/) ,用来存储颜色主题。这个目录中的任何文件都会在命令行命令 :color 中显示出来。

如果您有一个文件 ~/.vim/colors/beautifulprettycolors.vim,当您运行 :color 然后按 Tab,您将会看到 beautifulprettycolors 出现在颜色选项中。 如果您想添加自己的颜色主题,就放在这个地方。

如果您想看其他人做的颜色主题,有一个好地方值得推荐:vimcolors

语法高亮

Vim有一个语法runtime路径 (~/.vim/syntax/),用来定义语法高亮。

假设您有一个文件 hello.chocodonut,在文件里面有以下内容:

1
2
(donut "tasty")
(donut "savory")

虽然Vim现在知道了正确的文件类型,但所有的文本都是相同的颜色。让我们添加语法高亮规则,使 “donut” 关键词高亮显示。创建一个新的 chocodonut 语法文件 ~/.vim/syntax/chocodonut.vim,在文件中添加:

1
2
3
syntax keyword donutKeyword donut

highlight link donutKeyword Keyword

现在重新打开 hello.chocodonut 文件,关键词 donut 已经高亮显示了。

本章不会详细介绍语法高亮。它是一个庞大的主题。如果您感兴趣,可以查阅 :h syntax.txt

vim-polyglot 插件非常的棒,它提供了很多流行的编程语言的语法高亮。

文档

如果您写了一个插件,您还得创建一个您自己的文档。您可以使用文档runtime路径完成这个。

让我们为 chocodonut 和 plaindonut 关键字创建一个基本文档。创建文件 donut.txt (~/.vim/doc/donut.txt)。在文件中,添加一下内容:

1
2
3
*chocodonut* Delicious chocolate donut

*plaindonut* No choco goodness but still delicious nonetheless

如果您试着搜索 chocodonutplaindonut (:h chocodonut:h plaindonut),您找不到任何东西。

首先,你需要运行 :helptags来创建新的帮助入口。运行 :helptags ~/.vim/doc/

现在,如果您运行 :h chocodonut:h plaindonut,您将找到上面那些新的帮助入口。注意,现在文件是只读的,而且类型是 “help”。

延时加载脚本

到现在,本章您学到的所有runtime路径都是自动运行的。如果您想手动加载一个脚本,可使用 autoload runtime路径。

创建一个目录名为 autoload(~/.vim/autoload/)。在目录中,创建一个新文件,起名为 tasty.vim (~/.vim/autoload/tasty.vim)。在文件中:

1
2
3
4
5
echo "tasty.vim global"

function tasty#donut()
echo "tasty#donut"
endfunction

注意,函数名是 tasty#donut 而不是 donut()。要想使用autoload功能,井号(#)是必须的。在使用autoload功能时,函数的命名惯例是:

1
2
3
function fileName#functionName()
...
endfunction

在本例中,文件名是 tasty.vim,而函数名是donut

要调用一个函数,可以使用 call 命令。让我们call这个函数 :call tasty#donut()

您第一次调用这个函数时,您应当会 同时 看到两条信息 (“tasty.vim global” 和 “tasty#donut”) 。后面再调用 tasty#donut 函数,将只会显示 “testy#donut”。

当您在Vim中打开一个文件,不像前面说的runtime路径,autoload脚本不会被自动加载。仅当您显式地调用 tasty#donut(),Vim才会查找文件tasty.vim,然后加载文件中的内容,包括函数 tasty#donut()。有些函数会占用大量资源,但我们又不常用,这时候 Autoload runtime路径就是最佳的解决方案。

您可以在autoload目录任意添加嵌套的目录。如果您有一个runtime路径 ~/.vim/autoload/one/two/three/tasty.vim,您可以使用:call one#two#three#tasty#donut()来调用函数。

After脚本

Vim有一个 after runtime路径 (~/.vim/after/) ,它的结构是 ~/.vim/的镜像。在此目录中的任何脚本都会最后执行,所以开发者通常使用这个路径来重载脚本。

比如,如果您想重载 plugin/chocolate.vim 中的脚本,您可以创建~/.vim/after/plugin/chocolate.vim来放置重载脚本。Vim将会先运行 ~/.vim/plugin/chocolate.vim然后运行 ~/.vim/after/plugin/chocolate.vim

$VIMRUNTIME

Vim有一个环境变量 $VIMRUNTIME 用来加载默认脚本和支持文件。您可以运行 :e $VIMRUNTIME查看。

它的结构应该看起来很熟悉。它包含的很多runtime路径都是我们本章前面学过的。

回想第22章,当您打开Vim时,它会在6个不同的位置查找vimrc文件。当时我说最后一个位置就是 $VIMRUNTIME/default.vim,如果Vim在前5个位置查找用户vimrc文件失败,就会使用default.vim 作为vimrc。

不知您是否尝试过,运行Vim是不加载比如vim-polyglot之类的语法插件,但您的文件依然有语法高亮?这是因为当Vim在runtime路径查找语法文件失败时,会从$VIMRUNTIME 的语法目录中查找语法文件。

查阅 :h $VIMRUNTIME了解更多信息。

Runtimepath选项

运行 :set runtimepath?,可以查看您的runtime路径。

如果您使用 Vim-Plug 或其他流行的第三方插件管理器,它应该会显示一个目录列表。比如,我的显示如下:

1
runtimepath=~/.vim,~/.vim/plugged/vim-signify,~/.vim/plugged/base16-vim,~/.vim/plugged/fzf.vim,~/.vim/plugged/fzf,~/.vim/plugged/vim-gutentags,~/.vim/plugged/tcomment_vim,~/.vim/plugged/emmet-vim,~/.vim/plugged/vim-fugitive,~/.vim/plugged/vim-sensible,~/.vim/plugged/lightline.vim, ...

插件管理器做了一件事,就是将每个插件添加到runtime路径中。每个runtime路径都有一个类似 ~/.vim/的目录结构。

如果您有一个目录 ~/box/of/donuts/,然后您想将这个目录添加到您的runtime路径中,您可以在vimrc中添加以下内容:

1
set rtp+=$HOME/box/of/donuts/

如果在 ~/box/of/donuts/ 里面,您有一个plugin目录 (~/box/of/donuts/plugin/hello.vim) 以及ftplugin目录 (~/box/of/donuts/ftplugin/chocodonut.vim),当您打开Vim时,Vim将会运行 plugin/hello.vim 中所有脚本。同样,当您打开一个 chocodonut 文件时,Vim 将会运行 ftplugin/chocodonut.vim

自己试着做一下:创建一个任意目录,然后将它添加到您的 runtimepath中。添加一些我们本章学到的runtime路径。确保它们按预期工作。

聪明地学习Runtime

花点时间阅读本章,还有认真研究一下这些runtime路径。看一下真实环境下runtime路径是如何使用的。浏览一下您最喜欢的Vim插件仓库,仔细研究一下它的目录结构,您应该能够理解它们中的绝大部分。试着领会重点并跟着做。现在您已经理解了Vim的目录结构,您可以准备学习Vimscript了。

第25章 Vimscript 基础和数据类型

Ex 模式

技术上来说,Vim 没有内置的交互式解释器 (REPL),但是有一个 Ex 模式,可以使 vim 像使用 REPL 一样、Ex 模式更现实命令行模式的拓展,即非停止的命令行模式

进入EX模式的方法:

  • Q
  • gQ

退出Ex模式

  • 输入: :visual

echo

类似于 pythonprint()

echom: 和 echo 相同, 不同支出在于 echom 会将结果保存到 message 历史记录中

  • 查看 message 历史记录

    :messages

  • 清空 mwssage 历史记录

    :message clear

数字

模式指的是==十进制==数字

十进制 Decimal

1, -1, -10

十六进制 Hexadecimal

0xoX 开头

二进制 Binary

0b0B 开头

八进制 Octal

0, 0o0O 开头

真假值

  • 真:非0
  • 假:0

数值运算

同 C++ 的数值运算

字符串

使用 双引号("") 或 单引号('') 包裹起来

字符串连接

使用 . 连接字符串

1
:echo "Hello" . "world"

字符串算数

当使用数字和字符串使用运算符 + - * / 进行算数运算时,vim会将字符串转化为数字

1
:echo "12 donuts" + 3

也可以是字符串与字符串进行数值计算

1
:echo "12 donuts" * "6 pastries"

要求

  • 数字字符必须是字符串的第一个字符

注意

  • 当使用浮点型进行运算时,将被转化成整数

数字与字符串连接

使用 . 运算符

注意

  • 不适用于浮点数

字符串作为条件时

  • 真:第一个字符为数字
  • 假:第一个字符不为数字

单引号与双引号

  • 单引号只会输出字面值
  • 双引号可以接受特殊字符,如: \n, \"

内置字符串程序

程序 功能
strlen() 获取字符串长度
str2nr() 将字符串转化为数字
str2float() 将字符串转化为浮点数
substitute() 字符串模式替换
可以结合 getline() 使用
其他函数 :h string-functions

getline(): 获取传入行号的文本

列表

类似于 Python 的列表,类型可以混合在一起

子列表

获取单个值

同 Python 的索引,类似于list[n]

切片

  • list[n:m] 获取 list 列表中的索引号 n 到 m 的元素 (包括m)
  • list[n:]: n到末尾
  • list[:m]: 从头到m

注: 若 m > list 的长度,那么就是获取 m 到末尾的元素

切片字符串

同list

数值运算

使用 +: 连接两个劫镖

函数

函数 功能
len() 获得列表长度
insert() 将元素传输到列表开始位置
remove() 删除列表元素
map()
filter()
用来过滤元素 ==????==
1
2
3
4
5
6
7
8
9
:let sweeterList = ["glazed", "chocolate", "strawberry"]
:call filter(sweeterList, 'v:val !~ "choco"')
:echo sweeterList
" returns ["glazed", "strawberry"]

:let sweetestList = ["chocolate", "glazed", "sugar"]
:call map(sweetestList, 'v:val . " donut"')
:echo sweetestList
" returns ['chocolate donut', 'glazed donut', 'sugar donut']

解包

像python

1
2
3
4
5
6
7
8
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let [flavor1, flavor2, flavor3] = favoriteFlavor

:echo flavor1
" returns "chocolate"

:echo flavor2
" returns "glazed"

使用 ; 分配剩余的元素

1
2
3
4
5
6
7
8
:let favoriteFruits = ["apple", "banana", "lemon", "blueberry", "raspberry"]
:let [fruit1, fruit2; restFruits] = favoriteFruits

:echo fruit1
" returns "apple"

:echo restFruits
" returns ['lemon', 'blueberry', 'raspberry']

修改列表

修改一项

1
2
3
4
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let favoriteFlavor[0] = "sugar"
:echo favoriteFlavor
" returns ['sugar', 'glazed', 'plain']

修改多项

1
2
3
4
:let favoriteFlavor = ["chocolate", "glazed", "plain"]
:let favoriteFlavor[2:] = ["strawberry", "chocolate"]
:echo favoriteFlavor
" returns ['chocolate', 'glazed', 'strawberry', 'chocolate']

字典

类似于 python

==区别==在于 vim 只使用字符串为“键”,即使使用数字,也会将数字转化为字符串

偷懒方法

使用 #{} 来表示

1
2
3
4
:let mealPlans = #{breakfast: "waffles", lunch: "pancakes", dinner: "donuts"}

:echo mealPlans
" returns {'lunch': 'pancakes', 'breakfast': 'waffles', 'dinner': 'donuts'}

要求

“键”的字符必须满足一下条件:

  • ASCII character.
  • Digit.
  • An underscore (_).
  • A hyphen (-).

访问字典

  • []

    1
    meal['breakfast']
  • .

    1
    meal.lunch

修改字典

  • []`

    1
    meal['breakfast'] = "..."
  • .

    1
    meal.lunch = "..."

内置函数

函数 功能
len() 获取字典长度
has_key() 获取字典的键
empty() 字典是否有键值对
remove() 移除键值对
items() 将键值对转化为列表:每个键值对为一个列表
filter() and map() 过滤作用

特殊基元

  • v:false
  • v:true
  • v:none
  • v:null

注:

  • v: 是 vim 内置的变量
  • 这些不常用
  • 常用如下
    • 布尔值:0(假);non-0 (真)
    • 空字符串:""

True

1
2
:echo json_encode({"test": v:true})
" returns {"test": true}

False

1
2
:echo json_encode({"test": v:false})
" returns {"test": false}

None

1
2
:echo json_encode({"test": v:none})
" returns {"test": null}

被解释成 null

Null

1
2
:echo json_encode({"test": v:null})
" returns {"test": null}

类似于 v:none

第26章 条件和循环

关系表达式

  • ==
  • !=
  • >
  • >=
  • <
  • <=

注意:这里的字符串仍被转换为数字

字符串逻辑运算

用关系运算符来比较字符串

  • =~:对字符串进行正则表达式匹配:可以模式匹配上
  • !~: 对字符串进行正则表达式匹配:不可以匹配上
  • ==: 完全匹配:相等
  • !=:完全匹配:不相等

注意:如果在设置了 set ignorecase,那么比较时也会忽略大小写

设置总是忽略大小写

  • 在末尾加一个 #,如:=~# (推荐:安全)

    1
    echo str =~# "hearty"
  • 在末尾加一个 ?,如:=~?

    1
    echo str =~? "hearty"

if

最小的 if 语句

1
2
3
if {clasuse}
{some expression}
endif

if 全貌

1
2
3
4
5
6
7
8
9
if {predicate1}
{expression1}
elseif {predicate2}
{expression2}
elseif {predicate3}
{expression3}
else
{expression4}
endif

三元表达式

1
{predicate} ? expressiontrue : expressionfalse

补充

  • background: 背景
  • strftime(): 目前时间

or

||

注意:短路运算

and

&&

注意:短路运算

for

方法1

1
2
3
4
5
let breakfasts = ["pancakes", "waffles", "eggs"]

for breakfast in breakfasts
echo breakfast
endfor

方法2

1
2
3
4
5
let meals = [["breakfast", "pancakes"], ["lunch", "fish"], ["dinner", "pasta"]]

for [meal_type, food] in meals
echo "I am having " . food . " for " . meal_type
endfor

方法3

1
2
3
4
let beverages = #{breakfast: "milk", lunch: "orange juice", dinner: "water"}
for beverage_type in keys(beverages)
echo "I am drinking " . beverages[beverage_type] . " for " . beverage_type
endfor

while

常见

1
2
3
4
5
let counter = 1
while counter < 5
echo "Counter is: " . counter
let counter += 1
endwhile

获得每行内容

1
2
3
4
5
6
7
let current_line = line(".")
let last_line = line("$")

while current_line <= last_line
echo getline(current_line)
let current_line += 1
endwhile

Error Handling

break

和 python 一样:中断循环

Continue

和 python 一样:跳过当前轮

try, finally, and catch

和 python 一样

  • finally: 总是被执行,且是最后被执行
  • catch: 捕获错误

  • throw:抛出一个错误

catch 捕捉的错误

:h :catch

1
2
3
4
5
6
7
8
catch /^Vim:Interrupt$/.             " catch interrupts (CTRL-C)
catch /^Vim\\%((\\a\\+)\\)\\=:E/. " catch all Vim errors
catch /^Vim\\%((\\a\\+)\\)\\=:/. " catch errors and interrupts
catch /^Vim(write):/. " catch all errors in :write
catch /^Vim\\%((\\a\\+)\\)\\=:E123:/ " catch error E123
catch /my-exception/. " catch user exception
catch /.*/ " catch everything
catch. " same as /.*/

第27章 变量范围

变量与常量

变量

定义变量

1
let val = "value"

let 定义的是==变量==

修改变量

当需要改变变量的值时,需要使用 let

1
2
let val = "value"
let val = "value1"

使用变量

1
echo val

常量

定义

使用 const

1
const val = "value"

变量源

  • 环境变量
  • 选项变量
  • 寄存器变量

环境变量

vim 可以访问==终端的环境变量==

语法

1
${Enviroment_variable}

示例

1
echo $SHELL

注意:不要使用 {},错误的用法: echo ${SHELL}

选项变量

这些变量也就是 .vimrc 中使用 set 设置的那些变量

访问

  • 方法1: 使用 &`

    1
    echo &background
  • 方法2: set background?

寄存器变量

访问

使用 @

更新寄存器的值

使用 let

示例

  • 访问寄存器 a

    1
    echo @a
  • 更新寄存器 a 的值

    1
    let @a .= " donut"

变量范围

变量范围 说明
g: Global variable
{nothing} Global variable
b: Buffer-local variable
w: Window-local variable
t: Tab-local variable
s: Sourced Vimscript variable
l: Function local variable
a: Function formal parameter variable
v: Built-in Vim variable

全局变量

范围

可以在任何地方使用

定义

  • let val = "value"
  • let g:val = "value"

使用

  • echo val
  • echo g:val

缓冲区变量

定义

添加前缀 b:

1
const b:donut = "chocoldate donut"

说明

定义的每个缓冲区变量仅在定义时所在的缓冲区有效,也就是说缓冲区变量的作用域是定义所在缓冲区

特殊缓冲区值

b:changedtick: 跟踪了在当前缓冲区所有的改变

窗口变量

定义

添加前缀 w:

1
let w:donut = "donut"

说明

类似于缓冲区变量,变量的作用域为定义的窗口

标签变量

定义

添加前缀 t:

1
let t:done

说明

类似于窗口变量和缓冲区变量

脚本变量

定义

使用前缀 s:

说明

仅在定义变量的脚本内部才可以访问到

Function Local and Function Formal Parameter Variable

定义

  • 函数局部变量:l:
  • 函数形参变量: a:

内置变量

说明

前缀为 v:

注意

无法定义这些变量

常见

变量 说明
v:version vim 版本
v:key contains the current item value when iterating through a dictionary.
v:val contains the current item value when running a map() or filter() operation.
v:true, v:false, v:null, and v:none special data types.

注意:查看其他的内置变量 :h vim-variable or :h v:.

第28章 函数

函数语法

1
2
3
function {FunctionNmae}()
{do-something}
endfunction

注意:

  • ==函数名必须以大写字母开头==
  • 函数名不能以数字开头

函数名小写字母开始

使用脚本变量前缀 s:,如:function s:tasty()

同名函数

不允许有重名函数

覆写函数

function 后面使用 !

1
2
3
function! Tasty()
echo "Tasty"
endfunction

覆写之后上面的函数就会没有。==覆写不是重载==哦

查看所有可用函数

使用 :function

查看某一个函数内容

:function Tasty

模式搜索函数

:function /pattern

查看函数位置

使用 :verbose:function 命令

  • 某一函数

    1
    :verbose function <FunctionName>
  • 模式匹配函数

    1
    :verbose function /<pattern>

删除函数

删除一个已有函数: :delfunction {FunctionName}

例如: :delfunction Tasty

函数返回值

  • 默认返回 0
  • return: 等价于返回0

函数参数

用法

使用 a:

示例

如函数 Tasty() 使用参数 food

1
2
3
function! Tasty(food)
return "Tasty" . a:food
endfunction

局部变量

用法

使用 l:

常见问题

问题 1

若在函数内部定义一个变量,那么这个变量默认为局部变量

1
2
3
4
function! Tasty()
let location = "tasty"
return location . " in Tasty"
endfunction

与下面代码等价,但是==没有下面的代码好==

1
2
3
4
function! Tasty()
let l:location = "tasty"
return l:location . " in Tasty"
endfunction

问题 2

1
2
3
4
5
6
7
function! Calories()
let count = "count"
return "I do not " . count . " my calories"
endfunction

echo Calories()
" throws an error

变量会与特殊变量同名,导致错误

错误根源:let count = "count"

调用函数

使用

纯粹调用函数

使用 call 调用函数

  • 命令行

    1
    :call Tasty()
  • 脚本

    1
    call Tasty()

You cannot call a command-line command with another command-line command.

你不能使用命令行命令调用命令行命令

使用函数返回值
使用 call("<FunctionName>", [<参数列表>])

[] 不可以省略

区别

  • :call 是命令行命令
  • call(): 函数
    • 第一个参数接收函数名
    • 第二个参数接收形参列表

默认参数

使用 =

示例

1
2
3
4
5
6
7
8
9
function! Breakfast(meal, beverage = "Milk")
return "I had " . a:meal . " and " . a:beverage . " for breakfast"
endfunction

echo Breakfast("Hash Browns")
" returns I had hash browns and milk for breakfast

echo Breakfast("Cereal", "Orange Juice")
" returns I had Cereal and Orange Juice for breakfast

可变参数

使用 ...

示例

1
2
3
function! Buffet(...)
return a:1
endfunction
  • a:0: 共有多少个参数
  • a:1 是第一个参数
  • a:2 是第二个参数
  • a:20 是第20个参数
  • a:000 是所有参数值组成的列表

a:0 示例

1
2
3
4
5
6
7
8
9
function! Buffet(...)
let l:food_counter = 1
let l:foods = ""
while l:food_counter <= a:0
let l:foods .= a:{l:food_counter} . " "
let l:food_counter += 1
endwhile
return l:foods
endfunction

a:000 + for

1
2
3
4
5
6
7
8
9
10
function! Buffet(...)
let l:foods = ""
for food_item in a:000
let l:foods .= food_item . " "
endfor
return l:foods
endfunction

echo Buffet("Noodles", "Sushi", "Ice cream", "Tofu", "Mochi")
" returns Noodles Sushi Ice cream Tofu Mochi

Range

在定义函数的第一行最后加入 range

效果

会给该函数添加两个特定变量:

  • a:firstline: 所选范围的第一行的行号
  • b:lastline: 所选范围的最后一行的行号

与 line(“.”) 区别

  • line("."): 会在所选范围的所有行上,每行会执行一次函数
  • range: 只会在第一行执行一次

示例

1
2
3
function! Breakfast() range
echo line(".")
endfunction
1
:11,20 call Breakfast

字典

没什么用处

也没怎么看懂

函数引用

类似于函数指针,将变量指向一个函数

使用

使用 function()

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function! Breakfast(item)
return "I am having " . a:item . " for breakfast"
endfunction

let Breakfastify = Breakfast
" returns error

let Breakfastify = function("Breakfast")

echo Breakfastify("oatmeal")
" returns "I am having oatmeal for breakfast"

echo Breakfastify("pancake")
" returns "I am having pancake for breakfast"

Lambda

未命名函数

示例

1
2
3
4
5
6
7
let Plus = {x,y -> x + y}
echo Plus(1,2)
" returns 3

let Tasty = { -> 'tasty'}
echo Tasty()
" returns "tasty"

You can call a function from insisde a lambda expression(没看懂)

方法链

用法

使用 ->

->跟在函数名后面,且中间不能添加空格

1
Source->Method1()->Method2()->...->MethodN()

说明

shell 的 ==管道==

Closure

1
2
3
4
5
6
7
8
9
function! Lunch()
let appetizer = "shrimp"

function! SecondLunch()
return appetizer
endfunction

return funcref("SecondLunch")
endfunction

appetizer在Lunch函数中定义,该函数返回SecondLunch函数。注意,SecondLunch使用了appetizer,但在Vimscript中,它没有访问该变量的权限。如果你尝试运行echo Lunch()(), Vim将抛出一个未定义变量错误。

解决

1
2
3
4
5
6
7
8
9
function! Lunch()
let appetizer = "shrimp"

function! SecondLunch() closure
return appetizer
endfunction

return funcref("SecondLunch")
endfunction

写插件

第13章 全局命令

到目前为止,您已经了解了如何使用点命令(.)重复上一次更改,如何使用宏(q)重复动作以及将文本存储在寄存器中(")。

在本章中,您将学习如何在全局命令中重复命令行命令。

全局命令概述

Vim的全局命令用于同时在多行上运行命令行命令。

顺便说一句,您之前可能已经听说过 “Ex命令” 一词。在本书中,我将它们称为==命令行命令==,但Ex命令和命令行命令是相同的。它们是以冒号(:)开头的命令。在上一章中,您了解了替代命令。这是一个Ex命令的示例。它们之所以称为Ex,是因为它们最初来自Ex文本编辑器。在本书中,我将继续将它们称为命令行命令。有关Ex命令的完整列表,请查看:h ex-cmd-index

全局命令具有以下语法:

1
:g/pattern/command

pattern匹配包含该模式串的所有行,类似于替代命令中的模式串。command可以是任何命令行命令。全局命令通过对与pattern匹配的每一行执行command来工作。

如果您具有以下表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

要删除所有包含”console”的行,可以运行:

1
:g/console/d

结果:

1
2
3
4
5
const one = 1;

const two = 2;

const three = 3;

全局命令在与”console”模式串匹配的所有行上执行删除命令(d)。

运行g命令时,Vim对文件进行两次扫描。在第一次运行时,它将扫描每行并标记与/console/模式传教匹配的行。一旦所有匹配的行都被标记,它将进行第二次运行,并在标记的行上执行d命令。

如果要删除所有包含”const”的行,请运行:

1
:g/const/d

结果:

1
2
3
4
5
console.log("one: ", one);

console.log("two: ", two);

console.log("three: ", three);

逆向匹配

要在不匹配的行上运行全局命令,可以运行:

1
:g!/{pattern}/{command}

或者

1
:v/{pattern}/{command}

如果运行:v/console/d,它将删除 包含”console”的所有行。

模式串

全局命令使用与替代命令相同的模式串系统,因此本节将作为更新。随意跳到下一部分或继续阅读!

如果您具有以下表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

要删除包含”one”或”two”的行,请运行:

1
:g/one\|two/d

要删除包含任何一位数字的行,请运行以下任一命令:

1
:g/[0-9]/d

或者

1
:g/\d/d

如果您有表达式:

1
2
3
const oneMillion = 1000000;
const oneThousand = 1000;
const one = 1;

要匹配包含三到六个零的行,请运行:

1
:g/0\{3,6\}/d

传递范围参数

您可以在g命令之前传递一个范围。您可以通过以下几种方法来做到这一点:

  • :1,5g/console/d 删除第1行和第5行之间匹配字符串”console”的行。
  • :,5g/console/d 如果逗号前没有地址,则从当前行开始。它在当前行和第5行之间寻找字符串”console”并将该行删除。
  • :3,g/console/d 如果逗号后没有地址,则在当前行结束。它在第3行和当前行之间寻找字符串”console”并将该行删除。
  • :3g/console/d 如果只传递一个地址而不带逗号,则仅在第3行执行命令。在第3行查找,如果包含字符串”console”,则将其删除。

除了数字,您还可以将这些符号用作范围:

  • . 表示当前行。范围.,3表示当前行和第3行之间。
  • $ 表示文件的最后一行。 3,$范围表示在第3行和最后一行之间。
  • +n 表示当前行之后的n行。您可以将其与.结合使用,也可以不结合使用。 3,+13,.+1表示在第3行和当前行之后的行之间。

如果您不给它任何范围,默认情况下它将影响整个文件。这实际上不是常态。如果您不传递任何范围,Vim的大多数命令行命令仅在当前行上运行(两个值得注意的例外是:这里介绍的全局命令(:g)和save(:w)命令)。

普通模式命令

您可以将全局命令和:normal命令行命令一起运行。

如果您有以下文字:

1
2
3
4
5
6
7
8
9
const one = 1
console.log("one: ", one)

const two = 2
console.log("two: ", two)

const three = 3
console.log("three: ", three)

要添加”;”运行到每一行的末尾:

1
:g/./normal A;

让我们分解一下:

  • :g 是全局命令。
  • /./ 是“非空行”的模式。它匹配至少包含1个字符的行。因此将与包含“const”和“console”的行匹配。它不匹配空行。
  • normal A; 运行:normal命令行命令。 A; 是普通模式命令,用于在该行的末尾插入”;”。

执行宏

您也可以使用全局命令执行宏。宏只是普通模式下的操作,因此可以使用:normal来执行宏。如果您有以下表达式:

1
2
3
4
5
6
7
8
const one = 1
console.log("one: ", one);

const two = 2
console.log("two: ", two);

const three = 3
console.log("three: ", three);

请注意,带有”const”的行没有分号。让我们创建一个宏,以在寄存器”a”的这些行的末尾添加逗号:

1
qa0A;<esc>q

如果您需要复习,请查看有关宏的章节。现在运行:

1
:g/const/normal @a

现在,所有带有”const”的行的末尾将带有”;”。

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

如果您一步一步按照示例做,您将会在第一行末尾看到两个分号。为避免这种情况,使用全局命令时,给一个范围参数,从第2行到最后一行, :2,$g/const/normal @a

递归全局命令

全局命令本身是命令行命令的一种,因此您可以从技术上在全局命令中运行全局命令。

给定表达式:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

如果您运行:

1
:g/console/g/two/d

首先,g将查找包含模式”console”的行,并找到3个匹配项。然后,第二个”g”将从那三个匹配项中查找包含模式”two”的行。最后,它将删除该匹配项。

您也可以将gv结合使用以找到正负模式。例如:

1
:g/console/v/two/d

与前面的命令不同,它将查找 包含”two”的行。

更改定界符

您可以像替代命令一样更改全局命令的定界符。规则是相同的:您可以使用任何单字节字符,但字母,数字,", |, 和 \除外。

要删除包含”console”的行:

1
:g@console@d

如果在全局命令中使用替代命令,则可以有两个不同的定界符:

1
g@one@s+const+let+g

此处,全局命令将查找包含”one”的所有行。 替换命令将从这些匹配项中将字符串”const”替换为”let”。

默认命令

如果在全局命令中未指定任何命令行命令,会发生什么?

全局命令将使用打印(:p)命令来打印当前行的文本。如果您运行:

1
:g/console

它将在屏幕底部打印所有包含”console”的行。

顺便说一下,这是一个有趣的事实。因为全局命令使用的默认命令是p,所以这使g语法为:

1
:g/re/p
  • g = 全局命令
  • re = 正则表达式模式
  • p = 打印命令

这三个元素连起来拼写为 “grep”,与命令行中的grep 相同。但这 是巧合。 g/re/p命令最初来自Ed编辑器(一个行文本编辑器)。 grep命令的名称来自Ed。

您的计算机可能仍具有Ed编辑器。从终端运行ed(提示:要退出,请键入q)。

反转整个缓冲区

要翻转整个文件,请运行:

1
:g/^/m 0 

^表示行的开始。使用^匹配所有行,包括空行。

如果只需要反转几行,请将其传递一个范围。要将第5行到第10行之间的行反转,请运行:

1
:5,10g/^/m 0

要了解有关move命令的更多信息,请查看:h :move

汇总所有待办事项

当我编码时,有时我会想到一个随机的绝妙主意。不想失去专注,我通常将它们写在我正在编辑的文件中,例如:

1
2
3
4
5
6
7
8
9
10
11
const one = 1;
console.log("one: ", one);
// TODO: 喂小狗

const two = 2;
// TODO:自动喂小狗
console.log("two: ", two);

const three = 3;
console.log("three: ", three);
// TODO:创建一家销售自动小狗喂食器的初创公司

跟踪所有已创建的TODO可能很困难。 Vim有一个:t(copy)方法来将所有匹配项复制到一个地址。要了解有关复制方法的更多信息,请查看:h :copy

要将所有TODO复制到文件末尾以便于自省,请运行:

1
:g/TODO/t $

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const one = 1;
console.log("one: ", one);
// TODO:喂小狗

const two = 2;
// TODO:自动喂小狗
console.log("two: ", two);

const three = 3;
console.log("three: ", three);
// TODO:创建一家销售自动小狗喂食器的初创公司

// TODO:喂小狗
// TODO:自动喂小狗
// TODO:创建一家销售自动小狗喂食器的初创公司

现在,我可以查看我创建的所有TODO,另外找个时间来完成它们,或将它们委托给其他人,然后继续执行下一个任务。

如果不想复制,而是将所有的 TODO 移动到末尾,可以使用移动命令 m

1
:g/TODO/m $

结果:

1
2
3
4
5
6
7
8
9
10
11
12
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

// TODO:喂小狗
// TODO:自动喂小狗
// TODO:创建一家销售自动小狗喂食器的初创公司

黑洞删除

回想一下寄存器那一章,已删除的文本存储在编号寄存器中(允许它们足够大)。每当运行:g/console/d时,Vim都会将删除的行存储在编号寄存器中。如果删除多行,所有编号的寄存器将很快被填满。为了避免这种情况,您可以使用黑洞寄存器("_ 将删除的行存储到寄存器中。

1
:g/console/d _

通过在d之后传递_,Vim不会将删除的行保存到任何寄存器中。

将多条空行减少为一条空行

如果您的文件带有多个空行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
const one = 1;
console.log("one: ", one);


const two = 2;
console.log("two: ", two);





const three = 3;
console.log("three: ", three);

您可以快速将多个空行减少为一条空行。运行:

1
:g/^$/,/./-1j

结果:

1
2
3
4
5
6
7
8
const one = 1;
console.log("one: ", one);

const two = 2;
console.log("two: ", two);

const three = 3;
console.log("three: ", three);

一般情况下全局命令遵循下列格式::g/pattern/command。但是,您也可以使用下面的格式::g/pattern1/,/pattern2/command。用这种格式,Vim将会使command作用在pattern1pattern2上。

记住上面说的格式,让我们根据:g/pattern1/,/pattern2/command这个格式分解一下命令:g/^$/,/./-1j

  • /pattern1/ 就是 /^$/ 。它表示一个空行(一个没有任何字符的行)。
  • /pattern2/ 就是 /./(用-1作为行修正)。/./表示一个非空行(一个含有至少1个字符的行)。这里的 -1 意思是向上偏移1行。
  • command 就是 j,一个联接命令(:j)。在这个示例中,该全局命令联接所有给定的行。

顺便说一句,如果您想要将多个空行全部删去,运行下面的命令:

1
:g/^$/,/./j

或者:

1
:g/^$/-j

您的文本将会减少为:

1
2
3
4
5
6
const one = 1;
console.log("one: ", one);
const two = 2;
console.log("two: ", two);
const three = 3;
console.log("three: ", three);

(译者补充:j连接命令的格式是::[range]j。比如::1,5j将连接第1至5行。在前面的命令中:g/pattern1/,/pattern2/-1j/pattern1//pattern2都是j命令的范围参数,表示连接空行至非空行上方一行,这样就会保留1个空行。在早前的英文版本中有关于j命令的介绍,不知为何在后面的更新中,原作者删除了关于j命令的介绍)

高级排序

Vim有一个:sort命令来对一个范围内的行进行排序。例如:

1
2
3
4
5
d
b
a
e
c

您可以通过运行:sort对它们进行排序。如果给它一个范围,它将只对该范围内的行进行排序。例如,:3,5sort仅在第三和第五行之间排序。

如果您具有以下表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arrayB = [
"i",
"g",
"h",
"b",
"f",
"d",
"e",
"c",
"a",
]

const arrayA = [
"h",
"b",
"f",
"d",
"e",
"a",
"c",
]

如果需要排序数组中的元素,而不是数组本身,可以运行以下命令:

1
:g/\[/+1,/\]/-1sort

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const arrayB = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
]

const arrayA = [
"a"
"b",
"c",
"d",
"e",
"f",
"h",
]

这很棒!但是命令看起来很复杂。让我们分解一下。该命令依然遵循 :g/pattern1/,/pattern2/command这个格式。

  • :g 是全局命令
  • /\[/+1 是第一个模式串,它匹配左方括号”[“。+1表示匹配行的下面1行。
  • /\]/-1 是第二个模式串,它匹配右方括号”]”。-1表示匹配行的上面1行。
  • /\[/+1,/\]/-1 表示在”[“和”]”之间的行。
  • sort 是命令行命令:排序。

聪明地学习全局命令

全局命令针对所有匹配的行执行命令行命令。有了它,您只需要运行一次命令,Vim就会为您完成其余的工作。要精通全局命令,需要做两件事:良好的命令行命令词汇表和正则表达式知识。随着您花费更多的时间使用Vim,您自然会学到更多的命令行命令。正则表达式知识需要更多的实际操作。但是一旦您适应了使用正则表达式,您将领先于很多其他人。

这里的一些例子很复杂。不要被吓到。真正花时间了解它们。认真阅读每个模式串,不要放弃。

每当需要在多个位置应用命令时,请暂停并查看是否可以使用g命令。寻找最适合工作的命令,并编写一个模式串以同时定位多个目标。

既然您已经知道全局命令的功能强大,那么让我们学习如何使用外部命令来增加工具库。

第14章 外部命令

在Unix系统内部,您会发现许多小型的,超专业化命令,每个命令只做一件事(而且能很好地完成)。您可以将这些命令链接在一起以共同解决一个复杂的问题。如果可以从Vim内部使用这些命令,那不是很好吗?

答案是肯定的!在本章中,您将学习如何扩展Vim以使其与外部命令无缝协作。

Bang 命令

Vim有一个Bang(!)命令,可以执行三件事:

1.将外部命令的STDOUT读入当前缓冲区。
2.将缓冲区的内容作为STDIN写入外部命令。
3.从Vim内部执行外部命令。

让我们一个个认真看一下。

将外部命令的标准输出STDOUT读入Vim

将外部命令的STDOUT读入当前缓冲区的语法为:

1
:r !{cmd}

:r是Vim的读命令。如果不带!使用它,则可以使用它来获取文件的内容。如果当前目录中有文件file1.txt,运行:

1
:r file1.txt

Vim会将file1.txt的内容放入当前缓冲区。

如果您运行的:r命令后面跟一个!和外部命令,则该命令的输出将插入到当前缓冲区中。要获取ls命令的结果,请运行:

1
:r !ls

它返回类似下列的文本:

1
2
3
file1.txt
file2.txt
file3.txt

您可以从curl命令读取数据:

1
:r !curl -s 'https://jsonplaceholder.typicode.com/todos/1'

r命令也接受一个地址:

1
:10r !cat file1.txt

现在,将在第10行之后插入来自运行cat file.txt的STDOUT。

将缓冲区内容写入外部命令

:w命令除了保存文件,还可以用来将当前缓冲区中的文本作为作为STDIN传递给外部命令。语法为:

1
:w !cmd

如果您具有以下表达式:

1
2
console.log("Hello Vim");
console.log("Vim is awesome");

确保在计算机中安装了node,然后运行:

1
:w !node

Vim将使用node执行Javascript表达式来打印”Hello Vim”和”Vim is awesome”。

当使用:w命令时,Vim使用当前缓冲区中的所有文本,与global命令类似(大多数命令行命令,如果不给它传递范围,则仅对当前行执行该命令)。如果您通过:w来指定地址:

1
:2w !node

“Vim”只使用第二行中的文本到node解释器中。

:w !node:w! node形式上区别很小,但功能上相隔千里。使用:w !node,您是将当前缓冲区中的文本”写入”到外部命令node中。用:w! node,则您将强制保存文件并将其命名为”node”。

执行外部命令

您可以使用bang命令从Vim内部执行外部命令。语法为:

1
:!cmd

要以长格式查看当前目录的内容,请运行:

1
:!ls -ls

要终止在PID 3456上运行的进程,可以运行:

1
:!kill -9 3456

您可以在不离开Vim的情况下运行任何外部命令,因此您可以专注于自己的任务。

过滤文本

如果给!范围,则可用于过滤文本。假设您有:

1
2
hello vim
hello vim

让我们使用tr (translate)命令将当前行大写。运行:

1
:.!tr '[:lower:]' '[:upper:]'

结果:

1
2
HELLO VIM
hello vim

命令分解:

  • .! 在当前行执行filter命令。
  • !tr '[:lower:]' '[:upper:]' 调用tr外部命令将所有小写字符替换为大写字符。

==必须传递范围以运行外部命令作为过滤器。==如果您尝试在没有.的情况下运行上述命令(:!tr '[:lower:]' '[:upper:]'),则会看到错误。

假设您需要使用awk命令删除两行的第二列:

1
:%!awk "{print $1}"

结果:

1
2
hello
hello

命令分解:

  • :%! 在所有行(%)上执行filter命令。
  • awk "{print $1}" 仅打印匹配项的第一列。

您可以使用管道运算符(|)链接多个命令,就像在终端中一样。假设您有一个包含这些美味早餐的文件:

1
2
3
4
name price
chocolate pancake 10
buttermilk pancake 9
blueberry pancake 12

如果您需要根据价格对它们进行排序,并且仅以均匀的间距显示菜单,则可以运行:

1
:%!awk 'NR > 1' | sort -nk 3 | column -t

结果:

1
2
3
buttermilk pancake 9
chocolate pancake 10
blueberry pancake 12

命令分解:

  • :%! 将过滤器应用于所有行(%)。
  • awk 'NR > 1' 仅从第二行开始显示文本。
  • |链接下一个命令。
  • sort -nk 3使用列3(k 3)中的值对数字进行排序(n)。
  • column -t以均匀的间距组织文本。

普通模式命令

在普通模式下,Vim有一个过滤运算符(!)。如果您有以下问候:

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

要大写当前行和下面的行,可以运行:

1
!jtr '[a-z]' '[A-Z]'

命令分解:

  • !j 运行常规命令过滤器运算符(!),目标是当前行及其下方的行。回想一下,因为它是普通模式运算符,所以适用语法规则”动词+名词”。
  • tr '[a-z]' '[A-Z]'将小写字母替换为大写字母。

filter normal命令仅适用于至少一行以上的motion或至少一行以上的文本对象。如果您尝试运行!iwtr'[az]''[AZ]'(在内部单词上执行tr),您会发现它在整个行上都应用了tr命令,而不是光标所在的单词。

聪明地学习外部命令

Vim不是IDE。它是一种轻量级的模式编辑器,通过设计可以高度扩展。由于这种可扩展性,您可以轻松访问系统中的任何外部命令。这样,Vim离成为IDE仅一步之遥。有人说Unix系统是有史以来的第一个IDE。

Bang 命令的有用程度与您知道多少个外部命令相关。如果您的外部命令知识有限,请不要担心。我还有很多东西要学。以此作为持续学习的动力。每当您需要过滤文本时,请查看是否存在可以解决问题的外部命令。不必担心掌握所有的命令。只需学习完成当前任务所需的内容即可。

第15章 命令行模式

在前三章中,您已经学习了如何使用搜索命令(/, ?)、替换命令(:s)、全局命令(:g),以及外部命令(!)。这些都是命令行模式命令的一些例子。

在本章中,您将学习命令行模式的更多技巧。

进入和退出命令行模式

命令行模式本身也是一种模式,就像普通模式、输入模式、可视模式一样。在这种模式中,光标将转到屏幕底部,此时您可以输入不同的命令。

有 4 种进入命令行模式的方式:

  • 搜索命令 (/, ?)
  • 命令行指令 (:)
  • 外部命令 (!)

您可以从正常模式或可视模式进入命令行模式。

若要离开命令行模式,您可以使用 <esc>Ctrl-cCtrl-[

有时其他资料可能会将”命令行指令”称为”Ex 命令”,将”外部命令”称为”过滤命令”或者”叹号运算符”。

重复上一个命令

您可以用 @: 来重复上一个命令行指令或外部命令。

如果您刚运行 :s/foo/bar/g,执行 @: 将重复该替换。如果您刚运行 :.!tr '[a-z]' '[A-Z]',执行 @: 将重复上一次外部命令转换过滤。

命令行模式快捷键

在命令行模式中,您可以使用 LeftRight 方向键,来左右移动一个字符。

如果需要移动一个单词,使用 Shift-LeftShift-Right (在某些操作系统中,您需要使用 Ctrl 而不是 Shift)。

使用 Ctrl-b移动到该行的开始,使用 Ctrl-e移动到该行的结束。

和输入模式类似,在命令行模式中,有三种方法可以删除字符:

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

最后,如果您想像编辑文本文件一样来编辑命令,可以使用 Ctrl-f

这样还可以查看过往的命令,并在这种”命令行编辑的普通模式”中编辑它们,同时还能按下 Enter 来运行它们。

寄存器和自动补全

当处于命令行模式时,您可以像在插入模式中一样使用 Ctrl-r 从Vim寄存器中插入文本。如果您在寄存器 a 中存储了字符串 “foo” ,您可以执行 Ctrl-r a 从寄存器a中插入该文本。任何在插入模式中您可以从寄存器中获取的内容,在命令行模式中您也可以获取。

另外,您也可以按 Ctrl-r Ctrl-w 获取当前光标下的单词(按 Ctrl-r Ctrl-A 获取当前光标下的词组)。还可以按 Ctrl-r Ctlr-l 获取当前光标所在行。按 Ctrl-r Ctrl-f 获取光标下的文件名。

您也可以对已存在的命令使用自动补全。要自动补全 echo 命令,当处于命令行模式时,首先输入 “ec”,接着按下 <Tab>,此时您应该能在左下角看到一些 “ec” 开头的 Vim 命令(例如:echo echoerr echohl echomsg econ)。按下 <Tab>Ctrl-n 可以跳到下一个选项。按下 <Shift-Tab>Ctrl-p 可以回到上一个选项。

一些命令行指令接受文件名作为参数。edit 就是一个例子,这时候您也可以使用自动补全。当输入 :e 后(不要忘记空格了),按下 <Tab>,Vim 将列出所有相关的文件名,这样您就可以进行选择而不必完整的输入它们。

历史记录窗口

您可以查看命令行指令和搜索项的历史记录(要确保在运行 vim --version 时,Vim 的编译选项中含有+cmdline_hist)。

运行 :his : 来查看命令行指令的历史记录:

1
2
3
4
##  cmd History
2 e file1.txt
3 g/foo/d
4 s/foo/bar/g

Vim 列出了您运行的所有 : 命令。默认情况下,Vim 存储最后 50 个命令。运行 :set history=100 可以将 Vim 记住的条目总数更改为 100。

一个更有用的做法是使用命令行历史记录窗口,按q:将会打开一个可搜索、可编辑的历史记录窗口。假设按下q:后您有如下的表达式:

1
2
3
51  s/verylongsubstitutionpattern/pancake/g
52 his :
53 wq

如果您当前任务是执行 s/verylongsubstitutionpattern/donut/g(”pancake”换成了”donut”),为什么不复用 s/verylongsubstitutionpattern/pancake/g 呢?毕竟,两条命令唯一不同的是替换的单词,”donut” vs “pancake” ,所有其他的内容都是相同的。

当您运行 q:后,在历史记录中找到 s/verylongsubstitutionpattern/pancake/g(在这个环境中,您可以使用Vim导航),然后直接编辑它! 在历史记录窗口中将 “pancake” 改为 “donut” ,然后按 <Enter。Vim立刻执行 s/verylongsubstitutionpattern/donut/g 命令,超级方便!

类似地,运行 :his /:his ? 可以查看搜索记录。要想打开您可以直接搜索和编辑的搜索历史记录窗口,您可以运行 q/q?

要退出这个窗口,按 Ctrl-c, Ctrl-w c, 或输入 :quit

更多命令行指令

Vim有几百个内置指令,要查看Vim的所有指令,执行 :h ex-cmd-index:h :index

聪明地学习命令行模式

对比其他三种模式,命令行模式就像是文本编辑中的瑞士军刀。寥举几例,您可以编辑文本、修改文件和执行命令。本章是命令行模式的零碎知识的集合。同时,Vim 模式的介绍也走向尾声。现在,您已经知道如何使用普通、输入、可视以及命令行模式,您可以比以往更快地使用 Vim 来编辑文本了。

是时候离开 Vim 模式,来了解如何使用 Vim 标记进行更快的导航了。

第16章 标签

快速转到任意定义处,是文本编辑中一个非常有用的特性。在本章中,您将学习如何使用 Vim 标签来做到这一点。

标签概述

假设有人给了您一个新的代码库:

1
2
one = One.new
one.donut

Onedonut?呃,对于当时编写代码的开发者而言,这些代码的含义可能显而易见。问题是当时的开发者已经不在了,现在要由您来理解这些费解的代码。而跟随有Onedonut定义的源代码,是帮助您理解的一个有效方法。

您可以使用fzfgrep来搜索它们,但这种情况下,但使用标签将更快。

把标签想象成地址簿:

1
2
3
Name    Address
Iggy1 1234 Cool St, 11111
Iggy2 9876 Awesome Ave, 2222

当然,标签可不是存储着“姓名-地址”对,而是“定义-地址”对。

假设您在一个目录中有两个 Ruby 文件:

1
2
3
4
5
6
7
8
9
10
## one.rb
class One
def initialize
puts "Initialized"
end

def donut
puts "Bar"
end
end

以及

1
2
3
4
5
## two.rb
require './one'

one = One.new
one.donut

在普通模式下,您可以使用Ctrl-]跳转到定义。在two.rb中,转到one.donut所在行,将光标移到donut处,按下Ctrl-]

哦豁,Vim 找不到标签文件,您需要先生成它。

标签生成器

现代 Vim 不自带标签生成器,您需要额外下载它。有几个选项可供选择:

  • ctags = 仅用于 C,基本随处可见。
  • exuberant ctags = 最流行的标签生成器之一,支持许多语言。
  • universal ctags = 和 exuberant ctags 类似,但比它更新。
  • etags = 用于 Emacs,嗯……
  • JTags = Java
  • ptags.py = Python
  • ptags = Perl
  • gnatxref = Ada

如果您查看 Vim 在线教程,您会发现许多都会推荐 exuberant ctags,它支持 41 种编程语言,我用过它,挺不错的。但自2009年以来一直没有维护,因此 Universal ctags 更好些,它和 exuberant ctags 相似,并仍在维护。

我不打算详细介绍如何安装 Universal ctags,您可以在 universal ctags 仓库了解更多说明。

假设您已经安装好了ctags,接下来,生成一个基本的标签文件。运行:

1
ctags -R .

R 选项告诉 ctags 从当前位置 (.) 递归扫描文件。稍后,您应该在当前文件夹看到一个tags 文件,里面您将看到类似这样的内容:

1
2
3
4
5
6
7
8
9
10
11
12
!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
!_TAG_OUTPUT_FILESEP slash /slash or backslash/
!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/
!_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/
!_TAG_PROGRAM_AUTHOR Universal Ctags Team //
!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/
!_TAG_PROGRAM_URL <https://ctags.io/> /official site/
!_TAG_PROGRAM_VERSION 0.0.0 /b43eb39/
One one.rb /^class One$/;" c
donut one.rb /^ def donut$/;" f class:One
initialize one.rb /^ def initialize$/;" f class:One

根据 Vim 设置和 ctag 生成器的不同,您的tags 文件可能会有些不同。一个标签文件由两部分组成:标签元数据和标签列表。那些标签元数据 (!TAG_FILE...) 通常由 ctags 生成器控制。这里我不打算介绍它们,您可以随意查阅文档。标签列表是一个由所有定义组成的列表,由ctags建立索引。

现在回到 two.rb,将光标移至 donut,再输入Ctrl-],Vim 将带您转到 one.rb 文件里def donut 所在的行上。成功啦!但 Vim 怎么做到的呢?

解剖标签文件

来看看donut 标签项:

1
donut	one.rb	/^  def donut$/;"	f	class:One

上面的标签项由四个部分组成:一个tagname、一个tagfile、一个tagaddress,以及标签选项。

  • donuttagname。当光标在 “donut” 时,Vim 搜索标签文件里含有 “donut” 字符串的一行。
  • one.rbtagfile。Vim 会搜寻 one.rb 文件。
  • /^ def donut$/tagaddress/.../ 是模式指示器。^ 代表一行中第一个元素,后面跟着两个空格,然后是def donut字符串,最后 $ 代表一行中最后一个元素。
  • f class:One 是标签选项,它告诉 Vim,donut 是一种函数 (f),并且是 One 类的一部分。

再看看另一个标签项:

1
One	one.rb	/^class One$/;"	c

这一行和 donut也是一样的:

  • Onetagname。注意,对于标签,第一次扫描区分大小写。如果列表中有 Oneone, Vim 会优先考虑 One 而不是 one
  • one.rbtagfile。Vim 会搜寻 one.rb 文件。
  • /^class One$/tagaddress 。Vim 会查找以 class 开头 (^) 、以 One 结尾 ($) 的行。
  • c 是可用标签选项之一。由于 One 是一个 ruby 类而不是过程,因此被标签为 c

标签文件的内容可能不尽相同,根据您使用的标签生成器而定。但至少,标签文件必须具有以下格式之一:

1
2
1.  {tagname} {TAB} {tagfile} {TAB} {tagaddress}
2. {tagname} {TAB} {tagfile} {TAB} {tagaddress} {term} {field} ..

标签文件

您知道,在运行 ctags -R . 后,一个新 tags 文件会被创建。但是,Vim 是如何知道在哪儿查找标签文件的呢?

如果运行 :set tags?,您可能会看见 tags=./tags,tags(根据您的 Vim 设置,内容可能有所不同)。对于 ./tags,Vim 会在当前文件所在路径查找所有标签;对于 tags,Vim 会在当前目录(您的项目根路径)中查找。

此外,对于 ./tags,Vim 会在当前文件所在路径内查找一个标签文件,无论它被嵌套得有多深。接下来,Vim 会在当前目录(项目根路径)查找。Vim 在找到第一个匹配项后会停止搜索。

如果您的 'tags' 文件是 tags=./tags,tags,/user/iggy/mytags/tags,那么 Vim 在搜索完 ./tagstags 目录后,还会在 /user/iggy/mytags 目录内查找。所以您可以分开存放标签文件,不必将它们置于项目文件夹中。

要添加标签文件位置,只需要运行:

1
:set tags+=path/to/my/tags/file

为大型项目生成标签:

如果您尝试在大型项目中运行 ctag,则可能需要很长时间,因为 Vim 也会查看每个嵌套目录。如果您是 Javascript 开发者,您会知道 node_modules 非常大。假设您有五个子项目,每个都包含自己的 node_modules 目录。一旦运行 ctags -R .,ctags 将尝试扫描这5个 node_modules。但您可能不需要为 node_modules 运行 ctag。

如果要排除 node_modules 后执行 ctags,可以运行:

1
ctags -R --exclude=node_modules .

这次应该只需要不到一秒钟的时间。另外,您还可以多次使用 exclude 选项:

1
ctags -R --exclude=.git --exclude=vendor --exclude=node_modules --exclude=db --exclude=log .

标签导航

仅使用 Ctrl-] 也挺好,但我们还可以多学几个技巧。其实,标签跳转键 Ctrl-] 还有命令行模式::tag my-tag。如果您运行:

1
:tag donut

Vim 就会跳转至 donut 方法,就像在 “donut” 字符串上按 Ctrl-] 一样。您还可以使用 <Tab> 来自动补全参数:

1
:tag d<Tab>

Vim 会列出所有以 “d” 开头的标签。对于上面的命令,结果则是 “donut”。

在实际项目中,您可能会遇到多个同名的方法。我们来更新下这两个文件。先是 one.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## one.rb
class One
def initialize
puts "Initialized"
end

def donut
puts "one donut"
end

def pancake
puts "one pancake"
end
end

然后 two.rb

1
2
3
4
5
6
7
8
9
10
## two.rb
require './one.rb'

def pancake
"Two pancakes"
end

one = One.new
one.donut
puts pancake

由于新添加了一些过程,因此编写完代码后,==不要忘记运行 ctags -R .==。现在,您有了两个 pancake 过程。如果您在 two.rb 内按下 Ctrl-],会发生什么呢?

Vim 会跳转到 two.rb 内的 def pancake,而不是 one.rbdef pancake。这是因为 Vim 认为 two.rb 内部的 pancake 过程比其他的pancake 过程具有更高优先级。

标签优先级

并非所有的标签都有着相同的地位。一些标签有着更高的优先级。如果有重复的标签项,Vim 会检查关键词的优先级。顺序是:

  1. 当前文件中完全匹配的静态标签。
  2. 当前文件中完全匹配的全局标签。
  3. 其他文件中完全匹配的全局标签。
  4. 其他文件中完全匹配的静态标签。
  5. 当前文件中不区分大小写匹配的静态标签。
  6. 当前文件中不区分大小写匹配的全局标签。
  7. 其他文件中区分大小写匹配的全局标签。
  8. 当前文件中不区分大小写匹配的静态标签。

根据优先级列表,Vim 会对在同一个文件上找到的精确匹配项进行优先级排序。这就是为什么 Vim 会选择 two.rb 里的 pancake 过程而不是 one.rb 里的。但是,上述优先级列表有些例外,取决于您的'tagcase''ignorecase''smartcase' 设置。我不打算介绍它们,您可以自行查阅 :h tag-priority

选择性跳转标签

如果可以选择要跳转到哪个标签,而不是始终转到优先级最高的,那就太好了。因为您可能想跳转到 one.rb 里的 pancake 方法,而不是 two.rb 里的。现在您可以使用 :tselect 做到它!运行:

1
:tselect pancake

您可以在屏幕底部看到:

1
2
3
4
5
6
## pri kind tag               file
1 F C f pancake two.rb
def pancake
2 F f pancake one.rb
class:One
def pancake

如果输入2 后再 <Return>,Vim 将跳转到 one.rb 里的pancake 过程。如果输入1 后再 <Return>,Vim 将跳转到 two.rb 里的。

注意pri 列,第一个匹配中该列是F C,第二个匹配中则是F。这就是 Vim 用来确定标签优先级的凭据。F C表示在当前 (C) 文件中完全匹配 (F) 的全局标签。F 表示仅完全匹配 (F) 的全局标签。F C 的优先级永远比 F 高。(译注:FFully-matchedCCurrent file

如果运行:tselect donut,即使只有一个标签可选,Vim 也会提示您选择跳转到哪一个。有没有什么方法可以让 Vim 仅在有多个匹配项时才提示标签列表,而只找到一个标签时就立即跳转呢?

当然!Vim 有一个 :tjump 方法。运行:

1
:tjump donut

Vim 将立即跳转到 one.rb 里的donut 过程,就像在运行 :tag donut 一样。现在试试:

1
:tjump pancake

Vim 将提示您从标签选项中选择一个,就像在运行:tselect pancaketjump 能两全其美。

tjump 在普通模式下有一个快捷键:g Ctrl-]。我个人喜欢g Ctrl-]胜过 Ctrl-]

标签的自动补全

标签能有助于自动补全。回想下第6章“插入模式”,您可以使用 Ctrl-x 子模式来进行各式自动补全。其中有一个我没有提到过的自动补全子模式便是 Ctrl-]。如果您在插入模式中输入Ctrl-x Ctrl-],Vim 将使用标签文件来自动补全。

在插入模式下输入Ctrl-x Ctrl-],您会看到:

1
2
3
4
One
donut
initialize
pancake

标签堆栈

Vim 维持着一个标签堆栈,上面记录着所有您从哪儿来、跳哪儿去的标签列表。使用 :tags 可以看到这个堆栈。如果您首先跳转到pancake,紧接着是donut,此时运行:tags,您将看到:

1
2
3
4
  # TO tag         FROM line  in file/text
1 1 pancake 10 ch16_tags/two.rb
2 1 donut 9 ch16_tags/two.rb
>

注意上面的 > 符号,它代表着您当前在堆栈中的位置。要“弹出”堆栈,从而回到上一次的状态,您可以运行:pop。试试它,再运行:tags看看:

1
2
3
4
  # TO tag         FROM line  in file/text
1 1 pancake 10 puts pancake
> 2 1 donut 9 one.donut

注意现在 > 符号位于 donut 所在的第二行了。再 pop 一次,然后运行:tags

1
2
3
  # TO tag         FROM line  in file/text
> 1 1 pancake 10 puts pancake
2 1 donut 9 one.donut

在普通模式下,您可以按下 Ctrl-t 来达到和 :pop 一样的效果。

自动生成标签

Vim 标签最大的缺点之一是,每当进行重大改变时,您需要重新生成标签文件。如果您将pancake 过程重命名为 waffle,标签文件不知道 pancake 被重命名了,标签列表仍旧存储着 pancake 过程。运行ctags -R . 可以创建更新的标签文件,但这可能会很缓慢。

幸运的是,有几种可以自动生成标签的方法。这一小节不打算介绍一个简单明了的过程,而是提出一些想法,以便您可以扩展它们。

在保存时生成标签

Vim 有一个自动命令 (autocmd) 方法,可以在触发事件时执行任意命令。您可以使用这个方法,以便在每次保存时生成标签。运行:

1
:autocmd BufWritePost *.rb silent !ctags -R .

上面命令的分解如下:

  • autocmd 是 Vim 的自动命令方法,它接受一个事件名称、文件和一个命令。
  • BufWritePost 是保存缓冲区时的一个事件。每次保存文件时将触发一次 BufWritePost 事件。
  • .rb 是 ruby (rb) 文件的一种文件模式。
  • ==silent 是您传递的命令的一部分。如果不输入它,每次触发自动命令时,Vim 都会提示 press ENTER or type command to continue。==
  • !ctags -R . 是要执行的命令。回想一下,!cmd 从 Vim 内部执行终端命令。

现在,每次您保存一个 ruby 文件时,Vim 都会运行ctags -R .

使用插件

有几种插件可以自动生成 ctags:

我使用 ==vim-gutentags==。它的使用方法很简单,而且装上就可以直接使用。

Ctags 以及 Git 钩子

Tim Pope 是一个写了很多非常棒的 Vim 插件的作者,他写了一篇博客,建议使用 git 钩子。可以看一看

聪明地学习标签

只要配置得当,标签是非常有用的。假设在一个新的代码库中,您想要搞清楚 functionFood 干了什么,您可以通过跳转到它的定义来搞懂它们。在那儿可以看到,它又调用了 functionBreakfast。继续跟踪,发现还调用了 functionPancake。现在您明白了,函数调用路径图长这样:

1
functionFood -> functionBreakfast -> functionPancake

进一步可以知道,这段代码和早餐吃煎饼有关。

现在您已经知道如何使用标签,通过 :h tags 可以学习更多有关标签的知识。接下来让我们一起来探索另一个功能:折叠。

第17章 折叠

在阅读文件时,经常会有一些不相关的文本会妨碍您理解。使用 Vim 折叠可以隐藏这些不必要的信息。

本章中,您将学习如何使用不同的折叠方法。

手动折叠

想象您正在折叠一张纸来覆盖一些文本,实际的文本不会消失,它仍在那儿。Vim 折叠的工作方式与此相同,它折叠一段文本,在显示时会隐藏起来,但实际上并不会真的删除它。

折叠操作符是z。(折叠纸张时,它看起来也像字母 “z”)。

假设有如下文本:

1
2
Fold me
Hold me

把光标放在第一行,输入 zfj。Vim 将这两行折叠成一行,同时会看到类似消息:

1
+-- 2 lines: Fold me -----

上面的命令分解如下:

  • zf 是折叠操作符。
  • j 是用于折叠操作符的动作。

您可以使用 zo 打开/展开已折叠文本,使用 zc 关闭/收缩文本。

折叠是一个操作符,所以它遵循语法规则(动词+名词)。您可以在折叠运算符后,加上一个动作(motion)或文本对象。例如,使用 zfip 可以折叠内部段落;使用 zfG 可以折叠至文件末尾;使用 zfa{ 可以折叠 {} 之间的文本。

您可以在可视模式下进行折叠。高亮您想要折叠的区域后 (v, V, 或 Ctrl-v),再输入 zf 即可。

您也可以在命令行模式下,使用 :fold 命令执行一次折叠。若要折叠当前行及紧随其后的第二行,可以运行:

1
:,+1fold

,+1 是要折叠的范围。如果不传递范围参数,默认当前行。+1 是代表下一行的范围指示器。运行 :5,10fold 可以折叠第5至10行。运行 :,$fold 可以折叠当前行至文件末尾。

还有许多其他折叠和展开的命令。我发现他们实在太多,以至于在刚起步时很难记住。最有用的一些命令是:

  • zR 展开所有折叠。
  • zM 收缩所有折叠。
  • za 切换折叠状态。

zRzM 可用于任意行上,但 za 仅能用于已折叠/未折叠的行上。输入 :h fold-commands 可查阅更多有关折叠的指令。

不同的折叠方法

以上部分涵盖了 Vim 手动折叠的内容。实际上,Vim 有六种不同的折叠方法:

  1. 手动折叠
  2. 缩进折叠
  3. 表达式折叠
  4. 语法折叠
  5. 差异折叠
  6. 标志折叠

运行 :set foldmethod? 可查看您当前正在使用哪一种折叠方式。默认情况下,Vim 使用手动方式。

在本章的剩余部分,您将学习其他五种折叠方法。让我们从缩进折叠开始。

缩进折叠

要使用缩进折叠,需要将 'foldmethod' 选项更改为缩进:

1
:set foldmethod=indent

假设有如下文本:

1
2
3
One
Two
Two again

运行 :set foldmethod=indent 后将看到:

1
2
One
+-- 2 lines: Two -----

使用缩进折叠后,Vim 将会查看每行的开头有多少空格,并将它与 'shiftwidth' 选项进行比较,以此来决定该行可折叠性。'shiftwidth' 返回每次缩进所需的空格数。如果运行:

1
:set shiftwidth?

Vim 的默认 'shiftwidth' 值为2。对于上面的文本而言,”Two” 和 “Two again” 的开头都有两个空格。当 Vim 看到了空格数 'shiftwidth'值都为2时,Vim 认为该行的缩进折叠级别为1。

假设这次文本开头只有一个空格:

1
2
3
One
Two
Two again

运行 :set foldmethod=indent 后,Vim 不再折叠已缩进的行了,因为这些行没有足够的空格。1个空格不会被视作一个缩进。然而,当您改变 'shiftwidth' 的值为1后:

1
:set shiftwidth=1

文本现在可以折叠了!现在一个空格将被视为一个缩进。

现在,我们将 'shiftwidth' 以及文本开头的空格数都重新恢复为2后,另外添加一些内容:

1
2
3
4
5
One
Two
Two again
Three
Three again

运行折叠命令 (zM) 后可以看到:

1
2
One
+-- 4 lines: Two -----

展开已折叠的行 (zR),接着移动光标至 “Three”,然后切换文本的折叠状态 (za):

1
2
3
4
One
Two
Two again
+-- 2 lines: Three -----

这是啥?叠中叠?

是的,您可以嵌套折叠。文本 “Two” 和 “Two again” 的折叠级别都为1,文本 “Three” 和 “Three again” 的折叠级别都为2。如果在一段可折叠文本中,具有另一段折叠级别更高的可折叠文本,则可以具有多个折叠层。

表达式折叠

表达式折叠允许您定义要匹配折叠的表达式。定义折叠表达式后,Vim 会计算每行的 'foldexpr' 值。这是必须配置的变量,它要返回适当的值。如果返回 0,则不折叠行。如果它返回 1,则该行的折叠级别为 1。如果它返回 2,则该线的折叠级别为 2。除了整数外还有其他的值,但我不打算介绍它们。如果你好奇,可以查阅:h fold-expr

首先,更改折叠方法:

1
:set foldmethod=expr

假设您有一份早餐食品列表,并且想要折叠所有以 “p” 开头的早餐项:

1
2
3
4
5
6
donut
pancake
pop-tarts
protein bar
salmon
scrambled eggs

其次,更改 foldexpr 为捕获以 “p” 开头的表达式:

1
:set foldexpr=getline(v:lnum)[0]==\\"p\\"

这表达式看起来有点吓人。我们来分解下:

  • :set foldexpr 设置 'foldexpr' 为自定义表达式。
  • getline() 是 Vim 脚本的一个函数,它返回指定行的内容。如运行 :echo getline(5) 可以获取第5行的内容。
  • v:lnum 是 Vim 'foldexpr' 表达式的特殊变量。Vim 在扫描每一行时,都会将行号存储至 v:lnum 变量。在第5行,v:lnum 值为5。在第10行,v:lnum值为10。
  • [0] 处于 getline(v:lnum)[0] 语境时,代表每一行的第一个字符。Vim 在扫描某一行时,getline(v:lnum) 返回该行的内容,而 getline(v:lnum)[0] 则返回这一行的第一个字符。例如,我们早餐食品列表的第一行是 “donut”,则 getline(v:lnum)[0] 返回 “d”;列表的第二行是 “pancake”,则 getline(v:lnum)[0] 返回 “p”。
  • ==\\"p\\" 是等式表达式的后半部分,它检查刚才表达式的计算结果是否等于 “p”。如果是,则返回1,否则返回0。在 Vim 的世界里,1代表真,0代表假。所以,那些以 “p” 开头的行,表达式都会返回1。回想一下本节的开始,如果 'foldexpr' 的值为1,则折叠级别为1。

在运行这个表达式后,您将看到:

1
2
3
4
donut
+-- 3 lines: pancake -----
salmon
scrambled eggs

语法折叠

语法折叠是由Vim的语法高亮决定的。如果您使用了语法高亮插件,比如vim-polyglot,那么装上插件就可以直接使用语法折叠。仅仅需要将foldmethod选项改为 syntax

1
:set foldmethod=syntax

假设您正在编辑一个JavaScript文件,而且您已经装好了 vim-polyglot 插件。如果您有以下文本:

1
2
3
4
5
6
const nums = [
one,
two,
three,
four
]

上述文本将会使用语法折叠折起来。当您为某个特定语言(位于 syntax/ 目录中的语言即是典型例子)定义了语法高亮,您可以添加一个 fold 属性,使它支持折叠。下面是 vim-polyglot 中JavaScript语法文件中的一个片段。注意最后的 fold 关键字。

1
syntax region  jsBracket                      matchgroup=jsBrackets            start=/\[/ end=/\]/ contains=@jsExpression,jsSpreadExpression extend fold

本书不会详细介绍 syntax 功能。如果您感兴趣,可以查阅 :h syntax.txt

差异折叠

Vim 可以对多个文件进行差异比较。

如果您有 file1.txt

1
2
3
4
5
6
7
8
9
10
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome

以及 file2.txt

1
2
3
4
5
6
7
8
9
10
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
emacs is ok

运行 vimdiff file1.txt file2.txt

1
2
3
4
5
6
7
8
+-- 3 lines: vim is awesome -----
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
vim is awesome
[vim is awesome] / [emacs is ok]

Vim 会自动折叠一些相同的行。运行 vimdiff 命令时,Vim 会自动使用 foldmethod=diff。此时如果运行 :set foldmethod?,它将返回 diff

标志折叠

要使用标志折叠,请运行:

1
:set foldmethod=marker

假设有如下文本:

1
2
3
4
5
6
Hello

{{{
world
vim
}}}

输入 zM 后会看到:

1
2
3
hello

+-- 4 lines: -----

Vim 将 {{{}}} 视为折叠指示器,并折叠其中的内容。使用标志折叠时,Vim 会寻找由 'foldmarker' 选项定义的特殊标志,并标记折叠区域。要查看 Vim 使用的标志,请运行:

1
:set foldmarker?

默认情况下,Vim 把 {{{}}} 作为指示器。如果您想将指示器更改为其他诸如 “coffee1” 和 “coffee2” 的字符串,可以运行:

1
:set foldmarker=coffee1,coffee2

假设有如下文本:

1
2
3
4
5
6
hello

coffee1
world
vim
coffee2

现在,Vim 将使用 coffee1coffee2 作为新折叠标志。注意,指示器必须是文本字符串,不能是正则表达式。

持久化折叠

当关闭 Vim 会话后,您将失去所有的折叠信息。假设您有 count.txt 文件:

1
2
3
4
5
one
two
three
four
five

手动从第三行开始往下折叠 (:3,$fold):

1
2
3
one
two
+-- 3 lines: three ---

当您退出 Vim 再重新打开 count.txt 后,这些折叠都不见了!

要在折叠后保留它们,可以运行:

1
:mkview

当打开 count.txt 后,运行:

1
:loadview

您的折叠信息都被保留下来了。然而,您需要手动运行 mkviewloadview。我知道,终有一日,我会忘记运行 mkview 就关闭文件了,接着便会丢失所有折叠信息。能不能自动实现这个呢?

当然能!要在关闭 .txt 文件时自动运行 mkview,以及在打开 .txt 文件后自动运行 loadview,将下列内容添加至您的 vimrc:

1
2
autocmd BufWinLeave *.txt mkview
autocmd BufWinEnter *.txt silent loadview

在上一章您已经见过 autocmd 了,它用于在事件触发时执行一条命令。这里的两个事件是:

  • BufWinLeave 从窗口中删除缓冲时。
  • BufWinEnter 在窗口中加载缓冲时。

现在,即使您在 .txt 文件内折叠内容后直接退出 Vim,下次再打开该文件时,您的折叠信息都能自动恢复。

默认情况下,当运行 mkview 时,Vim将折叠信息保存在~/.vim/view (Unix 系统)。您可以查阅 :h 'viewdir' 来了解更多信息。

聪明地学习折叠

当我刚开始使用 Vim 时, 我会跳过学习 Vim 折叠,因为我觉得它不太实用。然而,随着我码龄的增长,我越发觉得折叠功能大有用处。得当地使用折叠功能,文本结构可以更加清晰,犹如一本书籍的目录。

当您学习折叠时,请从手动折叠开始,因为它可以随学随用。然后逐渐学习不同的技巧来使用缩进和标志折叠。最后,学习如何使用语法和表达式折叠。您甚至可以使用后两个来编写您自己的 Vim 插件。

第18章 Git

Vim 和 Git 是两种实现不同功能的伟大工具。Vim 用于文本编辑,Git 用于版本控制。

在本章中,您将学习如何将 Vim 和 Git 集成在一起。

差异比较

在上一章中,您看到了如何运行 vimdiff 命令以显示多个文件之间的差异。

假设您有两个文件,file1.txtfile2.txt

file1.txt 的内容如下:

1
2
3
4
5
6
7
8
pancakes
waffles
apples

milk
apple juice

yogurt

file2.txt 的内容如下:

1
2
3
4
5
6
7
8
pancakes
waffles
oranges

milk
orange juice

yogurt

若要查看两个文件之间的差异,请运行:

1
vimdiff file1.txt file2.txt

或者也可以运行:

1
vim -d file1.txt file2.txt

vimdiff 并排显示两个缓冲区。左边是 file1.txt,右边是 file2.txt。不同的两行(apples 和 oranges)会被高亮显示。

假设您要使第二个缓冲区相应位置变成 apples,而不是 oranges。若想从 file1.txt 传输您当前位置(当前您在 file1.txt)的内容到 file2.txt,首先使用 ]c 跳转到下一处差异(使用 [c 可跳回上一处差异),现在光标应该在 apples 上了。接着运行 :diffput。此时,这两个文件都是 apples 了。

如果您想从另一个缓冲区(orange juice,file2.txt)传输文本来替代当前缓冲区(apple juice,file1.txt),让您的光标仍然位于 file1.txt 的窗口中,首先使用 ]c 跳转至下一处差异,此时光标应该在 apple juice 上。接着运行 :diffget 获取另一个缓冲区的 orange juice 来替代当前缓冲区中的 apple juice。

:diffput 将文本从当前缓冲区 输出 到另一个缓冲区。:diffget 从另一个缓冲区 获取 文本到当前缓冲区。

如果有多个缓冲区,可以运行 :diffput fileN.txt:diffget fileN.txt 来指定目标缓冲区 fileN。

使用 Vim 作为合并工具

“我非常喜欢解决合并冲突。” ——佚名

我不知道有谁喜欢解决合并冲突,但总之,合并冲突是无法避免的。在本节中,您将学习如何利用 Vim 作为解决合并冲突的工具。

首先,运行下列命令来将默认合并工具更改为 vimdiff

1
2
3
git config merge.tool vimdiff
git config merge.conflictstyle diff3
git config mergetool.prompt false

或者您也可以直接修改 ~/.gitconfig(默认情况下,它应该处于根目录中,但您的可能在不同的位置)。上面的命令应该会将您的 gitconfig 改成如下设置的样子,如果您还没有运行上面的命令,您也可以手动更改您的 gitconfig。

1
2
3
4
5
6
7
[core]
editor = vim
[merge]
tool = vimdiff
conflictstyle = diff3
[difftool]
prompt = false

让我们创建一个假的合并冲突来测试一下。首先创建一个目录 /food,并初始化 git 仓库:

1
git init

添加 breakfast.txt 文件,内容为:

1
2
3
pancakes
waffles
oranges

添加文件并提交它:

1
2
git add .
git commit -m "Initial breakfast commit"

接着,创建一个新分支 apples:

1
git checkout -b apples

更改 breakfast.txt 文件为:

1
2
3
pancakes
waffles
apples

保存文件,添加并提交更改:

1
2
git add .
git commit -m "Apples not oranges"

真棒!现在 master 分支有 oranges,而 apples 分支有 apples。接着回到 master 分支:

1
git checkout master

breakfast.txt 文件中,您应该能看到原来的文本 oranges。接着将它改成 grapes,因为它是现在的应季水果:

1
2
3
pancakes
waffles
grapes

保存、添加、提交:

1
2
git add .
git commit -m "Grapes not oranges"

嚯!这么多步骤!现在准备要将 apples 分支合并进 master 分支了:

1
git merge apples

您应该会看到如下错误:

1
2
3
Auto-merging breakfast.txt
CONFLICT (content): Merge conflict in breakfast.txt
Automatic merge failed; fix conflicts and then commit the result.

没错,一个冲突!现在一起来用一下新配置的 mergetool 来解决冲突吧!运行:

1
git mergetool

Vim 显示了四个窗口。注意一下顶部三个:

  • LOCAL 包含了 grapes。这是“本地”中的变化,也是您要合并的内容。
  • BASE 包含了 oranges。这是 LOCALREMOTE 的共同祖先,用于比较它们之间的分歧。
  • REMOTE 包含了 apples。这是要被合并的内容。

底部窗口(也即第四个窗口),您能看到:

1
2
3
4
5
6
7
8
9
pancakes
waffles
<<<<<<< HEAD
grapes
||||||| db63958
oranges
=======
apples
>>>>>>> apples

第四个窗口包含了合并冲突文本。有了这步设置,就能更轻松看到哪个环境发生了什么变化。您可以同时查看 LOCALBASEREMOTE 的内容。

您的光标应该在第四个窗口的高亮区域。再运行 :diffget LOCAL,就可以获取来自 LOCAL 的改变(grapes)。同样,运行 :diffget BASE 可以获取来自 BASE 的改变(oranges),而运行 :diffget REMOTE 可以获取来自 REMOTE 的改变(apples)。

在这个例子中,我们试着获取来自 LOCAL 的改变。运行 :diffget LOLOCAL 的简写),第四个窗口变成了 grapes。完成后,就可以保存并退出所有文件(:wqall)了。还不错吧?

稍加留意您会发现,现在多了一个 breakfast.txt.orig 文件。这是 Git 防止事与愿违而创建的备份文件。如果您不希望 Git 在合并期间创建备份文件,可以运行:

1
git config --global mergetool.keepBackup false

在 Vim 中使用 Git

Vim 本身没有集成 Git,要在 Vim 中执行 Git 命令,一种方法是在命令行模式中使用 ! 叹号运算符。

使用 ! 可以运行任何 Git 命令:

1
2
3
4
:!git status
:!git commit
:!git diff
:!git push origin master

您还可以使用 Vim 的特殊字符 % (当前缓冲区) 或 # (其他缓冲区):

1
2
:!git add %         " git add current file
:!git checkout # " git checkout the other file

这里有一个Vim技巧,您可以用来添加不同Vim窗口中的多个文件,运行:

1
windo !git add %

然后提交:

1
:!git commit "添加了Vim窗口中的所有文件,酷"

windo命令是VIm的 “do” 命令其中之一,类似于您前面看到的 argdowindo 将命令执行在每一个窗口中。

插件

这里有很多提供git支持的Vim插件。以下是 Vim 中较流行的 Git 相关插件列表(您读到这篇文章时可能又有更多):

其中最流行的是 vim-fugitive。本章的剩余部分,我将使用此插件来介绍几个 git 工作流。

Vim-Fugitive

vim-fugitive 插件允许您在不离开 Vim 编辑器的情况下运行 git 命令行界面。您会发现,有些命令在 Vim 内部执行时会更好。

开始前,请先使用 Vim 插件管理器(vim-plugvundledein.vim 等)安装 vim-fugitive。

Git Status

当您不带参数地运行 :Git 命令时,vim-fugitive 将显示一个 git 概要窗口,它显示了未跟踪、未暂存和已暂存的文件。在此 “git status” 模式下,您可以做一些操作:

  • Ctrl-n / Ctrl-p 转到下一个 / 上一个文件。
  • - 暂存或取消暂存光标处的文件。
  • s 暂存光标处的文件。
  • u 取消暂存光标处的文件。
  • > / < 内联显示或隐藏光标处文件的差异变化。

查阅 :h fugitive-staging-maps 可获得更多信息。

Git Blame

在当前文件运行 :Git blame 命令,vim-fugitive 可以显示一个拆分的问责窗口。这有助于追踪那些 BUG 是谁写的,接着就可以冲他/她怒吼(开个玩笑)。

"git blame" 模式下您可以做:

  • q 关闭问责窗口。
  • A 调整作者列大小。
  • C 调整提交列大小。
  • D 调整日期/时间列大小。

查阅 :h :Git_blame 可获得更多信息。

Gdiffsplit

当您运行 :Gdiffsplit 命令后,vim-fugitive 会执行 vimdiff,比对索引或工作树中的版本与当前文件最新更改的区别。如果运行 :Gdiffsplit <commit>,vim-fugitive 则会根据 <commit> 中的版本来执行 vimdiff

由于您处于 vimdiff 模式中,因此您可以使用 :diffput:diffget获取输出 差异。

Gwrite 和 Gread

当您在更改文件后运行 :Gwrite 命令,vim-fugitive 将暂存更改,就像运行 git add <current-file> 一样。

当您在更改文件后运行 :Gread 命令,vim-fugitive 会将文件还原至更改前的状态,就像运行 git checkout <current-file> 一样。使用 :Gread 还有一个好处是操作可撤销。如果在运行 :Gread 后您改变主意,想要保留原来的更改,您只需要撤消(u),Vim 将撤回 :Gread 操作。要换作是在命令行中运行 git checkout <current-file>,就完成不了这种操作了。

Gclog

当您运行 :Gclog 命令时,vim-fugitive 将显示提交历史记录,就像运行 git log 命令一样。Vim-fugitive 使用 Vim 的 quickfix 来完成此任务,因此您可以使用 :cnext:cprevious 来遍历下一个或上一个日志信息。您还可以使用 :copen:cclose 打开或关闭日志列表。

"git log" 模式中,您可以做两件事:

  • 查看树。
  • 访问父级(上一个提交)。

您可以像 git log 命令一样,传递参数给 :Gclog 命令。如果您项目的提交历史记录很长,只想看最后三个提交,则可以运行 :Gclog -3。如果需要根据提交日期来筛选记录,可以运行类似 :Gclog --after="January 1" --before="March 14" 的命令。

Vim-Fugitive 的更多功能

以上只是寥寥几个 vim-fugitive 功能的例子,您可以查阅 :h fugitive.txt 来了解更多有关 vim-fugitive 的信息。大多数流行的 git 命令可能都有 vim-fugitive 的优化版本,您只需在文档中查找它们。

如果您处于 vim-fugitive 的“特殊模式”(如 :Git:Git blame 模式)中,按下 g? 可以了解当前有哪些可用的快捷键,Vim-fugitive 将为您所处的模式显示相应的 :help 窗口。棒极了!

聪明地学习 Vim 和 Git

每个人都有不同的 git 工作流,可能 vim-fugitive 非常合适您的工作流(也可能不适合)。总之,我强烈建议您试试上面列出的所有插件。可能还有一些其他的我没有列出来,都可以去试一试。

要让Vim-git的集成工作得更好,一个显而易见的办法就是去深入了解git。Git 本身是一个很庞大的主题,我只向您展示了它其中很小的一部分。好了,接下来谈谈如何使用 Vim 编译您的代码。

第19章 编译

编译是许多编程语言的重要主题。在本章中,您将学习如何在 Vim 中编译。此外,您将看到如何利用好 Vim 的 :make 命令。

从命令行编译

您可以使用叹号运算符(!)进行编译。如果您需要使用 g++ 来编译 .cpp 文件,可以运行:

1
:!g++ hello.cpp -o hello

但要每次手动指定文件名和输出文件名会非常繁琐和容易出错。而 makefile 是条可行之路。

Make命令

Vim 有运行 makefile 的 :make 命令。当您运行它时,Vim 会在当前目录查找 makefile 并执行它。

在当前目录创建一个文件名为 makefile ,然后添加下列内容:

1
2
3
4
5
6
all:
echo "Hello all"
foo:
echo "Hello foo"
list_pls:
ls

在 Vim 中运行:

1
:make

Vim 执行它的方式与从终端运行它的方式相同。:make 命令也接受终端中 make 命令的参数。运行:

1
2
3
4
5
:make foo
" Outputs "Hello foo"

:make list_pls
" Outputs the ls command result

如果命令执行异常,:make 命令将使用 Vim 的 quickfix 来存储这些错误。现在试着运行一个不存在的目标:

1
:make dontexist

您应该会看到该命令执行错误。运行 quickfix 命令 :copen 可以打开 quickfix 窗口来查看该错误:

1
|| make: *** No rule to make target `dontexist'.  Stop.

使用 Make 编译

让我们使用 makefile 来编译一个基本的 .cpp 程序。首先创建一个 hello.cpp 文件:

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello!\n";
return 0;
}

然后,更新 makefile 来编译和运行 .cpp 文件:

1
2
3
4
5
6
all:
echo "build, run"
build:
g++ hello.cpp -o hello
run:
./hello

现在运行:

1
:make build

g++ 将编译 ./hello.cpp 并且生成 ./hello。接着运行:

1
:make run

您应该会看到终端上打印出了 "Hello!"

不同的Make程序

当您运行 :make 时,Vim 实际上会执行 makeprg 选项所设置的任何命令,您可以运行 :set makeprg? 来查看它:

1
makeprg=make

:make 的默认命令是外部的 make 命令。若想修改 :make 命令,使每次运行它时执行 g++ <your-file-name>,请运行:

1
:set makeprg=g++\ %

\ 用于转义 g++ 后的空格。Vim 中 % 符号代表当前文件。因此,g++\ % 命令等于运行 g++ hello.cpp

转到 ./hello.cpp 然后运行 :make,Vim 将编译 hello.cpp 并输出 a.out(因为您没有指定输出)。让我们重构一下,使用去掉扩展名的原始文件名来命名编译后的输出。运行下面的命令(或将它们添加到vimrc):

1
:set makeprg=g++\ %\ -o\ %<

上面的命令分解如下:

  • g++\\ % 如上所述,等同于运行 g++ <your-file>
  • -o 输出选项。
  • %< 在 Vim 中代表了没有扩展名的当前文件名(如 hello.cpp 变成 hello)。

当您在 ./hello.cpp 中运行 :make 时,它将编译为 ./hello。要在 ./hello.cpp 中快速地执行 ./hello,可以运行 :!./%<。同样,它等同于运行 :!./<无后缀的当前文件名>

查阅 :h :compiler:h write-compiler-plugin 可以了解更多信息。

保存时自动编译

有了自动化编译,您可以让生活更加轻松。回想一下,您可以使用 Vim 的 autocmd 来根据某些事件自动执行操作。例如,要自动在每次保存后编译 .cpp 文件,您可以将下面内容添加到vimrc:

1
:autocmd BufWritePost *.cpp make

现在您每次保存 .cpp 文件后,Vim 都将自动执行 make 命令。

切换编译器

Vim 有一个 :compiler 命令可以快速切换编译器。您的 Vim 可能附带了一些预构建的编译配置。要检查您拥有哪些编译器,请运行:

1
:e $VIMRUNTIME/compilers/<tab>

您应该会看到一个不同编程语言的编译器列表。

若要使用 :compiler 命令,假设您有一个 ruby 文件 hello.rb,内容是:

1
puts "Hello ruby"

回想一下,如果运行 :make,Vim 将执行赋值给 makeprg 的任何命令(默认是 make)。如果您运行:

1
:compiler ruby

Vim 执行 $VIMRUNTIME/compiler/ruby.vim 脚本,并将 makeprg 更改为使用 ruby 命令。现在如果您运行 :set makeprg?,它会显示 makeprg=ruby(这取决于您 $VIMRUNTIME/compiler/ruby.vim 里的内容,如果您有其他自定义的 ruby 编译器,您的结果可能会有不同)。:compiler <your-lang> 命令允许您快速切换至其他编译器。如果您的项目使用多种语言,这会非常有用。

您不必使用 :compilermakeprg 来编译程序。您可以运行测试脚本、分析文件、发送信号或任何您想要的内容。

创建自定义编译器

让我们来创建一个简单的 Typescript 编译器。先在您的设备上安装 Typescript(npm install -g typescript),安装完后您将有 tsc 命令。如果您之前没有尝试过 typescript,tsc 将 Typescript 文件编译成 Javascript 文件。假设您有一个 hello.ts 文件:

1
2
const hello = "hello";
console.log(hello);

运行 tsc hello.ts 后,它将被编译成 hello.js。然而,如果您的 hello.ts 文件中有如下内容:

1
2
3
const hello = "hello";
hello = "hello again";
console.log(hello);

这会抛出错误,因为不能更改一个 const 变量。运行 tsc hello.ts 的错误如下:

1
2
3
4
5
6
7
hello.ts:2:1 - error TS2588: Cannot assign to 'person' because it is a constant.

2 person = "hello again";
~~~~~~


Found 1 error.

要创建一个简单的 Typescript 编译器,请在您的 ~/.vim/ 目录中新添加一个 compiler 目录(即 ~/.vim/compiler/),接着创建 typescript.vim 文件(即 ~/.vim/compiler/typescript.vim),并添加如下内容:

1
2
CompilerSet makeprg=tsc
CompilerSet errorformat=%f:\ %m

第一行将 makeprg 设置为运行 tsc 命令。第二行将错误格式设置为显示文件(%f),后跟冒号(:)和转义的空格(\ ),最后是错误消息(%m)。查阅 :h errorformat 可了解更多关于错误格式的信息。

您还可以阅读一些预制的编译器,看看它们是如何实现的。输入 :e $VIMRUNTIME/compiler/<some-language>.vim 查看。

有些插件可能会干扰 Typescript 文件,可以使用 --noplugin 标志以零插件的形式打开hello.ts 文件:

1
vim --noplugin hello.ts

检查 makeprg

1
:set makeprg?

它应该会显示默认的 make 程序。要使用新的 Typescript 编译器,请运行:

1
:compiler typescript

当您运行 :set makeprg? 时,它应该会显示 tsc 了。我们来测试一下:

1
:make %

回想一下,% 代表当前文件。看看您的 Typescript 编译器是否如预期一样工作。运行 :copen 可以查看错误列表。

异步编译器

有时编译可能需要很长时间。在等待编译时,您不会想眼睁睁盯着已冻结的 Vim 的。如果可以异步编译,就可以在编译期间继续使用 Vim 了,岂不美哉?

幸运的是,有插件来运行异步进程。有两个比较好的是:

在这一章中,我将介绍 vim-dispatch,但我强烈建议您尝试上述列表中所有插件。

Vim 和 NeoVim 实际上都支持异步作业,但它们超出了本章的范围。如果您好奇,可以查阅 :h job-channel-overview.txt

插件:Vim-dispatch

Vim-dispatch 有几个命令,最主要的两个是 :Make:Dispatch

异步Make

Vim-dispatch 的 :Make 命令与 Vim 的 :make 相似,但它以异步方式运行。如果您正处于 Javascript 项目中,并且需要运行 npm t,可以将 makeprg 设置为:

1
:set makeprg=npm\\ t

如果运行:

1
:make

Vim 将执行 npm t。但同时,您只能盯着冻结了的屏幕。有了 vim-dispatch,您只需要运行:

1
:Make

Vim 将启用后台进程异步运行 npm t,同时您还能在 Vim 中继续编辑您的文本。棒极了!

异步调度(Dispatch)

:Dispatch 命令的工作方式和 :compiler:! 类似,它可以在Vim中运行任意外部命令。

假设您在 ruby spec 文件中,需要执行测试,可以运行:

1
:Dispatch rspec %

Vim 将对当前文件异步运行 rspec 命令。

自动调度

Vim-dispatch 有一个缓冲区变量b:dispatch,您可以配置它来自动执行特定命令,您可以利用 autocmd和它一起工作。如果在您的 vimrc 中添加如下内容:

1
autocmd BufEnter *_spec.rb let b:dispatch = 'bundle exec rspec %'

现在每当您进入(BufEnter)一个以 _spec.rb 结尾的文件,运行:Dispatch 将自动执行 bundle exec rspec <your-current-ruby-spec-file>

聪明地学习编译

在本章中,您了解到可以使用 makecompiler 命令从Vim内部异步运行 任何 进程,以完善您的编程工作流程。Vim 拥有通过其他程序来扩展自身的能力,这使其变得强大。

第20章 视图、会话和 Viminfo

当您做了一段时间的项目后,您可能会发现这个项目逐渐形了成自己的设置、折叠、缓冲区、布局等,就像住了一段时间公寓后,精心装饰了它一样。问题是,关闭 Vim 后,所有的这些更改都会丢失。如果能保留这些更改,等到下次打开 Vim 时,一切恢复如初,岂不美哉?

本章中,您将学习如何使用 视图、会话 和 Viminfo 来保存项目的“快照”。

视图

视图是这三个部分(视图、会话、Viminfo)中的最小子集,它是单个窗口相关设置的集合。如果您长时间在一个窗口上工作,并且想要保留其映射和折叠,您可以使用视图。

我们来创建一个 foo.txt 文件:

1
2
3
4
5
6
7
8
9
10
foo1
foo2
foo3
foo4
foo5
foo6
foo7
foo8
foo9
foo10

在这个文件中,做三次修改:

  1. 在第 1 行,创建一个手动折叠 zf4j(折叠接下来 4 行)。
  2. 更改 number 设置:setlocal nonumber norelativenumber。这会移除窗口左侧的数字指示器。
  3. 创建本地映射,每当按一次 j 时,向下两行::nnoremap <buffer> j jj

您的文件看起来应该像:

1
2
3
4
5
6
+-- 5 lines: foo1 -----
foo6
foo7
foo8
foo9
foo10

配置视图属性

运行:

1
:set viewoptions?

默认情况下会显示(根据您的 vimrc 可能会有所不同):

1
viewoptions=folds,cursor,curdir

我们来配置 viewoptions。要保留的三个属性分别是折叠、映射和本地设置选项。如果您的设置和我的相似,那么您已经有了 folds 选项。运行下列命令使视图记住 localoptions

1
:set viewoptions+=localoptions

查阅 :h viewoptions 可了解 viewoptions 的其他可用选项。现在运行 :set viewoptions?,您将看到:

1
viewoptions=folds,cursor,curdir,localoptions

保存视图

foo.txt 窗口经过适当折叠并设置了 nonumber norelativenumber 选项后,现在我们来保存视图。运行:

1
:mkview

Vim 创建了一个视图文件。

视图文件

您可能会想“Vim 将这个视图文件保存到哪儿了呢?”,运行下列命令就可以看到答案了:

1
:set viewdir?

默认情况下会显示 ~/.vim/view(根据您的操作系统,可能会有不同的路径。查阅 :h viewdir 获得更多信息)。如果您运行的是基于Unix的操作系统,想修改该路径,可以在您的 vimrc 中添加下列内容:

1
set viewdir=$HOME/else/where

加载视图文件

关闭并重新打开 foo.txt,您会看到原来的文本,没有任何改变。这是预期行为。运行下列命令可以加载视图文件:

1
:loadview

现在您将看到:

1
2
3
4
5
6
+-- 5 lines: foo1 -----
foo6
foo7
foo8
foo9
foo10

那些折叠、本地设置以及映射都恢复了。如果您细心还可以发现,光标位于上一次您运行 :mkview 时所处的行上。只要您有 cursor 选项,视图将记住光标位置。

多个视图

Vim 允许您保存 9 个编号的视图(1-9)。

假设您想用 :9,10 fold 来额外折叠最后两行,我们把这存为视图 1。运行:

1
:mkview 1

如果您又想用 :6,7 fold 再折叠一次,并存为不同的视图,运行:

1
:mkview 2

关闭并重新打开 foo.txt 文件,运行下列命令可以加载视图 1:

1
:loadview 1

要加载视图 2,运行:

1
:loadview 2

要加载原始视图,运行:

1
:loadview

自动创建视图

有一件可能会发生的很倒霉的事情是,您花了很长时间在一个大文件中进行折叠,一不小心关闭了窗口,接着丢失了所有折叠信息。您可以在 vimrc 中添加下列内容,使得在关闭缓冲区后 Vim 能自动创建视图,防止此类灾难发生:

1
autocmd BufWinLeave *.txt mkview

另外也能在打开缓冲区后自动加载视图:

1
autocmd BufWinEnter *.txt silent loadview

现在,当您编辑 txt 文件时,不用再担心创建和加载视图了。但也注意,==随着时间的推移,视图文件会不断积累,记得每隔几个月清理一次。==

会话

如果说视图保存了某个窗口的设置,那么会话则保存了所有窗口(包括布局)的信息。

创建新会话

假设您在 foobarbaz 工程中编辑着 3 个文件:

foo.txt 的内容:

1
2
3
4
5
6
7
8
9
10
foo1
foo2
foo3
foo4
foo5
foo6
foo7
foo8
foo9
foo10

bar.txt 的内容:

1
2
3
4
5
6
7
8
9
10
bar1
bar2
bar3
bar4
bar5
bar6
bar7
bar8
bar9
bar10

baz.txt 的内容:

1
2
3
4
5
6
7
8
9
10
baz1
baz2
baz3
baz4
baz5
baz6
baz7
baz8
baz9
baz10

假设您的窗口布局如下所示(适当地使用 splitvsplit 来放置):

要保留这个外观,您需要保存会话。运行:

1
:mksession

与默认存储在 ~/.vim/viewmkview 不同,mksession 在当前目录存储会话文件(Session.vim)。如果好奇,您可以看看文件。

如果您想将会话文件另存他处,可以将参数传递给 mksession

1
:mksession ~/some/where/else.vim

使用 ! 来调用命令可以覆盖一个已存在的会话文件(:mksession! ~/some/where/else.vim)。

加载会话

运行下列命令可以加载会话:

1
:source Session.vim

现在 Vim 看起来就像您离开它时的样子!或者,您也可以从终端加载会话文件:

1
vim -S Session.vim

配置会话属性

您可以配置会话要保存的属性。若要查看当前哪些属性正被保存,请运行:

1
:set sessionoptions?

我的显示:

1
blank,buffers,curdir,folds,help,tabpages,winsize,terminal

如果在保存会话时不想存储 terminal,可以运行下列命令将其从会话选项中删除:

1
:set sessionoptions-=terminal

如果要在保存会话时存储 options,请运行:

1
:set sessionoptions+=options

下面是一些 sessionoptions 可以存储的属性:

  • blank 存储空窗口
  • buffers 存储缓冲区
  • folds 存储折叠
  • globals 存储全局变量(必须以大写字母开头,并且至少包含一个小写字母)
  • options 存储选项和映射
  • resize 存储窗口行列
  • winpos 存储窗口位置
  • winsize 存储窗口大小
  • tabpages 存储选项卡
  • unix 以 Unix 格式存储文件

查阅 :h 'sessionoptions' 来获取完整列表。

会话是保存项目外部属性的好工具。但是,一些内部属性不存储在会话中,如本地标记、寄存器、历史记录等。要保存它们,您需要使用 Viminfo!

Viminfo

如果您留意,在复制一个单词进寄存器 a,再退出并重新打开 Vim 后,您仍然可以看到存储在寄存器中的文本。这就是 Viminfo 的功劳。没有它,在您关闭 Vim 后,Vim 会忘记这些寄存器。

如果您使用 Vim 8 或更高版本,Vim 会默认启用 Viminfo。因此您可能一直在使用 Viminfo,而您对它毫不知情!

您可能会问:Viminfo 存储了什么?与会话有何不同?

要使用 Viminfo,您必须启用了 +viminfo 特性(:version)。Viminfo 存储着:

  • 命令行历史记录。
  • 字符串搜索历史记录。
  • 输入行历史记录。
  • 非空寄存器的内容。
  • 多个文件的标记。
  • 文件标记,它指向文件中的位置。
  • 上次搜索 / 替换模式(用于 “n” 和 “&”)。
  • 缓冲区列表。
  • 全局变量。

通常,会话存储“外部”属性,Viminfo 存储“内部”属性。

每个项目可以有一个会话文件,而 Viminfo 与会话不同,通常每台计算机只使用一个 Viminfo。Viminfo 是项目无关的。

对于 Unix,Viminfo 的默认位置是 $HOME/.viminfo~/.viminfo)。如果您用其他操作系统,Viminfo 位置可能会有所不同。可以查阅 :h viminfo-file-name。每一次您做出的“内部”更改,如将文本复制进一个寄存器,Vim 都会自动更新 Viminfo 文件。

请确保您设置了 nocompatible 选项(set nocompatible),否则您的 Viminfo 将不起作用。

读写 Viminfo

尽管只使用一个 Viminfo 文件,但您还是可以创建多个 Viminfo 文件。使用 :wviminfo 命令(缩写为 :wv)来创建多个 Viminfo 文件。

1
:wv ~/.viminfo_extra

要覆盖现有的 Viminfo 文件,向 wv 命令多添加一个叹号:

1
:wv! ~/.viminfo_extra

Vim 默认情况下会读取 ~/.viminfo 文件。运行 :rviminfo(缩写为 :rv)可以读取不同的 Vimfile 文件:

1
:rv ~/.viminfo_extra

要在终端使用不同的 Viminfo 文件来启动 Vim,请使用 “i” 标志:

1
vim -i viminfo_extra

如果您要将 Vim 用于不同的任务,比如写代码和写作,您可以创建两个 Viminfo,一个针对写作优化,另一个为写代码优化。

1
2
3
vim -i viminfo_writing

vim -i viminfo_coding

不使用 Viminfo 启动 Vim

要不使用 Viminfo 启动 Vim,可以在终端运行:

1
vim -i NONE

要永不使用 Viminfo,可以在您的 vimrc 文件添加:

1
set viminfo="NONE"

配置 Viminfo 属性

viewoptions 以及 sessionoptions 类似,您可以用 viminfo 选项指定要存储的属性。请运行:

1
:set viminfo?

您会得到:

1
!,'100,<50,s10,h

看起来有点晦涩难懂。命令分解如下:

  • ! 保存以大写字母开头、却不包含小写字母的全局变量。回想一下 g: 代表了一个全局变量。例如,假设您写了赋值语句 let g:FOO = "foo",Viminfo 将存储全局变量 FOO。然而如果您写了 let g:Foo = "foo",Viminfo 将不存储它,因为它包含了小写字母。没有 !,Vim 不会存储这些全局变量。
  • '100 代表标记。在这个例子中,Viminfo 将保存最近 100 个文件的本地标记(a-z)。注意,如果存储的文件过多,Vim 会变得很慢,1000 左右就可以了。
  • <50 告诉 Viminfo 每个寄存器最多保存多少行(这个例子中是 50 行)。如果我复制 100 行文本进寄存器 a("ay99j)后关闭 Vim,下次打开 Vim 并从寄存器 a("ap)粘贴时,Vim 最多只粘贴 50 行;如果不指定最大行号, 所有 行都将被保存;如果指定 0,什么都不保存了。
  • s10 为寄存器设置大小限制(kb)。在这个例子中,任何大于 10kb 的寄存器都会被排除。
  • h 禁用高亮显示(hlsearch 时)。

可以查阅 :h 'viminfo' 来了解其他更多选项。

聪明地使用视图、会话和 Viminfo

Vim 能使用视图、会话和 Viminfo 来保存不同级别的 Vim 环境快照。对于微型项目,可以使用视图;对于大型项目,可以使用会话。您应该花些时间来查阅视图、会话和 Viminfo 提供的所有选项。

为您的编辑风格创建属于您自己的视图、会话和 Viminfo。如果您要换台计算机使用 Vim,只需加载您的设置,立刻就会感到就像在家里的工作环境一样!

第21章 多文件操作

多文件编辑更新是一个值得掌握、非常有用的编辑工具。前面您已经学会了如何使用 cfdo 命令在多个文本中进行更新。本章,您将学到如何在Vim中进行多文件编辑的更多不同方法。

在多个文件中执行命令的几种方法

要在多个文件中执行命令,Vim有8种方法:

  • 参数列表 (argdo)
  • 缓冲区列表 (bufdo)
  • 窗口列表 (windo)
  • tab 列表(tabdo)
  • 快速修复列表 (cdo)
  • 文件方式的快速修复列表 (cfdo)
  • 位置列表 (ldo)
  • 文件方式的位置列表 (lfdo)

实际上,大部分时间您可能只会用到1种或2种(就我个人而言,我使用 cdoargdo比其他的多得多),但了解所有可行方法还是很有用的,这样您就可以选择一个最符合您个人编辑风格的方法。

学习所有8个命令可能听起来让人有点打退堂鼓。但实际上,这些命令工作方式很相似。学习了其中一个后,再学习剩余的将容易的多。它们的运行方式都大体相同:分别创建一个列表(列表中的元素根据命令有所不同),然后向列表传递一个您想执行的命令。

参数列表

参数列表是最基础的列表。它创建一个文件列表。要想为 file1, file2, file3创建文件列表,您可以执行:

1
:args file1 file2 file3

您也可以传递一个通配符(*),所以如果您想为当前目录下所有的 .js 文件创建一个列表,运行:

1
:args *.js

如果您想为当前目录下所有以 “a” 开头的Javascript文件创建列表,运行:

1
:args a*.js

*)通配符匹配当前目录下的1个或多个任意文件名中的字符。但如果您想在某个目录下进行递归搜索怎么办呢?您可以使用双通配符(**)。要得到您当前位置下所有子目录中的Javascript文件,运行:

1
:args **/*.js

您运行了 args 命令后,您的当前buffer将会切换到列表中的第一个文件。运行 :args可以查看您刚才创建的文件列表。当您创建好了您的列表后,您就可以遍历它们了。:first 将让您跳至列表中的第一个文件。:last 将跳到最后一个文件。运行:next可以在列表中一次向前移动一个文件。运行 :prev可以在列表中一次向后移动一个文件。运行:wnext:wprev命令,在向前/向后移动文件的同时还会保存修改。查阅 : arglist 了解更多导航命令。

参数列表在定位某个特定类型的文件或少量文件时特别有用。假如您需要将所有 yml 文件中的donut 更新为 pancake。运行:

1
2
3
:args **/*.yml
:argdo %s/donut/pancake/g | update

注意如果您再次执行 args 命令,它将覆盖先前的列表。比如,如果您先前运行了:

1
:args file1 file2 file3

假设这些文件都是存在的,那么现在您的列表为 file1, file2,以及 file3。然后再运行:

1
:args file4 file5

您的初始列表 file1, file2, file3将被覆盖为 file4, file5。如果您的参数列表中已经有了 file1, file2, file3 ,而您想将 file4, file5 添加到初始列表中,请使用 :arga命令。运行

1
:arga file4 file5

现在您的列表为file1, file2, file3, file4, file5

如果您运行 :arga 时没有给任何参数,Vim会添加当前buffer到参数列表中。例如,如果您的参数列表中已经有了 file1, file2, file3,而您当前buffer是 file5,运行 :arga 将添加 file5 到您的列表中。

在前面的命令(:argdo %s/donut/pancake/g)中您已经看到过了,当您创建好列表后就可以向它传递任意命令行命令。其他的一些示例:

  • 删除参数列表所有文件内包含 “dessert” 的行, 运行 :argdo g/dessert/d.
  • 在参数列表每个文件中执行宏a(假设您已经在a中录好了一个宏),运行 :argdo norm @a.
  • 向参数列表所有文件的第一行插入”hello “+文件名 ,运行 :argdo 0put='hello ' . @%(译者注:在英文版中,原作者给出的命令是 :argdo 0put='hello ' .. @:,貌似这个命令有问题)。

把所有工作完成后,别忘了使用 :update 命令保存(:update只会保存当前buffer,要保存列表所有文件的修改,请用 :argdo update)。

有时候您仅仅需要在参数列表的前n个文件执行某条命令。如果是这种情况,只需要向 argdo 命令传递一个地址就可以了。比如,要在列表的前3个文件执行替换命令,运行::1,3argdo %s/donut/pancake/g

缓冲区列表

因为每次您创建新文件或打开文件时,Vim将它保存在一个buffer中(除非您显式地删除它),所以当您编辑新文件时,缓冲区列表就有组织地被创建了。如果您已经打开了3个文件:file1.rb file2.rb file3.rb,您的缓冲区列表就已经有了3个元素。运行 :buffers(或者:ls、或:files)可以显示缓冲区列表。要想向前或向后遍历缓冲区列表,可以使用 :bnext :bprev。要想跳至列表中第一个或最后一个buffer,可使用 :bfirst:blast

另外,这里有一个和本章内容不相关,但是很酷的缓冲区技巧:如果您的缓冲区有大量的文件,您可以使用 :ball 显示所有缓冲区。:ball 命令默认使用水平分割窗口进行显示,如果想使用垂直分割的窗口显示,运行::vertical ball

回到本章主题。在缓冲区列表中执行某个操作的方法与参数列表操作非常相似。当您创建好缓冲区列表后,您只需要在您想执行的命令前用 :bufdo 代替 :argdo就可以了。例如,如果您想将缓冲区列表内每个文件中所有的 “donut” 替换为 “pancake”并保存修改,可以运行::bufdo %s/donut/pancake/g | update

窗口列表和选项卡(Tab)列表

窗口列表、选项卡列表的操作和参数列表、缓冲区列表同样非常相似。唯一的区别在于它们的内容和语法。

窗口操作作用在每一个打开的窗口上,使用的命令是 :windo。选项卡(Tab)操作作用在每一个打开的选项卡上,使用的命令是 :tabdo。可以查询 :h list-repeat, :h :windo:h :tabdo,了解更多信息。

比如,如果您打开了4个窗口(您可以使用 Ctrl-w v打开一个垂直分割的窗口,也可以使用 Ctrl-w s打开一个水平分割的窗口),然后您运行 :windo 0put = 'hello' . @%,Vim将在所有打开的窗口的第一行输出 “hello”+文件名。

快速修复列表

在前面的章节中(第3章和第19章),我曾提到过快速修复(quickfix)。快速修复有很多作用,很多流行的插件都在使用快速修复提供的功能,因此值得花时间去理解它。

如果您是Vim新手,快速修复对于您可能是个新概念。回想以前您执行代码编译的时候,编译期间您可能遇到过错误,而这些错误都显示在一个特殊的窗口。这就是快速修复(quickfix)的由来。当您编译您的代码的时候,Vim会在快速修复窗口显示错误信息,您可以稍后去解决。许多现代语言已经不再需要进行显式地编译,但快速修复并没有被淘汰。现在,人们使用快速修复来做各种各样的事,比如显示虚拟终端的输入、存储搜索结果等。我们重点研究后者,存储搜索结果。

除编译命令外,某些特定的Vim命令也依赖快速修复接口。其中一种就是搜索命令,其使用过程中大量的使用了快速修复窗口,:vimgrep:grep 都默认使用快速修复。

比如,如果您需要在所有的Javascript文件中递归地搜索 “donut”,您可以运行:

1
:vimgrep /donut/ **/*.js

“donut”的搜索结果存储在快速修复窗口中。要查看快速修复窗口的结果,运行:

1
:copen

要关闭快速修复窗口,运行:

1
:cclose

在快速修复列表中向前或向后遍历,运行:

1
2
:cnext
:cprev

跳至第一个或最后一个匹配的元素,运行:

1
2
:cfirst
:clast

在前面我提到过,有两种快速修复命令:cdocfdo 。它们有什么区别?cdo 在修复列表中的每一个元素上执行命令,而 cfdo 在修复列表中的每一个文件上执行命令。

让我讲清楚一点,假设运行完上面的 vimgrep 命令后,您找到以下结果:

  • 1 result in file1.js
  • 10 results in file2.js

如果您运行 :cfdo %s/donut/pancake/g, 这个命令将会在 file1.jsfile2.js 上分别有效地运行一次%s/donut/pancake/g. 它执行的次数与 匹配结果中文件的数量 相同。因为搜索结果中有2个文件,因此Vim在 file1.js 上运行一次替换命令,在 file2.js 上再运行一次替换命令。 尽管在第二个文件中有10个搜索结果,但 cfdo 只关注快速修复列表中有多少个文件。

而如果您运行 :cdo %s/donut/pancake/g ,这个命令将会在 file1.js 上有效运行一次,然后在 file2.js 上运行10次。它执行的次数与 快速修复列表中元素的数量 相同。因为在 file1.js 上找到1个匹配结果,在 file2.js 上找到10个匹配结果,因此它执行的总次数是11次。

由于您要在列表中运行的命令是 %s/donut/pancake/g ,所以使用 cfdo命令是比较合理的。而使用 cdo 是不合理的,因为它将在 file2.js 中运行10次 %s/donut/pancake/g命令(%s已经是一个针对整个文件的替换操作)。一个文件运行一次 %s 就足够了。如果您使用 cdo,则传给它的命令应当改为 s/donut/pancake/g 才是合理的。

那到底什么时候该用 cfdo?什么时候该用 cdo? 这应当想一想您要传递的命令的作用域,要看命令作用域是整个文件(比如 :%s:g)?还是某一行(比如 :s:!)?

位置列表

位置列表在某种意义上和快速修复列表很像。Vim也使用一个特殊的窗口来显示位置列表的信息。区别在于:您==任何时候都只能有1个快速修复列表,而位置列表则是,有多少个窗口就可以有多少个位置列表。==

假设您打开了两个窗口,其中一个窗口显示 food.txt ,而另一个显示 drinks.txt。在 food.txt里面,运行一个位置列表搜索命令 :lvimgrep:vimgrep命令关于位置列表的一个变体)。

1
:lvim /bagel/ **/*.md

Vim将为 food.txt所在 窗口创建一个位置列表,用于存储所有的bagel搜索结果。用 :lopen命令可以查看位置列表。现在转到另一个窗口 drinks.txt,运行:

1
:lvimgrep /milk/ **/*.md

Vim将为 drinks.txt所在 窗口再创建一个 单独的位置列表,用于存储所有关于milk的搜索结果。

对于每个不同的窗口中您运行的位置命令,Vim都会单独创建一个位置列表。如果您有10个不同的窗口,您就可以有最多10个不同的位置列表。对比前面介绍的快速修复列表,快速修复列表任何时候都只能有1个。就算您有10个不同的窗口,您也只能有1个快速修复列表。

大多数位置列表命令都和快速修复列表命令相似,唯一不同就是位置列表命令有一个 l-前缀,比如: :lvimgrep, :lgrep, 还有 :lmake。在快速修复列表命令中与之对应的是: :vimgrep, :grep, 以及 :make。操作位置列表窗口的方式和快速修复窗口也很相似::lopen, :lclose, :lfirst, :llast, :lnext, 还有:lprev,与之对应快速修复版本是::copen, :cclose, :cfirst, :clast, :cnext, and :cprev

两个位置列表参数的多文件操作命令也和快速修复列表的多文件操作命令也很类似::ldo:lfdo:ldo 对位置列表中每一个元素执行命令,而 :lfdo 对位置列表中每一个文件执行命令。可以查阅 :h location-list了解更多信息。

在Vim中运行多文件操作命令

在编辑工作中,知道如何进行多文件操作是一个非常有用的技能。当您需要在多个文件中改变一个变量名字的时候,您肯定想一个操作就全部搞定。Vim有8种不同的方法支持你完成这个事。

事实上,您可能并不会用到所有8种方法。您会慢慢倾向于其中1中或2种。当您刚开始时,选择其中1个(我个人建议从参数列表开始 :argdo)并掌握它。当您习惯了其中1个,然后再学下一个。您将会发现,学习第二个、第三个、第四个时要容易多了。记得要创造性的使用,即将它和其他各种不同命令组合起来使用。坚持练习直到您可以不经思考地高效的使用它。让它成为您的肌肉记忆。

就像前面已经说过的,您现在已经掌握了Vim的编辑功能。恭喜您!

0%