1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 */ 6 module button.command; 7 8 import button.log; 9 import button.resource; 10 import button.context; 11 12 /** 13 * Thrown if a command fails. 14 */ 15 class CommandError : Exception 16 { 17 int exitCode; 18 19 this(int exitCode) 20 { 21 import std.format : format; 22 23 super("Command failed with exit code %d".format(exitCode)); 24 25 this.exitCode = exitCode; 26 } 27 } 28 29 /** 30 * Escapes the argument according to the rules of bash, the most commonly used 31 * shell. This is mostly used for cosmetic purposes when printing out argument 32 * arrays where they could be copy-pasted into a shell. 33 */ 34 string escapeShellArg(string arg) pure 35 { 36 import std.array : appender; 37 import std.algorithm.searching : findAmong; 38 import std.range : empty; 39 import std.exception : assumeUnique; 40 41 if (arg.empty) 42 return `""`; 43 44 // Characters that require the string to be quoted. 45 static immutable special = " '~*[]?"; 46 47 immutable quoted = !arg.findAmong(special).empty; 48 49 auto result = appender!(char[]); 50 51 if (quoted) 52 result.put('"'); 53 54 foreach (c; arg) 55 { 56 // Characters to escape 57 if (c == '\\' || c == '"' || c == '$' || c == '`') 58 { 59 result.put("\\"); 60 result.put(c); 61 } 62 else 63 { 64 result.put(c); 65 } 66 } 67 68 if (quoted) 69 result.put('"'); 70 71 return assumeUnique(result.data); 72 } 73 74 unittest 75 { 76 assert(escapeShellArg(``) == `""`); 77 assert(escapeShellArg(`foo`) == `foo`); 78 assert(escapeShellArg(`foo bar`) == `"foo bar"`); 79 assert(escapeShellArg(`foo'bar`) == `"foo'bar"`); 80 assert(escapeShellArg(`foo?bar`) == `"foo?bar"`); 81 assert(escapeShellArg(`foo*.c`) == `"foo*.c"`); 82 assert(escapeShellArg(`foo.[ch]`) == `"foo.[ch]"`); 83 assert(escapeShellArg(`~foobar`) == `"~foobar"`); 84 assert(escapeShellArg(`$PATH`) == `\$PATH`); 85 assert(escapeShellArg(`\`) == `\\`); 86 assert(escapeShellArg(`foo"bar"`) == `foo\"bar\"`); 87 assert(escapeShellArg("`pwd`") == "\\`pwd\\`"); 88 } 89 90 /** 91 * A single command. 92 */ 93 struct Command 94 { 95 /** 96 * Arguments to execute. The first argument is the name of the executable. 97 */ 98 immutable(string)[] args; 99 100 alias args this; 101 102 // Root of the build directory. This is used to normalize implicit resource 103 // paths. 104 string buildRoot; 105 106 /** 107 * The result of executing a command. 108 */ 109 struct Result 110 { 111 import core.time : TickDuration; 112 113 /** 114 * Implicit input and output resources this command used. 115 */ 116 Resource[] inputs, outputs; 117 118 /** 119 * How long it took the command to run from start to finish. 120 */ 121 TickDuration duration; 122 } 123 124 this(immutable(string)[] args) 125 { 126 assert(args.length > 0, "A command must have >0 arguments"); 127 128 this.args = args; 129 } 130 131 /** 132 * Compares this command with another. 133 */ 134 int opCmp()(const auto ref typeof(this) that) const pure nothrow 135 { 136 import std.algorithm.comparison : cmp; 137 return cmp(this.args, that.args); 138 } 139 140 /// Ditto 141 bool opEquals()(const auto ref typeof(this) that) const pure nothrow 142 { 143 return this.opCmp(that) == 0; 144 } 145 146 unittest 147 { 148 import std.algorithm.comparison : cmp; 149 150 static assert(Command(["a", "b"]) == Command(["a", "b"])); 151 static assert(Command(["a", "b"]) != Command(["a", "c"])); 152 static assert(Command(["a", "b"]) < Command(["a", "c"])); 153 static assert(Command(["b", "a"]) > Command(["a", "b"])); 154 155 static assert(cmp([Command(["a", "b"])], [Command(["a", "b"])]) == 0); 156 static assert(cmp([Command(["a", "b"])], [Command(["a", "c"])]) < 0); 157 static assert(cmp([Command(["a", "c"])], [Command(["a", "b"])]) > 0); 158 } 159 160 /** 161 * Returns a string representation of the command. 162 * 163 * Since the command is in argv format, we format it into a string as one 164 * would enter into a shell. 165 */ 166 string toPrettyString() const pure 167 { 168 import std.array : join; 169 import std.algorithm.iteration : map; 170 171 return args.map!(arg => arg.escapeShellArg).join(" "); 172 } 173 174 /** 175 * Returns a short string representation of the command. 176 */ 177 @property string toPrettyShortString() const pure nothrow 178 { 179 return args[0]; 180 } 181 182 /** 183 * Executes the command. 184 */ 185 Result execute(ref BuildContext ctx, string workDir, TaskLogger logger) const 186 { 187 import std.path : buildPath; 188 import std.datetime : StopWatch, AutoStart; 189 import button.handler : executeHandler = execute; 190 191 auto inputs = Resources(ctx.root, workDir); 192 auto outputs = Resources(ctx.root, workDir); 193 194 auto sw = StopWatch(AutoStart.yes); 195 196 executeHandler( 197 ctx, 198 args, 199 buildPath(ctx.root, workDir), 200 inputs, outputs, 201 logger 202 ); 203 204 return Result(inputs.data, outputs.data, sw.peek()); 205 } 206 }