Java 查漏补缺:Java 8 中接口 default 方法

Java 8 新特性:

  • lambda expressions
  • functional interfaces
  • method references
  • streams
  • Optional

还有 interface 中的 staticdefault 方法。

基本使用

Java 8 允许在接口中定义默认方法。

interface Collection {

	void add();

	default void debug(){
		System.out.println("put the key in");
	}
}

为什么要引入 default 方法

和接口中定义的其他方法一样,default 方法默认是 public

接口是用来定义类的行为的,如果要在接口中新添加方法,那么所有实现此接口的类都需要强制的实现新添加的方法。而 default 方法就可以规避该问题。

当多个接口定义了相同 default 方法

当一个类实现的多个接口定义了相同的 default 方法,那么编译时会失败。需要子类 Override 该方法实现。在子类中可以通过 Interface.super.xxx() 方法来调用接口的 default 方法。

@Override
public void turnOnAlarm() {
	Vehicle.super.turnOnAlarm();
	Alarm.super.turnOnAlarm();
}

接口中的 static 方法

除了 default 方法,Java 8 也允许在接口中定义 static 方法。

接口中的静态方法属于类,在接口中定义 static 方法和在类中定义一样。

因为 Java 不支持多继承,所以在遇到一些代码共享的时候,就不能通过多继承来实现,通常的做法是定义一个静态类,包含可能被多个类使用到的共同的方法,比如 Java 中的 Collections 类。

而通过接口中的 static 方法可以提高代码的 cohesion,将相关的逻辑集中到一起,而不用另外定义一个 Object。

同样 Abstract 类也能做到,但是和抽象类的区别在于,抽象类是有 Constructors, state, behavior 的。

总结

理想状态下,接口不应该封装具体的行为,只应该用来定义某类型的公开接口。

但是为了弥补 Java 不能多继承而带来的一些缺点,Java 8 中引入接口的 default 和 static 方法,也肯定是 JDK 工程师仔细考量后的一个权衡。

reference


2019-09-10 java , java8 , interface

理解 GraphQL Schema 结构定义

在上一篇 GraphQL 使用 中了解了 GraphQL 大致用法,如果要更加深入的了解 GraphQL ,那就不得不重新从 Schema 来认识 GraphQL,说到底 GraphQL 还是一个强类型定义,客户端可操作的类型都是需要提前定义好的,这个结构就是这篇文章的重点 – Schema.

因为已经有很多的语言已经实现了 GraphQL,官方不能以某一个语言来具体表达,所以他们自定义了一套表示 GraphQL Schema 的简单表达。1

GraphQL 支持的数据操作

GraphQL 对数据支持的操作:

  • 查询 Query,获取数据的基本查询
  • 变更 Mutation,支持对数据的增删改等操作
  • 订阅 Subscription,用于监听数据变动,并依靠 WebSocket 等协议推送变动消息

内置类型

GraphQL 中的 Type 可以分成,Scalar Type(标量类型),和 Object Type(对象类型)。

GraphQL 自带一些默认类型

  • Int, 32 位有符号整型
  • Float,双精度有符号浮点类型。
  • String, UTF-8 字符串
  • Boolean,true or false
  • ID, 表示唯一标识,通常用来作为主键来获取内容。ID type 和 String 类型使用相同方式序列化,但是如果定义为 ID,那么不可读。

不同的实现,可能会有自己的类型,比如 Date 类型,具体实现要看各个语言。

Enumeration types

枚举类型,将输入固定为几个预定义的值。

Lists and Non-Null

GraphQL 可以表达非空,使用 ! 即可。

type Character {
  name: String!
  appearsIn: [Episode]!
}

或者使用如下来表达非空数组

myField: [String]!

或者可以使用

myField: [String!]

来表达数组可以为空,但是元素不能为 null

Interfaces

GraphQL 支持 Interfaces,直面来看就是接口,GraphQL 可以定义接口,每个接口可以有不同的实现。 [^interface]

[^interface](https://graphql.org/learn/schema/#interfaces)

Type Modifier 类型修饰符

类型修饰符用来修饰类型。

比如定义 User 对象。

type User {
    id: ID!
    name: String!
    articles: [Article]!
    bookNames: [String!]!
}

分别表示了:

  • 列表 [Type]
  • 非空 Type!
  • 列表非空 [Type]!
  • 列表非空,列表内容类型非空 [Type!]!

reference

  1. https://graphql.org/learn/schema/ 


2019-09-08 graphql , schema , graphql-schema , datatype

从 MySQL 迁移到 PostgreSQL 方案调研

之前的文章 PostgreSQL 初识PostgreSQL 数据类型 大致的把 PostgreSQL 了解了一下,那么接下来就是真正地把它用起来。

PGLoader

开源迁移工具,通过一行命令即可做到无缝迁移

PGLoader 原本可以将不同数据源导入到 PostgreSQL 数据库,MySQL 只是它支持的一种。PGLoader 使用 PostgreSQL 的 COPY 命令将数据从源数据库或文件复制到目标 PostgreSQL 数据库中。

安装

Debian 系:

sudo apt-get install pgloader

验证

$ pgloader --version
pgloader version "3.5.2"
compiled with SBCL 1.4.5.debian

或者使用 Docker

docker pull dimitri/pgloader
docker run --rm --name pgloader dimitri/pgloader:latest pgloader --version
docker run --rm --name pgloader dimitri/pgloader:latest pgloader --help

或者参考官方自行编译安装。1

Usage

PGLoader 通过命令来复制,所以必须要配置一个 PGLoader 可以访问 PostgreSQL 的用户来方便执行命令。PostgreSQL 使用角色来管理数据库访问,需要配置该角色与 PGLoader 命令执行用户同一名称。之前提到过 PostgreSQL 普通数据不要轻易使用超级用户来管理,但是 PGLoader 需要使用非常多的权限来管理、访问、加载表中数据,因此需要授予 PGLoader 超级用户权限。

PostgreSQL 用户

创建超级用户 pgloader 使用 -P 来为用户创建密码:

sudo -u postgres createuser --superuser pgloader -P

创建数据库,准备导入该数据库:

sudo -u postgres createdb quotes -O pgloader

MySQL 准备

PostgreSQL 准备工作结束,假设 MySQL 的超级用户和密码都已经设置好,并且该用户拥有要迁移的数据库所有权限。本地执行验证:

mysql -h localhost -u root -p

迁移

注意在任何会影响到数据库数据完整性的操作前,备份数据库,虽然 PGLoader 迁移时并不会修改或者删除数据,但是必要的备份一定不能掉以轻心。使用 mysqldump 来备份数据库。

在本地做一个简单的测试,本地 MySQL 数据库 quotes:

mysql://root@localhost:3306/quotes

如果想要把这个数据库迁移到 PostgreSQL 中。

postgresql://user:pass@localhost:5432/quotes

执行

pgloader mysql://root:password@localhost:3306/quotes postgresql://pgloader:password@localhost:5432/quotes

pgloader 接受两个参数,一个是源数据库连接,一个是目标数据库连接。

➜ build/bin/pgloader mysql://root:password@localhost:3306/quotes postgresql://postgres:password@localhost:5432/quotes
2019-09-07T09:56:38.030000+08:00 LOG pgloader version "3.6.26cc9ca"
2019-09-07T09:56:38.047000+08:00 LOG Migrating from #<MYSQL-CONNECTION mysql://root@localhost:3306/quotes {1005B1DE43}>
2019-09-07T09:56:38.048000+08:00 LOG Migrating into #<PGSQL-CONNECTION pgsql://postgres@localhost:5432/quotes {1005D6D903}>
2019-09-07T09:56:38.335000+08:00 LOG report summary reset
			 table name     errors       rows      bytes      total time
-----------------------  ---------  ---------  ---------  --------------
		fetch meta data          0          1                     0.100s
		 Create Schemas          0          0                     0.004s
	   Create SQL Types          0          0                     0.005s
		  Create tables          0          2                     0.026s
		 Set Table OIDs          0          1                     0.004s
-----------------------  ---------  ---------  ---------  --------------
		  quotes.quotes          0        100    15.9 kB          0.058s
-----------------------  ---------  ---------  ---------  --------------
COPY Threads Completion          0          4                     0.058s
 Index Build Completion          0          0                     0.000s
		Reset Sequences          0          0                     0.015s
		   Primary Keys          0          0                     0.000s
	Create Foreign Keys          0          0                     0.000s
		Create Triggers          0          0                     0.001s
		Set Search Path          0          1                     0.001s
	   Install Comments          0          0                     0.000s
-----------------------  ---------  ---------  ---------  --------------
	  Total import time          ✓        100    15.9 kB          0.075s

校验数据

sudo -u postgres psql
\c quotes
select * from quotes.quotes limit 1;

这里就会发现,导入的数据没有默认到 public Schema 下,而是在自己的 Schema 下。在 PostgreSQL 中,每当我们创建一个数据库,都会自动产生一个 public Schema,当登录数据库查询时,如果没有加特定的 Schema,则会默认使用 public.

在使用的时候有几个问题,Debian 源中的 pgloader 有些老,3.5.2 的版本似乎有些 Bug,我在使用时报错

2019-09-07T02:03:45.044000Z LOG Migrating from #<MYSQL-CONNECTION mysql://root@localhost:3306/imdb {1005805E43}>
2019-09-07T02:03:45.047000Z LOG Migrating into #<PGSQL-CONNECTION pgsql://pgloader@localhost:5432/quotes {1005A56E73}>
KABOOM!
INFO: Control stack guard page unprotected
Control stack guard page temporarily disabled: proceed with caution

What I am doing here?

Control stack exhausted (no more space for function call frames).
This is probably due to heavily nested or infinitely recursive function
calls, or a tail call that SBCL cannot or has not optimized away.

PROCEED WITH CAUTION.

所以最后不得不直接使用源码编译使用最新版。

➜ build/bin/pgloader --version
pgloader version "3.6.26cc9ca"
compiled with SBCL 1.4.5.debian

该版本没有任何问题。

同 pgloader 还有其他一些命令:

pgloader./test/sqlite/sqlite.db postgresql:///newdb

其他使用方式

PGLoader 是一个可以高度配置的工具,除了上面提到的简单命令行迁移之外,PGLoader 还提供了强大的配置文件来帮助迁移。PGLoader 可以使用一个文件来配置告诉 PGLoader 如何迁移文件,该文件可以配置 PGLoader 的运行方式,并且可以执行更加复杂的迁移。

创建文件 vi pgload.load:

LOAD DATABASE
	FROM mysql://root:einverne.@localhost:3306/wordpress
	INTO pgsql://pgloader:einverne.@localhost:5432/quotes

	WITH include drop, create tables, create indexes, workers = 8, concurrency = 1

ALTER SCHEMA 'wordpress' RENAME TO 'public'
;

注意最后的 ; 一定要加。

解释:

  • LOAD DATABASE 指定从数据库加载
  • FROM 源数据库
  • INTO 目标数据库
  • WITH 指定 PGLoader 行为
    • include drop,迁移过程中,PGLoader 会删除目标 PostgreSQL 数据库中在源数据库中同名的任何表。注意备份。
    • create tables, 配置 PGLoader 根据源数据库数据在目标数据库中创建新表,如果使用 create no tables,则需要手动在目标数据库中创建好对应的表。
  • ALTER SCHEMA, 在 WITH 语句之后,配置特定 SQL 来告诉 PGLoader 执行其他操作。

更多更加详细的配置可以参考官方文档

创建完该文件后使用如下命令执行

pgloader pgload.load

其他方案

下面也是可选的方案,不过没有仔细研究

reference


2019-09-07 mysql , postgresql , database , sql , migration-tool , migration , rbdms

GraphQL 使用

很多人都知道 GraphQL 起源于 Facebook,但是似乎很少中文材料提到 GraphQL 出现的契机,我在看完这个纪录片 之后才对 GraphQL 的出现有更加深刻的了解。都知道当年 Facebook 的移动客户端都是网页,随着移动互联网发展,Facebook 网页实现的客户端性能和体验受到非常严重的影响,所以后来不得不去做原生的应用。那么这个时候就遇到了一个问题,原来直接使用网页,那么不同客户端用的接口都是给网页用的,最多做一下屏幕的适配,但是如果使用原生的应用,那么必然会需要设计不同的 API,而 Facebook 的工程师发现,对于复杂的 Feed 流,评论等等,用 RESTful 接口将是一个灾难,所以一帮人开始设计一种查询语言,这就是后来的 GraphQL。也应该纠正一下 Facebook 那帮工程师,只是开源了一份设计和一份实现,但他们万万没有想到开源社区的力量,就像片中 Nick Schrock 说的那样,他低估了社区的力量,在短短的几个月,几年时间中,GraphQL 就已经有了非常多语言的支持,周边应用也非常丰富。原来在脑海里的想法,都没有到生产环境中用过,就这样在所有社区的人的努力下成为了改变这个行业的一部分。

在了解 GraphQL 的过程中,看到一个非常有意思的比喻,经常有人会问起 GraphQL 和 RESTful 接口的区别,如果用去餐厅吃放,自助餐或者点餐来比喻,那么 GraphQL 就像是自助餐,你想要吃什么去餐盘中自己选择,而 RESTful 就像是点菜,需要看菜单,然后根据菜单选择。虽然两者都能吃饱,但是使用感受完全不同。1 当然这篇文章就不再继续讲 GraphQL 是什么,有什么用了,之前的 文章 也列了很多资料,差不多可以了解到它的具体使用场景了。这篇文章重点在于怎么把 GraphQL 用到实际项目中。

GitHub 上的 awesome-graphql 项目列举了太多的开源实现,从 c++, java, 到 python, nodejs, ruby 等等,这里我就选择上手最快的 python 更具体一些是 Django 来体验一下 GraphQL 和 Django 结合的效果。

先放上代码:

原始项目有些时间了,所以更新了一下,加了一些其他特性。

Python

主要借用的是这个项目:

Django 支持 https://github.com/graphql-python/graphene-django/

查询 Query

GraphQL 的查询语法。

Field

最常见的查询,请求方根据 Schema 定义结构,查询:

query {
	allPeople {
		id
		username
	}
}

结果和查询结构相同,包裹在 data 结构中:

{
  "data": {
	"allPeople": [
	  {
		"id": "1",
		"username": "steve"
	  },
	  {
		"id": "2",
		"username": "aholovaty"
	  },
	  {
		"id": "3",
		"username": "swillison"
	  },
	  {
		"id": "4",
		"username": "gvr"
	  },
	  {
		"id": "5",
		"username": "admin"
	  }
	]
  }
}

GraphQL 的接口是有类型定义的,对于上面的查询可以总结出

type RootQuery {
	allPeople: Person
}

type Person {
	id: int
	username: String
}

条件查询 Arguments

官方的说法 叫做 Arguments,参数化请求。

比如查询条件为 id 为 5 的用户信息

query {
  person(id: "5") {
	id
	username
	email
  }
}

结果是

{
  "data": {
	"person": {
	  "id": "5",
	  "username": "admin",
	  "email": "i@gmail.com"
	}
  }
}

结构

type RootQuery {
	person(id: int) : Person
}

GraphQL 可以有不同的参数类型,这取决于你的类型定义,Schema 定义。2 并且你可以自定义你的参数类型,只要你能够序列化该参数,这就使得 GraphQL 扩展性大大增强。

Aliases

没有看到官方的翻译,暂且称之为“别名”好了 [^aliases],某些情况下想要重写结果的 KEY,可以使用改方式来实现。

假如查询的时候要查两个用户的信息

query {
  person(id: "5") {
	id
	username
	email
  },
  person(id: "4") {
	id
	username
	email
  }
}

这么查肯定是会返回两个 person 的 key 的 JSON,那么就会有问题,GraphQL 允许我们重命名

query {
  p5: person(id: "5") {
	id
	username
	email
  },
  p4: person(id: "4") {
	id
	username
	email
  }
}

返回值是这样的:

{
  "data": {
	"p5": {
	  "id": "5",
	  "username": "admin",
	  "email": "i@gmail.com"
	},
	"p4": {
	  "id": "4",
	  "username": "gvr",
	  "email": "gvr@dropbox.com"
	}
  }
}

[^Aliases](http://graphql.org/learn/queries/#aliases)

Fragments

Fragments Android 里面也用了这个名词,我也不知道怎么翻译,暂且叫做“片段”好了,为什么要有 Fragments 呢? 就是因为要重用,看到上面请求两个用户的 Query 语句了吗?其中每个用户都想要请求其 id, username, email,这些参数是不是每次都要手写,假如这个 Person 信息不仅包含着三个,还有很多,假如不止请求两个用户信息,难道还要复制 N 遍,这显然是不合理的,所以 GraphQL 引入了 Fragments 概念,可以定义片段,然后再复用。

{
	p4: person(id: "4") {
	...personFields
  },
  p5: person(id: "5") {
	...personFields
  }
}

fragment personFields on PersonType {
  id
  username
  email
  fullName
  firstName
  lastName
  friends {
	id
	username
  }
}

结果就是对应的

{
  "data": {
	"p4": {
	  "id": "4",
	  "username": "gvr",
	  "email": "gvr@dropbox.com",
	  "fullName": "Guido van Rossum",
	  "firstName": "Guido",
	  "lastName": "van Rossum",
	  "friends": [
		{
		  "id": "2",
		  "username": "aholovaty"
		},
		{
		  "id": "3",
		  "username": "swillison"
		}
	  ]
	},
	"p5": {
	  "id": "5",
	  "username": "admin",
	  "email": "i@gmail.com",
	  "fullName": " ",
	  "firstName": "",
	  "lastName": "",
	  "friends": []
	}
  }
}

Use Variables in Fragments

如果再进一步,既然能够定义 Fragments 了,那么在其中定义变量也是可以的吧。

query person($first: Int = 2) {
	p4: person(id: "4") {
	...personFields
  },
  p5: person(name: "admin") {
	...personFields
  }
}

fragment personFields on PersonType {
  id
  username
  email
  fullName
  friends(first: $first) {
	id
	username
  }
}

结果是这样的

{
  "data": {
	"p4": {
	  "id": "4",
	  "username": "gvr",
	  "email": "gvr@dropbox.com",
	  "fullName": "Guido van Rossum",
	  "friends": [
		{
		  "id": "2",
		  "username": "aholovaty"
		},
		{
		  "id": "3",
		  "username": "swillison"
		}
	  ]
	},
	"p5": {
	  "id": "5",
	  "username": "admin",
	  "email": "i@gmail.com",
	  "fullName": " ",
	  "friends": [
		{
		  "id": "1",
		  "username": "steveluscher"
		},
		{
		  "id": "2",
		  "username": "aholovaty"
		}
	  ]
	}
  }
}

要使用变量,需要有三个步骤:

  • 将静态变量替换为带$ 操作符的 $variableName 变量名
  • 将变量名作为某个查询 operation name 的参数
  • variableName: value 参数以可以传输的格式(通常是 JSON) 传递给查询

变量在定义时需要注意会在后面跟着 : type 来表示该变量的类型。定义的变量只能是这些类型

  • scalars 标量
  • enums 枚举
  • input object types

对于复杂变量,需要到 Schema 中知道该变量的内容。这部分可以参考 Schema.

变量如果是必须的,那么需要在变量类型后面增加 ! 来表示

操作名 Operation name

在上面的例子中也能看到,在 Query 的时候,有些情况下可以省略最前面的 query 关键字,这个 query 关键字在 GraphQL 中叫做 operation type,之所以叫做 type,是因为这只是查询,到后面还有 mutation (修改), subscription 等等。这个操作类型隐含了此次操作的具体动作,比如说是查询,还是修改,还是订阅等等。

接在 query 关键字后面的是 operation name,操作名,该名字定义了此次查询的含义,比如上面的例子就是查询 person 信息。operation name 就像是大部分语言里面的方法,operation name 在获取多个文档时才是必须的,但是非常建议给予每一个查询一个名字,operation name 在调试服务端程序时非常有用,可以快速定位问题。

更改 mutation

插入或者更新数据,和查询类似,使用 mutation operation type, 后面接 operation name,再就是传参,以及定义返回值。

mutation createCategory {
	createCategory(name: "Milk") {
	category {
	  id
	  name
	}
  }
}

class CreateCategory(graphene.Mutation):
	"""
	"""
	class Arguments:
		name = graphene.String(required=True)

	category = graphene.Field(CategoryType)

	def mutate(self, info, **kwargs):
		name = kwargs.get('name')
		category = Category(name=name)
		Category.save(category)
		return CreateCategory(category=category)


class UpdateCategory(graphene.Mutation):
	"""
mutation updateCategory {
	updateCategory(id: "5", name: "MilkV2") {
	category {
	  id
	  name
	}
  }
}
	"""
	class Arguments:
		id = graphene.ID()
		name = graphene.String(required=True)

	category = graphene.Field(CategoryType)

	def mutate(self, info, id, name):
		ca = Category.objects.get(pk=id)
		ca.name = name
		ca.save()
		return UpdateCategory(category=ca)


class Mutation(graphene.ObjectType):
	create_category = CreateCategory.Field()
	update_category = UpdateCategory.Field()

Mutation 和 Query 有一个显著的差异,Query Field 会同时查询,而 Mutation Field 只会顺序,一个接一个查询。

扩展阅读

其他 Python 实现的 GraphQL

用 node 实现第一个 GraphQL

GraphQL with Flask

JSON API

这里是一些已经实现 GraphQL 的公开的 API

reference

  1. https://www.youtube.com/watch?v=X3QM6Ap6u-4 

  2. https://graphql.org/learn/schema/ 


2019-09-07 graphql , apollo , facebook , restful , api

Drools 学习笔记之决策表: Guided Decision Table

Column

决策表的列定义。

Ruleflow-Group

A string identifying a rule flow group. In rule flow groups, rules can fire only when the group is activated by the associated rule flow. Example: ruleflow-group "GroupName"

Agenda-Group

A string identifying an agenda group to which you want to assign the rule. Agenda groups allow you to partition the agenda to provide more execution control over groups of rules. Only rules in an agenda group that has acquired a focus are able to be activated. Example: agenda-group "GroupName"

Agenda Groups 是一种分区规则的方法,任何时候只有一组规则拥有 Focus,只有拥有 Focus 焦点的规则才会生效。 agenda-group 默认值 MAIN,类型是 String,可以如下方式定义:

rule "Is of valid age"
agenda-group "GroupA"
when
	Applicant( age < 18 )
	$a : Application()
then
	$a.setValid( false );
	System.out.println("GroupA fired age < 18");
end

Agenda-Group 像栈一样工作,当给定 Group 设置 Focus 时,该 Group 会被放到栈顶。

Metadata column

元数据列,默认情况下元数据列是隐藏的。可以在决策表的列属性中显示。

reference


2019-09-05 drools , kie , guided-decision-table

使用 ripgrep 通过正则快速查找文件内容

ripgrep(简称 rg),是一个用 Rust 实现的命令行搜索工具,可以通过正则来搜索当前的目录。默认情况下 ripgrep 会遵循 .gitignore 的内容,并且自动跳过隐藏的文件目录,以及二进制文件。 ripgrep 原生支持 Windows, MacOS, Linux。ripgrep 和其他流行的搜索工具非常相似,比如 The Silver Searcher, ackgrep.

rg 的优势

目前 Linux 下可用的搜索工具非常多,GNU 中的 grepack-grepThe Silver Searcher 等等,而 rg 的优势在于

  • ripgrep 是真正的快,我在一个有 26G 代码的目录中查找一个方法也可以在几乎秒级的速度找到,所以我经常用来搜索不确定调用关系,但代码又分布在不同项目中时使用
  • ripgrep 遵循 .gitignore,在默认情况下会跳过二进制文件,隐藏的文件目录,不会追踪软链接,更进一步加快了速度
  • ripgrep 支持 Unicode, 可以搜索压缩文件,还可以自己选择正则表达式匹配引擎,比如 PCRE2

Installation

安装的内容直接参考官方页面 即可。

brew install ripgrep

Usage

来看看 rg 的通用格式

USAGE:
	rg [OPTIONS] PATTERN [PATH ...]
	rg [OPTIONS] [-e PATTERN ...] [PATH ...]
	rg [OPTIONS] [-f PATTERNFILE ...] [PATH ...]
	rg [OPTIONS] --files [PATH ...]
	rg [OPTIONS] --type-list
	command | rg [OPTIONS] PATTERN

最不用记忆的就是直接:

rg "keyword"

会显示当前目录下的搜索内容,会打印出文件名关键字出现的行数

和 grep 命令类似,也有三个打印出上下行的选项

  • -A NUM 打印匹配行后面 after N 行
  • -B NUM 打印匹配行前面 before N 行
  • -C NUM 打印匹配行前后 N 行

用正则表达式搜索

使用 -e REGEX 来指定正则表达式

rg -e "*sql" -C2

搜索所有内容包括 gitignore 和隐藏文件

默认 rg 会忽略 .gitignore 和隐藏文件,可以使用 -uu 来查询所有内容:

rg -uu "word" .

显示匹配的次数

使用 -c 来显示匹配的次数:

rg -c "word" .

结果会在文件名后面增加一个次数。

搜索指定的文件类型

可以使用 -t type 来指定文件类型:

rg -t markdown "mysql" .

支持的文件类型可以通过

rg --type-list

来查看。

看到这里,有些读者可能要问假如我要在两个文件类型中查找呢,这个时候 -t 参数就无法满足了,需要引入新的 -g 参数,man rg 看一下 -g 就知道该选项后面跟着一个 GLOB,正则表达式,包括或者去除一些文件或者目录。比如要在 md 文件或者 html 文件中查找 “mysql” 关键字

rg -g "*.{md,html}" "mysql"

注意这里是花括号。

只打印包含匹配内容的文件名

使用 -l 来打印文件名

rg -l -w "word" .

相反的是如果要打印没有匹配内容的文件名

rg --files-without-match -w "word" .

启用大小写敏感

使用 -s 选项来启用大小写敏感

rg -s "word" .

使用 -i 来关闭大小写敏感。

显示不包含关键字的行

使用选项 -v 来显示不包含关键字的行

rg -v "word" .

搜索单词

添加 -w 参数仅显示该单词的内容,该选项等同于在搜索 Pattern 前后加上 \b,这样可以避免因为模糊搜索而导致的不精确。

rg -w "myword" .

比如搜索 abc,可能有些单词包含 dabce ,那么也会被搜索出来,而加上 -w 就不会搜索出来了。

reference

有两个方法查看 rg 使用


2019-09-02 ripgrep , grep , find , ag , rg , search , regex

在 Vim 下使用 fzf

fzf 的介绍看这篇文章,这篇文章主要总结一下 Fzf 在 vim 下面的使用。

安装过程就不再说,安装后 :help fzf 可以查看所有帮助。

fzf-vim 安装之后, :FZF 命令会被添加。

" 在当前目录搜索"
:FZF
" 在 home 目录搜索"
:FZF ~
" fzf 全屏"
:FZF!

插件配置

常用的配置 :help 中都能看到。

" An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines)
  call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
  copen
  cc
endfunction

let g:fzf_action = {
  \ 'ctrl-q': function('s:build_quickfix_list'),
  \ 'ctrl-t': 'tab split',
  \ 'ctrl-x': 'split',
  \ 'ctrl-v': 'vsplit' }

" Default fzf layout
" - down / up / left / right
let g:fzf_layout = { 'down': '~40%' }

let g:fzf_history_dir = '~/.local/share/fzf-history'

Usage

常用命令

在当前目录下查找

Files  :FZF

和 ctrip.vim 类似,使用回车, Ctrl-T, Ctrl-X 或者 Ctrl-V 可以分别在当前窗口,标签页,水平分隔或者垂直分隔窗口中打开。

FZF_DEFAULT_COMMANDFZF_DEFAULT_OPTS 环境变量也会被使用。

Vim 内部 Buffers, Windows 查找

跳转到 Buffer 内的某一行

:Lines

或者是当前 Buffer 内的行

:BLines

查找 Buffers

:Buffers

查找 Windows:

:Windows

查找可用的命令

:Commands

Normal Mode Mappings

:Maps

reference


2019-09-02 fzf , vim , vim-plugins

我的 Linux 环境及 Cinnamon 桌面配置记录

记录一下 Ubuntu 和 Linux Mint 的一些初始化设置,以便于快速恢复自己熟悉的工作环境。当然我更建议日常进行硬盘及文件的快照备份。Linux Mint 下可以是 timeshift 进行文件备份。而如果要对整块硬盘进行迁移和备份,切换到新的机器中,我推荐使用 clonezilla12 来备份与恢复。

桌面环境 Cinnamon

毕竟用了多年 GUI,我至今还没有接受完全的命令行,虽然有人非常推荐用 i3,但是至少在我熟悉目前这一套工作环境前,我还没有去尝试 i3,等某一天真的对目前的桌面非常熟悉了,并且能够希望通过快捷键来加速窗口管理了,我一定会去试试 i3 桌面环境。所以在剩下的桌面环境中,我选择了 Cinnamon。个人觉得是我用过所有桌面中最 User-Friendly 的。

当然如果是 Mint 默认就会选择 Cinnamon,而 Ubuntu 可以使用 PPA 安装:

sudo add-apt-repository ppa:embrosyn/cinnamon && sudo apt update
sudo apt install cinnamon

linux-mint-neofetch

Prerequisite

这篇文章涉及到很多 Debian/Ubuntu/Linux Mint 下关于 /etc/apt/ 配置的内容。所以有必要先了解一下该配置目录。

sources.list

在该目录下有一个 /etc/apt/sources.list 目录,该目录配置了 Ubuntu 官方的软件源,一般可以用国内的镜像 来替换。

其次就是 sources.list.d 配置目录,其中放置了第三方软件源的配置,比如 Google Chrome 的源,Microsoft 的 vscode 软件源,在比如下方可能提到的其他软件源的配置都存放在这里。

save distUpgrade

在这些软件源的配置文件中可能看到 sources.list.savesources.list.distUpgrade 这样的文件,打开文件查看,这些文件和 sources.list 文件差不多。其实这些文件都是一些备份文件。3

  • 在添加一个 repository 后,Ubuntu 会自动生成 .save 文件,以备份之前的配置,如果出现 apt update 时的问题,可以随时回退回去 sudo cp /etc/apt/sources.list.save /etc/apt/sources.list
  • .distUpgrade 文件则是在运行了 dist-upgrade 之后生成的备份文件。

APT

安装必要工具:

sudo apt install vim zsh wget git tree htop fcitx fcitx-rime vlc gimp inkscape shutter filezilla audacity exuberant-ctags iotop iftop nethogs

基础环境配置

基础环境配置,包括日常使用这一台机器需要用到的应用软件,必不可少的组成部分,包括终端,输入法,浏览器等等。

Terminal

guake

下拉式终端,F12 可以让终端无处不在,配合 tmux 基本上也实现了类似 i3 中对终端的管理。

sudo apt install guake

Terminator

日常也会装一个 Terminator,如果有第三块屏幕的话,放着看日志。

sudo add-apt-repository ppa:gnome-terminator
sudo apt-get update
sudo apt-get install terminator

输入法

有一段时间使用搜狗的 linux 版本,没啥大问题,直到有一天开始频繁崩溃 一怒之下换成了 fcitx-rime ,之后彻底的爱上了 Rime.

sudo apt install fcitx fcitx-rime im-config

Rime 配置放到 GitHub 托管。

Browser

Chrome

通过如下方式添加 Google Chrome 的源

wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'

Package in this source:

  • google-chrome-stable
  • google-chrome-beta
  • google-chrome-unstable

Vivaldi

Vivaldi 是另一个我长期关注的浏览器,和 Chrome 一样都源自 Chromium,都使用 Blink 渲染引擎,但是 Vivaldi 在界面显示和 UI 交互方面做的太贴心了,原来一直没有的同步功能,也加上了,并且可以直接使用 Chrome Web Store,可以做到无痛切换。

FireFox

一般都默认自带了,但基本上没怎么用。

Syncthing

同步工具,代替了我原来长期使用的 Dropbox

# Add the release PGP keys:
curl -s https://syncthing.net/release-key.txt | sudo apt-key add -

# Add the "stable" channel to your APT sources:
echo "deb https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list

# Update and install syncthing:
sudo apt-get update
sudo apt-get install syncthing

除了 Syncthing,还有 NextCloud 作为备份。

笔记

笔记应用

  • WizNote

字典

GoldenDict

Programming

开发环境配置

Java 开发环境

推荐使用 OpenJDK

sudo apt install openjdk-8-jdk

更多内容参考 OpenJDK 官网,进行手动安装。

安装 Java 9 及以后:

sudo apt install openjdk-11-jdk

如果不想使用 OpenJDK,使用 Oracle JDK:

sudo add-apt-repository ppa:webupd8team/java && sudo apt update
sudo apt install oracle-java8-installer

sudo update-alternatives --config java
sudo update-alternatives --config javac

更加详细的安装 JDK 的方式,可以参考这里

IDE

  • IntelliJ IDEA

JD-GUI

Java Decompiler JD-GUI

Python

直接看这里 Pyenv

Database

MySQL

我使用 MySQL Workbench,虽然有点重,但不影响使用。IntelliJ 自带的 SQL 工具也是很好用的。

Redis

我使用 Redis Desktop Manager 客户端,当然直接用命令行也是可以的。

sudo snap install redis-desktop-manager

SmartGit

使用多年的 Git 客户端,日常命令行即可。

Others

Screenshot

之前都直接使用 Cinnamon 自带的截图工具 desktop-capture,这个工具非常轻便,并且自带截图,录屏,录制 GIF 等等工具,但是唯一的缺点是无法 mark,无法在截图后直接在图片上做标注。所以后来又发现了一款叫做 flameshot 的工具,相辅相成,很舒服。

sudo apt install flameshot

邮件客户端

sudo apt install evolution evolution-ews evolution-indicator evolution-plugins

SNAP

sudo snap install postman telegram-desktop

PDF 阅读器

Cinnamon 自己带这一个简易的 PDF 阅读器,日常使用本身也没有任何问题,不过如果要截取 PDF 内容,尤其是影印的 PDF,就需要 Okular

Best PDF reader ever

sudo snap install okular

下载

Transgui with Tranmission

MultiMedia

多媒体相关包括音视频播放器,解码器,转码,以及媒体信息查看。

Player

Windows 上有 PotPlayer,Linux 上自然也不输。

VLC

播放器 VLC

SMPlayer

SMPlayer 也非常好用。

sudo add-apt-repository ppa:rvm/smplayer
sudo apt-get update
sudo apt-get install smplayer smplayer-themes smplayer-skins

ffmpeg

大名鼎鼎的 ffmpeg 不得不拥有。(LGPL 2.14)

sudo apt install ffmpeg

MediaInfo

MediaInfo 用来查看音视频的各个详细参数,包括音视频编码格式信息,封装类型,字幕等等信息。(BSD 协议下开源 5)

命令安装:

sudo apt install mediainfo

或者安装界面:

Stacer

System Dashboard, Optimizer & Monitor.

stacer linux

Install:

sudo add-apt-repository ppa:oguzhaninan/stacer -y
sudo apt-get update
sudo apt-get install stacer -y

GitHub page:

电子书管理

Calibre

PPA

一些值得一提的 PPA。

sudo add-apt-repository ppa:noobslab/icons
sudo apt install arc-theme


sudo add-apt-repository ppa:noobslab/themes
sudo apt install flatabulous-theme

sudo add-apt-repository ppa:numix/ppa
sudo apt install numix-icon-theme

sudo add-apt-repository ppa:snwh/pulp
sudo apt install paper-icon-theme
# 同时也可以安装 GTK 和 Cursor 主题
sudo apt install paper-gtk-theme
sudo apt install paper-cursor-theme


sudo add-apt-repository ppa:papirus/papirus
sudo apt install papirus-icon-theme


sudo add-apt-repository ppa:atareao/telegram -y
sudo add-apt-repository ppa:atareao/atareao -y

wget https://launchpadlibrarian.net/292068009/indicator-sound-switcher_2.1.1ubuntu0-1_all.deb
wget http://ppa.launchpad.net/nilarimogard/webupd8/ubuntu/pool/main/s/syspeek/syspeek_0.3+bzr26-1~webupd8~zesty_all.deb
wget https://linux.dropbox.com/packages/ubuntu/dropbox_2015.10.28_amd64.deb
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
wget https://jaist.dl.sourceforge.net/project/deadbeef/debian/deadbeef-static_0.7.2-2_amd64.deb

sudo apt install fcitx-module-cloudpinyin gtk2-engines-murrine:i386 libudev1:i386 i965-va-driver vainfo openshot classicmenu-indicator numix-gtk-theme shimmer-themes numix-icon* caffeine leafpad git unity-tweak-tool fcitx-mozc ibus-qt4 curl ctags vim-doc vim-scripts cscope fonts-dejavu indent psensor libluajit-5.1-2 python3-pip  ubuntu-make ppa-purge jayatana compizconfig-settings-manager zsh wget papirus-folders papirus-icon-theme filezilla-theme-papirus exfat-fuse exfat-utils ttf-bitstream-vera -y

WPS

wget http://kdl1.cache.wps.com/ksodl/download/linux/a21//wps-office_10.1.0.5707~a21_amd64.deb wget http://kdl.cc.ksosoft.com/wps-community/download/fonts/wps-office-fonts_1.0_all.deb wget http://kr.archive.ubuntu.com/ubuntu/pool/main/libp/libpng/libpng12-0_1.2.54-1ubuntu1_amd64.deb sudo dpkg -i libpng12-0_1.2.54-1ubuntu1_amd64.deb sudo dpkg -i wps-office_10.1.0.5707~a21_amd64.deb sudo dpkg -i wps-office-fonts_1.0_all.deb

sudo apt-get install wqy-*
sudo gedit /etc/fonts/conf.avail/69-language-selector-zh-cn.conf

文泉译微米黑字体

sudo apt install fonts-wqy-microhei

sudo vim /usr/share/X11/xorg.conf.d/40-libinput.conf

Option “Tapping” “on” Option “NaturalScrolling” “true” Option “ClickMethod” “clickfinger” Option “DisableWhileTyping” “True”

sudo apt install sassc autoconf automake pkg-config libgtk-3-dev git -y
git clone https://github.com/andreisergiu98/arc-flatabulous-theme && cd arc-flatabulous-theme
./autogen.sh --prefix=/usr --disable-transparency
sudo make install



sudo apt-get install terminator
sudo update-alternatives --config x-terminal-emulator
gsettings set org.gnome.desktop.default-applications.terminal exec 'terminator'


sudo dpkg-reconfigure ca-certificates
sudo apt install shadowsocks-libev simple-obfs
sudo vim /etc/shadowsocks-libev/config.json
sudo vim /lib/systemd/system/shadowsocks-libev.service


sudo apt-get install tlp tlp-rdw tp-smapi-dkms acpi-call-dkms thermald powertop


sudo add-apt-repository ppa:gottcode/gcppa -y
sudo add-apt-repository ppa:nextcloud-devs/client -y
sudo add-apt-repository -u ppa:snwh/ppa -y
sudo add-apt-repository ppa:ubuntuhandbook1/shutter -y
sudo add-apt-repository ppa:kasra-mp/ubuntu-indicator-weather -y
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo add-apt-repository -u ppa:snwh/ppa -k


wget https://launchpadlibrarian.net/292068009/indicator-sound-switcher_2.1.1ubuntu0-1_all.deb
wget http://ppa.launchpad.net/nilarimogard/webupd8/ubuntu/pool/main/s/syspeek/syspeek_0.3+bzr26-1~webupd8~zesty_all.deb
wget https://www.dropbox.com/download?dl=packages/ubuntu/dropbox_2019.01.31_amd64.deb
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
wget https://jaist.dl.sourceforge.net/project/deadbeef/debian/deadbeef-static_0.7.2-2_amd64.deb
wget https://launchpadlibrarian.net/330848294/libgtk2-appindicator-perl_0.15-1build5_amd64.deb

Hi Res 与蓝牙 LDAC

sudo add-apt-repository ppa:eh5/pulseaudio-a2dp -y
sudo apt-get install  pulseaudio-module-bluetooth

sudo vim /etc/pulse/daemon.conf

resample-method = soxr-vhq
default-sample-format = s32le
default-sample-rate = 192000
alternate-sample-rate = 96000

pulseaudio -k
pulseaudio --start

sudo vim /etc/pulse/default.pa

# LDAC Standard Quality
load-module module-bluetooth-discover a2dp_config="ldac_eqmid=sq"

# LDAC High Quality; Force LDAC/PA PCM sample format as Float32LE
#load-module module-bluetooth-discover a2dp_config="ldac_eqmid=hq ldac_fmt=f32"

粘贴板工具

sudo add-apt-repository ppa:hluk/copyq-beta
sudo apt install copyq

Theme

  • flatabulous-theme
  • communitheme

ICON

图标包我非常喜欢的是 Numix 这个系列的图标,特别是其中的 numix-icon-theme-circle。那么就安装 numix-icon-theme-circle 这套图标包了。

Numix-icon-theme-circle

sudoadd-apt-repository ppa:numix/ppa
sudo apt-get update
sudo apt-get install numix-icon-theme-circle

图标包安装好了设置使用上该套图标包还需要使用 Gnome Tweak,安装方法也是通过到 AppStore 中搜索 Gnome Tweak 即可,也可以通过 sudo apt-get install gnome-tweak-tool 来完成安装。由于系统是中文系统,该软件中文名叫做“优化”。

App List

  • 4K Video Downloader 下载工具
  • Anbox 在 Linux 上模拟 Android,可以安装安卓应用
  • Anki 单词记忆
  • Atom 文本编辑器
  • Brasero
  • Bustle
  • Cerebro
  • Character Map 字体工具
  • Charles 抓包工具
  • Cheese
  • Chess
  • Cinnamon Control Center
  • Color
  • Conky
  • Conky Manager
  • CopyQ
  • CroseOver
  • Dropbox
  • Calibre 电子书管理器
  • Electronic Wechat
  • Emojione Picker
  • Evolution
  • Fcitx 输入法
  • Firefox
  • Five or More
  • GHex
  • GIMP
  • GParted
  • GPaste
  • GVim
  • GitKraken
  • Gnote
  • Goldendict**
  • Google Chrome
  • Gpick
  • Guake
  • Hiri
  • Inkscape
  • JD-GUI
  • Joplin
  • KeePassXC
  • Meld
  • MySQL Workbench
  • Nemo
  • NemoVim
  • Netease Cloud Music
  • NextCloud desktop sync client
  • Okular
  • Postman
  • Remmina
  • Rhythmbox
  • Shadowsocks
  • ShotWell
  • Simple Scan
  • Slack
  • SmartGit**
  • Stacer
  • Syncthing
  • SublimeText
  • Teamviewer
  • Telegram
  • Terminator
  • VLC
  • VMware Workstation
  • Vim
  • Visual Studio Code
  • Vivaldi
  • WPS
  • Wireshark
  • Wiznote
  • Zeal
  • Zenmap
  • zsh
  • bmon
  • Calibre
  • haroopad
  • pCloud
  • mpv
  • qBittorent
  • tagspaces
  • Zeal

全局快捷键

快捷键 说明
F12 Guake
Ctrl-c-c GoldenDict 查词
Ctrl-grave Rime 切换输入方案

reference


2019-08-30 ubuntu , linux , linut-mint , cinnamon , ppa , apt

在 Ubuntu 下为 Postman 创建 icon

Postman 早两年就不再更新 Chrome 版本的应用,转而发布 Native app,这个原生的应用非常完美,但唯一的不足就是没有启动 ICON,下载之后就只有一个 tar 包,解压到任意一个文件夹之后就能使用,但是在桌面上,或者启动器中是无法找到该应用的。

和之前说过的给 Wiznote 一样,可以使用 gnome 提供的工具来给 Postman 也提供一个 ICON

gnome-desktop-item-edit ~/.local/share/applications --create-new

然后填入应用图标,路径,等等就可以。然后去该目录下查看就能看到已经创建了一个 Postman.desktop 文件,当然你也可以手动创建该文件。

[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Name=Postman
Icon=postman.png
Path=/home/[your username]/Postman
Exec=/home/[your username]/Postman/Postman
StartupNotify=false
StartupWMClass=Postman

如果只想要展示在 Unity;GNOME 下可以配置:

OnlyShowIn=Unity;GNOME;
X-UnityGenerated=true

系统主题的大部分图标在该目录下:

/usr/share/icons/

reference


2019-08-30 ubuntu , linux-mint , wiznote , postman , icon , launcher

NTSC 和 PAL

之前折腾相机,剪视频的时候遇到这两个标准制式,就一直在待办事项中,这里就简单的记录一下。

制式

NTSC

NTSC 是 National Television Standard Committee,美国电视播放标准。

PAL

PAL 是 Phase Alternating Line,适用于其他国家的播放标准。

差异

播放速率

NTSC | PAL ———-|—————- 24 FPS | 24 FPS 30 FPS | 25 FPS 48 FPS | 48 FPS 60 FPS | 50 FPS

在搜索视频相关的内容时看到了有高场、地场的差别,联系到平时经常看到的一些词汇感觉到需要记录一下,这是两个经常能看到的词汇:

  • 1080i
  • 1080p

这里的 1080 都很熟悉就是视频分辨率,而这里的 ip 分别指的是

  • Interlace 隔行扫描
  • Progressive 逐行扫描

在过去电视时代,要传输图像,根据人眼的视觉残留制定出 24 帧每秒的标准,当每秒有 24 帧及以上的静止图像连续出现时,人眼就认为是动影像,但是实现这个动影像效果,就需要电视画面传输时及考虑到传输带宽,也要考虑到画面质量。所以过去人们就发明了隔行扫描和逐行扫描,隔行扫描也就是画面在传输时,隔行传输,这样可以节省一半的带宽。

常见的帧率有:

  • 24 frame/sec (film, ATSC, 2k, 4k, 6k)
  • 25 frame/sec (PAL, used in Europe, Uruguay, Argentina, Australia), SECAM, DVB, ATSC)
  • 29.97 (30 ÷ 1.001) frame/sec (NTSC American System (US, Canada, Mexico, Colombia, etc.), ATSC, PAL-M (Brazil))
  • 30 frame/sec (ATSC)

为什么有这么多的帧率呢,这就要回到过去黑白电视时代,世界上不同地区的用电频率不一样,这就涉及到传输电视画面的频率问题,美国的电力是 60Hz 所以上面提到的 NTSC 用的是 30 FPS,而其他地区,欧洲, 中国等等用的是 50 Hz 电,所以是 25 FPS 的画面。当彩色电视被发明出来,电气工程师们为了向后兼容,所以他们把彩色信号做了一个小小的偏移,轻微地将帧率从 30 FPS 改成了 30 / 1.001 = 29.97 FPS,因此诞生了 NTSC 色彩标准。

reference


2019-08-29 ntsc , pal , camera , frame

电子书

最近文章

  • 读书是否是唯一重要的事? 不久之前和朋友约去了趟植物园,聊起读书是否是第一要务的时候产生了一些分歧,关于是否要去学习如何学习这一件事情产生了一些分歧。我站在的立场是读书是必须的,而我朋友则认为在有限的时间里面实践要优先于读书。而关于要不要学习如何学习这一件事情,他仍然坚持自己的实践而非去了解如何学习。
  • Android 上的 RIME 输入法 trime 同文输入法使用 早之前就已经在 Linux 和 macOS 上配置了 RIME 并且一直使用到现在,但是在主力的 Android 上从最早的触宝输入法,后来切换成 Gboard,日常使用倒是没什么大问题,就是有一些词总是需要翻页才能找到,这让我非常不爽,就想手机上能不能用 RIME,于是就有了这篇水文。
  • Obsidian 使用篇一:使用 markdown-clipper 全文保存网页 之前使用整理 Evernote 代替品 的时候就提出了我自己的一个需求,就是有一个完善的 Web Clip 系统,Evernote 和 WizNote 都做的比较不错。但 Obsidian 并没有提供类似的工具,不过幸好 Obsidian 使用 Markdown 来管理文档,这样的开放程度使得我可以寻找一个将网页变为 Markdown 的浏览器扩展就能做到。
  • 使用了半年 macOS 之后 我又回到了 Linux 的怀抱 我在使用了半年 macOS 之后,又回到了 Linux 的怀抱,虽然 macOS 有其自身的优势,我也不否认 macOS 系统上软件生态的友好,但我发现即使我将日常开发主力机器装回到 Linux,也没有丧失操作系统的便捷性和易用性。这或许和我下意识的只使用跨平台的软件有关,并且最长使用的软件几乎都是一套快捷键。
  • 重置 macOS S.M.C 和 NVRAM 今天用得好好的电脑突然三次黑屏,两次发生在早上刚刚使用的时候,一次发生在晚上回家之后。所以一怒之下就直接上官网联系了 Apple Support,但是也不知道是不是我直接登录的 .com 网站,在我提交了 Support 之后一分钟一个外国小哥打了电话过来,我一下子没反应过来,只能用着不那么熟练的英语开始了 macOS 修复之路。