0%

Windows CMD 脚本学习,实现 Markdown Table 解析

命令提示符

命令提示符是在操作系统中,提示进行命令输入的一种工作提示符。在不同的操作系统环境下,命令提示符各不相同。在windows环境下,命令行程序为cmd.exe,是一个32位的命令行程序(在64位系统中 cmd.exe 也存在于 SysWOW64 目录,WOW64 是 x86 模拟器,允许基于 Windows 的 32 位应用程序在 64 位 Windows 上运行),微软Windows系统基于Windows上的命令解释程序,类似于微软的DOS操作系统(可惜,我这个年代的人都没见过DOS~)。

批处理文件

批处理文件是无格式的文本文件,它包含一条或多条命令。它的文件扩展名为 .bat 或 .cmd。在命令提示下输入批处理文件的名称,或者双击该批处理文件,系统就会调用 cmd.exe 按照该文件中各个命令出现的顺序来逐个运行它们。使用批处理文件(也被称为批处理程序或脚本),可以简化日常或重复性任务。

BAT实现些小功能

echo 和 @

Cmd中的echo命令通过echo /?可以看介绍。

用于显示消息:ECHO [message],(注意:echo不会去message引号)
启用或关闭命令回显:ECHO [ON | OFF],(提一句:Cmd命令不区分大小写)

还有个关回显的方式:@表示本条命令不回显,仅在本条命令生效,优先级高于echo off。

设变量

和Shell不同,Cmd中设变量需要加set,如set VariableName=100,等号两边不应有空格。设好变量后可用echo %VariableName%来打印刚才设置的变量。

如果要看系统所有的环境变量可以在Cmd中输入set就会打印到终端里,如果要保存到文件中可以set > Variable.txt>和Shell一样,可以将左边命令输出重定向到指定文件,用>>就是追加写入。

系统中有很多自带的环境变量,下面几个是我感觉可能会用到的:

  • %CD% 本地 返回当前目录字符串。
  • %USERNAME% 本地 返回当前登录的用户的名称。
  • %DATE% 系统 返回当前日期。使用与 date /t 命令相同的格式。
  • %ERRORLEVEL% 系统 返回上一条命令的错误代码。(通常用非零值表示错误)
  • %PATH% 系统 指定可执行文件的搜索路径。(有多个版本Gcc工具链可以通过设这个环境变量再指定,越在前优先级越高,可以把要用的加在前面)
  • %WINDIR% 系统 返回操作系统目录的位置。

值得思考:变量来自哪里?不同地方定义的环境变量哪个优先级高?

发现异常

用上面提到的ERRORLEVEL环境变量可以知道上一次命令的返回结果,成功为0(默认值),失败非0。可以根据这个变量去做命令执行失败的处理。

变长参数解析

配合shift命令,将参数一个个解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo Usage: build.bat [--relese ^<LOG_FILE^>] [make target ...]
:parse-args
rem 检测到没有下个参数,跳到编译
if "%1"=="" ( goto pre-build1 )
if "%1"=="--release" (
rem 解析到--release,还需再读一个后面跟的文件地址
set RELEASE_FLAG=1
set SVN_LOG_FILE=%RUN_PATH%\%2
shift
) else (
rem 其他参数都归自定义make参数
set CUSTOM_MAKE_ARGS=%CUSTOM_MAKE_ARGS% %1
)
rem shift更改参数的位置
shift
goto parse-args

传地址参数

脚本经常会遇到传地址参数,比如我的–release参数后面就要跟一个LOG_FILE的参数,需要传递文件路径。

传进来的相对参数正常是基于脚本启动路径,脚本里如果有切换执行路径,在使用地址参数时要加这个路径偏移。可以在执行脚本刚开始记录启动路径(绝对路径)set RUN_PATH=%cd%,后面在解析文件参数时加上启动路径set SVN_LOG_FILE=%RUN_PATH%\%2

这样可以解决相对路径的传参问题,但是绝对路径无法解决,Cmd脚本如何识别传入的路径是相对or绝对?

在Windows中,绝对路径组成有:驱动器号:\目录名\目录名\...\文件名,可以通过识别有无:来判断是否为绝对路径。

丢弃异常输出

在Windows命令行脚本中,> nul 是一种将命令输出重定向到空设备的方式。这通常用于抑制命令的输出,使其不显示在命令行窗口中。通常在删除时不报找不到文件错可以加,让异常输出不显示使用 2 > nul

1
2
3
4
5
6
7
set FILE_PATH=%1
echo %FILE_PATH% | find ":" > nul
if errorlevel 1 (
echo 传入的路径为相对路径
) else (
echo 传入的路径为绝对路径
)

延迟拓展

在cmd执行命令前会对脚本进行预处理,其中有一个过程是变量识别过程,在这个过程中,如果有两个%括起来的如%value%类似这样的变量,就会对其进行识别,并且查找这个变量对应的值,再而将值替换掉这个变量,这个替换值的过程,就叫做变量扩展,然后再执行命令。

1
2
3
@echo on
set a=4
set a=5& echo %a%

Cmd脚本会逐行预处理(用到括号会被当成一行,开@echo on可以看到),上面脚本在预处理时第三行会被变量拓展为set a=5& echo 4,所以就算设置了a=5,打印内容已经被拓展了还是4。

1
2
3
4
@echo on
setlocal enabledelayedexpansion
set a=4
set a=5& echo !a!

设置本地开启延时拓展:setlocal enabledelayedexpansion,设置本地关闭延时拓展:setlocal disabledelayedexpansion。这两个也相当于命令,需要运行到才会起作用。如果不开启延时拓展,使用到的延时拓展变量不会被拓展echo的内容为!a!

开启延时拓展后,要被延时拓展的变量要用一对叹号!value!括起来,上面脚本在预处理后为set a=5&echo !a!,在执行echo时才会解析变量a,打印的内容是5。

小技巧:可以开启延时拓展后使用echo %value% !value!看是否一致,如果一致可以不使用延时拓展该变量(最好还是理解原理)。

调子程序

子程序写法:

1
2
3
4
5
:getSum
set /a sum+=%1
shift /1
if not "%1"=="" goto getSum
goto :eof

调用方法:

1
2
3
set sum=0
call:getSum 1 2 3
echo %sum%

输出:6

脚本不用像C一样先声明后使用,子程序可以写在调用的下方。

条件语句

语法格式:if [not] <条件> (执行语句),注意:执行语句如果用括号,左括号和条件间必须有个空格。

多个条件与:if [not] <条件> [if [not] <条件> ...] (执行语句)

多个条件或(写法比较不优雅):

1
2
3
4
5
6
if [not] <条件> goto do-something
if [not] <条件> goto do-something
...
:do-something
执行语句
...

循环语句

Windows bat脚本的for语句基本形态如下:
在cmd窗口中:for %I in (command1) do command2
在批处理文件中:for %%I in (command1) do command2

for、in、do是for语句的关键字,它们三个缺一不可。%%I是for语句中对形式变量的引用,即使变量I在do后的语句中没有参与语句的执行,也是必须出现的。in之后,do之前的括号不能省略。command1表示字符串或变量,command2表示字符串、变量或命令语句。具体使用可以看下面的BAT解析 MarkdownTable。

脱引号(转载

很多情况下,我们需要脱除一个字符串中可能会存在的引号,然后在加上自己的引号使其中的特殊字符(命令连接符& 、| 、&&、||,命令行参数界定符Space 、tab 、 ; 、= ,字符化转义符^ 、” ,变量化转义符%等)字符化,失去特定的作用,而作为普通的字符成为字符串的一个组成部分。

如果字符串存在于命令行参数%1中,可以使用%~1脱去第一对外侧引号,如果没有外侧引号则字符串不变。

如果字符串存在于for替代变量%%i中,可以使用%%~i脱去第一对外侧引号,如果没有外侧引号则字符串不变。

如果字符串存在于环境变量%temp%中,可以使用%temp:"=%脱去其中所有的引号,如果没有引号则字符串不变。

BAT解析 MarkdownTable

解析 MarkdownTable 子程序:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
::==============================Parse Markdown Function============================
rem 定位Markdown中相应标题所在起始行和结束行
:locateMarkdownTitleFunc
echo Entering Function %0
echo inFileName %1
rem titleName需加引号
echo titleName %2

set startLineNum=
set endLineNum=

rem 查找所有#开头的标题行
for /F "tokens=*" %%i in ('findstr /i /n "^#" %1') do (
set "line=%%i"
rem 解析标题行的行号和内容
for /F "tokens=1* delims=:" %%j in ("!line!") do (
set linenum=%%j
set "linestr=%%k"
rem 已有起始行 没有结束行,设置结束行
if "!startLineNum!" neq "" if "!endLineNum!" equ "" (
set endLineNum=!linenum!
)
rem 去标题内容首部#和空格
for /F "tokens=* delims=#" %%a in ("!linestr!") do set "linestr=%%a"
for /F "tokens=* delims= " %%a in ("!linestr!") do set "linestr=%%a"
rem 标题名匹配 没有起始行,设置起始行(多个匹配使用第一个)
if /i "!linestr!" equ %2 if "!startLineNum!" equ "" (
set startLineNum=!linenum!
)
)
)
rem 有起始行没有结束行则取文件总行数+1作为结束行
if "%startLineNum%" neq "" if "!endLineNum!" equ "" (
for /F %%a in ('find /c /v "" ^< %1') do set /a endLineNum=%%a+1
)
echo Leaving Function %0
goto:eof

rem 解析README中的Repo Link表格,设环境变量
:parseRepoLinkFunc
echo Entering Function %0
echo inFileName %1
echo argsSuffix %2

rem 定位Markdown中Repo Link标题所在起始行和结束行
call:locateMarkdownTitleFunc %1 "Repo link"
set RepoLinkStartLineNum=%startLineNum%
set RepoLinkEndLineNum=%endLineNum%
echo RepoLinkStartLineNum=%RepoLinkStartLineNum%
echo RepoLinkEndLineNum=%RepoLinkEndLineNum%

rem 解析svn配置行
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "svn.*http" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于标题开始行 且 小于标题结束行 则解析,设环境变量
if !linenum! gtr %RepoLinkStartLineNum% if !linenum! lss %RepoLinkEndLineNum% (
set SvnUrlMd_%2=%%l
)
)

rem 解析git配置行
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "git.*http" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于标题开始行 且 小于标题结束行 则解析,设环境变量
if !linenum! gtr %RepoLinkStartLineNum% if !linenum! lss %RepoLinkEndLineNum% (
set GitUrlMd_%2=%%l
set BranchMd_%2=%%k
)
)

echo Leaving Function %0
goto:eof

rem 解析README中的Config表格,输出到文件
:parseConfigFunc
echo Entering Function %0
echo inFileName %1
echo outFileName %2

rem 清空outFile
del %2 2>nul

rem 定位Markdown中Config标题所在起始行和结束行
call:locateMarkdownTitleFunc %1 "Config"
set ConfigStartLineNum=%startLineNum%
set ConfigEndLineNum=%endLineNum%
echo ConfigStartLineNum=%ConfigStartLineNum%
echo ConfigEndLineNum=%ConfigEndLineNum%

rem 查找Config标题中Config Name所在行
for /F "tokens=*" %%i in ('findstr /i /n /c:"Config Name" %1') do (
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
if !linenum! gtr %ConfigStartLineNum% if !linenum! lss %ConfigEndLineNum% (
rem 设置Config Table内容开始行
set /a tableStart=!linenum!+2
)
)
)

rem 逐行遍历Markdown文件
for /F "tokens=1,2,3,4 delims=^| " %%i in ('findstr /i /n "^" %1') do (
rem 解析行号
for /F "tokens=1* delims=:" %%a in ("%%i") do (
set linenum=%%a
)
rem 行号大于等于表格内容开始行则解析
if !linenum! geq %tableStart% (
set configName=%%j
set configValue=%%k
rem 检测为空判定为表格结束,退出函数
if "!configName!" equ "" ( goto leaveParseConfig )
if "!configValue!" equ "" ( goto leaveParseConfig )
rem 存到输出文件
echo !configName!=!configValue!>>%2
echo !configName!=!configValue!
rem 是PostSyncJob 则 设环境变量
if "!configName!" equ "PostSyncJob" (
set PostSyncJob=!configValue!
)
)
)
:leaveParseConfig
echo Leaving Function %0
goto:eof
::=================================================================================

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo 进入git仓库, 解析README.md文件
call:parseRepoLinkFunc repo\repo_git\README.md Git
echo GitRepo README.md Info:
echo SvnUrlMd_Git = %SvnUrlMd_Git%
echo GitUrlMd_Git = %GitUrlMd_Git%
echo BranchMd_Git = %BranchMd_Git%

echo 进入svn仓库, 解析README.md文件
call:parseRepoLinkFunc repo\repo_svn\README.md Svn
echo SvnRepo README.md Info:
echo SvnUrlMd_Svn = %SvnUrlMd_Svn%
echo GitUrlMd_Svn = %GitUrlMd_Svn%
echo BranchMd_Svn = %BranchMd_Svn%

rem Config写入README_Config.txt
call:parseConfigFunc repo\repo_git\README.md README_Config.txt

结尾

这几天边写边学BAT脚本,刚接触BAT感觉和Python比起来不够可读和易用,和Shell比起来命令功能不强大。BAT的编程能力与C语言等编程语句比起来是十分有限的,也是十分不规范的。批处理的程序语句就是一条条的DOS命令(包括内部命令和外部命令),而批处理的能力主要取决于你所使用的命令,但在Windows平台上命令确实不如Linux上的好用,在Windows上装MinGW使用Linux上的命令又涉及到了字符转义和编码不同,bat中调Linux下的命令会非常乱,不推荐我这种菜鸡使用,不如直接全部上Linux。

好东西

Linux下命令有很多,不常用或者首次用比较陌生,好在可以使用tldr这个命令做提示,比命令自带的帮助文档精简。发现一个网页版的tldr,数据库还经常更新,tldr-inbrowser