MyProfile.psm1
# References: # 1. Below are the list of predefined vars that can be used: # - $PSScriptRoot [System defined] The folder path for current scipt file, NOT the caller script to call this function . "$PSScriptRoot\Common.ps1" if (-not ("ddrr.PowerShell.MyProfileModule.MyProfile" -as [Type])) { Add-Type @" namespace ddrr.PowerShell.MyProfileModule { public class MyProfile { public string Name { get; set; } public int LoadOrder { get; set; } public string Path { get; set; } public bool Installed { get; set; } public bool AutoDetected { get; set; } } } "@ } ############################################################################### # MyProfile private functions ############################################################################### function SetupMyProfileModule { # Setup PSProfile.ps1, it will support the auto loading for PSProfile.ps1 for all of MyProfiles. InstallPsProfile -CodeId "34418a32-466b-456e-bc36-e8f6f71f465d" -ScriptBlock { # The entrance of the PSProfile for MyProfile if (-NOT [string]::IsNullOrEmpty($Env:MyProfileModuleDevPath)) { Write-Host "MyProfile module's DevMode is enabled: '$($Env:MyProfileModuleDevPath)'" -ForegroundColor Magenta Import-Module $Env:MyProfileModuleDevPath -Verbose } if (Get-Command Invoke-MyPSProfile -ErrorAction Ignore) { . Invoke-MyPSProfile } } # Setup environment vars: MyProfileBinPath, MyProfilePSModulePath Add-EnvironmentListVariableValues -Name 'Path' -ValuesToAppend "%$MyProfileBinPathVarName%" -Target User $currentUserPsModulesPath = Join-Path $([Environment]::GetFolderPath("MyDocuments")) "WindowsPowerShell\Modules" Add-EnvironmentListVariableValues -Name 'PSModulePath' -ValuesToAppend @($currentUserPsModulesPath,"%$MyProfilePSModulesPathVarName%") -Target User # Setup SysProfile.ps1 #$sysRunRegPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" #$sysProfileTrigger = "PowerShell -Executionpolicy Unrestricted -WindowStyle Hidden -File `"$MyProfileModuleSysProfilePath`"" #New-ItemProperty -Path $sysRunRegPath -Name "MyProfile" -Value $sysProfileTrigger -PropertyType String -Force | Out-Null Copy-Item $MyProfileModuleMyProfileShortcutPath -Destination $([Environment]::GetFolderPath("Startup")) -Force } <# .SYNOPSIS Check whether the specified path is a valid MyProfile. #> function Test-MyProfile { param( [Parameter(Position=0)] [string] $MyProfilePath ) # Check the path exists or not if ([string]::IsNullOrWhiteSpace($MyProfilePath)) { return $false } if (-NOT (Test-Path $MyProfilePath)) { return $false } # Check the manifest $manifestFilePath = Join-Path $MyProfilePath $MyProfileRelativeManifestPath if (-NOT(Test-Path $manifestFilePath)) { return $false } return $true } <# .SYNOPSIS Get the list of paths for my profile that has been installed. #> function Get-InstalledMyProfilePaths { return Get-EnvironmentListVariable -Name $MyProfileInstalledListVarName } <# .SYNOPSIS Get the list of auto detected my profile paths. #> function Get-AutoDetectedMyProfilePaths { $ret = @() # Default 1: SynologyDrive $ret += "$($env:USERPROFILE)\SynologyDrive\MyProfile" # Default 2: OneDrive - Personal $ret += "$($env:USERPROFILE)\OneDrive\MyProfile" # Default 3: OneDrive - <Business> Get-ChildItem -Path $env:USERPROFILE -Filter "OneDrive - *" | % { $ret += "$($env:USERPROFILE)\$($_.Name)\MyProfile" } return $ret } function Get-MyProfileManifest { param( [Parameter(Position=0)] [string] $MyProfilePath ) $manifestFilePath = Join-Path $MyProfilePath $MyProfileRelativeManifestPath return Import-PowerShellDataFile $manifestFilePath } function Install-MyToolbox { [CmdletBinding()] param( [Parameter(Position=0)] [string] $App, [string] $MyProfile, [switch] $Force ) } function Invoke-MyProfileSetups { param( [string] $MyProfilePath, [string] $SetupName = $null, [ValidateSet("Install", "Uninstall")] [string] $Operation = 'Install', [switch] $Force ) [string] $setupFolderPath = Join-Path $MyProfilePath $MyProfileRelativeSetupPath if (Test-Path $setupFolderPath) { # Note: If setups need to be ordered for some or all, then adjust logic here with logic below: # 1. Add a order file to list all the setups needs to be ordered # 2. The final order is the setup list in order file appends the setups not in the order file. [array] $setupList = Get-ChildItem -Path $setupFolderPath -Filter "*.ps1" -File | ? { $_.Name -ne 'Template.ps1' -AND ([string]::IsNullOrWhiteSpace($SetupName) -OR $_.Name -eq "$SetupName.ps1") } if ($setupList) { Write-ColorHost "Invoke $Operation operation on $($setupList.Count) setup(s):" -Level Highlight $setupList | % { try { $curSetupName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) Write-ColorHost " $Operation <Cyan>$($curSetupName.PadRight(60, ' '))</Cyan>" -NoNewLine # Invoke the setup operation & "$($_.VersionInfo.FileName)" -Operation $Operation -Force:$Force | Out-Null Write-ColorHost "<Green>Success</Green>" } catch { Write-ColorHost "<Red>Fail</Red>" Write-ColorHost "<Red>Failed with error</Red>: {$_}" -Level Debug } } } else { Write-ColorHost "No setup to invoke." -Level Warning } } } function Install-MyChocoApp { [CmdletBinding()] param( [Parameter(Position=0)] [string] $App, [switch] $Force ) # # Step 1: Install the Choco if not yet # if (!(Test-Path "$ENV:ChocolateyInstall\bin\choco.exe")){ Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) #Set-ExecutionPolicy Bypass -Scope Process -Force #Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) #$ENV:Path = "$ENV:ALLUSERSPROFILE\chocolatey\bin;$ENV:Path" } } function Import-MyModule { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Name, [string] $MyProfileName, [switch] $Force ) if ((-NOT [string]::IsNullOrWhiteSpace($Name)) -AND (Test-Path $Name)) { # Note: When $Name is a path to a module, will not check whether the corresponding MyProfile has been installed or not, as the MyProfile may not have been installed. try { Import-Module $Name -Global -Force:$Force -ErrorAction Stop if (-NOT [string]::IsNullOrWhiteSpace($MyProfileName)) { Write-ColorHost "[$MyProfileName] " -NoNewLine -Level Highlight } Write-ColorHost "Imported my module '$Name'" -Level Highlight } catch { Write-ColorHost "Failed to import my module '$Name'" -Level Error Write-Host $_ } } else { Get-MyProfile | ? { $_.Installed } | ? { [string]::IsNullOrWhiteSpace($MyProfileName) -OR (($_.Name -like $MyProfileName) -OR ($_.Path -eq $MyProfileName)) } | % { $curMyProfile = $_ $psModulesPath = Join-Path $_.Path $MyProfileRelativePSMoudlesPath if (Test-Path $psModulesPath -PathType Container) { Get-ChildItem $psModulesPath -Directory | ? { -NOT($_.Name -like "Template*") } | ? { [string]::IsNullOrWhiteSpace($Name) -OR ($_.Name -like $Name) } | % { Import-MyModule -Name (Join-Path $psModulesPath $_.Name) -MyProfileName $curMyProfile.Name -Force:$Force } } } } } ############################################################################### # MyProfile public functions ############################################################################### function New-MyProfile { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Path = '' ) if ([string]::IsNullOrWhiteSpace($Path)) { $Path = '.'} if (Test-Path $Path) { $Path = Resolve-Path $Path if ((Get-ChildItem $Path).Count -gt 0) { Write-ColorHost "The path for a new MyProfile must be a new path or an empty folder. Fail to create new MyProfile at '$Path'." -Level Error return } } else { New-Item -Path $Path -ItemType Directory | Out-Null } Copy-Item -Path "$MyProfileModuleTemplateRootPath\*" -Destination $Path Write-ColorHost "A new MyProfile has been created at '<Green>$Path</Green>'. You can update the <Green>$MyProfileRelativeManifestPath</Green> file to setup the details." -Level Highlight } <# .SYNOPSIS Get specified profile(s). .DESCRIPTION If nothing is specifed in parameter "Identity", return all visible profiles (auto detectable profiles + installed profiles) If profile name is specified in parameter "Identity", find the profile match with the name from all visible profiles (auto detectable profiles + installed profiles) If profile path is specified in parameter "Identity", try to directly get the profile in that path .PARAMETER Identity The profile name (support wildcard), or the profile path. Optional. #> function Get-MyProfile { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Identity ) [array]$installedPathList = Get-InstalledMyProfilePaths [array]$autoDetectedPathList = Get-AutoDetectedMyProfilePaths $isPathMode = $false if ((-NOT [string]::IsNullOrWhiteSpace($Identity)) -AND (Test-Path $Identity)) { [array]$profilePathList = @($Identity) $isPathMode = $true } else { [array]$profilePathList = $autoDetectedPathList + $installedPathList } [array]$profiles = $profilePathList | ? { Test-MyProfile $_ } | % { (Resolve-Path $_).Path.TrimEnd('\') } | Sort-Object -Unique | % { $curManifest = Get-MyProfileManifest $_ if ($isPathMode -OR ([string]::IsNullOrWhiteSpace($Identity) -OR ($curManifest.Name -like $Identity))) { $curProfile = New-Object ddrr.PowerShell.MyProfileModule.MyProfile $curProfile.Name = if ([string]::IsNullOrWhiteSpace($curManifest.Name)){ $_ } else { $curManifest.Name } $curProfile.LoadOrder = $curManifest.LoadOrder $curProfile.Path = $_ $curProfile.Installed = $installedPathList -contains $_ $curProfile.AutoDetected = $autoDetectedPathList -contains $_ $curProfile } } return $profiles | Sort-Object LoadOrder,Path } <# .SYNOPSIS Install specified profile(s). .DESCRIPTION If nothing is specifed in parameter "Identity", install all visible profiles (auto detectable profiles + installed profiles) If profile name is specified in parameter "Identity", install the profile match with the name from all visible profiles (auto detectable profiles + installed profiles) If profile path is specified in parameter "Identity", install the profile in that path .PARAMETER Identity The profile name (support wildcard), or the profile path. Optional. #> function Install-MyProfile { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Identity, [switch] $Force ) # Make sure this module has been setup (hooked to system). SetupMyProfileModule # Build the profile list [array]$profileList = Get-MyProfile [bool]$isPathMode = $false if ((-NOT [string]::IsNullOrWhiteSpace($Identity)) -AND (Test-Path $Identity)) { $pathProfile = Get-MyProfile $Identity | Select-Object -First 1 if ($null -eq $pathProfile) { # Directly return as the specified profile does not exists Write-ColorHost "Skip to install the invalid profile `"$Identity`"" -Level Warning return } $isPathMode = $true # Update to the normalized path $Identity = $pathProfile.Path # Only adjust the profileList when pathProfile is a new profile to avoid duplicate profile in the list. if (-NOT $pathProfile.Installed) { $profileList = $profileList + $pathProfile | ? { $_.Installed -OR $_ -eq $pathProfile } | Sort-Object LoadOrder,Path } } else { $profileList = $profileList | ? { [string]::IsNullOrWhiteSpace($Identity) -OR $_.Name -like $Identity } } $profilePaths = @() $binPaths = @() $psModulesPaths = @() $profileList | % { if ((-NOT $_.Installed) -OR $Force) { # # Only does these things for the profiles that specified to install. # Write-ColorHost "Installing $($_.Name)" -Level Keynote # Invoke PSModules for current MyProfile after it is installed. # Note: Although the PSProfile is also invoked here, but the behavior may not match with run them on PS start as there may have dependency between PSProfiles. Invoke-MyProfile $_.Path -PSModules -PSProfile -PSCmdlets # Run all setups to install other things, e.g.: toolbox Invoke-MyProfileSetups -MyProfilePath $_.Path -Operation Install -Force:$Force } # # Do below things for all installed/installing profiles # # Register MyProfile path. This will support PowerShell Profile, System Profile, Cron $profilePaths += $_.Path # Register Bin path $binPaths += Join-Path $_.Path $MyProfileRelativeBinPath # Register PowerShell Modules $psModulesPaths += Join-Path $_.Path $MyProfileRelativePSMoudlesPath } # Save paths to environment variables Set-EnvironmentListVariable -Name $MyProfileInstalledListVarName -ListValue $profilePaths -Target User Set-EnvironmentListVariable -Name $MyProfileBinPathVarName -ListValue $binPaths -Target User Set-EnvironmentListVariable -Name $MyProfilePSModulesPathVarName -ListValue $psModulesPaths -Target User # Update related environment variables Set-EnvironmentVariable -Name "PSModulePath" -Target Process -Value ([Environment]::ExpandEnvironmentVariables($ENV:PSModulePath)) Set-EnvironmentVariable -Name "Path" -Target Process -Value ([Environment]::ExpandEnvironmentVariables($ENV:Path)) } function Uninstall-MyProfile { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Identity ) $profilePaths = @() $binPaths = @() $psModulesPaths = @() Get-MyProfile | ? { $_.Installed } | % { if ([string]::IsNullOrWhiteSpace($Identity) -OR ($_.Name -like $Identity) -OR ($_.Path -eq $Identity)) { # # Only does these things for the profiles that specified to install. # Write-ColorHost "Uninstalling $($_.Name)" -Level Keynote # Unimport PSModules for current MyProfile after it is uninstalled. # Run all setups to uninstall other things, e.g.: toolbox Invoke-MyProfileSetups -MyProfilePath $_.Path -Operation Uninstall } else { # # Do below things for all rest installed profiles # # Register MyProfile path. This will support PowerShell Profile, System Profile, Cron $profilePaths += $_.Path # Register Bin path $binPaths += Join-Path $_.Path $MyProfileRelativeBinPath # Register PowerShell Modules $psModulesPaths += Join-Path $_.Path $MyProfileRelativePSMoudlesPath } } # Save paths to environment variables Set-EnvironmentListVariable -Name $MyProfileInstalledListVarName -ListValue $profilePaths -Target User Set-EnvironmentListVariable -Name $MyProfileBinPathVarName -ListValue $binPaths -Target User Set-EnvironmentListVariable -Name $MyProfilePSModulesPathVarName -ListValue $psModulesPaths -Target User # Update related environment variables Set-EnvironmentVariable -Name "PSModulePath" -Target Process -Value ([Environment]::ExpandEnvironmentVariables($ENV:PSModulePath)) Set-EnvironmentVariable -Name "Path" -Target Process -Value ([Environment]::ExpandEnvironmentVariables($ENV:Path)) } function Invoke-MyProfile { [CmdletBinding()] param( [Parameter(Position=0)] [string] $Identity, [switch] $SysProfile, [switch] $PSProfile, [switch] $PSModules, [switch] $PSCmdlets ) if ((-NOT $SysProfile) -AND (-NOT $PSProfile) -AND (-NOT $PSModules) -AND (-NOT $PSCmdlets)) { $SysProfile = $true $PSProfile = $true $PSModules = $true $PSCmdlets = $true } $sysProfiles = @() # Note, only allow to invoke Installed MyProfile. Get-MyProfile $Identity | ? { $_.Installed } | % { if ($PSCmdlets) { $cmdletsModuleName = "MyCmdlets_$($_.Name)" $cmdletsPath = Join-Path $_.Path $MyProfileRelativeCmdletsPath $cmdletModule = New-Module -Name $cmdletsModuleName -ArgumentList @($cmdletsPath) -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "*.ps1" | ? { -NOT ($_.Name -like "Template*") } | % { $fileName = $_.Name.Trim() $cmdletName = $fileName.Substring(0, $fileName.Length - 4) $cmdletScript = $_.FullName Set-Alias -Name $cmdletName -Value $cmdletScript -Option AllScope -Scope Global Export-ModuleMember -Alias $cmdletName Write-ColorHost "Export alias <Green>$cmdletName</Green> for $cmdletScript" -Level Verbose } } Import-Module $cmdletModule -Scope Global } if ($PSProfile) { $curPSProfilePath = Join-Path $_.Path $MyProfileRelativePSProfilePath # Note, use operator '.' here to invoke all MyPrfile's PowerShell profiles in the same scope of the MyProfile's PSProfile. # So, these PowerShell profiles can impact each other, and the MyProfile load order is important. . InvokeScriptInMyProfileModuleCallerScope $curPSProfilePath } if ($PSModules) { Import-MyModule -MyProfileName $_.Path } # For SysProfile, it should not impact current PS scope, so they will not be invoked via operator '.' # But similar to PSProfiles, all SysProfiles should share the same scope. So they will be not invoked here. Instead, they will be invoked together later. # This is also consistent with the normal behavior when system startup: SysProfiles are invoked after all PSProfiles are invoked. if ($SysProfile) { $sysProfiles += Join-Path $_.Path $MyProfileRelativeSysProfilePath } } # NOT use operator '.' here to avoid all MyProfile's system profiles impact current PS scope. # But these SysProfiles will share the same scope, so the MyProfile load order is important. if ($sysProfiles.Count -gt 0){ InvokeScriptInMyProfileModuleCallerScope $sysProfiles } } <# .SYNOPSIS The entrance when invoking from PowerShell profile. Note: the signature should NOT be changed. Otherwise, there will be upgrade issue. #> function Invoke-MyPSProfile { try { # For cmdlets script based module, PowerShell cannot auto detect the updates of cmdlets. # Manually import them when PowerShell start to make sure the latest cmdlets are available. # Use dot operator here to prompt the inner scope. . Invoke-MyProfile -PSProfile -PSCmdlets } catch { Write-Host "Failed to run MyPSProfile with error:" -ForegroundColor Red Write-Host $_ } } <# .SYNOPSIS The entrance when invoking from system startup (sys profile). Note: the signature should NOT be changed. Otherwise, there will be upgrade issue. #> function Invoke-MySysProfile { Invoke-MyProfile -SysProfile } function Show-MyProfileHelp { Get-MyProfile $Identity | ? { $_.Installed } | % { $curProfile = $_ $helpScriptPath = Join-Path $curProfile.Path $MyProfileRelativeHelpPath if (Test-Path $helpScriptPath) { try { Write-ColorHost "[$($curProfile.Name)]" -Level Keynote & "$helpScriptPath" Write-ColorHost } catch { Write-ColorHost "Failed to show help from '<Cyan>$($curProfile.Name)</Cyan>' with error $_" -Level Error } } } } ############################################################################### # Alias ############################################################################### Set-Alias -Name myhelp -Value Show-MyProfileHelp |