1 /// Contains functions for interacting with the shell.
2 module jaster.cli.shell;
3 
4 /++
5  + Contains utility functions regarding the Shell/process execution.
6  + ++/
7 static final abstract class Shell
8 {
9     import std.stdio : writeln, writefln;
10     import std.traits : isInstanceOf;
11     import jaster.cli.binder;
12     import jaster.cli.userio : UserIO;
13 
14     /// The result of executing a process.
15     struct Result
16     {
17         /// The output produced by the process.
18         string output;
19 
20         /// The status code returned by the process.
21         int statusCode;
22     }
23 
24     private static
25     {
26         string[] _locationStack;
27     }
28 
29     /+ LOGGING +/
30     public static
31     {
32         deprecated("Use UserIO.configure().useVerboseLogging")
33         bool useVerboseOutput = false;
34 
35         deprecated("Use UserIO.verbosef, or one of its helper functions.")
36         void verboseLogfln(Args...)(string format, Args args)
37         {
38             if(Shell.useVerboseOutput)
39                 writefln(format, args);
40         }
41     }
42 
43     /+ COMMAND EXECUTION +/
44     public static
45     {
46         /++
47          + Executes a command via `std.process.executeShell`, and collects its results.
48          +
49          + Params:
50          +  command = The command string to execute.
51          +
52          + Returns:
53          +  The `Result` of the execution.
54          + ++/
55         Result execute(string command)
56         {
57             import std.process : executeShell;
58 
59             UserIO.verboseTracef("execute: %s", command);
60             auto result = executeShell(command);
61             UserIO.verboseTracef(result.output);
62 
63             return Result(result.output, result.status);
64         }
65 
66         /++
67          + Executes a command via `std.process.executeShell`, enforcing that the process' exit code was 0.
68          +
69          + Throws:
70          +  `Exception` if the process' exit code was anything other than 0.
71          +
72          + Params:
73          +  command = The command string to execute.
74          +
75          + Returns:
76          +  The `Result` of the execution.
77          + ++/
78         Result executeEnforceStatusZero(string command)
79         {
80             import std.format    : format;
81             import std.exception : enforce;
82 
83             auto result = Shell.execute(command);
84             enforce(result.statusCode == 0,
85                 "The command '%s' did not return status code 0, but returned %s."
86                 .format(command, result.statusCode)
87             );
88 
89             return result;
90         }
91 
92         /++
93          + Executes a command via `std.process.executeShell`, enforcing that the process' exit code was >= 0.
94          +
95          + Notes:
96          +  Positive exit codes may still indicate an error.
97          +
98          + Throws:
99          +  `Exception` if the process' exit code was anything other than 0 or above.
100          +
101          + Params:
102          +  command = The command string to execute.
103          +
104          + Returns:
105          +  The `Result` of the execution.
106          + ++/
107         Result executeEnforceStatusPositive(string command)
108         {
109             import std.format    : format;
110             import std.exception : enforce;
111 
112             auto result = Shell.execute(command);
113             enforce(result.statusCode >= 0,
114                 "The command '%s' did not return a positive status code, but returned %s."
115                 .format(command, result.statusCode)
116             );
117 
118             return result;
119         }
120 
121         /++
122          + Executes a command via `std.process.executeShell`, and checks to see if the output was empty.
123          +
124          + Params:
125          +  command = The command string to execute.
126          +
127          + Returns:
128          +  Whether the process' output was either empty, or entirely made up of white space.
129          + ++/
130         bool executeHasNonEmptyOutput(string command)
131         {
132             import std.ascii     : isWhite;
133             import std.algorithm : all;
134 
135             return !Shell.execute(command).output.all!isWhite;
136         }
137     }
138 
139     /+ WORKING DIRECTORY +/
140     public static
141     {
142         /++
143          + Pushes the current working directory onto a stack, and then changes directory.
144          +
145          + Usage:
146          +  Use `Shell.popLocation` to go back to the previous directory.
147          +
148          +  Combining `pushLocation` with `scope(exit) Shell.popLocation` is a good practice.
149          +
150          + See also:
151          +  Powershell's `Push-Location` cmdlet.
152          +
153          + Params:
154          +  dir = The directory to change to.
155          + ++/
156         void pushLocation(string dir)
157         {
158             import std.file : chdir, getcwd;
159 
160             UserIO.verboseTracef("pushLocation: %s", dir);
161             this._locationStack ~= getcwd();
162             chdir(dir);
163         }
164 
165         /++
166          + Pops the working directory stack, and then changes the current working directory to it.
167          +
168          + Assertions:
169          +  The stack must not be empty.
170          + ++/
171         void popLocation()
172         {
173             import std.file : chdir;
174 
175             assert(this._locationStack.length > 0, 
176                 "The location stack is empty. This indicates a bug as there is a mis-match between `pushLocation` and `popLocation` calls."
177             );
178 
179             UserIO.verboseTracef("popLocation: [dir after pop] %s", this._locationStack[$-1]);
180             chdir(this._locationStack[$-1]);
181             this._locationStack.length -= 1;
182         }
183     }
184 
185     /+ MISC +/
186     public static
187     {
188         /++
189          + $(B Tries) to determine if the current shell is Powershell.
190          +
191          + Notes:
192          +  On Windows, this will always be `false` because Windows.
193          + ++/
194         bool isInPowershell()
195         {
196             // Seems on Windows, powershell isn't used when using `execute`, even if the program itself is launched in powershell.
197             version(Windows) return false;
198             else return Shell.executeHasNonEmptyOutput("$verbosePreference");
199         }
200 
201         /++
202          + $(B Tries) to determine if the given command exists.
203          +
204          + Notes:
205          +  In Powershell, `Get-Command` is used.
206          +
207          +  On Linux, `which` is used.
208          +
209          +  On Windows, `where` is used.
210          +
211          + Params:
212          +  command = The command/executable to check.
213          +
214          + Returns:
215          +  `true` if the command exists, `false` otherwise.
216          + ++/
217         bool doesCommandExist(string command)
218         {
219             if(Shell.isInPowershell)
220                 return Shell.executeHasNonEmptyOutput("Get-Command "~command);
221 
222             version(linux)
223                 return Shell.executeHasNonEmptyOutput("which "~command);
224             else version(Windows)
225             {
226                 import std.algorithm : startsWith;
227 
228                 auto result = Shell.execute("where "~command);
229                 if(result.output.length == 0)
230                     return false;
231 
232                 if(result.output.startsWith("INFO: Could not find files"))
233                     return false;
234 
235                 return true;
236             }
237             else
238                 static assert(false, "`doesCommandExist` is not implemented for this platform. Feel free to make a PR!");
239         }
240 
241         /++
242          + Enforce that the given command/executable exists.
243          +
244          + Throws:
245          +  `Exception` if the given `command` doesn't exist.
246          +
247          + Params:
248          +  command = The command to check for.
249          + ++/
250         void enforceCommandExists(string command)
251         {
252             import std.exception : enforce;
253             enforce(Shell.doesCommandExist(command), "The command '"~command~"' does not exist or is not on the PATH.");
254         }
255     }
256 }