Script directory for a PowerShell profile. https://blog.lksz.me/keeping-track-of-everything/#-mypsscriptroot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
17 KiB

  1. [CmdletBinding(SupportsShouldProcess)]param(
  2. [switch]$sudo,
  3. [switch]$RemoveOnly,
  4. [switch]$ShowSkipped,
  5. [switch]$Force,
  6. [switch]$NoGitAction,
  7. [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts',
  8. [Parameter(DontShow)]
  9. [string]$My_PSScriptRoot = $MyPSScriptRoot
  10. )
  11. function _Setup {
  12. [CmdletBinding(SupportsShouldProcess)]param(
  13. [switch]$sudo,
  14. [switch]$RemoveOnly,
  15. [switch]$ShowSkipped,
  16. [switch]$Force,
  17. [switch]$NoGitAction,
  18. [string]$GitURL = 'https://code.lksz.me/lksz/PowerShell_Scripts',
  19. [Parameter(DontShow)]
  20. [string]$My_PSScriptRoot = $MyPSScriptRoot)
  21. #######################################################################
  22. ## ProfileCode function (source) will contain the code to be written
  23. #######################################################################
  24. function ProfileCode_pre_common {
  25. # # If TMUX isn't loaded, start byobu, then exit this script (and current shell)
  26. # if ( -not ( Test-Path env:TMUX ) ) { byobu; exit }
  27. }
  28. #######################################################################
  29. ## common Profile Code - receommended not to change this
  30. ## Code will be loaded from profile.d sub-directory after this.
  31. #######################################################################
  32. function ProfileCode_post_common {
  33. # This loads the personal profile section from the $MyPSScriptRoot/profile.d directory
  34. Get-Command Reload-MyScripts -ErrorAction SilentlyContinue | ForEach-Object { . $_.Name }
  35. }
  36. #######################################################################
  37. ## _Setup Logic starts here
  38. #######################################################################
  39. . ProfileCode_common
  40. if( -not $MyPSScriptRoot ) { throw '$MyPSScriptRoot does NOT exist!' }
  41. if ( -not $NoGitAction ) {
  42. if ( -not [bool]$(Get-Command git -ErrorAction SilentlyContinue) ) {
  43. throw "No git command found, you may either omit run with the -NoGitAction switch or install git and try again."
  44. }
  45. Push-Location $MyPSScriptRoot
  46. if( Test-Path $(Join-Path $MyPSScriptRoot '.git') ) {
  47. git pull
  48. } elseif ( $PSCmdlet.ShouldProcess("Pull git repo from $GitURL into $MyPSScriptRoot ?") ) {
  49. $local:tmpGitDir = New-TemporaryFile
  50. Remove-Item $tmpGitDir
  51. New-Item -Type Directory $tmpGitDir.FullName | Out-Null
  52. Write-Verbose "Cloning git repo [$GitURL] into temporary location ('$($tmpGitDir.FullName)')..."
  53. $local:GitOutput = "$(& git clone $GitURL $tmpGitDir.FullName --no-checkout)"
  54. Write-Verbose $GitOutput
  55. Write-Verbose "Moving git repo from temp location to $MyPSScriptRoot . . ."
  56. Copy-Item -Recurse (Join-Path $tmpGitDir.FullName .git) ./.git
  57. Remove-Item $tmpGitDir.FullName
  58. Write-Verbose "Checking out repo..."
  59. $GitOutput = "$(& git checkout --force)"
  60. Write-Verbose $GitOutput
  61. }
  62. Pop-Location
  63. Write-Verbose "Git operation done."
  64. }
  65. $local:ProfileSignature = [PSCustomObject]([ordered]@{
  66. Begin = '#### SZ Auto Profile Setup - BEGIN ####'
  67. End = '#### SZ Auto Profile Setup - END ####'
  68. Removed = '#### SZ Auto Profile Setup - ----- ####'
  69. Match = '#### SZ Auto Profile Setup'
  70. })
  71. function New-ProfileSetupStatus {
  72. param($Name, $Path, $Status, $Exist)
  73. return [PSCustomObject]([ordered]@{
  74. Name = $Name
  75. Path = $Path
  76. Status = $Status
  77. Exist = $Exist
  78. })
  79. }
  80. $local:_profiles = $PROFILE | fl * -Force | Out-String -Stream | ForEach-Object {
  81. $local:p = $($_ -split ' : [/A-Z\\]');
  82. if ( $p[0] -match 'User' ) {
  83. $p[0].Trim()
  84. }
  85. }
  86. $local:_profile = $profile.CurrentUserAllHosts;
  87. $local:randomSeed = "####$(Get-Random)####";
  88. if ( $RemoveOnly -and $ConfirmPreference -eq 'High' -and (-not $PSBoundParameters -or -not $PSBoundParameters.ContainsKey('Confirm')) ) {
  89. $script:ConfirmPreference = 'Low'
  90. }
  91. if ( $sudo -or $(Test-IsAdmin) ) { $_profile = $profile.AllUsersAllHosts }
  92. $_profiles | Foreach-Object {
  93. $local:p = Invoke-Expression "`$profile.$_"
  94. $local:profileContent = $randomSeed;
  95. $local:exists = Test-Path $p
  96. $local:userFile = $($_ -match 'CurrentUser')
  97. $local:cleanupValue = ''
  98. if ( $exists ) {
  99. if ( (Get-Content -Path $p | Out-String ) -match $ProfileSignature.Match ) {
  100. Write-Verbose "Found code in [$_]'$p', preparing..."
  101. $local:gate = $true;
  102. $profileContent = Get-Content $p | ForEach-Object {
  103. if ( -not $gate ) {
  104. if ( $_ -match $ProfileSignature.End ) {
  105. $gate = $true;
  106. }
  107. }
  108. else {
  109. if ( $_ -match $ProfileSignature.Match ) {
  110. if ( $_ -match $ProfileSignature.Begin ) {
  111. $gate = $false;
  112. }
  113. $cleanupValue = $ProfileSignature.Removed
  114. return $randomSeed;
  115. }
  116. return $_
  117. }
  118. }
  119. } else {
  120. $profileContent += "`n`n$(Get-Content $p -Raw)"
  121. }
  122. }
  123. if( -not( $profileContent -match $randomSeed ) ) { $profileContent = "$randomSeed`n$profileContent" }
  124. $local:insertProfileCode = $false;
  125. if ( ($p -eq $_profile) -and -not $RemoveOnly ) {
  126. $insertProfileCode = $true
  127. Write-Verbose "Inserting ProfileCode into $($_)..."
  128. $cleanupValue = $((@(
  129. '',
  130. $ProfileSignature.Begin,
  131. '',
  132. $(Export-FunctionSource -NoHeader ProfileCode_pre_common),
  133. '',
  134. '',
  135. $(Export-FunctionSource -NoHeader ProfileCode_common),
  136. '',
  137. '',
  138. $(Export-FunctionSource -NoHeader ProfileCode_post_common),
  139. '',
  140. $ProfileSignature.End
  141. ) -join "`n" ).Trim().Replace(('$' + '_'), ('$$' + '_')))
  142. }
  143. $profileContent = $($profileContent -replace $randomSeed, $cleanupValue).Trim()
  144. $local:shouldSudo = $sudo -and -not $userFile
  145. $local:status = 'Skipped'
  146. if ( $VerbosePreference -eq 'Continue' ) { $ShowSkipped = $true }
  147. # Test if new profile Content will be empty
  148. if ( -not ([string]::IsNullOrWhiteSpace($profileContent)) ) {
  149. $status = "Need $(if($insertProfileCode){if($exists){'Setup'}else{'Init'}}else{'Cleanup'})"
  150. if ( $sudo -or $userFile ) {
  151. Write-Verbose "Writing content to $($_)..."
  152. $local:tmpOutput = New-TemporaryFile -WhatIf:$false -Confirm:$false
  153. $profileContent | Out-File $tmpOutput.FullName -WhatIf:$false -Confirm:$false
  154. if ( $shouldSudo -and $( $PSVersionTable.Platform -eq 'Unix' ) ) {
  155. # Make sure the file has the right permissions on a Unix machine
  156. Invoke-ExpressionEx -sudo:$shouldSudo chmod 'a+r' $tmpOutput.FullName
  157. }
  158. try {
  159. $local:errMsg = $(Invoke-ExpressionEx -Confirm:$false -sudo:$shouldSudo @"
  160. if( -not (Test-Path $(Split-Path -Parent $p)) ) { New-Item -Type Directory "$(Split-Path -Parent $p)" -Force };
  161. `$null = Copy-Item "$($tmpOutput.FullName)" "$p" -Force:`$$Force -ErrorAction Stop;
  162. Remove-Item "$($tmpOutput.FullName)" -Force:`$$Force -ErrorAction SilentlyContinue
  163. "@ 2>&1)
  164. if ( $errMsg ) {
  165. Write-Error "$errMsg"
  166. $status = $status -replace 'Need ', 'Failed '
  167. }
  168. else {
  169. $status = $status -replace 'Need ', ''
  170. }
  171. }
  172. catch {
  173. Write-Error "Writing $p failed!"
  174. }
  175. }
  176. else {
  177. $status = "Need $status"
  178. Write-Error "Cannot write into [$_]'$p'! Please re-run with sudo."
  179. }
  180. }
  181. elseif ( ($Force -or $RemoveOnly) -and $exists ) {
  182. $status = 'Need Removal'
  183. if ( $sudo -or $userFile ) {
  184. Write-Verbose "Removing [$_]'$p'..."
  185. try {
  186. Invoke-ExpressionEx -sudo:$shouldSudo Remove-Item $p
  187. if ( -not (Test-Path $p) ) { $status = 'Removed' }
  188. }
  189. catch {
  190. Write-Error "Removal of $p failed!"
  191. }
  192. }
  193. else {
  194. Write-Error "Cannot remove [$_]'$p'! Please re-run with sudo."
  195. }
  196. }
  197. if ( $status -ne 'Skipped' -or $ShowSkipped ) {
  198. if ( $shouldSudo -and $status -ne 'Skipped' ) { $status = "$status via sudo" }
  199. New-ProfileSetupStatus -Name $_ -Path $p -Status $status -Exist $(Test-Path $p)
  200. }
  201. }
  202. Remove-Item function:ProfileCode* -Confirm:$false
  203. }
  204. #######################################################################
  205. ## ProfileCode_common (source) containing mandatory code for $PROFILE
  206. #######################################################################
  207. function ProfileCode_common {
  208. function Get-PowerShellPath {
  209. Get-Process -PID $PID | ForEach-Object { $_.Path, $_.Parent.Path } | Where-Object { $_ -match $('(powershell|pwsh)' + "`$") } | Select-Object -First 1
  210. }
  211. function ConvertTo-Base64 {
  212. param([string]$String)
  213. return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($String))
  214. }
  215. function Invoke-ExpressionEx {
  216. [CmdletBinding(SupportsShouldProcess)]param(
  217. [switch]$sudo,
  218. [switch]$Force,
  219. [string]$Function,
  220. [switch]$MarshalOutput,
  221. [Parameter(Position = 1, ValueFromRemainingArguments = $true)]
  222. [string[]]$expr
  223. )
  224. function do_sudo {
  225. [CmdletBinding()]param(
  226. [string]$sudo_cmd
  227. )
  228. function do_sudo_win {
  229. [CmdletBinding()]param(
  230. [string]$sudo_cmd
  231. )
  232. $local:sudo_exec = [string]::Empty
  233. if ( (Get-Command scoop -ErrorAction SilentlyContinue) ) {
  234. $sudo_exec = $(scoop which gsudo *>&1)
  235. if ( "$sudo_exec" -match 'not found' ) {
  236. $sudo_exec = $(scoop which sudo *>&1)
  237. }
  238. if ( "$sudo_exec" -match 'not found' ) { $sudo_exec = [string]::Empty }
  239. }
  240. if ( $sudo_exec ) {
  241. & $sudo_exec $sudo_cmd
  242. }
  243. else {
  244. Write-Error "Didn't find a known sudo command"
  245. }
  246. }
  247. if ( $PSVersionTable.Platform -eq 'Unix' ) {
  248. /usr/bin/env sudo "--preserve-env" $(Get-PowerShellPath) "-noprofile" "-EncodedCommand" $(ConvertTo-Base64 $sudo_cmd)
  249. }
  250. else {
  251. $local:currentIdenity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
  252. if ( $currentIdenity.UserClaims.Value -contains 'S-1-5-32-544' ) {
  253. do_sudo_win $sudo_cmd
  254. }
  255. else {
  256. Write-Error "User [$($currentIdenity.Name)]does not have permissions to run as admin."
  257. }
  258. }
  259. }
  260. if ( $sudo -and -not $(Test-IsAdmin) ) {
  261. $local:tmpOutputFile = $null
  262. $local:tmpScriptFile = $null
  263. $local:sudo_cmd = "$($expr -join ' ')".Trim()
  264. if ( $Function ) {
  265. $tmpScriptFile = New-TemporaryFile -WhatIf:$false -Confirm:$false
  266. $tmpScriptFile = Move-Item $tmpScriptFile.FullName "$($tmpScriptFile.FullName).ps1" -PassThru -WhatIf:$false -Confirm:$false
  267. $local:tmpScriptContent = Export-FunctionSource $Function
  268. $sudo_cmd = "$Function $sudo_cmd"
  269. $tmpScriptContent | Out-File $tmpScriptFile.FullName -WhatIf:$false
  270. $expr = @( ".", $tmpScriptFile.FullName, ";", "`n$Function" ) + $expr
  271. }
  272. $expr = @(
  273. "`n`$WhatIfPreference=`$$WhatIfPreference;",
  274. "`n`$VerbosePreference='$VerbosePreference';",
  275. "`n`$ErrorActionPreference='$ErrorActionPreference';",
  276. "`n`$ConfirmPreference='$ConfirmPreference';`n"
  277. ) + $expr
  278. if ( $MarshalOutput ) {
  279. $tmpOutputFile = New-TemporaryFile -WhatIf:$false -Confirm:$false
  280. $expr += @( "| Export-CliXml -Path '$($tmpOutputFile.FullName)'" )
  281. }
  282. $expr += @( ";`n exit `$LastExitCode" )
  283. ## $tmpScriptContent += "$($expr -join ' ')".Trim()
  284. Write-Verbose "Performing the following command line via SUDO:`n{$sudo_cmd}"
  285. $sudo_cmd = "$($expr -join ' ')".Trim()
  286. do_sudo $sudo_cmd
  287. ## do_sudo 'Get-Content' $tmpScriptFile.FullName '|' 'Invoke-Expression' '|' 'Export-Clixml' '-Path' $tmpOutputFile.FullName
  288. #do_sudo $sudo_cmd
  289. if ( $tmpScriptFile ) { Remove-Item $tmpScriptFile }
  290. if ( $tmpOutputFile ) { Import-Clixml $tmpOutputFile; Remove-Item $tmpOutputFile }
  291. return
  292. }
  293. Write-Verbose "Perofrming the following expression in-line:`n{$($expr -join ' ')}"
  294. Invoke-Expression "$expr"
  295. }
  296. function Test-IsAdmin {
  297. [CmdletBinding(ConfirmImpact = 'None')]
  298. [OutputType([bool])]
  299. param()
  300. switch ($true) {
  301. $( ($PSVersionTable.PSEdition -eq 'Desktop') -or
  302. ($PSVersionTable.Platform -eq 'Win32NT')
  303. ) {
  304. # Fastest way on Windows
  305. ([Security.Principal.WindowsPrincipal](
  306. [Security.Principal.WindowsIdentity]::GetCurrent()
  307. )).IsInRole(
  308. [Security.Principal.WindowsBuiltInRole]'Administrator'
  309. )
  310. }
  311. $( ($PSVersionTable.PSEdition -eq 'Core') -and
  312. ($PSVersionTable.Platform -eq 'Unix')
  313. ) {
  314. # On macOS and Linux we use ID to figure out if we run elevated (0 means superuser rights)
  315. return $((id -u) -eq 0)
  316. }
  317. default {
  318. # Unable to figure it out!
  319. Write-Warning -Message 'Unknown'
  320. return
  321. }
  322. }
  323. }
  324. function Export-FunctionSource {
  325. param(
  326. [switch]$NoHeader,
  327. [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
  328. #[ValidateSet([Functions])]
  329. [string[]]$FunctionName
  330. )
  331. $local:src = ""
  332. foreach ( $local:func in $FunctionName ) {
  333. $local:funcDef = $null
  334. foreach( $cmd in (Get-Command -All $func) ) {
  335. if( $cmd.CommandType -in @('Function','Filter') ) {
  336. $funcDef = $cmd.Definition
  337. break
  338. } elseif( -$funcDef -and ($cmd.CommandType -in @('Script','ExternalScript')) ) {
  339. $funcDef = Get-Content $cmd.Definition
  340. }
  341. }
  342. if( -not $funcDef ) {
  343. default { throw "Don't know how to handle $func" }
  344. }
  345. if ( -not $NoHeader ) { $src += "`nfunction $func {" }
  346. $src += "`n"
  347. $src += $funcDef
  348. if ( -not $NoHeader ) { $src += "`n}" }
  349. $src += "`n"
  350. }
  351. return $src.Trim()
  352. }
  353. # Are we running in Unix or Windows?
  354. $global:PathEnvDelimiter = $(if ( $PSVersionTable.Platform -match 'unix' ) { ':' } else { ';' })
  355. function Split-PathEnv {
  356. param([string]$EnvPath)
  357. $EnvPath -split $PathEnvDelimiter
  358. }
  359. # Establish Module Path
  360. $global:MyPSModulePath = Split-PathEnv $env:PSModulePath |
  361. Where-Object { $_ -match "^$($(Resolve-Path ~) -replace '\\',"\\")" } |
  362. Where-Object { Test-Path $_ } |
  363. Get-Item | Sort-Object -Property LastWriteTime -Descending |
  364. Select-Object -ExpandProperty FullName -First 1
  365. if ( -not $MyPSModulePath ) {
  366. $MyPSModulePath = $(Join-Path $(Join-Path $(Resolve-Path ~) 'powershell') 'Modules')
  367. $env:PSModulePath = "$MyPSModulePath$PathEnvDelimiter$env:PSModulePath"
  368. }
  369. if ( -not (Test-Path $MyPSModulePath) ) {
  370. New-Item -ItemType Directory -Path $MyPSModulePath -Force | Out-Null
  371. }
  372. Write-Verbose $(Get-Item $MyPSModulePath | Select-Object -ExpandProperty FullName)
  373. # Establish local/personal Script Root and make sure it's in $env:Path
  374. $local:p = Split-PathEnv $env:PATH
  375. $local:My_PSScriptRoot = Join-Path (Split-Path -Parent $MyPSModulePath) Scripts
  376. Write-Verbose "MyPSScriptRoot = $My_PSScriptRoot"
  377. if ( -not (Test-Path $My_PSScriptRoot) ) {
  378. New-Item -ItemType Directory -Path $My_PSScriptRoot -Force | Out-Null
  379. New-Item -ItemType Directory -Path $(Join-Path $My_PSScriptRoot base) -Force | Out-Null
  380. New-Item -ItemType Directory -Path $(Join-Path $(Join-Path $My_PSScriptRoot base) profile.d) -Force | Out-Null
  381. }
  382. $global:MyPSScriptRoot = $My_PSScriptRoot
  383. $p = @($p[0], $(Join-Path $My_PSScriptRoot base)) + $($p | Select-Object -Skip 1)
  384. $env:PATH = $p -join $PathEnvDelimiter
  385. }
  386. $SetupFromWeb = [bool]$(@( $SetupFromWeb, $sfw, $LoadCode ) | Where-Object { $_ })
  387. if ( -not $SetupFromWeb ) {
  388. _setup @PSBoundParameters
  389. Get-Item function:_setup | Remove-Item -Confirm:$false
  390. Get-Command Reload-MyScripts -ErrorAction SilentlyContinue |
  391. ForEach-Object { . $_.Name -Confirm:$false }
  392. }
  393. Remove-Variable SetupFromWeb -ErrorAction SilentlyContinue -Confirm:$false