需求
在Windows系统中,以命令行方式,找出当前目录下所有拓展名不是.md
、.js
、.bat
的文件。
解决方法一
> dir /a-d /b | findstr /v ".*\.bat$ .*\.js$ .*\.md$"
分析
dir命令
首先使用dir
命令,找到当前目录下所有文件:
> dir /a-d /b
命令行中使用> help dir
可以查看该命令的参数详解。
对上面这个命令,/a
表示 显示具有指定属性(attribute)的文件,其后紧跟属性:-
表示“否”,而d
表示目录文件(即文件夹)。所以/a-d
表示非文件夹的文件。
/b
表示使用空格式(没有标题信息或摘要)。
C:\Users\Berlin\Desktop\demo>dir /a-d
驱动器 C 中的卷没有标签。
卷的序列号是 F2B9-76C6
C:\Users\Berlin\Desktop\demo 的目录
2018/04/16 11:21 263 override.ts
2018/04/14 10:37 525,295 package-lock.json
2018/04/14 10:37 1,328 package.json
.....
16 个文件 536,101 字节
0 个目录 83,691,253,760 可用字节
C:\Users\Berlin\Desktop\demo>dir /a-d /b
override.ts
package-lock.json
package.json
...
findstr命令
基本用法就是findstr <模板字符串> <待查找字符串>
。这里模板字符串默认是使用正则表达式的,可以使用通配符等,但是不能用圆括号的捕获组。
例如为了匹配以.md
、.js
、.bat
为拓展名的文件,以JS风格的正则表达式可能会写成:.*\.(md|js|bat)$
,注意第二个点字符要转义,第一个点字符表通配符。
然而在这里将无法使用上述风格的正则表达式,为了能够匹配,我们需要写成
".*\.bat$ .*\.js$ .*\.md$"
即一个字符串,使用空格作为分隔,那么它就会匹配以.md
、.js
、.bat
为拓展名的文件。
假设有一个叫temp file.txt
和another file.txt
的文件,而只想匹配前者,则表达式"^te.* e\.txt$"
会把两个文件都匹配到,而我们的初衷可能是希望匹配 以te开头、后跟任意多个字符、加一个空格、以e.txt 结尾的文件,anthore file.txt
不符合这个规则,不应该被匹配到。
实际上,因为这个表达式有空格,所以实际上会被拆分为两个表达式:^te.*
和e\.txt$
,显然后者表达式可以把another file.txt
匹配到。
所以为了只匹配temp file.txt
,需要写成^te.*e\.txt
,即点通配符也包含了空格。
另一种需求是,如果想把引号中的所有字符解释为一个整体,那么可以使用/c:
选项,例如,/c:".txt .bat"
,这意味着检索时将".txt .bat"看作是整体,并且点字符就是点字符,不再具有正则表达式的通配符功效。也就是说,/c:
选项后跟的字符串是一个普通字符串,并且是一个整体不分割。
/c:
选项可以使用多个。例如,想检索包含.js
和.md
的文件名,可以使用命令dir /a-d /b | findstr /c:".js" /c:".md"
完成。但问题在于,由于他解释为普通字符串而不是正则表达式,所以类似package.json
的文件也会被匹配到,而我们本意是找到以它们结尾。此时$
符也无法使用了。
最后,findstr
命令有一个选项是/v
,它表示“只打印不包含匹配的行。”。而这个功能正是我们需要的。
综上所述,使用dir
和findstr
,利用管道,我们在命令行中完成了需求。
解决方案二
这里使用bat文件来完成,并且使用另一种思路,即用FOR循环和IF语句完成。重点是理解setlocal enableDelayedExpansion
的用途。
@echo off
setlocal enableDelayedExpansion
for %%I in (*.*) do (
set /a res=0
if "%%~xI" == ".md" set /a res+=1
if "%%~xI" == ".js" set /a res+=1
if "%%I" == "%~nx0" set /a res+=1
if !res! equ 0 echo %%I
)
endlocal
分析
注意循环变量I
的写法:在命令行中是%I
,而在批处理文件中是%%I
。并且在这里,循环变量只能是一个大写或小写字母组成。
使用FOR循环,在当前目录下所有的文件中(*.*
)中遍历。每次循环时,设置一个变量res
并初始化为0.
set
的/a
选项表示将等号右边的字符解释为数学表达式。如果没有/a
,那么就会把字符0赋给res
。使用/a
选项可以完成后续的+=
等数学运算。具体使用help set
查看帮助。
如果当前文件的拓展名等于.md
,则res
自增1。
~x
表示显示拓展名,~n
表示文件名,~nx
表示文件名和拓展名。例如,对于遍历到package.json
时,相关输出如下:
echo %%I => package.json
echo %%~xI => .json
echo %%~nI => package
echo %%~nxI => package.json
而
%0
表示批处理文件本身(即其值为字符串"foo.bat"
)
做字符串比较时,例如
"%%~xI" == ".md"
,要把变量I
放到双引号中,否则可能无法比较
当三个if都完成后,如果拓展名不是.md
、.js
、.bat
,那么res
的值就为0,则输出。
启用延迟环境变量扩展
语句
setlocal enableDelayedExpansion
将启用延迟环境变量扩展。为什么要这样做呢?
详细讲解见参考资料[1][3]。简单来说,批处理程序是逐条执行批处理脚本的,它会先读入一条语句,然后对该语句里的变量作替换,然后开始解释这个语句。
例如:
set a=4
set a=5&echo %a%
echo %a%
- 第一条语句执行完后,局部变量
a
的值为4。 - 接下来,处理程序先读入第二条语句(
set a=5&echo %a%
),然后对变量进行替换,此后就变成了set a=5&echo 4
,替换完成后才开始执行这个语句,因此最终结果是输出4而不是5. - 第二条执行完后,变量
a
变成了5,所以第三句输出5
这里的关键在于,语句执行之前,变量被替换为当前的值。因此在本条语句执行过程中,该变量的变化是不会被检测到的(它会在之后的语句中生效,例如例子中的第三句)
为了能使得局部变量的变化能被检测到,那么在执行之前就不能对其进行值替换,也就是延迟赋值。这就是为什么要使用setlocal enableDelayedExpansion
的原因:
setlocal enableDelayedExpansion
set a=4
set a=5&echo !a!
echo %a%
上面的批处理运行结果是输出两个5. 我们注意到第三句中的变量a
使用感叹号进行取值而不是百分号。
由于启动了变量延迟,所以批处理能够感知到动态变化,即不是先给该行变量赋值,而是在运行过程中给变量赋值,因此此时a的值就是5了
启用延迟赋值后,应该在适当地方使用endlocal
命令进行关闭。
没有使用
endlocal
语句停用延迟环境变量扩展功能,则MS-DOS解释器会在程序的末尾自动调用endlocal
命令重置MS-DOS环境默认值。
回到本方案代码中去,在最后一句IF判断中,使用了
if !res! equ 0 echo %%I
即res
使用了延迟赋值(两个感叹号)。因此在之前的IF中如果res
发生了变化,则此处也会被检测到并使用。
作为一个反例,我们来看看下面的代码:
@ECHO OFF
set res=Bonjour
for %%I in (A B C) do (
set /a res=99
echo %res%
)
在FOR循环开始之前对局部变量res赋值为Bonjour(因为没有使用/a
,故认为是字符串)。在FOR循环中,每一轮都将res
赋值为数值99,我们可能认为会连续输出三次99,但事实是,连续输出三次字符串“Bonjour”:
C:\Users\Berlin\Desktop\demo>set_local.bat
Bonjour
Bonjour
Bonjour
这是因为,FOR循环的循环体使用括号括起来的,因此整个循环体语句实际上是一条语句!!。因此在执行FOR循环之前res
已经是“Bonjour”了,在执行FOR的时候,自然会做变量替换,因此它等价于下面的程序
@ECHO OFF
set res=Bonjour
for %%I in (A B C) do (
set /a res=99
echo Bonjour
)
解决办法:
@ECHO OFF
setlocal enableDelayedExpansion
set res=Bonjour
for %%I in (A B C) do (
set /a res=99
echo !res!
)
endlocal
输出:
C:\Users\Berlin\Desktop\demo>set_local.bat
99
99
99
【要点】:
- 使用
setlocal enableDelayedExpansion
开启延迟赋值 - 局部变量使用
!foo!
进行引用,而不是%foo%