Skip to content

Instantly share code, notes, and snippets.

@tsmarvin
Last active December 24, 2023 23:55
Show Gist options
  • Save tsmarvin/fe2d09ed245e6951f77937febfe5bba9 to your computer and use it in GitHub Desktop.
Save tsmarvin/fe2d09ed245e6951f77937febfe5bba9 to your computer and use it in GitHub Desktop.
TM-ProfileUtility

TM-ProfileUtility Module

Introduction

TM-ProfileUtility is a PowerShell module designed to provide various profile-related utility functions. Specifically this module is designed for use with my PowerShell Profile.

This module is part of a suite of tools designed to improve and streamline the PowerShell commandline and scripting experience.
Check out the rest of the modules by visiting my page on the PowerShell Gallery.

Features

  • Get-CurrentPath: Retrieves the current path.
  • Get-LastExecutionDuration: Gets the duration of the last executed command.
  • Get-PSProfileEditionString: Determines the "plain english" PSEdition string.
  • Get-PSVersionString: Determines the "plain english" PowerShell version string.
  • Get-ShellPath: Creates a .shell directory in the users home and returns the directoryinfo object to the user.
  • Initialize-ProfileModule: Attempts to import a module and will automatically install it if it doesn't exist.
  • New-UserProcessEnvVar: Creates a new environment variable in the user and process scopes.
  • Set-ProfileLinks: Consolidates all editions of your PowerShell profile by linking them to a common profile file.
  • Set-WindowTitle: Sets the window title for the PowerShell window.
  • Test-Admin: Tests if the current user has administrative privileges.
  • Update-ProfileScriptFromGist: Updates the profile script from a GitHub Gist.

Requirements

Installation

Install TM-ProfileUtility from the PowerShell Gallery:

Install-Module TM-ProfileUtility -Scope CurrentUser -Repository PSGallery

For manual installation, download the module files and place them in a "TM-ProfileUtility" folder in your PowerShell modules directory ($Env:PSModulePath).

MIT License
Copyright (c) 2023 Taylor Marvin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@{
# Script module or binary module file associated with this manifest.
RootModule = 'TM-ProfileUtility.psm1'
# Version number of this module.
ModuleVersion = '0.0.14'
# Supported PSEditions
CompatiblePSEditions = @('Desktop','Core')
# ID used to uniquely identify this module
GUID = '82c4123f-77ae-497f-849c-a9a549d42984'
# Author of this module
Author = 'Taylor Marvin'
# Company or vendor of this module
CompanyName = 'N/A'
# Copyright statement for this module
Copyright = 'Taylor Marvin (2023)'
# Description of the functionality provided by this module
Description = 'Provides various profile related utilty functionality.'
# Minimum version of the PowerShell engine required by this module
PowerShellVersion = '5.1'
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @(
@{ModuleName='TM-PSGitHubGistManagement'; ModuleVersion='0.0.6'; GUID='429cacf0-6b52-4e7c-a86a-ddbb7fae4b88'},
@{ModuleName='TM-RandomUtility'; ModuleVersion='0.0.7'; GUID='c07a9da5-9562-42b6-8aba-1279fdb25a8e'},
@{ModuleName='TM-GitUtility'; ModuleVersion='0.0.7'; GUID='a0331b25-5435-4ee7-9638-6729d75afe88'},
@{ModuleName='TM-ValidationUtility'; ModuleVersion='0.0.4'; GUID='1f1eebe8-7a0b-49ae-901e-c877f090a7fc'}
)
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
'Get-CurrentPath',
'Get-LastExecutionDuration',
'Get-PSProfileEditionString',
'Get-PSVersionString',
'Get-ShellPath',
'Initialize-ProfileModule',
'New-UserProcessEnvVar',
'Set-ProfileLinks',
'Set-WindowTitle',
'Test-Admin',
'Update-ProfileScriptFromGist'
)
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('Profile', 'Utility')
# A URL to the license for this module.
LicenseUri = 'https://gist.github.com/tsmarvin/fe2d09ed245e6951f77937febfe5bba9#file-license'
# A URL to the main website for this project.
ProjectUri = 'https://gist.github.com/tsmarvin/fe2d09ed245e6951f77937febfe5bba9'
# Prerelease string of this module
# Prerelease = ''
# Flag to indicate whether the module requires explicit user acceptance for install/update/save
RequireLicenseAcceptance = $false
}
}
}
using namespace Microsoft.PowerShell.Commands
using namespace System
using namespace System.IO
using namespace System.Management.Automation
# Ensure we're using the primary write commands from the Microsoft.PowerShell.Utility module.
Set-Alias -Name 'Write-Progress' -Value 'Microsoft.PowerShell.Utility\Write-Progress' -Scope Script
Set-Alias -Name 'Write-Debug' -Value 'Microsoft.PowerShell.Utility\Write-Debug' -Scope Script
Set-Alias -Name 'Write-Verbose' -Value 'Microsoft.PowerShell.Utility\Write-Verbose' -Scope Script
Set-Alias -Name 'Write-Host' -Value 'Microsoft.PowerShell.Utility\Write-Host' -Scope Script
Set-Alias -Name 'Write-Information' -Value 'Microsoft.PowerShell.Utility\Write-Information' -Scope Script
Set-Alias -Name 'Write-Warning' -Value 'Microsoft.PowerShell.Utility\Write-Warning' -Scope Script
Set-Alias -Name 'Write-Error' -Value 'Microsoft.PowerShell.Utility\Write-Error' -Scope Script
function Get-CurrentPath {
<#
.SYNOPSIS
Returns the current location's provider path.
.OUTPUTS
Returns the ProviderPath except when the current location matches the start to a UNC path.
In those cases the $executionContext.SessionState.Path.CurrentLocation.Path is returned instead.
#>
[CmdletBinding()]
[OutputType([string])]
param()
$ProviderPath = (Get-Location).ProviderPath
$result = if ($ProviderPath -match '\\\\') {
$executionContext.SessionState.Path.CurrentLocation.Path
} else {
$ProviderPath
}
return $result
}
function Get-LastExecutionDuration {
<#
.SYNOPSIS
Returns the duration of the last executed command.
This logic was taken from Steve Lee's powershell profile:
https://gist.github.com/SteveL-MSFT/a208d2bd924691bae7ec7904cab0bd8e
#>
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter(Mandatory=$false)]
[HistoryInfo]$LastCommand = (Get-History -Count 1 -ErrorAction Ignore)
)
if ($null -ne $LastCommand) {
$cmdTime = if ($PSVersionTable.PSEdition -eq 'Desktop') {
($LastCommand.EndExecutionTime - $LastCommand.StartExecutionTime).TotalMilliseconds
} else {
$LastCommand.Duration.TotalMilliseconds
}
$units = 'ms'
if ($cmdTime -ge 1000) {
$units = 's'
$cmdTime = $LastCommand.Duration.TotalSeconds
if ($cmdTime -ge 60) {
$units = 'm'
$cmdTIme = $LastCommand.Duration.TotalMinutes
}
}
if ($cmdTime) { return "$($cmdTime.ToString('#.##'))$units " }
}
return ''
}
function Get-PSProfileEditionString {
<#
.SYNOPSIS
Returns a formatted string to more easily determine which platform is currently running.
#>
[CmdletBinding()]
[OutputType([string])]
param()
$Edition = if ($PSVersionTable.PSEdition -eq 'Desktop') {
'PS'
} elseif ($PSVersionTable.PSEdition -eq 'Core') {
'Pwsh' + (. {if ($IsLinux) { '-Linux' } elseif ($IsMacOS) { '-MacOS' } })
} elseif ([string]::IsNullOrWhiteSpace($PSVersionTable.PSEdition.ToString()) -eq $false) {
$PSVersionTable.PSEdition.ToString()
} else {
[string]::Empty
}
return $Edition
}
function Get-PSVersionString {
<#
.SYNOPSIS
Returns a string containing the PSProfileEditionString and the PSVersion major minor and patch information.
#>
[CmdletBinding()]
[OutputType([string])]
param()
$Psv = $PSVersionTable.PSVersion
return (Get-PSProfileEditionString) +" v$($Psv.Major).$($Psv.Minor).$($Psv.Patch)"
}
function Get-ShellPath {
<#
.SYNOPSIS
Creates a .shell directory in the users home and returns the directoryinfo object to the user.
.DESCRIPTION
Creates a .shell directory under the users home directory (or the windows users home directory for WSL) and returns
the directoryinfo object to the user.
#>
[CmdletBinding()]
[OutputType([System.IO.DirectoryInfo])]
param()
$ShellPath = if ($IsLinux -and [IO.Directory]::Exists("/mnt/c/Users/$env:USER")) {
Join-Path -Path "/mnt/c/Users/$env:USER" -ChildPath '.shell'
} else {
Join-Path -Path (. { if ($env:OS -eq 'Windows_NT') { $HOME } else { $env:HOME } }) -ChildPath '.shell'
}
# Create the ShellPath if it doesn't exist.
return (New-Item -Path $ShellPath -ItemType Directory -Force)
}
function Initialize-ProfileModule {
<#
.SYNOPSIS
Initialize-ProfileModule attempts to import a module and will automatically install it if it doesn't exist.
.PARAMETER Name
The name of the module to be imported.
.PARAMETER MinimumVersion
The minimum version of the module to be imported. Default is '0.0.1'.
.PARAMETER AllowPrerelease
Allow the import of prerelease module versions.
.PARAMETER ArgumentList
The ArgumentList passed into the Import-Module command.
#>
[CmdletBinding()]
[OutputType([Void])]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$Names,
[Parameter(Mandatory=$false)]
[string]$MinimumVersion = '0.0.1',
[Parameter(Mandatory=$false)]
[switch]$AllowPrerelease,
[Parameter(Mandatory=$false)]
[string[]]$ArgumentList = @()
)
begin {
$Scope = if (Test-Admin) { 'AllUsers' } else { 'CurrentUser' }
$Verbose = if ($PSBoundParameters.ContainsKey('Verbose')) { $PSBoundParameters['Verbose'] } else { $false }
# Fix missing PSGet versions on windows.
$PSGet = 'PowerShellGet'
$PSGV = '2.2.5'
if (
($env:OS -eq 'Windows_NT') -and
($null -eq (Get-Module -Name $PSGet -ListAvailable | Where-Object {$_.Version -eq $PSGV}))
) {
$PSApp = if ($IsWindows) { 'pwsh.exe' } else { 'powershell.exe' }
# Start the update in another process so that we don't have issues with clobbering.
$InstallPSGet = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(@"
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor
[Net.SecurityProtocolType]::Tls12
if (`$null -eq (Get-PackageProvider -Name 'NuGet' -ErrorAction Ignore)) {
Install-PackageProvider -Name NuGet -Force
}
Install-Module -Name '$PSGet' -RequiredVersion '$PSGV' -Scope '$Scope' -Repository 'PSGallery' -AllowClobber -Force
"@
))
if ($Verbose) { $InstallPSGet += ' -Verbose' }
Start-Process -FilePath $PSApp -Wait -NoNewWindow -ArgumentList '-noprofile', '-ec', $InstallPSGet
}
}
process {
foreach ($moduleName in $Names){
$InstallModule = @{
Name = $moduleName
MinimumVersion = $MinimumVersion
AllowPrerelease = $AllowPrerelease
Scope = $Scope
Repository = 'PSGallery'
AcceptLicense = $true
Force = $true
Verbose = $Verbose
}
$ImportModuleParams = @{
Name = $moduleName
MinimumVersion = $MinimumVersion
Scope = 'Global'
Verbose = $Verbose
}
if ($ArgumentList.Length -gt 0) {
$ImportModuleParams.Add('ArgumentList', $ArgumentList)
}
try {
Import-Module @ImportModuleParams -ErrorAction Stop
} catch [FileNotFoundException] {
Install-Module @InstallModule
Import-Module @ImportModuleParams
}
}
}
}
function New-UserProcessEnvVar {
<#
.SYNOPSIS
Creates a new environment variable with a value of the current datetime.
.PARAMETER VarName
The name of the new environment variable to create.
.PARAMETER Value
The value to put into the environment variable.
#>
[CmdletBinding()]
[OutputType([Void])]
param (
[Parameter(Mandatory)]
[string]$VarName,
[Parameter(Mandatory = $false)]
[string]$Value = [DateTime]::Now
)
[Environment]::SetEnvironmentVariable( $VarName, $Value, [EnvironmentVariableTarget]::User )
[Environment]::SetEnvironmentVariable( $VarName, $Value, [EnvironmentVariableTarget]::Process )
}
function Set-ProfileLinks {
<#
.SYNOPSIS
This function consolidates all editions of your PowerShell profile by linking them to a common profile file
located at the specified $ShellPath.
Any changes made to this profile script will apply across all your PowerShell sessions and editions.
.DESCRIPTION
This function is meant to be used with the profile script found here:
https://gist.github.com/tsmarvin/3ec5df59e030886a9f81a693ba01f785
The function begins by checking for the ProfileLink environment variable:
((Get-PSProfileEditionString) + '_UpdateProfileLinks')
If the environment variable exists, the function exits without performing any actions.
If the environment variable does not exist:
On Windows:
First determine whether the $CommandPath is hardlinked to the $ShellProfile ($ShellPath + profile.ps1) file.
If it is not hardlinkd, prompt the user to create a new hardlink between the files.
All Platforms:
The function checks if the $CommandPath is linked to the $Profile.CurrentUserAllHosts script.
If the $Profile.CurrentUserAllHosts script doesn't exist, prompt the user to create a new link.
On Linux, a symbolic link will be created, while on Windows, a hardlink will be used instead.
Finally, the function creates the ProfileLink environment variable and sets it to the current date and time.
.PARAMETER ShellPath
This parameter specifies the path to the directory where the shell profile script ('profile.ps1') is located.
.PARAMETER PSProfileEdition
This parameter represents the PowerShell edition, as determined by the PowerShell Profile script.
.PARAMETER CommandPath
This parameter is used to specify the path of the currently executing script.
#>
[CmdletBinding()]
[OutputType([void])]
param (
[Parameter(Mandatory)]
[Validation.ValidatePathExists('File')]
[string]$CommandPath
)
# The path to the shell profile script
$ShellProfile = Join-Path -Path (Get-ShellPath).FullName -ChildPath 'profile.ps1'
# The name of the environment variable storing the status of profile linking
$ProfileLink = (Get-PSProfileEditionString) + '_UpdateProfileLinks'
# Check if the ProfileLink environment variable is empty
if ([string]::IsNullOrEmpty((Get-Item -Path "Env:\$ProfileLink" -ErrorAction Ignore).Value)) {
# If the operating system is Windows and the executing script is not linked to the shell profile script
if (
($env:OS -eq 'Windows_NT') -and (
($null -eq (Get-Item -Path $CommandPath).Target) -or
((Get-Item -Path $ShellProfile -ErrorAction Ignore).Target -notcontains $CommandPath)
)
) {
# Parameters for creating a new hard link
$NewHardLinkParams = [hashtable]@{
ItemType = 'HardLink'
Force = $true
Verbose = $true
}
# If the current executing script is not the same as the shell profile script
if ($CommandPath -ne $ShellProfile) {
Write-Warning (
"Currently executing profile script '$CommandPath' " +
"has not been linked to the ShellProfile '$ShellProfile'."
)
# Prompt the user to link the current executing script to the shell profile script
if ((Read-Host "Link '$CommandPath' to '$ShellProfile'? (Y/N)") -ieq 'Y') {
New-Item -Path $ShellProfile -Value $CommandPath @NewHardLinkParams | Out-Null
}
}
if <# the $Profile.CurrentUserAllHosts is not linked to the shell profile script #> (
($null -eq (Get-Item -Path $Profile.CurrentUserAllHosts -ErrorAction Ignore).Target) -and
((Resolve-Path -Path $CommandPath) -eq (Resolve-Path -Path $ShellProfile))
) {
Write-Warning (
"Profile script '$($Profile.CurrentUserAllHosts)' " +
"has not been linked to the ShellProfile '$ShellProfile'."
)
# Prompt the user to link the profile for all hosts of the current user to the shell profile script
if ((Read-Host "Link '$($Profile.CurrentUserAllHosts)' to '$ShellProfile'? (Y/N)") -ieq 'Y') {
New-Item -Path $Profile.CurrentUserAllHosts -Value $ShellProfile @NewHardLinkParams | Out-Null
}
}
}
# If the $Profile.CurrentUserAllHosts script doesn't exist then link it to the shell profile.
if ($null -eq (Get-Item -Path $Profile.CurrentUserAllHosts -ErrorAction Ignore)) {
Write-Warning "PowerShell Profile '$($Profile.CurrentUserAllHosts)' does not exist."
$LinkProfilePrompt = (. {
"$(if ($env:OS -eq 'Windows_NT') { 'Hard' } else { 'Symbolic' })" +
"Link '$($Profile.CurrentUserAllHosts)' to '$ShellProfile'? (Y/N)"
})
# Prompt the user to link the profile for all hosts of the current user to the shell profile script
if ((Read-Host -Prompt $LinkProfilePrompt) -ieq 'Y') {
$newItemSplat = @{
Path = $Profile.CurrentUserAllHosts
ItemType = "$(if ($env:OS -eq 'Windows_NT') { 'HardLink' } else { 'SymbolicLink' })"
Value = $ShellProfile
Force = $true
Verbose = $true
ErrorAction = 'Stop'
}
try {
New-Item @newItemSplat | Out-Null
} catch {
Write-Warning (
"Failed to create link between '$($Profile.CurrentUserAllHosts)' and '$ShellProfile'. " +
"Error: $($_.Exception.Message)"
)
break # Don't run ProfileLinkCreation
}
}
}
# Create the ProfileLink environment variable with the current date and time
New-UserProcessEnvVar -VarName $ProfileLink
}
}
function Set-WindowTitle {
<#
.SYNOPSIS
Sets the window title based on the current PowerShell session and provider.
.DESCRIPTION
Updates the window title with the PowerShell version, provider name, and adds an [Admin] prefix
if the session is running with administrative privileges.
#>
[CmdletBinding()]
[OutputType([Void])]
param()
try {
$AdminText = if (Test-Admin) { '[Admin] ' } else { [string]::Empty }
$Host.Ui.RawUi.WindowTitle = "$AdminText$(Get-PSVersionString) [$($pwd.Provider.Name)]"
} catch { <# Do Not Fail #> }
}
function Test-Admin {
<#
.SYNOPSIS
Tests to determine if the user is running the powershell process as an admin.
.DESCRIPTION
On Windows, checks the security group to determine if the user is a member of the local admin group.
On Linux, checks to see if the user is root, or has sudo enabled.
On MacOs, always returns false at this time.
#>
[CmdletBinding()]
[OutputType([boolean])]
param()
$IsAdmin = if ($env:OS -eq 'Windows_NT') {
[Security.Principal.WindowsPrincipal]::New(
[Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} elseif ($IsLinux -and (($env:USER -ieq 'root') -or ([string]::IsNullOrWhiteSpace($env:SUDO_USER) -eq $false))) {
$true
} else {
$false
}
return $IsAdmin
}
function Update-ProfileScriptFromGist {
<#
.SYNOPSIS
Update the profile script every 25+ hours.
.DESCRIPTION
Update-ProfileScriptFromGist checks the last update time from the environment variable 'ProfileGistUpdate'.
If it has been more than a 25 hours since the last update, the function starts a new job that updates the profile
script.
This function is meant to be used with the profile script found here:
https://gist.github.com/tsmarvin/3ec5df59e030886a9f81a693ba01f785
.PARAMETER CommandPath
The path to the PowerShell profile script.
#>
[CmdletBinding()]
[OutputType([System.Management.Automation.Job],[string])]
param (
[Parameter(Mandatory)]
[Validation.ValidatePathExists('File')]
[string]$CommandPath,
[Parameter(Mandatory=$false)]
[ValidateRange(0, [int]::MaxValue)]
[int]$UpdateIntervalHours = 25
)
$EnvVarName = 'ProfileGistUpdate'
$EnvVarValue = (Get-Item -Path "Env:\$EnvVarName" -ErrorAction Ignore).Value
$UpdateTime = if ([string]::IsNullOrWhiteSpace($EnvVarValue) -eq $false) {
[DateTime]::Parse($EnvVarValue)
} else {
[DateTime]::Now.AddHours(-$UpdateIntervalHours)
}
$result = if ($UpdateTime.AddDays(1) -lt [DateTime]::Now) {
Start-BackgroundGistScriptUpdate -LocalScriptPath $CommandPath
New-UserProcessEnvVar -VarName $EnvVarName
} else {
[string]::Empty
}
return $result
}
if ($IsWindows) {
# Add Support for retrieving hardlink definitions on windows using dotnet core.
# Recreates the "Target" property with integrated hardlink support which emulates Windows PowerShell functionality.
# This c# code was based on https://github.com/PowerShell/PowerShell/issues/15139#issuecomment-812567971
# This functionality is specifically used by Set-ProfileLinks - but its also just nice to have in general.
$AddWinUtilNTFS = [hashtable]@{
Name = 'NTFS'
Namespace = 'WinUtil'
UsingNamespace = 'System.Text', 'System.Collections.Generic', 'System.IO'
MemberDefinition = @'
#region WinAPI P/Invoke declarations
public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); // 0xffffffff;
public const int MAX_PATH = 65535; // Max. NTFS path length.
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFileNameW(
string lpFileName,
uint dwFlags,
ref uint StringLength,
StringBuilder LinkName
);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindNextFileNameW(
IntPtr hFindStream,
ref uint StringLength,
StringBuilder LinkName
);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FindClose(IntPtr hFindFile);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool GetVolumePathName(
string lpszFileName,
[Out] StringBuilder lpszVolumePathName,
uint cchBufferLength
);
#endregion
/// <summary>
/// Returns the enumeration of hardlinks for the given filepath excluding the input file itself.
/// </summary>
/// <param name="filePath"></param>
/// <returns>
/// If the file has one or more hardlink (including itself) then the enumerate hardlinks (excluding itself)
/// are returned. <br/>
///
/// This means that if the hardlink only links to itself then you will receive an empty array. <br/>
///
/// If the target volume doesn't support enumerating hardlinks, the filePath doesn't exist, or the filePath
/// is the path to a directory than a null value is returned instead.
/// </returns>
public static string[] GetHardLinks(string filePath) {
string fullFilePath = Path.GetFullPath(filePath);
// If the filepath is a directory or the file does not exist then return early.
if (Directory.Exists(fullFilePath) || File.Exists(fullFilePath) == false) { return null; }
// Generate Volume Path
StringBuilder sbPath = new(MAX_PATH);
_ = GetVolumePathName(fullFilePath, sbPath, MAX_PATH); // Get target file volume (e.g. "C:\")
// Trim the trailing "\" from the volume path, to enable simple concatenation with the volume-relative
// paths returned by the FindFirstFileNameW() and FindFirstFileNameW() functions, which have a leading "\"
string volume = sbPath.ToString()[..(sbPath.Length > 0 ? sbPath.Length - 1 : 0)];
// Loop over and collect all hard links as their full paths.
uint charCount = MAX_PATH; // in/out character-count variable for the WinAPI calls.
IntPtr findHandle;
if (INVALID_HANDLE_VALUE != (findHandle = FindFirstFileNameW(fullFilePath, 0, ref charCount, sbPath))) {
List<string> result = new();
// Add each non-self path to the results list
do {
string fullHardlinkPath = volume + sbPath.ToString();
if (fullHardlinkPath.Equals(fullFilePath, StringComparison.OrdinalIgnoreCase) == false) {
result.Add(fullHardlinkPath); // Add the full path to the result list.
}
charCount = MAX_PATH; // Prepare for the next FindNextFileNameW() call.
} while (FindNextFileNameW(findHandle, ref charCount, sbPath));
FindClose(findHandle);
return result.ToArray();
}
return null;
}
'@
}
try {
Add-Type @AddWinUtilNTFS -ErrorAction Stop
} catch {
# Don't fail if the type already exists
if ($_.Exception.Message -ne "Cannot add type. The type name 'WinUtil.NTFS' already exists."){
throw
}
}
Update-TypeData -Force -TypeName System.IO.FileInfo -MemberName Target -MemberType ScriptProperty -Value {
# Output the target, if the file at hand is a symbolic link (reparse point).
[string[]]$local:TempTarget = [InternalSymbolicLinkLinkCodeMethods]::GetTarget($this)
if ($local:TempTarget) {
, [string[]]$local:TempTarget
} else {
[string[]]$local:TempHardlinks = [WinUtil.NTFS]::GetHardLinks($this.FullName)
if ($null -ne $local:TempHardlinks -and $local:TempHardlinks.Length -gt 0) {
, [string[]]$local:TempHardlinks
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment