Java使用ANTLR4对Lua脚本语法校验

文章目录

  • 什么是ANTLR?
  • 第一个例子
  • ANTLR4 的工作流程
  • Lua脚本语法校验
    • 准备一个Lua Grammar文件
    • maven配置
    • 新建实体类
    • Lua语法遍历器
    • 语法错误监听器
    • 单元测试
  • 参考

什么是ANTLR?

https://www.antlr.org/

ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.

ANTLR(ANother Tool for Language Recognition)是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。 它被广泛用于构建语言、工具和框架。ANTLR 根据语法定义生成解析器,解析器可以构建和遍历解析树。

第一个例子

https://github.com/antlr/antlr4/blob/master/doc/getting-started.md#a-first-example

  1. 新建个Hello.g4文件:
// Define a grammar called Hello
grammar Hello;
r  : 'hello' ID ;         // match keyword hello followed by an identifier
ID : [a-z]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
  1. 安装IDEA插件
    ANTLR v4:https://plugins.jetbrains.com/plugin/7358-antlr-v4

  2. 打开ANTLR Preview
    r : 'hello' ID ; // match keyword hello followed by an identifier这行上右键,点击Test Rule r
    输入hello world,能够准确识别出ID为word。
    在这里插入图片描述
    输入hello World,就不能够识别出ID为world了。
    在这里插入图片描述

ANTLR4 的工作流程

  • 词法分析器 (Lexer) :将字符序列转换为单词(Token)的过程。词法分析器(Lexer)一般是用来供语法解析器(Parser)调用的。
  • 语法解析器 (Parser) :通常作为编译器或解释器出现。它的作用是进行语法检查,并构建由输入单词(Token)组成的数据结构(即抽象语法树)。语法解析器通常使用词法分析器(Lexer)从输入字符流中分离出一个个的单词(Token),并将单词(Token)流作为其输入。实际开发中,语法解析器可以手工编写,也可以使用工具自动生成。
  • 抽象语法树 (Parse Tree) :是源代码结构的一种抽象表示,它以树的形状表示语言的语法结构。抽象语法树一般可以用来进行代码语法的检查,代码风格的检查,代码的格式化,代码的高亮,代码的错误提示以及代码的自动补全等。
    在这里插入图片描述
    如上左边的点线流程代表了通过 ANTLR4,将原始的.g4 规则转化为 Lexer、Parser、Listener 和 Visitor。右边的虚线流程代表了将原始的输入流通过 Lexer 转化为 Tokens,再将 Tokens 通过 Parser 转化为语法树,最后通过 Listener 或 Visitor 遍历 ParseTree 得到最终结果。

Lua脚本语法校验

准备一个Lua Grammar文件

https://github.com/antlr/grammars-v4/tree/master/lua

/*
BSD LicenseCopyright (c) 2013, Kazunori Sakamoto
Copyright (c) 2016, Alexander Alexeev
All rights reserved.Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:1. Redistributions of source code must retain the above copyrightnotice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyrightnotice, this list of conditions and the following disclaimer in thedocumentation and/or other materials provided with the distribution.
3. Neither the NAME of Rainer Schuster nor the NAMEs of its contributorsmay be used to endorse or promote products derived from this softwarewithout specific prior written permission.THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.This grammar file derived from:Lua 5.3 Reference Manualhttp://www.lua.org/manual/5.3/manual.htmlLua 5.2 Reference Manualhttp://www.lua.org/manual/5.2/manual.htmlLua 5.1 grammar written by Nicolai Mainierohttp://www.antlr3.org/grammar/1178608849736/Lua.gTested by Kazunori Sakamoto with Test suite for Lua 5.2 (http://www.lua.org/tests/5.2/)Tested by Alexander Alexeev with Test suite for Lua 5.3 http://www.lua.org/tests/lua-5.3.2-tests.tar.gz
*/grammar Lua;chunk: block EOF;block: stat* retstat?;stat: ';'| varlist '=' explist| functioncall| label| 'break'| 'goto' NAME| 'do' block 'end'| 'while' exp 'do' block 'end'| 'repeat' block 'until' exp| 'if' exp 'then' block ('elseif' exp 'then' block)* ('else' block)? 'end'| 'for' NAME '=' exp ',' exp (',' exp)? 'do' block 'end'| 'for' namelist 'in' explist 'do' block 'end'| 'function' funcname funcbody| 'local' 'function' NAME funcbody| 'local' attnamelist ('=' explist)?;attnamelist: NAME attrib (',' NAME attrib)*;attrib: ('<' NAME '>')?;retstat: 'return' explist? ';'?;label: '::' NAME '::';funcname: NAME ('.' NAME)* (':' NAME)?;varlist: var_ (',' var_)*;namelist: NAME (',' NAME)*;explist: exp (',' exp)*;exp: 'nil' | 'false' | 'true'| number| string| '...'| functiondef| prefixexp| tableconstructor| <assoc=right> exp operatorPower exp| operatorUnary exp| exp operatorMulDivMod exp| exp operatorAddSub exp| <assoc=right> exp operatorStrcat exp| exp operatorComparison exp| exp operatorAnd exp| exp operatorOr exp| exp operatorBitwise exp;prefixexp: varOrExp nameAndArgs*;functioncall: varOrExp nameAndArgs+;varOrExp: var_ | '(' exp ')';var_: (NAME | '(' exp ')' varSuffix) varSuffix*;varSuffix: nameAndArgs* ('[' exp ']' | '.' NAME);nameAndArgs: (':' NAME)? args;/*
var_: NAME | prefixexp '[' exp ']' | prefixexp '.' NAME;prefixexp: var_ | functioncall | '(' exp ')';functioncall: prefixexp args | prefixexp ':' NAME args;
*/args: '(' explist? ')' | tableconstructor | string;functiondef: 'function' funcbody;funcbody: '(' parlist? ')' block 'end';parlist: namelist (',' '...')? | '...';tableconstructor: '{' fieldlist? '}';fieldlist: field (fieldsep field)* fieldsep?;field: '[' exp ']' '=' exp | NAME '=' exp | exp;fieldsep: ',' | ';';operatorOr: 'or';operatorAnd: 'and';operatorComparison: '<' | '>' | '<=' | '>=' | '~=' | '==';operatorStrcat: '..';operatorAddSub: '+' | '-';operatorMulDivMod: '*' | '/' | '%' | '//';operatorBitwise: '&' | '|' | '~' | '<<' | '>>';operatorUnary: 'not' | '#' | '-' | '~';operatorPower: '^';number: INT | HEX | FLOAT | HEX_FLOAT;string: NORMALSTRING | CHARSTRING | LONGSTRING;// LEXERNAME: [a-zA-Z_][a-zA-Z_0-9]*;NORMALSTRING: '"' ( EscapeSequence | ~('\\'|'"') )* '"';CHARSTRING: '\'' ( EscapeSequence | ~('\''|'\\') )* '\'';LONGSTRING: '[' NESTED_STR ']';fragment
NESTED_STR: '=' NESTED_STR '='| '[' .*? ']';INT: Digit+;HEX: '0' [xX] HexDigit+;FLOAT: Digit+ '.' Digit* ExponentPart?| '.' Digit+ ExponentPart?| Digit+ ExponentPart;HEX_FLOAT: '0' [xX] HexDigit+ '.' HexDigit* HexExponentPart?| '0' [xX] '.' HexDigit+ HexExponentPart?| '0' [xX] HexDigit+ HexExponentPart;fragment
ExponentPart: [eE] [+-]? Digit+;fragment
HexExponentPart: [pP] [+-]? Digit+;fragment
EscapeSequence: '\\' [abfnrtvz"'\\]| '\\' '\r'? '\n'| DecimalEscape| HexEscape| UtfEscape;fragment
DecimalEscape: '\\' Digit| '\\' Digit Digit| '\\' [0-2] Digit Digit;fragment
HexEscape: '\\' 'x' HexDigit HexDigit;fragment
UtfEscape: '\\' 'u{' HexDigit+ '}';fragment
Digit: [0-9];fragment
HexDigit: [0-9a-fA-F];COMMENT: '--[' NESTED_STR ']' -> channel(HIDDEN);LINE_COMMENT: '--'(                                               // --| '[' '='*                                      // --[==| '[' '='* ~('='|'['|'\r'|'\n') ~('\r'|'\n')*   // --[==AA| ~('['|'\r'|'\n') ~('\r'|'\n')*                // --AAA) ('\r\n'|'\r'|'\n'|EOF)-> channel(HIDDEN);WS: [ \t\u000C\r\n]+ -> skip;SHEBANG: '#' '!' ~('\n'|'\r')* -> channel(HIDDEN);

maven配置

使用JDK8的注意:antlr4最高版本为4.9.3,原因如下:
来源:https://github.com/antlr/antlr4/releases/tag/4.10

Increasing minimum java version
Going forward, we are using Java 11 for the source code and the compiled .class files for the ANTLR tool. The Java runtime target, however, and the associated runtime tests use Java 8 (bumping up from Java 7).

<dependencies><dependency><groupId>org.antlr</groupId><artifactId>antlr4-runtime</artifactId><version>${antlr.version}</version></dependency>
</dependencies><build><plugins><plugin><groupId>org.antlr</groupId><artifactId>antlr4-maven-plugin</artifactId><version>${antlr.version}</version><configuration><visitor>true</visitor><listener>true</listener></configuration><executions><execution><goals><goal>antlr4</goal></goals></execution></executions></plugin></plugins>
</build><properties><!--https://mvnrepository.com/artifact/org.antlr/antlr4-runtime--><antlr.version>4.9.3</antlr.version><mojo.version>3.0.0</mojo.version>
</properties>

新建实体类

语法错误:每行有什么错误。

package com.baeldung.antlr.lua.model;/*** 语法错误** @author duhongming* @see* @since 1.0.0*/
public class SyntaxErrorEntry {private Integer lineNum;private String errorInfo;public Integer getLineNum() {return lineNum;}public void setLineNum(Integer lineNum) {this.lineNum = lineNum;}public String getErrorInfo() {return errorInfo;}public void setErrorInfo(String errorInfo) {this.errorInfo = errorInfo;}
}

语法错误报告:每行有什么错误的集合。

package com.baeldung.antlr.lua.model;import java.util.LinkedList;
import java.util.List;/*** 语法错误报告** @author duhongming* @see* @since 1.0.0*/
public class SyntaxErrorReportEntry {private final List<SyntaxErrorEntry> syntaxErrorList = new LinkedList<>();public void addError(int line, int charPositionInLine, Object offendingSymbol, String msg) {SyntaxErrorEntry syntaxErrorEntry = new SyntaxErrorEntry();syntaxErrorEntry.setLineNum(line);syntaxErrorEntry.setErrorInfo(line + "行," + charPositionInLine + "列," + offendingSymbol + "字符处,存在语法错误:" + msg);syntaxErrorList.add(syntaxErrorEntry);}public List<SyntaxErrorEntry> getSyntaxErrorReport() {return syntaxErrorList;}
}

Lua语法遍历器

package com.baeldung.antlr.lua;import com.baeldung.antlr.LuaParser;
import com.baeldung.antlr.LuaVisitor;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;/*** Lua语法遍历器** @author duhongming* @see* @since 1.0.0*/
public class LuaSyntaxVisitor implements LuaVisitor<Object> {
// ctrl+O Override即可
}

语法错误监听器

package com.baeldung.antlr.lua;import com.baeldung.antlr.lua.model.SyntaxErrorReportEntry;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;/*** 语法错误监听器** @author duhongming* @see* @since 1.0.0*/
public class SyntaxErrorListener extends BaseErrorListener {private final SyntaxErrorReportEntry reporter;public SyntaxErrorListener(SyntaxErrorReportEntry reporter) {this.reporter = reporter;}@Overridepublic void syntaxError(Recognizer<?, ?> recognizer,Object offendingSymbol, int line, int charPositionInLine,String msg, RecognitionException e) {this.reporter.addError(line, charPositionInLine, offendingSymbol, msg);}
}

单元测试

package com.baeldung.antlr;import com.baeldung.antlr.lua.LuaSyntaxVisitor;
import com.baeldung.antlr.lua.SyntaxErrorListener;
import com.baeldung.antlr.lua.model.SyntaxErrorEntry;
import com.baeldung.antlr.lua.model.SyntaxErrorReportEntry;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.Test;import java.util.List;import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;public class LuaSyntaxErrorUnitTest {public static List<SyntaxErrorEntry> judgeLuaSyntax(String luaScript) {//新建一个CharStream,读取数据CharStream charStreams = CharStreams.fromString(luaScript);//包含一个词法分析器的定义,作用是将输入的字符序列聚集成词汇符号。LuaLexer luaLexer = new LuaLexer(charStreams);//新建一个词法符号的缓冲区,用于存储词法分析器生成的词法符号(Token)CommonTokenStream tokenStream = new CommonTokenStream(luaLexer);//新建一个语法分析器,用于分析词法符号缓冲区中的词法符号LuaParser luaParser = new LuaParser(tokenStream);SyntaxErrorReportEntry syntaxErrorReporter = new SyntaxErrorReportEntry();SyntaxErrorListener errorListener = new SyntaxErrorListener(syntaxErrorReporter);luaParser.addErrorListener(errorListener);LuaSyntaxVisitor luaSyntaxVisitor = new LuaSyntaxVisitor();luaSyntaxVisitor.visit(luaParser.chunk());return syntaxErrorReporter.getSyntaxErrorReport();}@Testpublic void testGood() throws Exception {List<SyntaxErrorEntry> errorEntryList = judgeLuaSyntax("if a~=1 then print(1) end");assertThat(errorEntryList.size(), is(0));}@Testpublic void testBad() throws Exception {//新建一个CharStream,读取数据List<SyntaxErrorEntry> errorEntryList = judgeLuaSyntax("if a!=1 then print(1) end");assertThat(errorEntryList.size(), is(2));}
}

最终目录情况,及单元测试情况!
在这里插入图片描述

参考

https://www.baeldung.com/java-antlr
https://juejin.cn/post/7018521754125467661
https://www.nosuchfield.com/2023/08/26/ANTLR4-from-Beginning-to-Practice/
https://blog.csdn.net/qq_37771475/article/details/106387201
https://blog.csdn.net/qq_37771475/article/details/106426327

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/901388.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

观察者模式(行为模式)

观察者模式 观察者模式属于行为模式&#xff0c;个人理解&#xff1a;和发布订阅者魔模式是有区别的 细分有两种&#xff1a;推模式和拉模式两种&#xff0c;具体区别在于推模式会自带推送参数&#xff0c;拉模式是在接收通知后要自己获取更新参数 观察者模式&#xff08;Obs…

内网渗透 --- 之杀软工具探测

目录 内网杀软探测与应对实战方案 一、总体思路 二、探测阶段——杀软工具与手法 2.1 进程与服务检测 2.2 注册表与文件系统检测 2.3 Nmap 与 NSE 脚本扫描 三、处理阶段——探测到杀软后的应对措施 3.1 分析评估 3.2 应对策略 四、判断与验证——注入 webshell 后如…

(2025亲测可用)Chatbox多端一键配置Claude/GPT/DeepSeek-网页端配置

1. 资源准备 API Key&#xff1a;此项配置填写在一步API官网创建API令牌&#xff0c;一键直达API令牌创建页面创建API令牌步骤请参考API Key的获取和使用API Host&#xff1a;此项配置填写https://yibuapi.com/v1查看支持的模型请参考这篇教程模型在线查询 2. ChatBox网页版配…

【Pandas】pandas DataFrame keys

Pandas2.2 DataFrame Indexing, iteration 方法描述DataFrame.head([n])用于返回 DataFrame 的前几行DataFrame.at快速访问和修改 DataFrame 中单个值的方法DataFrame.iat快速访问和修改 DataFrame 中单个值的方法DataFrame.loc用于基于标签&#xff08;行标签和列标签&#…

Redis存储“大数据对象”的常用策略及StackOverflowError错误解决方案

Hi&#xff0c;大家好&#xff0c;我是灰小猿&#xff01; 在一些功能的开发中&#xff0c;我们一般会有一些场景需要将得到的数据先暂时的存储起来&#xff0c;以便后面的接口或业务使用&#xff0c;这种场景我们一般常用的场景就是将数据暂时存储在缓存中&#xff0c;之后再…

【Python】读取xyz坐标文件输出csv文件

Python读取xyz坐标文件输出csv文件 import sys import numpy as np import pandas as pd from tqdm import tqdm import cv2 import argparsedef read_xyz(file_path):with open(file_path, "r") as f: # 打开文件data f.readlines() # 读取文件datas []for …

leetcode 139. Word Break

这道题用动态规划解决。 class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {unordered_set<string> wordSet;for(string& word:wordDict){wordSet.insert(word);}int s_len s.size();//s的下标从1开始起算&#xff0c;dp[j]…

驱动开发硬核特训 · Day 11(下篇):从 virtio_blk 看虚拟总线驱动模型的真实落地

&#x1f50d; B站相应的视屏教程&#xff1a; &#x1f4cc; 内核&#xff1a;博文视频 - 总线驱动模型实战全解析 敬请关注&#xff0c;记得标为原始粉丝。 &#x1f527; 在上篇中&#xff0c;我们已经从理论视角分析了“虚拟总线驱动模型”在 Linux 驱动体系中的独特定位。…

音视频转换器 AV 接口静电保护方案

方案简介 音视频转换器是将音视频&#xff08;AV&#xff09;信号转换成其他格式或信号类型的设备或软件。 它能够实现大多数视频、音频以及图像格式之间的转换&#xff0c;包括但不限于 RMVB、AVI、 MP4、MOV 等常见格式&#xff0c;同时也支持将不同采样率、位深度、声道数…

AI agents系列之全从零开始构建

在我们上一篇博客文章中&#xff0c;我们全面介绍了智能代理&#xff0c;讨论了它们的特性、组成部分、演变过程、面临的挑战以及未来的可能性。 这篇文章&#xff0c;咱们就来聊聊怎么用 Python 从零开始构建一个智能代理。这个智能代理能够根据用户输入做出决策&#xff0c;…

【Python爬虫】详细工作流程以及组成部分

目录 一、Python爬虫的详细工作流程 确定起始网页 发送 HTTP 请求 解析 HTML 处理数据 跟踪链接 递归抓取 存储数据 二、Python爬虫的组成部分 请求模块 解析模块 数据处理模块 存储模块 调度模块 反爬虫处理模块 一、Python爬虫的详细工作流程 在进行网络爬虫工…

Kotlin 集合过滤全指南:all、any、filter 及高级用法

在 Kotlin 中&#xff0c;集合过滤是数据处理的核心操作之一。无论是简单的条件筛选&#xff0c;还是复杂的多条件组合&#xff0c;Kotlin 都提供了丰富的 API。本文将详细介绍 filter、all、any、none 等操作符的用法&#xff0c;并展示如何在实际开发中灵活运用它们。 1. 基础…

爬虫:一文掌握 curl-cffi 的详细使用(支持 TLS/JA3 指纹仿真的 cURL 库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、curl-cffi 概述1.1 curl-cffi介绍1.2 主要特性1.3 适用场景1.4 使用 curl-cffi 的注意事项1.5 与 requests 和 pycurl 对比1.6 curl-cffi 的安装二、基本使用2.1 同步请求2.2 异步请求三、高级功能3.1 模拟浏览器指…

AllData数据中台升级发布 | 支持K8S数据平台2.0版本

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨杭州奥零数据科技官网&#xf…

dnf install openssl失败的原因和解决办法

网上有很多编译OpenSSL源码(3.x版本)为RPM包的文章&#xff0c;这些文章在安装RPM包时都是执行rpm -ivh openssl-xxx.rpm --nodeps --force 这个命令能在缺少依赖包的情况下能强行执行安装 其实根据Centos的文档&#xff0c;安装RPM包一般是执行yum install或dnf install。后者…

从入门到进阶:React 图片轮播 Carousel 的奇妙世界!

全文目录&#xff1a; 开篇语&#x1f590; 前言✨ 目录&#x1f3af; 什么是图片轮播组件&#xff1f;&#x1f528; 初识 React 中的轮播实现示例代码分析 &#x1f4e6; 基于第三方库快速实现轮播示例&#xff1a;用 react-slick优势局限性 &#x1f6e0;️ 自己动手实现一个…

2025第十六届蓝桥杯PythonB组部分题解

一、攻击次数 题目描述 小蓝操控三个英雄攻击敌人&#xff0c;敌人初始血量2025&#xff1a; 第一个英雄每回合固定攻击5点第二个英雄奇数回合攻击15点&#xff0c;偶数回合攻击2点第三个英雄根据回合数除以3的余数攻击&#xff1a;余1攻2点&#xff0c;余2攻10点&#xff0…

新手宝塔部署thinkphp一步到位

目录 一、下载对应配置 二、加载数据库 三、添加FTP​ 四、上传项目到宝塔​ 五、添加站点​ 六、配置伪静态 七、其他配置 开启监控 八、常见错误 一、打开宝塔页面&#xff0c;下载对应配置。 二、加载数据库 从本地导入数据库文件 三、添加FTP 四、上传项目到宝塔…

2025年,HarmonyOS认证学习及考试

HarmonyOS应用开发者认证考试 基础认证 通过系统化的课程学习&#xff0c;熟练掌握 DevEco Studio&#xff0c;ArkTS&#xff0c;ArkUI&#xff0c;预览器&#xff0c;模拟器&#xff0c;SDK 等 HarmonyOS 应用开发的关键概念&#xff0c;具备基础的应用开发能力。 高级认证…

3-1 Git分布式版本控制特性探讨

Git 的分布式版本控制特性是其核心优势之一,它使 Git 在版本管理方面具有高度的灵活性、可靠性和高效性。以下从多个方面来理解这一特性: 分布式存储 在 Git 中,每个开发者的本地机器上都拥有完整的版本库,包含了项目的所有历史记录和元数据。这与集中式版本控制系统(如…