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