1 /** 2 * Functions for retrieving standard paths in cross-platform manner. 3 * 4 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 5 */ 6 7 module standardpaths; 8 9 private { 10 import std.process : environment; 11 import std.array; 12 import std.path; 13 import std.file; 14 import std.algorithm : splitter; 15 16 debug(standardpaths) { 17 import std.stdio : stderr; 18 } 19 } 20 21 version(Windows) { 22 private { 23 import std.c.windows.windows; 24 import std.utf; 25 import std.algorithm : canFind; 26 import std.uni : toLower, sicmp; 27 } 28 } else version(OSX) { 29 private { 30 //what to import? 31 } 32 } else version(Posix) { 33 private { 34 import std.stdio : File, StdioException; 35 import std.exception : assumeUnique, assumeWontThrow; 36 import std.conv : octal; 37 } 38 } else { 39 static assert(false, "Unsupported platform"); 40 } 41 42 /** 43 * Locations that can be passed to writablePath and standardPaths functions. 44 * See_Also: 45 * writablePath, standardPaths 46 */ 47 enum StandardPath { 48 /** 49 * Location of persisted application data. 50 */ 51 Data, 52 /** 53 * Location of configuration files. 54 * Note: on Windows it's the same as $(B Data) path. 55 */ 56 Config, 57 /** 58 * Location of cached data. 59 * Note: on Windows it's the same as $(B Data)/cache. 60 */ 61 Cache, 62 Desktop, ///User's desktop directory 63 Documents, ///User's documents 64 Pictures, ///User's pictures 65 Music, ///User's music 66 Videos, ///User's videos (movies) 67 68 /** 69 * Directory for user's downloaded files. 70 * Note: currently always return null on Windows. 71 */ 72 Download, 73 Templates, ///Location of templates. 74 75 /** 76 * Public share folder. 77 * Note: available only on systems with xdg-user-dirs (Linux, FreeBSD) 78 */ 79 PublicShare, 80 /** 81 * Location of fonts files. 82 * Note: don't relie on this on freedesktop. Better consider using $(LINK2 http://www.freedesktop.org/wiki/Software/fontconfig/, fontconfig library) 83 */ 84 Fonts, 85 Applications, ///User's applications. 86 } 87 88 /** 89 * Returns: path to user home directory, or an empty string if could not determine home directory. 90 * Relies on environment variables. 91 * Note: this function does not provide caching of its result. 92 */ 93 string homeDir() nothrow @safe 94 { 95 version(Windows) { 96 try { //environment.get may throw on Windows 97 98 //Use GetUserProfileDirectoryW from Userenv.dll? 99 string home = environment.get("USERPROFILE"); 100 if (home.empty) { 101 string homeDrive = environment.get("HOMEDRIVE"); 102 string homePath = environment.get("HOMEPATH"); 103 if (homeDrive.length && homePath.length) { 104 home = homeDrive ~ homePath; 105 } 106 } 107 return home; 108 } 109 catch(Exception e) { 110 debug(standardpaths) stderr.writeln(e.msg); 111 return null; 112 } 113 } else { 114 string home = assumeWontThrow(environment.get("HOME")); 115 return home; 116 } 117 118 } 119 120 /** 121 * Returns: path where files of $(U type) should be written to by current user, or an empty string if could not determine path. 122 * This function does not ensure if the returned path exists and appears to be accessible directory. 123 * Note: this function does not provide caching of its results. 124 */ 125 string writablePath(StandardPath type) nothrow @safe; 126 127 /** 128 * Returns: array of paths where files of $(U type) belong including one returned by $(B writablePath), or an empty array if no paths are defined for $(U type). 129 * This function does not ensure if all returned paths exist and appear to be accessible directories. 130 * Note: this function does not provide caching of its results. Also returned strings are not required to be unique. 131 * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation. 132 * See_Also: 133 * writablePath 134 */ 135 string[] standardPaths(StandardPath type) nothrow @safe; 136 137 version(Windows) { 138 private enum pathVarSeparator = ';'; 139 } else version(Posix) { 140 private enum pathVarSeparator = ':'; 141 } 142 143 version(Windows) { 144 145 private { 146 enum { 147 CSIDL_DESKTOP = 0, 148 CSIDL_INTERNET, 149 CSIDL_PROGRAMS, 150 CSIDL_CONTROLS, 151 CSIDL_PRINTERS, 152 CSIDL_PERSONAL, 153 CSIDL_FAVORITES, 154 CSIDL_STARTUP, 155 CSIDL_RECENT, 156 CSIDL_SENDTO, 157 CSIDL_BITBUCKET, 158 CSIDL_STARTMENU, // = 11 159 CSIDL_MYMUSIC = 13, 160 CSIDL_MYVIDEO, // = 14 161 CSIDL_DESKTOPDIRECTORY = 16, 162 CSIDL_DRIVES, 163 CSIDL_NETWORK, 164 CSIDL_NETHOOD, 165 CSIDL_FONTS, 166 CSIDL_TEMPLATES, 167 CSIDL_COMMON_STARTMENU, 168 CSIDL_COMMON_PROGRAMS, 169 CSIDL_COMMON_STARTUP, 170 CSIDL_COMMON_DESKTOPDIRECTORY, 171 CSIDL_APPDATA, 172 CSIDL_PRINTHOOD, 173 CSIDL_LOCAL_APPDATA, 174 CSIDL_ALTSTARTUP, 175 CSIDL_COMMON_ALTSTARTUP, 176 CSIDL_COMMON_FAVORITES, 177 CSIDL_INTERNET_CACHE, 178 CSIDL_COOKIES, 179 CSIDL_HISTORY, 180 CSIDL_COMMON_APPDATA, 181 CSIDL_WINDOWS, 182 CSIDL_SYSTEM, 183 CSIDL_PROGRAM_FILES, 184 CSIDL_MYPICTURES, 185 CSIDL_PROFILE, 186 CSIDL_SYSTEMX86, 187 CSIDL_PROGRAM_FILESX86, 188 CSIDL_PROGRAM_FILES_COMMON, 189 CSIDL_PROGRAM_FILES_COMMONX86, 190 CSIDL_COMMON_TEMPLATES, 191 CSIDL_COMMON_DOCUMENTS, 192 CSIDL_COMMON_ADMINTOOLS, 193 CSIDL_ADMINTOOLS, 194 CSIDL_CONNECTIONS, // = 49 195 CSIDL_COMMON_MUSIC = 53, 196 CSIDL_COMMON_PICTURES, 197 CSIDL_COMMON_VIDEO, 198 CSIDL_RESOURCES, 199 CSIDL_RESOURCES_LOCALIZED, 200 CSIDL_COMMON_OEM_LINKS, 201 CSIDL_CDBURN_AREA, // = 59 202 CSIDL_COMPUTERSNEARME = 61, 203 CSIDL_FLAG_DONT_VERIFY = 0x4000, 204 CSIDL_FLAG_CREATE = 0x8000, 205 CSIDL_FLAG_MASK = 0xFF00 206 } 207 } 208 209 private { 210 alias GetSpecialFolderPath = extern(Windows) BOOL function (HWND, wchar*, int, BOOL) nothrow @system; 211 212 version(LinkedShell32) { 213 extern(Windows) BOOL SHGetSpecialFolderPathW(HWND, wchar*, int, BOOL) nothrow @system; 214 __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = &SHGetSpecialFolderPathW; 215 } else { 216 __gshared GetSpecialFolderPath ptrSHGetSpecialFolderPath = null; 217 } 218 219 bool hasSHGetSpecialFolderPath() nothrow @trusted { 220 return ptrSHGetSpecialFolderPath != null; 221 } 222 } 223 224 version(LinkedShell32) {} else { 225 shared static this() 226 { 227 HMODULE lib = LoadLibraryA("Shell32"); 228 if (lib) { 229 ptrSHGetSpecialFolderPath = cast(GetSpecialFolderPath)GetProcAddress(lib, "SHGetSpecialFolderPathW"); 230 } 231 } 232 } 233 234 235 private string getCSIDLFolder(int csidl) nothrow @trusted 236 { 237 import core.stdc.wchar_ : wcslen; 238 239 wchar[MAX_PATH] path = void; 240 if (ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) { 241 size_t len = wcslen(path.ptr); 242 try { 243 return toUTF8(path[0..len]); 244 } catch(Exception e) { 245 246 } 247 } 248 return null; 249 } 250 251 /// Path to $(B Roaming) data directory. This function is Windows only. 252 string roamingPath() nothrow @safe 253 { 254 return getCSIDLFolder(CSIDL_APPDATA); 255 } 256 257 string writablePath(StandardPath type) nothrow @safe 258 { 259 if (!hasSHGetSpecialFolderPath()) { 260 return null; 261 } 262 263 final switch(type) { 264 case StandardPath.Config: 265 case StandardPath.Data: 266 return getCSIDLFolder(CSIDL_LOCAL_APPDATA); 267 case StandardPath.Cache: 268 { 269 string path = getCSIDLFolder(CSIDL_LOCAL_APPDATA); 270 if (path.length) { 271 return buildPath(getCSIDLFolder(CSIDL_LOCAL_APPDATA), "cache"); 272 } 273 return null; 274 } 275 case StandardPath.Desktop: 276 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY); 277 case StandardPath.Documents: 278 return getCSIDLFolder(CSIDL_PERSONAL); 279 case StandardPath.Pictures: 280 return getCSIDLFolder(CSIDL_MYPICTURES); 281 case StandardPath.Music: 282 return getCSIDLFolder(CSIDL_MYMUSIC); 283 case StandardPath.Videos: 284 return getCSIDLFolder(CSIDL_MYVIDEO); 285 case StandardPath.Download: 286 return null; 287 case StandardPath.Templates: 288 return getCSIDLFolder(CSIDL_TEMPLATES); 289 case StandardPath.PublicShare: 290 return null; 291 case StandardPath.Fonts: 292 return null; 293 case StandardPath.Applications: 294 return getCSIDLFolder(CSIDL_PROGRAMS); 295 } 296 } 297 298 string[] standardPaths(StandardPath type) nothrow @safe 299 { 300 if (!hasSHGetSpecialFolderPath()) { 301 return null; 302 } 303 304 string commonPath; 305 306 switch(type) { 307 case StandardPath.Config: 308 case StandardPath.Data: 309 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA); 310 break; 311 case StandardPath.Desktop: 312 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY); 313 break; 314 case StandardPath.Documents: 315 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS); 316 break; 317 case StandardPath.Pictures: 318 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES); 319 break; 320 case StandardPath.Music: 321 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC); 322 break; 323 case StandardPath.Videos: 324 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO); 325 break; 326 case StandardPath.Templates: 327 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES); 328 break; 329 case StandardPath.Fonts: 330 commonPath = getCSIDLFolder(CSIDL_FONTS); 331 break; 332 case StandardPath.Applications: 333 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS); 334 break; 335 default: 336 break; 337 } 338 339 string[] paths; 340 string userPath = writablePath(type); 341 if (userPath.length) 342 paths ~= userPath; 343 if (commonPath.length) 344 paths ~= commonPath; 345 return paths; 346 } 347 348 private string[] executableExtensions() nothrow @safe 349 { 350 static bool filenamesEqual(string first, string second) nothrow { 351 try { 352 return filenameCmp(first, second) == 0; 353 } catch(Exception e) { 354 return false; 355 } 356 } 357 358 string[] extensions; 359 try { 360 extensions = environment.get("PATHEXT").splitter(pathVarSeparator).array; 361 if (canFind!(filenamesEqual)(extensions, ".exe") == false) { 362 extensions = []; 363 } 364 } catch (Exception e) { 365 366 } 367 if (extensions.empty) { 368 extensions = [".exe", ".com", ".bat", ".cmd"]; 369 } 370 return extensions; 371 } 372 } else version(OSX) { 373 374 string fsPath(short domain, OSType type) nothrow @trusted 375 { 376 import std.string : fromStringz; 377 378 FSRef fsref; 379 OSErr err = FSFindFolder(domain, type, false, &fsref); 380 if (err) { 381 return null; 382 } else { 383 ubyte[2048] buf; 384 ubyte* path = buf.ptr; 385 if (FSRefMakePath(&fsref, path, path.sizeof) == noErr) { 386 auto cpath = cast(const(char)*)path; 387 return fromStringz(cpath).idup; 388 } else { 389 return null; 390 } 391 } 392 } 393 394 string writablePath(StandardPath type) nothrow @safe 395 { 396 final switch(type) { 397 case StandardPath.Config: 398 return fsPath(kUserDomain, kPreferencesFolderType); 399 case StandardPath.Cache: 400 return fsPath(kUserDomain, kCachedDataFolderType); 401 case StandardPath.Data: 402 return fsPath(kUserDomain, kApplicationSupportFolderType); 403 case StandardPath.Desktop: 404 return fsPath(kUserDomain, kDesktopFolderType); 405 case StandardPath.Documents: 406 return fsPath(kUserDomain, kDocumentsFolderType); 407 case StandardPath.Pictures: 408 return fsPath(kUserDomain, kPictureDocumentsFolderType); 409 case StandardPath.Music: 410 return fsPath(kUserDomain, kMusicDocumentsFolderType); 411 case StandardPath.Videos: 412 return fsPath(kUserDomain, kMovieDocumentsFolderType); 413 case StandardPath.Download: 414 return null; 415 case StandardPath.Templates: 416 return null; 417 case StandardPath.PublicShare: 418 return fsPath(kUserDomain, kPublicFolderType ); 419 case StandardPath.Fonts: 420 return fsPath(kUserDomain, kFontsFolderType); 421 case StandardPath.Applications: 422 return fsPath(kUserDomain, kApplicationsFolderType); 423 } 424 } 425 426 string[] standardPaths(StandardPath type) nothrow @safe 427 { 428 string commonPath; 429 430 switch(type) { 431 case StandardPath.Fonts: 432 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 433 case StandardPath.Applications: 434 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 435 case StandardPath.Data: 436 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 437 case StandardPath.Cache: 438 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 439 } 440 441 string[] paths; 442 string userPath = writablePath(type); 443 if (userPath.length) 444 paths ~= userPath; 445 if (commonPath.length) 446 paths ~= commonPath; 447 return paths; 448 } 449 450 } else version(Posix) { 451 452 //Concat two strings, but if the first one is empty, then null string is returned. 453 private string maybeConcat(string start, string path) nothrow @safe 454 { 455 return start.empty ? null : start ~ path; 456 } 457 458 private string xdgBaseDir(in char[] envvar, string fallback) nothrow @trusted { 459 string dir = assumeWontThrow(environment.get(envvar)); 460 if (!dir.length) { 461 dir = maybeConcat(homeDir(), fallback); 462 } 463 return dir; 464 } 465 466 private string xdgUserDir(in char[] key, string fallback = null) nothrow @trusted { 467 import std.algorithm : startsWith; 468 import std.string : strip; 469 470 string fileName = maybeConcat(writablePath(StandardPath.Config), "/user-dirs.dirs"); 471 string home = homeDir(); 472 try { 473 auto f = File(fileName, "r"); 474 475 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 476 477 char[] buf; 478 while(f.readln(buf)) { 479 char[] line = strip(buf); 480 auto index = xdgdir.length; 481 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') { 482 line = line[index+1..$]; 483 if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 484 { 485 line = line[1..$-1]; 486 487 if (line.startsWith("$HOME")) { 488 return maybeConcat(home, assumeUnique(line[5..$])); 489 } 490 if (line.length == 0 || line[0] != '/') { 491 continue; 492 } 493 return assumeUnique(line); 494 } 495 } 496 } 497 } catch(Exception e) { 498 499 } 500 501 if (home.length) { 502 try { 503 auto f = File("/etc/xdg/user-dirs.defaults", "r"); 504 char[] buf; 505 while(f.readln(buf)) { 506 char[] line = strip(buf); 507 auto index = key.length; 508 if (line.startsWith(key) && line.length > index && line[index] == '=') 509 { 510 line = line[index+1..$]; 511 return home ~ "/" ~ assumeUnique(line); 512 } 513 } 514 } catch (Exception e) { 515 516 } 517 if (fallback.length) { 518 return home ~ fallback; 519 } 520 } 521 return null; 522 } 523 524 private string[] xdgConfigDirs() nothrow @trusted { 525 string configDirs = assumeWontThrow(environment.get("XDG_CONFIG_DIRS")); 526 try { 527 if (configDirs.length) { 528 return splitter(configDirs, pathVarSeparator).array; 529 } 530 } 531 catch(Exception e) { 532 533 } 534 return ["/etc/xdg"]; 535 } 536 537 private string[] xdgDataDirs() nothrow @trusted { 538 string dataDirs = assumeWontThrow(environment.get("XDG_DATA_DIRS")); 539 try { 540 if (dataDirs.length) { 541 return splitter(dataDirs, pathVarSeparator).array; 542 } 543 } catch(Exception e) { 544 545 } 546 return ["/usr/local/share", "/usr/share"]; 547 } 548 549 550 version(fontsconf) { 551 private string[] readFontsConfig(string configFile) nothrow @trusted 552 { 553 //Should be changed in future since std.xml is deprecated 554 import std.xml; 555 556 string[] paths; 557 try { 558 string contents = cast(string)read(configFile); 559 check(contents); 560 auto parser = new DocumentParser(contents); 561 parser.onEndTag["dir"] = (in Element xml) 562 { 563 string path = xml.text; 564 565 if (path.length && path[0] == '~') { 566 path = maybeConcat(homeDir(), path[1..$]); 567 } else { 568 const(string)* prefix = "prefix" in xml.tag.attr; 569 if (prefix && *prefix == "xdg") { 570 string dataPath = writablePath(StandardPath.Data); 571 if (dataPath.length) { 572 path = buildPath(dataPath, path); 573 } 574 } 575 } 576 if (path.length) { 577 paths ~= path; 578 } 579 }; 580 parser.parse(); 581 } 582 catch(Exception e) { 583 584 } 585 return paths; 586 } 587 588 private string[] fontPaths() nothrow @trusted 589 { 590 string[] paths; 591 592 string homeConfig = homeFontsConfig(); 593 if (homeConfig.length) { 594 paths ~= readFontsConfig(homeConfig); 595 } 596 597 enum configs = ["/etc/fonts/fonts.conf", //path on linux 598 "/usr/local/etc/fonts/fonts.conf"]; //path on freebsd 599 foreach(config; configs) { 600 paths ~= readFontsConfig(config); 601 } 602 return paths; 603 } 604 605 private string homeFontsConfig() nothrow @trusted { 606 return maybeConcat(writablePath(StandardPath.Config), "/fontconfig/fonts.conf"); 607 } 608 609 private string homeFontsPath() nothrow @trusted { 610 string[] paths = readFontsConfig(homeFontsConfig()); 611 if (paths.length) 612 return paths[0]; 613 return null; 614 } 615 616 } else { 617 private string homeFontsPath() nothrow @trusted { 618 return maybeConcat(homeDir(), "/.fonts"); 619 } 620 621 private string[] fontPaths() nothrow @trusted 622 { 623 enum localShare = "/usr/local/share/fonts"; 624 enum share = "/usr/share/fonts"; 625 626 string homeFonts = homeFontsPath(); 627 if (homeFonts.length) { 628 return [homeFonts, localShare, share]; 629 } else { 630 return [localShare, share]; 631 } 632 } 633 634 } 635 636 637 638 /** 639 * Returns user's runtime directory determined by $(B XDG_RUNTIME_DIR) environment variable. 640 * If directory does not exist it tries to create one with appropriate permissions. On fail returns an empty string. 641 * Note: this function is defined only on $(B Posix) systems (except for OS X) 642 */ 643 string runtimeDir() nothrow @trusted 644 { 645 // Do we need it on BSD systems? 646 647 import core.sys.posix.pwd; 648 import core.sys.posix.unistd; 649 import core.sys.posix.sys.stat; 650 import core.sys.posix.sys.types; 651 import core.stdc.errno; 652 import core.stdc.string; 653 654 import std.string : fromStringz, toStringz; 655 656 const uid_t uid = getuid(); 657 string runtime = assumeWontThrow(environment.get("XDG_RUNTIME_DIR")); 658 659 mode_t runtimeMode = octal!700; 660 661 if (!runtime.length) { 662 setpwent(); 663 passwd* pw = getpwuid(uid); 664 endpwent(); 665 666 try { 667 if (pw && pw.pw_name) { 668 runtime = tempDir() ~ "/runtime-" ~ assumeUnique(fromStringz(pw.pw_name)); 669 670 if (!(runtime.exists && runtime.isDir)) { 671 if (mkdir(runtime.toStringz, runtimeMode) != 0) { 672 debug(standardpaths) stderr.writefln("Failed to create runtime directory %s: %s", runtime, fromStringz(strerror(errno))); 673 return null; 674 } 675 } 676 } else { 677 debug(standardpaths) stderr.writefln("Failed to get user name to create runtime directory"); 678 return null; 679 } 680 } catch(Exception e) { 681 debug(standardpaths) stderr.writeln(e.msg); 682 return null; 683 } 684 } 685 stat_t statbuf; 686 stat(runtime.toStringz, &statbuf); 687 if (statbuf.st_uid != uid) { 688 debug(standardpaths) stderr.writefln("Wrong ownership of runtime directory %s, %d instead of %d", runtime, statbuf.st_uid, uid); 689 return null; 690 } 691 if ((statbuf.st_mode & octal!777) != runtimeMode) { 692 debug(standardpaths) stderr.writefln("Wrong permissions on runtime directory %s, %o instead of %o", runtime, statbuf.st_mode, runtimeMode); 693 return null; 694 } 695 696 return runtime; 697 } 698 699 string writablePath(StandardPath type) nothrow @safe 700 { 701 final switch(type) { 702 case StandardPath.Config: 703 return xdgBaseDir("XDG_CONFIG_HOME", "/.config"); 704 case StandardPath.Cache: 705 return xdgBaseDir("XDG_CACHE_HOME", "/.cache"); 706 case StandardPath.Data: 707 return xdgBaseDir("XDG_DATA_HOME", "/.local/share"); 708 case StandardPath.Desktop: 709 return xdgUserDir("DESKTOP", "/Desktop"); 710 case StandardPath.Documents: 711 return xdgUserDir("DOCUMENTS"); 712 case StandardPath.Pictures: 713 return xdgUserDir("PICTURES"); 714 case StandardPath.Music: 715 return xdgUserDir("MUSIC"); 716 case StandardPath.Videos: 717 return xdgUserDir("VIDEOS"); 718 case StandardPath.Download: 719 return xdgUserDir("DOWNLOAD"); 720 case StandardPath.Templates: 721 return xdgUserDir("TEMPLATES", "/Templates"); 722 case StandardPath.PublicShare: 723 return xdgUserDir("PUBLICSHARE", "/Public"); 724 case StandardPath.Fonts: 725 return homeFontsPath(); 726 case StandardPath.Applications: 727 return maybeConcat(writablePath(StandardPath.Data), "/applications"); 728 } 729 } 730 731 string[] standardPaths(StandardPath type) nothrow @safe 732 { 733 string[] paths; 734 735 switch(type) { 736 case StandardPath.Data: 737 paths = xdgDataDirs(); 738 break; 739 case StandardPath.Config: 740 paths = xdgConfigDirs(); 741 break; 742 case StandardPath.Applications: 743 paths = ["/usr/local/share/applications", "/usr/share/applications"]; 744 break; 745 case StandardPath.Fonts: 746 return fontPaths(); 747 default: 748 break; 749 } 750 751 string userPath = writablePath(type); 752 if (userPath.length) { 753 paths = userPath ~ paths; 754 } 755 return paths; 756 } 757 } else { 758 static assert(false, "Unsupported platform"); 759 } 760 761 private bool isExecutable(string filePath) nothrow @trusted { 762 try { 763 version(Posix) { 764 return (getAttributes(filePath) & octal!100) != 0; 765 } else version(Windows) { 766 //Use GetEffectiveRightsFromAclW? 767 768 string extension = filePath.extension; 769 const(string)[] exeExtensions = executableExtensions(); 770 foreach(ext; exeExtensions) { 771 if (sicmp(extension, ext) == 0) 772 return true; 773 } 774 return false; 775 776 } else { 777 static assert(false, "Unsupported platform"); 778 } 779 } catch(Exception e) { 780 return false; 781 } 782 } 783 784 private string checkExecutable(string filePath) nothrow @trusted { 785 try { 786 if (filePath.isFile && filePath.isExecutable) { 787 return buildNormalizedPath(filePath); 788 } else { 789 return null; 790 } 791 } 792 catch(Exception e) { 793 return null; 794 } 795 } 796 797 /** 798 * Finds executable by $(B fileName) in the paths specified by $(B paths). 799 * Returns: absolute path to the existing executable file or an empty string if not found. 800 * Params: 801 * fileName = name of executable to search 802 * paths = array of directories where executable should be searched. If not set, search in system paths, usually determined by PATH environment variable 803 * Note: on Windows when fileName extension is omitted, executable extensions will be automatically appended during search. 804 */ 805 string findExecutable(string fileName, in string[] paths = []) nothrow @safe 806 { 807 try { 808 if (fileName.isAbsolute()) { 809 return checkExecutable(fileName); 810 } 811 812 const(string)[] searchPaths = paths; 813 if (searchPaths.empty) { 814 string pathVar = environment.get("PATH"); 815 if (pathVar.length) { 816 searchPaths = splitter(pathVar, pathVarSeparator).array; 817 } 818 } 819 820 if (searchPaths.empty) { 821 return null; 822 } 823 824 string toReturn; 825 foreach(string path; searchPaths) { 826 string candidate = buildPath(absolutePath(path), fileName); 827 828 version(Windows) { 829 if (candidate.extension.empty) { 830 foreach(exeExtension; executableExtensions()) { 831 toReturn = checkExecutable(setExtension(candidate, exeExtension.toLower())); 832 if (toReturn.length) { 833 return toReturn; 834 } 835 } 836 } 837 } 838 839 toReturn = checkExecutable(candidate); 840 if (toReturn.length) { 841 return toReturn; 842 } 843 } 844 } catch (Exception e) { 845 846 } 847 return null; 848 } 849 850