Sign in Join
Wiki copytemporary

Executing External Programs/zh CN: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11执行外部程序
22
33## Contents
44
55 * 1 概述:对比
66 * 2 (Process.)RunCommand
77 * 2.1 RunCommand 的扩展版本
88 * 3 SysUtils.ExecuteProcess
99 * 4 微软 Windows 系统:CreateProcess、ShellExecute 和 WinExec
1010 * 4.1 针对提权/管理员权限使用 ShellExecuteEx
1111 * 5 Unix 的 fpsystem、fpexecve 和 shell
1212 * 6 TProcess
1313 * 6.1 最简单的示例
1414 * 6.2 简单示例
1515 * 6.3 改良后的示例(但仍不够正确)
1616 * 6.4 大数据量输出的读取
1717 * 6.5 TProcess 输入输出的用法
1818 * 6.6 TProcess 用法提示
1919 * 6.7 前台显示 macOS 应用程序包
2020 * 6.8 让程序独立运行
2121 * 6.9 与 aspell 进程通讯的示例代码
2222 * 6.10 替换“| < >”之类的 shell 操作符
2323 * 6.10.1 为什么用操作符重定向输出无效
2424 * 6.11 如何实现 TProcess 输出重定向
2525 * 6.11.1 注意
2626 * 6.12 重定向输入和输出,并在 root 权限下运行
2727 * 6.13 在 Linux 中用 sudo 运行 fdisk
2828 * 6.14 包含空格的参数(替换 Shell 命令中的引号)
2929 * 7 LCLIntf 替代方案
3030 * 7.1 用默认应用程序打开文档
3131 * 7.2 用默认浏览器打开网页
3232 * 8 WinExec
3333 * 9 参阅
3434 * 10 贡献者和更改
3535
3636## 概述:对比
3737
3838以下是 RTL、FCL 和 LCL 库中执行外部命令/进程/程序的各种方法。
3939
4040方法 | 库 | 平台 | 可单行完成 | 功能
4141---|---|---|---|---
4242[[Executing External Programs|ExecuteProcess]] | RTL | 跨平台 | 是 | 非常有限,以同步方式执行
4343[[Executing External Programs|ShellExecute]] | WinAPI | 仅限微软的 Windows 系统 | 是 | 很多。能够提权启动需管理员权限的程序。
4444[[Executing External Programs|fpsystem、fpexecve]] | Unix | 仅限 Unix 系统 | |
4545[[Executing External Programs|TProcess]] | FCL | 跨平台 | 否 | 完整
4646[[Executing External Programs|RunCommand]]RunCommand> "Executing External Programs") | FCL | 跨平台 需要FPC 2.6.2+ | 是 | 支持常用的 TProcess 用法
4747[[Executing External Programs|OpenDocument]] | LCL | 跨平台 | 是 | 只能打开文档。将会用该文档类型关联的应用程序打开。
4848
4949## (Process.)RunCommand
5050
5151 * [RunCommand](<http://www.freepascal.org/docs-html/fcl/process/runcommand.html>)参考手册
5252 * [RunCommandInDir](<http://www.freepascal.org/docs-html/fcl/process/runcommandindir.html>)参考手册
5353
5454FPC 2.6.2 为 process 单元的 TProcess 加入了一些有用的函数,来自 [[Projects using Lazarus|fpcup]] 项目的封装。 这些函数适用于初、中级应用,能将输出捕获到单个字符串中,并完全支持 _大数据量输出_ 。
5555
5656以下是简单示例:
5757```
5858program project1;
5959
6060 {$mode objfpc}{$H+}
6161
6262 uses
6363 Process;
6464
6565 var
6666 s : ansistring;
6767
6868 begin
6969
7070 if RunCommand('/bin/bash',['-c','echo $PATH'],s) then
7171 writeln(s);
7272
7373 end.
7474```
7575
7676但请注意,并不是所有 shell“内置”命令(如 alias)都能支持,因为默认情况下,在非交互式 shell 中不会用到别名。除非设置了 BASH_ENV 环境变量,不然非交互式 shell 也不会读取 .bashrc 文件。因此,以下代码不会输出任何结果:
7777```
7878program project2;
7979
8080 {$mode objfpc}{$H+}
8181
8282 uses
8383 Process;
8484
8585 var
8686 s : ansistring;
8787
8888 begin
8989
9090 if RunCommand('/bin/bash',['-c','alias'],s) then
9191 writeln(s);
9292
9393 end.
9494```
9595
9696RunCommand 有一个重载版本,可返回程序的退出码。RunCommandInDir 则可在指定目录中运行命令(设置 p.CurrentDirectory):
9797```
9898function RunCommandIndir(const curdir:string;const exename:string;const commands:array of string;var outputstring:string; var exitstatus:integer): integer;
9999 function RunCommandIndir(const curdir:string;const exename:string;const commands:array of string;var outputstring:string): boolean;
100100 function RunCommand(const exename:string;const commands:array of string;var outputstring:string): boolean;
101101```
102102
103103在 FPC 3.2.0 以上版本中,Runcommand 有了新的版本,允许覆盖 TProcessOptions 和 TShowWindowOptions。
104104
105105
106106
107107### RunCommand 的扩展版本
108108
109109在 **FPC 3.2.0 以上** 版本中,RunCommand 实现了泛型化,并重新归入了 TProcess,以便能更迅速地构建自己的 RunCommand。以下是带有超时机制的 RunCommand:
110110```
111111program TestTProcessTimeout;
112112
113113 uses classes, sysutils, process, dateutils;
114114
115115 type
116116 { TProcessTimeout }
117117 TProcessTimeout = class(TProcess)
118118 protected
119119 timeoutperiod: TTime;
120120 timedout : boolean;
121121 started : TDateTime;
122122 procedure LocalnIdleSleep(Sender,Context : TObject;status:TRunCommandEventCode;const message:string);
123123 public
124124 class function RunCommandwithTimeout(const exename:TProcessString;const commands:array of TProcessString;const dir:string;out outputstring:string;out errorstring:string;out exitstate:integer; ProcessOptions : TProcessOptions = [];SWOptions:TShowWindowOptions=swoNone;timeout:integer=15):boolean;static;
125125 end;
126126
127127 procedure TProcessTimeout.LocalnIdleSleep(Sender,Context : TObject;status:TRunCommandEventCode;const message:string);
128128 begin
129129 if status=RunCommandIdle then
130130 begin
131131 writeln(Executable+'('+ Parameters.CommaText+')' +' time: '+TimeToStr(now-started));
132132 if (now-started)>timeoutperiod then
133133 begin
134134 writeln('process timed out ');
135135 timedout:=true;
136136 Terminate(255);
137137 exit;
138138 end;
139139 sleep(RunCommandSleepTime);
140140 end;
141141 end;
142142
143143 class function TProcessTimeout.RunCommandwithTimeout(
144144 const exename : TProcessString;
145145 const commands : array of TProcessString;
146146 const dir : string;
147147 out outputstring : string;
148148 out errorstring : string;
149149 out exitstate : integer;
150150 ProcessOptions : TProcessOptions = [];
151151 SWOptions : TShowWindowOptions=swoNone;
152152 timeout : integer=15
153153 ):boolean;
154154 Var
155155 p : TProcessTimeout;
156156 i : integer;
157157 begin
158158 p:=TProcessTimeout.create(nil);
159159 p.OnRunCommandEvent:=@p.LocalnIdleSleep;
160160 p.CurrentDirectory:=dir;
161161
162162 //timeout 单位为分钟,每秒检测一次
163163 p.timeoutperiod:=timeout/MinsPerDay;
164164
165165 (*
166166 //若要求 Timeout 单位为秒
167167 p.timeoutperiod:=timeout/SecsPerDay;
168168 *)
169169
170170 // 每秒检测一次是否该结束
171171 p.runcommandsleeptime := 1000;
172172
173173 if ProcessOptions<>[] then
174174 P.Options:=ProcessOptions - [poRunSuspended,poWaitOnExit];
175175 p.options:=p.options+[poRunIdle]; // 需要运行 RUNIDLE 事件。参见 User Changes 3.2.0
176176
177177 P.ShowWindow:=SwOptions;
178178 p.Executable:=exename;
179179 if high(commands)>=0 then
180180 for i:=low(commands) to high(commands) do
181181 p.Parameters.add(commands[i]);
182182 p.timedout:=false;
183183 p.started:=now;
184184 try
185185 // runcommand() 的主循环。最初基于本 wiki 内的“大数据量输出”场景,只是 5 年内不断有所扩展。
186186 RunCommandwithTimeout:=p.RunCommandLoop(outputstring,errorstring,exitstate)=0;
187187 if p.timedout then
188188 RunCommandwithTimeout:=false;
189189 finally
190190 p.free;
191191 end;
192192 if exitstate<>0 then RunCommandwithTimeout:=false;
193193 end;
194194
195195
196196 // 用法示例
197197
198198 var
199199 output,errors : string;
200200 exitcode :integer;
201201 begin
202202 //Windows 和 Unix 还有其他程序可用
203203 {$if defined(WINDOWS)}
204204 if TProcessTimeout.RunCommandwithTimeout('cmd',['/c','dir','C:\*.exe','/s'],'c:\',output,errors,exitcode,[],swoNone,1) then // dir c:\*.exe /s
205205 {$else$}
206206 if TProcessTimeout.RunCommandwithTimeout('find',['/','-name','"*.sh"'],'/',output,errors,exitcode,[],swoNone,1) then // find / -name "*.sh"
207207 {$endif}
208208 begin
209209 writeln('program finished');
210210 writeln('--------------------output-------------------');
211211 writeln(output);
212212 writeln('--------------------errors-------------------');
213213 writeln(errors);
214214 if not(exitcode=0) then
215215 begin
216216 writeln('-program finished with errors (exitcode: '+exitcode.tostring+')-');
217217 end;
218218 readln();
219219 end
220220 else
221221 begin
222222 writeln('program not finished');
223223 writeln('--------------------output-------------------');
224224 writeln(output);
225225 writeln('--------------------errors-------------------');
226226 writeln(errors);
227227 writeln('-------------------EXITCODE------------------');
228228 writeln(exitcode);
229229 readln();
230230 end;
231231 end.
232232```
233233
234234## SysUtils.ExecuteProcess
235235
236236(跨平台)
237237
238238尽管存在很多限制,但启动一个程序(模态、没有管道或任何控制)最简单的方式就是用:
239239```
240240SysUtils.ExecuteProcess(UTF8ToSys('/full/path/to/binary'), '', []);
241241```
242242
243243发起调用的进程将以同步方式运行:暂时“挂起”直至外部程序运行完毕 - 但如果要求用户在程序继续运行之前执行某些操作,同步方式可能很有用。更多的执行方式请参阅推荐的跨平台 **RunCommand** 或其他 **TProcess** 函数;如果只需要考虑 Windows 系统,则可以使用 **ShellExecute** 。
244244
245245调用进程同步运行:它'挂起(hangs)',直到内部出现完成 - 但是,在你的程序中继续前,如果你想要用户来做 一些事,这可能是有用的。对于更多用途的方法,看下一关于优先考虑跨平台的**TProcess** 部分, 或如果你仅希望瞄准Windows,你可以使用**ShellExecute** 。
246246
247247 * [ExecuteProcess 参考手册](<http://www.freepascal.org/docs-html/rtl/sysutils/executeprocess.html>)
248248
249249## 微软 Windows 系统:CreateProcess、ShellExecute 和 WinExec
250250
251251![Light bulb](https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png) **Note:** 虽然 FPC/Lazarus 已支持 **CreateProcess** 、**ShellExecute** 和 **WinExec** ,但仅限 Win32/64环境。跨平台程序请考虑采用 **RunCommand** 或 **TProcess** 。
252252
253253![Light bulb](https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png) **Note:** WinExec 是 16 位调用,在 Windows API 中已废弃多年。最新版的 FPC 中会对其生成警告。
254254
255255**ShellExecute** 是标准的微软 Windows 函数(ShellApi.h),[documentation on MSDN](<http://msdn.microsoft.com/en-us/library/windows/desktop/bb762153\(v=vs.85\).aspx>) 中有完善的文档。(如果函数无法可靠运行,请注意查看关于初始化 COM 的评论).
256256```
257257uses ..., ShellApi;
258258
259259 // 简单的单行代码调用(忽略返回错误):
260260 if ShellExecute(0,nil, PChar('"C:\my dir\prog.exe"'),PChar('"C:\somepath\some_doc.ext"'),nil,1) =0 then;
261261
262262 // 执行批命令:
263263 if ShellExecute(0,nil, PChar('cmd'),PChar('/c mybatch.bat'),nil,1) =0 then;
264264
265265 // 在给定目录打开命令行窗口:
266266 if ShellExecute(0,nil, PChar('cmd'),PChar('/k cd \path'),nil,1) =0 then;
267267
268268 // 用“start”命令在默认浏览器中打开 URL(通过短暂显示并隐藏的 cmd 窗口):
269269 if ShellExecute(0,nil, PChar('cmd'),PChar('/c start www.lazarus.freepascal.org/'),nil,0) =0 then;
270270
271271 // 助手函数:
272272 procedure RunShellExecute(const prog,params:string);
273273 begin
274274 // ( Handle, nil/'open'/'edit'/'find'/'explore'/'print', // 'open' 不是必需的
275275 // path+prog, params, working folder,
276276 // 0=hide / 1=SW_SHOWNORMAL / 3=max / 7=min) //要使用 SW_ constants 请 uses Windows 单元
277277 if ShellExecute(0,'open',PChar(prog),PChar(params),PChar(extractfilepath(prog)),1) >32 then; //success
278278 // 返回 0..32 表示出错
279279 end;
280280```
281281
282282还有个 WideChar 版本的 ShellExecuteExW,而 ShellExecuteExA 则是 AnsiChar 版。
283283
284284fMask 选项还可以是 SEE_MASK_DOENVSUBST、SEE_MASK_FLAG_NO_UI、SEE_MASK_NOCLOSEPROCESS 等。
285285
286286若是在 Delphi 中,要对 Word 文档或 URL 之类的 **文档** 调用 ShellExecute ,请查阅 lclintf 中的 open*(OpenUrl 等)函数(参见本页下方的替代方案)。
287287
288288### 针对提权/管理员权限使用 ShellExecuteEx
289289
290290若要用管理员/提权的权限执行外部程序,可使用各版本 ShellExecuteEx 函数的 **runas** 方法:
291291```
292292uses ShellApi, ...;
293293
294294 function RunAsAdmin(const Handle: Hwnd; const Path, Params: string): Boolean;
295295 var
296296 sei: TShellExecuteInfoA;
297297 begin
298298 FillChar(sei, SizeOf(sei), 0);
299299 sei.cbSize := SizeOf(sei);
300300 sei.Wnd := Handle;
301301 sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
302302 sei.lpVerb := 'runas';
303303 sei.lpFile := PAnsiChar(Path);
304304 sei.lpParameters := PAnsiChar(Params);
305305 sei.nShow := SW_SHOWNORMAL;
306306 Result := ShellExecuteExA(@sei);
307307 end;
308308
309309 procedure TFormMain.RunAddOrRemoveApplication;
310310 begin
311311 // 用提权后的 rundll 打开控制面板,访问“程序和功能”面板
312312 RunAsAdmin(FormMain.Handle, 'rundll32.exe shell32.dll,Control_RunDLL appwiz.cpl', '');
313313 end;
314314```
315315
316316## Unix 的 fpsystem、fpexecve 和 shell
317317
318318这些函数是依赖于特定平台的。
319319
320320 * [fpsystem 参考手册](<http://www.freepascal.org/docs-html/rtl/unix/fpsystem.html>)
321321 * [fpexecve 参考手册](<http://www.freepascal.org/docs-html/rtl/baseunix/fpexecve.html>)
322322
323323Linux.Shell/Unix.Shell 相当于 fpsystem 的 1.0.x 版本,但其错误处理方式定义得不太严格,经过长时间废弃后最终被移除了。Shell 几乎在任何场合下都可替换为 fpsystem,后者的错误处理方式更像 POSIX。
324324
325325## TProcess
326326
327327TProcess 可用于启动外部程序。采用 TProcess 有一些好处:
328328
329329 * 平台无关
330330 * 支持读取 stdout 和写入 stdin。
331331 * 程序可以等待命令完成,也可以在命令运行的同时继续运行。
332332
333333重要注意事项:
334334
335335 * TProcess 不是终端/shell!不能直接执行脚本,不能用“|”、“>”、“<”、“&”之类的运算符重定向输出。通过调用 TProcess 的 pascal 代码,可以得到相同的效果,下面有一些示例。
336336 * 在 Linux/Unix 中,很可能**必须** 指定可执行文件的完整路径。比如用 '/bin/cp' 替代 'cp'。如果程序位于标准的 PATH 内,那可以使用 LCL 的[FileUtil](<http://lazarus-ccr.sourceforge.net/docs/lcl/fileutil/index.html> "doc:lcl/fileutil/index.html") 单元中的 [FindDefaultExecutablePath](<http://lazarus-ccr.sourceforge.net/docs/lcl/fileutil/finddefaultexecutablepath.html> "doc:lcl/fileutil/finddefaultexecutablepath.html") 函数。
337337 * 在 Windows 中,如果命令位于 PATH 中,则无需给定完整路径。
338338 * [TProcess 参考手册](<http://lazarus-ccr.sourceforge.net/docs/fcl/process/tprocess.html> "doc:fcl/process/tprocess.html")
339339
340340### 最简单的示例
341341
342342在 [[Executing External Programs|Runcommand]] 函数中已给出了很多例程。在开始复制和粘贴以下示例之前,请先予以查看。
343343
344344### 简单示例
345345
346346本例仅仅是展示运行外部程序的方法,**不能用于生产性代码,请参阅大数据量输出或更好的[[Executing External Programs|Runcommand]]**。
347347```
348348// 本例程展示了如何启动外部程序
349349 program launchprogram;
350350
351351 // 包含必要的单元文件,里面有需要用到的函数和过程
352352 uses
353353 Classes, SysUtils, Process;
354354
355355 // 定义 "TProcess" 类型的变量 "AProcess"
356356 var
357357 AProcess: TProcess;
358358
359359 // 开始运行
360360 begin
361361 // 创建 TProcess 对象并赋给变量 AProcess
362362 AProcess := TProcess.Create(nil);
363363
364364 // 将需要执行的外部程序告知 AProcess
365365 // 这里要调用 Free Pascal 编译器(i386版)
366366 AProcess.Executable:= 'ppc386';
367367
368368 // 给 ppc386 带上参数 -h,实际执行的是 'ppc386 -h' 命令:
369369 AProcess.Parameters.Add('-h');
370370
371371 // 定义运行选项
372372 // 让主程序暂停运行,直至外部程序停止执行。
373373 // vvvvvvvvvvvvvv
374374 AProcess.Options := AProcess.Options + [poWaitOnExit];
375375
376376 // 让 AProcess 运行外部程序
377377 AProcess.Execute;
378378
379379 // ppc386 停止执行后才会运行至此
380380 AProcess.Free;
381381 end.
382382```
383383
384384这样就可以从自己的程序中运行外部程序了。
385385
386386### 改良后的示例(但仍不够正确)
387387
388388上述例子运行良好,但外部程序的输出结果该如何读取呢?
389389
390390那就稍微添加一些代码来实现吧: **为了便于学习,本例程已尽量做了简化。请不要在产品代码中使用本例,而应采用#大数据量输出的读取中的代码**。
391391```
392392// 本示例待有缺陷,
393393 // 演示了如何启动外部程序,并读取其输出
394394 program launchprogram;
395395
396396 // 包含必要的单元文件,内有需要用到的函数和过程
397397 uses
398398 Classes, SysUtils, Process;
399399
400400 // 定义 "TProcess" 类型的变量 "AProcess"
401401 // 还增加了一个 TStringList,用于存放从外部程序输出中读取的数据。
402402 var
403403 AProcess: TProcess;
404404 AStringList: TStringList;
405405
406406 // 开始运行
407407 begin
408408 // 创建 TProcess 对象并赋给变量 AProcess
409409 AProcess := TProcess.Create(nil);
410410
411411 // 将需要执行的外部程序告知 AProcess
412412 AProcess.Executable := '/usr/bin/ppc386';
413413 AProcess.Parameters.Add('-h');
414414
415415 // 定义执行时的选项
416416 // 确保主程序暂停运行,直至外部程序停止执行。
417417 // 还要读取输出
418418 AProcess.Options := AProcess.Options + [poWaitOnExit, poUsePipes];
419419
420420 // AProcess 知道要执行的命令行了
421421 AProcess.Execute;
422422
423423 // AProcess 执行完毕后,继续运行
424424
425425 // 读取刚执行完毕的外部程序的输出,并存入 TStringList。
426426 AStringList := TStringList.Create;
427427 AStringList.LoadFromStream(AProcess.Output);
428428
429429 // 将输出结果存入文件,并清空 TStringList。
430430 AStringList.SaveToFile('output.txt');
431431 AStringList.Free;
432432
433433 // 现在 AProcess 的输出处理完毕,可以释放了
434434 AProcess.Free;
435435 end.
436436```
437437
438438### 大数据量输出的读取
439439
440440上一示例中,主程序在外部程序退出之前一直处于等待状态,然后再读取其写到输出中去的数据。
441441
442442假定外部程序会往输出写入很多数据。这就会填满输出管道,然后被调用的程序就会进入等待状态,直至管道中的数据被读走。
443443
444444但在被调用程序结束之前,调用方程序不会去读取其输出。于是死锁发生了。
445445
446446因此以下示例不采用 poWaitOnExit 方式,而是在执行外部程序的同时读取其输出。输出数据存于内存流中,以供后续读取并输出至 TStringList 中。
447447
448448若要读取外部进程的输出数据,又无法使用 RunCommand,那就应该以下面的示例为基础编写生产代码。
449449```
450450program LargeOutputDemo;
451451
452452 {$mode objfpc}{$H+}
453453
454454 uses
455455 Classes, SysUtils, Process; // TProcess 位于 Process 单元内
456456
457457 const
458458 BUF_SIZE = 2048; // 分块读取输出数据的缓冲区大小
459459
460460 var
461461 AProcess : TProcess;
462462 OutputStream : TStream;
463463 BytesRead : longint;
464464 Buffer : array[1..BUF_SIZE] of byte;
465465
466466 begin
467467 // 设置 TProcess,
468468 // 这里采用递归搜索目录作为例子,因为这通常会生成大量数据
469469 AProcess := TProcess.Create(nil);
470470
471471 // Windows 和 *nix 的命令往往不同,因此用了 $IFDEFs
472472 {$IFDEF Windows}
473473 // Windows 中的 dir 是个内置命令,所以不能直接使用
474474 // 因此需要采用 cmd.exe 加参数的方式。
475475 AProcess.Executable := 'c:\windows\system32\cmd.exe';
476476 AProcess.Parameters.Add('/c');
477477 AProcess.Parameters.Add('dir /s c:\windows');
478478 {$ENDIF Windows}
479479
480480 {$IFDEF Unix}
481481 AProcess.Executable := '/bin/ls';
482482
483483 {$IFDEF Darwin}
484484 AProcess.Parameters.Add('-recursive');
485485 AProcess.Parameters.Add('-all');
486486 {$ENDIF Darwin}
487487
488488 {$IFDEF Linux}
489489 AProcess.Parameters.Add('--recursive');
490490 AProcess.Parameters.Add('--all');
491491 {$ENDIF Linux}
492492
493493 {$IFDEF FreeBSD}
494494 AProcess.Parameters.Add('-R');
495495 AProcess.Parameters.Add('-a');
496496 {$ENDIF FreeBSD}
497497
498498 AProcess.Parameters.Add('-l');
499499 {$ENDIF Unix}
500500
501501 // 用到了 poUsePipes 选项以便捕获输出结果。
502502 // 不能使用 poWaitOnExit 选项,因为会阻塞当前程序,也就无法读取进程输出的结果
503503 AProcess.Options := [poUsePipes];
504504
505505 // 启动进程(运行 dir 或 ls 命令)
506506 AProcess.Execute;
507507
508508 // 创建流对象,用于存放进程生成的输出数据。
509509 // 也可以采用文件流,直接将结果存入磁盘。
510510 OutputStream := TMemoryStream.Create;
511511
512512 // 循环读取 AProcess 输出的全部数据,直至无数据可读
513513 repeat
514514 // 读取由进程新生成的数据,最多读取已申请缓冲区的上限。
515515 // 注意每次 read(...) 都会阻塞式调用,只有最后一次除外(返回0)。
516516 BytesRead := AProcess.Output.Read(Buffer, BUF_SIZE);
517517
518518 // 将读到的字节加入流,以供后续使用
519519 OutputStream.Write(Buffer, BytesRead)
520520
521521 until BytesRead = 0; // 无数据可读时停止循环
522522
523523 // 进程结束,可被释放
524524 AProcess.Free;
525525
526526 // 全部读到的数据已可用,比如可存入磁盘
527527 with TFileStream.Create('output.txt', fmCreate) do
528528 begin
529529 OutputStream.Position := 0; // 需确保从头开始复制所有数据
530530 CopyFrom(OutputStream, OutputStream.Size);
531531 Free
532532 end;
533533
534534 // 或者可显示到屏幕上
535535 with TStringList.Create do
536536 begin
537537 OutputStream.Position := 0; // 需确保从头开始复制所有数据
538538 LoadFromStream(OutputStream);
539539 writeln(Text);
540540 writeln('--- Number of lines = ', Count, '----');
541541 Free
542542 end;
543543
544544 // Clean up
545545 OutputStream.Free;
546546 end.
547547```
548548
549549注意上述代码也可以利用 RunCommand 来实现:
550550```
551551var s: string;
552552 ...
553553 RunCommand('c:\windows\system32\cmd.exe', ['/c', 'dir /s c:\windows'], s);
554554```
555555
556556### TProcess 输入输出的用法
557557
558558请参阅 [Lazarus-CCR SVN](<https://lazarus-ccr.svn.sourceforge.net/svnroot/lazarus-ccr/examples/process>) 中的 processdemo 示例。
559559
560560### TProcess 用法提示
561561
562562在创建跨平台程序时,可用“{$IFDEF}”和“{$ENDIF}”指令根据各操作系统设置不同的可执行文件名称。
563563
564564示例:
565565```
566566{...}
567567 AProcess := TProcess.Create(nil)
568568
569569 {$IFDEF WIN32}
570570 AProcess.Executable := 'calc.exe';
571571 {$ENDIF}
572572
573573 {$IFDEF LINUX}
574574 AProcess.Executable := FindDefaultExecutablePath('kcalc');
575575 {$ENDIF}
576576
577577 AProcess.Execute;
578578 {...}
579579```
580580
581581### 前台显示 macOS 应用程序包
582582
583583TProcess 可启动**应用程序包** 中的可执行文件。例如:
584584```
585585AProcess.Executable:='/Applications/iCal.app/Contents/MacOS/iCal';
586586```
587587
588588上述代码将会启动 _Calendar_ ,但其窗口位于当前应用程序之后。若要让应用程序显示于前台,可用带 **-n** 参数的 **open** :
589589```
590590AProcess.Executable:='/usr/bin/open';
591591 AProcess.Parameters.Add('-n');
592592 AProcess.Parameters.Add('-a'); // 可选参数:指定应用程序,仅搜索 Application 目录
593593 AProcess.Parameters.Add('-W'); // 可选参数:open 保持等待,直至打开(或已打开状态)的外部程序退出
594594 AProcess.Parameters.Add('Pages.app'); // 可选的包含 .app
595595```
596596
597597如果应用程序需要参数,可以给 **open** 传入 **\--args** 参数,后面加入的所有参数都会传给应用程序:
598598```
599599AProcess.Parameters.Add('--args');
600600 AProcess.Parameters.Add('argument1');
601601 AProcess.Parameters.Add('argument2');
602602```
603603
604604参见:[[macOS Open Sesame|macOS open 命令]]
605605
606606### 让程序独立运行
607607
608608通常,由其他应用程序启动的是一个子进程,主应用终止时子应用也将被终止。若要启动一个保持运行的独立应用,可以采用以下方式:
609609```
610610var
611611 Process: TProcess;
612612 I: Integer;
613613 begin
614614 Process := TProcess.Create(nil);
615615 try
616616 Process.InheritHandles := False;
617617 Process.Options := [];
618618 Process.ShowWindow := swoShow;
619619
620620 // 复制包含 DISPLAY 在内的默认环境变量,以便 GUI 应用能够正常运行
621621 for I := 1 to GetEnvironmentVariableCount do
622622 Process.Environment.Add(GetEnvironmentString(I));
623623
624624 Process.Executable := '/usr/bin/gedit';
625625 Process.Execute;
626626 finally
627627 Process.Free;
628628 end;
629629 end;
630630```
631631
632632### 与 aspell 进程通讯的示例代码
633633
634634[pasdoc](<https://github.com/pasdoc/pasdoc/wiki>) 的源代码中有两个单元,演示了通过管道与正在运行的 aspell 进程进行通讯,以执行拼写检查功能:
635635
636636 * [PasDoc_ProcessLineTalk.pas 单元](<https://github.com/pasdoc/pasdoc/blob/master/source/component/PasDoc_ProcessLineTalk.pas>)实现了 TProcessLineTalk 类,衍生自 TProcess,便于以行为单位与任何进程进行对话。
637637 * [PasDoc_Aspell.pas 单元](<https://github.com/pasdoc/pasdoc/blob/master/source/component/PasDoc_Aspell.pas>)实现了 TAspellProcess 类,执行拼写检查功能,通过底层的 TProcessLineTalk 实例执行 aspell,并与运行中的 aspell 进 程通讯完成拼写检查.
638638
639639这两个单元都相对独立于其他 pasdoc 源代码,因此可作为用 TProcess 运行其它程序并通过管道与之通讯的真实案例。
640640
641641### 替换“| < >”之类的 shell 操作符
642642
643643有时需要运行更为复杂的命令,将数据通过管道传给另一个命令或文件。类似于:
644644```
645645ShellExecute('firstcommand.exe | secondcommand.exe');
646646```
647647
648648或
649649```
650650ShellExecute('dir > output.txt');
651651```
652652
653653这是 TProcess 无法完成的,比如:
654654```
655655// 无法生效
656656 Process.CommandLine := 'firstcommand.exe | secondcommand.exe';
657657 Process.Execute;
658658```
659659
660660#### 为什么用操作符重定向输出无效
661661
662662TProcess 不是 shell 环境,而是进程。进程只有一个,不是两个。不过还是有办法实现重定向输出的。请参阅[[Executing External Programs|下一节]]。
663663
664664### 如何实现 TProcess 输出重定向
665665
666666可为**每一条** 命令使用一个 TProcess 实例,将输出重定向至另一命令。
667667
668668以下示例说明了如何将一个进程的输出重定向至另一个进程。要将进程的输出重定向到文件/流,请参阅示例 [[Executing External Programs|大数据量输出的读取]]
669669
670670如果指定了 poStderrToOutPut 选项,则不仅可以重定向“常规” 输出(也称 stdout),还可以重定向错误输出 (stderr),参见第二个进程的选项。
671671```
672672program Project1;
673673
674674 uses
675675 Classes, sysutils, process;
676676
677677 var
678678 FirstProcess,
679679 SecondProcess: TProcess;
680680 Buffer: array[0..127] of char;
681681 ReadCount: Integer;
682682 ReadSize: Integer;
683683 begin
684684 FirstProcess := TProcess.Create(nil);
685685 SecondProcess := TProcess.Create(nil);
686686
687687 FirstProcess.Options := [poUsePipes];
688688 FirstProcess.Executable := 'pwd';
689689
690690 SecondProcess.Options := [poUsePipes,poStderrToOutPut];
691691 SecondProcess.Executable := 'grep';
692692 SecondProcess.Parameters.Add(DirectorySeparator+ ' -');
693693 // 等效于 "pwd | grep / -"
694694
695695 FirstProcess.Execute;
696696 SecondProcess.Execute;
697697
698698 while FirstProcess.Running or (FirstProcess.Output.NumBytesAvailable > 0) do
699699 begin
700700 if FirstProcess.Output.NumBytesAvailable > 0 then
701701 begin
702702 // 确保读取的数据不会超过申请的缓冲区大小
703703 ReadSize := FirstProcess.Output.NumBytesAvailable;
704704 if ReadSize > SizeOf(Buffer) then
705705 ReadSize := SizeOf(Buffer);
706706 // 将输出读入缓冲区
707707 ReadCount := FirstProcess.Output.Read(Buffer[0], ReadSize);
708708 // 将缓冲区内容写入第二个进程
709709 SecondProcess.Input.Write(Buffer[0], ReadCount);
710710
711711 // 如果 SecondProcess 输出数据太多,
712712 // 应当在此读出数据,以防死锁
713713 // 参见上面的例子 "大数据量输出的读取"
714714 end;
715715 end;
716716 // 关闭 SecondProcess 的输出
717717 // 数据处理结束
718718 SecondProcess.CloseInput;
719719
720720 // 等待完成
721721 // 小心所执行的命令可能在输入关闭时不会退出
722722 // 以下代码可能陷入死循环
723723 while SecondProcess.Running do
724724 Sleep(1);
725725 // 好了!
726726 // 余下的代码只是为了让本例稍显有用
727727
728728 // 复用一下缓冲区 Buffer,
729729 // 将 SecondProcess 的输出内容输出到本程序的 stdout 去
730730
731731 WriteLn('Grep output Start:');
732732 ReadSize := SecondProcess.Output.NumBytesAvailable;
733733 if ReadSize > SizeOf(Buffer) then
734734 ReadSize := SizeOf(Buffer);
735735 if ReadSize > 0 then
736736 begin
737737 ReadCount := SecondProcess.Output.Read(Buffer, ReadSize);
738738 WriteLn(Copy(Buffer,0, ReadCount));
739739 end
740740 else
741741 WriteLn('grep did not find what we searched for. ', SecondProcess.ExitStatus);
742742 WriteLn('Grep output Finish:');
743743
744744 // 释放进程对象
745745 FirstProcess.Free;
746746 SecondProcess.Free;
747747 end.
748748```
749749
750750就此结束。现在可以将输出重定向到另一个程序了。
751751
752752#### 注意
753753
754754上述例子貌似有些夸张了,因为可以通过 TProcess 再用 shell 运行“复杂”命令,比如:
755755```
756756Process.Commandline := 'sh -c "pwd | grep / -"';
757757```
758758
759759但上述例子更具跨平台性,无需修改即可在 Windows、Linux 等平台运行。“sh”可能不一定存在,一般仅在 *nix 平台可用。另外,上述例子的灵活性更高,因为可以单独读写每个进程的输、输出和 stderr,可能对项目开发非常有用。
760760
761761### 重定向输入和输出,并在 root 权限下运行
762762
763763在 Unix(FreeBSD、macOS)和 Linux 平台中,有一个常见问题就是需在 root(或更通用些是另一个用户)账户下执行某些程序。比如执行 _ping_ 命令。
764764
765765如果有 sudo 可用,则把以下例子改改即可实现,此例改自 andyman 在论坛上发布的一个帖子([[1]](<http://lazarus.freepascal.org/index.php/topic,14479.0.html>))。此例会在 `/root` 目录中运行 `ls` 命令,当然可以自行修改代码。
766766
767767更好的方案是使用 PolicyKit 包,所有新版 Linux 上都会提供。详情请参阅[论坛帖子](<http://lazarus.freepascal.org/index.php/topic,14479.0.html>)。
768768
769769以下代码的大部分与上述例子类似,不过还演示了如何将单独调用的进程的 stdout 和 stderr 重定向至当前代码的 stdout 和 stderr。
770770```
771771program rootls;
772772
773773 { 演示使用 TProcess 时将 stdout/stderr 重定向至当前 stdout/stderr,
774774 在 FreeBSD/Linux/macOS 中调用 sudo, 并在 stdin 提供输入}
775775 {$mode objfpc}{$H+}
776776
777777 uses
778778 Classes,
779779 Math, {提供 min 函数}
780780 Process;
781781
782782 procedure RunsLsRoot;
783783 var
784784 Proc: TProcess;
785785 CharBuffer: array [0..511] of char;
786786 ReadCount: integer;
787787 ExitCode: integer;
788788 SudoPassword: string;
789789 begin
790790 WriteLn('Please enter the sudo password:');
791791 Readln(SudoPassword);
792792 ExitCode := -1; //初始值为失败,观察后续是否生效
793793 Proc := TProcess.Create(nil); //新建进程
794794 try
795795 Proc.Options := [poUsePipes, poStderrToOutPut]; //用管道重定向程序的 stdin、stdout、stderr
796796 Proc.CommandLine := 'sudo -S ls /root'; //用 sudo 以 root 权限运行 ls /root
797797 // -S 使得 sudo 从 stdin 读取 root 账户的口令
798798 Proc.Execute; //启动进程。sudo 会要求输入口令
799799
800800 // 向 sudo 程序的 stdin 写入口令
801801 SudoPassword := SudoPassword + LineEnding;
802802 Proc.Input.Write(SudoPassword[1], Length(SudoPassword));
803803 SudoPassword := '%*'; //用短字符串填充,希望会稍稍搅乱一些内存数据,更安全的做法是用 PChars
804804 SudoPassword := ''; // 能让程序更难破解?!?
805805
806806 // 主循环从 sudo 的 stdout 和 stderr 读取输出数据
807807 while Proc.Running or (Proc.Output.NumBytesAvailable > 0) or
808808 (Proc.Stderr.NumBytesAvailable > 0) do
809809 begin
810810 // 读取 stdout 并写入当前 stdout
811811 while Proc.Output.NumBytesAvailable > 0 do
812812 begin
813813 ReadCount := Min(512, Proc.Output.NumBytesAvailable); //读入缓冲区,不超过最大值
814814 Proc.Output.Read(CharBuffer, ReadCount);
815815 Write(StdOut, Copy(CharBuffer, 0, ReadCount));
816816 end;
817817 // read stderr and write to our stderr
818818 while Proc.Stderr.NumBytesAvailable > 0 do
819819 begin
820820 ReadCount := Min(512, Proc.Stderr.NumBytesAvailable); //读入缓冲区,不超过最大值
821821 Proc.Stderr.Read(CharBuffer, ReadCount);
822822 Write(StdErr, Copy(CharBuffer, 0, ReadCount));
823823 end;
824824 end;
825825 ExitCode := Proc.ExitStatus;
826826 finally
827827 Proc.Free;
828828 Halt(ExitCode);
829829 end;
830830 end;
831831
832832 begin
833833 RunsLsRoot;
834834 end.
835835```
836836
837837其他想法:无疑最好是验证一下 sudo 是否真的提示输入口令。可将环境变量 SUDO_PROMPT 设为自定义值,并在读取 TProcess 的 stdout 时检查是否一致,这样即可避免不同语言环境下的提示符不同问题。设置环境变量会清除其默认值(从当前进程继承),因此用到的时候得去程序当前环境复制。
838838
839839### 在 Linux 中用 sudo 运行 fdisk
840840
841841以下例子展示了如何用 sudo 命令获取 root 权限,在 Linux 计算机上运行 fdisk。**注意:仅供演示,不适合大数据量输出。**
842842```
843843program getpartitioninfo;
844844 { {最初由 Lazarus 论坛 wjackon153 贡献,有问题、评论请联系他。
845845 由 BigChimp 将 Lazarus 代码修改为 FPC 程序, 便于理解和简化}
846846
847847 Uses
848848 Classes, SysUtils, FileUtil, Process;
849849
850850 var
851851 hprocess: TProcess;
852852 sPass: String;
853853 OutputLines: TStringList;
854854
855855 begin
856856 sPass := 'yoursudopasswordhere'; // 需要改为自己的 sudo 口令
857857 OutputLines:=TStringList.Create; // 最好用 try...finally 块
858858 // 确保 OutputLines 能被释放,hProcess 同理
859859
860860 // 以下代码将在后台打开 fdisk 并获取分区信息
861861 // 因为 fdisk 需要提权
862862 // 需要用 -S 选项将口令作为参数传给 sudo
863863 // 因此程序将进入等待状态,直至密码发送至 sudo 程序
864864 hProcess := TProcess.Create(nil);
865865 // 在 Linux/Unix/FreeBSD/macOS 中,需要指定可执行程序的完整路径
866866 hProcess.Executable := '/bin/sh';
867867 // 现在加入所有命令行参数
868868 hprocess.Parameters.Add('-c');
869869 // 现在将口令通过管道传给 sudo 命令,让它执行 fdisk -l
870870 hprocess.Parameters.add('echo ' + sPass + ' | sudo -S fdisk -l');
871871 // 同步方式执行(等待进程退出)并使用管道,以便由管道读取输出数据
872872 hProcess.Options := hProcess.Options + [poWaitOnExit, poUsePipes];
873873 // 执行进程
874874 hProcess.Execute;
875875
876876 // 现在 hProcess 应该将外部程序执行完毕(因为用了 poWaitOnExit)
877877 // 可以处理进程的输出数据了(stdout 和 strerr 等)
878878 OutputLines.Add('stdout:');
879879 OutputLines.LoadFromStream(hprocess.Output);
880880 OutputLines.Add('stderr:');
881881 OutputLines.LoadFromStream(hProcess.Stderr);
882882 // 在屏幕上显示输出数据
883883 writeln(OutputLines.Text);
884884
885885 // 释放资源,避免内存泄漏
886886 hProcess.Free;
887887 OutputLines.Free;
888888
889889 // 以下给出了一些示例,可传递非法字符,就像在终端中一样
890890 // 即使其他地方说是无法实现,我也保证以下方法有效 :)
891891
892892 //hprocess.Parameters.Add('ping -c 1 www.google.com');
893893 //hprocess.Parameters.Add('ifconfig wlan0 | grep ' + QuotedStr('inet addr:') + ' | cut -d: -f2');
894894
895895 // QuotedStr() 不是必需的,但可以让代码更清晰
896896 // 用双引号效果也一样
897897 //hprocess.Parameters.Add('glxinfo | grep direct');
898898
899899 // 以下方法还可以用于从软件包仓库中安装应用
900900
901901 //hprocess.Parameters.add('echo ' + sPass + ' | sudo -S apt-get install -y pkg-name');
902902
903903 end.
904904```
905905
906906### 包含空格的参数(替换 Shell 命令中的引号)
907907
908908Linux shell 可以接受引号包裹的参数,例如:
909909```
910910gdb --batch --eval-command="info symbol 0x0000DDDD" myprogram
911911```
912912
913913这里 GDB 可以接受 3 个参数(除第一个参数外),第一个参数是可执行文件的完整路径:
914914
915915 1. \--batch
916916 2. \--eval-command=info symbol 0x0000DDDD
917917 3. myprogram 的完整路径
918918
919919若要避免复杂的引号处理,最佳方案是在复杂场景下改用 TProcess.Parameters.Add,而不是直接给 CommandLine 赋值,例如::
920920```
921921AProcess.Executable:='/usr/bin/gdb';
922922 AProcess.Parameters.Add('--batch');
923923 AProcess.Parameters.Add('--eval-command=info symbol 0x0000DDDD'); // 注意这里没有用到引号
924924 AProcess.Parameters.Add('/home/me/myprogram');
925925```
926926
927927还请记得只传递完整路径。
928928
929929不过 TProcess.Commandline 确实支持对带有引号的参数进行一些简单的转义。用双引号包裹含有空格的 _整个_ 参数。比如:
930930```
931931AProcess.CommandLine := '/usr/bin/gdb --batch "--eval-command=info symbol 0x0000DDDD" /home/me/myprogram';
932932```
933933
934934.CommandLine 属性已[废弃](<http://bugs.freepascal.org/view.php?id=14446>),涉及更复杂的引号处理的缺陷报告,将不予接受或处理。
935935
936936## LCLIntf 替代方案
937937
938938有时不用显式调用外部程序,即可获取所需功能。不用既打开应用程序又把文档告诉它,只需让操作系统直接打开文档即可,操作系统会去选择与文档类型关联的默认应用。以下是一些示例。
939939
940940### 用默认应用程序打开文档
941941
942942某些时候需要用默认关联应用程序打开文档,而不是非要执行某个程序。不同的操作系统有不同的做法。Lazarus 提供了一个平台无关的过程 OpenDocument,可以完成这个操作。调用程序会继续运行,无需等待文档处理进程的关闭。
943943```
944944uses LCLIntf;
945945 ...
946946 OpenDocument('manual.pdf');
947947 ...
948948```
949949
950950 * [[opendocument|OpenDocument 参考手册]]
951951
952952### 用默认浏览器打开网页
953953
954954只需传入所需的 URL,在有些环境下好像可以省略前面的 <http://。此外,传入文件名可能效果等同于> OpenDocument()。
955955```
956956uses LCLIntf;
957957 ...
958958 OpenURL('www.lazarus.freepascal.org/');
959959```
960960
961961请参阅:
962962
963963 * [[OpenURL|OpenURL 参考手册]]
964964
965965或者可以以以下方式使用 **TProcess** :
966966```
967967uses Process;
968968
969969 procedure OpenWebPage(URL: string);
970970 // 看来传入的 URL 需用双引号包裹,比如 "www.lazarus.freepascal.org"
971971 var
972972 Browser, Params: string;
973973 begin
974974 FindDefaultBrowser(Browser, Params);
975975 with TProcess.Create(nil) do
976976 try
977977 Executable := Browser;
978978 Params:=Format(Params, [URL]);
979979 Params:=copy(Params,2,length(Params)-2); // 移除双引号,新版 TProcess.Parameters 会自动处理
980980 Parameters.Add(Params);
981981 Options := [poNoConsole];
982982 Execute;
983983 finally
984984 Free;
985985 end;
986986 end;
987987```
988988
989989## WinExec
990990
991991在陈旧的 Delphi 代码中,有时还会存在 Windows 3.x 时代的 CreateProcess。请用上述一种方案替换。CreateProcess 在 Windows 单元中依然存在,已标记为废弃状态。
992992
993993## 参阅
994994
995995 * [TProcess 文档](<http://lazarus-ccr.sourceforge.net/docs/fcl/process/tprocess.html>)
996996 * [[opendocument|OpenDocument]]
997997 * [OpenUrl](</index.php?title=OpenUrl&action=edit&redlink=1> "OpenUrl \(page does not exist\)")
998998 * [[TProcessUTF8]]
999999 * [[TXMLPropStorage]]
10001000 * [[Webbrowser|网页浏览器]]
10011001
10021002## 贡献者和更改
10031003
10041004 * 简体中文版本由 robsean 于 2019-03-07 创建。
10051005
10061006---
1007-*From the Free Pascal wiki, page "Executing External Programs/zh CN", as the Internet Archive kept it on 2025-02-07. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
1007+*From the Free Pascal wiki, page "Executing External Programs/zh CN", as the Internet Archive kept it on 2025-02-07, last changed on 2024-09-30. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history