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.process : pipeProcess;
17 import std.stdio : write, writeln, writefln, File, stdin, stdout, stderr;
18 import std.algorithm.mutation : move;
19 import nxt.logging : LogLevel, defaultLogLevel, trace, info, warning, error;
20 
21 @safe:
22 
23 ExitStatus exitMessage(ExitStatus code) => typeof(return)(code);
24 
25 /** Process exit status (code).
26  *
27  * See: https://en.wikipedia.org/wiki/Exit_status
28  */
29 struct ExitStatus
30 {
31 	int value;
32 	alias value this;
33 }
34 
35 /** Process spawn state.
36 	Action accessing predeclared file system resources.
37  */
38 struct Spawn
39 {
40 	import std.process : Pid, ProcessPipes;
41 
42 	this(ProcessPipes processPipes, in LogLevel logLevel = defaultLogLevel) {
43 		this.processPipes = processPipes;
44 		this.logLevel = logLevel;
45 	}
46 
47 	this(this) @disable;		// avoid copying `File`s for now
48 
49 	auto ref setLogLevel(in LogLevel logLevel) scope pure nothrow @nogc {
50 		this.logLevel = logLevel;
51 		return this;
52 	}
53 
54 	ExitStatus wait() {
55 		if (logLevel <= LogLevel.trace) .trace("Waiting");
56 
57 		import std.process : wait;
58 		auto result = typeof(return)(processPipes.pid.wait());
59 
60 		static void echo(File src, ref File dst, in string name)
61 			in(src.isOpen)
62 			in(dst.isOpen) {
63 			if (src.eof)
64 				return; // skip if empty
65 			writeln("  - ", name, ":");
66 			src.flush();
67 			import std.algorithm.mutation: copy;
68 			writeln("src:",src, "dst:",dst, "stdout:",stdout, "stderr:",stderr);
69 			() @trusted { src.byLine().copy(dst.lockingBinaryWriter); } (); /+ TODO: writeIndented +/
70 		}
71 
72 		if (result == 0) {
73 			if (logLevel <= LogLevel.trace)
74 				.trace("Process exited successfully with exitStatus:", result);
75 		} else {
76 			if (logLevel <= LogLevel.error)
77 				.error("Process exited unsuccessfully with exitStatus:", result);
78 		}
79 
80 		import std.stdio : stdout, stderr;
81 		if (result != 0 && logLevel <= LogLevel.info) {
82 			() @trusted { if (processPipes.stdout != stdout) echo(processPipes.stdout, stdout, "OUT"); } ();
83 		}
84 		if (result != 0 && logLevel <= LogLevel.warning) {
85 			() @trusted { if (processPipes.stderr != stderr) echo(processPipes.stderr, stderr, "ERR"); } ();
86 		}
87 
88 		return result;
89 	}
90 
91 package:
92 	ProcessPipes processPipes;
93 	LogLevel logLevel;
94 }
95 
96 Spawn spawn(scope const(char[])[] args,
97 			in LogLevel logLevel = defaultLogLevel,) {
98 	import std.process : spawnProcess;
99 	if (logLevel <= LogLevel.trace) .trace("Spawning ", args);
100 	return typeof(return)(pipeProcess(args), logLevel);
101 }
102 
103 /++ Variant of `std.stdio.write` that flushes `stdout` afterwards.
104  +/
105 void writeFlushed(Args...)(scope Args args) {
106 	import std.stdio : stdout;
107 	() @trusted { write(args, " ... "); stdout.flush(); } ();
108 }
109 
110 /++ Variant of `std.stderr.write` that flushes `stderr` afterwards.
111  +/
112 void ewriteFlushed(Args...)(scope Args args) {
113 	import std.stdio : stderr;
114 	() @trusted { stderr.write(args, " ... "); stderr.flush(); } ();
115 }