1 /** Module that defines the main `Process` struct and associated components.
2   **/
3 module theprocess.process;
4 
5 private import std.format;
6 private import std.process;
7 private import std.file;
8 private import std.stdio;
9 private import std.exception;
10 private import std.string: join;
11 private import std.typecons;
12 private import std.format: format;
13 
14 version(Posix) {
15     private import core.sys.posix.unistd;
16 }
17 
18 private import thepath;
19 
20 private import theprocess.utils;
21 private import theprocess.exception: ProcessException;
22 
23 
24 /** Process result, produced by 'execute' method of Process.
25   **/
26 @safe immutable struct ProcessResult {
27     /// The program that was invoked to obtain this result.
28     private string _program;
29 
30     /// The arguments passed to the program to obtain this result.
31     private string[] _args;
32 
33     /// exit code of the process
34     int status;
35 
36     /// text output of the process
37     string output;
38 
39     // Do not allow to create records without params
40     @disable this();
41 
42     private pure this(
43             in string program,
44             immutable string[] args,
45             in int status,
46             in string output) nothrow {
47         this._program = program.idup;
48         this._args = args;
49         this.status = status;
50         this.output = output.idup;
51     }
52 
53     /** Check if status is Ok.
54       *
55       * Params:
56       *     expected = expected exit code. Default: 0
57       *
58       * Returns:
59       *     True it exit status is equal to expected result, otherwise False.
60       **/
61     bool isOk(in int expected=0) const { return this.status == expected; }
62 
63     /** Check if status is not Ok.
64       *
65       * Params:
66       *     expected = expected successfule exit code. Default: 0
67       *
68       * Returns:
69       *     True it exit status is NOT equal to expected result, otherwise False.
70       **/
71     bool isNotOk(in int expected=0) const { return !isOk(expected); }
72 
73     /** Ensure that program exited with expected exit code.
74       *
75       * Params:
76       *     msg = message to throw in exception in case of check failure
77       *     add_output = if set to True, then output of command will be attached
78       *         to message on failure.
79       *     expected = expected exit-code, if differ, then
80       *         exception will be thrown.
81       **/
82     auto ref ensureStatus(E : Throwable = ProcessException)(
83             in string msg, in bool add_output, in int expected=0) const {
84         enforce!E(
85             isOk(expected),
86             !add_output ? msg : msg ~ "\nOutput: " ~ output);
87         return this;
88     }
89 
90     /// ditto
91     auto ref ensureStatus(E : Throwable = ProcessException)(
92             in string msg, in int expected=0) const {
93         return ensureStatus!E(
94             msg,
95             false,
96             expected);
97     }
98 
99     /// ditto
100     auto ref ensureStatus(E : Throwable = ProcessException)(in bool add_output, in int expected=0) const {
101         return ensureStatus!E(
102             "Program %s with args %s failed! Expected exit-code %s, got %s.".format(
103                 _program, _args, expected, status),
104             add_output,
105             expected);
106     }
107 
108     /// ditto
109     auto ref ensureStatus(E : Throwable = ProcessException)(in int expected=0) const {
110         return ensureStatus!E(false, expected);
111     }
112 
113     /// ditto
114     alias ensureOk = ensureStatus;
115 
116 }
117 
118 
119 /** This struct is used to prepare configuration for process and run it.
120   *
121   * The following methods of running a process are supported:
122   *
123   * - execute: run process and catch its output and exit code.
124   * - spawn: spawn the process in background, and optionally pipe its output.
125   * - pipe: spawn the process and attach configurable pipes to catch output.
126   *
127   * The configuration of a process can be done like so:
128   *
129   * 1. Create the `Process` instance specifying the program to run.
130   * 2. Apply your desired configuration (args, env, workDir)
131   *    via calls to one of the corresponding methods.
132   * 3. Run one of `execute`, `spawn` or `pipe` methods, that will actually
133   *    start the process.
134   *
135   * Configuration methods come in two families with distinct semantics:
136   *
137   * - `set*` / `add*` methods mutate the current instance in place and return
138   *   `void`, making them suitable for conditional modification of an
139   *   already-stored `Process` variable.
140   * - `with*` / `in*` methods return a new `Process` by value, leaving the
141   *   original unchanged, making them safe to use in chained expressions.
142   *   Most `set*` / `add*` method has a `with*` counterpart:
143   *   `addArgs` ↔ `withArgs`,
144   *   `setWorkDir` ↔ `inWorkDir`, `setEnv` ↔ `withEnv`, etc.
145   *
146   * Examples:
147   * ---
148   * // It is possible to run process in following way:
149   * auto result = Process("my-program")
150   *         .withArgs("--verbose", "--help")
151   *         .withEnv("MY_ENV_VAR", "MY_VALUE")
152   *         .inWorkDir("my/working/directory")
153   *         .execute()
154   *         .ensureStatus!MyException("My error message on failure");
155   * writeln(result.output);
156   * ---
157   * ---
158   * // Also, in Posix system it is possible to run command as different user:
159   * auto result = Process("my-program")
160   *         .withUser("bob")
161   *         .execute()
162   *         .ensureStatus!MyException("My error message on failure");
163   * writeln(result.output);
164   * ---
165   **/
166 @safe struct Process {
167     private string _program;
168     private string[] _args;
169     private string[string] _env=null;
170     private string _workdir=null;
171     private std.process.Config _config=std.process.Config.none;
172 
173     /* TODO: May be it have sense to add somekind of lock
174      *       to wrap execution of process in multithreaded mode.
175      *       It seems that this is needed on Posix systems,
176      *       especially in case when running process with different
177      *       uid/git, that requires temporary change of uid/gid of current
178      *       process, but uid and gid are attributes of process, not thread.
179      */
180 
181     version(Posix) {
182         /* On posix we have ability to run process with different user,
183          * thus we have to keep desired uid/gid to run process with and
184          * original uid/git to revert uid/gid change after process completed.
185          */
186         private Nullable!uid_t _uid;
187         private Nullable!gid_t _gid;
188         private Nullable!uid_t _original_uid;
189         private Nullable!gid_t _original_gid;
190     }
191 
192     /** Create new Process instance to run specified program.
193       *
194       * Params:
195       *     program = name of program to run or path of program to run
196       **/
197     this(in string program) {
198         _program = program.idup;
199     }
200 
201     /// ditto
202     this(in Path program) {
203         _program = program.toAbsolute.toString;
204     }
205 
206     /** Copy the process configuration. Could be useful when needed to run
207       * command multipe times with slightly different configuration.
208       * Returns new instance of process.
209       **/
210     Process copy() const {
211         Process res = Process(this._program);
212 
213         res._config = this._config;
214 
215         if (this._args)
216             res.setArgs(this._args);
217         if (this._env)
218             res.setEnv(this._env);
219         if (this._workdir)
220             res.setWorkDir(this._workdir);
221 
222         version(Posix) {
223             res._uid = this._uid;
224             res._gid = this._gid;
225             res._original_uid = this._original_uid;
226             res._original_gid = this._original_gid;
227         }
228         return res;
229     }
230 
231     /// Ensure that copy, setArgs, addArgs, and withArgs work correctly
232     unittest {
233         import unit_threaded.assertions;
234 
235         auto p = Process("some-test-program").withArgs("arg1", "arg2");
236         p._args.should == ["arg1", "arg2"];
237 
238         // setArgs mutates in place (void return)
239         p.setArgs("arg3", "arg4");
240         p._args.should == ["arg3", "arg4"];
241 
242         // addArgs mutates in place (void return)
243         p.addArgs("arg4b");
244         p._args.should == ["arg3", "arg4", "arg4b"];
245 
246         // withArgs returns a new Process with args appended, without modifying the original
247         auto p2 = p.withArgs("arg5", "arg6");
248         p2._args.should == ["arg3", "arg4", "arg4b", "arg5", "arg6"];
249         p._args.should == ["arg3", "arg4", "arg4b"];
250 
251         // withArgs on a fresh process works as expected (append to empty = set)
252         auto p3 = Process("other-program").withArgs("arg7");
253         p3._args.should == ["arg7"];
254     }
255 
256     /** Return string representation of process to be started
257       **/
258     string toString() const {
259         return "Program: %s, args: %s, env: %s, workdir: %s".format(
260             _program, _args.join(" "), _env, _workdir);
261     }
262 
263     /** Set arguments for the process
264       *
265       * Note, replaces currently configured args for the process with provided args
266       *
267       * Params:
268       *     args = array of arguments to run program with
269       **/
270     void setArgs(in string[] args...) {
271         _args = args.dup;
272     }
273 
274     /** Return a new Process with the provided arguments appended,
275       * leaving the original unchanged.
276       *
277       * This is the non-mutating counterpart of addArgs.
278       * Can be called multiple times to progressively build up arguments.
279       **/
280     Process withArgs(in string[] args...) const {
281         auto result = this.copy();
282         result.addArgs(args);
283         return result;
284     }
285 
286     /** Add arguments to the process.
287       *
288       * This could be used if you do not know all the arguments for program
289       * to run at single point, and you need to add it conditionally.
290       *
291       * Params:
292       *     args = array of arguments to add
293       *
294       * Examples:
295       * ---
296       * auto program = Process("my-program")
297       *     .withArgs("--some-option");
298       *
299       * if (some condition)
300       *     program.addArgs("--some-other-opt", "--verbose");
301       *
302       * auto result = program
303       *     .execute()
304       *     .ensureStatus!MyException("My error message on failure");
305       * writeln(result.output);
306       * ---
307       **/
308     void addArgs(in string[] args...) {
309         _args ~= args;
310     }
311 
312     /** Append a single argument, returning a new Process (non-mutating).
313       * Equivalent to withArgs.
314       *
315       * Examples:
316       * ---
317       * auto git = Process("git").withArgs("--git-dir", myPath);
318       * (git ~ "clone" ~ url).execute.ensureOk;
319       * ---
320       **/
321     Process opBinary(string op)(in string arg) const if (op == "~") {
322         return this.withArgs(arg);
323     }
324 
325     /** Append multiple arguments, returning a new Process (non-mutating).
326       * Equivalent to withArgs.
327       *
328       * Examples:
329       * ---
330       * auto git = Process("git").withArgs("--git-dir", myPath);
331       * (git ~ ["clone", url]).execute.ensureOk;
332       * ---
333       **/
334     Process opBinary(string op)(in string[] args) const if (op == "~") {
335         return this.withArgs(args);
336     }
337 
338     /** Append a single argument in place (mutating).
339       * Equivalent to addArgs.
340       **/
341     void opOpAssign(string op)(in string arg) if (op == "~") {
342         this.addArgs(arg);
343     }
344 
345     /** Append multiple arguments in place (mutating).
346       * Equivalent to addArgs.
347       **/
348     void opOpAssign(string op)(in string[] args) if (op == "~") {
349         this.addArgs(args);
350     }
351 
352     /// Ensure that ~ and ~= work correctly
353     unittest {
354         import unit_threaded.assertions;
355 
356         auto p = Process("git").withArgs("--git-dir", "/my/path");
357 
358         // ~ with single string returns new Process, original unchanged
359         auto p2 = p ~ "clone";
360         p2._args.should == ["--git-dir", "/my/path", "clone"];
361         p._args.should == ["--git-dir", "/my/path"];
362 
363         // ~ with string[] returns new Process, original unchanged
364         auto p3 = p ~ ["clone", "https://example.com"];
365         p3._args.should == ["--git-dir", "/my/path", "clone", "https://example.com"];
366         p._args.should == ["--git-dir", "/my/path"];
367 
368         // ~ chains correctly
369         auto p4 = p ~ "clone" ~ "https://example.com";
370         p4._args.should == ["--git-dir", "/my/path", "clone", "https://example.com"];
371         p._args.should == ["--git-dir", "/my/path"];
372 
373         // ~= with single string mutates in place
374         auto p5 = p.copy();
375         p5 ~= "status";
376         p5._args.should == ["--git-dir", "/my/path", "status"];
377 
378         // ~= with string[] mutates in place
379         auto p6 = p.copy();
380         p6 ~= ["log", "--oneline"];
381         p6._args.should == ["--git-dir", "/my/path", "log", "--oneline"];
382     }
383 
384     /** Set work directory for the process to be started
385       *
386       * Params:
387       *     workdir = working directory path to run process in
388       **/
389     void setWorkDir(in string workdir) {
390         _workdir = workdir.idup;
391     }
392 
393     /// ditto
394     void setWorkDir(in Path workdir) {
395         _workdir = workdir.toString.idup;
396     }
397 
398     /** Return a new Process with the working directory set to the provided
399       * path, leaving the original unchanged.
400       **/
401     Process inWorkDir(in string workdir) const {
402         auto result = this.copy();
403         result.setWorkDir(workdir);
404         return result;
405     }
406 
407     /// ditto
408     Process inWorkDir(in Path workdir) const {
409         auto result = this.copy();
410         result.setWorkDir(workdir);
411         return result;
412     }
413 
414     /** Set environemnt for the process to be started.
415       * Could be called multiple times to update environment.
416       *
417       * Params:
418       *     env = associative array to update environment to run process with.
419       **/
420     void setEnv(in string[string] env) {
421         foreach(i; env.byKeyValue)
422             _env[i.key] = i.value;
423     }
424 
425     /** Set environment variable (specified by key) to provided value
426       *
427       * Params:
428       *     key = environment variable name
429       *     value = environment variable value
430       **/
431     void setEnv(in string key, in string value) {
432         _env[key.idup] = value.idup;
433     }
434 
435     /** Return a new Process with the environment updated with the provided
436       * key-value pairs, leaving the original unchanged.
437       **/
438     Process withEnv(in string[string] env) const {
439         auto result = this.copy();
440         result.setEnv(env);
441         return result;
442     }
443 
444     /// ditto
445     Process withEnv(in string key, in string value) const {
446         auto result = this.copy();
447         result.setEnv(key, value);
448         return result;
449     }
450 
451     /** Run process with new environment
452       * (do not inherit environment variables from parent process)
453       **/
454     void setNewEnv() {
455         _config.flags |= std.process.Config.Flags.newEnv;
456     }
457 
458     /** Return a new Process configured to start with a fresh environment
459       * (not inheriting parent environment variables), leaving the original
460       * unchanged.
461       **/
462     Process withNewEnv() const {
463         auto result = this.copy();
464         result.setNewEnv();
465         return result;
466     }
467 
468     /** Set process configuration
469       **/
470     void setConfig(in std.process.Config config) {
471         _config.flags = config.flags;
472     }
473 
474     /** Return a new Process with the process configuration set to the
475       * provided value, leaving the original unchanged.
476       **/
477     Process withConfig(in std.process.Config config) const {
478         auto result = this.copy();
479         result.setConfig(config);
480         return result;
481     }
482 
483     /** Set configuration flag for process to be started
484       **/
485     void setFlag(in std.process.Config.Flags flag) {
486         _config.flags |= flag;
487     }
488 
489     /// ditto
490     void setFlag(in std.process.Config flags) {
491         _config |= flags;
492     }
493 
494     /** Return a new Process with the given configuration flag set,
495       * leaving the original unchanged.
496       **/
497     Process withFlag(in std.process.Config.Flags flag) const {
498         auto result = this.copy();
499         result.setFlag(flag);
500         return result;
501     }
502 
503     /// ditto
504     Process withFlag(in std.process.Config flags) const {
505         auto result = this.copy();
506         result.setFlag(flags);
507         return result;
508     }
509 
510     /** Apply Config.stderrPassThrough flag.
511       * With this flag, stderr will not be captured,
512       * but instead directly passed to console or terminal.
513       **/
514     void setStderrPassThrough() {
515         setFlag(std.process.Config.stderrPassThrough);
516     }
517 
518     /** Return a new Process with Config.stderrPassThrough set,
519       * leaving the original unchanged.
520       **/
521     Process withStderrPassThrough() const {
522         return withFlag(std.process.Config.stderrPassThrough);
523     }
524 
525     /** Set UID to run process with
526       *
527       * Params:
528       *     uid = UID (id of system user) to run process with
529       *
530       * Returns:
531       *     reference to this (process instance)
532       *
533       **/
534     version(Posix) void setUID(in uid_t uid) {
535         _uid = uid;
536     }
537 
538     /** Return a new Process configured to run with the given UID,
539       * leaving the original unchanged.
540       **/
541     version(Posix) Process withUID(in uid_t uid) const {
542         auto result = this.copy();
543         result.setUID(uid);
544         return result;
545     }
546 
547     /** Set GID to run process with
548       *
549       * Params:
550       *     gid = GID (id of system group) to run process with
551       *
552       * Returns:
553       *     reference to this (process instance)
554       *
555       **/
556     version(Posix) void setGID(in gid_t gid) {
557         _gid = gid;
558     }
559 
560     /** Return a new Process configured to run with the given GID,
561       * leaving the original unchanged.
562       **/
563     version(Posix) Process withGID(in gid_t gid) const {
564         auto result = this.copy();
565         result.setGID(gid);
566         return result;
567     }
568 
569     /** Run process as specified user
570       *
571       * If this method applied, then the UID and GID to run process with
572       * will be taked from record in passwd database
573       *
574       * Params:
575       *     username = login of user to run process as
576       **/
577     version(Posix) void setUser(in string username, in bool userWorkDir=false) @trusted {
578         auto user = getSystemUser(username);
579         if (user.isNull)
580             throw new ProcessException("User %s does not exist".format(username));
581 
582         _uid = user.get.uid;
583         _gid = user.get.gid;
584 
585         if (userWorkDir)
586             _workdir = user.get.homeDir;
587     }
588 
589     /** Return a new Process configured to run as the given user,
590       * leaving the original unchanged.
591       **/
592     version(Posix) Process withUser(
593             in string username, in bool userWorkDir=false) @trusted const {
594         auto result = this.copy();
595         result.setUser(username, userWorkDir);
596         return result;
597     }
598 
599     /// Called before running process to run pre-exec hooks;
600     private void setUpProcess() {
601         version(Posix) {
602             /* We set real user and real group here,
603              * keeping original effective user and effective group
604              * (usually original user/group is root, when such logic used)
605              * Later in preExecFunction, we can update effective user
606              * for child process to be same as real user.
607              * This is needed, because bash, changes effective user to real
608              * user when effective user is different from real.
609              * Thus, we have to set both real user and effective user
610              * for child process.
611              *
612              * We can accomplish this in two steps:
613              *     - Change real uid/gid here for current process
614              *     - Change effective uid/gid to match real uid/gid
615              *       in preexec fuction.
616              * Because preexec function is executed in child process,
617              * that will be replaced by specified command proces, it works.
618              *
619              * Also, note, that first we have to change group ID, because
620              * when we change user id first, it may not be possible to change
621              * group.
622              */
623 
624              /*
625               * TODO: May be it have sense to change effective user/group
626               *       instead of real user, and update real user in
627               *       child process.
628               */
629 
630             // TODO: It seems that in latest releases better preexec function was implemented
631             //       Check it, may be it have sense to use it.
632             if (!_gid.isNull && _gid.get != getgid) {
633                 _original_gid = getgid().nullable;
634                 errnoEnforce(
635                     setregid(_gid.get, -1) == 0,
636                     "Cannot set real GID to %s before starting process: %s".format(
637                         _gid, this.toString));
638             }
639             if (!_uid.isNull && _uid.get != getuid) {
640                 _original_uid = getuid().nullable;
641                 errnoEnforce(
642                     setreuid(_uid.get, -1) == 0,
643                     "Cannot set real UID to %s before starting process: %s".format(
644                         _uid, this.toString));
645             }
646 
647             if (!_original_uid.isNull || !_original_gid.isNull)
648                 _config.preExecFunction = () @trusted nothrow @nogc {
649                     /* Because we cannot pass any parameters here,
650                      * we just need to make real user/group equal to
651                      * effective user/group for child proces.
652                      * This is needed, because bash could change effective user
653                      * when it is different from real user.
654                      *
655                      * We change here effective user/group equal
656                      * to real user/group because we have changed
657                      * real user/group in parent process
658                      * before running this function.
659                      *
660                      * Also, note, that this function will be executed
661                      * in child process, just before calling execve.
662                      */
663                     if (setegid(getgid) != 0)
664                         return false;
665                     if (seteuid(getuid) != 0)
666                         return false;
667                     return true;
668                 };
669 
670         }
671     }
672 
673     /// Called after process started to run post-exec hooks;
674     private void tearDownProcess() {
675         version(Posix) {
676             // Restore original uid/gid after process started, then clear
677             // the saved values so re-running this Process is safe.
678             if (!_original_gid.isNull) {
679                 errnoEnforce(
680                     setregid(_original_gid.get, -1) == 0,
681                     "Cannot restore real GID to %s after process started: %s".format(
682                         _original_gid, this.toString));
683                 _original_gid.nullify();
684             }
685             if (!_original_uid.isNull) {
686                 errnoEnforce(
687                     setreuid(_original_uid.get, -1) == 0,
688                     "Cannot restore real UID to %s after process started: %s".format(
689                         _original_uid, this.toString));
690                 _original_uid.nullify();
691             }
692             _config.preExecFunction = null;
693         }
694     }
695 
696     /** Execute the configured process and capture output.
697       *
698       * Params:
699       *     max_output = max size of output to capture.
700       *
701       * Returns:
702       *     ProcessResult instance that contains output and exit-code
703       *     of program
704       *
705       **/
706     auto execute(in size_t max_output=size_t.max) {
707         setUpProcess();
708         scope(exit) tearDownProcess();
709         auto res = std.process.execute(
710             [_program] ~ _args,
711             _env,
712             _config,
713             max_output,
714             _workdir);
715         return ProcessResult(_program, _args.idup, res.status, res.output);
716     }
717 
718     /// Spawn process
719     auto spawn(File stdin=std.stdio.stdin,
720                File stdout=std.stdio.stdout,
721                File stderr=std.stdio.stderr) {
722         setUpProcess();
723         scope(exit) tearDownProcess();
724         auto res = std.process.spawnProcess(
725             [_program] ~ _args,
726             stdin,
727             stdout,
728             stderr,
729             _env,
730             _config,
731             _workdir);
732         return res;
733     }
734 
735     /// Pipe process
736     auto pipe(in Redirect redirect=Redirect.all) {
737         setUpProcess();
738         scope(exit) tearDownProcess();
739         auto res = std.process.pipeProcess(
740             [_program] ~ _args,
741             redirect,
742             _env,
743             _config,
744             _workdir);
745         return res;
746     }
747 
748     /** Replace current process by executing program as configured by
749       * Process instance.
750       *
751       * Under the hood, this method will call $(REF execvpe, std, process) or
752       * $(REF execvp, std, process).
753       **/
754     version(Posix) void execv() @system {
755         import std.algorithm;
756         import std.array;
757 
758         if (!_gid.isNull && _gid.get != getgid) {
759             // Change rgid and egid if needed
760             errnoEnforce(
761                 setregid(_gid.get, _gid.get) == 0,
762                 "Cannot set real GID to %s before starting process: %s".format(
763                     _gid, this.toString));
764         }
765         if (!_uid.isNull && _uid.get != getuid) {
766             // Change ruid and euid if needed
767             errnoEnforce(
768                 setreuid(_uid.get, _uid.get) == 0,
769                 "Cannot set real UID to %s before starting process: %s".format(
770                     _uid, this.toString));
771         }
772 
773         // Change working directory, when needed before executing the program
774         if (_workdir)
775             std.file.chdir(_workdir);
776 
777         // Prepare environment variable for process
778         string[string] env;
779         if (_config.flags & std.process.Config.Flags.newEnv)
780             env = _env;
781         else {
782             // If we do not need new environment, then merge parent process
783             // environment with environment configured for process execution.
784             env = std.process.environment.toAA;
785             foreach(i; _env.byKeyValue)
786                 env[i.key] = i.value;
787         }
788 
789         /* We call `execvpe` function, thus we have to provide environment
790          * variables in format suitable for this function
791          * (array of strings in format `key=value`).
792          * If there is no environment required, then we just need to provide
793          * empty string.
794          **/
795         string[] env_arr = env.byKeyValue.map!(
796             (i) => "%s=%s".format(i.key, i.value)
797         ).array;
798         enforce!ProcessException(
799             std.process.execvpe(_program, [_program] ~ _args, env_arr) != -1,
800             "Cannot exec program %s".format(this.toString));
801     }
802 }
803 
804 
805 // Test simple api
806 @safe unittest {
807     import unit_threaded.assertions;
808 
809     auto process = Process("my-program")
810         .withArgs("--verbose", "--help")
811         .withEnv("MY_VAR", "42")
812         .inWorkDir("/my/path");
813     process._program.should == "my-program";
814     process._args.should == ["--verbose", "--help"];
815     process._env.should == ["MY_VAR": "42"];
816     process._workdir.should == "/my/path";
817     process.toString.should ==
818         "Program: %s, args: %s, env: %s, workdir: %s".format(
819             process._program, process._args.join(" "),
820             process._env, process._workdir);
821 
822     // Change some params of the process
823     process.setWorkDir(Path("/some/other/path"));
824     process.setEnv([
825         "MY_VAR_2": "72",
826     ]);
827     process.addArgs("arg2", "arg3");
828 
829     // Check that changes took effect
830     process._program.should == "my-program";
831     process._args.should == ["--verbose", "--help", "arg2", "arg3"];
832     process._env.should == ["MY_VAR": "42", "MY_VAR_2": "72"];
833     process._workdir.should == "/some/other/path";
834     process.toString.should ==
835         "Program: %s, args: %s, env: %s, workdir: %s".format(
836             process._program, process._args.join(" "),
837             process._env, process._workdir);
838 }
839 
840 /// Test simple execution of the script
841 @safe unittest {
842     import std.string;
843     import std.ascii : newline;
844 
845     import unit_threaded.assertions;
846 
847     auto temp_root = createTempPath();
848     scope(exit) temp_root.remove();
849 
850     version(Posix) {
851         import std.conv: octal;
852         auto script_path = temp_root.join("test-script.sh");
853         script_path.writeFile(
854             "#!" ~ nativeShell ~ newline ~
855             `echo "Test out: $1 $2"` ~ newline);
856         // Add permission to run this script
857         script_path.setAttributes(octal!755);
858     } else version(Windows) {
859         auto script_path = temp_root.join("test-script.cmd");
860         script_path.writeFile(
861             "@echo off" ~ newline ~
862             "echo Test out: %1 %2" ~ newline);
863     }
864 
865     // Test the case when process executes fine
866     auto result = Process(script_path)
867         .withArgs("Hello", "World", "test")
868         .execute
869         .ensureOk;
870     result.status.should == 0;
871     result.output.chomp.should == "Test out: Hello World";
872     result.isOk.shouldBeTrue;
873     result.isNotOk.shouldBeFalse;
874     // When we expect different successful exit-code
875     result.isOk(42).shouldBeFalse;
876     result.isNotOk(42).shouldBeTrue;
877     result.ensureOk(42).shouldThrow!ProcessException;
878 }
879 
880 /// Test simple execution of the script that handles environment variables
881 @safe unittest {
882     import std.string;
883     import std.ascii : newline;
884 
885     import unit_threaded.assertions;
886 
887     auto temp_root = createTempPath();
888     scope(exit) temp_root.remove();
889 
890     /* Do similar trick as in Phobos for portable newline output
891      *
892      * To avoid printing the newline characters, we use the echo|set trick on
893      * Windows, and printf on POSIX (neither echo -n nor echo \c are portable).
894      */
895     version(Posix) {
896         import std.conv: octal;
897         auto script_path = temp_root.join("test-script.sh");
898         script_path.writeFile(
899             "#!" ~ nativeShell ~ newline ~
900             `printf "Test out: $1 $2, $MY_PARAM_1 $MY_PARAM_2"` ~ newline);
901         // Add permission to run this script
902         script_path.setAttributes(octal!755);
903     } else version(Windows) {
904         auto script_path = temp_root.join("test-script.cmd");
905         script_path.writeFile(
906             `@echo off` ~ newline ~
907             `echo|set /p DUMMY="Test out: %1 %2, %MY_PARAM_1% %MY_PARAM_2%"` ~ newline);
908     }
909 
910     // Test the case when process executes fine
911     auto result = Process(script_path)
912         .withArgs("Hello")
913         .withArgs("World")
914         .withEnv("MY_PARAM_1", "the")
915         .withEnv("MY_PARAM_2", "Void")
916         .execute
917         .ensureOk;
918     result.status.should == 0;
919     result.output.chomp.should == "Test out: Hello World, the Void";
920     result.isOk.shouldBeTrue;
921     result.isNotOk.shouldBeFalse;
922     // When we expect different successful exit-code
923     result.isOk(42).shouldBeFalse;
924     result.isNotOk(42).shouldBeTrue;
925 
926     // Ensure that status is ok, if not ok, then raise error
927     result.ensureOk(42).shouldThrow!ProcessException;
928 
929     // Optionally allow to print command output on failure with custom error message or with standard one.
930     result.ensureOk("Custom error message", 42).shouldThrowWithMessage!ProcessException(
931         "Custom error message");
932     result.ensureOk("Error message", true, 42).shouldThrowWithMessage!ProcessException(
933         "Error message\nOutput: %s".format("Test out: Hello World, the Void"));
934     result.ensureOk(true, 42).shouldThrowWithMessage!ProcessException(
935         "Program %s with args %s failed! Expected exit-code %s, got %s.\nOutput: %s".format(
936             result._program, result._args, 42, 0, "Test out: Hello World, the Void"));
937 }
938 
939 /// Test simple execution of the script with user (use current user)
940 version(Posix) @safe unittest {
941     import std.string;
942     import std.ascii : newline;
943 
944     import unit_threaded.assertions;
945 
946     auto temp_root = createTempPath();
947     scope(exit) temp_root.remove();
948 
949     import std.conv: octal;
950     auto script_path = temp_root.join("test-script.sh");
951     script_path.writeFile(
952         "#!" ~ nativeShell ~ newline ~
953         `echo "Test out: $1 $2"` ~ newline);
954     // Add permission to run this script
955     script_path.setAttributes(octal!755);
956 
957     auto username = Process("whoami").execute.ensureOk(true).output.strip;
958 
959     // Test the case when process executes fine
960     auto result = Process(script_path)
961         .withArgs("Hello", "World", "test")
962         .withUser(username)
963         .execute
964         .ensureOk;
965     result.status.should == 0;
966     result.output.chomp.should == "Test out: Hello World";
967     result.isOk.shouldBeTrue;
968     result.isNotOk.shouldBeFalse;
969     // When we expect different successful exit-code
970     result.isOk(42).shouldBeFalse;
971     result.isNotOk(42).shouldBeTrue;
972     result.ensureOk(42).shouldThrow!ProcessException;
973 }
974 
975 
976 /// Test simple execution of the script within user's home directory
977 version(Posix) @safe unittest {
978     import std.string;
979     import std.ascii : newline;
980 
981     import unit_threaded.assertions;
982 
983     // Change current working dir to /tmp
984     Path.tempDir.chdir;
985 
986     auto current_user = Process("whoami").execute.ensureOk(true).output.strip;
987     auto workdir = Process("pwd")
988         .withUser(current_user)
989         .execute
990         .ensureOk(true)
991         .output.strip;
992 
993     Path(workdir).realPath.should == Path.tempDir.realPath;
994 
995     workdir = Process("pwd")
996         .withUser(current_user, true)
997         .execute
998         .ensureOk(true)
999         .output.strip;
1000 
1001     Path(workdir).realPath.should == Path("~").realPath;
1002 }