批处理之家's Archiver

Spring 发表于 2010-6-27 05:55

脚本学习及应用分享 — 批处理和脚本的交互

脚本学习及应用分享

批处理和Windows脚本的交互

(一)在批处理中调用Windows脚本

---------------

脚本可以使用 cscript.exe 在控制台中运行,而我们知道命令控制台中的输出,包括标准输出和标准错误输出,可以使用管道 1 或者 2 来重定向以加以利用,所以在批处理中调用Windows脚本并处理它的结果是比较容易的。

示例:
一般情况下调用VBS都带参数的,因此我们在这个小脚本中加入参数的获取。
它的功能是我们给定一个人名,显示 XX你好,如果参数不对则提示错误。
(关于脚本参数的获取可以查阅 WScript.Arguments 相关的资料)
SayHello.vbs[code]' 进入程序是显示欢迎信息
WScript.Echo "欢迎使用本程序。"
' 得到参数对象
Set args = WScript.Arguments
' 如果参数个数是一个,正常运行,否则提示错误信息并退出。
If args.Count = 1 Then
  ' 得到第一个参数
  name = WScript.Arguments(0)
  ' 在标准输出中,即控制台的 通道1 输出一行文字
  WScript.StdOut.WriteLine name & ",你好。"
Else
  ' 在标准错误中,即控制台的 通道2 输出一行文字
  WScript.StdErr.WriteLine "参数个数不为1,请重试。"
  ' 退出程序
  WScript.Quit
End If[/code]cscript.exe默认会在标准输出的开始加上微软版权信息,我们一般加上 //NoLogo 参数来屏蔽掉。
我们在CMD中来试试运行这个脚本,分别执行下面四条命令,看看结果如何:[code]cscript //nologo SayHello.vbs "%username%"[/code][code]cscript //nologo SayHello.vbs "%username%" 1>nul[/code][code]cscript //nologo SayHello.vbs "%username%" "第二个参数"[/code][code]cscript //nologo SayHello.vbs "%username%" "第二个参数" 2>nul[/code]结果显示,
第一条显示欢迎和XX你好,第二天什么都不显示,第三条显示欢迎和错误信息,第四条只显示欢迎。

用 cscript.exe 在控制台运行脚本时:
① WScript.Echo 在标准输出,通道1 中输出一些信息,并且在输出信息后会换行;
② WScript.StdOut 和 WScript.StdErr 分别在 【通道1】 和 【通道2】输出信息,WriteLine 输出信息后会自动换行,而用 Write 方法的话默认输出信息后不换行;

以前VBS需要交互输入信息时都是使用 InputBox 函数,其实在控制台运行时,还可以直接读取控制台中的输入,像批处理一样:
批处理
SayHello2.bat[code]@echo off
echo *** 开始: %date% %time% ***
set /p name=请输入你的名字:
echo %name%,你好!
set /p=按回车结束程序...
echo *** 结束: %date% %time% ***[/code]外观很相近的VBS脚本
SayHello2.vbs[code]WScript.Echo "*** 开始:" & Now & " ***"
WScript.StdOut.Write "请输入你的名字:"
name = WScript.StdIn.ReadLine()
WScript.StdOut.WriteLine name & ",你好!"
WScript.StdOut.Write "按回车结束程序..."
WScript.StdIn.ReadLine
WScript.Echo "*** 结束:" & Now & " ***"[/code]③ 在控制台中运行脚本可以用 var = WScript.StdIn.ReadLine() 读取输入的一行信息,也可以用来实现暂停程序的作用。

到此我们知道了用VBS如何在控制台中读取出入和现实输出或错误信息,那批处理中如何获取这些信息并加以使用呢?
这不是新鲜的东西了,cscript.exe 就是一个命令行嘛,现在问题就变成了“批处理中如何获取命令的输出信息到变量?”
我用过的有两种方法,
一种是把输出信息重定向到临时文件,然后再读取这个文件到变量,比如接着用上面的脚本:[code]@echo off
cscript //nologo SayHello.vbs "%username%" 1>temp.txt 2>nul
echo VBS运行结果是
echo.
echo ------- 全  部 --------
type temp.txt
echo -----------------------
echo.
echo ------- 第一行 --------
set /p line1=<temp.txt
echo %line1%
echo -----------------------
echo.
echo ------- 连  接 --------
setlocal enabledelayedexpansion
set lines=
for /f "delims=" %%a in (temp.txt) do (set lines=!lines!%%a )
echo !lines!
endlocal
echo -----------------------
del temp.txt
pause>nul[/code]另一种就是用 for %%a in ('command') do 这种格式来获取,比如我们就要获取所有行连起来的内容

[code]@echo off
setlocal enabledelayedexpansion
set lines=
for /f "delims=" %%a in ('cscript //nologo SayHello.vbs "%username%" 2^>nul') do (
  set lines=!lines!%%a
)
echo 结果是
echo !lines!
pause>nul[/code]

注意命令里面有特殊符号是要用转义符进行标志,如 2>nul 改为 2^>nul ,不然要出错。
一般我们编写的脚本都是只输出一行信息,或者我们只在乎的都是最后一行信息,于是可以采取这种方式:
[code]@echo off
for /f "delims=" %%a in ('cscript //nologo SayHello.vbs "%username%" 2^>nul') do (set "lines=%%a")
echo 结果是
echo %lines%
pause>nul[/code]

写了个函数专门获取最后一行的输出,不知道实用性如何:[code]@echo off
call :GetScriptExecuteResult 0 result SayHello.vbs "%username%"
echo 脚本执行的输出是:%result%
pause>nul
goto :EOF

:: 执行脚本并将最后一行输出信息存入变量
:: 参数依次为
::     第一个:0=所有输出,1=仅获取通道1, 2=仅获取通道2
::     第二个:要存入的变量名
::     第三个:要执行的脚本
::     后面若干个:脚本需要传入的参数
:GetScriptExecuteResult
IF [%1]==[] GOTO :GetScriptExecuteResult_End
SET "___PT="
IF [%1]==[0] SET "___PT= "
IF [%1]==[1] SET "___PT= 2^>nul "
IF [%1]==[2] SET "___PT= 1^>nul "
IF "%___PT%"=="" GOTO :GetScriptExecuteResult_End
IF [%2]==[] (GOTO :GetScriptExecuteResult_End) ELSE (SET "__VAR=%2")
IF [%3]==[] (GOTO :GetScriptExecuteResult_End) ELSE (SET "___FN=%3")
SET "__CMD=CSCRIPT.EXE //NOLOGO "%___FN%""
SHIFT
SHIFT
:GetScriptExecuteResult_Block1
SHIFT
IF NOT [%1]==[] (
  SET "__CMD=%__CMD% %1"
  GOTO :GetScriptExecuteResult_Block1
)
FOR /F "DELIMS=" %%I IN ('%__CMD%%___PT%') DO (SET "%__VAR%=%%I")
:GetScriptExecuteResult_End[/code]=======================================

最后再看一个有意思的模拟等待的例子,回车可以把光标移动到行首,所以可以擦写屏幕的一行。
本例的退出条件是时间到了,使用时根据自己的需求更改条件。

VBS脚本
Loading.vbs[code]Set args = WScript.Arguments
If args.Count < 3 Then WScript.Quit
o = Timer
t = args(0)
s = args(1)
w = args(2)
l = Len(w) + 1
w = w & String(Len(w), " ")
i = 0
While CInt(Timer) < o + t
  i = i + 1
  WScript.Sleep 333
  temp = s & Left(w, i Mod l) & Right(w, l - (i Mod l)) & Chr(13)
  WScript.StdOut.Write temp
WEnd
WScript.StdOut.WriteLine vbCrLf & "完成"[/code]在CMD中输入命令[code]cscript -nologo Loading.vbs 10 "正在下载,请稍候" "......"[/code]在 10 秒钟之内显示等待,那六个点是循环一直在变的,到时间后退出脚本。

famersoft 发表于 2012-5-7 09:11

学习了,新手中

junmt 发表于 2012-12-2 09:04

支持~~顶顶~~~安心大巴

Spring 发表于 2014-5-24 00:37

(二)在Windows脚本中使用批处理或程序

---------------

这些东西一直没整理好发出去,这才发觉一晃好多年过去了,对我个人的来说这些功能其实没有多大实用意义,好多内容可能别人的帖子里面也有提及的,但还是贴出来了却以往的一个心结吧。

需要说明的就是,本人的用词可能不是很科学或者说精准,对于某些原理之类的东西也一般没有去深究,也不会再文中大量说明,第一是关于各种资料性的文档仅仅本论坛发过的应该就有很多了,再重复没有意义,第二点,也是最重要的一点就是,我不是来搞研究的,用脚本只是想要把电脑弄的好玩一点,所以我主要精力放在实际结果上了,细节的原理有些也不太明白,说错了误导就不好了,作为一个工具我需要用到什么功能的时候再去查就行。

在脚本中可以创建 WScript.Shell 的对象,其中有 Run 和 Exec 方法可以运行外部程序。其中 Run 最典型的例子就是
1.         有参数可以控制窗口的状态,可以用于隐藏运行程序。比如
隐藏运行批处理 [url]http://bbs.bathome.net/thread-5131-1-97.html[/url]
2.         自动登录QQ,自动XXXXXX。原理就是启动一个程序,然后模拟按键进行输入,模拟按键需要 Sendkeys 函数支持,论坛里面已经有很多帖子了,比如
启动记事本进行操作 [url]http://bbs.bathome.net/thread-1380-1-1.html[/url]

看起来像是脚本于程序交互?其实这种方式很不稳定,我也是在最开始装13的时候用过,自动安装腾讯QQ2007。延时有点问题或者遇到焦点跑掉就可能产生无法遇到的后果。
哈哈,对于焦点跑掉的情况,基本上都是用一个 AppActivate "标题" 的方法去重新进行激活到最前,但有很多缺陷,比如有重名的可能不准确,有些窗口未知或者会变的。
如果用 Exec 方法启动的话,就能获取到该进程的 PID ,然后可以进行精确的激活,比如[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set ps = ws.Exec("C:\WINDOWS\notepad.exe")
WScript.Sleep 1000
WScript.Echo "已启动记事本,进程ID为 " & ps.ProcessID
' 暂停 5秒,这段时间你可以试试把其他窗口,比如浏览器切换到最前
WScript.Sleep 5000
ws.AppActivate ps.ProcessID
[/code]方法 Exec 另一个不同之处是启动之后可以得到它的标准输入输出,但是没法进行可视界面操作。用来得到命令行的输出结果还是很方便的。

来看个简单的例子把,如果你刚刚学了最基本VBS脚本语法,你可能不知道怎样获取计算机当前登录用户名和处理器信息呢,但是想到在命令行里面不是一个变量就搞定了吗
EXP02U.BAT[code]
@echo %username%
[/code]我们可以调用这个批处理,然后获取它的输出,不就得到信息了吗,于是有
EXP02U.VBS[code]
Dim sUser, ws, bat, batout
' 创建对象
Set ws = WScript.CreateObject("WScript.Shell")
' 执行批处理文件
Set bat = ws.Exec("EXP02U.BAT")
' 获取标准输出
Set batout = bat.StdOut
' 从输出中读取全部内容
sUser = batout.ReadAll()
' 显示结果
WScript.Echo "用户名是:" & sUser
[/code]如果要获取处理器信息,换成这样[code]
@set|find "PROCESSOR"
[/code]那我们怎么不能一次就获取两个呢?改成这样
EXP02UP.BAT[code]
@echo %username%
@set|find "PROCESSOR"
[/code]这时输出的就不能一股脑的全部获取了,要分别区分,第一行是用户名,剩下的是处理器信息
EXP02UP.VBS[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set bat = ws.Exec("EXP02UP.BAT")
Set batout = bat.StdOut
sUser = batout.ReadLine()
sTemp = batout.ReadAll()
WScript.Echo "登录人:" & vbCrLf & sUser
WScript.Echo "处理器信息:" & vbCrLf & sTemp
[/code]等等,刚才运行过那两个批处理文件都是一闪而过啊,完全没有看到是什么东西,我们改成这样吧
EXP02UP.BAT 第二版[code]
@echo off
echo %username%
set|find "PROCESSOR"
pause>nul
[/code]嘿,行了,这才像是批处理嘛,多亲切的黑框框,按下回车结束它。
然后在运行一下我们的VBS脚本,咦,怎么一直就是个黑框,什么也没有,难道是卡住了?
等了一分钟,算了,估计出不来了,点X关闭吧,嘿,真行了,脚本又有提示了。

这是怎么回事呢?原来 ReadAll 方法想要读取全部内容,但是输出流在 pause 的时候还没有结束,于是要等到我们 “随便按一下”,也就是输入一个任意字符,这时批处理才会 “自杀” 以结束,才能得到输出流全部的内容。
黑的什么也没有,就是因为标准输出流被脚本“接管”了;我们使劲按键盘也不会结束,因为标准输入也被“接管”了,外面怎么按都没反映啦。
但是在“卡住”的时候,它已经将我们需要的信息都输出了,所以,在我们百般无奈把它 “强X” 的时候,所有的一切自然都断开玩完了,脚本也认为程序的输出结束了,就将之前已经有的部分读取到了。

难道我们只能用这么野蛮的办法吗?当然不是啦,刚才不是已经用过 StdOut 么,还有个输入的 StdIn 没用呢,既然你要按(其实人家不是要按啦,不要Sendkey哦,而是要从标准输入流中接收一个字符),那我们就给你吧,在脚本里面加一句,这样就可以 “圆寂” 啦
EXP02UP.VBS 第二版[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set bat = ws.Exec("EXP02UP.BAT")
' 获取标准输入流
Set batin = bat.StdIn
' 随便输入一个字符
batin.Write "Spring Brother"
Set batout = bat.StdOut
sUser = batout.ReadLine()
sTemp = batout.ReadAll()
WScript.Echo "登录人:" & vbCrLf & sUser
WScript.Echo "处理器信息:" & vbCrLf & sTemp
[/code]你不是说输入“一个”字符么,怎么那么多个。。。还有,为什么输入放到那么前面,没关系吗?
试试就知道了,估计多了的别人也不管你吧。
其实你在控制台运行命令的时候有没有发现过,如果一个命令正在执行中的还未结束的时候,你输入另外一个命令并且回车,那么这个命令会在当前过程结束后马上执行的,比如你正在执行[code]dir /s C:\*.sb[/code]这可能会花一些时间,不要紧,你正好可以慢慢的在键盘敲入[code]echo %time%[/code]并且回车,现在还卡着没有反映?如果不想再等查找了,按下 CTRL+C 将其强制结束,你会发现之后的显示时间其实是执行了的,如果你还在怀疑后面这句是在当时按下回车就执行了,还是强制结束查找之后才执行的,看看那个时间就清楚啦。那个 StdIn.Write 干的事就跟你刚才敲键盘的效果一样,所以你可以理解它为什么放哪里都没关系了。

但是这样也许也不靠谱,如果批处理执行过程中有些不可遇到的异常,你输入一堆东西它可能也不会结束,所以,我们就用提供的 Terminate 方法私下决定给当事人一个 “安乐死” 吧
EXP02UP.VBS 第三版[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set bat = ws.Exec("EXP02UP.BAT")
' 等他一会儿,看起来已经 “没有意识” 了
WScript.Sleep 333
' 这样痛苦的活着,不如给他解脱吧
bat.Terminate
Set batout = bat.StdOut
sUser = batout.ReadLine()
sTemp = batout.ReadAll()
WScript.Echo "登录人:" & vbCrLf & sUser
WScript.Echo "处理器信息:" & vbCrLf & sTemp
[/code]写批处理代码起家的,有没有觉得那个BAT文件还不完美啊,一点都不灵活对吧,弄成一个可以灵活选择需要什么信息就输入相应代码的怎么样:
EXP02UPT.BAT[code]
@echo off
:Spring
echo What can I do for you?
echo 1.用户名 2.处理器信息 3.日期时间 0.退出
set "Brother="
set /p Brother=
if [%Brother%]==[0] GOTO :EOF
if [%Brother%]==[1] GOTO :U
if [%Brother%]==[2] GOTO :P
if [%Brother%]==[3] GOTO :T
echo *** ERROR ***
GOTO :Spring

:U
echo %username%
endlocal
GOTO :Spring

:P
set|find "PROCESSOR"
GOTO :Spring

:T
echo %date% %time%
GOTO :Spring
[/code]试了一下,功能强大啊,可以无限执行下去,所以要实时的时间都可以一直取的到哦。
那我的脚本要依次读取 时间、用户名、处理器信息 该怎么写呢?
EXP02UPT.VBS[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set bat = ws.Exec("EXP02UPT.BAT")
Set batin = bat.StdIn
Set batout = bat.StdOut
batin.Write "3" & vbCr
WScript.Sleep 333
batin.Write "1" & vbCr
WScript.Sleep 333
batin.Write "2" & vbCr
WScript.Sleep 333
batin.Write "0" & vbCr
WScript.Sleep 333
sAll = batout.ReadAll()
WScript.Echo sAll
[/code]加延时是因为需要输入间隔么,还是需要给他一点处理时间,这我不太清楚了,反正用 vbCrLf 好像有些问题。哎,得到的结果就是一整砣,批处理那些没用的提示信息也混在一起,不好拆出来啊,我都懒得写了,有兴趣的自己试试吧,就当字符串操作练手。
我们还有一个标准错误输出流可以用啊,既然不好剔除无用信息,那我们就分通道吧
在批处理中指定提示信息还是默认的 1 通道,把需要的结果放到 2 通道,也就是一般用于输出错误信息的(你管我这是不是错误信息呢,我的地盘我做主)
EXP02UPT.BAT 第二版[code]
@echo off
:Spring
echo What can I do for you?
echo 1.用户名 2.处理器信息 3.日期时间 0.退出
set "Brother="
set /p Brother=
if [%Brother%]==[0] GOTO :EOF
if [%Brother%]==[1] GOTO :U
if [%Brother%]==[2] GOTO :P
if [%Brother%]==[3] GOTO :T
echo *** ERROR ***
GOTO :Spring

:U
>&2 (echo %username%)
endlocal
GOTO :Spring

:P
>&2 (set|find "PROCESSOR")
GOTO :Spring

:T
>&2 (echo %date% %time%)
GOTO :Spring
[/code]然后VBS运行它的时候,我们就不管 StdOut 里面的东西了,从 StdErr 里面获取纯的想要的信息
EXP02UPT.VBS 第二版[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set bat = ws.Exec("EXP02UPT.BAT")
Set batin = bat.StdIn
Set baterr = bat.StdErr
batin.Write "3" & vbCr
WScript.Sleep 333
batin.Write "1" & vbCr
WScript.Sleep 333
batin.Write "2" & vbCr
WScript.Sleep 333
bat.Terminate
sTime = baterr.ReadLine()
sUser = baterr.ReadLine()
sProccesor = baterr.ReadAll()
WScript.Echo "时间:" & sTime
WScript.Echo "用户:" & sUser
WScript.Echo "处理器:" & vbCrLf & sProccesor
[/code]忽然灵机一动,如果用 Exec 运行 FTP 是不是就可以实现模拟人敲命令自动上传下载了呢?
呵呵,直到这里都讨论的是“标准”输入输出,对于“不标准”的,可能就爱莫能助了,谁知道哪些标准哪些不标准呢,你可以自己去试试,也可以搜搜相关的讨论,我就不废话太多了,毕竟实践才是检验真理的唯一标准啊。

能不能摒弃 WScript.Sleep 延时语句呢,每次看到有这个东西第一感觉就是不专业!
如果想要实现根据第一次获取时间,如果是奇数秒就获取用户名,如果是偶数就获取处理器信息,又怎么弄呢?
输出流也有 Read() 方法,用 Read 或者 ReadLine 逐字读取分析就好了,感觉这些都没什么实用性,就不写在这里了,有兴趣的可以参看附件中内容。

回头一看,这一大串实例都是由想要在 VBS 中获取 %username% 但又不会写引起的,对于环境变量可以这样获取[code]
Set WshShell = WScript.CreateObject("WScript.Shell")
Set WshEnv = WshShell.Environment("PROCESS")
sUser = WshEnv("username")
MsgBox sUser
[/code]环境变量类型也有几种,其中 SYSTEM 和 USER 类型的都保存在计算机上,在注册表 HKEY_CURRENT_USER\Environment\ 路径可以看到用户环境变量,而在 PROCESS 中设置的只在运行时有效,有兴趣的可以去查查相关文档。

不管是用批处理来调用VBS脚本,还是在VBS中启动外部程序,都可以看成是一个程序甲调用另一个程序乙,对于在甲中设置的环境变量,是可以被乙读取到的,在乙启动之后甲修改值不会对乙有影响,乙中的更改也不会对甲起作用。
最后还是来做一个轻松的小玩意结束吧。
cgcym.bat[code]
@echo off
set cgcym=春 哥 纯 爷 们
for %%a in (%cgcym%) do call echo %%a%%%%a%%
pause>nul
[/code]看看这个批处理运行起来是什么样的。然后我们在写个 VBS 来启动它。
txzhz.vbs[code]
Set ws = WScript.CreateObject("WScript.Shell")
Set envp = ws.Environment("PROCESS")
envp("春") = Chr(8) & "铁"
envp("哥") = Chr(8) & "血"
envp("纯") = Chr(8) & "真"
envp("爷") = Chr(8) & "汉"
envp("们") = Chr(8) & "子"
ws.Run "cgcym.bat"
[/code]怎么样,是不是很好玩。


参考资料:
Run Method (Windows Script Host)
[url]http://msdn.microsoft.com/en-us/library/d5fk67ky[/url]

AppActivate Function
[url]http://msdn.microsoft.com/en-us/library/dyz95fhy[/url]

Exec Method (Windows Script Host)
[url]http://msdn.microsoft.com/en-us/library/ateytk4a[/url]

WshScriptExec Object
[url]http://msdn.microsoft.com/en-us/library/2f38xsxe[/url]

Environment Property
[url]http://msdn.microsoft.com/en-us/library/fd7hxfdd[/url]

linjuming 发表于 2015-1-29 22:20

怎么样,是不是很好玩。

zrc20d 发表于 2015-12-6 09:56

非常好,学习了

页: [1]

Powered by Discuz! Archiver 7.2  © 2001-2009 Comsenz Inc.