FileSystem.js

/* globals Wsh: false */

(function () {
  if (Wsh && Wsh.FileSystem) return;

  /**
   * This module takes charge of handling file and directory (similar to Node.js-FileSystem).
   *
   * @namespace FileSystem
   * @memberof Wsh
   * @requires {@link https://github.com/tuckn/WshOS|tuckn/WshOS}
   */
  Wsh.FileSystem = {};

  // Shorthands
  var CD = Wsh.Constants;
  var util = Wsh.Util;
  var fso = Wsh.FileSystemObject;
  var path = Wsh.Path;
  var os = Wsh.OS;

  var insp = util.inspect;
  var obtain = util.obtainPropVal;
  var isArray = util.isArray;
  var isString = util.isString;
  var isSolidString = util.isSolidString;
  var parseDate = util.createDateString;
  var startsWith = util.startsWith;
  var endsWith = util.endsWith;
  var srrd = os.surroundCmdArg;
  var XCOPY = os.exefiles.xcopy;

  var fs = Wsh.FileSystem;

  /** @constant {string} */
  var MODULE_TITLE = 'WshModeJs/FileSystem.js';

  /**
   * @function fs.throwTypeErrorNonExisting {{{
   * @param {string} moduleTitle
   * @param {string} functionName
   * @param {any} errVal
   */
  fs.throwTypeErrorNonExisting = function (moduleTitle, functionName, errVal) {
    throw new Error('Error: ENOENT: no such file or directory\n'
      + '  at ' + functionName + ' (' + moduleTitle + ')\n'
      + '  path: ' + insp(errVal));
  }; // }}}

  var throwErrNonExist = function (functionName, typeErrVal) {
    fs.throwTypeErrorNonExisting(MODULE_TITLE, functionName, typeErrVal);
  };

  var throwErrNonArray = function (functionName, typeErrVal) {
    util.throwTypeError('array', MODULE_TITLE, functionName, typeErrVal);
  };

  var throwErrNonStr = function (functionName, typeErrVal) {
    util.throwTypeError('string', MODULE_TITLE, functionName, typeErrVal);
  };

  // fs.constants {{{
  /**
   * Returns an object containing commonly used constants for file system operations. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_constants|Node.js-Path}.
   *
   * @namespace constants
   * @memberof Wsh.FileSystem
   */
  /** @lends Wsh.FileSystem.constants */
  fs.constants = {
    // File Access Constants
    /** @constant {number} */
    F_OK: 0,
    /** @constant {number} */
    R_OK: 4,
    /** @constant {number} */
    W_OK: 2,
    /** @constant {number} */
    X_OK: 1,
    // File Copy Constants
    /** @constant {number} */
    COPYFILE_EXCL: 1,
    // File Open Constants
    /** @constant {number} */
    O_RDONLY: 0,
    /** @constant {number} */
    O_WRONLY: 1,
    /** @constant {number} */
    O_RDWR: 2,
    /** @constant {number} */
    O_CREAT: 256,
    /** @constant {number} */
    O_EXCL: 1024,
    /** @constant {number} */
    O_TRUNC: 512,
    /** @constant {number} */
    O_APPEND: 8,
    // File Type Constants
    /** @constant {number} */
    S_IFMT: 61440,
    /** @constant {number} */
    S_IFREG: 32768,
    /** @constant {number} */
    S_IFDIR: 16384,
    /** @constant {number} */
    S_IFCHR: 8192,
    /** @constant {number} */
    S_IFLNK: 40960,
    // ?
    /** @constant {number} */
    UV_FS_COPYFILE_EXCL: 1
  }; // }}}

  // fs.inspectPathWhetherMAX_PATH {{{
  /**
   * Checks the length of a path. In the Windows API, the maximum length for a path is MAX_PATH, which is defined as 260 characters. {@link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation|Maximum Path Length Limitation}.
   *
   * @example
   * var tooLongPath = 'C:\\Users\\UserName\\AppData\\Roaming\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\ImplicitAppShortcuts\\database\\Document and Settings\\MongoDB Inc\\MongoDB Compass Community\\computer\\operationsManagement\\img\\01_original_screencapture-20180213T043515+0900.png';
   *
   * Wsh.FileSystem.inspectPathWhetherMAX_PATH(tooLongPath); // Throws an Error
   * @function inspectPathWhetherMAX_PATH
   * @memberof Wsh.FileSystem
   * @param {string} fullPath - The file-path to chekc.
   * @throws {string} - If a length of the file-path is over then 255.
   * @returns {void}
   */
  fs.inspectPathWhetherMAX_PATH = function (fullPath) {
    var FN = 'fs.inspectPathWhetherMAX_PATH';
    var pathLen = fullPath.length;
    if (pathLen > 255) {
      throw new Error('Error: [Too long file path!] Over 255 characters\n'
          + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
          + ' ' + pathLen + ' length "' + fullPath + '"');
    }
  }; // }}}

  // Create

  // fs.mkdirSync {{{
  /**
   * Synchronously creates the directory. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_mkdirsync_path_options|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.mkdirSync('D:\\MyDir');
   *
   * // The following does not work.
   * // Ex.1 The existing directory
   * fs.mkdirSync('D:\\MyDir'); // Throws an Error
   * // Ex.2 Non-existing parent directory
   * fs.mkdirSync('R:\\NonExistingDir\\NewDir'); // Throws an Error
   * @function mkdirSync
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - The directory file path to create.
   * @param {object} [options] - Optional parameters.
   * @param {boolean} [options.isRecursive] - @TODO W.I.P
   * @returns {void}
   */
  fs.mkdirSync = function (dirPath/* , options */) {
    var FN = 'fs.mkdirSync';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);

    fs.inspectPathWhetherMAX_PATH(dirPath);
    fso.CreateFolder(dirPath);
  }; // }}}

  // fs.writeFileSync {{{
  /**
   * Synchronously writes data to the file. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_writefilesync_file_data_options|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   * var writeData = 'Foo Bar';
   *
   * // The default is writing data to the file as binary (UTF-16 LE NoBOM)
   * fs.writeFileSync('D:\\MyData.bin', writeData);
   *
   * fs.writeFileSync('D:\\my-note.txt', writeData, { encoding: 'utf8' });
   *
   * fs.writeFileSync('D:\\MyNote.txt', writeData, { encoding: 'sjis' });
   *
   * fs.writeFileSync('D:\\my-script.wsf', writeData, {
   *   encoding: 'utf8', bom: true
   * });
   *
   * fs.writeFileSync('D:\\fixme.txt', '', { encoding: 'utf8' });
   * // Thorws a error! Fix this (T_T)
   * @function writeFileSync
   * @memberof Wsh.FileSystem
   * @param {string} fpath - The file-path to write.
   * @param {(string|Buffer)} data - Data of the file.
   * @param {object} [options] - Optional parameters.
   * @param {string} [options.encoding='binary'] - utf8(UTF-8), utf16(UTF-16 LE), sjis(Shift_JIS, CP932), All Charset -> HKEY_CLASSES_ROOT\Mime\Database\Charset\
   * @param {boolean} [options.bom=false] - true => enable, others => disable
   * @returns {void}
   */
  fs.writeFileSync = function (fpath, data, options) {
    var FN = 'fs.writeFileSync';
    if (!isString(fpath)) throwErrNonStr(FN, fpath);
    if (data === undefined) data = 'undefined'; // No error in Node.js

    var encoding = obtain(options, 'encoding', 'binary');
    var bom = obtain(options, 'bom', false);

    // @note Stringを.strm.Write()するとErrorとなる。WSHのString型の内部エンコードはUTF16LE?っぽいので、それ用にoptionsを切り替える
    if (/^bin(ary)?$/i.test(encoding) && isString(data)) {
      encoding = 'unicode';
      bom = false;
    }

    var strm = WScript.CreateObject('ADODB.Stream');

    /** -2146825284エラー ファイルへ書き込めませんでした。{@link https://stackoverflow.com/questions/16652896/adodb-stream-error-800a0bbc-write-to-file-failed} の対策用ダミーファイル。ダミー→本保存とすることで回避する */
    var fpathTmp = os.makeTmpPath();

    try {
      // Writing as binary
      if (/^bin(ary)?$/i.test(encoding)) {
        strm.Type = CD.ado.types.binary;
        // strm.Charset = CD.ado.charset.latin1; // 1 Byte character
        // strm.Type = CD.ado.types.text; // Write as 1 Byte text
        strm.Open();
        strm.Write(data);
        strm.SaveToFile(fpathTmp, CD.ado.saveCreates.overWrite);
        strm.Close();
        fs.copyFileSync(fpathTmp, fpath); // Save
        fs.unlinkSync(fpathTmp); // Delete the dummy file

        strm = null;
        return;
      }

      // Writing as text
      strm.Type = CD.ado.types.text;

      // Set character encoding
      if (/utf(-)?8/i.test(encoding)) {
        strm.Charset = CD.ado.charset.utf8;
      } else if (/utf(-)?16/i.test(encoding)) {
        strm.Charset = CD.ado.charset.utf16; // @note Unicode
      } else if (/(cp932|sjis|shift([-_])?jis)/i.test(encoding)) {
        strm.Charset = CD.ado.charset.sjis;
      } else {
        strm.Charset = encoding;
      }

      strm.Open();
      strm.WriteText(data);

      if (!/(utf-|unicode)/i.test(strm.Charset)) {
        strm.SaveToFile(fpathTmp, CD.ado.saveCreates.overWrite);
        strm.Close();
      } else if (bom) {
        /** CharasetがUTF系でWriteTextするとBOM付きで書き込まれるため、BOM付き指示があった場合は何もしない @todo support utf-7 */
        strm.SaveToFile(fpathTmp, CD.ado.saveCreates.overWrite);
        strm.Close();
      } else if (!bom) {
        // Remove BOM
        var bomLength = (strm.Charset === CD.ado.charset.utf8) ? 3 : 2;
        var strmNoBom = WScript.CreateObject('ADODB.Stream');
        var binNoBom;

        // バイナリモードにするためにPositionを一度0に戻す
        // Readするためにはバイナリタイプでないといけない
        strm.Position = 0;
        strm.Type = CD.ado.types.binary;
        // Positionに数値をいれると、そのByte分、先頭ポインタが移動する
        // 最初の2 or 3 Byteを飛ばして保存することでBOMを削除する
        strm.Position = bomLength;
        binNoBom = strm.Read(CD.ado.reads.all);
        // @TODO ここでSwapすればUTF16BEにも対応できる…が、今はいいや
        strm.Close();

        // 読み込んだバイナリデータをファイルに出力する
        strmNoBom.Type = CD.ado.types.binary;
        strmNoBom.Open();
        strmNoBom.Write(binNoBom);
        strmNoBom.SaveToFile(fpathTmp, CD.ado.saveCreates.overWrite);
        strmNoBom.Close();
      }

      fs.copyFileSync(fpathTmp, fpath); // Save
      fs.unlinkSync(fpathTmp); // Delete the dummy file

      strm = null;
    } catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  file: "' + fpath + '"\n  encoding: "' + encoding + '"\n'
        + '  bom: "' + bom + '"\n  data: ' + data);
    }
  }; // }}}

  // fs.writeTmpFileSync {{{
  /**
   * Writes the data to a new temporary path, and Return the path.
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   * var writeData = 'Foo Bar';
   *
   * var tmpPath = fs.writeTmpFileSync(writeData, { encoding: 'utf8' });
   * // Returns: 'C:\\Users\\UserName\\AppData\\Local\\Temp\\fs-writeTmpFileSync_rad6E884.tmp'
   * @function writeTmpFileSync
   * @memberof Wsh.FileSystem
   * @param {string} data - The temporary data to write.
   * @param {object} [options] - See {@link Wsh.FileSystem.writeFileSync}
   * @returns {string} - The temporary file path.
   */
  fs.writeTmpFileSync = function (data, options) {
    var tmpFilePath = os.makeTmpPath('fs-writeTmpFileSync_');
    fs.writeFileSync(tmpFilePath, data, options);
    return tmpFilePath;
  }; // }}}

  // Read

  // fs.existsSync {{{
  /**
   * Returns true if the path exists, false otherwise. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_existssync_path|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.existsSync('D:\\ExistingDir'); // true
   * fs.existsSync('D:\\ExistingDir\\File.path'); // true
   * fs.existsSync('D:\\NonExistingDir\\File.path'); // false
   * fs.existsSync('\\\\MyComp\\Public\\ExistingDir\\File.path'); // true
   * @function existsSync
   * @memberof Wsh.FileSystem
   * @param {string} fpath - The file-path to check.
   * @returns {boolean} - If the file is existing returns true. else false.
   */
  fs.existsSync = function (fpath) {
    if (!isSolidString(fpath)) return false;
    fs.inspectPathWhetherMAX_PATH(fpath);

    return fso.FileExists(fpath) || fso.FolderExists(fpath);
  }; // }}}

  // fs.statSync {{{
  /**
   * @typedef {object} typeFsStat
   * @property {number} size
   * @property {function} isFile
   * @property {function} isDirectory
   * @property {function} isSymbolicLink
   * @property {Date} atime - The last time the file was accessed.
   * @property {Date} mtime - The last time the file was modified.
   * @property {Date} [ctime] - The last time the file status was changed.
   * @property {Date} birthtime - The creation time of the file.
   */

  /**
   * Returns information about the file. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_statsync_path_options|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * var stat = fs.statSync('D:\\My Dir\\File.path');
   * stat.size; // 2345
   * stat.isFile(); // true
   * stat.isDirectory(); // false
   * stat.isSymbolicLink(); // false
   * stat.atime; // Thu Sep 3 07:16:02 UTC+0900 2020,
   * stat.mtime; // Sat Feb 3 07:18:51 UTC+0900 2018,
   * stat.ctime; // null,
   * stat.birthtime; // Thu Sep 3 07:16:02 UTC+0900 2020
   *
   * var stat = fs.statSync('D:\\Symlink Dir');
   * stat.size; // 0
   * stat.isFile(); // false
   * stat.isDirectory(); // true
   * stat.isSymbolicLink(); // true
   * @function statSync
   * @memberof Wsh.FileSystem
   * @param {string} fpath - The file-path to check.
   * @returns {typeFsStat} - An object provides information about a file.
   */
  fs.statSync = function (fpath) {
    var FN = 'fs.statSync';
    if (!isString(fpath)) throwErrNonStr(FN, fpath);
    if (!fs.existsSync(fpath)) throwErrNonExist(FN, fpath);

    var fObj;
    var size;
    var isFile;
    var isDir;

    if (fso.FileExists(fpath)) {
      fObj = fso.GetFile(fpath);
      size = fObj.Size;
      isFile = true;
      isDir = false;
    } else if (fso.FolderExists(fpath)) {
      fObj = fso.GetFolder(fpath);
      size = 0;
      isFile = false;
      isDir = true;
    } else {
      // Throws a error
    }

    var attr = fObj.Attributes + 0;
    /* global FILE_ATTRIBUTE_SYMLINKD_DIR, FILE_ATTRIBUTE_SYMLINKD_FILE */
    var isSymlink = (attr === FILE_ATTRIBUTE_SYMLINKD_DIR || attr === FILE_ATTRIBUTE_SYMLINKD_FILE);
    /**
     * Old code. Use fsutil {@link https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc753059(v=ws.11)|MS Docs}
     */
    // var exeCmd = 'fsutil reparsepoint query "' + fpath + '"';
    // var oExec = sh.execSync(exeCmd);
    //
    // return (oExec.stdout.indexOf(': Symbolic Link') !== -1);

    var atime = new Date(fObj.DateLastAccessed); // Accessed Time
    var mtime = new Date(fObj.DateLastModified); // Modified Time
    var ctime = null; // Changed Time
    var birthtime = new Date(fObj.DateCreated); // Creation Time

    return {
      size: size,
      isFile: function () { return isFile; },
      isDirectory: function () { return isDir; },
      isSymbolicLink: function () { return isSymlink; },
      atime: atime,
      mtime: mtime,
      ctime: ctime,
      birthtime: birthtime
    };
  }; // }}}

  // fs.readFileSync {{{
  /**
   * Synchronously reads the entire contents of a file. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_readfilesync_path_options|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * var readBin = fs.readFileSync('D:\\MyPage\\index.html');
   * // The default is reading the file as binary
   *
   * var readHtml = fs.readFileSync('D:\\MyPage\\index.html', { encoding: 'utf8' });
   * var readText = fs.readFileSync('D:\\MyNote\\memo.txt', { encoding: 'sjis' });
   * @function readFileSync
   * @memberof Wsh.FileSystem
   * @param {string} fpath - The file-path to read.
   * @param {object} [options] - Optional parameters.
   * @param {string} [options.encoding='binary'] - latin1 (iso-8859-1), utf8, utf16, sjis (shift_jis, cp932), All Charset -> HKEY_CLASSES_ROOT\Mime\Database\Charset\
   * @param {boolean} [options.throws=true] - Whether throws an error or not when catch.
   * @returns {unknown|string} - 'unknown' is the result of the typeof judgment of JScript. Means binary?
   */
  fs.readFileSync = function (fpath, options) {
    var FN = 'fs.readFileSync';
    if (!isString(fpath)) throwErrNonStr(FN, fpath);
    if (!fs.statSync(fpath).isFile()) throwErrNonExist(FN, fpath);

    var encoding = obtain(options, 'encoding', 'binary');
    var throws = obtain(options, 'throws', true);
    var strm = WScript.CreateObject('ADODB.Stream');
    var data = '';

    try {
      if (/^bin(ary)?$/i.test(encoding)) {
        strm.Type = CD.ado.types.binary;
        strm.Open();
        strm.LoadFromFile(fpath);
        data = strm.Read(CD.ado.reads.all);
      } else {
        strm.Type = CD.ado.types.text;

        // Set character encoding
        if (/latin(-)?1/i.test(encoding)) {
          strm.Charset = CD.ado.charset.latin1;
        } else if (/utf(-)?8/i.test(encoding)) {
          strm.Charset = CD.ado.charset.utf8;
        } else if (/utf(-)?16/i.test(encoding)) {
          strm.Charset = CD.ado.charset.utf16; // @note Unicode
        } else if (/(cp932|sjis|shift([-_])?jis)/i.test(encoding)) {
          strm.Charset = CD.ado.charset.sjis;
        } else {
          strm.Charset = encoding;
        }

        strm.Open();
        strm.LoadFromFile(fpath);
        data = strm.ReadText(CD.ado.reads.all);
      }
      strm.Close();
    } catch (e) {
      if (throws) {
        throw new Error(insp(e) + '\n'
          + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
          + '  file: "' + fpath + '"\n  encoding: "' + encoding + '"\n');
      }
    }

    strm = null;
    return data;
  }; // }}}

  // fs.getChildrenFiles {{{
  /**
   * @typedef {object} typeGetChildrenFilesOptions
   * @property {string} [prefixDirName]
   * @property {boolean} [withFileTypes=false] - When false, the result is string[], like ['relative path1', 'relative path1', ...]. When true, the result is Object[], like [{ name: 'full path1', isDirectory: true} ...]
   * @property {boolean} [options.ignoresErr=false] - Even if an error occurs during processing, it will be ignored.
   * @property {boolean} [excludesSymlink=false] - Excluding symblic-links
   * @property {string} [matchedRegExp] - Ex. "\\w+\\.txt$"
   * @property {string} [ignoredRegExp] - Ex. "[_\\-.]cache\\d+"
   */

  /**
   * Gets files info in the specified directory.
   *
   * @function getChildrenFiles
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - The directory path to read.
   * @param {typeGetChildrenFilesOptions} [options] - Optional parameters.
   * @returns {(string[]|Object[])}
   */
  fs.getChildrenFiles = function (dirPath, options) {
    var FN = 'fs.getChildrenFiles';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);
    if (!fs.statSync(dirPath).isDirectory()) throwErrNonExist(FN, dirPath);

    var prefixDirName = obtain(options, 'prefixDirName', '');

    var matchedRegExp = obtain(options, 'matchedRegExp', null);
    var mtchRE = isSolidString(matchedRegExp) ? new RegExp(matchedRegExp, 'i') : null;

    var ignoredRegExp = obtain(options, 'ignoredRegExp', null);
    var ignrRE = isSolidString(ignoredRegExp) ? new RegExp(ignoredRegExp, 'i') : null;

    var ignoresErr = obtain(options, 'ignoresErr', false);
    var excludesSymlink = obtain(options, 'excludesSymlink', false);
    var withFileTypes = obtain(options, 'withFileTypes', false);

    var objDir = fso.GetFolder(path.normalize(dirPath));
    var enmFile = new Enumerator(objDir.Files);
    var files = [];
    var itm, filename, fullName, isSymlink, fileInfo;

    // @NOTE なるべくstatSyncとisSymbokickLinkをかまさないことで処理を軽くする
    for (enmFile.moveFirst(); !enmFile.atEnd(); enmFile.moveNext()) {
      itm = enmFile.item();
      filename = itm.Name.toString();
      fullName = path.join(prefixDirName, filename);

      if (mtchRE && !mtchRE.test(fullName)) continue;
      if (ignrRE && ignrRE.test(fullName)) continue;

      try {
        if (withFileTypes) {
          isSymlink = fs.statSync(itm.Path).isSymbolicLink();
          if (excludesSymlink && isSymlink) continue;

          fileInfo = {
            name: fullName,
            path: itm.Path,
            attributes: itm.Attributes,
            isDirectory: false,
            isFile: true,
            isSymbolicLink: isSymlink,
            size: itm.Size,
            // type: itm.Type, "ファイル フォルダー"しか取れない…?
            dateCreated: parseDate(null, new Date(itm.DateCreated)),
            dateModified: parseDate(null, new Date(itm.DateLastModified)) };
        } else {
          if (excludesSymlink && fs.statSync(itm.Path).isSymbolicLink()) {
            continue;
          }
          fileInfo = fullName;
        }

        files.push(fileInfo);
      } catch (e) {
        if (!ignoresErr) {
          throw new Error(insp(e) + '\n'
            + '  at ' + FN + ' (' + MODULE_TITLE + ')');
        } else {
          continue;
        }
      }
    }

    return files;
  }; // }}}

  // fs.getChildrenDirectories {{{
  /**
   * Gets directories info in the specified directory.
   *
   * @function getChildrenDirectories
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - A directory path to read.
   * @param {typeGetChildrenFilesOptions} [options] - Optional parameters.
   * @returns {(string[]|Object[])}
   */
  fs.getChildrenDirectories = function (dirPath, options) {
    var FN = 'fs.getChildrenDirectories';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);
    if (!fs.statSync(dirPath).isDirectory()) throwErrNonExist(FN, dirPath);

    var prefixDirName = obtain(options, 'prefixDirName', '');

    var matchedRegExp = obtain(options, 'matchedRegExp', null);
    var mtchRE = isSolidString(matchedRegExp) ? new RegExp(matchedRegExp, 'i') : null;

    var ignoredRegExp = obtain(options, 'ignoredRegExp', null);
    var ignrRE = isSolidString(ignoredRegExp) ? new RegExp(ignoredRegExp, 'i') : null;

    var ignoresErr = obtain(options, 'ignoresErr', false);
    var excludesSymlink = obtain(options, 'excludesSymlink', false);
    var withFileTypes = obtain(options, 'withFileTypes', false);

    var objDir = fso.GetFolder(path.normalize(dirPath));
    var enmDir = new Enumerator(objDir.SubFolders);
    var dirs = [];
    var itm, dirName, fullName, isSymlink, fileInfo;

    // @NOTE なるべくstatSyncとisSymbokickLinkをかまさないことで処理を軽くする
    for (enmDir.moveFirst(); !enmDir.atEnd(); enmDir.moveNext()) {
      itm = enmDir.item();
      dirName = itm.Name.toString();
      fullName = path.join(prefixDirName, dirName);

      if (mtchRE && !mtchRE.test(fullName)) continue;
      if (ignrRE && ignrRE.test(fullName)) continue;

      try {
        if (withFileTypes) {
          isSymlink = fs.statSync(itm.Path).isSymbolicLink();
          if (excludesSymlink && isSymlink) continue;

          fileInfo = {
            name: fullName,
            path: itm.Path,
            attributes: itm.Attributes,
            isDirectory: true,
            isFile: false,
            isSymbolicLink: isSymlink,
            dateCreated: parseDate(null, new Date(itm.DateCreated)),
            dateModified: parseDate(null, new Date(itm.DateLastModified)) };
        } else {
          if (excludesSymlink && fs.statSync(itm.Path).isSymbolicLink()) {
            continue;
          }
          fileInfo = fullName;
        }

        dirs.push(fileInfo);
      } catch (e) {
        if (!ignoresErr) {
          throw new Error(insp(e) + '\n'
            + '  at ' + FN + ' (' + MODULE_TITLE + ')');
        } else {
          continue;
        }
      }
    }

    return dirs;
  }; // }}}

  // fs.readdirSync {{{
  /**
   * @typedef {typeGetChildrenFilesOptions} typeReaddirSyncOptions
   * @property {boolean} [isOnlyFile=false]
   * @property {boolean} [isOnlyDir=false]
   */

  /**
   * Reads the contents of a directory. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * var testDir = 'D:\\testDir';
   * //  D:\testDir\
   * //  │  fileRoot1.txt
   * //  │  fileRoot2-Symlink.log // <SYMLINKD>
   * //  │  fileRoot2.log
   * //  │
   * //  ├─DirBar\
   * //  ├─DirBar-Symlink\ // <SYMLINKD>
   * //  └─DirFoo\
   * //          fileFoo1.txt
   *
   * // Ex.1 No options
   * fs.readdirSync(testDir);
   * // Returns: [
   * //   'fileRoot1.txt',
   * //   'fileRoot2-Symlink.log', // <SYMLINKD>
   * //   'fileRoot2.log',
   * //   'DirBar',
   * //   'DirBar-Symlink', // <SYMLINKD>
   * //   'DirFoo' ]
   *
   * // Ex.2 Files only
   * fs.readdirSync(testDir, { isOnlyFile: true });
   * // Returns: [
   * //   'fileRoot1.txt',
   * //   'fileRoot2-Symlink.log', // <SYMLINKD>
   * //   'fileRoot2.log' ]
   *
   * // Ex.3 Directories only
   * fs.readdirSync(testDir, { isOnlyDir: true });
   * // Returns: [
   * //   'DirBar',
   * //   'DirBar-Symlink', // <SYMLINKD>
   * //   'DirFoo' ]
   *
   * // Ex.4 Excludes Symlink
   * fs.readdirSync(testDir, { excludesSymlink: true });
   * // Returns: [
   * //   'fileRoot1.txt',
   * //   'fileRoot2.log',
   * //   'DirBar',
   * //   'DirFoo' ]
   *
   * // Ex.5 Filtering
   * fs.readdirSync(testDir, { matchedRegExp: '\\d+\\.txt$' });
   * // Returns: ['fileRoot1.txt']
   *
   * // Ex.6 withFileTypes
   * fs.readdirSync(testDir, { withFileTypes: true });
   * // Returns: [
   * //   { name: 'fileRoot1.txt',
   * //     path: 'D:\\testDir\\fileRoot1.txt',
   * //     attributes: 32,
   * //     isDirectory: false,
   * //     isFile: true,
   * //     isSymbolicLink: false },
   * //   ...
   * //   ..
   * //   { name: 'DirFoo.txt',
   * //     path: 'D:\\testDir\\DirFoo',
   * //     attributes: 16,
   * //     isDirectory: true,
   * //     isFile: false,
   * //     isSymbolicLink: false }]
   * @function readdirSync
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - The directory path to read.
   * @param {typeReaddirSyncOptions} [options] - Optional parameters.
   * @returns {(string[]|Object[])}
   */
  fs.readdirSync = function (dirPath, options) {
    var FN = 'fs.readdirSync';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);
    if (!fs.statSync(dirPath).isDirectory()) throwErrNonExist(FN, dirPath);

    var isOnlyFile = obtain(options, 'isOnlyFile', false);
    var isOnlyDir = obtain(options, 'isOnlyDir', false);

    // File
    var files = [];
    if (!isOnlyDir) files = fs.getChildrenFiles(dirPath, options);

    // Directory
    var dirs = [];
    if (!isOnlyFile) dirs = fs.getChildrenDirectories(dirPath, options);

    return files.concat(dirs);
  }; // }}}

  // fs.excludeSymboliclinkPaths {{{
  /**
   * Excludes Symbolic-link paths from the list of file-path.
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   * var list = [
   *   'D:\\My Dir\\memo.txt',
   *   'D:\\MyBook-SymLink.xlsx', // SymbolicLink
   *   'D:\\image.png' ];
   *
   * var files = fs.excludeSymboliclinkPaths(list);
   * // Returns: [
   * //   'D:\\My Dir\\memo.txt',
   * //   'D:\\image.png' ];
   * @function excludeSymboliclinkPaths
   * @memberof Wsh.FileSystem
   * @param {string[]} filePaths - The list of file paths to check.
   * @param {object} [options] - Optional parameters.
   * @param {boolean} [options.ignoresErr=false] - Even if an error occurs during processing, it will be ignored.
   * @returns {string[]} - The list of file paths that excluded symlinks.
   */
  fs.excludeSymboliclinkPaths = function (filePaths, options) {
    var FN = 'fs.excludeSymboliclinkPaths';
    if (!isArray(filePaths)) throwErrNonArray(FN, filePaths);

    var ignoresErr = obtain(options, 'ignoresErr', false);
    var filteredDirPaths;
    var symlinkDirs = [];

    filteredDirPaths = filePaths.filter(function (fp) {
      for (var i = 0, len = symlinkDirs.length; i < len; i++) {
        if (startsWith(fp, symlinkDirs[i], 0, 'i')) return false;
      }

      try {
        var fstat = fs.statSync(fp);
        if (fstat.isSymbolicLink()) {
          if (fstat.isDirectory()) symlinkDirs.push(fp);
          return false;
        }
      } catch (e) {
        if (!ignoresErr) {
          throw new Error(insp(e) + '\n'
            + '  at ' + FN + ' (' + MODULE_TITLE + ')');
        }

        return false;
      }

      return true;
    });

    return filteredDirPaths;
  }; // }}}

  // fs.getAllChildrensFullPaths {{{
  /**
   * [Experimental] Creates a list of files in the directory with using `dir` instead of `fso.GetFolder`.
   *
   * @function getAllChildrensFullPaths
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - The directory path to list.
   * @param {object} [options] - Optional parameters.
   * @param {boolean} [options.ignoresErr=false] - Even if an error occurs during processing, it will be ignored.
   * @param {boolean} [options.isOnlyDir=false]
   * @param {boolean} [options.excludesSymlink=false]
   * @returns {string[]} - The list of full file paths.
   */
  fs.getAllChildrensFullPaths = function (dirPath, options) {
    var FN = 'fs.getAllChildrensFullPaths';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);
    if (!fs.statSync(dirPath).isDirectory()) throwErrNonExist(FN, dirPath);

    var mainCmd = 'dir';
    var args = [path.normalize(dirPath)];

    if (obtain(options, 'isOnlyDir', false)) {
      // List directory only
      args = args.concat(['/A:D', '/B', '/N', '/S', '/O:N']);
    } else {
      args = args.concat(['/B', '/N', '/S', '/O:N']);
    }

    var retObj = os.shExecSync(mainCmd, args, { shell: true });
    if (retObj.exitCode !== CD.runs.ok) {
      throw new Error('Error: [Error Exit Code]\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  mainCmd: ' + mainCmd + '\n  args: ' + insp(args) + '\n'
        + '  exitCode: ' + retObj.exitCode + '\n'
        + '  stdout: ' + retObj.stdout + '\n'
        + '  stderr: ' + retObj.stderr);
    }

    var fullPaths = retObj.stdout.split('\r\n');
    fullPaths.pop(); // Remove the last empty line

    if (obtain(options, 'excludesSymlink', false)) {
      return fullPaths;
    } else {
      // Exclude symboliclink paths (@FIXME Too slow)
      return fs.excludeSymboliclinkPaths(fullPaths, options);
    }
  }; // }}}

  // Update

  // fs.copyFileSync {{{
  /**
   * Synchronously copies src to dest. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_copyfilesync_src_dest_flags|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.copyFileSync('D:\\SrcFile.path', 'R:\\DestFile.path');
   *
   * // The following does not work.
   * // Ex.1 Directory
   * fs.copyFileSync('D:\\SrcDir', 'R:\\DestDir');
   * // Ex.2 Non-existing parent directory
   * fse.copySync('D:\\SrcFile.path', 'R:\\NonExistingDir\\DestFile.path');
   * @function copyFileSync
   * @memberof Wsh.FileSystem
   * @param {string} src - The source file path.
   * @param {string} dest - The destination file path.
   * @param {Wsh.FileSystem.constants.COPYFILE_EXCL} [flag]
   * @returns {void}
   */
  fs.copyFileSync = function (src, dest, flag) {
    var FN = 'fs.copyFileSync';
    if (!isString(src)) throwErrNonStr(FN, src);
    if (!isString(dest)) throwErrNonStr(FN, dest);
    if (!fs.existsSync(src)) throwErrNonExist(FN, src);

    if (flag === fs.constants.COPYFILE_EXCL && fs.existsSync(dest)) {
      throw new Error('Error: [EXIST]: file already exists\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  copyfile "' + src + '" -> "' + dest + '"');
    }

    try {
      fso.CopyFile(src, dest, CD.fso.overwrites.yes);
    } catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  EPERM: operation not permitted, copyfile "' + src + '" -> "' + dest + '")');
    }
  }; // }}}

  // fs.linkSync {{{
  /**
   * [Requires admin rights] Creates a new link (also known as Symbolic Link) to an existing file. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_linksync_existingpath_newpath|Node.js-Path}
   *
   * @example
   * // Run this script as admin
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.linkSync('D:\\MyDir\\BackUp', 'C:\\BackUp-Symlink'); // Requires admin
   * @function linkSync
   * @memberof Wsh.FileSystem
   * @param {string} existingPath
   * @param {string} newPath
   * @param {object} [options] - Optional parameters.
   * @param {number} [options.msecTimeOut=10000] - default: 10sec
   * @param {boolean} [options.isDryRun=false] - No execute, returns the string of command.
   * @returns {boolean|string} - If isDryRun is true, returns string.
   */
  fs.linkSync = function (existingPath, newPath, options) {
    var FN = 'fs.linkSync';
    if (!isString(newPath)) throwErrNonStr(FN, newPath);
    if (!isString(existingPath)) throwErrNonStr(FN, existingPath);

    if (fs.existsSync(newPath)) {
      throw new Error('Error: [EXIST]: file of directory already exists\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  newPath "' + newPath + '"');
    }
    if (!fs.existsSync(existingPath)) throwErrNonExist(FN, existingPath);

    var mainCmd = 'mklink';
    var args = [];

    var statSrc = fs.statSync(existingPath);
    if (statSrc.isFile()) {
      args = args.concat([newPath, existingPath]);
    } else if (statSrc.isDirectory()) {
      args = args.concat(['/D', newPath, existingPath]);
    } else {
      throw new Error('Error: [Unknwon file type]:\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  existingPath "' + existingPath + '"');
    }

    var isDryRun = obtain(options, 'isDryRun', false);

    var retVal = os.runAsAdmin(mainCmd, args, {
      shell: true,
      winStyle: 'hidden',
      isDryRun: isDryRun
    });
    if (isDryRun) return 'dry-run [' + FN + ']: ' + retVal;

    /*
     * @note Cannot catch an error and a termination, if current process is not running as administrator. Therefore, wait 10 sec for existing the link.
     */

    var msecTimeOut = obtain(options, 'msecTimeOut', 10000);
    do {
      try {
        if (fs.existsSync(newPath)) return true;
      } catch (e) {
        WScript.Sleep(100);
        msecTimeOut -= 100;
      }
    } while (msecTimeOut > 0);

    return false;
  }; // }}}

  // fs.xcopySync {{{
  /**
   * Copies file with {@link https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/xcopy|xcopy}.
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.xcopySync('D:\\SrcFile.path', 'R:\\DestFile.path');
   * fs.xcopySync('D:\\SrcDir', 'R:\\DestDir');
   *
   * var stdObj = fs.xcopySync('D:\\src.js', 'R:\\dest.js', { withStd: true });
   * // Returns: {
   * //   error: false,
   * //   exitCode: 0,
   * //   stdout: '<String of StdOut>',
   * //   stderr: '<String of StdErr>' }
   * @function xcopySync
   * @memberof Wsh.FileSystem
   * @param {string} src - The source file/directory path.
   * @param {string} dest - The destination file/directory path.
   * @param {object} [options] - Optional parameters.
   * @param {boolean} [options.withStd=false]
   * @param {boolean} [options.isDryRun=false] - No execute, returns the string of command.
   * @returns {void|typeExecSyncReturn|string} - If withStd is true returns {@link https://tuckn.net/docs/WshUtil/Wsh.Constants.windowStyles.html|Wsh.OS.typeExecSyncReturn} or if isDryRun is true, returns string.
   */
  fs.xcopySync = function (src, dest, options) {
    var FN = 'fs.xcopySync';
    if (!isString(src)) throwErrNonStr(FN, src);
    if (!isString(dest)) throwErrNonStr(FN, dest);
    if (!fs.existsSync(src)) throwErrNonExist(FN, src);

    var mainCmd = 'ECHO';

    var argStr = '';
    if (fs.statSync(src).isDirectory()) {
      argStr += ' D|' + XCOPY + ' ' + srrd(src) + ' ' + srrd(dest) + ' /E /I';
    } else {
      argStr += ' F|' + XCOPY + ' ' + srrd(src) + ' ' + srrd(dest);
    }

    argStr += ' /H /R /Y';

    // // debug
    // console.log(os.convToCmdlineStr(mainCmd, argStr, { shell: true }));

    var withStd = obtain(options, 'withStd', false);
    var isDryRun = obtain(options, 'isDryRun', false);
    var retVal;

    if (!withStd) {
      retVal = os.shRunSync(mainCmd, argStr, {
        shell: true,
        winStyle: 'hidden',
        isDryRun: isDryRun
      });

      if (isDryRun) return 'dry-run [' + FN + ']: ' + retVal;
      if (retVal === CD.runs.ok) return;

      throw new Error('Error [ExitCode is not Ok] "' + retVal + '"\n');
    } else {
      retVal = os.shExecSync(mainCmd, argStr, {
        shell: true,
        isDryRun: isDryRun
      });

      if (isDryRun) return 'dry-run [' + FN + ']: ' + retVal;
      if (retVal.exitCode === CD.runs.ok) return retVal;

      throw new Error('Error: [Error Exit Code]\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  mainCmd: ' + mainCmd + '\n  argStr: ' + argStr + '\n'
        + '  exitCode: ' + retVal.exitCode + '\n'
        + '  stdout: ' + retVal.stdout + '\n'
        + '  stderr: ' + retVal.stderr);
    }
  }; // }}}

  // Delete

  // fs.rmdirSync {{{
  /**
   * Removes the directory. Can not remove a file. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_rmdirsync_path|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.rmdirSync('D:\\MyDir');
   *
   * // The following does not work.
   * // Ex.1 File
   * fs.rmdirSync('D:\\MyFile.path');
   * // Ex.2 A non-existing directory
   * fs.mkdirSync('R:\\NonExistingDir');
   * @function rmdirSync
   * @memberof Wsh.FileSystem
   * @param {string} dirPath - The directory path to delete.
   * @returns {void}
   */
  fs.rmdirSync = function (dirPath) {
    var FN = 'fs.rmdirSync';
    if (!isString(dirPath)) throwErrNonStr(FN, dirPath);
    if (!fs.statSync(dirPath).isDirectory()) throwErrNonExist(FN, dirPath);

    try {
      fso.DeleteFolder(dirPath, CD.fso.force.yes);
    } catch (e) {
      /**
       * fso.DeleteFolder can not delete the directory including symlinks? On the other hand, `rmdir` can delete a directory including symlinks, If its name has the last backslash.
       * [ATTENTION] But! If it is the symlink directory, `rmdir` will even delete the files from which the link originated
       */
      try {
        if (!endsWith(dirPath, path.sep)) dirPath += path.sep;

        var retVal = os.shRunSync('rmdir', ['/S', '/Q', dirPath], {
          shell: true,
          winStyle: 'hidden'
        });

        if (retVal === CD.runs.ok) return;

        throw new Error('Error [ExitCode is not Ok] "' + retVal + '"\n');
      } catch (e) {
        throw new Error(insp(e) + '\n'
          + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
          + '  EPERM: operation not permitted, "' + dirPath + '"');
      }
    }
  }; // }}}

  // fs.unlinkSync {{{
  /**
   * Removes the file with {@link https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/deletefile-method|DeleteFile method}. Can not remove a directory. Similar to {@link https://nodejs.org/api/fs.html#fs_fs_unlinksync_path|Node.js-Path}
   *
   * @example
   * var fs = Wsh.FileSystem; // Shorthand
   *
   * fs.unlinkSync('D:\\MyFile.path');
   * fs.unlinkSync('D:\\MyDir'); // Throws an Error
   * @function unlinkSync
   * @memberof Wsh.FileSystem
   * @param {string} fpath - The file-path to remove.
   * @returns {void}
   */
  fs.unlinkSync = function (fpath) {
    var FN = 'fs.unlinkSync';
    if (!isString(fpath)) throwErrNonStr(FN, fpath);
    if (!fs.statSync(fpath).isFile()) throwErrNonExist(FN, fpath);

    try {
      fso.DeleteFile(fpath, CD.fso.force.yes);
    } catch (e) {
      throw new Error(insp(e) + '\n'
        + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
        + '  EPERM: operation not permitted, unlink "' + fpath + '"');
    }
  }; // }}}
})();

// vim:set foldmethod=marker commentstring=//%s :