批处理学习:查找文件夹下非指定拓展名的文件

需求

在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.txtanother 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,它表示“只打印不包含匹配的行。”。而这个功能正是我们需要的。

综上所述,使用dirfindstr,利用管道,我们在命令行中完成了需求。

解决方案二

这里使用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%
  1. 第一条语句执行完后,局部变量a的值为4。
  2. 接下来,处理程序先读入第二条语句(set a=5&echo %a%),然后对变量进行替换,此后就变成了set a=5&echo 4,替换完成后才开始执行这个语句,因此最终结果是输出4而不是5.
  3. 第二条执行完后,变量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

【要点】

  1. 使用setlocal enableDelayedExpansion开启延迟赋值
  2. 局部变量使用!foo!进行引用,而不是%foo%

参考资料

  1. bat中if语句的用法
  2. 请问批处理中符号%nx0和!num!%%xm表示什么意思呢?
  3. 批处理中setlocal enabledelayedexpansion的作用详细整理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269