[CmdletBinding(SupportsShouldProcess)]param( [switch]$WriteInitScript, [string]$CustomInitScriptPath, [switch]$sudo, [switch]$RemoveOnly, [switch]$ShowSkipped, [switch]$Force, [switch]$NoGitAction, [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts', [Parameter(DontShow)] [string]$My_PSScriptRoot = $MyPSScriptRoot, [switch]$NoReloadScripts ) function LoadProfileCodeFunctions { ####################################################################### ## ProfileCode function (source) will contain the code to be written ####################################################################### function ProfileCode_pre_common { # # If TMUX isn't loaded, start byobu, then exit this script (and current shell) # if ( -not ( Test-Path env:TMUX ) ) { byobu; exit } } ####################################################################### ## common Profile Code - receommended not to change this ## Code will be loaded from profile.d sub-directory after this. ####################################################################### function ProfileCode_post_common { # This loads the personal profile section from the $MyPSScriptRoot/profile.d directory Get-Command Reload-MyScripts -ErrorAction SilentlyContinue | ForEach-Object { . $_.Name } } ####################################################################### ## ProfileCode_common (source) containing mandatory code for $PROFILE ####################################################################### function ProfileCode_common {[CmdletBinding()]param() function Get-PowerShellPath {[CmdletBinding()]param() Get-Process -PID $PID | ForEach-Object { $_.Path, $_.Parent.Path } | Where-Object { $_ -match $('(?:powershell|pwsh)(?:.exe)?' + "`$") } | Select-Object -First 1 } function ConvertTo-Base64 { param([string]$String,[switch]$AsPowershellCommandLine) return "$(if($AsPowershellCommandLine){"$(Get-PowerShellPath) -EncodedCommand "} )$([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($String)))" } function Get-TempPath { #New-TemporaryFile | ForEach-Object { Remove-Item $_; $_.DirectoryName } [System.IO.Path]::GetTempPath() } class SessionData { static [hashtable]$Data = [ordered]@{} static [string]$Path = $null static [void] Init() { if( -not [SessionData]::Data.SessionID ) { if( Get-Command -Name Get-CimInstance -ErrorAction SilentlyContinue ) { [SessionData]::Data.SessionID = Get-CimInstance -ClassName Win32_Service -Filter 'State="Running" AND DisplayName like "%[_]%"' -Property 'DisplayName','Name' | Select-Object -ExpandProperty DisplayName | ForEach-Object { $_ -split '_' | Select-Object -Last 1 } | Where-Object { $_ -notin ('agent') } | Select-Object -First 1 } else { $local:p = Get-Process -PID $global:PID $local:t_p = $null do { $t_p = $null if( $p.Parent.Id ) { $t_p = Get-Process -Id $($p.Parent.Id) | Where-Object StartTime } if( $t_p ) { $p = $t_p } } while ($t_p) [SessionData]::Data.SessionID = "$( $p.StartTime.ToString("yyyyMMddHHmmss") )-$( $p.SessionId )" } [SessionData]::Data.CreateTimestamp = Get-Date [SessionData]::Path = Join-Path $(Get-TempPath) "session.store.$([SessionData]::Data.SessionID).xmlcli" } } static [string] GetSessionID() { if( -not [SessionData]::Data.SessionID ) { [SessionData]::Init() [SessionData]::Save() } return [SessionData]::Data.SessionID } static [void] Save() { if( -not [SessionData]::Path ) { [SessionData]::Init() } [SessionData]::Data | Export-Clixml -Path $([SessionData]::Path) } static [void] Load() { if( -not [SessionData]::Path ) { [SessionData]::Init() } if( Test-Path $([SessionData]::Path) ) { [SessionData]::Data = Import-Clixml -Path $([SessionData]::Path) } } static [void] Set([string]$Name,[object]$Value) { $null = [SessionData]::GetSessionID() [SessionData]::Data[$Name] = $Value [SessionData]::Save() } static [object] Get([string]$Name,[object]$DefaultValue=$null) { [SessionData]::Load() if( -not [SessionData]::Data.ContainsKey($Name) ) { if( $DefaultValue ) { [SessionData]::Set($Name,$DefaultValue) } else { return $DefaultValue } } return [SessionData]::Data[$Name] } } function Invoke-ExpressionEx { [CmdletBinding(SupportsShouldProcess)]param( [switch]$sudo, [switch]$Force, [string]$Function, [switch]$MarshalOutput, [string[]]$sudoArgs, [Parameter(Position = 1, ValueFromRemainingArguments = $true)] [string[]]$expr ) function do_sudo { [CmdletBinding()]param( [string]$sudo_cmd ) function do_sudo_win { [CmdletBinding()]param( [string]$sudo_cmd ) $local:sudo_exec = [string]::Empty if ( (Get-Command scoop -ErrorAction SilentlyContinue) ) { $sudo_exec = $(scoop which gsudo *>&1) if ( "$sudo_exec" -match 'not found' ) { $sudo_exec = $(scoop which sudo *>&1) } if ( "$sudo_exec" -match 'not found' ) { $sudo_exec = [string]::Empty } } if ( $sudo_exec ) { if( [bool]$WhatIfPreference.IsPresent ) { Write-Host "What if: using sudo [$sudo_exec] to run [$sudo_cmd]" } else { & $sudo_exec "$sudoArgs" $sudo_cmd } } else { Write-Error "Didn't find a known sudo command" } } if ( $PSVersionTable.Platform -eq 'Unix' ) { if( [bool]$WhatIfPreference.IsPresent ) { Write-Host "What if: passing encoded command to powershell [$sudo_cmd]" } else { if( -not $sudoArgs ) { $sudoArgs = @( "--preserve-env" ) } /usr/bin/env sudo $sudoArgs $(Get-PowerShellPath) "-noprofile" "-EncodedCommand" $(ConvertTo-Base64 $sudo_cmd) } } else { $local:currentIdenity = [System.Security.Principal.WindowsIdentity]::GetCurrent() if ( $currentIdenity.UserClaims.Value -contains 'S-1-5-32-544' ) { do_sudo_win $sudo_cmd } else { Write-Error "User [$($currentIdenity.Name)]does not have permissions to run as admin." } } } if ( $sudo -and -not $(Test-IsAdmin) ) { $local:tmpOutputFile = $null $local:tmpScriptFile = $null $local:sudo_cmd = "$($expr -join ' ')".Trim() if ( $Function ) { $tmpScriptFile = New-TemporaryFile -WhatIf:$false -Confirm:$false $tmpScriptFile = Move-Item $tmpScriptFile.FullName "$($tmpScriptFile.FullName).ps1" -PassThru -WhatIf:$false -Confirm:$false $local:tmpScriptContent = Export-FunctionSource $Function $sudo_cmd = "$Function $sudo_cmd" $tmpScriptContent | Out-File $tmpScriptFile.FullName -WhatIf:$false $expr = @( ".", $tmpScriptFile.FullName, ";", "`n$Function" ) + $expr } $expr = @( "`n`$WhatIfPreference=`$$WhatIfPreference;", "`n`$VerbosePreference='$VerbosePreference';", "`n`$ErrorActionPreference='$ErrorActionPreference';", "`n`$ConfirmPreference='$ConfirmPreference';`n" ) + $expr if ( $MarshalOutput ) { $tmpOutputFile = New-TemporaryFile -WhatIf:$false -Confirm:$false $expr += @( "| Export-CliXml -Path '$($tmpOutputFile.FullName)'" ) } $expr += @( ";`n exit `$LastExitCode" ) ## $tmpScriptContent += "$($expr -join ' ')".Trim() Write-Verbose "Performing the following command line via SUDO:`n{$sudo_cmd}" $sudo_cmd = "$($expr -join ' ')".Trim() do_sudo $sudo_cmd ## do_sudo 'Get-Content' $tmpScriptFile.FullName '|' 'Invoke-Expression' '|' 'Export-Clixml' '-Path' $tmpOutputFile.FullName #do_sudo $sudo_cmd if ( $tmpScriptFile ) { Remove-Item $tmpScriptFile -Confirm:$false } if ( $tmpOutputFile ) { Import-Clixml $tmpOutputFile; Remove-Item $tmpOutputFile -Confirm:$false } return } Write-Verbose "Perofrming the following expression in-line:`n{$($expr -join ' ')}" if( [bool]$WhatIfPreference.IsPresent ) { Write-Host "What if: Invoke-ExpressionEx executing [$expr]" } else { Invoke-Expression "$expr" } } function Test-IsAdmin { [CmdletBinding(ConfirmImpact = 'None')] [OutputType([bool])] param() switch ($true) { $( ($PSVersionTable.PSEdition -eq 'Desktop') -or ($PSVersionTable.Platform -eq 'Win32NT') ) { # Fastest way on Windows ([Security.Principal.WindowsPrincipal]( [Security.Principal.WindowsIdentity]::GetCurrent() )).IsInRole( [Security.Principal.WindowsBuiltInRole]'Administrator' ) } $( ($PSVersionTable.PSEdition -eq 'Core') -and ($PSVersionTable.Platform -eq 'Unix') ) { # On macOS and Linux we use ID to figure out if we run elevated (0 means superuser rights) return $((id -u) -eq 0) } default { # Unable to figure it out! Write-Warning -Message 'Unknown' return } } } function Export-FunctionSource { param( [switch]$NoHeader, [Parameter(Position = 0, ValueFromRemainingArguments = $true)] #[ValidateSet([Functions])] [string[]]$FunctionName ) $local:src = "" foreach ( $local:func in $FunctionName ) { $local:funcDef = $null foreach( $cmd in (Get-Command -All $func) ) { if( $cmd.CommandType -in @('Function','Filter') ) { $funcDef = $cmd.Definition break } elseif( -$funcDef -and ($cmd.CommandType -in @('Script','ExternalScript')) ) { $funcDef = Get-Content $cmd.Definition } } if( -not $funcDef ) { default { throw "Don't know how to handle $func" } } if ( -not $NoHeader ) { $src += "`nfunction $func {" } $src += "`n" $src += $funcDef if ( -not $NoHeader ) { $src += "`n}" } $src += "`n" } return $src.Trim() } # Are we running in Unix or Windows? $global:PathEnvDelimiter = $(if ( $PSVersionTable.Platform -match 'unix' ) { ':' } else { ';' }) function Split-PathEnv { param([string]$EnvPath) $EnvPath -split $PathEnvDelimiter } # Establish Module Path $global:MyPSModulePath = Split-PathEnv $env:PSModulePath | Where-Object { $_ -match "^$($(Resolve-Path ~) -replace '\\',"\\")" } | Where-Object { Test-Path $_ } | Get-Item | Sort-Object -Property LastWriteTime -Descending | Select-Object -ExpandProperty FullName -First 1 if ( -not $MyPSModulePath ) { $MyPSModulePath = $(Join-Path $(Join-Path $(Resolve-Path ~) 'powershell') 'Modules') $env:PSModulePath = "$MyPSModulePath$PathEnvDelimiter$env:PSModulePath" } if ( -not (Test-Path $MyPSModulePath) ) { New-Item -ItemType Directory -Path $MyPSModulePath -Force | Out-Null } Write-Verbose "`$MyPSModulePath = $(Get-Item $MyPSModulePath | Select-Object -ExpandProperty FullName)" # Establish local/personal Script Root and make sure it's in $env:Path if( -not $MyPSScriptRoot ) { if( $My_PSScriptRoot -and -not (Test-Path $My_PSScriptRoot) ) { throw "`$My_PSScriptRoot is defined ($My_PSScriptRoot) but path does NOT exist! stopping" } $local:My_PSScriptRoot = Join-Path (Split-Path -Parent $MyPSModulePath) Scripts Write-Verbose "My_PSScriptRoot = $My_PSScriptRoot" if ( -not (Test-Path $My_PSScriptRoot) ) { New-Item -ItemType Directory -Path $My_PSScriptRoot -Force | Out-Null New-Item -ItemType Directory -Path $(Join-Path $My_PSScriptRoot base) -Force | Out-Null New-Item -ItemType Directory -Path $(Join-Path $(Join-Path $My_PSScriptRoot base) profile.d) -Force | Out-Null } $global:MyPSScriptRoot = $My_PSScriptRoot } if( -not (Test-Path $MyPSScriptRoot) ) { throw "`$MyPSScriptRoot ($My_PSScriptRoot) path does NOT exist! stopping!" } $local:p1 = Split-PathEnv $env:PATH | Select-Object -Unique Write-Verbose "MyPSScriptRoot = $MyPSScriptRoot" $p2 = @($p1[0], $(Join-Path $MyPSScriptRoot base)) + $($p1 | Select-Object -Skip 1) | Select-Object -Unique $env:PATH = $p2 -join $PathEnvDelimiter Write-Verbose "PATH $(if( $p1.Count -eq $p2.Count ) { "modification" } else { "was modified"} )" Remove-Variable -Name "p1","p2","My_PSScriptRoot" -Confirm:$false } function ProfileCode_cleanup { [CmdletBinding()]param( [switch]$Full ) Remove-Item function:ProfileCode* -Confirm:$false if( $Full ) { Remove-Item function:LoadProfileCodeFunctions -Confirm:$false } } . ProfileCode_common } ####################################################################### ## _Init Logic - writing starts here ####################################################################### function _Init { [CmdletBinding(SupportsShouldProcess)]param( [switch]$WriteInitScript, [string]$CustomInitScriptPath, [switch]$sudo, [switch]$RemoveOnly, [switch]$ShowSkipped, [switch]$Force, [switch]$NoGitAction, [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts', [Parameter(DontShow)] [string]$My_PSScriptRoot = $MyPSScriptRoot, [switch]$NoReloadScripts ) $local:LoadMissing = ". LoadProfileCodeFunctions" if( Test-Path function:ProfileCode_cleanup ) { $LoadMissing = "#" } . Invoke-Expression $LoadMissing if ( -not $NoGitAction ) { if ( -not [bool]$(Get-Command git -ErrorAction SilentlyContinue) ) { throw "No git command found, you may either omit run with the -NoGitAction switch or install git and try again." } Push-Location $MyPSScriptRoot if( Test-Path $(Join-Path $MyPSScriptRoot '.git') ) { git pull } elseif ( $PSCmdlet.ShouldProcess("Pull git repo from $GitURL into $MyPSScriptRoot ?") ) { $local:tmpGitDir = New-TemporaryFile Remove-Item $tmpGitDir -Confirm:$false -Force New-Item -Type Directory $tmpGitDir.FullName | Out-Null Write-Verbose "Cloning git repo [$GitURL] into temporary location ('$($tmpGitDir.FullName)')..." $local:GitOutput = "$(& git clone $GitURL $tmpGitDir.FullName --no-checkout)" Write-Verbose $GitOutput Write-Verbose "Moving git repo from temp location to $MyPSScriptRoot . . ." Copy-Item -Force -Recurse (Join-Path $tmpGitDir.FullName .git) ./.git Remove-Item $tmpGitDir.FullName -Recurse -Force -Confirm:$false Write-Verbose "Checking out repo..." $GitOutput = "$(& git checkout --force)" Write-Verbose $GitOutput } Pop-Location Write-Verbose "Git operation done." } $local:ProfileSignature = [PSCustomObject]([ordered]@{ Begin = '#### SZ Auto Profile Setup - BEGIN ####' End = '#### SZ Auto Profile Setup - END ####' Removed = '#### SZ Auto Profile Setup - ----- ####' Match = '#### SZ Auto Profile Setup' }) function New-ProfileSetupStatus { param($Name, $Path, $Status, $Exist) return [PSCustomObject]([ordered]@{ Name = $Name Path = $Path Status = $Status Exist = $Exist }) } $local:_profiles = @() $local:_profile = $CustomInitScriptPath $local:randomSeed = "####$(Get-Random)####"; if( $_profile ) { $_profiles += ,$_profile } else { $_profiles = $PROFILE | Get-Member -Type NoteProperty | Select-Object -ExpandProperty Name $_profile = $profile.CurrentUserAllHosts; if ( $sudo -or $(Test-IsAdmin) ) { $_profile = $profile.AllUsersAllHosts } } Write-Verbose $( $_profiles | Out-String ) Write-Verbose $( $_profile | Out-String ) if ( $RemoveOnly -and $ConfirmPreference -eq 'High' -and (-not $PSBoundParameters -or -not $PSBoundParameters.ContainsKey('Confirm')) ) { $script:ConfirmPreference = 'Low' } $_profiles | Foreach-Object { $local:p = Invoke-Expression "`$profile.$_" if( -not $p ) { $p = $_ } $local:profileContent = $randomSeed; $local:exists = Test-Path $p $local:userFile = $($_ -match 'CurrentUser') $local:cleanupValue = '' if ( $exists ) { if ( (Get-Content -Path $p | Out-String ) -match $ProfileSignature.Match ) { Write-Verbose "Found code in [$_]'$p', preparing..." $local:gate = $true; $profileContent = Get-Content $p | ForEach-Object { if ( -not $gate ) { if ( $_ -match $ProfileSignature.End ) { $gate = $true; } } else { if ( $_ -match $ProfileSignature.Match ) { if ( $_ -match $ProfileSignature.Begin ) { $gate = $false; } $cleanupValue = $ProfileSignature.Removed return $randomSeed; } return $_ } } } else { $profileContent += "`n`n$(Get-Content $p -Raw)" } } if( -not( $profileContent -match $randomSeed ) ) { $profileContent = "$randomSeed`n$profileContent" } $local:insertProfileCode = $false; if ( ($p -eq $_profile) -and -not $RemoveOnly ) { $insertProfileCode = $true Write-Verbose "Inserting ProfileCode into $($_)..." $cleanupValue = $((@( '', $ProfileSignature.Begin, '', $(Export-FunctionSource -NoHeader ProfileCode_pre_common), '', '', $(Export-FunctionSource -NoHeader ProfileCode_common), '', '', $(Export-FunctionSource -NoHeader ProfileCode_post_common), '', $ProfileSignature.End ) -join "`n" ).Trim().Replace(('$' + '_'), ('$$' + '_'))) } $profileContent = $($profileContent -replace $randomSeed, $cleanupValue).Trim() $local:shouldSudo = $sudo -and -not $userFile $local:status = 'Skipped' if ( $VerbosePreference -eq 'Continue' ) { $ShowSkipped = $true } # Test if new profile Content will be empty if ( -not ([string]::IsNullOrWhiteSpace($profileContent)) ) { $status = "Need $(if($insertProfileCode){if($exists){'Setup'}else{'Init'}}else{'Cleanup'})" if ( $sudo -or $userFile ) { Write-Verbose "Writing content to $($_)..." $local:tmpOutput = New-TemporaryFile -WhatIf:$false -Confirm:$false $profileContent | Out-File $tmpOutput.FullName -WhatIf:$false -Confirm:$false if ( $shouldSudo -and $( $PSVersionTable.Platform -eq 'Unix' ) ) { # Make sure the file has the right permissions on a Unix machine Invoke-ExpressionEx -sudo:$shouldSudo chmod 'a+r' $tmpOutput.FullName } try { $local:errMsg = $(Invoke-ExpressionEx -Confirm:$false -sudo:$shouldSudo @" if( -not (Test-Path $(Split-Path -Parent $p)) ) { New-Item -Type Directory "$(Split-Path -Parent $p)" -Force }; `$null = Copy-Item "$($tmpOutput.FullName)" "$p" -Force:`$$Force -ErrorAction Stop; Remove-Item "$($tmpOutput.FullName)" -Force:`$$Force -ErrorAction SilentlyContinue "@ 2>&1) if ( $errMsg ) { Write-Error "$errMsg" $status = $status -replace 'Need ', 'Failed ' } else { $status = $status -replace 'Need ', '' } } catch { Write-Error "Writing $p failed!" } } else { #$status = $status -replace "Need ", '' -replace '^ ?','Need ' Write-Error "Cannot write into [$_]'$p'! Please re-run with sudo." } } elseif ( ($Force -or $RemoveOnly) -and $exists ) { $status = 'Need Removal' if ( $sudo -or $userFile ) { Write-Verbose "Removing [$_]'$p'..." try { Invoke-ExpressionEx -sudo:$shouldSudo Remove-Item $p if ( -not (Test-Path $p) ) { $status = 'Removed' } } catch { Write-Error "Removal of $p failed!" } } else { Write-Error "Cannot remove [$_]'$p'! Please re-run with sudo." } } if ( $status -ne 'Skipped' -or $ShowSkipped ) { if ( $shouldSudo -and $status -ne 'Skipped' ) { $status = "$status via sudo" } New-ProfileSetupStatus -Name $_ -Path $p -Status $status -Exist $(Test-Path $p) } } Get-Item function:_Init | Remove-Item -Confirm:$false . ProfileCode_cleanup -Full } . LoadProfileCodeFunctions if( -not $MyPSScriptRoot ) { throw '$MyPSScriptRoot does NOT exist!' } if( $WriteInitScript ) { _Init @PSBoundParameters } if( Test-Path function:ProfileCode_cleanup ) { . ProfileCode_cleanup } if( $NoReloadScripts ) { return } Get-Command Reload-MyScripts -ErrorAction SilentlyContinue | ForEach-Object { Write-Verbose "Reload-MyScripts" . $_.Name -Confirm:$false }