批处理(bat)利用set p与重定向输入分行获取文本内容
起因是前几天的某个帖子中看到 cmd<1.txt 的用法,原以为1.txt 中的 pause 之所以被跳过是因为执行完后马上接收到了一个回车符,于是我把1.txt 中的所有 pause 都改成 pause&rem ,并去除所有回车符进行试验,下为去除回车符的代码:
1. | @echo off&setlocal enabledelayedexpansion |
2. | set hh=^ |
3. | |
4. | |
5. | ::获取换行符 |
6. | for %%a in ( |
7. | "@echo off" "pause&rem" "echo abc" "pause&rem" |
8. | ) do set str=!str!!hh!%%~a |
9. | echo !str:~2!>3.txt |
10. | ::只用换行符断行 |
11. | cmd<3.txt |
12. | echo; |
13. | echo ________end________ |
14. | Pause |
假设修改后 1.txt内容如下(测试时,此文本中不存在回车符):
1. | @echo off |
2. | pause&rem |
3. | echo abc |
4. | pause&rem |
结果仍然没有等待用户输入,并且还吞掉了下一行 echo 的第一个字符,导致 cmd 显示:
1. | 请按任意键继续. . . |
2. | 'cho' 不是内部或外部命令,也不是可运行的程序 |
3. | 或批处理文件。 |
4. | 请按任意键继续. . . |
对此感到非常疑惑,百思不得其解之下去请教寒夜版主,他问我 pause 等待的是什么输入,我才忽然醒悟,原来 pause 等待的是“任意键”,也就是说,它把 echo 的 e 当作用户输入给接收了(因为行末的 0D 0A被 cmd 接收了,所以 pause 接收的第一个字符就是下一行的首字符 e),因此这里用 pause 无法实现暂停的效果,这就推翻了我原来的认识,证明等待用户输入的命令并不是以回车符作为终止输入的信号。
进一步思考一下,众所周知, set /p 首行=<1.txt 能获取 1.txt 第一行,那么对含有大量 set /p 的语块进行重定向,又是什么结果呢?
1. | @echo off |
2. | (for /l %%a in (1 1 10) do set /p .%%a=)<%0 |
3. | set. |
4. | pause |
可以看到,通过 set /p 配合重定向,能够把文本每一行都设为变量值,这是全新的技巧,更重要的是,这是一种全新的遍历文本的方式,它相当于不跳过空行的 for until ,这与 for /f 的skip 参数相映成趣,而且还对特殊字符有极佳的兼容性。不过有得必有失,使用 set /p 赋值时,变量长度不能超过 1024 字节,所以局限了这个技巧的适用范围。
激动之余,又产生了两个疑惑:
1、set /p 是以什么为依据断行
2、当循环数大于文本行数时,为什么没有停顿下来等待用户输入
和寒夜版主一起做了几个试验,证明无论是单纯的 0D 回车符或者 0A 换行符都无法实现平时在 cmd 窗口中敲回车结束 set /p 输入的效果,必须出现连续的一组 0D 0A 才能够终止对一个 set /p 的输入。关于终止 set /p 输入的“特征码”,25 楼的 mxxcgzxxx 提出了更合理的猜想:0D 0A 和 0A 0D 这两种组合都能起到终止 set /p 输入的作用。
(25楼链接:
http://bbs.bathome.net/viewthread.php?tid=13327&page=2&fromuid=30406#pid86638)
而第二个问题,绕了半天弯子终于得到一个比较合理的猜测:当重定向的输入被前面执行的命令取用完的时候,剩下的就是从空设备的输入,也就是 set /p .5=
1. | (set /p .a= |
2. | set /p .b=)<只有一行的文件.txt |
3. | set.[/code]其作用相当于:[code]set /p .a=<只有一行的文件.txt |
4. | ::取首行 |
5. | set /p .b=<nul |
6. | ::从空设备获取输入,等于无输入 |
7. | set. |
8. | ::显示以.开头的变量 |
通过这个猜测和其他一些命令接收重定向输入时表现出的特性衍生出一个推测,那就是 cmd 在接受重定向输入到命令的时候,也许是一个字符一个字符顺序传递给语块\语句的,那些能够接受重定向输入的命令会自发地从中获取输入,直到命令自行关闭输入句柄为止。
这可以理解为cmd中出现重定向输入的时候,输入中的字符在排队等候被命令依次提走,一直到无字符可提的时候,重定向输入的来源就成为了一个空设备 nul。
好比一个旅行团在打车,出现愿意载客的出租车时,队伍就有序地依次上车,一辆车客满后就再等下一辆(旅行团并不知道当前这辆车何时客满,他们只需要机械地让排头的人上车、直到司机喊停为止),最终所有人都打车走光,这时候新来的出租车就找不到客人了,所以空车离开时当然还是空车。
当然有些命令是以任意合法字符或者固定字符来判定何时结束输入的,比如choice、set /p和pause,这就很有利用的价值。
此处仅以 set /p 举几个例子:
1. | @echo off |
2. | set /p line=要获取的行所在行数: |
3. | (for /l %%a in (1 1 %line%) do set /p 内容=)<a.txt |
4. | set 内容 |
5. | ::获取指定行内容的新方法,由于无需遍历整个文本,要获取的行位置靠前的情况下有很大优势 |
1. | @echo off |
2. | (for /l %%a in (1 1 100) do set /p .%%a=)<%0 |
3. | |
4. | ::不跳过空行赋值,但是 tmplinshi 版主的测试结果标明这中方法比常规办法稍慢,它只在某些场合有优势。 |
(5楼链接:
http://bbs.bathome.net/viewthread.php?tid=13327&page=1&fromuid=30406#pid86516)
1. | @echo off&setlocal enabledelayedexpansion |
2. | |
3. | (for /l %%a in (1 1 5) do ( |
4. | if not defined .!n! set /a n+=1 |
5. | set /p .!n!= |
6. | ))<%0 |
7. | ::当然也同样可以跳过空行只将前N行赋值 |
8. | |
9. | @echo off |
10. | (for /l %%a in (1 1 7) do ( |
11. | pause |
12. | set /p echo= |
13. | echo !echo! |
14. | )<%0 |
15. | ::去除每行行首第一个任意字符的另一种方法,如果不计较效率的话,用choice可以去除指定字符 |
1. | @echo off |
2. | (for /l %%a in (1 2 7) do ( |
3. | set /a a=1,b=2 |
4. | set /p a= |
5. | set /p b= |
6. | if !a!==!b! echo 相等 |
7. | )<%0 |
8. | ::以两行为周期判断其内容是否相等,这比起老方法省下了许多麻烦,比如无需用 setlocal、endlocal 来兼容特殊字符 |
1. | @echo off |
2. | (for %%a in ( |
3. | 1-关回显 2-for循环 3-循环内容 4-do 4-设变量 5-输入 6-查看变量 7-注释 |
4. | ) do ( |
5. | set /p .%%a= |
6. | ))<%0 |
7. | set. |
8. | ::可以通过无参数的for来循环,实现了以往无法实现的效果 |
9. | |
10. | @echo off&setlocal enabledelayedexpansion |
11. | (for /f "tokens=1* delims=:" %%a in ('findstr /n .* 1.txt') do ( |
12. | set t2= |
13. | set /p t2= |
14. | echo;%%b!t2! |
15. | ))<2.txt>合并.txt |
16. | ::由于可以有两个不同的输入来源并存,所以双文本乃至多文本合并就成为轻而易举的事了 |
无奈的是,可以分段接受重定向输入的命令寥寥无几,所以暂时还没有想到更多的实用技巧,还是等待大家来补充吧。
感谢寒夜孤星、mxxcgzxxx、tmplinshi给予指点和共同探讨、测试。