Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5919,7 +5919,20 @@ private static string GetNamespaceToRemove(CompletionContext context, TypeComple
internal static List<CompletionResult> CompleteHelpTopics(CompletionContext context)
{
var results = new List<CompletionResult>();
var dirPath = Utils.GetApplicationBase(Utils.DefaultPowerShellShellID) + StringLiterals.DefaultPathSeparator + CultureInfo.CurrentCulture.Name;
var searchPaths = new List<string>();
var currentCulture = CultureInfo.CurrentCulture.Name;

// Add the user scope path first, since it is searched in order.
var userHelpRoot = Path.Combine(HelpUtils.GetUserHomeHelpSearchPath(), currentCulture);

if(Directory.Exists(userHelpRoot))
{
searchPaths.Add(userHelpRoot);
}

var dirPath = Path.Combine(Utils.GetApplicationBase(Utils.DefaultPowerShellShellID), currentCulture);
searchPaths.Add(dirPath);

var wordToComplete = context.WordToComplete + "*";
var topicPattern = WildcardPattern.Get("about_*.help.txt", WildcardOptions.IgnoreCase);
List<string> files = new List<string>();
Expand All @@ -5928,11 +5941,14 @@ internal static List<CompletionResult> CompleteHelpTopics(CompletionContext cont
{
var wildcardPattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);

foreach(var file in Directory.GetFiles(dirPath))
foreach (var dir in searchPaths)
{
if(wildcardPattern.IsMatch(Path.GetFileName(file)))
foreach (var file in Directory.GetFiles(dir))
{
files.Add(file);
if (wildcardPattern.IsMatch(Path.GetFileName(file)))
{
files.Add(file);
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/System.Management.Automation/help/CabinetNativeApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,19 @@ internal delegate IntPtr FdiOpenDelegate(
internal static IntPtr FdiOpen(string filename, int oflag, int pmode)
{
FileMode mode = CabinetNativeApi.ConvertOpflagToFileMode(oflag);

FileAccess access = CabinetNativeApi.ConvertPermissionModeToFileAccess(pmode);
FileShare share = CabinetNativeApi.ConvertPermissionModeToFileShare(pmode);

// This method is used for opening the cab file as well as saving the extracted files.
// When we are opening the cab file we only need read permissions.
// We force read permissions so that non-elevated users can extract cab files.
if(mode == FileMode.Open || mode == FileMode.OpenOrCreate)
{
access = FileAccess.Read;
share = FileShare.Read;
}

try
{
FileStream stream = new FileStream(filename, mode, access, share);
Expand Down
11 changes: 8 additions & 3 deletions src/System.Management.Automation/help/CommandHelpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ private HelpInfo GetHelpInfo(CommandInfo commandInfo, bool reportErrors, bool se
// and the nested module that implements the command
GetModulePaths(commandInfo, out moduleName, out moduleDir, out nestedModulePath);

Collection<String> searchPaths = new Collection<String>();
Collection<String> searchPaths = new Collection<String>(){ HelpUtils.GetUserHomeHelpSearchPath() };

if (!String.IsNullOrEmpty(moduleDir))
{
searchPaths.Add(moduleDir);
Expand Down Expand Up @@ -511,14 +512,18 @@ private string GetHelpFile(string helpFile, CmdletInfo cmdletInfo)
// we have to search only in the application base for a mshsnapin...
// if you create an absolute path for helpfile, then MUIFileSearcher
// will look only in that path.
helpFileToLoad = Path.Combine(mshSnapInInfo.ApplicationBase, helpFile);

searchPaths.Add(HelpUtils.GetUserHomeHelpSearchPath());
searchPaths.Add(mshSnapInInfo.ApplicationBase);
}
else if (cmdletInfo.Module != null && !string.IsNullOrEmpty(cmdletInfo.Module.Path))
{
helpFileToLoad = Path.Combine(cmdletInfo.Module.ModuleBase, helpFile);
searchPaths.Add(HelpUtils.GetModuleBaseForUserHelp(cmdletInfo.Module.ModuleBase, cmdletInfo.Module.Name));
searchPaths.Add(cmdletInfo.Module.ModuleBase);
}
else
{
searchPaths.Add(HelpUtils.GetUserHomeHelpSearchPath());
searchPaths.Add(GetDefaultShellSearchPath());
searchPaths.Add(GetCmdletAssemblyPath(cmdletInfo));
}
Expand Down
27 changes: 27 additions & 0 deletions src/System.Management.Automation/help/HelpFileHelpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,28 @@ private Collection<string> FilterToLatestModuleVersion(Collection<string> filesM
}
}

// Deduplicate by filename to compensate for two sources, currentuser scope and allusers scope.
// This is done after the version check filtering to ensure we do not remove later version files.
HashSet<string> fileNameHash = new HashSet<string>();

foreach(var file in filesMatched)
{
string fileName = Path.GetFileName(file);

if(!fileNameHash.Contains(fileName))
{
fileNameHash.Add(fileName);
}
else
{
// If the file need to be removed, add it to matchedFilesToRemove, if not already present.
if(!matchedFilesToRemove.Contains(file))
{
matchedFilesToRemove.Add(file);
}
}
}

return matchedFilesToRemove;
}

Expand Down Expand Up @@ -323,6 +345,7 @@ internal Collection<string> GetExtendedSearchPaths()

// Add $pshome at the top of the list
String defaultShellSearchPath = GetDefaultShellSearchPath();

int index = searchPaths.IndexOf(defaultShellSearchPath);
if (index != 0)
{
Expand All @@ -333,6 +356,9 @@ internal Collection<string> GetExtendedSearchPaths()
searchPaths.Insert(0, defaultShellSearchPath);
}

// Add the CurrentUser help path.
searchPaths.Add(HelpUtils.GetUserHomeHelpSearchPath());

// Add modules that are not loaded. Since using 'get-module -listavailable' is very expensive,
// we load all the directories (which are not empty) under the module path.
foreach (string psModulePath in ModuleIntrinsics.GetModulePath(false, this.HelpSystem.ExecutionContext))
Expand Down Expand Up @@ -366,6 +392,7 @@ internal Collection<string> GetExtendedSearchPaths()
catch (System.Security.SecurityException) { }
}
}

return searchPaths;
}

Expand Down
68 changes: 68 additions & 0 deletions src/System.Management.Automation/help/HelpUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.IO;
using System.Management.Automation;
using System.Management.Automation.Help;
using Microsoft.PowerShell.Commands;

namespace System.Management.Automation
{
internal class HelpUtils
{
private static string userHomeHelpPath = null;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: Please remove the extra space.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

/// <summary>
/// Get the path to $HOME
/// </summary>
internal static string GetUserHomeHelpSearchPath()
{
if (userHomeHelpPath == null)
{
#if UNIX
var userModuleFolder = Platform.SelectProductNameForDirectory(Platform.XDG_Type.USER_MODULES);
string userScopeRootPath = System.IO.Path.GetDirectoryName(userModuleFolder);
#else
string userScopeRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "PowerShell");
#endif
userHomeHelpPath = Path.Combine(userScopeRootPath, "Help");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about SxS PSCore6.x?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added a PSVersion string in path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted this change, since the source location for help can only host 1 help content version. So even if we have SxS PowerShell versions, the content only has 1 published version.

}

return userHomeHelpPath;
}

internal static string GetModuleBaseForUserHelp(string moduleBase, string moduleName)
{
string newModuleBase = moduleBase;

// In case of inbox modules, the help is put under $PSHOME/<current_culture>,
// since the dlls are not published under individual module folders, but under $PSHome.
// In case of other modules, the help is under moduleBase/<current_culture> or
// under moduleBase/<Version>/<current_culture>.
// The code below creates a similar layout for CurrentUser scope.
// If the the scope is AllUsers, then the help goes under moduleBase.

var userHelpPath = GetUserHomeHelpSearchPath();
string moduleBaseParent = Directory.GetParent(moduleBase).Name;

if (moduleBase.EndsWith(moduleName, StringComparison.OrdinalIgnoreCase))
{
//This module is not an inbox module, so help goes under <userHelpPath>/<moduleName>
newModuleBase = Path.Combine(userHelpPath, moduleName);
}
else if (String.Equals(moduleBaseParent, moduleName, StringComparison.OrdinalIgnoreCase))
{
//This module has version folder.
var moduleVersion = Path.GetFileName(moduleBase);
newModuleBase = Path.Combine(userHelpPath, moduleName, moduleVersion);
}
else
{
//This module is inbox module, help should be under <userHelpPath>
newModuleBase = userHelpPath;
}

return newModuleBase;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ public SwitchParameter Force
}
internal bool _force;

/// <summary>
/// Sets the scope to which help is saved.
/// </summary>
[Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true)]
public UpdateHelpScope Scope
{
get;
set;
}

#endregion

#region Events
Expand Down Expand Up @@ -862,4 +872,19 @@ internal void ProcessException(string moduleName, string culture, Exception e)

#endregion
}

/// <summary>
/// Scope to which the help should be saved.
/// </summary>
public enum UpdateHelpScope
{
/// <summary>
/// Save the help content to the user directory.
CurrentUser,

/// <summary>
/// Save the help content to the module directory. This is the default behavior.
/// </summary>
AllUsers
}
}
22 changes: 17 additions & 5 deletions src/System.Management.Automation/help/UpdateHelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,17 @@ internal override bool ProcessModuleWithCulture(UpdatableHelpModuleInfo module,
UpdatableHelpInfo newHelpInfo = null;
string helpInfoUri = null;

string moduleBase = module.ModuleBase;

if(this.Scope == UpdateHelpScope.CurrentUser)
{
moduleBase = HelpUtils.GetModuleBaseForUserHelp(moduleBase, module.ModuleName);
}

// reading the xml file even if force is specified
// Reason: we need the current version for ShouldProcess
string xml = UpdatableHelpSystem.LoadStringFromPath(this,
SessionState.Path.Combine(module.ModuleBase, module.GetHelpInfoName()),
SessionState.Path.Combine(moduleBase, module.GetHelpInfoName()),
null);

if (xml != null)
Expand All @@ -230,7 +237,7 @@ internal override bool ProcessModuleWithCulture(UpdatableHelpModuleInfo module,
}

// Don't update too frequently
if (!_alreadyCheckedOncePerDayPerModule && !CheckOncePerDayPerModule(module.ModuleName, module.ModuleBase, module.GetHelpInfoName(), DateTime.UtcNow, _force))
if (!_alreadyCheckedOncePerDayPerModule && !CheckOncePerDayPerModule(module.ModuleName, moduleBase, module.GetHelpInfoName(), DateTime.UtcNow, _force))
{
return true;
}
Expand Down Expand Up @@ -347,7 +354,7 @@ internal override bool ProcessModuleWithCulture(UpdatableHelpModuleInfo module,
continue;
}

if (Utils.IsUnderProductFolder(module.ModuleBase) && (!Utils.IsAdministrator()))
if (Utils.IsUnderProductFolder(moduleBase) && (!Utils.IsAdministrator()))
{
string message = StringUtil.Format(HelpErrors.UpdatableHelpRequiresElevation);
ProcessException(module.ModuleName, null, new UpdatableHelpSystemException("UpdatableHelpSystemRequiresElevation",
Expand Down Expand Up @@ -375,7 +382,12 @@ internal override bool ProcessModuleWithCulture(UpdatableHelpModuleInfo module,
// Gather destination paths
Collection<string> destPaths = new Collection<string>();

destPaths.Add(module.ModuleBase);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic at line 350 might have to be revised.
if (Utils.IsUnderProductFolder(module.ModuleBase) && (!Utils.IsAdministrator()))
{
string message = StringUtil.Format(HelpErrors.UpdatableHelpRequiresElevation);
ProcessException(module.ModuleName, null, new UpdatableHelpSystemException("UpdatableHelpSystemRequiresElevation",
message, ErrorCategory.InvalidOperation, null, null));
return false;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adityapatwardhan : As per our conversation, we need to update UpdatableHelpRequiresElevation to include the UpdateHelpScope change.

if(!Directory.Exists(moduleBase))
{
Directory.CreateDirectory(moduleBase);
}

destPaths.Add(moduleBase);

#if !CORECLR // Side-By-Side directories are not present in OneCore environments.
if (IsSystemModule(module.ModuleName) && Environment.Is64BitOperatingSystem)
Expand Down Expand Up @@ -442,7 +454,7 @@ internal override bool ProcessModuleWithCulture(UpdatableHelpModuleInfo module,
}

_helpSystem.GenerateHelpInfo(module.ModuleName, module.ModuleGuid, newHelpInfo.UnresolvedUri, contentUri.Culture.Name, newHelpInfo.GetCultureVersion(contentUri.Culture),
module.ModuleBase, module.GetHelpInfoName(), _force);
moduleBase, module.GetHelpInfoName(), _force);

foreach (string fileInstalled in filesInstalled)
{
Expand Down
31 changes: 19 additions & 12 deletions test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -938,22 +938,29 @@ dir -Recurse `
$completionOptions | Should -Be ([string]::Join("", $expected))
}
}
}

Describe "Tab completion help test" -Tags @('RequireAdminOnWindows', 'CI') {
It 'Should complete about help topic' {
Context "Tab completion help test" {
BeforeAll {
if ([System.Management.Automation.Platform]::IsWindows) {
$userHelpRoot = Join-Path $HOME "Documents/PowerShell/Help/"
} else {
$userModulesRoot = [System.Management.Automation.Platform]::SelectProductNameForDirectory([System.Management.Automation.Platform+XDG_Type]::USER_MODULES)
$userHelpRoot = Join-Path $userModulesRoot -ChildPath ".." -AdditionalChildPath "Help"
}
}

$aboutHelpPath = Join-Path $PSHOME (Get-Culture).Name
It 'Should complete about help topic' {
$aboutHelpPath = Join-Path $userHelpRoot (Get-Culture).Name

## If help content does not exist, tab completion will not work. So update it first.
if (-not (Test-Path (Join-Path $aboutHelpPath "about_Splatting.help.txt")))
{
Update-Help -Force -ErrorAction SilentlyContinue
}
## If help content does not exist, tab completion will not work. So update it first.
if (-not (Test-Path (Join-Path $aboutHelpPath "about_Splatting.help.txt"))) {
Update-Help -Force -ErrorAction SilentlyContinue -Scope 'CurrentUser'
}

$res = TabExpansion2 -inputScript 'get-help about_spla' -cursorColumn 'get-help about_spla'.Length
$res.CompletionMatches.Count | Should -Be 1
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting'
$res = TabExpansion2 -inputScript 'get-help about_spla' -cursorColumn 'get-help about_spla'.Length
$res.CompletionMatches.Count | Should -Be 1
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting'
}
}
}

Expand Down
Loading