From ae07e06c274a432fd8bcb92ee750574583e00678 Mon Sep 17 00:00:00 2001 From: lksz Date: Fri, 25 Sep 2020 01:04:05 -0400 Subject: [PATCH] Setup-Profile and co. Setup-Profile included, read all about it at https://blog.lksz.me/the-profile-and-setting-it-up When running Edit-MyScript without parameters, the $MyPSScriptRoot path will be sent (editing the directory) Added ability to edit scripts with Edit-MyPrfiles, making editing profiles alongside scripts easy. --- .gitignore | 1 + Edit-MyProfiles.ps1 | 21 ++- Edit-MyScript.ps1 | 4 + Edit-TextFile.ps1 | 25 --- Export-FunctionSource.ps1 | 16 -- Setup-Profile.ps1 | 328 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 43 deletions(-) delete mode 100644 Edit-TextFile.ps1 delete mode 100644 Export-FunctionSource.ps1 create mode 100644 Setup-Profile.ps1 diff --git a/.gitignore b/.gitignore index fb412a6..18b2acb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ profile.d/env.ps1 +*.local.ps1 diff --git a/Edit-MyProfiles.ps1 b/Edit-MyProfiles.ps1 index 4bfa455..984bf3c 100644 --- a/Edit-MyProfiles.ps1 +++ b/Edit-MyProfiles.ps1 @@ -1,6 +1,23 @@ [CmdletBinding(SupportsShouldProcess)]param( [switch]$sudo, - [switch]$Force + [switch]$Force, + [Parameter(ValueFromRemainingArguments = $true)] + [ArgumentCompleter({ param ( + $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameters + ) + [MyScript]::_GetValidValues($wordToComplete,$false) + })] + [string[]]$AlsoEditTheseScripts ) -Edit-TextFile -sudo:$sudo $( Get-Profiles | Where-Object { $_.Exists -or $Force } | Select-Object -ExpandProperty Path ) +$local:FilePaths = @() + $( Get-Profiles | Where-Object { $_.Exists -or $Force } | Select-Object -ExpandProperty Path ) +foreach( $local:p in $AlsoEditTheseScripts ) { + $local:sp = Join-Path $MyPSScriptRoot "$p`.ps1" + $FilePaths += $sp +} + +Edit-TextFile -sudo:$sudo $FilePaths diff --git a/Edit-MyScript.ps1 b/Edit-MyScript.ps1 index 3e631ec..b59c006 100644 --- a/Edit-MyScript.ps1 +++ b/Edit-MyScript.ps1 @@ -41,6 +41,10 @@ foreach( $local:p in $ScriptName ) { $ScriptPaths += $sp } +if( -not $ScriptPaths ) { + $ScriptPaths += $MyPSScriptRoot +} + Edit-TextFile $ScriptPaths Get-Command Reload-MyScripts -ErrorAction SilentlyContinue | diff --git a/Edit-TextFile.ps1 b/Edit-TextFile.ps1 deleted file mode 100644 index 54660a5..0000000 --- a/Edit-TextFile.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -[CmdletBinding(SupportsShouldProcess)]param( - [switch]$sudo, - [Parameter(Position = 0, ValueFromRemainingArguments = $true)] - [string[]]$Path -) - -$local:editor = '/bin/nvim' -if( (Test-Path env:EDITOR) -and (Test-Path $env:EDITOR) ) { - $editor = $env:EDITOR; -} - -if( $editor -match 'vim?$' ) { - $editor += ' -p' -} - -if( $sudo ) { - $editor = "sudo $editor" -} - -$local:arguments = $Path -join "' '" -if( $Path ) { $arguments = "'$arguments'" } - -if( $PSCmdlet.ShouldProcess( "Edit ($editor): $arguments" ) ) { - Invoke-Expression "$editor $arguments" -} diff --git a/Export-FunctionSource.ps1 b/Export-FunctionSource.ps1 deleted file mode 100644 index 29cb4c4..0000000 --- a/Export-FunctionSource.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -param( - [switch]$NoHeader, - [Parameter(Position = 0, ValueFromRemainingArguments = $true)] - #[ValidateSet([Functions])] - [string[]]$FunctionName - -) - -$local:src = "" -foreach( $local:func in $FunctionName ) { - if( -not $NoHeader ) { $src += "`nfunction $func {" } - $src += "`n $((Get-Command -Type Function $func).Definition)" - if( -not $NoHeader ) { $src += "`n}" } - $src += "`n" -} -return $src.Trim() diff --git a/Setup-Profile.ps1 b/Setup-Profile.ps1 new file mode 100644 index 0000000..56477d0 --- /dev/null +++ b/Setup-Profile.ps1 @@ -0,0 +1,328 @@ +[CmdletBinding(SupportsShouldProcess)]param( + [switch]$sudo, + [switch]$RemoveOnly, + [switch]$ShowSkipped, + [switch]$Force, + [switch]$GitClone, + [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts', + [Parameter(DontShow)] + [string]$MyPSScriptRoot = $MyPSScriptRoot +) + +function _Setup { +[CmdletBinding(SupportsShouldProcess)]param( + [switch]$sudo, + [switch]$RemoveOnly, + [switch]$ShowSkipped, + [switch]$Force, + [switch]$GitClone, + [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts', + [Parameter(DontShow)] + [string]$MyPSScriptRoot = $MyPSScriptRoot) + + +####################################################################### +## 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 } +} + +function ProfileCode_common { +function Get-PowerShellPath { + Get-Process -PID $PID | ForEach-Object { $_.Path,$_.Parent.Path } | Where-Object { $_ -match 'powershell|pwsh' } | Select-Object -First 1 +} + +function ConvertTo-Base64 { +param([string]$String) + return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($String)) +} + +function Invoke-ExpressionEx() { +[CmdletBinding(SupportsShouldProcess)]param( + [switch]$sudo, + [switch]$Force, + [string]$Function, + [Parameter(Position = 1, ValueFromRemainingArguments = $true)] + [string[]]$expr +) + + if( $sudo -and -not $(Test-IsAdmin) ) { + $local:tmpFile = $null + if( $Function ) { + $tmpFile = New-TemporaryFile -WhatIf:$false + $tmpFile = Move-Item $tmpFile.FullName "$($tmpFile.FullName).ps1" -PassThru -WhatIf:$false + Export-FunctionSource $Function | Out-File $tmpFile.FullName -WhatIf:$false + $expr = @( ".", $tmpFile.FullName, ";", $Function ) + $expr + } + + $expr = @( + '$WhatIfPreference', '=', "`$$WhatIfPreference", ';', + '$VerbosePreference', '=', "'$VerbosePreference'", ";", + '$ErrorActionPreference', '=', "'$ErrorActionPreference'", ";", + '$ConfirmPreference', '=', "'$ConfirmPreference'", ";" + ) + $expr + + $local:sudo_params = $expr -join ' ' + Write-Verbose "Perofrming the following command line via SUDO:`n{$sudo_params}" + $local:base64command = ConvertTo-Base64 $sudo_params + /usr/bin/env sudo $(Get-PowerShellPath) -EncodedCommand $base64command + if( $tmpFile ) { Remove-Item $tmpFile } + return + } + + Write-Verbose "Perofrming the following expression in-line:`n{$($expr -join ' ')}" + 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 ) { + if( -not $NoHeader ) { $src += "`nfunction $func {" } + $src += "`n" + $src += $((Get-Command -Type Function $func).Definition) ### .Replace(('$'+'_'),('$$'+'_')) + 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 '\\',"\\")" } + +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 $(Get-Item $MyPSModulePath | Select-Object -ExpandProperty FullName) + +# Establish local/personal Script Root and make sure it's in $env:Path +$local:p = Split-PathEnv $env:PATH +$MyPSScriptRoot = Join-Path (Split-Path -Parent $MyPSModulePath) Scripts +Write-Verbose "MyPSScriptRoot = $MyPSScriptRoot" +if( -not (Test-Path $MyPSScriptRoot) ) { + New-Item -ItemType Directory -Path $MyPSScriptRoot -Force | Out-Null + New-Item -ItemType Directory -Path $MyPSScriptRoot/profile.d -Force | Out-Null +} +$global:MyPSScriptRoot = $MyPSScriptRoot + +$p = @($p[0], $MyPSScriptRoot) + $($p | Select-Object -Skip 1) +$env:PATH = $p -join $PathEnvDelimiter +} +####################################################################### +## Setup-Profile code continues below +####################################################################### + +if ( -not $MyPSScriptRoot -or (Test-Path function:Test-IsAdmin,function:ConvertTo-Base64,function:Invoke-ExpressionEx,function:Get-PowerShellPath,function:Export-FunctionSource | Where-Object { -not $_ } | Measure-Object | Select-Object -ExpandProperty Count) ) { + Write-Verbose "Calling ProfileCode inline..." + . ProfileCode_common +} + +if ( $GitClone -and -not (Test-Path $(Join-Path $MyPSScriptRoot '.git')) ) { + if( -not [bool]$(Get-Command git -ErrorAction SilentlyContinue) ) { + throw "No git command found, you may either omit the -GitClone switch or install git and try again." + } + if( $PSCmdlet.ShouldProcess("Pull git repo from $GitURL into $MyPSScriptRoot ?") ) { + git clone $GitURL $MyPSScriptRoot + } +} + +# if( $sudo -and -not $(Test-IsAdmin) ) { +# $local:sudo_args = @('-MyPSScriptRoot',$MyPSScriptRoot) +# $ScriptPSBoundParams.Keys | Where-Object { +# ($ScriptPSBoundParams[$_] -is [Switch]) -and $ScriptPSBoundParams[$_] -and ($_ -notin ('sudo','GitClone','GitURL')) +# } | ForEach-Object { $sudo_args += "-$_" } +# +# return Invoke-ExpressionEx -sudo -Function 'Setup-ProfileInternal' $sudo_args +# } + +$local:ProfileSignature = [PSCustomObject]([ordered]@{ + Begin = '#### SZ Auto Profile Setup - BEGIN ####' + End = '#### SZ Auto Profile Setup - END ####' +}) + +function New-ProfileSetupStatus{ +param($Name,$Path,$Status,$Exist) + return [PSCustomObject]([ordered]@{ + Name = $Name + Path = $Path + Status = $Status + Exist = $Exist + }) +} + +$local:_profiles= $PROFILE | fl * -Force | Out-String -Stream | ForEach-Object { + $local:p = $($_ -split '( : /)|(:\\)'); + if( $p[0] -match 'User' ) { + $p[0].Trim() + } + } +$local:_profile = $profile.CurrentUserAllHosts; +$local:randomSeed = "####$(Get-Random)####"; +$local:written = $false; + +if( $RemoveOnly -and $ConfirmPreference -eq 'High' -and (-not $PSBoundParameters -or -not $PSBoundParameters.ContainsKey('Confirm')) ) { + $script:ConfirmPreference = 'Low' +} + + +if( $sudo -or $(Test-IsAdmin) ) { $_profile = $profile.AllUsersAllHosts } +$_profiles | Foreach-Object { + $local:p = Invoke-Expression "`$profile.$_" + $local:profileContent = $randomSeed; + $local:exists = Test-Path $p + $local:userFile = $($_ -match 'CurrentUser') + $local:cleanupValue = '' + if ( $exists -and ((Get-Content -Path $p | Out-String ) -match $ProfileSignature.Begin ) ) { + 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.Begin ) { + $gate = $false; + $cleanupValue = ' ' + return $randomSeed; + } + return $_ + } + } + } + + if( ($p -eq $_profile) -and -not $RemoveOnly ) { + Write-Verbose "Inserting ProfileCode into $($_)..." + $profileContent = $profileContent -replace $randomSeed,$((@( + '', + $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 } + if( -not ([string]::IsNullOrWhiteSpace($profileContent)) ) { + $status = "Need $(if($cleanupValue){'Setup'}else{'Cleanup'})" + if( $sudo -or $userFile ) { + Write-Verbose "Writing content to $($_)..." + $local:tmpOutput = New-TemporaryFile -WhatIf:$false + $profileContent | Out-File $tmpOutput.FullName -WhatIf:$false + if( $shouldSudo -and $( $PSVersionTable.Platform -eq 'Unix' ) ) { + Invoke-ExpressionEx -sudo:$shouldSudo chmod 'a+r' $tmpOutput.FullName + } + try { + $local:errMsg = $(Invoke-ExpressionEx -sudo:$shouldSudo "Move-Item $($tmpOutput.FullName) $p -Force:`$$Force -ErrorAction Stop | Out-Null" 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 = "Need $status" + 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) + } +} + +Remove-Item function:ProfileCode* -Confirm:$false +} + +if( -not $SetupFromWeb ) { + _setup @PSBoundParameters + Get-Item function:_setup | Remove-Item +} +Remove-Variable SetupFromWeb