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

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

作者: Azur_wxj | 来源:发表于2019-03-02 19:12 被阅读0次

    需求

    在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的作用详细整理

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/qfzeuqtx.html