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 }