Qeuroal's Blog

静幽正治

内存泄露相关

  • valgrind

汇编反汇编相关

  • objdump

debug相关

  • gdb
  • address2line

后台运行

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

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

&

在你的命令后添加一个 &

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

1
$ gedit &

对于需要长时间执行的任务,&符号可以使当前终端窗口不被占用,这样就能继续在同一个终端上工作,甚至关闭终端窗口也不会影响任务的正常执行。不过需要注意的是,如果要求这个任务输出内容到标准输出中(例如 echo 或 ls),即便使用了 &,也会等待这些输出任务在前台运行完毕;当使用 &将一个进程放置到后台运行的时候,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 ,诸如nohup和disown 之类的工具也会使您的命令保持运行。

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

查看日志

查看日志常用命令

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 ~/.bashrc 或 source ~/.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 将会允许你远程连接到你的系统,并且执行管理员任务。你将能够通过 scp和sftp安全传输文件。

在 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:19090 或 127.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-vim 和 nerdtree,将下面的片段放到您的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 (切换 norelativenumber 和 relativenumber 选项)。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.vim 和 chocolate.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时,事件BufNewFile 和 BufRead 就会被触发。 *.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文件 (glazeddonut 和 frieddonut),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

如果您试着搜索 chocodonut 或 plaindonut (: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

类似于 python 的 print()

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

  • 查看 message 历史记录

    :messages

  • 清空 mwssage 历史记录

    :message clear

数字

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

十进制 Decimal

1, -1, -10

十六进制 Hexadecimal

以 0x 或 oX 开头

二进制 Binary

以 0b 或 0B 开头

八进制 Octal

以 0, 0o 或 0O 开头

真假值

  • 真:非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

写插件

略

0%