tiPS.psm1
#Requires -Version 3.0 Set-StrictMode -Version Latest # All module functions that reference a file path in the module should use this variable, rather than PSScriptRoot. New-Variable -Name 'PSModuleRoot' -Value $PSScriptRoot -Option Constant -Scope Script if (-not ('tiPS.PowerShellTip' -as [type])) { [string] $assemblyFilePath = Resolve-Path -Path "$script:PSModuleRoot/Classes/tiPSClasses.dll" Add-Type -Path $assemblyFilePath } function StartModuleUpdateIfNeeded { [CmdletBinding()] [OutputType([void])] Param ( [Parameter(Mandatory = $true, HelpMessage = 'The tiPS configuration to use to determine if the module should be updated.')] [ValidateNotNullOrEmpty()] [tiPS.Configuration] $Config ) # For performance reasons, check if we should never update the module before doing anything else. if ($Config.AutoUpdateCadence -eq [tiPS.ModuleAutoUpdateCadence]::Never) { return } [DateTime] $modulesLastUpdateDate = ReadModulesLastUpdateDateOrDefault [TimeSpan] $timeSinceLastUpdate = [DateTime]::Now - $modulesLastUpdateDate [int] $daysSinceLastUpdate = $timeSinceLastUpdate.Days [bool] $moduleUpdateNeeded = $false switch ($Config.AutoUpdateCadence) { ([tiPS.ModuleAutoUpdateCadence]::Never) { $moduleUpdateNeeded = $false; break } ([tiPS.ModuleAutoUpdateCadence]::Daily) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 1; break } ([tiPS.ModuleAutoUpdateCadence]::Weekly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 7 ; break } ([tiPS.ModuleAutoUpdateCadence]::BiWeekly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 14; break } ([tiPS.ModuleAutoUpdateCadence]::Monthly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 30; break } } if ($moduleUpdateNeeded) { UpdateModule } else { Write-Debug "An auto-update of the tiPS module is not needed at this time." } } function UpdateModule { [CmdletBinding()] [OutputType([void])] Param() Write-Verbose "Updating the tiPS module in a background job." Start-Job -ScriptBlock { Write-Verbose "Updating the tiPS module." [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Update-Module -Name 'tiPS' -Force Write-Verbose "Removing all but the latest version of the tiPS module to keep the modules directory clean." $latestModuleVersion = Get-InstalledModule -Name 'tiPS' $allModuleVersions = Get-InstalledModule -Name 'tiPS' -AllVersions $allModuleVersions | Where-Object { $_.Version -ne $latestModuleVersion.Version } | Uninstall-Module -Force } [DateTime] $todayWithoutTime = [DateTime]::Now.Date # Exclude the time for a better user experience. WriteModulesLastUpdateDate -ModulesLastUpdateDate $todayWithoutTime } function ReadModulesLastUpdateDateOrDefault { [CmdletBinding()] [OutputType([DateTime])] Param() [DateTime] $modulesLastUpdateDate = [DateTime]::MinValue [string] $moduleUpdateDateFilePath = GetModulesLastUpdateDateFilePath if (Test-Path -Path $moduleUpdateDateFilePath -PathType Leaf) { [string] $modulesLastUpdateDateString = [System.IO.File]::ReadAllText($moduleUpdateDateFilePath) $modulesLastUpdateDate = [DateTime]::Parse($modulesLastUpdateDateString) } return $modulesLastUpdateDate } function WriteModulesLastUpdateDate { [CmdletBinding()] [OutputType([void])] Param ( [DateTime] $ModulesLastUpdateDate ) [string] $moduleUpdateDateFilePath = GetModulesLastUpdateDateFilePath Write-Verbose "Writing modules last update date '$ModulesLastUpdateDate' to '$moduleUpdateDateFilePath'." [System.IO.File]::WriteAllText($moduleUpdateDateFilePath, $ModulesLastUpdateDate.ToString()) } function GetModulesLastUpdateDateFilePath { [CmdletBinding()] [OutputType([string])] Param() [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath [string] $moduleUpdateDateFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'ModulesLastUpdateDate.txt' return $moduleUpdateDateFilePath } function WriteAutomaticPowerShellTipIfNeeded { [CmdletBinding()] [OutputType([void])] Param ( [Parameter(Mandatory = $true, HelpMessage = 'The tiPS configuration used to determine if a tip should be written.')] [ValidateNotNullOrEmpty()] [tiPS.Configuration] $Config ) # For performance reasons, check if we should never write a tip before doing anything else. if ($Config.AutoWritePowerShellTipCadence -eq [tiPS.WritePowerShellTipCadence]::Never) { return } [DateTime] $lastAutomaticTipWrittenDate = ReadLastAutomaticTipWrittenDateOrDefault [TimeSpan] $timeSinceLastAutomaticTipWritten = [DateTime]::Now - $lastAutomaticTipWrittenDate [int] $daysSinceLastAutomaticTipWritten = $timeSinceLastAutomaticTipWritten.Days [bool] $shouldShowTip = $false switch ($Config.AutoWritePowerShellTipCadence) { ([tiPS.WritePowerShellTipCadence]::Never) { $shouldShowTip = $false; break } ([tiPS.WritePowerShellTipCadence]::EverySession) { $shouldShowTip = $true; break } ([tiPS.WritePowerShellTipCadence]::Daily) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 1; break } ([tiPS.WritePowerShellTipCadence]::Weekly) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 7; break } } if ($shouldShowTip) { [bool] $isSessionInteractive = TestPowerShellSessionIsInteractive if (-not $isSessionInteractive) { Write-Verbose "tiPS is configured to write an automatic tip now, but this session is non-interactive. tiPS will only write automatic tips when it is imported into an interactive PowerShell session. This prevents a tip from being written at unexpected times, such as when the user or an automated process runs PowerShell scripts." return } WriteAutomaticPowerShellTip } else { Write-Debug "Showing a tiPS PowerShell tip is not needed at this time." } } function WriteAutomaticPowerShellTip { [CmdletBinding()] [OutputType([void])] Param() Write-PowerShellTip [DateTime] $todayWithoutTime = [DateTime]::Now.Date # Exclude the time for a better user experience. WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate $todayWithoutTime } function TestPowerShellSessionIsInteractive { [CmdletBinding()] [OutputType([bool])] Param() if (-not [Environment]::UserInteractive) { Write-Debug "The [Environment]::UserInteractive property shows this PowerShell session is not interactive." return $false } [string[]] $typicalNonInteractiveCommandLineArguments = @( '-Command' '-c' '-EncodedCommand' '-e' '-ec' '-File' '-f' '-NonInteractive' ) [string[]] $commandLineArgs = [Environment]::GetCommandLineArgs() Write-Debug "The PowerShell command line arguments are '$commandLineArgs'." [string[]] $nonInteractiveArgMatches = $commandLineArgs | Where-Object { $_ -in $typicalNonInteractiveCommandLineArguments } [bool] $isNonInteractive = $null -ne $nonInteractiveArgMatches -and $nonInteractiveArgMatches.Count -gt 0 if ($isNonInteractive) { return $false } return $true } function ReadLastAutomaticTipWrittenDateOrDefault { [CmdletBinding()] [OutputType([DateTime])] Param() [DateTime] $lastAutomaticTipWrittenDate = [DateTime]::MinValue [string] $lastAutomaticTipWrittenDateFilePath = GetLastAutomaticTipWrittenDateFilePath if (Test-Path -Path $lastAutomaticTipWrittenDateFilePath -PathType Leaf) { [string] $lastAutomaticTipWrittenDateString = [System.IO.File]::ReadAllText($lastAutomaticTipWrittenDateFilePath) $lastAutomaticTipWrittenDate = [DateTime]::Parse($lastAutomaticTipWrittenDateString) } return $lastAutomaticTipWrittenDate } function WriteLastAutomaticTipWrittenDate { [CmdletBinding()] [OutputType([void])] Param ( [DateTime] $LastAutomaticTipWrittenDate ) [string] $lastAutomaticTipWrittenDateFilePath = GetLastAutomaticTipWrittenDateFilePath Write-Verbose "Writing last automatic tip Written date '$LastAutomaticTipWrittenDate' to '$lastAutomaticTipWrittenDateFilePath'." [System.IO.File]::WriteAllText($lastAutomaticTipWrittenDateFilePath, $LastAutomaticTipWrittenDate.ToString()) } function GetLastAutomaticTipWrittenDateFilePath { [CmdletBinding()] [OutputType([string])] Param() [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath [string] $lastAutomaticTipWrittenDateFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'LastAutomaticTipWrittenDate.txt' return $lastAutomaticTipWrittenDateFilePath } function GetConfigurationFilePath { [CmdletBinding()] [OutputType([string])] Param() [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath [string] $configFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'tiPSConfiguration.json' return $configFilePath } function ReadConfigurationFromFileOrDefault { [CmdletBinding()] [OutputType([tiPS.Configuration])] Param() $config = [tiPS.Configuration]::new() [string] $configFilePath = GetConfigurationFilePath if (Test-Path -Path $configFilePath -PathType Leaf) { Write-Verbose "Reading configuration from '$configFilePath'." $config = [System.IO.File]::ReadAllText($configFilePath) | ConvertFrom-Json } return $config } function WriteConfigurationToFile { [CmdletBinding()] [OutputType([void])] Param ( [tiPS.Configuration] $Config ) [string] $configFilePath = GetConfigurationFilePath if (-not (Test-Path -Path $configFilePath -PathType Leaf)) { New-Item -Path $configFilePath -ItemType File -Force > $null } Write-Verbose "Writing configuration to '$configFilePath'." $Config | ConvertTo-Json -Depth 100 | Set-Content -Path $configFilePath -Force } function InitializeModule { [CmdletBinding()] [OutputType([void])] Param() Write-Debug "Ensuring the tiPS data directory exists." EnsureTiPSAppDataDirectoryExists Write-Debug 'Reading in configuration from JSON file and storing it in a $TiPSConfiguration variable for access by other module functions.' [tiPS.Configuration] $config = ReadConfigurationFromFileOrDefault New-Variable -Name 'TiPSConfiguration' -Value $config -Scope Script Write-Debug 'Reading all tips from JSON file and storing them in a $UnshownTips variable for access by other module functions.' [System.Collections.Specialized.OrderedDictionary] $tipDictionary = ReadAllPowerShellTipsFromJsonFile New-Variable -Name 'UnshownTips' -Value $tipDictionary -Scope Script Write-Debug 'Removing tips that have already been shown from the $UnshownTips variable.' RemoveTipsAlreadyShown -Tips $script:UnshownTips Write-Debug "Checking if we should write a PowerShell tip, and writing one if needed." WriteAutomaticPowerShellTipIfNeeded -Config $script:TiPSConfiguration Write-Debug 'Checking if the module needs to be updated, and updating it if needed.' StartModuleUpdateIfNeeded -Config $script:TiPSConfiguration } function EnsureTiPSAppDataDirectoryExists { [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath [bool] $directoryDoesNotExist = -not (Test-Path -Path $appDataDirectoryPath -PathType Container) if ($directoryDoesNotExist) { Write-Verbose "Creating tiPS data directory '$appDataDirectoryPath'." New-Item -Path $appDataDirectoryPath -ItemType Directory -Force > $null } } function GetPowerShellProfileFilePaths { [string[]] $profileFilePaths = @() # The $PROFILE variable may not exist depending on the host or the context in which PowerShell was started. if (Test-Path -Path variable:PROFILE) { $profileFilePaths = @( $PROFILE.CurrentUserAllHosts $PROFILE.CurrentUserCurrentHost $PROFILE.AllUsersAllHosts $PROFILE.AllUsersCurrentHost ) } return ,$profileFilePaths } function GetPowerShellProfileFilePathsThatExist { [string[]] $powerShellProfileFilePaths = GetPowerShellProfileFilePaths [string[]] $profileFilePathsThatExist = $powerShellProfileFilePaths | Where-Object { Test-Path -Path $_ -PathType Leaf } return ,$profileFilePathsThatExist } function GetPowerShellProfileFilePathToAddImportTo { [string] $profileFilePath = [string]::Empty # The $PROFILE variable may not exist depending on the host or the context in which PowerShell was started. if (Test-Path -Path variable:PROFILE) { $profileFilePath = $PROFILE.CurrentUserAllHosts } return $profileFilePath } function GetImportStatementToAddToPowerShellProfile { return 'Import-Module -Name tiPS # Added by tiPS to get automatic tips and updates.' } function ReadAllPowerShellTipsFromJsonFile { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] Param() [string] $powerShellTipsJsonFilePath = Join-Path -Path $script:PSModuleRoot -ChildPath 'PowerShellTips.json' Write-Verbose "Reading PowerShell tips from '$powerShellTipsJsonFilePath'." [tiPS.PowerShellTip[]] $tipObjects = [System.IO.File]::ReadAllText($powerShellTipsJsonFilePath) | # Use .NET method instead of Get-Content for performance. ConvertFrom-Json # We assume the tips are sorted by CreatedDate when added to the json file, so we don't need to sort them again here. # Otherwise we would just append '| Sort-Object -Property CreatedDate' here. [System.Collections.Specialized.OrderedDictionary] $tipDictionary = [System.Collections.Specialized.OrderedDictionary]::new() foreach ($tip in $tipObjects) { $tipDictionary[$tip.Id] = $tip } return $tipDictionary } function RemoveTipsAlreadyShown { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] Param ( [Parameter(Mandatory = $true, HelpMessage = 'The hashtable of tips to remove tips that have already been shown from.')] [System.Collections.Specialized.OrderedDictionary] $Tips ) [string[]] $tipIdsAlreadyShown = ReadTipIdsAlreadyShownOrDefault if ($tipIdsAlreadyShown.Count -gt 0) { Write-Verbose "Removing $($tipIdsAlreadyShown.Count) tips that have already been shown." foreach ($tipId in $tipIdsAlreadyShown) { $Tips.Remove($tipId) } } } function ReadTipIdsAlreadyShownOrDefault { [CmdletBinding()] [OutputType([string[]])] # PSScriptAnalyzer does not properly handle the OutputType attribute for string arrays, so just # suppress the warning: https://github.com/PowerShell/PSScriptAnalyzer/issues/1471#issuecomment-1735962402 [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] Param() [string[]] $tipIdsAlreadyShown = @() [string] $tipIdsAlreadyShownFilePath = GetTipIdsAlreadyShownFilePath if (Test-Path -Path $tipIdsAlreadyShownFilePath -PathType Leaf) { $tipIdsAlreadyShown = [System.IO.File]::ReadAllLines($tipIdsAlreadyShownFilePath) } return ,$tipIdsAlreadyShown } function AppendTipIdToTipIdsAlreadyShown { [CmdletBinding()] [OutputType([void])] Param ( [string] $TipId ) [string] $tipIdsAlreadyShownFilePath = GetTipIdsAlreadyShownFilePath [string[]] $tipIdAsArray = @($TipId) Write-Verbose "Appending Tip ID '$TipId' to '$tipIdsAlreadyShownFilePath'." [System.IO.File]::AppendAllLines($tipIdsAlreadyShownFilePath, $tipIdAsArray) } function ClearTipIdsAlreadyShown { [CmdletBinding()] [OutputType([void])] Param() [string] $tipIdsAlreadyShownFilePath = GetTipIdsAlreadyShownFilePath Write-Verbose "Clearing '$tipIdsAlreadyShownFilePath'." [System.IO.File]::WriteAllText($tipIdsAlreadyShownFilePath, [string]::Empty) } function GetTipIdsAlreadyShownFilePath { [CmdletBinding()] [OutputType([string])] Param() [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath [string] $tipIdsAlreadyShownFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'TipIdsAlreadyShown.txt' return $tipIdsAlreadyShownFilePath } function WritePowerShellTipToTerminal { [CmdletBinding()] [OutputType([void])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] Param ( [Parameter(Mandatory = $true, HelpMessage = 'The PowerShell Tip to write to the terminal.')] [ValidateNotNullOrEmpty()] [tiPS.PowerShellTip] $Tip ) [ConsoleColor] $headerColor = [ConsoleColor]::Cyan [ConsoleColor] $tipTextColor = [ConsoleColor]::White [ConsoleColor] $exampleColor = [ConsoleColor]::Yellow [ConsoleColor] $urlsColor = [ConsoleColor]::Green # Calculate how many header characters to put on each side of the title to make it look nice. [int] $numberOfCharactersInHeader = 90 [int] $headerContentLength = $Tip.Title.Length + 2 + $Tip.Category.ToString().Length + 3 [int] $numberOfHeaderCharactersOnEachSideOfTitle = [Math]::Floor(($numberOfCharactersInHeader - ($headerContentLength)) / 2) [int] $additionalHeaderCharacterNeeded = 0 if ($headerContentLength % 2 -eq 1) { $additionalHeaderCharacterNeeded = 1 } [string] $header = ('-' * $numberOfHeaderCharactersOnEachSideOfTitle) + ' ' + $Tip.Title + ' ' + '[' + $Tip.Category + '] ' + ('-' * ($numberOfHeaderCharactersOnEachSideOfTitle + $additionalHeaderCharacterNeeded)) Write-Host $header -ForegroundColor $headerColor Write-Host $Tip.TipText -ForegroundColor $tipTextColor if ($Tip.ExampleIsProvided) { Write-Host 'Example:' -ForegroundColor $exampleColor Write-Host $Tip.Example -ForegroundColor $exampleColor } if ($Tip.UrlsAreProvided) { Write-Host 'More information: ' -ForegroundColor $urlsColor -NoNewline Write-Host $Tip.Urls -ForegroundColor $urlsColor } Write-Host ('-' * $numberOfCharactersInHeader) -ForegroundColor $headerColor } function Add-TiPSImportToPowerShellProfile { <# .SYNOPSIS Adds the tiPS Import-Module statement to the user's PowerShell profile file. .DESCRIPTION This function edits the user's PowerShell profile file to import the tiPS module, which can provide automatic tips and updates. If the profile already imports the tiPS module, then no changes are made. Only the default PowerShell profile paths are searched to see if the tiPS module is already imported; if it is imported from a dot-sourced script, the function will not detect that and will add an import statement directly to the profile file. .INPUTS None. You cannot pipe objects to the function. .OUTPUTS None. The function does not return any objects. .EXAMPLE Add-TiPSImportToPowerShellProfile This example edits the PowerShell profile to add a tiPS Import-Module statement. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([void])] Param() Process { [bool] $moduleImportStatementIsAlreadyInProfile = Test-PowerShellProfileImportsTiPS if ($moduleImportStatementIsAlreadyInProfile) { Write-Verbose "PowerShell profile already imports the tiPS module, so no changes are necessary." return } [string] $profileFilePath = GetPowerShellProfileFilePathToAddImportTo [string] $contentToAddToProfile = GetImportStatementToAddToPowerShellProfile if ([string]::IsNullOrWhiteSpace($profileFilePath)) { Write-Error "Could not determine the PowerShell profile file path." return } if (-not (Test-Path -Path $profileFilePath -PathType Leaf)) { if ($PSCmdlet.ShouldProcess("PowerShell profile '$profileFilePath'", 'Create')) { Write-Verbose "Creating PowerShell profile '$profileFilePath'." New-Item -Path $profileFilePath -ItemType File -Force > $null } } if ($PSCmdlet.ShouldProcess("PowerShell profile '$profileFilePath'", 'Update')) { Write-Verbose "Adding '$contentToAddToProfile' to PowerShell profile '$profileFilePath'." Add-Content -Path $profileFilePath -Value $contentToAddToProfile -Force } } } function Get-PowerShellTip { <# .SYNOPSIS Get a PowerShellTip object. If no parameters are specified, a random tip is returned. .DESCRIPTION Get a PowerShellTip object. If no parameters are specified, a random tip is returned. The list of tips already shown is stored in a file in the TiPS data directory. If no parameters are provided, a random tip is returned from the list of tips that have not yet been shown. When a tip is shown, it is added to the list. If all tips have been shown, the list is reset. .PARAMETER Id The ID of the tip to retrieve. If not supplied, a random tip will be returned. .PARAMETER AllTips Return all tips. When this parameter is used, the list of tips shown is not updated. .INPUTS You can pipe a [string] of the ID of the tip to retrieve, or a PSCustomObject with a [string] 'Id' property. .OUTPUTS A [tiPS.PowerShellTip] object representing the PowerShell tip. If the -AllTips switch is provided, a [System.Collections.Specialized.OrderedDictionary] is returned. .EXAMPLE Get-PowerShellTip Get a random tip that has not been shown yet. .EXAMPLE Get-PowerShellTip -Id '2023-07-16-powershell-is-open-source' Get the tip with the specified ID. If no tip with the specified ID exists, an error is written. .EXAMPLE Get-PowerShellTip -AllTips Get all tips. .EXAMPLE '2023-07-16-powershell-is-open-source' | Get-PowerShellTip Pipe a [string] of the ID of the tip to retrieve. .EXAMPLE [PSCustomObject]@{ Id = '2023-07-16-powershell-is-open-source' } | Get-PowerShellTip Pipe an object with a [string] 'Id' property of the tip to retrieve. #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([tiPS.PowerShellTip], ParameterSetName = 'Default')] [OutputType([System.Collections.Specialized.OrderedDictionary], ParameterSetName = 'AllTips')] Param ( [Parameter(ParameterSetName = 'Default', Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The ID of the tip to retrieve. If not supplied, a random tip will be returned.')] [string] $Id, [Parameter(ParameterSetName = 'AllTips', Mandatory = $false, HelpMessage = 'Return all tips.')] [switch] $AllTips ) Process { if ($AllTips) { return ReadAllPowerShellTipsFromJsonFile } [bool] $allTipsHaveBeenShown = $script:UnshownTips.Count -eq 0 if ($allTipsHaveBeenShown) { ResetUnshownTips } [bool] $tipIdWasProvided = (-not [string]::IsNullOrWhiteSpace($Id)) if ($tipIdWasProvided) { [bool] $unshownTipsDoesNotContainTipId = (-not $script:UnshownTips.Contains($Id)) if ($unshownTipsDoesNotContainTipId) { [hashtable] $allTips = ReadAllPowerShellTipsFromJsonFile [bool] $tipIdDoesNotExist = (-not $allTips.Contains($Id)) if ($tipIdDoesNotExist) { Write-Error "A tip with ID '$Id' does not exist." return } [tiPS.PowerShellTip] $tip = $allTips[$Id] return $tip } } else { Write-Verbose "A Tip ID was not provided, so getting an unshown tip based on the user's configuration." switch ($script:TiPSConfiguration.TipRetrievalOrder) { ([tiPS.TipRetrievalOrder]::NewestFirst) { $Id = $script:UnshownTips.Keys | Select-Object -Last 1; break } ([tiPS.TipRetrievalOrder]::OldestFirst) { $Id = $script:UnshownTips.Keys | Select-Object -First 1; break } ([tiPS.TipRetrievalOrder]::Random) { $Id = $script:UnshownTips.Keys | Get-Random -Count 1; break } } } [tiPS.PowerShellTip] $tip = $script:UnshownTips[$Id] MarkTipIdAsShown -TipId $Id return $tip } } function ResetUnshownTips { [CmdletBinding()] [OutputType([void])] Param() Write-Verbose "Resetting the list of unshown tips, and clearing the list of shown tips." $script:UnshownTips = ReadAllPowerShellTipsFromJsonFile ClearTipIdsAlreadyShown } function MarkTipIdAsShown { [CmdletBinding()] [OutputType([void])] Param ( [Parameter(Mandatory = $true, HelpMessage = 'The ID of the tip to mark as shown.')] [string] $TipId ) $script:UnshownTips.Remove($TipId) if ($script:UnshownTips.Count -eq 0) { ResetUnshownTips } else { AppendTipIdToTipIdsAlreadyShown -TipId $TipId } } function Get-TiPSConfiguration { <# .SYNOPSIS Get the tiPS module configuration for the current user. .DESCRIPTION Get the tiPS module configuration for the current user. .INPUTS None. You cannot pipe objects to the function. .OUTPUTS A [tiPS.Configuration] object containing all of the tiPS module configuration for the current user. .EXAMPLE Get-TiPSConfiguration Get the tiPS module configuration. #> [CmdletBinding()] [OutputType([tiPS.Configuration])] Param() return $script:TiPSConfiguration } function Get-TiPSDataDirectoryPath { <# .SYNOPSIS Get the tiPS data directory path. .DESCRIPTION Get the tiPS data directory path where the tiPS module stores all of its data for the current user. .INPUTS None. You cannot pipe objects to the function. .OUTPUTS A [string] of the directory path. .EXAMPLE Get-TiPSDataDirectoryPath Get the tiPS data directory path. #> [CmdletBinding()] [OutputType([string])] Param() [string] $usersLocalAppDataPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData) [string] $appDataDirectoryPath = Join-Path -Path $usersLocalAppDataPath -ChildPath ( Join-Path -Path 'PowerShell' -ChildPath 'tiPS') return $appDataDirectoryPath } function Remove-TiPSImportFromPowerShellProfile { <# .SYNOPSIS Removes the tiPS Import-Module statement from the user's PowerShell profile file. .DESCRIPTION This function edits the user's PowerShell profile file to remove the Import-Module statement that is used to import the tiPS module. If the profile does not import the tiPS module, then no changes are made. Only the default PowerShell profile paths are searched to see if the tiPS module is imported; if it is imported from a dot-sourced script, the function will not detect the import statement and it will not be removed. This function will only remove the tiPS import statement added by the Add-TiPSImportToPowerShellProfile function. If you manually added the import statement to your profile, this function may not remove it. .INPUTS None. You cannot pipe objects to the function. .OUTPUTS None. The function does not return any objects. .EXAMPLE Remove-TiPSImportFromPowerShellProfile This example edits the PowerShell profile to remove the tiPS Import-Module statement. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([void])] Param() Process { [bool] $moduleImportStatementIsInProfile = Test-PowerShellProfileImportsTiPS if (-not $moduleImportStatementIsInProfile) { Write-Verbose "The PowerShell profiles do not import the tiPS module, so no changes are necessary." return } [string[]] $profileFilePathsThatExist = GetPowerShellProfileFilePathsThatExist [string[]] $importStatementsToRemoveFromProfile = @( GetImportStatementToAddToPowerShellProfile 'Import-Module -Name tiPS -Force' 'Import-Module -Name tiPS' 'Import-Module tiPS -Force' 'Import-Module tiPS' ) [bool] $atLeastOneProfileFileModified = $false foreach ($profileFilePath in $profileFilePathsThatExist) { [string] $fileContents = Get-Content -Path $profileFilePath -Raw foreach ($importStatement in $importStatementsToRemoveFromProfile) { [regex] $importLineRegex = '(?mi)' + # Enable multiline (match against newlines in the middle of the string) and case-insensitive matching. '^' + # Match against the beginning of the line. '\s*' + # Match any whitespace at the beginning of the line. $importStatement + # Match the import statement. '\s*' + # Match any whitespace at the end of the line. '$' # Match against the end of the line. if ($fileContents -match $importLineRegex) { if ($PSCmdlet.ShouldProcess("PowerShell profile '$profileFilePath'", 'Update')) { Write-Verbose "Removing '$($matches.Values)' from PowerShell profile '$profileFilePath'." [string] $updatedFileContents = $fileContents -replace $importLineRegex, '' Set-Content -Path $profileFilePath -Value $updatedFileContents -Force } $atLeastOneProfileFileModified = $true } } } if (-not $atLeastOneProfileFileModified) { Write-Warning "One of the PowerShell profiles does import the tiPS module, but not with the expected import statement. Run 'Test-PowerShellProfileImportsTiPS -Verbose' to see which profile files import the tiPS module, and then manually remove the statement from the file." } } } function Set-TiPSConfiguration { <# .SYNOPSIS Set the tiPS configuration. .DESCRIPTION Set the entire or partial tiPS configuration. .PARAMETER Configuration The tiPS configuration object to set. All configuration properties are updated to match the provided object. No other properties may be provided when this parameter is used. .PARAMETER AutomaticallyUpdateModule Whether to automatically update the tiPS module at session startup. The module update is performed in a background job, so it does not block the PowerShell session from starting. This also means that the new module version will not be used until the next time the module is imported, or the next time a PowerShell session is started. Old versions of the module are automatically deleted after a successful update. Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never. .PARAMETER AutomaticallyWritePowerShellTip Whether to automatically write a PowerShell tip at session startup. Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never. .PARAMETER TipRetrievalOrder The order in which to retrieve PowerShell tips. Valid values are NewestFirst, OldestFirst, and Random. Default is NewestFirst. .INPUTS You can pipe a [tiPS.Configuration] object containing the tiPS configuration to set, or a PSCustomObject with the individual properties to set (e.g. AutomaticallyUpdateModule and/or AutomaticallyWritePowerShellTip). .OUTPUTS None. The function does not return any objects. .EXAMPLE Set-TiPSConfiguration -Configuration $config Set the tiPS configuration. .EXAMPLE Set-TiPSConfiguration -AutomaticallyUpdateModule Weekly Set the tiPS configuration to automatically update the tiPS module every 7 days. .EXAMPLE Set-TiPSConfiguration -AutomaticallyWritePowerShellTip Daily Set the tiPS configuration to automatically write a PowerShell tip every day. .EXAMPLE Set-TiPSConfiguration -AutomaticallyUpdateModule Never -AutomaticallyWritePowerShellTip Never Set the tiPS configuration to never automatically update the tiPS module or write a PowerShell tip. .EXAMPLE Set-TiPSConfiguration -TipRetrievalOrder Random Set the tiPS configuration to retrieve PowerShell tips in random order. #> [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'PartialConfiguration')] [OutputType([void])] Param ( [Parameter(Mandatory = $true, ParameterSetName = 'EntireConfiguration', ValueFromPipeline = $true)] [ValidateNotNullOrEmpty()] [tiPS.Configuration] $Configuration, [Parameter(Mandatory = $false, ParameterSetName = 'PartialConfiguration', ValueFromPipelineByPropertyName = $true)] [tiPS.ModuleAutoUpdateCadence] $AutomaticallyUpdateModule = [tiPS.ModuleAutoUpdateCadence]::Never, [Parameter(Mandatory = $false, ParameterSetName = 'PartialConfiguration', ValueFromPipelineByPropertyName = $true)] [tiPS.WritePowerShellTipCadence] $AutomaticallyWritePowerShellTip = [tiPS.WritePowerShellTipCadence]::Never, [Parameter(Mandatory = $false, ParameterSetName = 'PartialConfiguration', ValueFromPipelineByPropertyName = $true)] [tiPS.TipRetrievalOrder] $TipRetrievalOrder = [tiPS.TipRetrievalOrder]::NewestFirst ) Process { # If the entire Configuration object parameter is passed in, set it and return. if ($PSBoundParameters.ContainsKey('Configuration')) { if ($PSCmdlet.ShouldProcess('tiPS configuration', 'Set')) { $script:TiPSConfiguration = $Configuration } } # If the AutomaticallyUpdateModule parameter is passed in, set it. if ($PSBoundParameters.ContainsKey('AutomaticallyUpdateModule')) { if ($PSCmdlet.ShouldProcess('tiPS configuration AutoUpdateCadence property', 'Set')) { $script:TiPSConfiguration.AutoUpdateCadence = $AutomaticallyUpdateModule } } # If the AutomaticallyWritePowerShellTip parameter is passed in, set it. if ($PSBoundParameters.ContainsKey('AutomaticallyWritePowerShellTip')) { if ($PSCmdlet.ShouldProcess('tiPS configuration AutoWritePowerShellTipCadence property', 'Set')) { $script:TiPSConfiguration.AutoWritePowerShellTipCadence = $AutomaticallyWritePowerShellTip } } # If the TipRetrievalOrder parameter is passed in, set it. if ($PSBoundParameters.ContainsKey('TipRetrievalOrder')) { if ($PSCmdlet.ShouldProcess('tiPS configuration TipRetrievalOrder property', 'Set')) { $script:TiPSConfiguration.TipRetrievalOrder = $TipRetrievalOrder } } Write-Debug "Saving the tiPS configuration to the configuration file." WriteConfigurationToFile -Config $script:TiPSConfiguration Write-Debug "Ensuring the user's PowerShell profile imports the tiPS module if their config expects it." [bool] $automaticActionsAreConfigured = $script:TiPSConfiguration.AutoUpdateCadence -ne [tiPS.ModuleAutoUpdateCadence]::Never -or $script:TiPSConfiguration.AutoWritePowerShellTipCadence -ne [tiPS.WritePowerShellTipCadence]::Never if ($automaticActionsAreConfigured) { [bool] $tiPSModuleIsImportedByPowerShellProfile = Test-PowerShellProfileImportsTiPS if (-not $tiPSModuleIsImportedByPowerShellProfile) { Write-Warning "tiPS can only perform automatic actions when it is imported into the current PowerShell session. Run 'Add-TiPSImportToPowerShellProfile' to update your PowerShell profile import tiPS automatically when a new session starts, or manually add 'Import-Module -Name tiPS' to your profile file. If you are importing the module in a different way, such as in a script that is dot-sourced into your profile, you can ignore this warning." } } } } function Test-PowerShellProfileImportsTiPS { <# .SYNOPSIS Tests whether the PowerShell profile imports the tiPS module. .DESCRIPTION Tests whether the PowerShell profile imports the tiPS module. Returns true if it finds an 'Import-Module -Name tiPS' statement in the profile, false otherwise. This only looks in the default PowerShell profile paths. If the tiPS module is imported from a dot-sourced file then this will return false. .INPUTS None. You cannot pipe objects to the function. .OUTPUTS System.Boolean representing if the tiPS module is imported by the PowerShell profile or not. .EXAMPLE Test-PowerShellProfileImportsTiPS Tests whether the PowerShell profile imports the tiPS module, returning true if it does and false otherwise. .EXAMPLE Test-PowerShellProfileImportsTiPS -Verbose Tests whether the PowerShell profile imports the tiPS module, returning true if it does and false otherwise. If true, the verbose output will list the profile file paths and the lines that import the tiPS module. If false, the verbose output will list the profile file paths that it checked. #> [CmdletBinding()] [OutputType([System.Boolean])] Param() [string[]] $profileFilePathsThatExist = GetPowerShellProfileFilePathsThatExist if ($null -eq $profileFilePathsThatExist -or $profileFilePathsThatExist.Count -eq 0) { Write-Verbose "No PowerShell profile files exist." return $false } [string] $requiredContentRegex = 'Import-Module\s.*tiPS' [Microsoft.PowerShell.Commands.MatchInfo] $results = Select-String -Path $profileFilePathsThatExist -Pattern $requiredContentRegex if ($null -ne $results) { Write-Verbose "The tiPS module is imported by the following profile lines:" $results | ForEach-Object { Write-Verbose " $($_.Path): $($_.Line)" } return $true } Write-Verbose "The tiPS module is not imported directly by any of the PowerShell profiles: $profileFilePathsThatExist" return $false } function Write-PowerShellTip { <# .SYNOPSIS Write a PowerShell tip to the terminal. .DESCRIPTION Write a PowerShell tip to the terminal. If no tip is specified, a random tip will be written. The tip is written to the terminal using the Write-Host cmdlet so that colours can be applied. Thus, the tip is not written to the pipeline and cannot be captured in a variable. If you want to capture the tip in a variable, use the Get-PowerShellTip function. .PARAMETER Id The ID of the tip to write. If not supplied, a random tip will be written. If no tip with the specified ID exists, an error is written. .INPUTS You can pipe a [string] of the ID of the tip to write, or a PSCustomObject with a [string] 'Id' property. .OUTPUTS None. The function does not return any objects. .EXAMPLE Write-PowerShellTip Write a random tip to the terminal. .EXAMPLE Write-PowerShellTip -Id '2023-07-16-powershell-is-open-source' Write the tip with the specified ID. #> [CmdletBinding()] [Alias('Tips')] [OutputType([void])] Param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = 'The ID of the tip to write. If not supplied, a random tip will be written.')] [string] $Id ) Process { [tiPS.PowerShellTip] $tip = Get-PowerShellTip -Id $Id if ($null -ne $tip) { WritePowerShellTipToTerminal -Tip $tip } } } Write-Debug 'Now that all types and functions are imported, initializing the module.' InitializeModule # Function and Alias exports are defined in the modules manifest (.psd1) file. |