diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index 6c1134332aa..bee784a4b79 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1131,7 +1131,7 @@ private static CommandInfo TryModuleAutoDiscovery(string commandName, // Get the available module files, preferring modules from $PSHOME so that user modules don't // override system modules during auto-loading if (etwEnabled) CommandDiscoveryEventSource.Log.SearchingForModuleFilesStart(); - var defaultAvailableModuleFiles = ModuleUtils.GetDefaultAvailableModuleFiles(true, true, context); + var defaultAvailableModuleFiles = ModuleUtils.GetDefaultAvailableModuleFiles(isForAutoDiscovery: true, context); if (etwEnabled) CommandDiscoveryEventSource.Log.SearchingForModuleFilesStop(); foreach (string modulePath in defaultAvailableModuleFiles) diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index 9e0ad430a7e..320961e1674 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -2512,7 +2512,7 @@ private string[] GetModulesForUnResolvedCommands(IEnumerable unresolvedC foreach (var unresolvedCommand in commandsToResolve) { // Use the analysis cache to find the first module containing the unresolved command. - foreach (string modulePath in ModuleUtils.GetDefaultAvailableModuleFiles(true, true, context)) + foreach (string modulePath in ModuleUtils.GetDefaultAvailableModuleFiles(isForAutoDiscovery: true, context)) { string expandedModulePath = IO.Path.GetFullPath(modulePath); var exportedCommands = AnalysisCache.GetExportedCommands(expandedModulePath, false, context); diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index f9488ffbc6b..9d019f95b0d 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -869,37 +869,19 @@ internal List GetModule(string[] names, bool all, bool refresh) moduleNames.Add(n); } } - modulesToReturn.AddRange(GetModuleForRootedPaths(modulePaths.ToArray(), all, refresh)); + modulesToReturn.AddRange(GetModuleForRootedPaths(modulePaths, all, refresh)); } // If no names were passed to this function, then this API will return list of all available modules if (names == null || moduleNames.Count > 0) { - modulesToReturn.AddRange(GetModuleForNonRootedPaths(moduleNames.ToArray(), all, refresh)); + modulesToReturn.AddRange(GetModuleForNames(moduleNames, all, refresh)); } return modulesToReturn; } - private IEnumerable GetModuleForNonRootedPaths(string[] names, bool all, bool refresh) - { - const WildcardOptions wildcardOptions = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; - IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings(names, wildcardOptions); - - Dictionary> availableModules = GetAvailableLocallyModulesCore(names, all, refresh); - foreach (var entry in availableModules) - { - foreach (PSModuleInfo module in entry.Value) - { - if (SessionStateUtilities.MatchesAnyWildcardPattern(module.Name, patternList, true)) - { - yield return module; - } - } - } - } - - private IEnumerable GetModuleForRootedPaths(string[] modulePaths, bool all, bool refresh) + private IEnumerable GetModuleForRootedPaths(List modulePaths, bool all, bool refresh) { // This is to filter out duplicate modules var modules = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -950,7 +932,7 @@ private IEnumerable GetModuleForRootedPaths(string[] modulePaths, var availableModuleFiles = all ? ModuleUtils.GetAllAvailableModuleFiles(resolvedModulePath) - : ModuleUtils.GetModuleVersionsFromAbsolutePath(resolvedModulePath); + : ModuleUtils.GetModuleFilesFromAbsolutePath(resolvedModulePath); bool foundModule = false; foreach (string file in availableModuleFiles) @@ -998,38 +980,31 @@ private ErrorRecord CreateModuleNotFoundError(string modulePath) return er; } - private Dictionary> GetAvailableLocallyModulesCore(string[] names, bool all, bool refresh) + private IEnumerable GetModuleForNames(List names, bool all, bool refresh) { - var modules = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var modulePaths = ModuleIntrinsics.GetModulePath(false, Context); - + IEnumerable allModules = null; + HashSet modulePathSet = new HashSet(StringComparer.OrdinalIgnoreCase); bool cleanupModuleAnalysisAppDomain = Context.TakeResponsibilityForModuleAnalysisAppDomain(); try { foreach (string path in ModuleIntrinsics.GetModulePath(false, Context)) { - try - { - var availableModules = all - ? GetAllAvailableModules(path, refresh) - : GetDefaultAvailableModules(names, path, refresh); + string uniquePath = path.TrimEnd(Utils.Separators.Directory); - // Add the path in $env:PSModulePath as the keys of the dictionary - // If the paths are repeated, ignore the repetitions - string uniquePath = path.TrimEnd(Utils.Separators.Directory); - if (!modules.ContainsKey(uniquePath)) - { - modules.Add(uniquePath, availableModules.OrderBy(m => m.Name).ToList()); - } - } - catch (IOException) + // Ignore repeated module path. + if (!modulePathSet.Add(uniquePath)) { continue; } + + try { - continue; // ignore directories that can't be accessed + IEnumerable modulesFound = GetModulesFromOneModulePath( + names, uniquePath, all, refresh).OrderBy(m => m.Name); + allModules = allModules == null ? modulesFound : allModules.Concat(modulesFound); } - catch (UnauthorizedAccessException) + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) { - continue; // ignore directories that can't be accessed + // ignore directories that can't be accessed + continue; } } } @@ -1041,64 +1016,43 @@ private Dictionary> GetAvailableLocallyModulesCore(st } } - return modules; + // Make sure we always return a non-null collection. + return allModules ?? Utils.EmptyArray(); } /// - /// Get a list of all modules - /// which can be imported just by specifying a non rooted file name of the module - /// (Import-Module foo\bar.psm1; but not Import-Module .\foo\bar.psm1) + /// Get modules based on the given names and module files. /// - private IEnumerable GetAllAvailableModules(string directory, bool refresh) + private IEnumerable GetModulesFromOneModulePath(List names, string modulePath, bool all, bool refresh) { - var availableModuleFiles = ModuleUtils.GetAllAvailableModuleFiles(directory); - - foreach (string file in availableModuleFiles) + const WildcardOptions options = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; + IEnumerable namePatterns = null; + if (names != null && names.Count > 0) { - PSModuleInfo module = CreateModuleInfoForGetModule(file, refresh); - if (module != null) - { - yield return module; - } + namePatterns = SessionStateUtilities.CreateWildcardsFromStrings(names, options); } - } - - /// - /// Get a list of the available modules - /// which can be imported just by specifying a non rooted directory name of the module - /// (Import-Module foo\bar; but not Import-Module .\foo\bar or Import-Module .\foo\bar.psm1) - /// - private List GetDefaultAvailableModules(string[] name, string directory, bool refresh) - { - List availableModules = new List(); - const WildcardOptions wildcardOptions = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; - IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings(name, wildcardOptions); + IEnumerable moduleFiles = all + ? ModuleUtils.GetAllAvailableModuleFiles(modulePath) + : ModuleUtils.GetDefaultAvailableModuleFiles(modulePath); - var availableModuleFiles = ModuleUtils.GetDefaultAvailableModuleFiles(directory); - - foreach (string file in availableModuleFiles) + foreach (string file in moduleFiles) { - string actualModuleName = System.IO.Path.GetFileNameWithoutExtension(file); - if (SessionStateUtilities.MatchesAnyWildcardPattern(actualModuleName, patternList, true)) + if (namePatterns == null || + SessionStateUtilities.MatchesAnyWildcardPattern( + Path.GetFileNameWithoutExtension(file), namePatterns, defaultValue: true)) { PSModuleInfo module = CreateModuleInfoForGetModule(file, refresh); + if (module == null) { continue; } - if (module != null) + if (all || !ModuleUtils.IsModuleInVersionSubdirectory(file, out Version directoryVersion) || directoryVersion == module.Version) { - Version directoryVersion; - if (!ModuleUtils.IsModuleInVersionSubdirectory(file, out directoryVersion) - || directoryVersion == module.Version) - { - availableModules.Add(module); - } + yield return module; } } } ClearAnalysisCaches(); - - return availableModules; } /// @@ -1191,10 +1145,10 @@ private PSModuleInfo CreateModuleInfoForGetModule(string file, bool refresh) moduleInfo = LoadModuleManifest( scriptInfo, flags /* - don't write errors, don't load elements */, - null, - null, - null, - null); + minimumVersion: null, + maximumVersion: null, + requiredVersion: null, + requiredModuleGuid: null); } else { @@ -1202,13 +1156,13 @@ private PSModuleInfo CreateModuleInfoForGetModule(string file, bool refresh) ImportModuleOptions options = new ImportModuleOptions(); bool found = false; - moduleInfo = LoadModule(file, null, String.Empty, null, ref options, flags, out found); + moduleInfo = LoadModule(file, moduleBase: null, prefix: String.Empty, ss: null, ref options, flags, out found); } // return fake PSModuleInfo if can't read the file for any reason if (moduleInfo == null) { - moduleInfo = new PSModuleInfo(file, null, null); + moduleInfo = new PSModuleInfo(file, context: null, sessionState: null); moduleInfo.HadErrorsLoading = true; // Prevent analysis cache from caching a bad module. } @@ -2677,9 +2631,13 @@ internal PSModuleInfo LoadModuleManifest( } } - var needToAnalyzeScriptModules = usedWildcard; + // We have to further analyze the module if any wildcard characters are used. + bool needToAnalyzeScriptModules = usedWildcard; - if (!needToAnalyzeScriptModules) + // We can skip further analysis if 'FunctionsToExport', 'CmdletsToExport' and 'AliasesToExport' + // are all declared and no wildcard character is used for them. But if any of 'FunctionsToExport', + // 'CmdletsToExport' or 'AliasesToExport' were not given, we must check to see if more analysis is needed. + if (!needToAnalyzeScriptModules && (!sawExportedCmdlets || !sawExportedFunctions || !sawExportedAliases)) { foreach (var nestedModule in nestedModules) { @@ -3173,9 +3131,9 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.DeclaredCmdletExports = manifestInfo.DeclaredCmdletExports; } - if (manifestInfo._detectedCmdletExports != null) + if (manifestInfo.DetectedCmdletExports != null) { - foreach (string detectedExport in manifestInfo._detectedCmdletExports) + foreach (string detectedExport in manifestInfo.DetectedCmdletExports) { newManifestInfo.AddDetectedCmdletExport(detectedExport); } @@ -3187,9 +3145,9 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.DeclaredFunctionExports = manifestInfo.DeclaredFunctionExports; } - if (manifestInfo._detectedFunctionExports != null) + if (manifestInfo.DetectedFunctionExports != null) { - foreach (string detectedExport in manifestInfo._detectedFunctionExports) + foreach (string detectedExport in manifestInfo.DetectedFunctionExports) { newManifestInfo.AddDetectedFunctionExport(detectedExport); } @@ -3200,9 +3158,9 @@ internal PSModuleInfo LoadModuleManifest( newManifestInfo.DeclaredAliasExports = manifestInfo.DeclaredAliasExports; } - if (manifestInfo._detectedAliasExports != null) + if (manifestInfo.DetectedAliasExports != null) { - foreach (var pair in manifestInfo._detectedAliasExports) + foreach (var pair in manifestInfo.DetectedAliasExports) { newManifestInfo.AddDetectedAliasExport(pair.Key, pair.Value); } @@ -3269,15 +3227,7 @@ internal PSModuleInfo LoadModuleManifest( } else { - Collection v = new Collection(); - if (manifestInfo.DeclaredFunctionExports != null) - { - foreach (var f in manifestInfo.DeclaredFunctionExports) - { - v.Add(f); - } - } - UpdateCommandCollection(v, exportedFunctions); + UpdateCommandCollection(manifestInfo.DeclaredFunctionExports, exportedFunctions); } } diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 93a97302560..8451daa80f1 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -690,9 +690,9 @@ private static string AddToPath(string basePath, string pathToAdd, int insertPos foreach (string subPathToAdd in pathToAdd.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) // in case pathToAdd is a 'combined path' (semicolon-separated) { int position = PathContainsSubstring(result.ToString(), subPathToAdd); // searching in effective 'result' value ensures that possible duplicates in pathsToAdd are handled correctly - if (-1 == position) // subPathToAdd not found - add it + if (position == -1) // subPathToAdd not found - add it { - if (-1 == insertPosition) // append subPathToAdd to the end + if (insertPosition == -1) // append subPathToAdd to the end { bool endsWithPathSeparator = false; if (result.Length > 0) endsWithPathSeparator = (result[result.Length - 1] == Path.PathSeparator); @@ -715,14 +715,12 @@ private static string AddToPath(string basePath, string pathToAdd, int insertPos /// /// Check if the current powershell is likely running in following scenarios: - /// - sxs ps started on windows [machine-wide env:PSModulePath will influence] - /// - sxs ps started from full ps - /// - sxs ps started from inbox nano/iot ps - /// - full ps started from sxs ps - /// - inbox nano/iot ps started from sxs ps + /// - PSCore started on windows [machine-wide env:PSModulePath will influence] + /// - PSCore started from full ps + /// - PSCore started from inbox nano/iot ps /// If it's likely one of them, then we need to clear the current process module path. /// - private static bool NeedToClearProcessModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath, bool runningSxS) + private static bool NeedToClearProcessModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath) { #if UNIX return false; @@ -733,30 +731,19 @@ private static bool NeedToClearProcessModulePath(string currentProcessModulePath const string winSxSModuleDirectory = @"PowerShell\Modules"; const string winLegacyModuleDirectory = @"WindowsPowerShell\Modules"; - if (runningSxS) - { - // The machine-wide and user-wide environment variables are only meaningful for full ps, - // so if the current process module path contains any of them, it's likely that the sxs - // ps was started directly on windows, or from full ps. The same goes for the legacy personal - // and shared module paths. - string hklmModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine); - string hkcuModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User); - string legacyPersonalModulePath = personalModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory); - string legacyProgramFilesModulePath = sharedModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory); - - return (!string.IsNullOrEmpty(hklmModulePath) && currentProcessModulePath.IndexOf(hklmModulePath, StringComparison.OrdinalIgnoreCase) != -1) || - (!string.IsNullOrEmpty(hkcuModulePath) && currentProcessModulePath.IndexOf(hkcuModulePath, StringComparison.OrdinalIgnoreCase) != -1) || - currentProcessModulePath.IndexOf(legacyPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 || - currentProcessModulePath.IndexOf(legacyProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1; - } - - // The sxs personal and shared module paths are only meaningful for sxs ps, so if they appear - // in the current process module path, it's likely the running ps was started from a sxs ps. - string sxsPersonalModulePath = personalModulePath.Replace(winLegacyModuleDirectory, winSxSModuleDirectory); - string sxsProgramFilesModulePath = sharedModulePath.Replace(winLegacyModuleDirectory, winSxSModuleDirectory); - - return currentProcessModulePath.IndexOf(sxsPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 || - currentProcessModulePath.IndexOf(sxsProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1; + // The machine-wide and user-wide environment variables are only meaningful for full ps, + // so if the current process module path contains any of them, it's likely that the sxs + // ps was started directly on windows, or from full ps. The same goes for the legacy personal + // and shared module paths. + string hklmModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine); + string hkcuModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User); + string legacyPersonalModulePath = personalModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory); + string legacyProgramFilesModulePath = sharedModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory); + + return (!string.IsNullOrEmpty(hklmModulePath) && currentProcessModulePath.IndexOf(hklmModulePath, StringComparison.OrdinalIgnoreCase) != -1) || + (!string.IsNullOrEmpty(hkcuModulePath) && currentProcessModulePath.IndexOf(hkcuModulePath, StringComparison.OrdinalIgnoreCase) != -1) || + currentProcessModulePath.IndexOf(legacyPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 || + currentProcessModulePath.IndexOf(legacyProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1; #endif } @@ -821,17 +808,14 @@ public static string GetModulePath(string currentProcessModulePath, string hklmM string personalModulePath = GetPersonalModulePath(); string sharedModulePath = GetSharedModulePath(); // aka location string psHomeModulePath = GetPSHomeModulePath(); // $PSHome\Modules location - bool runningSxS = Platform.IsInbox ? false : true; if (!string.IsNullOrEmpty(currentProcessModulePath) && - NeedToClearProcessModulePath(currentProcessModulePath, personalModulePath, sharedModulePath, runningSxS)) + NeedToClearProcessModulePath(currentProcessModulePath, personalModulePath, sharedModulePath)) { // Clear the current process module path in the following cases - // - start sxs ps on windows [machine-wide env:PSModulePath will influence] - // - start sxs ps from full ps - // - start sxs ps from inbox nano/iot ps - // - start full ps from sxs ps - // - start inbox nano/iot ps from sxs ps + // - start PSCore on windows [machine-wide env:PSModulePath will influence] + // - start PSCore from full ps + // - start PSCore from inbox nano/iot ps currentProcessModulePath = null; } @@ -860,7 +844,7 @@ public static string GetModulePath(string currentProcessModulePath, string hklmM } // EVT.Process exists // Now handle the case where the environment variable is already set. - else if (runningSxS) // The running powershell is an SxS PS instance + else { // When SxS PS instance A starts SxS PS instance B, A's PSHome module path might be inherited by B. We need to remove that path from B currentProcessModulePath = RemoveSxSPsHomeModulePath(currentProcessModulePath, personalModulePath, sharedModulePath, psHomeModulePath); @@ -871,87 +855,12 @@ public static string GetModulePath(string currentProcessModulePath, string hklmM currentProcessModulePath = AddToPath(currentProcessModulePath, personalModulePathToUse, 0); currentProcessModulePath = AddToPath(currentProcessModulePath, systemModulePathToUse, -1); } - else // The running powershell is Full PS or inbox Core PS - { - // If there is no personal path key, then if the env variable doesn't match the system variable, - // the user modified it somewhere, else prepend the default personal module path - if (hklmMachineModulePath != null) // EVT.Machine exists - { - if (hkcuUserModulePath == null) // EVT.User does Not exist - { - if (!(hklmMachineModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase)) - { - // before returning, use heuristic to conditionally add location - int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath - if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert location before $PSHome\Modules - { - return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition); - } // if $PSHome\Modules NOT found = = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules' - - return null; - } - currentProcessModulePath = personalModulePath + Path.PathSeparator + hklmMachineModulePath; // + EVT.Machine + inserted later in this function - } - else // EVT.User exists - { - // PSModulePath is designed to have behaviour like 'Path' var in a sense that EVT.User + EVT.Machine are merged to get final value of PSModulePath - string combined = string.Concat(hkcuUserModulePath, Path.PathSeparator, hklmMachineModulePath); // EVT.User + EVT.Machine - if (!((combined).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase) || - (hklmMachineModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase) || - (hkcuUserModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase))) - { - // before returning, use heuristic to conditionally add location - int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath - if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert location before $PSHome\Modules - { - return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition); - } // if $PSHome\Modules NOT found = = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules' - - return null; - } - currentProcessModulePath = combined; // = EVT.User + EVT.Machine + inserted later in this function - } - } - else // EVT.Machine does Not exist - { - // If there is no system path key, then if the env variable doesn't match the user variable, - // the user modified it somewhere, otherwise append the default system path - if (hkcuUserModulePath != null) // EVT.User exists - { - if (hkcuUserModulePath.Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase)) - { - currentProcessModulePath = hkcuUserModulePath + Path.PathSeparator + CombineSystemModulePaths(); // = EVT.User + (SharedModulePath + $PSHome\Modules) - } - else - { - // before returning, use heuristic to conditionally add location - int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath - if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert location before $PSHome\Modules - { - return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition); - } // if $PSHome\Modules NOT found = = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules' - - return null; - } - } - else // EVT.User does Not exist - { - // before returning, use heuristic to conditionally add location - int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath - if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert location before $PSHome\Modules - { - return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition); - } // if $PSHome\Modules NOT found = = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules' - - // Neither key is set so go with what the environment variable is already set to - return null; - } - } - } // if we reached this point - always add location to EVT.Process // everything below is the same behaviour as WMF 4 code - int indexOfPSHomeModulePath = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath + + // index of $PSHome\Modules in currentProcessModulePath + int indexOfPSHomeModulePath = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // if $PSHome\Modules not found (psHomePosition == -1) - append location to the end; // if $PSHome\Modules IS found (psHomePosition >= 0) - insert location before $PSHome\Modules currentProcessModulePath = AddToPath(currentProcessModulePath, sharedModulePath, indexOfPSHomeModulePath); @@ -969,12 +878,13 @@ internal static string GetModulePath() string currentModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Process); return currentModulePath; } + /// /// Checks if $env:PSModulePath is not set and sets it as appropriate. Note - because these /// strings go through the provider, we need to escape any wildcards before passing them /// along. /// - internal static string SetModulePath() + private static string SetModulePath() { string currentModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Process); string systemWideModulePath = PowerShellConfig.Instance.GetModulePath(ConfigScope.SystemWide); diff --git a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs index ac76e426ea2..f4f859aafc2 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleUtils.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleUtils.cs @@ -11,10 +11,26 @@ namespace System.Management.Automation.Internal { internal static class ModuleUtils { + // Default option for local file system enumeration: + // - Ignore files/directories when access is denied; + // - Search top directory only. + private readonly static System.IO.EnumerationOptions s_defaultEnumerationOptions = + new System.IO.EnumerationOptions() { AttributesToSkip = 0 }; + + // Default option for UNC path enumeration. Same as above plus a large buffer size. + // For network shares, a large buffer may result in better performance as more results can be batched over the wire. + // The buffer size 16K is recommended in the comment of the 'BufferSize' property: + // "A "large" buffer, for example, would be 16K. Typical is 4K." + private readonly static System.IO.EnumerationOptions s_uncPathEnumerationOptions = + new System.IO.EnumerationOptions() { AttributesToSkip = 0, BufferSize = 16384 }; + + /// + /// Check if a directory could be a module folder. + /// internal static bool IsPossibleModuleDirectory(string dir) { // We shouldn't be searching in hidden directories. - var attributes = File.GetAttributes(dir); + FileAttributes attributes = File.GetAttributes(dir); if (0 != (attributes & FileAttributes.Hidden)) { return false; @@ -27,7 +43,6 @@ internal static bool IsPossibleModuleDirectory(string dir) return false; } -#if !CORECLR dir = Path.GetFileName(dir); // Use some simple pattern matching to avoid the call into GetCultureInfo when we know it will fail (and throw). if ((dir.Length == 2 && char.IsLetter(dir[0]) && char.IsLetter(dir[1])) @@ -43,32 +58,29 @@ internal static bool IsPossibleModuleDirectory(string dir) } catch { } } -#endif return true; } /// - /// Get a list of all module files - /// which can be imported just by specifying a non rooted file name of the module - /// (Import-Module foo\bar.psm1; but not Import-Module .\foo\bar.psm1) + /// Get all module files by searching the given directory recursively. + /// All sub-directories that could be a module folder will be searched. /// - /// When obtaining all module files we return all possible - /// combinations for a given file. For example, for foo we return both - /// foo.psd1 and foo.psm1 if found. Get-Module will create the module - /// info only for the first one internal static IEnumerable GetAllAvailableModuleFiles(string topDirectoryToCheck) { + if (!Directory.Exists(topDirectoryToCheck)) { yield break; } + + var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; Queue directoriesToCheck = new Queue(); directoriesToCheck.Enqueue(topDirectoryToCheck); while (directoriesToCheck.Count > 0) { - var directoryToCheck = directoriesToCheck.Dequeue(); + string directoryToCheck = directoriesToCheck.Dequeue(); try { - var subDirectories = Directory.GetDirectories(directoryToCheck, "*", SearchOption.TopDirectoryOnly); - foreach (var toAdd in subDirectories) + string[] subDirectories = Directory.GetDirectories(directoryToCheck, "*", options); + foreach (string toAdd in subDirectories) { if (IsPossibleModuleDirectory(toAdd)) { @@ -79,7 +91,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto catch (IOException) { } catch (UnauthorizedAccessException) { } - var files = Directory.GetFiles(directoryToCheck, "*", SearchOption.TopDirectoryOnly); + string[] files = Directory.GetFiles(directoryToCheck, "*", options); foreach (string moduleFile in files) { foreach (string ext in ModuleIntrinsics.PSModuleExtensions) @@ -94,7 +106,7 @@ internal static IEnumerable GetAllAvailableModuleFiles(string topDirecto } } - internal static IEnumerable GetDefaultAvailableModuleFiles(bool force, bool isForAutoDiscovery, ExecutionContext context) + internal static IEnumerable GetDefaultAvailableModuleFiles(bool isForAutoDiscovery, ExecutionContext context) { HashSet uniqueModuleFiles = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -146,59 +158,74 @@ internal static IEnumerable GetDefaultAvailableModuleFiles(bool force, b } } - internal static List GetModuleVersionsFromAbsolutePath(string directory) + /// + /// Get a list of module files from the given directory without recursively searching all sub-directories. + /// This method assumes the given directory is a module folder or a version sub-directory of a module folder. + /// + internal static List GetModuleFilesFromAbsolutePath(string directory) { List result = new List(); string fileName = Path.GetFileName(directory); - Version moduleVersion; - // if the user give the module path including version, we should be able to find the module as well - if (Version.TryParse(fileName, out moduleVersion) && Directory.Exists(Directory.GetParent(directory).ToString())) - { - fileName = Directory.GetParent(directory).Name; - } - foreach (var version in GetModuleVersionSubfolders(directory)) + + // If the given directory doesn't exist or it's the root folder, then return an empty list. + if (!Directory.Exists(directory) || string.IsNullOrEmpty(fileName)) { return result; } + + // If the user give the module path including version, the module name could be the parent folder name. + if (Version.TryParse(fileName, out Version ver)) { - var qualifiedPathWithVersion = Path.Combine(directory, Path.Combine(version.ToString(), fileName)); - string manifestPath = qualifiedPathWithVersion + StringLiterals.PowerShellDataFileExtension; - if (File.Exists(manifestPath)) - { - bool isValidModuleVersion = version.Equals(ModuleIntrinsics.GetManifestModuleVersion(manifestPath)); + string parentDirPath = Path.GetDirectoryName(directory); + string parentDirName = Path.GetFileName(parentDirPath); - if (isValidModuleVersion) + // If the parent directory is NOT a root folder, then it could be the module folder. + if (!string.IsNullOrEmpty(parentDirName)) + { + string manifestPath = Path.Combine(directory, parentDirName); + manifestPath += StringLiterals.PowerShellDataFileExtension; + if (File.Exists(manifestPath) && ver.Equals(ModuleIntrinsics.GetManifestModuleVersion(manifestPath))) { result.Add(manifestPath); + return result; } } } - foreach (string ext in ModuleIntrinsics.PSModuleExtensions) + // If we reach here, then use the given directory as the module folder. + foreach (Version version in GetModuleVersionSubfolders(directory)) { - string moduleFile = Path.Combine(directory, fileName) + ext; - - if (!Utils.FileExists(moduleFile)) + string manifestPath = Path.Combine(directory, version.ToString(), fileName); + manifestPath += StringLiterals.PowerShellDataFileExtension; + if (File.Exists(manifestPath) && version.Equals(ModuleIntrinsics.GetManifestModuleVersion(manifestPath))) { - continue; + result.Add(manifestPath); } + } - result.Add(moduleFile); + foreach (string ext in ModuleIntrinsics.PSModuleExtensions) + { + string moduleFile = Path.Combine(directory, fileName) + ext; + if (File.Exists(moduleFile)) + { + result.Add(moduleFile); - // when finding the default modules we stop when the first - // match is hit - searching in order .psd1, .psm1, .dll - // if a file is found but is not readable then it is an - // error - break; + // when finding the default modules we stop when the first + // match is hit - searching in order .psd1, .psm1, .dll, + // if a file is found but is not readable then it is an error. + break; + } } return result; } /// - /// Get a list of the available module files - /// which can be imported just by specifying a non rooted directory name of the module - /// (Import-Module foo\bar; but not Import-Module .\foo\bar or Import-Module .\foo\bar.psm1) + /// Get a list of the available module files from the given directory. + /// Search all module folders under the specified directory, but do not search sub-directories under a module folder. /// internal static IEnumerable GetDefaultAvailableModuleFiles(string topDirectoryToCheck) { + if (!Directory.Exists(topDirectoryToCheck)) { yield break; } + + var options = Utils.PathIsUnc(topDirectoryToCheck) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; List versionDirectories = new List(); LinkedList directoriesToCheck = new LinkedList(); directoriesToCheck.AddLast(topDirectoryToCheck); @@ -207,11 +234,11 @@ internal static IEnumerable GetDefaultAvailableModuleFiles(string topDir { versionDirectories.Clear(); string[] subdirectories; - var directoryToCheck = directoriesToCheck.First.Value; + string directoryToCheck = directoriesToCheck.First.Value; directoriesToCheck.RemoveFirst(); try { - subdirectories = Directory.GetDirectories(directoryToCheck, "*", SearchOption.TopDirectoryOnly); + subdirectories = Directory.GetDirectories(directoryToCheck, "*", options); ProcessPossibleVersionSubdirectories(subdirectories, versionDirectories); } catch (IOException) { subdirectories = Utils.EmptyArray(); } @@ -219,10 +246,10 @@ internal static IEnumerable GetDefaultAvailableModuleFiles(string topDir bool isModuleDirectory = false; string proposedModuleName = Path.GetFileName(directoryToCheck); - foreach (var version in versionDirectories) + foreach (Version version in versionDirectories) { - var qualifiedPathWithVersion = Path.Combine(directoryToCheck, Path.Combine(version.ToString(), proposedModuleName)); - string manifestPath = qualifiedPathWithVersion + StringLiterals.PowerShellDataFileExtension; + string manifestPath = Path.Combine(directoryToCheck, version.ToString(), proposedModuleName); + manifestPath += StringLiterals.PowerShellDataFileExtension; if (File.Exists(manifestPath)) { isModuleDirectory = true; @@ -230,22 +257,23 @@ internal static IEnumerable GetDefaultAvailableModuleFiles(string topDir } } - foreach (string ext in ModuleIntrinsics.PSModuleExtensions) + if (!isModuleDirectory) { - string moduleFile = Path.Combine(directoryToCheck, proposedModuleName) + ext; - if (!File.Exists(moduleFile)) + foreach (string ext in ModuleIntrinsics.PSModuleExtensions) { - continue; - } - - isModuleDirectory = true; - yield return moduleFile; + string moduleFile = Path.Combine(directoryToCheck, proposedModuleName) + ext; + if (File.Exists(moduleFile)) + { + isModuleDirectory = true; + yield return moduleFile; - // when finding the default modules we stop when the first - // match is hit - searching in order .psd1, .psm1, .dll - // if a file is found but is not readable then it is an - // error - break; + // when finding the default modules we stop when the first + // match is hit - searching in order .psd1, .psm1, .dll + // if a file is found but is not readable then it is an + // error + break; + } + } } if (!isModuleDirectory) @@ -280,7 +308,8 @@ internal static List GetModuleVersionSubfolders(string moduleBase) if (!string.IsNullOrWhiteSpace(moduleBase) && Directory.Exists(moduleBase)) { - var subdirectories = Directory.GetDirectories(moduleBase); + var options = Utils.PathIsUnc(moduleBase) ? s_uncPathEnumerationOptions : s_defaultEnumerationOptions; + string[] subdirectories = Directory.GetDirectories(moduleBase, "*", options); ProcessPossibleVersionSubdirectories(subdirectories, versionFolders); } @@ -289,11 +318,10 @@ internal static List GetModuleVersionSubfolders(string moduleBase) private static void ProcessPossibleVersionSubdirectories(string[] subdirectories, List versionFolders) { - foreach (var subdir in subdirectories) + foreach (string subdir in subdirectories) { - var subdirName = Path.GetFileName(subdir); - Version version; - if (Version.TryParse(subdirName, out version)) + string subdirName = Path.GetFileName(subdir); + if (Version.TryParse(subdirName, out Version version)) { versionFolders.Add(version); } @@ -307,7 +335,7 @@ private static void ProcessPossibleVersionSubdirectories(string[] subdirectories internal static bool IsModuleInVersionSubdirectory(string modulePath, out Version version) { version = null; - var folderName = Path.GetDirectoryName(modulePath); + string folderName = Path.GetDirectoryName(modulePath); if (folderName != null) { folderName = Path.GetFileName(folderName); @@ -339,7 +367,7 @@ internal static IEnumerable GetMatchingCommands(string pattern, Exe ) ) { - foreach (string modulePath in GetDefaultAvailableModuleFiles(true, false, context)) + foreach (string modulePath in GetDefaultAvailableModuleFiles(isForAutoDiscovery: false, context)) { // Skip modules that have already been loaded so that we don't expose private commands. string moduleName = Path.GetFileNameWithoutExtension(modulePath); diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index df216e5a515..d0e13d9a6ad 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -486,6 +486,15 @@ public Version DotNetFrameworkVersion internal set; } + internal Collection DeclaredFunctionExports = null; + internal Collection DeclaredCmdletExports = null; + internal Collection DeclaredAliasExports = null; + internal Collection DeclaredVariableExports = null; + + internal List DetectedFunctionExports = new List(); + internal List DetectedCmdletExports = new List(); + internal Dictionary DetectedAliasExports = new Dictionary(); + /// /// Lists the functions exported by this module... /// @@ -496,18 +505,16 @@ public Dictionary ExportedFunctions Dictionary exports = new Dictionary(StringComparer.OrdinalIgnoreCase); // If the module is not binary, it may also have functions... - if ((DeclaredFunctionExports != null) && (DeclaredFunctionExports.Count > 0)) + if (DeclaredFunctionExports != null) { + if (DeclaredFunctionExports.Count == 0) { return exports; } + foreach (string fn in DeclaredFunctionExports) { FunctionInfo tempFunction = new FunctionInfo(fn, ScriptBlock.EmptyScriptBlock, null) { Module = this }; exports[fn] = tempFunction; } } - else if ((DeclaredFunctionExports != null) && (DeclaredFunctionExports.Count == 0)) - { - return exports; - } else if (SessionState != null) { // If there is no session state object associated with this list, @@ -525,7 +532,7 @@ public Dictionary ExportedFunctions } else { - foreach (var detectedExport in _detectedFunctionExports) + foreach (var detectedExport in DetectedFunctionExports) { if (!exports.ContainsKey(detectedExport)) { @@ -636,9 +643,6 @@ public String Prefix internal set; } - internal Collection DeclaredFunctionExports = null; - internal List _detectedFunctionExports = new List(); - /// /// Add function to the fixed exports list /// @@ -647,9 +651,9 @@ internal void AddDetectedFunctionExport(string name) { Dbg.Assert(name != null, "AddDetectedFunctionExport should not be called with a null value"); - if (!_detectedFunctionExports.Contains(name)) + if (!DetectedFunctionExports.Contains(name)) { - _detectedFunctionExports.Add(name); + DetectedFunctionExports.Add(name); } } @@ -662,18 +666,16 @@ public Dictionary ExportedCmdlets { Dictionary exports = new Dictionary(StringComparer.OrdinalIgnoreCase); - if ((DeclaredCmdletExports != null) && (DeclaredCmdletExports.Count > 0)) + if (DeclaredCmdletExports != null) { + if (DeclaredCmdletExports.Count == 0) { return exports; } + foreach (string fn in DeclaredCmdletExports) { CmdletInfo tempCmdlet = new CmdletInfo(fn, null, null, null, null) { Module = this }; exports[fn] = tempCmdlet; } } - else if ((DeclaredCmdletExports != null) && (DeclaredCmdletExports.Count == 0)) - { - return exports; - } else if ((CompiledExports != null) && (CompiledExports.Count > 0)) { foreach (CmdletInfo cmdlet in CompiledExports) @@ -683,7 +685,7 @@ public Dictionary ExportedCmdlets } else { - foreach (string detectedExport in _detectedCmdletExports) + foreach (string detectedExport in DetectedCmdletExports) { if (!exports.ContainsKey(detectedExport)) { @@ -696,8 +698,6 @@ public Dictionary ExportedCmdlets return exports; } } - internal Collection DeclaredCmdletExports = null; - internal List _detectedCmdletExports = new List(); /// /// Add CmdletInfo to the fixed exports list... @@ -707,9 +707,9 @@ internal void AddDetectedCmdletExport(string cmdlet) { Dbg.Assert(cmdlet != null, "AddDetectedCmdletExport should not be called with a null value"); - if (!_detectedCmdletExports.Contains(cmdlet)) + if (!DetectedCmdletExports.Contains(cmdlet)) { - _detectedCmdletExports.Add(cmdlet); + DetectedCmdletExports.Add(cmdlet); } } @@ -1065,7 +1065,6 @@ public Dictionary ExportedVariables return exportedVariables; } } - internal Collection DeclaredVariableExports = null; /// /// Lists the aliases exported by this module. @@ -1097,9 +1096,9 @@ public Dictionary ExportedAliases if (SessionState == null) { // Check if we detected any - if (_detectedAliasExports.Count > 0) + if (DetectedAliasExports.Count > 0) { - foreach (var pair in _detectedAliasExports) + foreach (var pair in DetectedAliasExports) { string detectedExport = pair.Key; if (!exportedAliases.ContainsKey(detectedExport)) @@ -1129,8 +1128,6 @@ public Dictionary ExportedAliases return exportedAliases; } } - internal Collection DeclaredAliasExports = null; - internal Dictionary _detectedAliasExports = new Dictionary(); /// /// Add alias to the detected alias list @@ -1141,7 +1138,7 @@ internal void AddDetectedAliasExport(string name, string value) { Dbg.Assert(name != null, "AddDetectedAliasExport should not be called with a null value"); - _detectedAliasExports[name] = value; + DetectedAliasExports[name] = value; } /// diff --git a/src/System.Management.Automation/engine/PSClassSearcher.cs b/src/System.Management.Automation/engine/PSClassSearcher.cs index e8e09931a6c..a583d650115 100644 --- a/src/System.Management.Automation/engine/PSClassSearcher.cs +++ b/src/System.Management.Automation/engine/PSClassSearcher.cs @@ -157,7 +157,7 @@ private bool FindTypeByModulePath(WildcardPattern classNameMatcher) { bool matchFound = false; - var moduleList = ModuleUtils.GetDefaultAvailableModuleFiles(false, false, _context); + var moduleList = ModuleUtils.GetDefaultAvailableModuleFiles(isForAutoDiscovery: false, _context); foreach (var modulePath in moduleList) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 new file mode 100644 index 00000000000..ec7eeb3f663 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Get-Module" -Tags "CI" { + + BeforeAll { + $originalPSModulePath = $env:PSModulePath + + New-Item -ItemType Directory -Path "$testdrive\Modules\Foo\1.1" -Force > $null + New-Item -ItemType Directory -Path "$testdrive\Modules\Foo\2.0" -Force > $null + New-Item -ItemType Directory -Path "$testdrive\Modules\Bar\Download" -Force > $null + New-Item -ItemType Directory -Path "$testdrive\Modules\Zoo\Too" -Force > $null + + New-ModuleManifest -Path "$testdrive\Modules\Foo\1.1\Foo.psd1" -ModuleVersion 1.1 + New-ModuleManifest -Path "$testdrive\Modules\Foo\2.0\Foo.psd1" -ModuleVersion 2.0 + New-ModuleManifest -Path "$testdrive\Modules\Bar\Bar.psd1" + New-ModuleManifest -Path "$testdrive\Modules\Zoo\Zoo.psd1" + + New-Item -ItemType File -Path "$testdrive\Modules\Foo\1.1\Foo.psm1" > $null + New-Item -ItemType File -Path "$testdrive\Modules\Foo\2.0\Foo.psm1" > $null + New-Item -ItemType File -Path "$testdrive\Modules\Bar\Bar.psm1" > $null + New-Item -ItemType File -Path "$testdrive\Modules\Bar\Download\Download.psm1" > $null + New-Item -ItemType File -Path "$testdrive\Modules\Zoo\Zoo.psm1" > $null + New-Item -ItemType File -Path "$testdrive\Modules\Zoo\Too\Zoo.psm1" > $null + + $env:PSModulePath = Join-Path $testdrive "Modules" + } + + AfterAll { + $env:PSModulePath = $originalPSModulePath + } + + It "Get-Module -ListAvailable" { + $modules = Get-Module -ListAvailable + $modules.Count | Should -Be 4 + $modules = $modules | Sort-Object -Property Name, Version + $modules.Name -join "," | Should -BeExactly "Bar,Foo,Foo,Zoo" + $modules[1].Version | Should -Be "1.1" + $modules[2].Version | Should -Be '2.0' + } + + It "Get-Module -ListAvailable" { + $modules = Get-Module F* -ListAvailable + $modules.Count | Should -Be 2 + $modules = $modules | Sort-Object -Property Version + $modules.Name -join "," | Should -BeExactly "Foo,Foo" + $modules[0].Version | Should -Be "1.1" + $modules[1].Version | Should -Be "2.0" + } + + It "Get-Module -ListAvailable -All" { + $modules = Get-Module -ListAvailable -All + $modules.Count | Should -Be 10 + $modules = $modules | Sort-Object -Property Name, Path + $modules.Name -join "," | Should -BeExactly "Bar,Bar,Download,Foo,Foo,Foo,Foo,Zoo,Zoo,Zoo" + + $modules[0].ModuleType | Should -BeExactly "Manifest" + $modules[1].ModuleType | Should -BeExactly "Script" + $modules[2].ModuleType | Should -BeExactly "Script" + $modules[3].ModuleType | Should -BeExactly "Manifest" + $modules[3].Version | Should -Be "1.1" + $modules[4].ModuleType | Should -BeExactly "Script" + $modules[5].ModuleType | Should -BeExactly "Manifest" + $modules[5].Version | Should -Be "2.0" + $modules[6].ModuleType | Should -BeExactly "Script" + $modules[7].ModuleType | Should -BeExactly "Script" + $modules[7].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Too\Zoo.psm1").Path + $modules[8].ModuleType | Should -BeExactly "Manifest" + $modules[8].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Zoo.psd1").Path + $modules[9].ModuleType | Should -BeExactly "Script" + $modules[9].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Zoo.psm1").Path + } + + It "Get-Module -ListAvailable -All" { + $modules = Get-Module down*, zoo -ListAvailable -All + $modules.Count | Should -Be 4 + $modules = $modules | Sort-Object -Property Name, Path + $modules.Name -join "," | Should -BeExactly "Download,Zoo,Zoo,Zoo" + + $modules[0].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Bar\Download\Download.psm1").Path + $modules[1].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Too\Zoo.psm1").Path + $modules[2].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Zoo.psd1").Path + $modules[3].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Zoo.psm1").Path + } + + It "Get-Module -ListAvailable" { + $modules = Get-Module "$testdrive\Modules\*" -ListAvailable + $modules.Count | Should -Be 4 + $modules = $modules | Sort-Object -Property Name, Version + $modules.Name -join "," | Should -BeExactly "Bar,Foo,Foo,Zoo" + $modules[1].Version | Should -Be "1.1" + $modules[2].Version | Should -Be '2.0' + } + + It "Get-Module -ListAvailable -All" { + $modules = Get-Module "$testdrive\Modules\*" -ListAvailable -All + $modules.Count | Should -Be 5 + $modules = $modules | Sort-Object -Property Name, Path + $modules.Name -join "," | Should -BeExactly "Bar,Foo,Foo,Zoo,Zoo" + $modules[3].Path | Should -BeExactly (Resolve-Path "$testdrive\Modules\Zoo\Too\Zoo.psm1").Path + } +}