Sign in Join
Wiki copytemporary

Executing External Programs/zh CN: what changed

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

OldNewLine
11执行外部程序
22
3-## Contents
4-
5- * 1 概述:对比
6- * 2 (Process.)RunCommand
7- * 2.1 RunCommand 的扩展版本
8- * 3 SysUtils.ExecuteProcess
9- * 4 微软 Windows 系统:CreateProcess、ShellExecute 和 WinExec
10- * 4.1 针对提权/管理员权限使用 ShellExecuteEx
11- * 5 Unix 的 fpsystem、fpexecve 和 shell
12- * 6 TProcess
13- * 6.1 最简单的示例
14- * 6.2 简单示例
15- * 6.3 改良后的示例(但仍不够正确)
16- * 6.4 大数据量输出的读取
17- * 6.5 TProcess 输入输出的用法
18- * 6.6 TProcess 用法提示
19- * 6.7 前台显示 macOS 应用程序包
20- * 6.8 让程序独立运行
21- * 6.9 与 aspell 进程通讯的示例代码
22- * 6.10 替换“| < >”之类的 shell 操作符
23- * 6.10.1 为什么用操作符重定向输出无效
24- * 6.11 如何实现 TProcess 输出重定向
25- * 6.11.1 注意
26- * 6.12 重定向输入和输出,并在 root 权限下运行
27- * 6.13 在 Linux 中用 sudo 运行 fdisk
28- * 6.14 包含空格的参数(替换 Shell 命令中的引号)
29- * 7 LCLIntf 替代方案
30- * 7.1 用默认应用程序打开文档
31- * 7.2 用默认浏览器打开网页
32- * 8 WinExec
33- * 9 参阅
34- * 10 贡献者和更改
35-
363## 概述:对比
374
385以下是 RTL、FCL 和 LCL 库中执行外部命令/进程/程序的各种方法。
396
407方法 | 库 | 平台 | 可单行完成 | 功能
418---|---|---|---|---
429[[Executing External Programs|ExecuteProcess]] | RTL | 跨平台 | 是 | 非常有限,以同步方式执行
4310[[Executing External Programs|ShellExecute]] | WinAPI | 仅限微软的 Windows 系统 | 是 | 很多。能够提权启动需管理员权限的程序。
4411[[Executing External Programs|fpsystem、fpexecve]] | Unix | 仅限 Unix 系统 | |
4512[[Executing External Programs|TProcess]] | FCL | 跨平台 | 否 | 完整
4613[[Executing External Programs|RunCommand]]RunCommand> "Executing External Programs") | FCL | 跨平台 需要FPC 2.6.2+ | 是 | 支持常用的 TProcess 用法
4714[[Executing External Programs|OpenDocument]] | LCL | 跨平台 | 是 | 只能打开文档。将会用该文档类型关联的应用程序打开。
4815
4916## (Process.)RunCommand
5017
5118 * [RunCommand](<http://www.freepascal.org/docs-html/fcl/process/runcommand.html>)参考手册
5219 * [RunCommandInDir](<http://www.freepascal.org/docs-html/fcl/process/runcommandindir.html>)参考手册
5320
5421FPC 2.6.2 为 process 单元的 TProcess 加入了一些有用的函数,来自 [[Projects using Lazarus|fpcup]] 项目的封装。 这些函数适用于初、中级应用,能将输出捕获到单个字符串中,并完全支持 _大数据量输出_ 。
5522
5623以下是简单示例:
5724```
5825program project1;
5926
6027 {$mode objfpc}{$H+}
6128
6229 uses
6330 Process;
6431
6532 var
6633 s : ansistring;
6734
6835 begin
6936
7037 if RunCommand('/bin/bash',['-c','echo $PATH'],s) then
7138 writeln(s);
7239
7340 end.
7441```
7542
7643但请注意,并不是所有 shell“内置”命令(如 alias)都能支持,因为默认情况下,在非交互式 shell 中不会用到别名。除非设置了 BASH_ENV 环境变量,不然非交互式 shell 也不会读取 .bashrc 文件。因此,以下代码不会输出任何结果:
7744```
7845program project2;
7946
8047 {$mode objfpc}{$H+}
8148
8249 uses
8350 Process;
8451
8552 var
8653 s : ansistring;
8754
8855 begin
8956
9057 if RunCommand('/bin/bash',['-c','alias'],s) then
9158 writeln(s);
9259
9360 end.
9461```
9562
9663RunCommand 有一个重载版本,可返回程序的退出码。RunCommandInDir 则可在指定目录中运行命令(设置 p.CurrentDirectory):
9764```
9865function RunCommandIndir(const curdir:string;const exename:string;const commands:array of string;var outputstring:string; var exitstatus:integer): integer;
9966 function RunCommandIndir(const curdir:string;const exename:string;const commands:array of string;var outputstring:string): boolean;
10067 function RunCommand(const exename:string;const commands:array of string;var outputstring:string): boolean;
10168```
10269
10370在 FPC 3.2.0 以上版本中,Runcommand 有了新的版本,允许覆盖 TProcessOptions 和 TShowWindowOptions。
10471
10572
10673
10774### RunCommand 的扩展版本
10875
10976在 **FPC 3.2.0 以上** 版本中,RunCommand 实现了泛型化,并重新归入了 TProcess,以便能更迅速地构建自己的 RunCommand。以下是带有超时机制的 RunCommand:
11077```
11178program TestTProcessTimeout;
11279
11380 uses classes, sysutils, process, dateutils;
11481
11582 type
11683 { TProcessTimeout }
11784 TProcessTimeout = class(TProcess)
11885 protected
11986 timeoutperiod: TTime;
12087 timedout : boolean;
12188 started : TDateTime;
12289 procedure LocalnIdleSleep(Sender,Context : TObject;status:TRunCommandEventCode;const message:string);
12390 public
12491 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;
12592 end;
12693
12794 procedure TProcessTimeout.LocalnIdleSleep(Sender,Context : TObject;status:TRunCommandEventCode;const message:string);
12895 begin
12996 if status=RunCommandIdle then
13097 begin
13198 writeln(Executable+'('+ Parameters.CommaText+')' +' time: '+TimeToStr(now-started));
13299 if (now-started)>timeoutperiod then
133100 begin
134101 writeln('process timed out ');
135102 timedout:=true;
136103 Terminate(255);
137104 exit;
138105 end;
139106 sleep(RunCommandSleepTime);
140107 end;
141108 end;
142109
143110 class function TProcessTimeout.RunCommandwithTimeout(
144111 const exename : TProcessString;
145112 const commands : array of TProcessString;
146113 const dir : string;
147114 out outputstring : string;
148115 out errorstring : string;
149116 out exitstate : integer;
150117 ProcessOptions : TProcessOptions = [];
151118 SWOptions : TShowWindowOptions=swoNone;
152119 timeout : integer=15
153120 ):boolean;
154121 Var
155122 p : TProcessTimeout;
156123 i : integer;
157124 begin
158125 p:=TProcessTimeout.create(nil);
159126 p.OnRunCommandEvent:=@p.LocalnIdleSleep;
160127 p.CurrentDirectory:=dir;
161128
162129 //timeout 单位为分钟,每秒检测一次
163130 p.timeoutperiod:=timeout/MinsPerDay;
164131
165132 (*
166133 //若要求 Timeout 单位为秒
167134 p.timeoutperiod:=timeout/SecsPerDay;
168135 *)
169136
170137 // 每秒检测一次是否该结束
171138 p.runcommandsleeptime := 1000;
172139
173140 if ProcessOptions<>[] then
174141 P.Options:=ProcessOptions - [poRunSuspended,poWaitOnExit];
175142 p.options:=p.options+[poRunIdle]; // 需要运行 RUNIDLE 事件。参见 User Changes 3.2.0
176143
177144 P.ShowWindow:=SwOptions;
178145 p.Executable:=exename;
179146 if high(commands)>=0 then
180147 for i:=low(commands) to high(commands) do
181148 p.Parameters.add(commands[i]);
182149 p.timedout:=false;
183150 p.started:=now;
184151 try
185152 // runcommand() 的主循环。最初基于本 wiki 内的“大数据量输出”场景,只是 5 年内不断有所扩展。
186153 RunCommandwithTimeout:=p.RunCommandLoop(outputstring,errorstring,exitstate)=0;
187154 if p.timedout then
188155 RunCommandwithTimeout:=false;
189156 finally
190157 p.free;
191158 end;
192159 if exitstate<>0 then RunCommandwithTimeout:=false;
193160 end;
194161
195162
196163 // 用法示例
197164
198165 var
199166 output,errors : string;
200167 exitcode :integer;
201168 begin
202169 //Windows 和 Unix 还有其他程序可用
203170 {$if defined(WINDOWS)}
204171 if TProcessTimeout.RunCommandwithTimeout('cmd',['/c','dir','C:\*.exe','/s'],'c:\',output,errors,exitcode,[],swoNone,1) then // dir c:\*.exe /s
205172 {$else$}
206173 if TProcessTimeout.RunCommandwithTimeout('find',['/','-name','"*.sh"'],'/',output,errors,exitcode,[],swoNone,1) then // find / -name "*.sh"
207174 {$endif}
208175 begin
209176 writeln('program finished');
210177 writeln('--------------------output-------------------');
211178 writeln(output);
212179 writeln('--------------------errors-------------------');
213180 writeln(errors);
214181 if not(exitcode=0) then
215182 begin
216183 writeln('-program finished with errors (exitcode: '+exitcode.tostring+')-');
217184 end;
218185 readln();
219186 end
220187 else
221188 begin
222189 writeln('program not finished');
223190 writeln('--------------------output-------------------');
224191 writeln(output);
225192 writeln('--------------------errors-------------------');
226193 writeln(errors);
227194 writeln('-------------------EXITCODE------------------');
228195 writeln(exitcode);
229196 readln();
230197 end;
231198 end.
232199```
233200
234201## SysUtils.ExecuteProcess
235202
236203(跨平台)
237204
238205尽管存在很多限制,但启动一个程序(模态、没有管道或任何控制)最简单的方式就是用:
239206```
240207SysUtils.ExecuteProcess(UTF8ToSys('/full/path/to/binary'), '', []);
241208```
242209
243210发起调用的进程将以同步方式运行:暂时“挂起”直至外部程序运行完毕 - 但如果要求用户在程序继续运行之前执行某些操作,同步方式可能很有用。更多的执行方式请参阅推荐的跨平台 **RunCommand** 或其他 **TProcess** 函数;如果只需要考虑 Windows 系统,则可以使用 **ShellExecute** 。
244211
245212调用进程同步运行:它'挂起(hangs)',直到内部出现完成 - 但是,在你的程序中继续前,如果你想要用户来做 一些事,这可能是有用的。对于更多用途的方法,看下一关于优先考虑跨平台的**TProcess** 部分, 或如果你仅希望瞄准Windows,你可以使用**ShellExecute** 。
246213
247214 * [ExecuteProcess 参考手册](<http://www.freepascal.org/docs-html/rtl/sysutils/executeprocess.html>)
248215
249216## 微软 Windows 系统:CreateProcess、ShellExecute 和 WinExec
250217
251218![Light bulb](https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png) **Note:** 虽然 FPC/Lazarus 已支持 **CreateProcess** 、**ShellExecute** 和 **WinExec** ,但仅限 Win32/64环境。跨平台程序请考虑采用 **RunCommand** 或 **TProcess** 。
252219
253220![Light bulb](https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png) **Note:** WinExec 是 16 位调用,在 Windows API 中已废弃多年。最新版的 FPC 中会对其生成警告。
254221
255222**ShellExecute** 是标准的微软 Windows 函数(ShellApi.h),[documentation on MSDN](<http://msdn.microsoft.com/en-us/library/windows/desktop/bb762153\(v=vs.85\).aspx>) 中有完善的文档。(如果函数无法可靠运行,请注意查看关于初始化 COM 的评论).
256223```
257224uses ..., ShellApi;
258225
259226 // 简单的单行代码调用(忽略返回错误):
260227 if ShellExecute(0,nil, PChar('"C:\my dir\prog.exe"'),PChar('"C:\somepath\some_doc.ext"'),nil,1) =0 then;
261228
262229 // 执行批命令:
263230 if ShellExecute(0,nil, PChar('cmd'),PChar('/c mybatch.bat'),nil,1) =0 then;
264231
265232 // 在给定目录打开命令行窗口:
266233 if ShellExecute(0,nil, PChar('cmd'),PChar('/k cd \path'),nil,1) =0 then;
267234
268235 // 用“start”命令在默认浏览器中打开 URL(通过短暂显示并隐藏的 cmd 窗口):
269236 if ShellExecute(0,nil, PChar('cmd'),PChar('/c start www.lazarus.freepascal.org/'),nil,0) =0 then;
270237
271238 // 助手函数:
272239 procedure RunShellExecute(const prog,params:string);
273240 begin
274241 // ( Handle, nil/'open'/'edit'/'find'/'explore'/'print', // 'open' 不是必需的
275242 // path+prog, params, working folder,
276243 // 0=hide / 1=SW_SHOWNORMAL / 3=max / 7=min) //要使用 SW_ constants 请 uses Windows 单元
277244 if ShellExecute(0,'open',PChar(prog),PChar(params),PChar(extractfilepath(prog)),1) >32 then; //success
278245 // 返回 0..32 表示出错
279246 end;
280247```
281248
282249还有个 WideChar 版本的 ShellExecuteExW,而 ShellExecuteExA 则是 AnsiChar 版。
283250
284251fMask 选项还可以是 SEE_MASK_DOENVSUBST、SEE_MASK_FLAG_NO_UI、SEE_MASK_NOCLOSEPROCESS 等。
285252
286253若是在 Delphi 中,要对 Word 文档或 URL 之类的 **文档** 调用 ShellExecute ,请查阅 lclintf 中的 open*(OpenUrl 等)函数(参见本页下方的替代方案)。
287254
288255### 针对提权/管理员权限使用 ShellExecuteEx
289256
290257若要用管理员/提权的权限执行外部程序,可使用各版本 ShellExecuteEx 函数的 **runas** 方法:
291258```
292259uses ShellApi, ...;
293260
294261 function RunAsAdmin(const Handle: Hwnd; const Path, Params: string): Boolean;
295262 var
296263 sei: TShellExecuteInfoA;
297264 begin
298265 FillChar(sei, SizeOf(sei), 0);
299266 sei.cbSize := SizeOf(sei);
300267 sei.Wnd := Handle;
301268 sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
302269 sei.lpVerb := 'runas';
303270 sei.lpFile := PAnsiChar(Path);
304271 sei.lpParameters := PAnsiChar(Params);
305272 sei.nShow := SW_SHOWNORMAL;
306273 Result := ShellExecuteExA(@sei);
307274 end;
308275
309276 procedure TFormMain.RunAddOrRemoveApplication;
310277 begin
311278 // 用提权后的 rundll 打开控制面板,访问“程序和功能”面板
312279 RunAsAdmin(FormMain.Handle, 'rundll32.exe shell32.dll,Control_RunDLL appwiz.cpl', '');
313280 end;
314281```
315282
316283## Unix 的 fpsystem、fpexecve 和 shell
317284
318285这些函数是依赖于特定平台的。
319286
320287 * [fpsystem 参考手册](<http://www.freepascal.org/docs-html/rtl/unix/fpsystem.html>)
321288 * [fpexecve 参考手册](<http://www.freepascal.org/docs-html/rtl/baseunix/fpexecve.html>)
322289
323290Linux.Shell/Unix.Shell 相当于 fpsystem 的 1.0.x 版本,但其错误处理方式定义得不太严格,经过长时间废弃后最终被移除了。Shell 几乎在任何场合下都可替换为 fpsystem,后者的错误处理方式更像 POSIX。
324291
325292## TProcess
326293
327294TProcess 可用于启动外部程序。采用 TProcess 有一些好处:
328295
329296 * 平台无关
330297 * 支持读取 stdout 和写入 stdin。
331298 * 程序可以等待命令完成,也可以在命令运行的同时继续运行。
332299
333300重要注意事项:
334301
335302 * TProcess 不是终端/shell!不能直接执行脚本,不能用“|”、“>”、“<”、“&”之类的运算符重定向输出。通过调用 TProcess 的 pascal 代码,可以得到相同的效果,下面有一些示例。
336303 * 在 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") 函数。
337304 * 在 Windows 中,如果命令位于 PATH 中,则无需给定完整路径。
338305 * [TProcess 参考手册](<http://lazarus-ccr.sourceforge.net/docs/fcl/process/tprocess.html> "doc:fcl/process/tprocess.html")
339306
340307### 最简单的示例
341308
342309在 [[Executing External Programs|Runcommand]] 函数中已给出了很多例程。在开始复制和粘贴以下示例之前,请先予以查看。
343310
344311### 简单示例
345312
346313本例仅仅是展示运行外部程序的方法,**不能用于生产性代码,请参阅大数据量输出或更好的[[Executing External Programs|Runcommand]]**。
347314```
348315// 本例程展示了如何启动外部程序
349316 program launchprogram;
350317
351318 // 包含必要的单元文件,里面有需要用到的函数和过程
352319 uses
353320 Classes, SysUtils, Process;
354321
355322 // 定义 "TProcess" 类型的变量 "AProcess"
356323 var
357324 AProcess: TProcess;
358325
359326 // 开始运行
360327 begin
361328 // 创建 TProcess 对象并赋给变量 AProcess
362329 AProcess := TProcess.Create(nil);
363330
364331 // 将需要执行的外部程序告知 AProcess
365332 // 这里要调用 Free Pascal 编译器(i386版)
366333 AProcess.Executable:= 'ppc386';
367334
368335 // 给 ppc386 带上参数 -h,实际执行的是 'ppc386 -h' 命令:
369336 AProcess.Parameters.Add('-h');
370337
371338 // 定义运行选项
372339 // 让主程序暂停运行,直至外部程序停止执行。
373340 // vvvvvvvvvvvvvv
374341 AProcess.Options := AProcess.Options + [poWaitOnExit];
375342
376343 // 让 AProcess 运行外部程序
377344 AProcess.Execute;
378345
379346 // ppc386 停止执行后才会运行至此
380347 AProcess.Free;
381348 end.
382349```
383350
384351这样就可以从自己的程序中运行外部程序了。
385352
386353### 改良后的示例(但仍不够正确)
387354
388355上述例子运行良好,但外部程序的输出结果该如何读取呢?
389356
390357那就稍微添加一些代码来实现吧: **为了便于学习,本例程已尽量做了简化。请不要在产品代码中使用本例,而应采用#大数据量输出的读取中的代码**。
391358```
392359// 本示例待有缺陷,
393360 // 演示了如何启动外部程序,并读取其输出
394361 program launchprogram;
395362
396363 // 包含必要的单元文件,内有需要用到的函数和过程
397364 uses
398365 Classes, SysUtils, Process;
399366
400367 // 定义 "TProcess" 类型的变量 "AProcess"
401368 // 还增加了一个 TStringList,用于存放从外部程序输出中读取的数据。
402369 var
403370 AProcess: TProcess;
404371 AStringList: TStringList;
405372
406373 // 开始运行
407374 begin
408375 // 创建 TProcess 对象并赋给变量 AProcess
409376 AProcess := TProcess.Create(nil);
410377
411378 // 将需要执行的外部程序告知 AProcess
412379 AProcess.Executable := '/usr/bin/ppc386';
413380 AProcess.Parameters.Add('-h');
414381
415382 // 定义执行时的选项
416383 // 确保主程序暂停运行,直至外部程序停止执行。
417384 // 还要读取输出
418385 AProcess.Options := AProcess.Options + [poWaitOnExit, poUsePipes];
419386
420387 // AProcess 知道要执行的命令行了
421388 AProcess.Execute;
422389
423390 // AProcess 执行完毕后,继续运行
424391
425392 // 读取刚执行完毕的外部程序的输出,并存入 TStringList。
426393 AStringList := TStringList.Create;
427394 AStringList.LoadFromStream(AProcess.Output);
428395
429396 // 将输出结果存入文件,并清空 TStringList。
430397 AStringList.SaveToFile('output.txt');
431398 AStringList.Free;
432399
433400 // 现在 AProcess 的输出处理完毕,可以释放了
434401 AProcess.Free;
435402 end.
436403```
437404
438405### 大数据量输出的读取
439406
440407上一示例中,主程序在外部程序退出之前一直处于等待状态,然后再读取其写到输出中去的数据。
441408
442409假定外部程序会往输出写入很多数据。这就会填满输出管道,然后被调用的程序就会进入等待状态,直至管道中的数据被读走。
443410
444411但在被调用程序结束之前,调用方程序不会去读取其输出。于是死锁发生了。
445412
446413因此以下示例不采用 poWaitOnExit 方式,而是在执行外部程序的同时读取其输出。输出数据存于内存流中,以供后续读取并输出至 TStringList 中。
447414
448415若要读取外部进程的输出数据,又无法使用 RunCommand,那就应该以下面的示例为基础编写生产代码。
449416```
450417program LargeOutputDemo;
451418
452419 {$mode objfpc}{$H+}
453420
454421 uses
455422 Classes, SysUtils, Process; // TProcess 位于 Process 单元内
456423
457424 const
458425 BUF_SIZE = 2048; // 分块读取输出数据的缓冲区大小
459426
460427 var
461428 AProcess : TProcess;
462429 OutputStream : TStream;
463430 BytesRead : longint;
464431 Buffer : array[1..BUF_SIZE] of byte;
465432
466433 begin
467434 // 设置 TProcess,
468435 // 这里采用递归搜索目录作为例子,因为这通常会生成大量数据
469436 AProcess := TProcess.Create(nil);
470437
471438 // Windows 和 *nix 的命令往往不同,因此用了 $IFDEFs
472439 {$IFDEF Windows}
473440 // Windows 中的 dir 是个内置命令,所以不能直接使用
474441 // 因此需要采用 cmd.exe 加参数的方式。
475442 AProcess.Executable := 'c:\windows\system32\cmd.exe';
476443 AProcess.Parameters.Add('/c');
477444 AProcess.Parameters.Add('dir /s c:\windows');
478445 {$ENDIF Windows}
479446
480447 {$IFDEF Unix}
481448 AProcess.Executable := '/bin/ls';
482449
483450 {$IFDEF Darwin}
484451 AProcess.Parameters.Add('-recursive');
485452 AProcess.Parameters.Add('-all');
486453 {$ENDIF Darwin}
487454
488455 {$IFDEF Linux}
489456 AProcess.Parameters.Add('--recursive');
490457 AProcess.Parameters.Add('--all');
491458 {$ENDIF Linux}
492459
493460 {$IFDEF FreeBSD}
494461 AProcess.Parameters.Add('-R');
495462 AProcess.Parameters.Add('-a');
496463 {$ENDIF FreeBSD}
497464
498465 AProcess.Parameters.Add('-l');
499466 {$ENDIF Unix}
500467
501468 // 用到了 poUsePipes 选项以便捕获输出结果。
502469 // 不能使用 poWaitOnExit 选项,因为会阻塞当前程序,也就无法读取进程输出的结果
503470 AProcess.Options := [poUsePipes];
504471
505472 // 启动进程(运行 dir 或 ls 命令)
506473 AProcess.Execute;
507474
508475 // 创建流对象,用于存放进程生成的输出数据。
509476 // 也可以采用文件流,直接将结果存入磁盘。
510477 OutputStream := TMemoryStream.Create;
511478
512479 // 循环读取 AProcess 输出的全部数据,直至无数据可读
513480 repeat
514481 // 读取由进程新生成的数据,最多读取已申请缓冲区的上限。
515482 // 注意每次 read(...) 都会阻塞式调用,只有最后一次除外(返回0)。
516483 BytesRead := AProcess.Output.Read(Buffer, BUF_SIZE);
517484
518485 // 将读到的字节加入流,以供后续使用
519486 OutputStream.Write(Buffer, BytesRead)
520487
521488 until BytesRead = 0; // 无数据可读时停止循环
522489
523490 // 进程结束,可被释放
524491 AProcess.Free;
525492
526493 // 全部读到的数据已可用,比如可存入磁盘
527494 with TFileStream.Create('output.txt', fmCreate) do
528495 begin
529496 OutputStream.Position := 0; // 需确保从头开始复制所有数据
530497 CopyFrom(OutputStream, OutputStream.Size);
531498 Free
532499 end;
533500
534501 // 或者可显示到屏幕上
535502 with TStringList.Create do
536503 begin
537504 OutputStream.Position := 0; // 需确保从头开始复制所有数据
538505 LoadFromStream(OutputStream);
539506 writeln(Text);
540507 writeln('--- Number of lines = ', Count, '----');
541508 Free
542509 end;
543510
544511 // Clean up
545512 OutputStream.Free;
546513 end.
547514```
548515
549516注意上述代码也可以利用 RunCommand 来实现:
550517```
551518var s: string;
552519 ...
553520 RunCommand('c:\windows\system32\cmd.exe', ['/c', 'dir /s c:\windows'], s);
554521```
555522
556523### TProcess 输入输出的用法
557524
558525请参阅 [Lazarus-CCR SVN](<https://lazarus-ccr.svn.sourceforge.net/svnroot/lazarus-ccr/examples/process>) 中的 processdemo 示例。
559526
560527### TProcess 用法提示
561528
562529在创建跨平台程序时,可用“{$IFDEF}”和“{$ENDIF}”指令根据各操作系统设置不同的可执行文件名称。
563530
564531示例:
565532```
566533{...}
567534 AProcess := TProcess.Create(nil)
568535
569536 {$IFDEF WIN32}
570537 AProcess.Executable := 'calc.exe';
571538 {$ENDIF}
572539
573540 {$IFDEF LINUX}
574541 AProcess.Executable := FindDefaultExecutablePath('kcalc');
575542 {$ENDIF}
576543
577544 AProcess.Execute;
578545 {...}
579546```
580547
581548### 前台显示 macOS 应用程序包
582549
583550TProcess 可启动**应用程序包** 中的可执行文件。例如:
584551```
585552AProcess.Executable:='/Applications/iCal.app/Contents/MacOS/iCal';
586553```
587554
588555上述代码将会启动 _Calendar_ ,但其窗口位于当前应用程序之后。若要让应用程序显示于前台,可用带 **-n** 参数的 **open** :
589556```
590557AProcess.Executable:='/usr/bin/open';
591558 AProcess.Parameters.Add('-n');
592559 AProcess.Parameters.Add('-a'); // 可选参数:指定应用程序,仅搜索 Application 目录
593560 AProcess.Parameters.Add('-W'); // 可选参数:open 保持等待,直至打开(或已打开状态)的外部程序退出
594561 AProcess.Parameters.Add('Pages.app'); // 可选的包含 .app
595562```
596563
597564如果应用程序需要参数,可以给 **open** 传入 **\--args** 参数,后面加入的所有参数都会传给应用程序:
598565```
599566AProcess.Parameters.Add('--args');
600567 AProcess.Parameters.Add('argument1');
601568 AProcess.Parameters.Add('argument2');
602569```
603570
604571参见:[[macOS Open Sesame|macOS open 命令]]
605572
606573### 让程序独立运行
607574
608575通常,由其他应用程序启动的是一个子进程,主应用终止时子应用也将被终止。若要启动一个保持运行的独立应用,可以采用以下方式:
609576```
610577var
611578 Process: TProcess;
612579 I: Integer;
613580 begin
614581 Process := TProcess.Create(nil);
615582 try
616583 Process.InheritHandles := False;
617584 Process.Options := [];
618585 Process.ShowWindow := swoShow;
619586
620587 // 复制包含 DISPLAY 在内的默认环境变量,以便 GUI 应用能够正常运行
621588 for I := 1 to GetEnvironmentVariableCount do
622589 Process.Environment.Add(GetEnvironmentString(I));
623590
624591 Process.Executable := '/usr/bin/gedit';
625592 Process.Execute;
626593 finally
627594 Process.Free;
628595 end;
629596 end;
630597```
631598
632599### 与 aspell 进程通讯的示例代码
633600
634601[pasdoc](<https://github.com/pasdoc/pasdoc/wiki>) 的源代码中有两个单元,演示了通过管道与正在运行的 aspell 进程进行通讯,以执行拼写检查功能:
635602
636603 * [PasDoc_ProcessLineTalk.pas 单元](<https://github.com/pasdoc/pasdoc/blob/master/source/component/PasDoc_ProcessLineTalk.pas>)实现了 TProcessLineTalk 类,衍生自 TProcess,便于以行为单位与任何进程进行对话。
637604 * [PasDoc_Aspell.pas 单元](<https://github.com/pasdoc/pasdoc/blob/master/source/component/PasDoc_Aspell.pas>)实现了 TAspellProcess 类,执行拼写检查功能,通过底层的 TProcessLineTalk 实例执行 aspell,并与运行中的 aspell 进 程通讯完成拼写检查.
638605
639606这两个单元都相对独立于其他 pasdoc 源代码,因此可作为用 TProcess 运行其它程序并通过管道与之通讯的真实案例。
640607
641608### 替换“| < >”之类的 shell 操作符
642609
643610有时需要运行更为复杂的命令,将数据通过管道传给另一个命令或文件。类似于:
644611```
645612ShellExecute('firstcommand.exe | secondcommand.exe');
646613```
647614
648615或
649616```
650617ShellExecute('dir > output.txt');
651618```
652619
653620这是 TProcess 无法完成的,比如:
654621```
655622// 无法生效
656623 Process.CommandLine := 'firstcommand.exe | secondcommand.exe';
657624 Process.Execute;
658625```
659626
660627#### 为什么用操作符重定向输出无效
661628
662629TProcess 不是 shell 环境,而是进程。进程只有一个,不是两个。不过还是有办法实现重定向输出的。请参阅[[Executing External Programs|下一节]]。
663630
664631### 如何实现 TProcess 输出重定向
665632
666633可为**每一条** 命令使用一个 TProcess 实例,将输出重定向至另一命令。
667634
668635以下示例说明了如何将一个进程的输出重定向至另一个进程。要将进程的输出重定向到文件/流,请参阅示例 [[Executing External Programs|大数据量输出的读取]]
669636
670637如果指定了 poStderrToOutPut 选项,则不仅可以重定向“常规” 输出(也称 stdout),还可以重定向错误输出 (stderr),参见第二个进程的选项。
671638```
672639program Project1;
673640
674641 uses
675642 Classes, sysutils, process;
676643
677644 var
678645 FirstProcess,
679646 SecondProcess: TProcess;
680647 Buffer: array[0..127] of char;
681648 ReadCount: Integer;
682649 ReadSize: Integer;
683650 begin
684651 FirstProcess := TProcess.Create(nil);
685652 SecondProcess := TProcess.Create(nil);
686653
687654 FirstProcess.Options := [poUsePipes];
688655 FirstProcess.Executable := 'pwd';
689656
690657 SecondProcess.Options := [poUsePipes,poStderrToOutPut];
691658 SecondProcess.Executable := 'grep';
692659 SecondProcess.Parameters.Add(DirectorySeparator+ ' -');
693660 // 等效于 "pwd | grep / -"
694661
695662 FirstProcess.Execute;
696663 SecondProcess.Execute;
697664
698665 while FirstProcess.Running or (FirstProcess.Output.NumBytesAvailable > 0) do
699666 begin
700667 if FirstProcess.Output.NumBytesAvailable > 0 then
701668 begin
702669 // 确保读取的数据不会超过申请的缓冲区大小
703670 ReadSize := FirstProcess.Output.NumBytesAvailable;
704671 if ReadSize > SizeOf(Buffer) then
705672 ReadSize := SizeOf(Buffer);
706673 // 将输出读入缓冲区
707674 ReadCount := FirstProcess.Output.Read(Buffer[0], ReadSize);
708675 // 将缓冲区内容写入第二个进程
709676 SecondProcess.Input.Write(Buffer[0], ReadCount);
710677
711678 // 如果 SecondProcess 输出数据太多,
712679 // 应当在此读出数据,以防死锁
713680 // 参见上面的例子 "大数据量输出的读取"
714681 end;
715682 end;
716683 // 关闭 SecondProcess 的输出
717684 // 数据处理结束
718685 SecondProcess.CloseInput;
719686
720687 // 等待完成
721688 // 小心所执行的命令可能在输入关闭时不会退出
722689 // 以下代码可能陷入死循环
723690 while SecondProcess.Running do
724691 Sleep(1);
725692 // 好了!
726693 // 余下的代码只是为了让本例稍显有用
727694
728695 // 复用一下缓冲区 Buffer,
729696 // 将 SecondProcess 的输出内容输出到本程序的 stdout 去
730697
731698 WriteLn('Grep output Start:');
732699 ReadSize := SecondProcess.Output.NumBytesAvailable;
733700 if ReadSize > SizeOf(Buffer) then
734701 ReadSize := SizeOf(Buffer);
735702 if ReadSize > 0 then
736703 begin
737704 ReadCount := SecondProcess.Output.Read(Buffer, ReadSize);
738705 WriteLn(Copy(Buffer,0, ReadCount));
739706 end
740707 else
741708 WriteLn('grep did not find what we searched for. ', SecondProcess.ExitStatus);
742709 WriteLn('Grep output Finish:');
743710
744711 // 释放进程对象
745712 FirstProcess.Free;
746713 SecondProcess.Free;
747714 end.
748715```
749716
750717就此结束。现在可以将输出重定向到另一个程序了。
751718
752719#### 注意
753720
754721上述例子貌似有些夸张了,因为可以通过 TProcess 再用 shell 运行“复杂”命令,比如:
755722```
756723Process.Commandline := 'sh -c "pwd | grep / -"';
757724```
758725
759726但上述例子更具跨平台性,无需修改即可在 Windows、Linux 等平台运行。“sh”可能不一定存在,一般仅在 *nix 平台可用。另外,上述例子的灵活性更高,因为可以单独读写每个进程的输、输出和 stderr,可能对项目开发非常有用。
760727
761728### 重定向输入和输出,并在 root 权限下运行
762729
763730在 Unix(FreeBSD、macOS)和 Linux 平台中,有一个常见问题就是需在 root(或更通用些是另一个用户)账户下执行某些程序。比如执行 _ping_ 命令。
764731
765732如果有 sudo 可用,则把以下例子改改即可实现,此例改自 andyman 在论坛上发布的一个帖子([[1]](<http://lazarus.freepascal.org/index.php/topic,14479.0.html>))。此例会在 `/root` 目录中运行 `ls` 命令,当然可以自行修改代码。
766733
767734更好的方案是使用 PolicyKit 包,所有新版 Linux 上都会提供。详情请参阅[论坛帖子](<http://lazarus.freepascal.org/index.php/topic,14479.0.html>)。
768735
769736以下代码的大部分与上述例子类似,不过还演示了如何将单独调用的进程的 stdout 和 stderr 重定向至当前代码的 stdout 和 stderr。
770737```
771738program rootls;
772739
773740 { 演示使用 TProcess 时将 stdout/stderr 重定向至当前 stdout/stderr,
774741 在 FreeBSD/Linux/macOS 中调用 sudo, 并在 stdin 提供输入}
775742 {$mode objfpc}{$H+}
776743
777744 uses
778745 Classes,
779746 Math, {提供 min 函数}
780747 Process;
781748
782749 procedure RunsLsRoot;
783750 var
784751 Proc: TProcess;
785752 CharBuffer: array [0..511] of char;
786753 ReadCount: integer;
787754 ExitCode: integer;
788755 SudoPassword: string;
789756 begin
790757 WriteLn('Please enter the sudo password:');
791758 Readln(SudoPassword);
792759 ExitCode := -1; //初始值为失败,观察后续是否生效
793760 Proc := TProcess.Create(nil); //新建进程
794761 try
795762 Proc.Options := [poUsePipes, poStderrToOutPut]; //用管道重定向程序的 stdin、stdout、stderr
796763 Proc.CommandLine := 'sudo -S ls /root'; //用 sudo 以 root 权限运行 ls /root
797764 // -S 使得 sudo 从 stdin 读取 root 账户的口令
798765 Proc.Execute; //启动进程。sudo 会要求输入口令
799766
800767 // 向 sudo 程序的 stdin 写入口令
801768 SudoPassword := SudoPassword + LineEnding;
802769 Proc.Input.Write(SudoPassword[1], Length(SudoPassword));
803770 SudoPassword := '%*'; //用短字符串填充,希望会稍稍搅乱一些内存数据,更安全的做法是用 PChars
804771 SudoPassword := ''; // 能让程序更难破解?!?
805772
806773 // 主循环从 sudo 的 stdout 和 stderr 读取输出数据
807774 while Proc.Running or (Proc.Output.NumBytesAvailable > 0) or
808775 (Proc.Stderr.NumBytesAvailable > 0) do
809776 begin
810777 // 读取 stdout 并写入当前 stdout
811778 while Proc.Output.NumBytesAvailable > 0 do
812779 begin
813780 ReadCount := Min(512, Proc.Output.NumBytesAvailable); //读入缓冲区,不超过最大值
814781 Proc.Output.Read(CharBuffer, ReadCount);
815782 Write(StdOut, Copy(CharBuffer, 0, ReadCount));
816783 end;
817784 // read stderr and write to our stderr
818785 while Proc.Stderr.NumBytesAvailable > 0 do
819786 begin
820787 ReadCount := Min(512, Proc.Stderr.NumBytesAvailable); //读入缓冲区,不超过最大值
821788 Proc.Stderr.Read(CharBuffer, ReadCount);
822789 Write(StdErr, Copy(CharBuffer, 0, ReadCount));
823790 end;
824791 end;
825792 ExitCode := Proc.ExitStatus;
826793 finally
827794 Proc.Free;
828795 Halt(ExitCode);
829796 end;
830797 end;
831798
832799 begin
833800 RunsLsRoot;
834801 end.
835802```
836803
837804其他想法:无疑最好是验证一下 sudo 是否真的提示输入口令。可将环境变量 SUDO_PROMPT 设为自定义值,并在读取 TProcess 的 stdout 时检查是否一致,这样即可避免不同语言环境下的提示符不同问题。设置环境变量会清除其默认值(从当前进程继承),因此用到的时候得去程序当前环境复制。
838805
839806### 在 Linux 中用 sudo 运行 fdisk
840807
841808以下例子展示了如何用 sudo 命令获取 root 权限,在 Linux 计算机上运行 fdisk。**注意:仅供演示,不适合大数据量输出。**
842809```
843810program getpartitioninfo;
844811 { {最初由 Lazarus 论坛 wjackon153 贡献,有问题、评论请联系他。
845812 由 BigChimp 将 Lazarus 代码修改为 FPC 程序, 便于理解和简化}
846813
847814 Uses
848815 Classes, SysUtils, FileUtil, Process;
849816
850817 var
851818 hprocess: TProcess;
852819 sPass: String;
853820 OutputLines: TStringList;
854821
855822 begin
856823 sPass := 'yoursudopasswordhere'; // 需要改为自己的 sudo 口令
857824 OutputLines:=TStringList.Create; // 最好用 try...finally 块
858825 // 确保 OutputLines 能被释放,hProcess 同理
859826
860827 // 以下代码将在后台打开 fdisk 并获取分区信息
861828 // 因为 fdisk 需要提权
862829 // 需要用 -S 选项将口令作为参数传给 sudo
863830 // 因此程序将进入等待状态,直至密码发送至 sudo 程序
864831 hProcess := TProcess.Create(nil);
865832 // 在 Linux/Unix/FreeBSD/macOS 中,需要指定可执行程序的完整路径
866833 hProcess.Executable := '/bin/sh';
867834 // 现在加入所有命令行参数
868835 hprocess.Parameters.Add('-c');
869836 // 现在将口令通过管道传给 sudo 命令,让它执行 fdisk -l
870837 hprocess.Parameters.add('echo ' + sPass + ' | sudo -S fdisk -l');
871838 // 同步方式执行(等待进程退出)并使用管道,以便由管道读取输出数据
872839 hProcess.Options := hProcess.Options + [poWaitOnExit, poUsePipes];
873840 // 执行进程
874841 hProcess.Execute;
875842
876843 // 现在 hProcess 应该将外部程序执行完毕(因为用了 poWaitOnExit)
877844 // 可以处理进程的输出数据了(stdout 和 strerr 等)
878845 OutputLines.Add('stdout:');
879846 OutputLines.LoadFromStream(hprocess.Output);
880847 OutputLines.Add('stderr:');
881848 OutputLines.LoadFromStream(hProcess.Stderr);
882849 // 在屏幕上显示输出数据
883850 writeln(OutputLines.Text);
884851
885852 // 释放资源,避免内存泄漏
886853 hProcess.Free;
887854 OutputLines.Free;
888855
889856 // 以下给出了一些示例,可传递非法字符,就像在终端中一样
890857 // 即使其他地方说是无法实现,我也保证以下方法有效 :)
891858
892859 //hprocess.Parameters.Add('ping -c 1 www.google.com');
893860 //hprocess.Parameters.Add('ifconfig wlan0 | grep ' + QuotedStr('inet addr:') + ' | cut -d: -f2');
894861
895862 // QuotedStr() 不是必需的,但可以让代码更清晰
896863 // 用双引号效果也一样
897864 //hprocess.Parameters.Add('glxinfo | grep direct');
898865
899866 // 以下方法还可以用于从软件包仓库中安装应用
900867
901868 //hprocess.Parameters.add('echo ' + sPass + ' | sudo -S apt-get install -y pkg-name');
902869
903870 end.
904871```
905872
906873### 包含空格的参数(替换 Shell 命令中的引号)
907874
908875Linux shell 可以接受引号包裹的参数,例如:
909876```
910877gdb --batch --eval-command="info symbol 0x0000DDDD" myprogram
911878```
912879
913880这里 GDB 可以接受 3 个参数(除第一个参数外),第一个参数是可执行文件的完整路径:
914881
915882 1. \--batch
916883 2. \--eval-command=info symbol 0x0000DDDD
917884 3. myprogram 的完整路径
918885
919886若要避免复杂的引号处理,最佳方案是在复杂场景下改用 TProcess.Parameters.Add,而不是直接给 CommandLine 赋值,例如::
920887```
921888AProcess.Executable:='/usr/bin/gdb';
922889 AProcess.Parameters.Add('--batch');
923890 AProcess.Parameters.Add('--eval-command=info symbol 0x0000DDDD'); // 注意这里没有用到引号
924891 AProcess.Parameters.Add('/home/me/myprogram');
925892```
926893
927894还请记得只传递完整路径。
928895
929896不过 TProcess.Commandline 确实支持对带有引号的参数进行一些简单的转义。用双引号包裹含有空格的 _整个_ 参数。比如:
930897```
931898AProcess.CommandLine := '/usr/bin/gdb --batch "--eval-command=info symbol 0x0000DDDD" /home/me/myprogram';
932899```
933900
934901.CommandLine 属性已[废弃](<http://bugs.freepascal.org/view.php?id=14446>),涉及更复杂的引号处理的缺陷报告,将不予接受或处理。
935902
936903## LCLIntf 替代方案
937904
938905有时不用显式调用外部程序,即可获取所需功能。不用既打开应用程序又把文档告诉它,只需让操作系统直接打开文档即可,操作系统会去选择与文档类型关联的默认应用。以下是一些示例。
939906
940907### 用默认应用程序打开文档
941908
942909某些时候需要用默认关联应用程序打开文档,而不是非要执行某个程序。不同的操作系统有不同的做法。Lazarus 提供了一个平台无关的过程 OpenDocument,可以完成这个操作。调用程序会继续运行,无需等待文档处理进程的关闭。
943910```
944911uses LCLIntf;
945912 ...
946913 OpenDocument('manual.pdf');
947914 ...
948915```
949916
950917 * [[opendocument|OpenDocument 参考手册]]
951918
952919### 用默认浏览器打开网页
953920
954921只需传入所需的 URL,在有些环境下好像可以省略前面的 <http://。此外,传入文件名可能效果等同于> OpenDocument()。
955922```
956923uses LCLIntf;
957924 ...
958925 OpenURL('www.lazarus.freepascal.org/');
959926```
960927
961928请参阅:
962929
963930 * [[OpenURL|OpenURL 参考手册]]
964931
965932或者可以以以下方式使用 **TProcess** :
966933```
967934uses Process;
968935
969936 procedure OpenWebPage(URL: string);
970937 // 看来传入的 URL 需用双引号包裹,比如 "www.lazarus.freepascal.org"
971938 var
972939 Browser, Params: string;
973940 begin
974941 FindDefaultBrowser(Browser, Params);
975942 with TProcess.Create(nil) do
976943 try
977944 Executable := Browser;
978945 Params:=Format(Params, [URL]);
979946 Params:=copy(Params,2,length(Params)-2); // 移除双引号,新版 TProcess.Parameters 会自动处理
980947 Parameters.Add(Params);
981948 Options := [poNoConsole];
982949 Execute;
983950 finally
984951 Free;
985952 end;
986953 end;
987954```
988955
989956## WinExec
990957
991958在陈旧的 Delphi 代码中,有时还会存在 Windows 3.x 时代的 CreateProcess。请用上述一种方案替换。CreateProcess 在 Windows 单元中依然存在,已标记为废弃状态。
992959
993960## 参阅
994961
995962 * [TProcess 文档](<http://lazarus-ccr.sourceforge.net/docs/fcl/process/tprocess.html>)
996963 * [[opendocument|OpenDocument]]
997- * [OpenUrl](</index.php?title=OpenUrl&action=edit&redlink=1> "OpenUrl \(page does not exist\)")
964+ * [[OpenUrl]]
998965 * [[TProcessUTF8]]
999966 * [[TXMLPropStorage]]
1000967 * [[Webbrowser|网页浏览器]]
1001968
1002969## 贡献者和更改
1003970
1004971 * 简体中文版本由 robsean 于 2019-03-07 创建。
1005972
1006973---
1007974*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