Giter Site home page Giter Site logo

blog's Introduction

Hi there, I'm Xuesong, AKA amorphobia 👋

GitHub stats

blog's People

Contributors

amorphobia avatar

Watchers

 avatar  avatar

blog's Issues

Hexo 命令笔记

搭建 blog 以来几乎没写过什么文字,导致了想要写的时候不知道 Hexo 该怎么用,然后去找官方文档,从繁杂的文档中找到自己想要的。现在就写一篇笔记来记录发新文章需要的命令吧。

新建文章

$ hexo new <title>

一般情况 <title> 就用单词加短线,在文章里再改成中文标题就好。

生成

$ hexo generate

可以加选项 -d 或者 --deploy 生成后立即部署,上传到 github.

本地服务器

$ hexo server

部署之前看看有什么毛病没有。可以加选项 -s 或者 --static 只使用生成好了的静态文件。

部署

$ hexo deploy

可以加选项 -g 或者 --generate 预先生成静态文件。

在 Windows 终端里指定 PowerShell 配置文件

问题

我在 Windows 里日常用的是 Windows Terminal 中的 PowerShell。这就有一个问题,一旦创建了配置文件,PowerShell 启动速度就会很慢,即使删除所有配置文件,启动速度也回不到从前。因此在最近一次重装系统后,我再也没搞过配置文件,我的 PowerShell 就一直保持秒开的速度。

想法

可是无配置文件的 PowerShell 的确是少了些方便,今天突发奇想,既然 PowerShell 自己读取配置很慢,那我能不能在启动它之后再指定一个配置文件,这个文件不在 PowerShell 默认的路径里,这样即使以后觉得速度太慢而删除它,也能恢复秒开。

思路

系统默认的配置文件路径分别是

  • $PSHOME\Profile.ps1
  • $PSHOME\Microsoft.PowerShell_profile.ps1
  • $Home\[My ]DocumentsPowerShell\\Profile.ps1
  • $Home\[My ]DocumentsPowerShell\\Microsoft.PowerShell_profile.ps1

挺好,那我在用户目录下创建一个 .profile.ps1 文件作为我的配置文件,就不会触发 PowerShell 的启动加载了。那么问题就变成了:

  1. 如何加载这个文件?
  2. 如何在启动后自动加载这个文件?
  3. 还有其他问题吗?

解决

中间的尝试略过的话,最后总结了一下解决方法

加载文件

由于我对 PowerShell 不熟悉,所以加载这个自定义的配置文件也算一个问题。在类 Unix 系统里加载 Shell 配置我是知道的,直接 source <file> 就成了。我来试试吧:

PS C:\Users\amorphobia> source $HOME\.profile.ps1
source : 无法将source项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径
正确,然后再试一次。
所在位置 行:1 字符: 1
+ source $HOME\.profile.ps1
+ ~~~~~~
    + CategoryInfo          : ObjectNotFound: (source:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

寄了呀!其实不试也猜得到,从来都没听说过 PowerShell 有 source 命令……那试试直接运行呢?

PS C:\Users\amorphobia> $HOME\.profile.ps1
所在位置 行:1 字符: 6
+ $HOME\.profile.ps1
+      ~~~~~~~~~~~~~
表达式或语句中包含意外的标记\.profile.ps1+ CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedToken

嗯……可能直接运行也不行吧?于是网上搜了一下,发现其实脚本是可以直接运行的,问题出现在这个 $HOME 环境变量,需要将其展开成完整的路径,才可以运行。那如果我不想展开呢?解决方案也有,就是使用 & 操作符。赶快试试吧:

PS C:\Users\amorphobia> & $HOME\.profile.ps1
PS C:\Users\amorphobia>

没有报错,而且配置生效了!撒花🎉🎉🎉

自动加载

由于我用的是 Windows 终端,能指定 PowerShell 启动时的参数,那么 PowerShell 应该也需要有类似 sh -c <cmd> 这样的命令,这回直接看看帮助吧:

PS C:\Users\amorphobia> powershell -h

PowerShell[.exe] [-PSConsoleFile <文件> | -Version <版本>]
    [-NoLogo] [-NoExit] [-Sta] [-Mta] [-NoProfile] [-NonInteractive]
    [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}]
    [-WindowStyle <样式>] [-EncodedCommand <Base64 编码命令>]
    [-ConfigurationName <字符串>]
    [-File <文件路径> <参数>] [-ExecutionPolicy <执行策略>]
    [-Command { - | <脚本块> [-args <参数数组>]
                  | <字符串> [<命令参数>] } ]
......

诶,有个 -Command 参数,讲了啥呢?

-Command
    执行指定的命令(和任何参数),就好像它们是
    在 Windows PowerShell 命令提示符下键入的一样,然后退出,除非
    指定了 NoExit。Command 的值可以为 "-"、字符串或
    脚本块。

    如果 Command 的值为 "-",则从标准输入中读取
    命令文本。

    如果 Command 的值为脚本块,则脚本块必须
    用大括号({})括起来。只有在 Windows PowerShell 中运行 PowerShell.exe 时,
    才能指定脚本块。脚本块的结果将作为反序列化的 XML 对象
    (而非活动对象)返回到父 Shell。

    如果 Command 的值为字符串,则 Command 必须是命令中的
    最后一个参数,因为在命令后面键入的所有字符
    都将解释为命令参数。

    若要编写运行 Windows PowerShell 命令的字符串,请使用以下格式:
        "& {<命令>}"
    其中,引号表示一个字符串,调用运算符(&)
    导致执行命令。

哎呀,帮助里就提示了用 & 来运行命令。好的,加到 Windows 终端的设置里吧!

...
    "profiles": 
    {
        "defaults": {},
        "list": 
        [
            {
                ...
                "commandline": "powershell.exe -Command \"& $HOME\\.profile.ps1\"",
                ...
            },
            ...
        ]
    },
...

保存,关掉 Windows 终端,再启动。咦,窗口闪了一下就没了,怎么回事呢?原来还不行,有其他问题呀。

其他问题

想想 sh -c <cmd> 这个命令,也是运行完成就退出吧,要想运行之后进入交互模式,还得用 sh -ic <cmd> 才行。那么 PowerShell 呢?看看刚才的帮助:

-NoExit
    运行启动命令后不退出。

就它了!在 Windows 终端的设置里把这玩意儿加上

...
    "profiles": 
    {
        "defaults": {},
        "list": 
        [
            {
                ...
                "commandline": "powershell.exe -NoExit -Command \"& $HOME\\.profile.ps1\"",
                ...
            },
            ...
        ]
    },
...

关掉再试试,成了!

回想一下我最初的需求,万一这东西速度慢了,不想要了,会不会和默认路径的配置文件删除后一样,速度恢复不了?既然我只是在 Windows 终端里干了这事,那就应该不会影响 PowerShell 本身吧。试了试不用 Windows 终端,直接启动 PowerShell 本体,速度依然飞快。嗯,目的达成了。

总结

想达成我的目的只需两步:

  1. 创建一个自定义配置文件,不要放到 PowerShell 默认的路径,比如我的文件是 C:\Users\amorphobia\.profile.ps1
  2. 在 Windows 终端配置里把 PowerShell 的启动命令改为 powershell.exe -NoExit -Command "& $HOME\.profile.ps1",如果直接改 Json 配置要注意引号和反斜杠的转义

完了。

关于虚岁的定义

一直以来,我对于虚岁的理解仅仅是机械地记住了“出生时记为一岁,每过一个春节加一岁”,总觉得这个定义无厘头。近期了解了一些农历,以及古代纪年的知识后,总算是了解了内在的逻辑。

简单来说,虚岁就跟古代纪年的年号是一个道理,甚至可以理解为使用你的名字来作“年号”。当然,在古代,只有皇帝才能有年号,但现代都没有皇帝了,你就意淫自己当个皇帝也无所谓的。

比如你出生后,你爸妈给你起的小名叫“狗蛋”,让你当“狗皇帝”,那么你出生那天所在的农历年就可以叫做“狗蛋元年”,第二年春节开始叫做“狗蛋二年”……以此类推。

这不就是虚岁的记法吗?🤣

龙书读书笔记 0x00

以前学习能力还行的时候看不进去这本书,现在脑子转不过来了估计更是学不会了,虽然不知道我了解编译原理有什么作用,姑且当作一个消遣吧。

文章并不是为了完整地写读书笔记,零散地记录一些我不理解的知识,以及看起来比较重要的知识,标题里方括号对应的就是书中的章节,方便我回顾的时候对照看书。

引论部分 [§1]

编译器的结构 [§1.2]

  • 分析 (analysis) 部分,即前端。
  • 综合 (synthesis) 部分,后端。
    仅看第一章还无法理解前后端分别做了什么,之后回顾的时候再补充。

环境与状态 [§1.6.2]

  1. 环境 (environment) 是从名字到储存位置的映射,或称为从名字到变量 (左值) 的映射;
  2. 状态 (state) 是从内存位置到值的映射,或称为把左值映射为右值。
    虽然暂时没看懂这两个定义,但是提到了左值和右值,可能是需要注意的地方。

上下文无关文法、BNF (Backus-Naur 范式) [§2.2]

定义相关 [§2.2.1]

上下文无关文法组成部分:

  1. 终结符号的集合,或“词法单元”
  2. 非终结符号的集合,或“语法变量”,每个非终结符号表示一个终结符号串的集合
  3. 产生式的集合,包含一个产生式头(为非终结符号),一个箭头,和一个产生式体(由终结符号及非终结符号组成的序列)
  4. 指定一个非终结符号为开始符号
    之前也看见过一些类似的文法,比如 toml 的定义 这种。大致能看懂这是在定义一些东西,不过不明白这个要怎么变成编译器的一部分。

另外,这里的“终结”不是指目标语言中的语句的终结,而是说这个定义就是它本身了,不再由其他东西产生,比如书里的例2.1:

list -> list + digit
list -> list - digit
list -> digit
digit -> [0-9] # 这里我直接用了正则来表示数字,我自认为意思是表达到了。

这个文法的终结符号就包括 +, -, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;而 listdigit 是非终结符号。因为它们可以由其他符号产生。

语法分析树 [§2.2.3]

语法分析树 (parse tree) 的性质:

  1. 根节点的标号为文法的开始符号;
  2. 每个叶子节点的标号为终结符号(或 ε);
  3. 内部节点的标号为非终结符号;
  4. 如果非终结符号 A 是某内部节点的标号,它的子节点的标号依次为 X₁, X₂, ... Xₙ,则必然存在产生式 A -> X₁X₂...Xₙ

运算优先级 [§2.2.6]

在文法中,运算符按照优先级递增的顺序排列,这里直接给出加减乘除运算符的产生式,如果有更多层优先级,可以类推。

expr -> expr + term | expr - term | term
term -> term * factor | term / factor | factor
factor -> digit | (expr)

语法分析 [§2.4]

自顶向下分析方法 [§2.4.1]

  1. 在非终结符号 A 的节点上,选择一个 A 的产生式,并为这个产生式中的每个符号构造子节点;
  2. 寻找下一个节点来构造子树。

当前被扫描的终结符号通常称为向前看 (lookahead) 符号。具体操作可以看书上的例子,比较通俗易懂。

预测分析法 [§2.4.2]

预测分析法是递归下降分析方法 (recursive-descent parsing) 的一种,Rust 的编译器就是用的递归下降分析法,书上的例子也大致描述了它是如何运作的。

关于这个站点

两年前注册了这个双拼域名,一开始是有买 VPS 打算弄个网站,而且也存在了一段时间。然而 VPS 贵,而且懒啊,所以后来就没续费了。那么,就用 Github Pages 随便搭个静态博客,一般情况下是什么都不会来写的,如果心情好可以随便发点文字。

嗯…就酱~

龙书读书笔记 0x01: 例程 2-27

上次看书看到了第二章,书中实现了一个简单的中缀到后缀的翻译器。这个例程在书中图2-27,本身是 Java 代码,如果用 C 或 C++ 来实现,那是相当简单的,几乎就是抄过来,正好我也在学习 Rust 语言,于是想到了使用 Rust 来实现书中的例程。

这个翻译器只能处理一位数字间不带括号的加减法,其产生式为:

expr -> expr + term
expr -> expr - term
term -> [0-9] # 直接使用正则来节省篇幅,就不展开了

代码在 gist 上保存了,也搬运在这里:

use std::io::{Read, Error, ErrorKind};
use std::fmt;

enum ParseError {
    SyntaxError,
    Parse(Error)
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseError::SyntaxError => write!(f, "syntax error"),
            ParseError::Parse(ref e) => e.fmt(f),
        }
    }
}

struct Parser {
    lookahead: u8,
}

impl Parser {
    pub fn new() -> Self {
        let ch = read_char().expect("Cannot read charactor from input!");
        Self {
            lookahead: ch,
        }
    }

    pub fn expr(&mut self) -> Result<(), ParseError> {
        if let Err(e) = self.term() {
            return Err(e);
        }
        loop {
            if self.lookahead == b'+' {
                if let Err(e) = self.pmatch(b'+') {
                    return Err(e);
                }
                if let Err(e) = self.term() {
                    return Err(e);
                }
                print!("+");
            } else if self.lookahead == b'-' {
                if let Err(e) = self.pmatch(b'-') {
                    return Err(e);
                }
                if let Err(e) = self.term() {
                    return Err(e);
                }
                print!("-");
            } else {
                return Ok(());
            }
        }
    }

    pub fn term(&mut self) -> Result<(), ParseError> {
        if self.lookahead.is_ascii_digit() {
            print!("{}", char::from(self.lookahead));
            self.pmatch(self.lookahead)
        } else {
            Err(ParseError::SyntaxError)
        }
    }

    pub fn pmatch(&mut self, t: u8) -> Result<(), ParseError> {
        if self.lookahead == t {
            let input = read_char();
            match input {
                Ok(ch) => {
                    self.lookahead = ch;
                    Ok(())
                },
                Err(e) => Err(ParseError::Parse(e)),
            }
        } else {
            Err(ParseError::SyntaxError)
        }
    }
}

fn read_char() -> Result<u8, Error> {
    std::io::stdin().bytes().next()
        .unwrap_or(Err(Error::new(ErrorKind::Other, "Cannot read from stdin!")))
}

fn main() {
    let mut parse = Parser::new();
    match parse.expr() {
        Ok(_) => println!(""),
        Err(e) => println!("{}", e),
    };
}

顺手也写了一个 Python 版本

import readchar

class Parser:
    def __init__(self):
        self.lookahead = readchar.readchar()

    def expr(self):
        self.term()
        while True:
            if self.lookahead == b'+':
                self.match(b'+')
                self.term()
                print('+', end='')
            elif self.lookahead == b'-':
                self.match(b'-')
                self.term()
                print('-', end='')
            else:
                return

    def term(self):
        if self.lookahead.isdigit():
            print(self.lookahead.decode("utf-8"), end='')
            self.match(self.lookahead)
        else:
            raise Exception("syntax error")

    def match(self, t):
        if self.lookahead == t:
            self.lookahead = readchar.readchar()
        else:
            raise Exception("syntax error")

if __name__ == '__main__':
    parse = Parser()
    parse.expr()
    print("")

在家庭服务器中设置 Home Assistant

记录一下是如何设置 Home Assistant 的吧。没有什么难点的部分就不写了,反正 Google 一下就找得到。

安装 Home Assistant

Google 之。

配置 Home Assistant 中的 HomeKit

最简配置,添加到 configuration.yaml 里面。

homekit:
  auto_start: False

自动启动关闭,否则 HomeKit 组件在设备还未接入完全的时候启动,在家庭 App 里就找不到设备。

相反,在 automations.yaml 里面加入以下代码,在 Home Assistant 启动五分钟后再启动 HomeKit 组件。

- id: '$IDOFAUTOMATION'
  alias: Start HomeKit
  trigger:
  - event: start
    platform: homeassistant
  condition: []
  action:
  - delay: 00:05
  - alias: ''
    data: {}
    service: homekit.start

接入设备

Yeelight 吸顶灯

Yeelight 吸顶灯可以在 App 里打开局域网控制,在启动 Home Assistant 的时候就会自动搜寻并添加,五分钟后启动 HomeKit 就能在 App 上出现了。

彩云天气

插件地址 caiyun.py。复制到 custom_components/sensor/ 里面,然后在 configuration.yaml 配置

sensor:
  - platform: caiyun

也可以顺便把默认的天气插件 yr 关掉。

米家空调伴侣 (lumi.acpartner.v2)

由于前期调查没做好,买了个不支持局域网控制协议的版本,无法将其当作网关加入 Home Assistant,只能当作空调加入。

首先获取设备的 token,参考这篇教程

然后使用这个插件添加,使用方法见其 README。

石头扫地机器人 (roborock.vacuum.s5)

获取 token 的方法与空调伴侣相同,然后参照官方教程添加到 Home Assistant。值得注意的是,扫地机器人不适用于 HomeKit 组件,有待以后研究其他解决方案。

最后看看成果吧:

Home Assistant 截图
家庭 App 截图

PowerShell 执行程序并复用终端

最近偶然得知了一个用 Free Pascal 写的编辑器 CudaText,便安装来玩玩。

轻量编辑器我一直都是使用 Sublime Text,而且重度依赖从终端命令启动编辑器打开文本,Sublime Text 就有一个官方的命令行可执行程序 subl.exe。例如当前目录下有一个文本文档 file.txt,我可以使用命令 subl file.txt 快捷打开,这个可执行程序会使用新的进程来运行 Sublime Text,因此当前的终端可以继续使用,不受影响。

当前终端无法复用

但 CudaText 就不行了,虽然它的可执行程序 cudatext.exe 也支持参数,可以便捷打开文件或目录,但打开后会持续占用当前的终端,这一点令人很不爽。若是 *nix 系统,可在命令最后加一个 & 使其在后台运行,但 Windows PowerShell 似乎没有这样的功能。

使用 Start-Process 创建新进程运行

一个简单的方法是使用系统中已有的命令 Start-Process 来运行。还是刚才的例子:

Start-Process cudatext file.txt
# 或者使用别名
start cudatext file.txt

如果你尝试了上面的命令则会发现,对于 CudaText 这类占用终端的软件,即使是 Start-Process 也会产生一个 conhost 窗口。
cudatext-with-conhost-window

所幸的是,Start-Process 支持 WindowStyle 参数,可以将其设置为 hidden,就不会显示 conhost 窗口了。

命令太长了

当然,这样的命令不适合日常使用,但我们可以写一个脚本,这样就可以使用很短的命令来运行程序了。

# run.ps1
if ($args.Length -lt 1) {
    exit 0
}

if ($args.Length -gt 1) {
    $Arguments = $args[1..($args.Length - 1)] -join ' '
    Start-Process -WindowStyle hidden $args[0] $Arguments
} else {
    Start-Process -WindowStyle hidden $args[0]
}

把这个脚本另存为 run.ps1,放到任意一个 $env:Path 目录中,就可以使用 run cudatext file.txt 快捷打开了。虽然这个脚本很短,但菜鸡如我,一开始写了一个很长的版本,边写边查给改成这样的。

对于文本编辑器这种常用程序,可以更进一步为其编写一个专用脚本

# ct.ps1
if ($args.Length -lt 1) {
    Start-Process -WindowStyle hidden cudatext.exe
} else {
    Start-Process -WindowStyle hidden cudatext.exe ($args -join ' ')
}

这样便可使用 ct file.txt 来打开了。

其他想法

在写脚本的过程中,也想过写成一个函数放到 $PROFILE 里,但始终没搞定参数的问题,只好作罢,如果有夶䎜对 PowerShell 比较了解,请不吝赐教!

保持 JBL go 2 活跃

自从我配了台式机以后,始终缺一个可靠的音箱。买过 2.0, 也买过 2.1 声道的,最终结果就是其中某个坏掉,或者整个都不行了。其实我对音箱的要求真的不高,能正常出声音、稳定、方便就行了。我也用过小米的蓝牙音箱连接电脑,可靠是可靠,就是连接不怎么方便。

后来机缘巧合入手了一个 JBL go 2 音箱,虽然是个蓝牙音箱,但同时支持 AUX 连接电脑,这才部分解决了方便性的问题。为什么说部分呢?因为这音箱有个脑残的“功能”——如果一段时间内没有播放任何声音,就会自动关机。

这个功能对于作为便携蓝牙音箱的情形还算是有用,自动关机节省电量,有需求的时候再开机。但是我是拿它当台式机音箱,充电线是始终连着的,不缺这点电呀……

解决方法

如何让这个音箱保持活跃呢?很简单,让它在关机之前播放声音就好了。通过多方搜索,我找到了 求问如何避免蓝牙音箱自动关机。? - 66666的回答 - 知乎 这个答案。

那么接下来就简单了,写了个 AHK 脚本,每分钟让系统发出一个人耳察觉不到的声音,顺利解决这个问题了!

Loop {
	RunWait, "%USERPROFILE%\scoop\apps\nircmd\current\nircmd.exe" beep 30 100 ;
	sleep, 60000 ;
}

编译成可执行文件放到启动里,电脑不关机,音箱就会一直待命了。

将文件差异导出为 HTML

一行命令完成需求

近日在处理文件差异的时候搜索到的一个方法,可以用 vim 把差异导出为 HTML (来源)

vimdiff -c TOhtml -c "w vimdiff_export.html | qa!" file1 file2

使用 vimdiff 对比两个文件,依次执行 TOhtml、保存、全部退出。


2024-04-20 更新

解决横向滚动同步问题

Vim 保存的网页在表格中使用了左右两个 <div>,可以自然地同步纵向的滚动,但横向的滚动并不同步。搜索一番后,我在 stackoverflow 上找到了这个回答。稍作修改,把如下代码放到 body 的末尾,就能同步两边的横向滚动了。

<script type="text/javascript">
var isSyncingLeftScroll = false;
var isSyncingRightScroll = false;
var leftDiv = document.getElementsByTagName('div')[0];
var rightDiv = document.getElementsByTagName('div')[1];

leftDiv.onscroll = function() {
  if (!isSyncingLeftScroll) {
    isSyncingRightScroll = true;
    rightDiv.scrollLeft = this.scrollLeft;
  }
  isSyncingLeftScroll = false;
}

rightDiv.onscroll = function() {
  if (!isSyncingRightScroll) {
    isSyncingLeftScroll = true;
    leftDiv.scrollLeft = this.scrollLeft;
  }
  isSyncingRightScroll = false;
}
</script>

缩短代码以便在 shell 脚本里完成

虽然这段代码已经够短了,但我还是把它缩得更短了:

<script>m=!1,s=!1,a=document.getElementsByTagName("div"),l=a[0],r=a[1];l.onscroll=function(){m||(s=!0,r.scrollLeft=l.scrollLeft),m=!1},r.onscroll=function(){s||(m=!0,l.scrollLeft=r.scrollLeft),s=!1}</script>

这样,在我的 shell 脚本里,只需三行就可以满足需求:

SRC='<script>m=!1,s=!1,a=document.getElementsByTagName("div"),l=a[0],r=a[1];l.onscroll=function(){m||(s=!0,r.scrollLeft=l.scrollLeft),m=!1},r.onscroll=function(){s||(m=!0,l.scrollLeft=r.scrollLeft),s=!1}</script>'
vimdiff -c TOhtml -c "w vimdiff_export.html | qa!" file1 file2
sed -i '/<\/body>/i'"${SRC}" vimdiff_export.html

龙书读书笔记 0x02: 词法分析器

距离上次看书已经过了很久了。这次看了书的 2.6 节,也花了很长时间。其实看书没花太久,因为没怎么看懂,反而实现例程用了很长时间,一方面是我没看懂书,另一方面是因为我的 Rust 写得太少了,最后写出来的代码也比较丑陋……

词法分析 (Lexical Analysis) [§2.6]

整个 2.6 节讲的就是如何进行词法分析,也就是怎么实现词法分析器。包括了剔除空白和注释、预读、字面量(书里写的是“常量”,但我感觉用字面量更好)、关键字和标识符。

词法单元 (Token)

这个是比较抽象的概念,就目前接触到的部分,我感觉词法单元就是组成语句或表达式的基本元素,比如:数字、标识符、关键字等。词法单元再分解就是单个字符了,因此我们的程序需要有能力把词法单元给提取出来。

在实现的时候,一个词法单元至少要有两个部分,其一是标签 (Tag),用来表示这是个啥,另一个是就是这个词法单元的内容,可以是值(当其为数字时)、可以是词素 (lexem) (当其为标识符时)。我在改写例程的时候定义了一个名为 Token 的 trait,然后用 Box<dyn Token> 来表示一个词法单元。代码是写出来了,但我不确定是不是用枚举来表示更好。

决定使用 trait 对象的一个原因是,书中的例程把词法单元的标签和内容(值或词素)声明为 public final,大概意思是可读,但初始化后不可写。而 Rust 没有 final 这个东西,我立马就想到了把 "getter" 公开,而不提供 "setter",只在关联函数 new 里初始化,而枚举好像不能这样。

4月5日更新

参考了一个 Rust 写的 c11 词法分析器 c_lexer,用枚举应该是更好的选择,于是我重新写了一遍这节的练习,也更改了其他一些地方,比如书中对于符号的处理,是直接把字符对应的 ASCII 码作为其标签,我参照 c_lexer 给这一节涉及到的符号添加了枚举变体。

剔除空白和注释 [§2.6.1]

对于空白来讲,其它的都好说,遇到就跳过,但换行需要特别注意。首先是换行需要把行号加一,其次是 Windows 的换行是 \r\n 而不是 \n,需要特殊处理(书中没处理)。

剔除注释在书上没给例程,留作练习 2.6.1 了,在我的代码里也丑陋地实现了。

预读 [§2.6.2]

我们读取代码的方式是一个字符一个字符地读,所以在遇到 <=< 这种符号的时候,必须预读下一个字符才能确定,这很好理解。而在实现的时候,我用了 Rust 里的 Peekable 来帮助我在不移动迭代器的情况下读取下一个字符。而书中也讲了,今后会用到一个缓冲技术来实现预读。

4月5日更新

c_lexer 使用了状态机来做预读,我看了看目录,应该会在第三章讲到,现在就还是用老方法来做。不过我稍微改了一下,增加使用回移“指针”来实现一个缓冲区。

字面量 [§2.6.3]

这节接触到的字面量只有无符号整数,在练习 2.6.3 会要求扩展到浮点数,但我还没做。对于无符号整数,词法单元里没有词素,而是直接保存了它的值。在我的代码里是用了一个结构体,实现 Token 这个 trait 来表示数字。

关键字和标识符 [§2.6.4]

书中用了一个散列表来储存所有关键字和用到的标识符。我遇到的问题主要是 HashMap 的键 (K) 以及值 (V) 中的词素 (lexem) 用什么类型。在 Java 里面太好办了,有垃圾回收,用字符串就好了;但 Rust 里如果用字符串切片的引用,就要考虑生命周期和所有权的问题,到底是键持有字符串,还是值里的词素持有字符串呢?我想了半天没想明白,也不知道怎么实现把字符串移动进 HashMap 的键/值的同时,拿到它的引用,并放到同一项的值/键里……最后我放弃使用字符串切片的引用,让所有地方都持有一个字符串了,等以后知识储备够了再来解决。

4月5日更新

在 c_lexer 里用了 internship 里的 IStr 储存标识符的字符串部分,这就相当于有一个全局的 HashSet<Rc<str>>,但只需当作 String 用就行了。如此一来,就不需要用 HashMap 来储存非关键字的标识符,因为其字符串部分已经用 IStr 实现了全局(线程内)唯一。

另一个改变是对于关键字,不用 HashMap,而用了一个完美散列 phfMap。好处是有一个宏 phf_map 可以方便地添加所有关键字,而不需要在初始化时一次次调用 reserve 来储存。

词法分析器 (Lexical Analyzer) [§2.6.5]

英文名也叫 lexer,就是这一节的内容合起来实现的东西。读书的时候我是没读懂,写代码的时候也似懂非懂,写笔记的时候对照代码再顺了几遍稍微有点理解了。

代码

代码在此:lexer.rs

目前代码只做到了例程(书中图 2-34 + 图 2-35)和练习 2.6.1 的内容。之后再继续把练习 2.6.2 和 2.6.3 也做了,就直接更新 gist 了。

4月5日更新

新的代码实现了所有练习,就不放在 gist 了,我搞了个仓库:dragon-book-lexer。跟之前的比起来,多了 lex 方法,可以一次次调用 scan,最终生成词法单元的向量。

为 eMule (community) 编译中文翻译动态库

社区版 eMule 可以在 https://github.com/irwir/eMule/releases 页面下载,虽然源代码里有中文翻译的资源,但发布的二进制文件并没有将其编译,因此需要自行编译来使用。

因为我一般只安装 Visual Studio 生成工具,所以记录一下我用命令行编译的方法。如果没有安装 MFC 相关模块,则需要把 zh_CN.rc 里引用 afxres.h 改为 引用 Windows.hwinres.h1。然后,使用命令 msbuild zh_CN.vcxproj /p:configuration=Dynamic /p:platform=x64 来编译2

Footnotes

  1. https://web.archive.org/web/20240406120953/https://blog.csdn.net/duiwangxiaomi/article/details/88822702

  2. https://learn.microsoft.com/cpp/build/walkthrough-using-msbuild-to-create-a-visual-cpp-project?view=msvc-170#using-msbuild-with-build-properties

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.