1 /** 2 * Parse command line arguments from response files. 3 * 4 * This file is not shared with other compilers which use the DMD front-end. 5 * 6 * Copyright: Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved 7 * Some portions copyright (c) 1994-1995 by Symantec 8 * Authors: $(LINK2 https://www.digitalmars.com, Walter Bright) 9 * License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 10 * Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/root/response.d, root/_response.d) 11 * Documentation: https://dlang.org/phobos/dmd_root_response.html 12 * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/root/response.d 13 */ 14 15 module dmd.root.response; 16 17 import dmd.root.file; 18 import dmd.root.filename; 19 20 /// 21 alias responseExpand = responseExpandFrom!lookupInEnvironment; 22 23 /********************************* 24 * Expand any response files in command line. 25 * Response files are arguments that look like: 26 * @NAME 27 * The names are resolved by calling the 'lookup' function passed as a template 28 * parameter. That function is expected to first check the environment and then 29 * the file system. 30 * Arguments are separated by spaces, tabs, or newlines. These can be 31 * imbedded within arguments by enclosing the argument in "". 32 * Backslashes can be used to escape a ". 33 * A line comment can be started with #. 34 * Recursively expands nested response files. 35 * 36 * To use, put the arguments in a Strings object and call this on it. 37 * 38 * Digital Mars's MAKE program can be notified that a program can accept 39 * long command lines via environment variables by preceding the rule 40 * line for the program with a *. 41 * 42 * Params: 43 * lookup = alias to a function that is called to look up response file 44 * arguments in the environment. It is expected to accept a null- 45 * terminated string and return a mutable char[] that ends with 46 * a null-terminator or null if the response file could not be 47 * resolved. 48 * args = array containing arguments as null-terminated strings 49 * 50 * Returns: 51 * `null` on success, or the first response file that could not be found 52 */ 53 const(char)* responseExpandFrom(alias lookup)(ref Strings args) nothrow 54 { 55 const(char)* cp; 56 bool recurse = false; 57 58 // i is updated by insertArgumentsFromResponse, so no foreach 59 for (size_t i = 0; i < args.length;) 60 { 61 cp = args[i]; 62 if (cp[0] != '@') 63 { 64 ++i; 65 continue; 66 } 67 args.remove(i); 68 auto buffer = lookup(&cp[1]); 69 if (!buffer) 70 { 71 /* error */ 72 /* BUG: any file buffers are not free'd */ 73 return cp; 74 } 75 76 recurse = insertArgumentsFromResponse(buffer, args, i) || recurse; 77 } 78 if (recurse) 79 { 80 /* Recursively expand @filename */ 81 if (auto missingFile = responseExpandFrom!lookup(args)) 82 /* error */ 83 /* BUG: any file buffers are not free'd */ 84 return missingFile; 85 } 86 return null; /* success */ 87 } 88 89 version (unittest) 90 { 91 char[] testEnvironment(const(char)* str) nothrow pure 92 { 93 import core.stdc.string: strlen; 94 import dmd.root.string : toDString; 95 switch (str.toDString()) 96 { 97 case "Foo": 98 return "foo @Bar #\0".dup; 99 case "Bar": 100 return "bar @Nil\0".dup; 101 case "Error": 102 return "@phony\0".dup; 103 case "Nil": 104 return "\0".dup; 105 default: 106 return null; 107 } 108 } 109 } 110 111 unittest 112 { 113 auto args = Strings(4); 114 args[0] = "first"; 115 args[1] = "@Foo"; 116 args[2] = "@Bar"; 117 args[3] = "last"; 118 119 assert(responseExpand!testEnvironment(args) == null); 120 assert(args.length == 5); 121 assert(args[0][0 .. 6] == "first\0"); 122 assert(args[1][0 .. 4] == "foo\0"); 123 assert(args[2][0 .. 4] == "bar\0"); 124 assert(args[3][0 .. 4] == "bar\0"); 125 assert(args[4][0 .. 5] == "last\0"); 126 } 127 128 unittest 129 { 130 auto args = Strings(2); 131 args[0] = "@phony"; 132 args[1] = "dummy"; 133 assert(responseExpand!testEnvironment(args)[0..7] == "@phony\0"); 134 } 135 136 unittest 137 { 138 auto args = Strings(2); 139 args[0] = "@Foo"; 140 args[1] = "@Error"; 141 assert(responseExpand!testEnvironment(args)[0..7] == "@phony\0"); 142 } 143 144 /********************************* 145 * Take the contents of a response-file 'buffer', parse it and put the resulting 146 * arguments in 'args' at 'argIndex'. 'argIndex' will be updated to point just 147 * after the inserted arguments. 148 * The logic of this should match that in setargv() 149 * 150 * Params: 151 * buffer = mutable string containing the response file 152 * args = list of arguments 153 * argIndex = position in 'args' where response arguments are inserted 154 * 155 * Returns: 156 * true if another response argument was found 157 */ 158 bool insertArgumentsFromResponse(char[] buffer, ref Strings args, ref size_t argIndex) nothrow pure 159 { 160 bool recurse = false; 161 bool comment = false; 162 163 for (size_t p = 0; p < buffer.length; p++) 164 { 165 //char* d; 166 size_t d = 0; 167 char c, lastc; 168 bool instring; 169 int numSlashes, nonSlashes; 170 switch (buffer[p]) 171 { 172 case 26: 173 /* ^Z marks end of file */ 174 return recurse; 175 case '\r': 176 case '\n': 177 comment = false; 178 goto case; 179 case 0: 180 case ' ': 181 case '\t': 182 continue; 183 // scan to start of argument 184 case '#': 185 comment = true; 186 continue; 187 case '@': 188 if (comment) 189 { 190 continue; 191 } 192 recurse = true; 193 goto default; 194 default: 195 /* start of new argument */ 196 if (comment) 197 { 198 continue; 199 } 200 args.insert(argIndex, &buffer[p]); 201 ++argIndex; 202 instring = false; 203 c = 0; 204 numSlashes = 0; 205 for (d = p; 1; p++) 206 { 207 lastc = c; 208 if (p >= buffer.length) 209 { 210 buffer[d] = '\0'; 211 return recurse; 212 } 213 c = buffer[p]; 214 switch (c) 215 { 216 case '"': 217 /* 218 Yes this looks strange,but this is so that we are 219 MS Compatible, tests have shown that: 220 \\\\"foo bar" gets passed as \\foo bar 221 \\\\foo gets passed as \\\\foo 222 \\\"foo gets passed as \"foo 223 and \"foo gets passed as "foo in VC! 224 */ 225 nonSlashes = numSlashes % 2; 226 numSlashes = numSlashes / 2; 227 for (; numSlashes > 0; numSlashes--) 228 { 229 d--; 230 buffer[d] = '\0'; 231 } 232 if (nonSlashes) 233 { 234 buffer[d - 1] = c; 235 } 236 else 237 { 238 instring = !instring; 239 } 240 break; 241 case 26: 242 buffer[d] = '\0'; // terminate argument 243 return recurse; 244 case '\r': 245 c = lastc; 246 continue; 247 // ignore 248 case ' ': 249 case '\t': 250 if (!instring) 251 { 252 case '\n': 253 case 0: 254 buffer[d] = '\0'; // terminate argument 255 goto Lnextarg; 256 } 257 goto default; 258 default: 259 if (c == '\\') 260 numSlashes++; 261 else 262 numSlashes = 0; 263 buffer[d++] = c; 264 break; 265 } 266 } 267 } 268 Lnextarg: 269 } 270 return recurse; 271 } 272 273 unittest 274 { 275 auto args = Strings(4); 276 args[0] = "arg0"; 277 args[1] = "arg1"; 278 args[2] = "arg2"; 279 280 char[] testData = "".dup; 281 size_t index = 1; 282 assert(insertArgumentsFromResponse(testData, args, index) == false); 283 assert(index == 1); 284 285 testData = (`\\\\"foo bar" \\\\foo \\\"foo \"foo "\"" # @comment`~'\0').dup; 286 assert(insertArgumentsFromResponse(testData, args, index) == false); 287 assert(index == 6); 288 289 assert(args[1][0 .. 9] == `\\foo bar`); 290 assert(args[2][0 .. 7] == `\\\\foo`); 291 assert(args[3][0 .. 5] == `\"foo`); 292 assert(args[4][0 .. 4] == `"foo`); 293 assert(args[5][0 .. 1] == `"`); 294 295 index = 7; 296 testData = "\t@recurse # comment\r\ntab\t\"@recurse\"\x1A after end\0".dup; 297 assert(insertArgumentsFromResponse(testData, args, index) == true); 298 assert(index == 10); 299 assert(args[7][0 .. 8] == "@recurse"); 300 assert(args[8][0 .. 3] == "tab"); 301 assert(args[9][0 .. 8] == "@recurse"); 302 } 303 304 unittest 305 { 306 auto args = Strings(0); 307 308 char[] testData = "\x1A".dup; 309 size_t index = 0; 310 assert(insertArgumentsFromResponse(testData, args, index) == false); 311 assert(index == 0); 312 313 testData = "@\r".dup; 314 assert(insertArgumentsFromResponse(testData, args, index) == true); 315 assert(index == 1); 316 assert(args[0][0 .. 2] == "@\0"); 317 318 testData = "ä&#\0".dup; 319 assert(insertArgumentsFromResponse(testData, args, index) == false); 320 assert(index == 2); 321 assert(args[1][0 .. 5] == "ä&#\0"); 322 323 testData = "one@\"word \0".dup; 324 assert(insertArgumentsFromResponse(testData, args, index) == false); 325 args[0] = "one@\"word"; 326 } 327 328 /********************************* 329 * Try to resolve the null-terminated string cp to a null-terminated char[]. 330 * 331 * The name is first searched for in the environment. If it is not 332 * there, it is searched for as a file name. 333 * 334 * Params: 335 * cp = null-terminated string to look resolve 336 * 337 * Returns: 338 * a mutable, manually allocated array containing the contents of the environment 339 * variable or file, ending with a null-terminator. 340 * The null-terminator is inside the bounds of the array. 341 * If cp could not be resolved, null is returned. 342 */ 343 private char[] lookupInEnvironment(scope const(char)* cp) nothrow { 344 345 import core.stdc.stdlib: getenv; 346 import core.stdc.string: strlen; 347 import dmd.root.rmem: mem; 348 349 if (auto p = getenv(cp)) 350 { 351 char* buffer = mem.xstrdup(p); 352 return buffer[0 .. strlen(buffer) + 1]; // include null-terminator 353 } 354 else 355 { 356 import dmd.root.string : toDString; 357 auto readResult = File.read(cp.toDString()); 358 if (!readResult.success) 359 return null; 360 // take ownership of buffer (leaking) 361 return cast(char[]) readResult.extractDataZ(); 362 } 363 }