1 /** Wrappers around commands such `git`, `patch`, etc.
2  *
3  * Original version https://gist.github.com/PetarKirov/b4c8b64e7fc9bb7391901bcb541ddf3a
4  *
5  * See_Also: https://github.com/CyberShadow/ae/blob/master/sys/git.d
6  * See_Also: https://forum.dlang.org/post/ziqgqpkdjolplyfztulp@forum.dlang.org
7  *
8  * TODO: integrate @CyberShadow’s ae.sys.git at ae/sys/git.d
9  */
10 module nxt.cmd;
11 
12 import std.exception : enforce;
13 import std.format : format;
14 import std.file : exists, isDir, isFile;
15 import std.path : absolutePath, buildNormalizedPath, dirName, relativePath;
16 import std.stdio : write, writeln, writefln, File, stdin, stdout, stderr;
17 import std.algorithm.mutation : move;
18 import nxt.logging : LogLevel, defaultLogLevel, trace, info, warning, error;
19 
20 @safe:
21 
22 ExitStatus exitMessage(ExitStatus code) => typeof(return)(code);
23 
24 /** Process exit status (code).
25  *
26  * See: https://en.wikipedia.org/wiki/Exit_status
27  */
28 struct ExitStatus
29 {
30 	int value;
31 	alias value this;
32 }
33 
34 /** Process spawn state.
35 	Action accessing predeclared file system resources.
36  */
37 struct Spawn
38 {
39 	import std.process : Pid;
40 
41 	this(Pid pid, File input_, File out_, File err_, in LogLevel logLevel = defaultLogLevel)
42 		in(input_.isOpen)
43 		in(out_.isOpen)
44 		in(err_.isOpen) {
45 		this.pid = pid;
46 		this.input = input_;
47 		this.output = out_;
48 		this.error = err_;
49 		this.logLevel = logLevel;
50 	}
51 
52 	this(this) @disable;		// avoid copying `File`s for now
53 
54 	auto ref setLogLevel(in LogLevel logLevel) scope pure nothrow @nogc {
55 		this.logLevel = logLevel;
56 		return this;
57 	}
58 
59 	ExitStatus wait() {
60 		if (logLevel <= LogLevel.trace) .trace("Waiting");
61 
62 		import std.process : wait;
63 		auto result = typeof(return)(pid.wait());
64 
65 		static void echo(ref File src, ref File dst, in string name)
66 			in(src.isOpen)
67 			in(dst.isOpen) {
68 			if (src.eof)
69 				return; // skip if empty
70 			writeln("  - ", name, ":");
71 			src.flush();
72 			import std.algorithm.mutation: copy;
73 			writeln("src:",src, "dst:",dst, "stdout:",stdout, "stderr:",stderr);
74 			() @trusted { src.byLine().copy(dst.lockingBinaryWriter); } (); /+ TODO: writeIndented +/
75 		}
76 
77 		if (result == 0) {
78 			if (logLevel <= LogLevel.trace)
79 				.trace("Process exited successfully with exitStatus:", result);
80 		} else {
81 			if (logLevel <= LogLevel.error)
82 				.error("Process exited unsuccessfully with exitStatus:", result);
83 		}
84 
85 		import std.stdio : stdout, stderr;
86 		if (result != 0 && logLevel <= LogLevel.info) {
87 			() @trusted { if (output != stdout) echo(output, stdout, "OUT"); } ();
88 		}
89 		if (result != 0 && logLevel <= LogLevel.warning) {
90 			() @trusted { if (error != stderr) echo(error, stderr, "ERR"); } ();
91 		}
92 
93 		return result;
94 	}
95 
96 package:
97 	Pid pid;
98 	File input;
99 	File output;
100 	File error;
101 	LogLevel logLevel;
102 }
103 
104 Spawn spawn(scope const(char[])[] args,
105 			in LogLevel logLevel = defaultLogLevel,
106 			File input_ = stdin,
107 			File out_ = stdout,
108 			File err_ = stderr) {
109 	import std.process : spawnProcess;
110 	if (logLevel <= LogLevel.trace) .trace("Spawning ", args);
111 	auto pid = spawnProcess(args, input_, out_, err_);
112 	return typeof(return)(pid, input_, out_, err_, logLevel);
113 }
114 
115 /++ Variant of `std.stdio.write` that flushes `stdout` afterwards.
116  +/
117 void writeFlushed(Args...)(scope Args args) {
118 	import std.stdio : stdout;
119 	() @trusted { write(args, " ... "); stdout.flush(); } ();
120 }
121 
122 /++ Variant of `std.stderr.write` that flushes `stderr` afterwards.
123  +/
124 void ewriteFlushed(Args...)(scope Args args) {
125 	import std.stdio : stderr;
126 	() @trusted { stderr.write(args, " ... "); stderr.flush(); } ();
127 }