├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Colors.ps1 ├── ConstrainedMode.ps1 ├── Data.ps1 ├── DocFx.ps1 ├── Files.ps1 ├── Functional.ps1 ├── Git.ps1 ├── LICENSE ├── PSScriptAnalyzerSettings.psd1 ├── PSToolset.psd1 ├── PSToolset.psm1 ├── Python.ps1 ├── README.md ├── SECURITY.md ├── Security.ps1 ├── TabExpansion.ps1 ├── Text.ps1 ├── Utils.ps1 ├── Xml.ps1 └── media └── source-output.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enableFiletypes": [ 3 | "powershell" 4 | ], 5 | "cSpell.words": [ 6 | "Aleksandr", 7 | "Jupyter", 8 | "Kostikov", 9 | "Linq", 10 | "MSRC", 11 | "Passthru", 12 | "Retryable", 13 | "Toolset", 14 | "USERDOMAIN", 15 | "adsi", 16 | "bing", 17 | "datetime", 18 | "hashtable", 19 | "ldap", 20 | "linenumber", 21 | "mkdir", 22 | "notcontains", 23 | "notmatch", 24 | "psitem", 25 | "psobject", 26 | "pstoolset", 27 | "repos", 28 | "rundll", 29 | "steppable", 30 | "timespan", 31 | "tracert", 32 | "xattr", 33 | "xcomm", 34 | "xelem", 35 | "xname" 36 | ], 37 | "powershell.codeFormatting.openBraceOnSameLine": false, 38 | "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", 39 | "powershell.codeFormatting.useCorrectCasing": true, 40 | "powershell.codeFormatting.whitespaceBeforeOpenBrace": false, 41 | "powershell.codeFormatting.whitespaceBeforeOpenParen": false, 42 | "powershell.codeFormatting.whitespaceInsideBrace": false, 43 | "powershell.codeFormatting.alignPropertyValuePairs": false 44 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require 4 | you to agree to a Contributor License Agreement (CLA) declaring that you have the 5 | right to, and actually do, grant us the rights to use your contribution. For 6 | details, visit https://cla.opensource.microsoft.com. 7 | 8 | When you submit a pull request, a CLA bot will automatically determine whether 9 | you need to provide a CLA and decorate the PR appropriately (e.g., status check, 10 | comment). Simply follow the instructions provided by the bot. You will only need 11 | to do this once across all repos using our CLA. 12 | 13 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 14 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 15 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 16 | 17 | # Technical Guidance 18 | This module doesn't require a build, all the code is in Powershell. While contributing please use the same codding style as used throughout the project. It may not be your default codding style, but for the project consistency it would be better to use the same style everywhere. 19 | 20 | I personally work with VS Code with Powershell code nowadays. Any editor would do, but VS Code with Powershell extension installed would ensure you can reformat the code with the same code style convention used. Hit Shift+Alt+F to format the currently opened document. 21 | 22 | All exported commands need to provide documentation with an example. 23 | -------------------------------------------------------------------------------- /Colors.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | <# 5 | To test if the process was redirected we can use: 6 | 7 | $process = Get-Process -id $pid 8 | 9 | function SCRIPT:Test-ProcessRedirected( $process ) 10 | { 11 | $process.StartInfo.RedirectStandardInput -or 12 | $process.StartInfo.RedirectStandardOutput -or 13 | $process.StartInfo.RedirectStandardError 14 | } 15 | 16 | #> 17 | 18 | function Write-Colorized 19 | { 20 | <# 21 | .SYNOPSIS 22 | Output object to stdout with specific color 23 | 24 | .DESCRIPTION 25 | Prints an object contents colorized in a specific color. Makes the output 26 | more readable on a console screen and the output can be still redirected 27 | as a regular stdout. 28 | 29 | .PARAMETER Color 30 | Name of the color to be used for coloring. 31 | Use Get-Colors command to output all available colors. 32 | 33 | .PARAMETER Object 34 | Object to be outputted to stdout. 35 | 36 | .EXAMPLE 37 | Write-Colorized green "=)", "test" 38 | 39 | Prints all items in passed string array to stdout with green color 40 | used in your console. 41 | 42 | .LINK 43 | Get-Colors 44 | #> 45 | 46 | param 47 | ( 48 | [Parameter(Mandatory = $true)] 49 | [string] $Color, 50 | [Parameter(Mandatory = $true)] 51 | [object] $Object 52 | ) 53 | 54 | $previous, [Console]::ForegroundColor = [Console]::ForegroundColor, [ConsoleColor]::$color 55 | $object 56 | [Console]::ForegroundColor = $previous 57 | } 58 | 59 | function Show-Highlight 60 | { 61 | <# 62 | .SYNOPSIS 63 | Highlight portion of some text to make it visually 64 | easier to find something in the text 65 | 66 | .DESCRIPTION 67 | Uses regex to find some some portion in the input text send via pipe. 68 | Matching text is highlighted with the color specified. 69 | 70 | Without regex specified this function would highlight code examples 71 | in the Powershell build in help. 72 | 73 | alias: hl 74 | 75 | .PARAMETER Regex 76 | Regular expression used to match interesting part of the input text. 77 | By default: regex that would match code snippets in the help examples. 78 | 79 | .PARAMETER Color 80 | Color that would be used to highlight matching text. 81 | By default: dark magenta 82 | 83 | .PARAMETER DropUnmatched 84 | If line doesn't match regex, don't return it. 85 | By default: unmatched lines are returned without highlighting 86 | 87 | .PARAMETER Interactive 88 | Render output as soon as it is received. 89 | By default: Out-String is called after all the input is received. 90 | 91 | .PARAMETER JSON 92 | Use pre-defined regex for JSON output. 93 | If user specifies $regex or color explicitly they take precedence. 94 | 95 | .EXAMPLE 96 | "test tee, please", "tee" | Show-Highlight e+ red 97 | 98 | Highlights all 'e' chars in the input with red. 99 | 100 | .EXAMPLE 101 | man hl -Examples | hl 102 | 103 | Highlights code snippets in Show-Highlight examples help. 104 | 105 | .EXAMPLE 106 | tracert bing.com | hl "[a-f0-9]+:[a-f0-9:]+" green -int 107 | 108 | Highlights IPv6 addresses from tracert output with green color. 109 | Output us rendered as soon as it is available. 110 | 111 | .EXAMPLE 112 | ls | hl ps1 -drop 113 | 114 | Highlights files or folders from ls output that match 'ps1' regex. 115 | If a line is not matching, it is dropped. 116 | 117 | .EXAMPLE 118 | Get-TimeZone | ConvertTo-Json | hl -JSON 119 | 120 | Highlights JSON syntax chars making output a bit more readable. 121 | 122 | .LINK 123 | Get-Colors 124 | #> 125 | 126 | param 127 | ( 128 | [string] $Regex = "^\s*PS\s+.*>.+", 129 | [ConsoleColor] $Color = "Blue", 130 | [switch] $DropUnmatched, 131 | [switch] $Interactive, 132 | [switch] $JSON 133 | ) 134 | 135 | begin 136 | { 137 | function Get-Markup 138 | { 139 | $start = 0 140 | $sections = [Regex]::Matches($line, $regex, "IgnoreCase") | select Index, Length, Value 141 | 142 | foreach( $section in $sections ) 143 | { 144 | [ordered] @{ type = "text"; start = $start; end = $section.Index - 1 } 145 | [ordered] @{ type = "high"; start = $section.Index; end = $section.Index + $section.Length - 1 } 146 | $start = $section.Index + $section.Length 147 | } 148 | 149 | [ordered] @{ type = "text"; start = $start; end = $line.Length - 1 } 150 | } 151 | 152 | function Use-Markup 153 | { 154 | foreach( $line in ($lines | Out-String | foreach TrimEnd) -split "`r?`n" ) 155 | { 156 | if( $line -notmatch $regex ) 157 | { 158 | if( -not $dropUnmatched ) 159 | { 160 | $line 161 | } 162 | continue 163 | } 164 | 165 | foreach( $info in (Get-Markup | where{ $psitem.start -le $psitem.end }) ) 166 | { 167 | $text = $line.Substring($info.start, $info.end - $info.start + 1) 168 | $foreground = if( $info.type -eq "text" ) { [Console]::ForegroundColor } else { $color } 169 | Write-Host $text -ForegroundColor $foreground -NoNewline 170 | } 171 | 172 | Write-Host "" 173 | } 174 | } 175 | 176 | $lines = @() 177 | 178 | if( $JSON ) 179 | { 180 | if( (-not $Regex) -or ($Regex -eq "^\s*PS\s+.*>.+") ) 181 | { 182 | $regex = '["{}/:,]' 183 | } 184 | } 185 | } 186 | process 187 | { 188 | $lines += $psitem 189 | if( $interactive ) 190 | { 191 | Use-Markup 192 | $lines = @() 193 | } 194 | } 195 | end 196 | { 197 | if( -not $interactive ) 198 | { 199 | Use-Markup 200 | } 201 | } 202 | } 203 | 204 | function Get-Colors 205 | { 206 | <# 207 | .SYNOPSIS 208 | Print all console host colors to the console in color 209 | 210 | .DESCRIPTION 211 | Possible color names taken from '[ConsoleColor] | gm -Static' are: 212 | Blue | DarkBlue 213 | Cyan | DarkCyan 214 | Gray | DarkGray 215 | Green | DarkGreen 216 | Magenta | DarkMagenta 217 | Red | DarkRed 218 | Yellow | DarkYellow 219 | White | Black 220 | 221 | .EXAMPLE 222 | Get-Colors 223 | 224 | Would output all colors to the console output. 225 | #> 226 | 227 | function color( $name ) 228 | { 229 | [Console]::ForegroundColor = [ConsoleColor]::$name 230 | } 231 | 232 | $previous = [Console]::ForegroundColor 233 | 234 | color Blue 235 | [Console]::Out.Write("Blue") 236 | color DarkBlue 237 | [Console]::Out.WriteLine(" DarkBlue") 238 | 239 | color Cyan 240 | [Console]::Out.Write("Cyan") 241 | color DarkCyan 242 | [Console]::Out.WriteLine(" DarkCyan") 243 | 244 | color Gray 245 | [Console]::Out.Write("Gray") 246 | color DarkGray 247 | [Console]::Out.WriteLine(" DarkGray") 248 | 249 | color Green 250 | [Console]::Out.Write("Green") 251 | color DarkGreen 252 | [Console]::Out.WriteLine(" DarkGreen") 253 | 254 | color Magenta 255 | [Console]::Out.Write("Magenta") 256 | color DarkMagenta 257 | [Console]::Out.WriteLine(" DarkMagenta") 258 | 259 | color Red 260 | [Console]::Out.Write("Red") 261 | color DarkRed 262 | [Console]::Out.WriteLine(" DarkRed") 263 | 264 | color Yellow 265 | [Console]::Out.Write("Yellow") 266 | color DarkYellow 267 | [Console]::Out.WriteLine(" DarkYellow") 268 | 269 | color White 270 | [Console]::Out.Write("White") 271 | color Black 272 | [Console]::Out.WriteLine(" Black") 273 | 274 | [Console]::ForegroundColor = $previous 275 | } 276 | 277 | function Get-Source 278 | { 279 | <# 280 | .SYNOPSIS 281 | Print source code of a command or script in color 282 | 283 | .DESCRIPTION 284 | Gets sources of a command, a script or an alias and outputs 285 | them with syntax highlighting to the host. 286 | 287 | Alias: source 288 | 289 | .PARAMETER CommandName 290 | Command name or alias name or path to a Powershell script file. 291 | 292 | .EXAMPLE 293 | Get-Source hl 294 | 295 | Get sources of the command that is resolved from hl alias in color. 296 | #> 297 | 298 | param 299 | ( 300 | [string] $CommandName 301 | ) 302 | 303 | function Get-CommandSource 304 | { 305 | # Getting corresponding command object 306 | $command = Get-Command $commandName | select -First 1 307 | if( $command.CommandType -eq "Alias" ) 308 | { 309 | $command = Get-Command $command.Definition 310 | } 311 | 312 | # Fixing shortcomings of $command.Definition - it truncates 313 | # function start for some reason 314 | $firstFix = $false 315 | $command.Definition -split "`r?`n" | foreach ` 316 | { 317 | if( (-not $firstFix) -and ($psitem -match "^\S") ) 318 | { 319 | "" 320 | " " + $psitem 321 | $firstFix = $true 322 | } 323 | else 324 | { 325 | $psitem 326 | } 327 | } 328 | } 329 | 330 | # Getting source code of the command 331 | if( Test-Path $commandName ) 332 | { 333 | $source = Get-Content $commandName 334 | } 335 | else 336 | { 337 | $source = Get-CommandSource 338 | } 339 | 340 | # Rendering sources in color 341 | Show-ColorizedContent $source 342 | } 343 | 344 | function Show-ColorizedContent 345 | { 346 | param 347 | ( 348 | $content = $(throw "Powershell script must be specified"), 349 | $highlightRanges = @(), 350 | [System.Management.Automation.SwitchParameter] $excludeLineNumbers 351 | ) 352 | 353 | $replacementColours = 354 | @{ 355 | Attribute = "DarkCyan" 356 | Command = "DarkCyan" 357 | CommandParameter = "DarkMagenta" 358 | CommandArgument = "Gray" 359 | Comment = "DarkGreen" 360 | Grouper = "DarkCyan" 361 | Keyword = "Gray" 362 | Member = "DarkCyan" 363 | Number = "DarkGray" 364 | Operator = "DarkRed" 365 | Property = "Gray" 366 | StatementSeparator = "DarkCyan" 367 | String = "DarkYellow" 368 | Type = "DarkCyan" 369 | Variable = "DarkGray" 370 | } 371 | $highlightColor = "Green" 372 | $highlightCharacter = ">" 373 | 374 | # Read the text of the file, and parse it 375 | $content = $content | Out-String 376 | $parsed = [Management.Automation.PsParser]::Tokenize($content, [ref] $null) | sort StartLine, StartColumn 377 | 378 | function WriteFormattedLine($formatString, [int] $line) 379 | { 380 | if($excludeLineNumbers) { return } 381 | 382 | $hColor = "Gray" 383 | $separator = "|" 384 | if($highlightRanges -contains $line) { $hColor = $highlightColor; $separator = $highlightCharacter } 385 | Write-Host -NoNewLine -Fore $hColor ($formatString -f $line, $separator) 386 | } 387 | 388 | Write-Host 389 | 390 | WriteFormattedLine "{0:D3} {1} " 1 391 | 392 | $column = 1 393 | foreach($token in $parsed) 394 | { 395 | $color = "Gray" 396 | 397 | # Determine the highlighting colour 398 | $color = $replacementColours[[string]$token.Type] 399 | if(-not $color) { $color = "Gray" } 400 | 401 | # Now output the token 402 | if(($token.Type -eq "NewLine") -or ($token.Type -eq "LineContinuation")) 403 | { 404 | $column = 1 405 | Write-Host 406 | WriteFormattedLine "{0:D3} {1} " ($token.StartLine + 1) 407 | } 408 | else 409 | { 410 | # Do any indenting 411 | if($column -lt $token.StartColumn) 412 | { 413 | Write-Host -NoNewLine (" " * ($token.StartColumn - $column)) 414 | } 415 | 416 | # See where the token ends 417 | $tokenEnd = $token.Start + $token.Length - 1 418 | 419 | # Handle the line numbering for multi-line strings 420 | $lineCounter = $token.StartLine 421 | $stringLines = $( -join $content[$token.Start..$tokenEnd] -split "`r`n") 422 | foreach($stringLine in $stringLines) 423 | { 424 | if($lineCounter -gt $token.StartLine) 425 | { 426 | WriteFormattedLine "$([Environment]::NewLine){0:D3} {1}" $lineCounter 427 | } 428 | Write-Host -NoNewLine -Fore $color $stringLine 429 | $lineCounter++ 430 | } 431 | 432 | # Update our position in the column 433 | $column = $token.EndColumn 434 | } 435 | } 436 | 437 | Write-Host ([Environment]::NewLine) 438 | } 439 | -------------------------------------------------------------------------------- /ConstrainedMode.ps1: -------------------------------------------------------------------------------- 1 | $FunctionsToExport = @( 2 | # Colors 3 | "Show-Highlight", # "Write-Colorized", "Show-Highlight", "Get-Colors", "Get-Source", 4 | # DocFx.ps1 5 | "Start-DocFx", 6 | # Data 7 | "ConvertTo-PsObject", "ConvertTo-Hash", 8 | "Use-Project", "Use-Filter", "Get-Parameter", 9 | "Get-Ini", "Show-Ini", "ConvertFrom-Ini", "Import-Ini", 10 | # Functional 11 | "Get-UniqueUnsorted", # 12 | "Test-Any", "Test-All", "Get-First", "Get-Last", "Get-Separation", 13 | "Get-Median", "Get-Reverse", "Get-Randomized", 14 | # Files 15 | Resolve-ScriptPath, #"Get-FileEncoding" 16 | ) 17 | 18 | function reload 19 | { 20 | get-module pstoolset | remove-module 21 | ipmo \\alexko-11\C$\home\Documents\Powershell\Modules\PSToolset\PSToolset.psd1 22 | } 23 | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" 24 | reload -------------------------------------------------------------------------------- /Data.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function ConvertTo-PsObject 5 | { 6 | <# 7 | .SYNOPSIS 8 | Convert a set of variables into a PsObject 9 | 10 | .DESCRIPTION 11 | Constructs new PsObject from the available variables. 12 | Simplifies packing of data into one object that Powershell can work with. 13 | 14 | Alias: construct 15 | 16 | .PARAMETER Args 17 | All parameters are detected dynamically. You should pass here names 18 | of the variables you'd like to convert into the PsObject. 19 | 20 | .EXAMPLE 21 | $a = "a_value" 22 | $b = [int] 5 23 | $c = "one", "two" 24 | construct a b c 25 | 26 | Constructs PsObject with properties a, b and c. Values are taken from 27 | variables $a, $b and $c. 28 | #> 29 | 30 | $properties = [ordered] @{} 31 | 32 | foreach( $name in $args ) 33 | { 34 | $local = Get-Variable $name -Scope local -ea Ignore 35 | if( $local ) { $properties.$name = $local.Value; continue } 36 | 37 | $script = Get-Variable $name -Scope script -ea Ignore 38 | if( $script ) { $properties.$name = $script.Value; continue } 39 | 40 | $global = Get-Variable $name -Scope global -ea Ignore 41 | if( $global ) { $properties.$name = $global.Value; continue } 42 | 43 | Write-Warning "Could not resolve variable with name '$name'" 44 | } 45 | 46 | New-Object -TypeName PSObject -Property $properties 47 | } 48 | 49 | function ConvertTo-Hash( [object] $object ) 50 | { 51 | <# 52 | .SYNOPSIS 53 | Convert an object into a hash table 54 | 55 | .DESCRIPTION 56 | This function takes any object, gets all not $false properties and creates 57 | hash table out of the found properties. This can be useful to pass some 58 | object through a process boundary or to remove property from an object. 59 | 60 | .PARAMETER Object 61 | Object to deconstruct into hash table 62 | 63 | .EXAMPLE 64 | ConvertTo-Hash (ls | select -f 1) 65 | 66 | Get hash table from the first found child item. 67 | #> 68 | 69 | $hash = [ordered] @{} 70 | $object | 71 | foreach{ $psitem.PsObject.Members } | 72 | where MemberType -match "^(Note)?Property$" | 73 | foreach Name | 74 | where{ $object.$psitem } | 75 | foreach{ $hash[$psitem] = $object.$psitem } 76 | $hash 77 | } 78 | 79 | function Get-Parameter 80 | { 81 | <# 82 | .SYNOPSIS 83 | Get names of all available parameters from input objects 84 | 85 | .DESCRIPTION 86 | This filter analyses what parameters are present in all objects passed to 87 | the filter and outputs all unique parameter names. 88 | 89 | .PARAMETER Pattern 90 | Constrain output only to parameter names that match this pattern. 91 | By default all parameter names are returned. 92 | 93 | .PARAMETER Single 94 | Specify this switch if there must be only one parameter that match the 95 | pattern. If there is no single matching parameter, exception is thrown. 96 | 97 | .EXAMPLE 98 | Get-ChildItem | Get-Parameter 99 | 100 | List all available parameters from Get-ChildItem command. 101 | Both file info and directory info parameter names will be listed. 102 | 103 | .EXAMPLE 104 | Get-Process | Get-Parameter priority 105 | 106 | List all parameters for Process object (returned from Get-Process) that 107 | contain 'priority' substring in the parameter name. 108 | 'priority' here is a regex. 109 | 110 | .LINK 111 | Use-Project 112 | Use-Filter 113 | #> 114 | 115 | param 116 | ( 117 | [string] $Pattern = ".*", 118 | [switch] $Single 119 | ) 120 | 121 | begin { $accumulator = @() } 122 | process { $accumulator += $psitem } 123 | end 124 | { 125 | if( -not $accumulator ) 126 | { 127 | return 128 | } 129 | 130 | # Get properties that match the pattern 131 | $result = @( 132 | $accumulator | 133 | foreach{ $psitem.PsObject.Members } | 134 | where Name -match $pattern | 135 | where MemberType -match "Property" | 136 | foreach Name | 137 | Get-UniqueUnsorted) 138 | 139 | # Need to return all entries 140 | if( -not $single ) { return $result } 141 | 142 | # No ambulation with matches 143 | if( $result.Length -eq 1 ) { return $result[0] } 144 | 145 | # Strict match disambiguation 146 | if( @($result | where{ $psitem -eq $pattern }).Length -eq 1 ) { return $pattern } 147 | 148 | # Warn user about ambulation 149 | Write-Warning "Disambiguate '$pattern'`n$($result | Out-String)" 150 | } 151 | } 152 | 153 | function Use-Project 154 | { 155 | <# 156 | .SYNOPSIS 157 | Project several parameters from input objects 158 | 159 | .DESCRIPTION 160 | This command performs project operation from relational algebra with not 161 | strict column name matching. It allows to compress data output only to 162 | the columns you are interested in. 163 | 164 | You are not forced to specify full column names to do so. You only need 165 | to supply enough info to perform unambiguous column name match. It makes 166 | working with table-like date more interactive and less time consuming. 167 | 168 | Behavior is very similar to Select-Object with not strict (but unambiguous) 169 | properties specified. 170 | 171 | Alias: p 172 | 173 | .PARAMETER Args 174 | Pass as many column name patterns as you like - they would be parsed 175 | dynamically. 176 | 177 | .EXAMPLE 178 | Get-ChildItem | Use-Project name len 179 | 180 | Would output Name and Length properties of all child items. Projection 181 | doesn't require you to specify full property name if you can supply 182 | unambiguous matching regex pattern. 183 | 184 | .EXAMPLE 185 | Get-ChildItem | Use-Project name time 186 | WARNING: Disambiguate 'time' 187 | CreationTime 188 | CreationTimeUtc 189 | LastAccessTime 190 | LastAccessTimeUtc 191 | LastWriteTime 192 | LastWriteTimeUtc 193 | 194 | Would output warning showing that 'time' parameter is ambiguous and there 195 | are 6 parameter names that match it. You must supply more specific 196 | parameter name so the match would be unambiguous. 197 | 198 | [The same command via aliases] 199 | ls | p name time 200 | 201 | .LINK 202 | Get-Parameter 203 | Use-Filter 204 | #> 205 | 206 | begin { $accumulator = @() } 207 | process { $accumulator += $psitem } 208 | end 209 | { 210 | # Display available parameters if no arguments are specified 211 | if( $args.Count -eq 0 ) { Write-Warning "What property?`n$($accumulator | Get-Parameter | Out-String)"; return } 212 | 213 | # Resolve all passed parameter names 214 | $resolved = [string[]] @($args | foreach{ $accumulator | Get-Parameter $psitem -Single } ) 215 | 216 | # Output all resolved parameters 217 | # If there is an unresolved parameter do nothing 218 | if( $args.Count -eq $resolved.Count ) { $accumulator | Select-Object -Property $resolved } 219 | } 220 | } 221 | 222 | function Use-Filter 223 | { 224 | <# 225 | .SYNOPSIS 226 | Regex based parameter filter for input objects 227 | 228 | .DESCRIPTION 229 | Filters pipeline passing through only objects that match specific property 230 | and value pattern. Allows to quickly explore data and discover property 231 | names and values. 232 | 233 | Alias: f 234 | 235 | .PARAMETER ParameterPattern 236 | Regex pattern for a parameter. Only not ambiguous matches are accepted. 237 | All ambiguities are explained via Warnings. If parameter pattern is omitted, 238 | all existing property names are shown. 239 | 240 | .PARAMETER ValuePattern 241 | Regex pattern for a parameter value. Only not ambiguous matches are accepted. 242 | All ambiguities are explained via Warnings. If value pattern is omitted, all 243 | existing property values are shown. 244 | 245 | .PARAMETER NoValue 246 | Specify this switch if you want to filter properties that match property 247 | pattern but have no value. 248 | 249 | .EXAMPLE 250 | PS> Get-ChildItem | Use-Filter name 251 | PS> ls | f name ps1 252 | 253 | Exploring Get-ChildItem output. Output unique values for a property that 254 | match 'name' pattern. Then specify ps1 files for the name. 255 | 256 | .EXAMPLE 257 | ls | f len -NoValue 258 | 259 | Filter ls output, find files which don't have Length property set. 260 | 261 | .LINK 262 | Get-Parameter 263 | Use-Project 264 | #> 265 | 266 | param 267 | ( 268 | [string] $ParameterPattern, 269 | [string] $ValuePattern, 270 | [switch] $NoValue 271 | ) 272 | 273 | begin { $accumulator = @() } 274 | process { $accumulator += $psitem } 275 | end 276 | { 277 | # Display available parameters if no parameter pattern is specified 278 | if( -not $parameterPattern ) { Write-Warning "What property?`n$($accumulator | Get-Parameter | Out-String)"; return } 279 | 280 | # Resolve parameter name 281 | $parameter = $accumulator | Get-Parameter $parameterPattern -Single 282 | if( -not $parameter ) { return } 283 | 284 | # No value special case 285 | if( $noValue ) { return $accumulator | where{ -not $psitem.$parameter } } 286 | 287 | # Display all values if no value pattern is specified 288 | if( -not $valuePattern ) { Write-Warning "What value?`n$($accumulator | foreach{ $psitem.$parameter } | Get-UniqueUnsorted | Out-String)"; return } 289 | 290 | # Output object that has matches in property and value 291 | $accumulator | where{ $psitem.$parameter -match $valuePattern } 292 | } 293 | } 294 | 295 | function Get-Ini 296 | { 297 | <# 298 | .SYNOPSIS 299 | Parse INI file as a hashtable object 300 | 301 | .DESCRIPTION 302 | Features: 303 | - Both section and section-less parameters are supported. 304 | - Comments are supported. 305 | - Non-literal names are supported. 306 | - Collapsing of empty sections is supported. 307 | 308 | .PARAMETER Path 309 | Path to INI file. 310 | Can't be used at the same time with Content. 311 | 312 | .PARAMETER KeepEmptySections 313 | Specify this switch if you want to keep empty INI sections in the output. 314 | By default empty sections are removed. 315 | 316 | .PARAMETER Content 317 | Content of the INI file. 318 | Can't be used at the same time with Path. 319 | 320 | .PARAMETER Comment 321 | Regex that specifies how comments are started in ini file. 322 | By default: ; 323 | 324 | .EXAMPLE 325 | Get-Ini Shared.ini 326 | 327 | Parse Shared.ini file. 328 | 329 | .LINK 330 | Show-Ini 331 | http://stackoverflow.com/questions/417798/ini-file-parsing-in-powershell 332 | #> 333 | 334 | param 335 | ( 336 | [string] $Path, 337 | [switch] $KeepEmptySections, 338 | [string[]] $Content, 339 | [string] $Comment = ";" 340 | ) 341 | 342 | # Parameter validation 343 | if( $path -and $content ) 344 | { 345 | throw "It is not possible to specify both -Path and -Content parameters" 346 | } 347 | 348 | # Initialize 349 | $section = "" 350 | $ini = [ordered]@{} 351 | $ini[$section] = [ordered]@{} 352 | $content = if( $content ) 353 | { 354 | $content -split "`r?`n" 355 | } 356 | else 357 | { 358 | Get-Content $path 359 | } 360 | 361 | # Parsing 362 | foreach( $line in $content ) 363 | { 364 | $withoutComments = ($line -replace "$comment.*").Trim() 365 | if( $withoutComments.Length -eq 0 ) { continue } 366 | 367 | switch -regex ($withoutComments) 368 | { 369 | "^\[([^\]]+)\]\s*$" 370 | { 371 | $section = $matches[1].Trim() 372 | $ini[$section] = [ordered]@{} 373 | } 374 | "^([^=]+)\s*=\s*(.*)?\s*$" 375 | { 376 | $name, $value = $matches[1..2] 377 | $ini[$section][$name.Trim()] = $value 378 | } 379 | default 380 | { 381 | Write-Warning "Unknown sentence '$line' in file '$path'. Skipping the line." 382 | } 383 | } 384 | } 385 | 386 | # Remove empty sections, we create a new ini object since in ConstrainedMode 387 | # it is not possible to call any methods, the needed $ini.Remove() included 388 | if( -not $KeepEmptySections ) 389 | { 390 | $newIni = [ordered]@{} 391 | $ini.keys | where{ $ini[$psitem].Count -gt 0 } | foreach{ $newIni[$psitem] = $ini.$psitem } 392 | $ini = $newIni 393 | } 394 | 395 | # Copy entries from no-section if it's possible 396 | if( $ini[""] -and $ini[""].Keys ) 397 | { 398 | $withoutSection = @($ini[""].Keys) | where{ @($ini.keys) -notcontains $psitem } 399 | $withoutSection | foreach{ $ini[$psitem] = $ini[""][$psitem] } 400 | } 401 | 402 | $ini 403 | } 404 | 405 | function ConvertFrom-Ini 406 | { 407 | <# 408 | .SYNOPSIS 409 | Converts ini strings into Powershell hashtable object 410 | 411 | .DESCRIPTION 412 | Features: 413 | - Both section and section-less parameters are supported. 414 | - Comments are supported. 415 | - Non-literal names are supported. 416 | - Collapsing of empty sections is supported. 417 | 418 | .PARAMETER Content 419 | Content of an INI file. 420 | 421 | .PARAMETER Comment 422 | Regex that specifies how comments are started in ini file. 423 | By default: ; 424 | 425 | .PARAMETER KeepEmptySections 426 | Specify this switch if you want to keep empty INI sections in the output. 427 | By default empty sections are removed. 428 | 429 | .EXAMPLE 430 | ConvertFrom-Ini (Get-Content Shared.ini) 431 | 432 | Convert content of Shared.ini file to a hashtable object. 433 | 434 | .LINK 435 | http://stackoverflow.com/questions/417798/ini-file-parsing-in-powershell 436 | #> 437 | 438 | param 439 | ( 440 | [string[]] $Content, 441 | [string] $Comment = ";", 442 | [switch] $KeepEmptySections 443 | ) 444 | 445 | # Initialize 446 | $section = "" 447 | $ini = [ordered]@{} 448 | $ini[$section] = [ordered]@{} 449 | $content = $content -split "`r?`n" 450 | 451 | # Parsing 452 | foreach( $line in $content ) 453 | { 454 | $withoutComments = ($line -replace "$comment.*").Trim() 455 | if( $withoutComments.Length -eq 0 ) { continue } 456 | 457 | switch -regex ($withoutComments) 458 | { 459 | "^\[([^\]]+)\]\s*$" 460 | { 461 | $section = $matches[1].Trim() 462 | $ini[$section] = [ordered]@{} 463 | } 464 | "^([^=]+)\s*=\s*(.*)?\s*$" 465 | { 466 | $name, $value = $matches[1..2] 467 | $ini[$section][$name.Trim()] = $value 468 | } 469 | default 470 | { 471 | Write-Warning "Unknown sentence '$line' in file '$path'. Skipping the line." 472 | } 473 | } 474 | } 475 | 476 | # Remove empty sections, we create a new ini object since in ConstrainedMode 477 | # it is not possible to call any methods, the needed $ini.Remove() included 478 | if( -not $KeepEmptySections ) 479 | { 480 | $newIni = [ordered]@{} 481 | $ini.keys | where{ $ini[$psitem].Count -gt 0 } | foreach{ $newIni[$psitem] = $ini.$psitem } 482 | $ini = $newIni 483 | } 484 | 485 | # Copy entries from no-section if it's possible 486 | if( $ini[""] -and $ini[""].Keys ) 487 | { 488 | $withoutSection = @($ini[""].Keys) | where{ @($ini.keys) -notcontains $psitem } 489 | $withoutSection | foreach{ $ini[$psitem] = $ini[""][$psitem] } 490 | } 491 | 492 | $ini 493 | } 494 | 495 | function Import-Ini 496 | { 497 | <# 498 | .SYNOPSIS 499 | Imports ini file into Powershell hashtable object 500 | 501 | .DESCRIPTION 502 | Features: 503 | - Both section and section-less parameters are supported. 504 | - Comments are supported. 505 | - Non-literal names are supported. 506 | - Collapsing of empty sections is supported. 507 | 508 | .PARAMETER Path 509 | Path to an existing INI file. 510 | 511 | .PARAMETER Comment 512 | Regex that specifies how comments are started in ini file. 513 | By default: ; 514 | 515 | .PARAMETER KeepEmptySections 516 | Specify this switch if you want to keep empty INI sections in the output. 517 | By default empty sections are removed. 518 | 519 | .EXAMPLE 520 | Import-Ini Shared.ini 521 | 522 | Convert content of Shared.ini file to a hashtable object. 523 | 524 | .LINK 525 | http://stackoverflow.com/questions/417798/ini-file-parsing-in-powershell 526 | #> 527 | 528 | param 529 | ( 530 | [Parameter(Mandatory = $true)] 531 | [ValidateScript({Test-Path $psitem -PathType Leaf})] 532 | [string] $Path, 533 | [string] $Comment = ";", 534 | [switch] $KeepEmptySections 535 | ) 536 | 537 | $content = Get-Content $path 538 | ConvertFrom-Ini $content -Comment:$Comment -KeepEmptySections:$KeepEmptySections 539 | } 540 | 541 | function Show-Ini 542 | { 543 | <# 544 | .SYNOPSIS 545 | Print contents of INI parsed file, received from Get-Ini command 546 | 547 | .DESCRIPTION 548 | Formats INI file in hash table form to make it console-readable. 549 | You can specify section filter to get only the sections of interest 550 | at the moment. 551 | 552 | .PARAMETER Ini 553 | Content of a INI file in hash table form. Usually it is out from 554 | Get-Ini command. 555 | 556 | .PARAMETER SectionFilter 557 | Filter that should pass each section in order to be outputted. 558 | By default all sections are shown. 559 | 560 | .EXAMPLE 561 | Show-Ini (Get-Ini Shared.ini) machine 562 | 563 | Print all sections of Shared.ini containing 'machine' word. 564 | 565 | .LINK 566 | Get-Ini 567 | #> 568 | 569 | param 570 | ( 571 | [hashtable] $Ini, 572 | [string] $SectionFilter = ".*" 573 | ) 574 | 575 | foreach( $section in ($ini.Keys | sort | where{ $psitem -match $sectionFilter }) ) 576 | { 577 | $section 578 | foreach( $key in $ini[$section].keys ) 579 | { 580 | " $($key) = $($ini[$section][$key])" 581 | } 582 | "" 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /DocFx.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Start-DocFx 5 | { 6 | <# 7 | .SYNOPSIS 8 | Start docfx in current folder or $env:DefaultDocFxPath. 9 | Reuse existing docfx instance already running if possible. 10 | 11 | .PARAMETER Force 12 | Don't reuse anything and don't use defaults. 13 | Just open a new docfx in the current folder. 14 | 15 | .EXAMPLE 16 | Start-DocFx 17 | 18 | Tries to reopen a currently opened docfx. 19 | #> 20 | 21 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 22 | 'PSUseShouldProcessForStateChangingFunctions', '', 23 | Justification = 'Intended to be this way')] 24 | param 25 | ( 26 | [switch] $Force 27 | ) 28 | 29 | # Test if docfx is installed 30 | if( -not (Get-Command docfx.exe -ea Ignore) ) 31 | { 32 | throw "docfx.exe must be discoverable via PATH environment variable" 33 | } 34 | 35 | # Cleanup cleanup jobs =) 36 | $cleanupJobName = "Start-DocFx cleanup" 37 | Get-Job $cleanupJobName -ea Ignore | where State -eq Completed | Remove-Job 38 | 39 | # Helper function 40 | function Open-DocFx( $folder = $pwd ) 41 | { 42 | Push-Location $folder 43 | $path = Get-ChildItem -Recurse "docfx.json" | select -First 1 44 | Pop-Location 45 | 46 | $ps = Start-Process ` 47 | -FilePath "pwsh" ` 48 | -ArgumentList ('-Command "docfx ' + $path + ' --serve"') ` 49 | -WorkingDirectory $folder ` 50 | -WindowStyle Hidden ` 51 | -PassThru 52 | 53 | Start-Job -Name $cleanupJobName { 54 | Start-Sleep -Seconds 60 55 | $ps | Stop-Process 56 | 57 | } | Out-Null 58 | 59 | Start-Process http://localhost:8080 60 | } 61 | 62 | # When need to open new docfx in current folder 63 | if( $Force ) 64 | { 65 | "Open new docfx in current folder $pwd" 66 | Open-DocFx 67 | return 68 | } 69 | 70 | # Trying to reuse opened docfx if possible 71 | if( Get-Process docfx -ea Ignore ) 72 | { 73 | "Found existing docfx, reopening default URL" 74 | Start-Process http://localhost:8080 75 | return 76 | } 77 | 78 | # Run notebook from default location if possible 79 | if( $env:DefaultDocFxPath ) 80 | { 81 | "Open new docfx in `$env:DefaultDocFxPath = $env:DefaultDocFxPath" 82 | Open-DocFx $env:DefaultDocFxPath 83 | } 84 | else 85 | { 86 | "Open new docfx in current folder $pwd, note that you can use `$env:DefaultDocFxPath instead if you define it" 87 | Open-DocFx 88 | } 89 | } -------------------------------------------------------------------------------- /Files.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Resolve-ScriptPath 5 | { 6 | <# 7 | .SYNOPSIS 8 | Resolve path that is local to the script 9 | 10 | .DESCRIPTION 11 | During script development it is useful to copy-paste function code and call 12 | scripts in the local folder. But for reusability in the script files it is 13 | best to combine paths with $PsScriptRoot variable that is available only 14 | when called from withing a script. 15 | 16 | This function bring good from both of the worlds together. Resolving paths 17 | with this function allows to: 18 | - Copy-paste code from editor. Paths would be resolved relative to 19 | current folder. 20 | - Use $PsScriptRoot when script is being called. Path would be resolved 21 | relative to script root folder. 22 | 23 | .PARAMETER Path 24 | Path to be resolved. 25 | 26 | .EXAMPLE 27 | Resolve-ScriptPath "Utils.ps1" 28 | 29 | When executed in console on copy-paste it would resolve to '.\Utils.ps1', 30 | but when executed from a script that somebody calls it would resolve to 31 | 'Drive:\Path\To\Script\Folder\Utils.ps1' 32 | #> 33 | 34 | param 35 | ( 36 | [Parameter(Mandatory = $true)] 37 | [string] $Path 38 | ) 39 | 40 | $location = if( $myInvocation.PSScriptRoot ) 41 | { 42 | $myInvocation.PSScriptRoot 43 | } 44 | else 45 | { 46 | "." 47 | } 48 | 49 | Join-Path $location $path 50 | } 51 | 52 | function Get-FileEncoding 53 | { 54 | <# 55 | .SYNOPSIS 56 | Gets file encoding 57 | 58 | .DESCRIPTION 59 | Useful if you want to update large volume of files and don't want 60 | to have regressions coming from encoding changes as a side-effect. 61 | 62 | .PARAMETER Path 63 | The path to the file you need get encoding from. 64 | 65 | .EXAMPLE 66 | Get-FileEncoding main.cpp 67 | 68 | Get encoding that is main.cpp file uses. 69 | 70 | .LINK 71 | http://franckrichard.blogspot.com/2010/08/powershell-get-encoding-file-type.html 72 | 73 | .NOTES 74 | Default encoding behaves as ASCII with support of currently used 75 | windows code page 76 | #> 77 | 78 | param 79 | ( 80 | [Parameter(Mandatory = $true)] 81 | [string] $Path 82 | ) 83 | 84 | function Test-Preamble( $encoding, [byte[]] $filePreamble ) 85 | { 86 | [byte[]] $preamble = $encoding.GetPreamble() 87 | 88 | if( $filePreamble.Count -lt $preamble.Count ) 89 | { 90 | return false 91 | } 92 | 93 | for( $i = 0; $i -lt $preamble.Count; $i += 1 ) 94 | { 95 | if( $filePreamble[$i] -ne $preamble[$i] ) 96 | { 97 | return $false 98 | } 99 | } 100 | 101 | return $true 102 | } 103 | 104 | $knownEncodings = @( 105 | [Text.Encoding]::BigEndianUnicode, 106 | [Text.Encoding]::UTF32, 107 | [Text.Encoding]::UTF8, 108 | [Text.Encoding]::Unicode, # that's UTF16 109 | [Text.Encoding]::Default # must come last 110 | ) 111 | 112 | [byte[]] $byte = Get-Content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path 113 | 114 | foreach( $encoding in $knownEncodings ) 115 | { 116 | if( Test-Preamble $encoding $byte ) 117 | { 118 | return $encoding 119 | } 120 | } 121 | 122 | # Usually Default encoding preamble is empty and we return it, but in case 123 | # that's not true we assume file without preamble to be UTP7 encoded 124 | [Text.Encoding]::UTF7 125 | } -------------------------------------------------------------------------------- /Functional.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Get-Reverse 5 | { 6 | <# 7 | .SYNOPSIS 8 | Reverse a sequence that is piped in 9 | 10 | .EXAMPLE 11 | 1,2,3 | Get-Reverse 12 | 13 | Would output 3 2 1 14 | #> 15 | 16 | $array = @($input) 17 | [array]::Reverse($array) 18 | $array 19 | } 20 | 21 | function Get-Randomized 22 | { 23 | <# 24 | .SYNOPSIS 25 | Randomize a sequence that is piped in 26 | 27 | .EXAMPLE 28 | 1, 2, 3, 4 | Get-Randomized 29 | 30 | Shuffles array elements and outputs array in a random order. 31 | Each element is outputted only once. 32 | #> 33 | 34 | $array = [Collections.ArrayList]::New(@($input)) 35 | 36 | while( $array ) 37 | { 38 | $index = Get-Random $array.Count 39 | $array[$index] 40 | $array.RemoveAt($index) 41 | } 42 | } 43 | 44 | function Get-Median 45 | { 46 | <# 47 | .SYNOPSIS 48 | Calculate median of numeric array piped in 49 | 50 | .EXAMPLE 51 | 5, 1, 20, 4, 4 | Get-Median 52 | 53 | Would output 4 54 | #> 55 | 56 | $sorted = @($input | sort) 57 | if( -not $sorted.Length ) 58 | { 59 | return 60 | } 61 | 62 | $middle = $sorted.Length / 2 63 | $skip = [math]::Ceiling($middle) - 1 64 | $take = if( $middle % 1 ) { 1 } else { 2 } 65 | $sorted | select -Skip $skip | select -First $take | measure -Average | foreach Average 66 | } 67 | 68 | function Get-UniqueUnsorted 69 | { 70 | <# 71 | .SYNOPSIS 72 | Get unique values from an unsorted array 73 | 74 | .PARAMETER Property 75 | Property to analyse for uniqueness 76 | 77 | .EXAMPLE 78 | "a","c","b","b","c","z" | Get-UniqueUnsorted 79 | 80 | Would return unique elements of the input array without changing element order: a, c, b, z 81 | 82 | .EXAMPLE 83 | "c", "bb", "a" | Get-UniqueUnsorted Length 84 | 85 | Would return unique length elements from the input array in the order of appearance: c, bb 86 | #> 87 | 88 | param 89 | ( 90 | [string] $Property 91 | ) 92 | 93 | $result = [ordered] @{} 94 | 95 | foreach( $item in $input ) 96 | { 97 | $name = $item 98 | if( $property ) { $name = $item.$property } 99 | $result.$name = $null 100 | } 101 | 102 | $result.Keys 103 | } 104 | 105 | function Test-Any( [scriptblock] $Condition = { $psitem -ne $null } ) 106 | { 107 | <# 108 | .SYNOPSIS 109 | Test if any element in the piped in input confirms to the condition 110 | 111 | .DESCRIPTION 112 | Useful for functional-style code. Returns $true if there is an element 113 | that confirms to the specified condition. $false if there are no such 114 | elements. 115 | 116 | .PARAMETER Condition 117 | Condition to test for each element. $psitem variable can be used. 118 | By default would return $true for the not null elements. 119 | 120 | .EXAMPLE 121 | "1", "2" | any{ $psitem -eq "2" } 122 | True since we have "2" element in the input collection. 123 | 124 | .EXAMPLE 125 | "1", "2" | any 126 | True since we have a not null element in the input collection. 127 | 128 | .EXAMPLE 129 | $notExistingVariable | any 130 | False since Powershell would create collection with one $null element. 131 | 132 | .EXAMPLE 133 | $() | any 134 | False since there is no element in the input collection that is not null. 135 | 136 | .NOTES 137 | Can't use 'break' for this - we would exit all pipelines, 138 | not just the current one 139 | #> 140 | 141 | begin 142 | { 143 | $found = $false 144 | } 145 | process 146 | { 147 | if( -not $found ) 148 | { 149 | if( $psitem | where $condition ) 150 | { 151 | $found = $true 152 | } 153 | } 154 | } 155 | end 156 | { 157 | $found 158 | } 159 | } 160 | 161 | function Test-All( [scriptblock] $Condition = { $psitem -ne $null } ) 162 | { 163 | <# 164 | .SYNOPSIS 165 | Test if all elements in the piped in input confirm to the condition 166 | 167 | .DESCRIPTION 168 | Useful for functional-style code. Returns $true if all elements in the 169 | piped in input confirm to the specified condition. $false if there is 170 | at least one element that doesn't confirm. 171 | 172 | .PARAMETER Condition 173 | Condition to test for each element. $psitem variable can be used. 174 | By default would return $true for the not null elements. 175 | 176 | .EXAMPLE 177 | "1", "2" | all{ $psitem -eq "2" } 178 | False since we have "1" element that doesn't equal to "2". 179 | 180 | .EXAMPLE 181 | "1", "2" | all 182 | True since all elements are not null. 183 | 184 | .EXAMPLE 185 | $notExistingVariable | all 186 | False since Powershell would create collection with one $null element. 187 | 188 | .EXAMPLE 189 | @() | all 190 | True since there is no element in the input collection that contradicts 191 | the not-null condition. 192 | 193 | .NOTES 194 | Can't use 'break' for this - we would exit all pipelines, 195 | not just the current one 196 | #> 197 | 198 | begin 199 | { 200 | $scannedMatched = $true 201 | } 202 | process 203 | { 204 | if( $scannedMatched ) 205 | { 206 | if( -not ($psitem | where $condition) ) 207 | { 208 | $scannedMatched = $false 209 | } 210 | } 211 | } 212 | end 213 | { 214 | $scannedMatched 215 | } 216 | } 217 | 218 | function Get-First( [scriptblock] $Condition = { $psitem -ne $null } ) 219 | { 220 | <# 221 | .SYNOPSIS 222 | Returns first element in the piped in input that confirms to the condition 223 | 224 | .DESCRIPTION 225 | Useful for functional-style code. Returns first found element that confirms 226 | to the specified condition. Nothing is returned when there are no such 227 | elements. 228 | 229 | .PARAMETER Condition 230 | Condition to test for each element. $psitem variable can be used. 231 | By default would return $true for the not null elements. 232 | 233 | .EXAMPLE 234 | "1", "23", "4" | first{ $psitem.Length -gt 1 } 235 | "1", "2" | first 236 | $notExisting | first 237 | 238 | First check returns "23" since length of this string is greater then 1. 239 | Second check returns "1" since it is first not null element. 240 | Third check returns nothing since Powershell would create collection with 241 | one $null element. 242 | 243 | .NOTES 244 | There is a way how to make it faster. See the response from Jason Shirk: 245 | 246 | I don’t think we expose a clean way to do that. Select-Object –First will 247 | stop a pipeline cleanly, but it does so with an exception type that we don’t 248 | make public. 249 | 250 | Here is an example of how you could implement Find-First combining proxies 251 | and Select-Object – I’ll admit it’s not obvious but it is efficient: 252 | 253 | function Find-First 254 | { 255 | [CmdletBinding()] 256 | param( 257 | [Parameter(ValueFromPipeline=$true)] 258 | [psobject] 259 | ${InputObject}, 260 | 261 | [Parameter(Mandatory=$true, Position=0)] 262 | [scriptblock] 263 | ${FilterScript}) 264 | 265 | begin 266 | { 267 | try { 268 | $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Where-Object', [System.Management.Automation.CommandTypes]::Cmdlet) 269 | $scriptCmd = {& $wrappedCmd @PSBoundParameters | Select-Object -First 1 } 270 | $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) 271 | $steppablePipeline.Begin($PSCmdlet) 272 | } catch { 273 | throw 274 | } 275 | } 276 | 277 | process { try {$steppablePipeline.Process($_)} catch {throw} } 278 | end { try {$steppablePipeline.End()} catch {throw} } 279 | } 280 | #> 281 | 282 | begin 283 | { 284 | # NOTE: Can't use 'break' for this - we would exit all pipelines, 285 | # not just the current one 286 | $resultKnown = $false 287 | } 288 | process 289 | { 290 | if( -not $resultKnown ) 291 | { 292 | if( $psitem | where $condition ) 293 | { 294 | $psitem 295 | $resultKnown = $true 296 | } 297 | } 298 | } 299 | } 300 | 301 | function Get-Last( [scriptblock] $Condition = { $psitem -ne $null } ) 302 | { 303 | <# 304 | .SYNOPSIS 305 | Returns last element in the piped in input that confirms to the condition 306 | 307 | .DESCRIPTION 308 | Useful for functional-style code. Returns last found element that confirms 309 | to the specified condition. Nothing is returned when there are no such 310 | elements. 311 | 312 | .PARAMETER Condition 313 | Condition to test for each element. $psitem variable can be used. 314 | By default would return $true for the not null elements. 315 | 316 | .EXAMPLE 317 | "1", "23", "42", "2" | last{ $psitem.Length -gt 1 } 318 | "1", "2" | last 319 | $notExisting | last 320 | 321 | First check returns "42" since this is last element with length greater 322 | then 1. Second check returns "2" since it is last not null element. 323 | Third check returns nothing since Powershell would create collection 324 | with one $null element. 325 | #> 326 | 327 | begin 328 | { 329 | $lastMatched = $null 330 | } 331 | process 332 | { 333 | if( $psitem | where $condition ) 334 | { 335 | $lastMatched = $psitem 336 | } 337 | } 338 | end 339 | { 340 | if( $lastMatched ) 341 | { 342 | $lastMatched 343 | } 344 | } 345 | } 346 | 347 | function Get-Separation( [scriptblock] $condition ) 348 | { 349 | <# 350 | .SYNOPSIS 351 | Separate collection into two based on some condition 352 | 353 | .DESCRIPTION 354 | Useful for functional-style code. Returns two collections: 355 | - first one stores input elements that tested True in condition 356 | - second one stores input element that tested False in condition 357 | 358 | The separation is implemented fast and uses hash tables inside. 359 | 360 | .PARAMETER Condition 361 | Scriptblock that separates elements in the input collection. 362 | 363 | .EXAMPLE 364 | $large, $small = ls | separate {$_.Length -gt 10kb} 365 | 366 | Separates files in the folder into two categories - large ones 367 | that are bigger than 10kb and smaller ones. 368 | #> 369 | 370 | $separated = $input | Group-Object $condition -AsHashTable 371 | @($separated[$true]), @($separated[$false]) 372 | } -------------------------------------------------------------------------------- /Git.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Initialize-GitConfig 5 | { 6 | <# 7 | .SYNOPSIS 8 | Configure git before the first use; assigns name and 9 | email for the current user and sets up some useful defaults 10 | #> 11 | 12 | [CmdletBinding()] 13 | param 14 | ( 15 | [switch] $Force 16 | ) 17 | 18 | $gitName = git config --global user.name 19 | if( $gitName -and (-not $Force) ) 20 | { 21 | Write-Warning "Looks like git is already configured. If you want to overwrite git config settings anyway, use -Force switch." 22 | return 23 | } 24 | 25 | # Git name and email (required) 26 | if( $env:USERDOMAIN -eq "Redmond" ) 27 | { 28 | # Figure out name of the current user from Active Directory 29 | $ntAccount = New-Object Security.Principal.NTAccount($env:USERDOMAIN, $env:USERNAME) 30 | $sid = $ntAccount.Translate([Security.Principal.SecurityIdentifier]) 31 | $ldap = [adsi] "LDAP://" 32 | 33 | git config --global user.name $ldap.cn 34 | git config --global user.email "$ENV:USERNAME@microsoft.com" 35 | } 36 | else 37 | { 38 | $name = Read-Host "User name" 39 | git config --global user.name $name 40 | 41 | $email = Read-Host "User email" 42 | git config --global user.email $email 43 | } 44 | Write-Output "Git user name and email are configured" 45 | 46 | git config --global --replace-all color.grep auto 47 | git config --global --replace-all color.grep.filename "green" 48 | git config --global --replace-all color.grep.linenumber "cyan" 49 | git config --global --replace-all color.grep.match "magenta" 50 | git config --global --replace-all color.grep.separator "black" 51 | git config --global --replace-all grep.lineNumber true 52 | git config --global --replace-all grep.extendedRegexp true 53 | 54 | git config --global --replace-all color.diff.meta "yellow" 55 | git config --global --replace-all color.diff.frag "cyan" 56 | git config --global --replace-all color.diff.func "cyan bold" 57 | git config --global --replace-all color.diff.commit "yellow bold" 58 | Write-Output "Git defaults are configured" 59 | 60 | # Aliases for the most used commands 61 | git config --global alias.co checkout 62 | git config --global alias.ci commit 63 | git config --global alias.st status 64 | git config --global alias.br branch 65 | git config --global alias.lg "log --graph --pretty=format:'%C(reset)%C(yellow)%h%C(reset) -%C(bold yellow)%d%C(reset) %s %C(green)(%cr) %C(cyan)<%an>%C(reset)' --abbrev-commit --date=relative -n 10" 66 | git config --global alias.gr "grep --break --heading --line-number -iIE" 67 | Write-Output "Git aliases are configured" 68 | } 69 | 70 | function Open-GitExtensions 71 | { 72 | <# 73 | .SYNOPSIS 74 | Open GitExtensions GUI frontend 75 | By default the browse window in the current folder would be opened 76 | 77 | .PARAMETER Args 78 | Any arguments that should be passed to the git extensions 79 | 80 | .PARAMETER NewEnvironment 81 | Use new environment for the process. 82 | 83 | This is a workaround for CoreXT that redefines the available dot net runtimes 84 | and this messes up with the .NET 8 runtime lookup done by the latest GitExtensions 85 | 86 | .EXAMPLE 87 | gite commit 88 | 89 | Open git extension commit dialog for the repo in the current folder 90 | #> 91 | 92 | param 93 | ( 94 | [Parameter( Mandatory = $false )] 95 | [string[]] $args, 96 | [switch] $NewEnvironment 97 | ) 98 | 99 | if( -not (Get-Command GitExtensions.exe -ea Ignore) ) 100 | { 101 | throw "GitExtensions.exe must be discoverable via PATH environment variable" 102 | } 103 | 104 | $param = $args 105 | if( -not $param ) { $param = @("browse") } 106 | 107 | if( $NewEnvironment ) 108 | { 109 | pwsh -nop -c "Start-Process GitExtensions.exe -UseNewEnvironment -WorkingDirectory $pwd -ArgumentList $($param -join ' ')" 110 | } 111 | else 112 | { 113 | & GitExtensions.exe $param 114 | } 115 | } 116 | 117 | function Get-CommitAuthorName( [string] $commit ) 118 | { 119 | <# 120 | .SYNOPSIS 121 | Get author name from a git commit 122 | #> 123 | 124 | git log -1 --pretty=format:'%aN' $commit 125 | } 126 | 127 | function Get-CommitAuthorEmail( [string] $commit ) 128 | { 129 | <# 130 | .SYNOPSIS 131 | Get author email from a git commit 132 | #> 133 | 134 | git log -1 --pretty=format:'%aE' $commit 135 | } 136 | 137 | function Get-CommitAuthorDate( [string] $commit ) 138 | { 139 | <# 140 | .SYNOPSIS 141 | Get author commit date from a git commit 142 | #> 143 | 144 | git log -1 --pretty=format:'%ai' $commit 145 | } 146 | 147 | function Get-CommitMessage( [string] $commit ) 148 | { 149 | <# 150 | .SYNOPSIS 151 | Get commit message from a git commit 152 | #> 153 | 154 | git log -1 --pretty=format:'%B' $commit 155 | } 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | PSToolset 4 | Copyright (c) Microsoft Corporation. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE 23 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | "Rules" = @{ 3 | "PSAvoidUsingCmdletAliases" = @{ 4 | "Whitelist" = @("select", "foreach", "where", "sort", "measure") 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /PSToolset.psd1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | @{ 5 | 6 | # Script module or binary module file associated with this manifest 7 | RootModule = 'PSToolset.psm1' 8 | 9 | # Version number of this module. 10 | # Combined symatic version and the release data are used 11 | # To be backward compatible with the old version scheme, the new major version was bumped to 100 12 | ModuleVersion = '100.0.20241001' 13 | 14 | # ID used to uniquely identify this module 15 | GUID = 'c2b885a6-dafe-4aff-9045-414874b9db36' 16 | 17 | # Author of this module 18 | Author = 'Aleksandr Kostikov, alexko@microsoft.com' 19 | 20 | # Copyright statement for this module 21 | Copyright = '(c) Microsoft Corporation' 22 | 23 | # Description of the functionality provided by this module 24 | Description = 'Toolset for Powershell environment' 25 | 26 | # Minimum version of the Windows PowerShell engine required by this module 27 | PowerShellVersion = '5.0' 28 | 29 | # Cmdlets to export from this module 30 | CmdletsToExport = '*' 31 | 32 | # Variables to export from this module 33 | VariablesToExport = '*' 34 | 35 | # Aliases to export from this module 36 | AliasesToExport = @( 37 | 'all', 38 | 'any', 39 | 'call', 40 | 'construct', 41 | 'default', 42 | 'dfx', 43 | 'f', # filter 44 | 'first', 45 | 'gite', 46 | 'hl', # highlight 47 | 'jn', # jupyter notebook 48 | 'last', 49 | 'lock', 50 | 'p', # project 51 | 'parse', 52 | 'separate', 53 | 'source', 54 | 'xattr', 55 | 'xcomm', 56 | 'xelem', 57 | 'xmlns', 58 | 'xname' 59 | ) 60 | 61 | # Functions to export from this module 62 | FunctionsToExport = @( 63 | # Colors 64 | "Write-Colorized", "Show-Highlight", "Get-Colors", "Get-Source", 65 | # Data 66 | "ConvertTo-PsObject", "ConvertTo-Hash", 67 | "Get-Parameter", "Use-Project", "Use-Filter", 68 | "Get-Ini", "Show-Ini", "ConvertFrom-Ini", "Import-Ini", 69 | # DoxFx 70 | "Start-DocFx", 71 | # Files 72 | "Resolve-ScriptPath", "Get-FileEncoding", 73 | # Functional 74 | "Test-Any", "Test-All", "Get-First", "Get-Last", "Get-Separation", 75 | "Get-Median", "Get-Reverse", "Get-UniqueUnsorted", "Get-Randomized", 76 | # Git 77 | "Initialize-GitConfig", "Open-GitExtensions", 78 | "Get-CommitAuthorName", "Get-CommitAuthorEmail", 79 | "Get-CommitAuthorDate", "Get-CommitMessage", 80 | # Python 81 | "Start-JupyterNotebook", "Stop-JupyterNotebook", 82 | # Security 83 | "Invoke-Elevated", "Test-Interactive", "Test-Elevated", "Set-DelayLock", 84 | # Text 85 | "Use-Parse", "Use-Default", "Format-Template", "Get-UnresolvedTemplateItem", 86 | # Utils 87 | "Use-Retries", "Set-CmdEnvironment", 88 | # Xml 89 | "New-XName", "New-XAttribute", "New-Xmlns", "New-XComment", "New-XElement" 90 | ) 91 | 92 | # Modules that must be imported into the global environment prior to importing this module 93 | RequiredModules = @() 94 | 95 | # List of all files packaged with this module 96 | FileList = 97 | 'Colors.ps1', 98 | 'Data.ps1', 99 | 'DocFx.ps1', 100 | 'Files.ps1', 101 | 'Functional.ps1', 102 | 'Git.ps1', 103 | 'PSToolset.psd1', 104 | 'PSToolset.psm1', 105 | 'Python.ps1', 106 | 'Security.ps1', 107 | 'TabExpansion.ps1', 108 | 'Text.ps1', 109 | 'Utils.ps1', 110 | 'Xml.ps1' 111 | } -------------------------------------------------------------------------------- /PSToolset.psm1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # Scripts use some not-strict mode features 5 | Set-StrictMode -Off 6 | 7 | # Include all used files 8 | . "$PSScriptRoot\Colors.ps1" 9 | . "$PSScriptRoot\Data.ps1" 10 | . "$PSScriptRoot\DocFx.ps1" 11 | . "$PSScriptRoot\Files.ps1" 12 | . "$PSScriptRoot\Functional.ps1" 13 | . "$PSScriptRoot\Git.ps1" 14 | . "$PSScriptRoot\Python.ps1" 15 | . "$PSScriptRoot\Security.ps1" 16 | . "$PSScriptRoot\TabExpansion.ps1" 17 | . "$PSScriptRoot\Text.ps1" 18 | . "$PSScriptRoot\Utils.ps1" 19 | . "$PSScriptRoot\Xml.ps1" 20 | 21 | # Test that no other version of this module is imported 22 | if( Get-Module PSToolset ) 23 | { 24 | Write-Warning 'Several versions of PSToolset detected. Check your $PROFILE and $env:PSModulePath and cleanup extra modules via Remove-Module.' 25 | } 26 | 27 | # Setting up aliases 28 | Set-Alias all Test-All 29 | Set-Alias any Test-Any 30 | Set-Alias call Set-CmdEnvironment 31 | Set-Alias construct ConvertTo-PsObject 32 | Set-Alias default Use-Default 33 | Set-Alias dfx Start-DocFx 34 | Set-Alias first Get-First 35 | Set-Alias f Use-Filter 36 | Set-Alias gite Open-GitExtensions 37 | Set-Alias hl Show-Highlight 38 | Set-Alias jn Start-JupyterNotebook 39 | Set-Alias last Get-Last 40 | Set-Alias lock Set-DelayLock 41 | Set-Alias lookup Get-Lookup 42 | Set-Alias p Use-Project 43 | Set-Alias parse Use-Parse 44 | Set-Alias separate Get-Separation 45 | Set-Alias source Get-Source 46 | Set-Alias xattr New-XAttribute 47 | Set-Alias xcomm New-XComment 48 | Set-Alias xelem New-XElement 49 | Set-Alias xmlns New-Xmlns 50 | Set-Alias xname New-XName -------------------------------------------------------------------------------- /Python.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Start-JupyterNotebook 5 | { 6 | <# 7 | .SYNOPSIS 8 | Start Jupyter Notebook in current folder or $env:DefaultJupyterNotebookPath. 9 | Reuse existing notebook already running if possible. 10 | 11 | .PARAMETER Force 12 | Don't reuse anything and don't use defaults. 13 | Just open a new notebook in the current folder. 14 | 15 | .EXAMPLE 16 | Start-JupyterNotebook 17 | 18 | Tries to reopen a currently opened jupyter notebook. 19 | #> 20 | 21 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 22 | 'PSUseShouldProcessForStateChangingFunctions', '', 23 | Justification = 'Intended to be this way')] 24 | param 25 | ( 26 | [switch] $Force 27 | ) 28 | 29 | # Test if jupyter is installed 30 | if( -not (Get-Command jupyter.exe -ea Ignore) ) 31 | { 32 | throw "jupyter.exe must be discoverable via PATH environment variable, you can install it via Anaconda" 33 | } 34 | 35 | # Cleanup cleanup jobs =) 36 | $cleanupJobName = "Start-JupyterNotebook cleanup" 37 | Get-Job $cleanupJobName -ea Ignore | where State -eq Completed | Remove-Job 38 | 39 | # Helper function 40 | function Open-Notebook( $folder = $pwd ) 41 | { 42 | $ps = Start-Process ` 43 | -FilePath "pwsh" ` 44 | -ArgumentList '-Command "jupyter notebook"' ` 45 | -WorkingDirectory $folder ` 46 | -WindowStyle Hidden ` 47 | -PassThru 48 | 49 | Start-Job -Name $cleanupJobName { 50 | Start-Sleep -Seconds 60 51 | $ps | Stop-Process 52 | } | Out-Null 53 | } 54 | 55 | # When need to open new notebook in current folder 56 | if( $Force ) 57 | { 58 | "Open new jupyter notebook in current folder $pwd" 59 | Open-Notebook 60 | return 61 | } 62 | 63 | # Trying to reuse opened notebooks if possible 64 | if( Get-Process jupyter -ea Ignore ) 65 | { 66 | "Found existing jupyter notebook, reopening default URL" 67 | Start-Process http://localhost:8888/ 68 | return 69 | } 70 | 71 | # Run notebook from default location if possible 72 | if( $env:DefaultJupyterNotebookPath ) 73 | { 74 | "Open new jupyter notebook in `$env:DefaultJupyterNotebookPath = $env:DefaultJupyterNotebookPath" 75 | Open-Notebook $env:DefaultJupyterNotebookPath 76 | } 77 | else 78 | { 79 | "Open new jupyter notebook in current folder $pwd, note that you can use `$env:DefaultJupyterNotebookPath instead if you define it" 80 | Open-Notebook 81 | } 82 | } 83 | 84 | function Stop-JupyterNotebook 85 | { 86 | <# 87 | .SYNOPSIS 88 | Stop all Jupyter Notebooks running 89 | #> 90 | 91 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 92 | 'PSUseShouldProcessForStateChangingFunctions', '', 93 | Justification = 'Intended to be this way')] 94 | param() 95 | 96 | # Test if jupyter is installed 97 | if( -not (Get-Command jupyter.exe -ea Ignore) ) 98 | { 99 | throw "jupyter.exe must be discoverable via PATH environment variable, you can install it via Anaconda" 100 | } 101 | 102 | # Stop all opened notebooks, even crashed ones 103 | jupyter notebook list | 104 | Use-Parse "localhost:(\d+)" | 105 | foreach{ jupyter notebook stop $psitem } 106 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The purpose and state of the repository 2 | This module represents a set of tools for Powershell that I find useful to have available in every Powershell console. It was started as a helper project for some daily work activities and polished over the years as an internal project. Some recent commands rely on Powershell 7 syntax, so the module is marked for Powershell 7, although most of the commands actually should work in older Powershell versions. 3 | 4 | You are welcome to use and contribute. 5 | 6 | # Installation 7 | To make the module auto-discoverable by Powershell, clone it into your Powershell Modules folder without changing it's name: 8 | 9 | ```powershell 10 | $modulesFolder = $env:PSModulePath -split ";" | select -f 1 11 | mkdir $modulesFolder -ea Ignore 12 | cd $modulesFolder 13 | git clone https://github.com/microsoft/PSToolset 14 | ``` 15 | 16 | Powershell should be able to discover module commands after that. If it doesn't you can import the module explicitly 17 | 18 | ```powershell 19 | Import-Module PSToolset 20 | ``` 21 | 22 | # Documentation 23 | List all exported commands from the module: 24 | 25 | ```powershell 26 | Get-Command -Module PSToolset 27 | ``` 28 | 29 | Get detailed help for a particular function: 30 | ``` powershell 31 | man Set-CmdEnvironment -Detailed 32 | ``` 33 | 34 | Get examples for a particular function: 35 | ```powershell 36 | man Show-Highlight -Examples 37 | ``` 38 | 39 | See implementation details in color: 40 | ```powershell 41 | source construct 42 | ``` 43 | 44 | ![Source output](media/source-output.png) 45 | 46 | # Commands 47 | ## Colors 48 | Name | Alias | Description 49 | -----|-------|------------- 50 | Get-Colors | | Print all console host colors to the console in color 51 | Get-Source | source | Print source code of a command or script in color 52 | Show-Highlight | hl | Highlight portion of some text to make it visually easier to find something in the text 53 | Write-Colorized | | Output object to stdout with specific color 54 | 55 | ## Data 56 | Name | Alias | Description 57 | -----|-------|------------- 58 | ConvertFrom-Ini | | Converts ini strings into Powershell hashtable object 59 | ConvertTo-Hash | | Convert an object into a hash table 60 | ConvertTo-PsObject | construct | Convert a set of variables into a PsObject 61 | Get-Ini | | Parse INI file as a hashtable object 62 | Get-Parameter | | Get names of all available parameters from input objects 63 | Import-Ini | | Imports ini file into Powershell hashtable object 64 | Show-Ini | | Print contents of INI parsed file, received from Get-Ini command 65 | Use-Filter | f | Regex based parameter filter for input objects 66 | Use-Project | p | Project several parameters from input objects 67 | 68 | ## Files 69 | Name | Alias | Description 70 | -----|-------|------------- 71 | Get-FileEncoding | | Gets file encoding 72 | Resolve-ScriptPath | | Resolve path that is local to the script 73 | 74 | ## Functional 75 | Name | Alias | Description 76 | -----|-------|------------- 77 | Get-First | first | Returns first element in the piped in input that confirms to the condition 78 | Get-Last | last | Returns last element in the piped in input that confirms to the condition 79 | Get-Median | | Calculate median of numeric array piped in 80 | Get-Randomized | | Randomize a sequence that is piped in 81 | Get-Reverse | | Reverse a sequence that is piped in 82 | Get-Separation | separate | Separate collection into two based on some condition 83 | Get-UniqueUnsorted | | Get unique values from an unsorted array 84 | Test-All | all | Test if all elements in the piped in input confirm to the condition 85 | Test-Any | any | Test if any element in the piped in input confirms to the condition 86 | 87 | ## Git 88 | Name | Alias | Description 89 | -----|-------|------------- 90 | Get-CommitAuthorDate | | Get author commit date from a git commit 91 | Get-CommitAuthorEmail | | Get author email from a git commit 92 | Get-CommitAuthorName | | Get author name from a git commit 93 | Get-CommitMessage | | Get commit message from a git commit 94 | Initialize-GitConfig | | Configure git before the first use; assigns name and email for the current user and sets up some useful defaults 95 | Open-GitExtensions | gite | Open GitExtensions GUI frontend, by default browse window in the current folder would be opened 96 | 97 | ## Python 98 | Name | Alias | Description 99 | -----|-------|------------- 100 | Start-JupyterNotebook | jn | Start Jupyter Notebook in current folder or $env:DefaultJupyterNotebookPath. Reuse existing notebook already running if possible 101 | Stop-JupyterNotebook | | Stop all Jupyter Notebooks running 102 | 103 | ## Security 104 | Name | Alias | Description 105 | -----|-------|------------- 106 | Invoke-Elevated | | Invoke script in elevated Powershell session 107 | Set-DelayLock | lock | Lock machine after the specified timeout 108 | Test-Elevated | | Test if current Powershell session is elevated 109 | Test-Interactive | | Determine if the current Powershell session is interactive 110 | 111 | ## Text 112 | Name | Alias | Description 113 | -----|-------|------------- 114 | Format-Template | | Render text template 115 | Get-UnresolvedTemplateItem | | Find template items that were not resolved yet 116 | Use-Default | default | Define default value if input is null, false or missing 117 | Use-Parse | parse | Parse incoming text to find relevant pieces in it 118 | 119 | ## Utils 120 | Name | Alias | Description 121 | -----|-------|------------- 122 | Set-CmdEnvironment | call | Call .bat or .cmd file and preserve all environment variables set by it 123 | Use-Retries | | Retry execution of a script that throws an exception 124 | 125 | ## Xml 126 | Name | Alias | Description 127 | -----|-------|------------- 128 | New-XAttribute | xattr | Create XAttribute object with specified name and value 129 | New-XComment | xcomm | Create XComment object with specified value 130 | New-XElement | xelem | Create XElement object and attach specified via script blocks other XObjects in a hierarchal form 131 | New-Xmlns | xmlns | Create Xmlns object with specified namespace and value 132 | New-XName | xname | Create XName object with specified name 133 | 134 | 135 | # How to regenerate table of exported commands 136 | ``` powershell 137 | 138 | Import-Module PSToolset 139 | $functions = get-module pstoolset | % ExportedFunctions | % Keys 140 | $aliases = get-module pstoolset | % ExportedAliases | % Keys 141 | $map = @{} 142 | 143 | foreach( $function in $functions ) 144 | { 145 | $map.$function = @{} 146 | $map.$function.Name = $function 147 | $map.$function.File = gi (ls function: | where Name -eq $function| % ScriptBlock | % File) | % BaseName 148 | 149 | $map.$function.Description = man $function | select -skip 5 -First 10 | % Trim 150 | $map.$function.Description = foreach( $line in $map.$function.Description ) 151 | { 152 | if( -not $line ){ break } 153 | $line 154 | } 155 | $map.$function.Description = $map.$function.Description -join " " 156 | } 157 | 158 | foreach( $alias in $aliases ) 159 | { 160 | $function = get-alias $alias | % ResolvedCommand | % Name 161 | $map.$function.Alias = $alias 162 | } 163 | 164 | $parsed = $map.Keys | %{ [PsCustomObject] $map.$psitem } 165 | $groups = $parsed | group File 166 | 167 | $table = foreach( $group in $groups ) 168 | { 169 | "## $($group.Name)" 170 | "Name | Alias | Description" 171 | "-----|-------|-------------" 172 | foreach( $element in $group.Group | sort Name ) 173 | { 174 | "$($element.Name) | $($element.Alias) | $($element.Description)" 175 | } 176 | "" 177 | } 178 | $table | clip 179 | 180 | "Table is saved to Windows clipboard" 181 | 182 | ``` 183 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /Security.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function Invoke-Elevated 5 | { 6 | <# 7 | .SYNOPSIS 8 | Invoke script in elevated Powershell session 9 | 10 | .DESCRIPTION 11 | Executes script in an elevated Powershell session. If elevation is needed, 12 | user is prompted via UAC, new elevated process is created, input and output 13 | objects are transferred between processes. 14 | 15 | Beware that not all objects are deserialized well by internally used 16 | Import-CliXml. If output received is unreadable try using | Out-String 17 | at the end of the script. 18 | 19 | .PARAMETER Scriptblock 20 | Script block that needs to be invoked in elevated session. 21 | 22 | .PARAMETER State 23 | State object that would be passes as argument to the executed script. 24 | On elevation state is serialized via Export-CliXml. 25 | 26 | .EXAMPLE 27 | PS> $name = "test" 28 | PS> $cred = Get-Credential 29 | PS> $state = construct name cred 30 | PS> Invoke-Elevated { param( $state ) $state; Test-Elevated } $state 31 | 32 | This sample shows how to call script in elevated session and pass a 33 | complex argument into it. Test-Elevated would return true here. 34 | 35 | .NOTES 36 | For text output it is possible to redirect it to the main program in async 37 | way. But that would not work for Powershell objects. 38 | #> 39 | 40 | param 41 | ( 42 | [Parameter(Mandatory = $true)] [ScriptBlock] $Scriptblock, 43 | [object] $State 44 | ) 45 | 46 | # Do direct invoke if we are already elevated 47 | if( Test-Elevated ) 48 | { 49 | return & $scriptblock $state 50 | } 51 | 52 | # Prepare input and output files 53 | $stateFile = [IO.Path]::GetTempFileName() 54 | $outputFile = [IO.Path]::GetTempFileName() 55 | $state | Export-CliXml -Depth 1 $stateFile 56 | 57 | # Prepare encoded command to be called 58 | $commandString = @" 59 | Set-Location '$($pwd.Path)' 60 | `$state = Import-CliXml '$stateFile' 61 | `$output = & { $($scriptblock.ToString()) } `$state *>&1 62 | `$output | Export-CliXml -Depth 1 '$outputFile' 63 | "@ 64 | $commandBytes = [Text.Encoding]::Unicode.GetBytes($commandString) 65 | $commandEncoded = [Convert]::ToBase64String($commandBytes) 66 | $commandLine = "-EncodedCommand $commandEncoded" 67 | 68 | # Start elevated PowerShell process 69 | try 70 | { 71 | $process = Start-Process ` 72 | -FilePath (Get-Command powershell).Definition ` 73 | -ArgumentList $commandLine ` 74 | -WindowStyle Hidden ` 75 | -Verb RunAs ` 76 | -Passthru 77 | } 78 | catch 79 | { 80 | # This is to make cancelled UAC a terminating error 81 | # -ea Stop doesn't work here for some reason 82 | throw 83 | } 84 | $process.WaitForExit() 85 | 86 | # Return output to the user and cleaning up 87 | Import-CliXml $outputFile 88 | Remove-Item $outputFile 89 | Remove-Item $stateFile 90 | } 91 | 92 | function Test-Interactive 93 | { 94 | <# 95 | .SYNOPSIS 96 | Determine if the current Powershell session is interactive 97 | 98 | .DESCRIPTION 99 | Interactive shell should have human being observing it =) You can ask 100 | something him/her via Read-Host command. If there is no human being, 101 | no reason to ask, right? 102 | 103 | .EXAMPLE 104 | Test-Interactive 105 | 106 | Would return true for a regular Powershell session. 107 | Would return false for an automation job. 108 | Would return false for a remote session. 109 | Does not detect -NonInteractive Powershell calling argument. 110 | #> 111 | 112 | [Environment]::UserInteractive 113 | } 114 | 115 | function Test-Elevated 116 | { 117 | <# 118 | .SYNOPSIS 119 | Test if current Powershell session is elevated 120 | 121 | .DESCRIPTION 122 | Several commands need to be executed in an elevated session to 123 | have administrator rights. This function allows safely and robustly 124 | detect if current session is elevated. 125 | 126 | .EXAMPLE 127 | Test-Elevated 128 | 129 | Would return true for an elevated Powershell session with administrator 130 | rights. 131 | Would return false for a regular Powershell session. 132 | Would return true for a remote Powershell session that is started under 133 | user that is a local administrator (by default in Powershell 3.0/Windows 134 | there is no way of running not elevated remote session if the user in in 135 | the administrator group). 136 | #> 137 | 138 | $identity = [Security.Principal.WindowsIdentity]::GetCurrent() 139 | $principal = [Security.Principal.WindowsPrincipal] $identity 140 | $role = [Security.Principal.WindowsBuiltInRole] "Administrator" 141 | $principal.IsInRole($role) 142 | } 143 | 144 | function Set-DelayLock 145 | { 146 | <# 147 | .SYNOPSIS 148 | Lock machine after the specified timeout 149 | #> 150 | 151 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 152 | 'PSUseShouldProcessForStateChangingFunctions', '', 153 | Justification='Intended to be this way')] 154 | param 155 | ( 156 | [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Minutes")] [int] $Minutes, 157 | [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "TimeSpan")] [timespan] $Timeout 158 | ) 159 | 160 | if( $Minutes ) 161 | { 162 | $Timeout = [timespan]::FromMinutes($Minutes) 163 | } 164 | 165 | "Setting timer for $timeout" 166 | "Computer would lock at $((Get-Date) + $timeout)" 167 | Start-Job -ArgumentList ($timeout.TotalSeconds) -ScriptBlock { 168 | Start-Sleep -Seconds $args[0] 169 | rundll32.exe user32.dll,LockWorkStation 170 | } | Out-Null 171 | } -------------------------------------------------------------------------------- /TabExpansion.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 5 | "PSAvoidGlobalVars", "", 6 | Justification = "We need global PSToolsetAutoCompleteOptions here")] 7 | param() 8 | 9 | # Test if we did that override already, PSToolset can be 10 | # loaded multiple times to one Powershell session 11 | if( $GLOBAL:PSToolsetAutoCompleteOptions ) 12 | { 13 | return 14 | } 15 | 16 | # Hook into the tab complete function 17 | $function:TabExpansion2 = $Function:TabExpansion2 -replace 'End\r\n{', (@' 18 | End 19 | { 20 | if ($options -ne $null) 21 | { 22 | $options += $GLOBAL:PSToolsetAutoCompleteOptions 23 | } 24 | else 25 | { 26 | $options = $GLOBAL:PSToolsetAutoCompleteOptions 27 | } 28 | '@) 29 | 30 | # Overrides 31 | $GLOBAL:PSToolsetAutoCompleteOptions = @{ CustomArgumentCompleters = @{}; NativeArgumentCompleters = @{} } 32 | 33 | $GLOBAL:PSToolsetAutoCompleteOptions['NativeArgumentCompleters']['git'] = 34 | { 35 | param( $completed, $ast ) 36 | 37 | if( -not $completed ) 38 | { 39 | $completed = "." 40 | } 41 | 42 | $gitCommand = $ast.CommandElements[1].Value 43 | switch -regex ($gitCommand) 44 | { 45 | "^(co|checkout|br|branch|rebase|merge)$" 46 | { 47 | git branch | parse "^\*?\s+(.+)" | where{ $psitem -match $completed } 48 | } 49 | "^(fetch|pull)$" 50 | { 51 | git remote | where{ $psitem -match $completed } 52 | } 53 | } 54 | } 55 | 56 | <# Sample how to make similar tab expansion for Powershell commands 57 | 58 | $GLOBAL:PSToolsetAutoCompleteOptions['CustomArgumentCompleters']['Remove-Outgoing:Branch'] = 59 | { 60 | param($commandName, $parameterName, $completed, $commandAst, $fakeBoundParameter) 61 | 62 | if( -not $completed ) 63 | { 64 | $completed = "." 65 | } 66 | 67 | git branch | parse "^\*?\s+(.+)" | where{ $psitem -match $completed } 68 | } 69 | 70 | #> -------------------------------------------------------------------------------- /Text.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | filter Use-Parse 5 | { 6 | <# 7 | .SYNOPSIS 8 | Parse incoming text to find relevant pieces in it 9 | 10 | .DESCRIPTION 11 | Parses incoming stream of strings and matches each string to a regex 12 | pattern. The regex pattern must define regex groups that are used to 13 | capture extracted text. The extracted text is stored in output object 14 | that is constructed dynamically. Each regex group match would be stored 15 | in that object as string property with name defined by $args unbound 16 | parameters. If no unbound parameter is specified, the first group match 17 | is returned. 18 | 19 | .PARAMETER Pattern 20 | Regex used in matching. Regex groups are used to define significant 21 | pieces of text that are extracted from incoming stream. 22 | 23 | .PARAMETER Args 24 | The $args array (the unbound command parameters) contain names of 25 | properties that would be used to capture each regex group match. 26 | Each property would be populated with corresponding regex group match 27 | value in the order of their definition. 28 | 29 | If no args are passed, the first regex group match value is returned 30 | as result. 31 | 32 | .PARAMETER Enforce 33 | Specify this flag if all passed object must match the regex pattern. 34 | If mismatch is found, an exception is thrown. 35 | By default: not set, all not matching elements would be silently skipped. 36 | 37 | .EXAMPLE 38 | "Info string ABC encoded in text CDE" | parse "string (.+) encoded in text (.+)$" First Second 39 | 40 | Parses string and extracts object with string properties First = "ABC" and Second = "CDE" 41 | 42 | .EXAMPLE 43 | "line 1", "line 2", "line three" | parse "line (\d+)" 44 | 45 | Parses incoming array of strings and extracts numeric line numbers. 46 | Last line that contain mismatched text is ignored. 47 | 48 | .EXAMPLE 49 | "line 1", "line 2", "line three" | parse "line (\d+)" -Enforce 50 | 51 | Parses incoming array of strings and extracts numeric line numbers. 52 | Last line that contain mismatched text throws an exception. 53 | #> 54 | 55 | param 56 | ( 57 | [string] $Pattern = $(throw "You must specify the pattern"), 58 | [switch] $Enforce 59 | ) 60 | 61 | if( $psitem -notmatch $pattern ) 62 | { 63 | # Process not matching elements 64 | if( $enforce ) 65 | { 66 | throw "Failed to parse: $($psitem)" 67 | } 68 | else 69 | { 70 | return 71 | } 72 | } 73 | 74 | if( -not $args ) 75 | { 76 | return $matches[ 1 ] 77 | } 78 | 79 | $bag = @{} 80 | 81 | for( $i = 0; $i -lt $args.Count; $i += 1 ) 82 | { 83 | $bag[$args[$i]] = $matches[$i + 1] 84 | } 85 | 86 | New-Object PsObject -Property $bag | select $args 87 | } 88 | 89 | function Use-Default 90 | { 91 | <# 92 | .SYNOPSIS 93 | Define default value if input is null, false or missing 94 | 95 | .EXAMPLE 96 | "test" | default "UNKNOWN" 97 | 1,2,2 | default "UNKNOWN" 98 | $false | default "UNKNOWN" 99 | $null | default "UNKNOWN" 100 | $null | select -f 1 | default "null" 101 | $head | parse "^([\d-]+\s[\d:]+)z" | foreach{ Get-Date -date $psitem } | default ([datetime]::MaxValue) 102 | #> 103 | 104 | param 105 | ( 106 | $unsetValue = $null 107 | ) 108 | 109 | begin 110 | { 111 | $noElements = $true 112 | } 113 | process 114 | { 115 | $noElements = $false 116 | if( $psitem ) { $psitem } else { $unsetValue } 117 | } 118 | end 119 | { 120 | if( $noElements ) { $unsetValue } 121 | } 122 | } 123 | 124 | filter Format-Template( [string] $Template = $(throw "Template is mandatory") ) 125 | { 126 | <# 127 | .SYNOPSIS 128 | Render text template 129 | 130 | .DESCRIPTION 131 | This function is used to render text from templates and variables that 132 | store template-specific information. When {property_name} text is 133 | encountered in the template, the function would try to resolve it via: 134 | - property of the piped in variables with the same name 'property_name'. 135 | - Powershell variable with the same name 'property_name'. Out-String would 136 | be used in that case. 137 | 138 | If multiple objects are piped in, the rendered text would be rendered for 139 | each object separately. 140 | 141 | That allows to conveniently generate text from data. 142 | 143 | .PARAMETER Template 144 | Template string to be used. Any occurrence of {property_name} would be 145 | tried to be resolved. If the property can't be resolved, it is left as 146 | it is in the template. 147 | 148 | .EXAMPLE 149 | $podXml = $edge | Format-Template @' 150 | 151 | {PowerRendered} 152 | {ServerRendered} 153 | 154 | '@ 155 | 156 | Template used here would use both properties from $edge collection objects 157 | (PodName,RackFloor, PodType,City that are specific to a concrete Edge) and 158 | from already rendered text (PowerRendered, ServerRendered). 159 | 160 | .LINK 161 | Get-UnresolvedTemplateItem 162 | #> 163 | 164 | $values = $psitem 165 | $keys = Get-UnresolvedTemplateItem $template 166 | $lastFrameVariables = (Get-PSCallStack)[1].GetFrameVariables() 167 | $result = $template 168 | 169 | foreach( $name in $keys ) 170 | { 171 | $value = $values.$name 172 | if( -not $value ) 173 | { 174 | $value = if( $lastFrameVariables.ContainsKey($name) ) 175 | { 176 | $lastFrameVariables[$name].Value 177 | } 178 | else 179 | { 180 | Get-Variable $name -ValueOnly -ea Ignore 181 | } 182 | $value = $value | Out-String | foreach TrimEnd 183 | } 184 | if( -not $value ) 185 | { 186 | continue 187 | } 188 | $result = $result.Replace("{" + $name + "}", $value) 189 | } 190 | 191 | $result 192 | } 193 | 194 | function Get-UnresolvedTemplateItem( [string] $Template = $(throw "Template is mandatory") ) 195 | { 196 | <# 197 | .SYNOPSIS 198 | Find template items that were not resolved yet 199 | 200 | .PARAMETER Template 201 | Template string to be used. Any occurrence of {property_name} would be 202 | tried to be resolved. If the property can't be resolved, it is left as 203 | it is in the template. 204 | 205 | .EXAMPLE 206 | Get-UnresolvedTemplateItem "{templateItem} is an unresolved template item" 207 | 208 | Finds that 'templateItem' is an unresolved template item. 209 | 210 | .LINK 211 | Format-Template 212 | #> 213 | 214 | [regex]::Matches($template, "\{([^}]+)\}") | foreach{ $psitem.Groups[1].Value } 215 | } -------------------------------------------------------------------------------- /Utils.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 5 | "PSAvoidGlobalVars", "", 6 | Justification = "We need global PSToolsetLastRetryError here")] 7 | param() 8 | 9 | function Use-Retries 10 | { 11 | <# 12 | .SYNOPSIS 13 | Retry execution of a script that throws an exception 14 | 15 | .DESCRIPTION 16 | Retries to execute the 'Action' script. If any exception is thrown, 17 | next sleep interval is taken from 'RetryIntervalsInMinutes' array. 18 | If all retries fail an error is thrown. 19 | 20 | -Verbose would print retry log to verbose stream 21 | 22 | .PARAMETER Action 23 | Script block that is used for retries. 24 | 25 | .PARAMETER RetryIntervalsInMinutes 26 | Array of retry intervals used between retries. 27 | Could be empty (the case of a single execution of the action script 28 | without any retries) Could not be null. 29 | 30 | .EXAMPLE 31 | Use-Retries $sendMail (0.1, 1, 5, 10, 30) 32 | 33 | Retry $sendMail script block. In case of any exception happened during 34 | the script block execution perform a retry. Retries should be done with 35 | gradually increasing retry time interval. If all retries failed then 36 | error would be thrown. 37 | #> 38 | param 39 | ( 40 | [Parameter(Mandatory = $true)] 41 | [scriptblock] $Action, 42 | [ValidateNotNull()] 43 | [double[]] $RetryIntervalsInMinutes 44 | ) 45 | 46 | # Perform action with retries 47 | $command = Get-PSCallStack | select -Skip 1 -First 1 | foreach{ "{0} from {1}" -f $psitem.Command, $psitem.Location } | Out-String | foreach Trim 48 | $retryIntervalsInMinutes += 0 49 | 50 | foreach( $interval in $retryIntervalsInMinutes ) 51 | { 52 | try 53 | { 54 | return & $action 55 | } 56 | catch 57 | { 58 | $GLOBAL:PSToolsetLastRetryError = $psitem 59 | 60 | Write-Verbose "Retryable action $command failed with error:" 61 | Expand-Exception $GLOBAL:PSToolsetLastRetryError.Exception | Write-Verbose 62 | Write-Verbose $GLOBAL:PSToolsetLastRetryError.InvocationInfo.PositionMessage 63 | Write-Verbose "Waiting before the next retry attempt: $interval (minutes)" 64 | 65 | Start-Sleep -Seconds ($interval * 60) 66 | } 67 | } 68 | 69 | throw "$command failed after $($retryIntervalsInMinutes.Count) attempts. See last error is stored in " + '$GLOBAL:PSToolsetLastRetryError or see verbose log for inner exceptions.' 70 | } 71 | 72 | function Set-CmdEnvironment 73 | { 74 | <# 75 | .SYNOPSIS 76 | Call .bat or .cmd file and preserve all environment variables set by it 77 | 78 | .DESCRIPTION 79 | Calls .bat or .cmd file, asynchronously prints all stdout and stderr output 80 | from it and saves all environment variables that the file sets into the 81 | current Powershell session. 82 | - Stderr is outputted into stdout. 83 | - Output coloring is not preserved. 84 | 85 | .PARAMETER Script 86 | Path to .bat or .cmd script to execute. 87 | 88 | .PARAMETER Parameters 89 | Optional .bat or .cmd script parameters. 90 | 91 | .PARAMETER InheritPSModulePath 92 | Set this switch if you want to inherit $env:PSModulePath from the 93 | current process. This switch was made as a workaround. When you call 94 | cmd that calls old powershell.exe it by default would try to use 95 | modules from pwsh and would fail. So instead we change that default 96 | to populate PSModulePath from machine and user environment variables 97 | instead. 98 | 99 | We don't do that for the whole environment as Start-Process switch 100 | -UseNewEnvironment does since this way we are missing essential parts 101 | of the environment that turned out to be quite needed. 102 | 103 | .PARAMETER PreservePSModulePath 104 | Set this switch if you want to keep PSModulePath from the current process. 105 | Without this switch it would be set to whatever the called .bat or .cmd 106 | script sets it. So if you have a script that explicitly uses old Powershell 107 | instead of pwsh you'll end up with parts of PsModulePath missing. 108 | 109 | .EXAMPLE 110 | Set-CmdEnvironment set-env-variables.bat 111 | 112 | Will execute 'set-env-variables.bat' script, dump all environment variables 113 | and transfer them into Powershell host. All original output will be shown as 114 | well. 115 | #> 116 | 117 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 118 | 'PSUseShouldProcessForStateChangingFunctions', '', 119 | Justification = 'Intended to be this way')] 120 | param 121 | ( 122 | [Parameter( Mandatory = $true )] 123 | [ValidatePattern('^.+(\.bat|\.cmd|\.exe)$')] 124 | [string] $Script, 125 | [string] $Parameters, 126 | [switch] $InheritPSModulePath, 127 | [switch] $PreservePSModulePath 128 | ) 129 | 130 | # Showing progress 131 | $info = "Calling $script $parameters" 132 | $lastProgressOutput = "Initialization" 133 | Write-Progress $info $lastProgressOutput 134 | 135 | # Preserve PSModulePath 136 | if( $PreservePSModulePath ) 137 | { 138 | $preservedPSModulePath = $env:PSModulePath 139 | } 140 | 141 | # Helper objects 142 | $preserved, $GLOBAL:shared = $GLOBAL:shared, @{ 143 | marker = [Guid]::NewGuid().ToString() 144 | afterMarker = $false 145 | lineQueue = New-Object Collections.Concurrent.ConcurrentQueue[string] 146 | } 147 | $line = "" 148 | $lineQueue = $GLOBAL:shared.lineQueue 149 | 150 | # Initialize process object 151 | $process = [Diagnostics.Process] @{ 152 | StartInfo = [Diagnostics.ProcessStartInfo] @{ 153 | FileName = (Get-Command 'cmd').Definition 154 | Arguments = "/c `"$script`" $parameters & echo $($GLOBAL:shared.marker) & set" 155 | WorkingDirectory = (Get-Location).Path 156 | UseShellExecute = $false 157 | RedirectStandardError = $true 158 | RedirectStandardOutput = $true 159 | RedirectStandardInput = $false 160 | } 161 | } 162 | 163 | # Check if we need to reinitialize PSModulePath 164 | if( -not $inheritPSModulePath ) 165 | { 166 | if( $process.StartInfo.EnvironmentVariables.ContainsKey("PSModulePath") ) 167 | { 168 | $process.StartInfo.EnvironmentVariables.Remove("PSModulePath") | Out-Null 169 | } 170 | 171 | $value = @( 172 | [Environment]::GetEnvironmentVariable("PSModulePath", "Machine") + ";" + 173 | [Environment]::GetEnvironmentVariable("PSModulePath", "User") 174 | ) 175 | $process.StartInfo.EnvironmentVariables.Add("PSModulePath", $value) | Out-Null 176 | } 177 | 178 | try 179 | { 180 | # Hook into the standard output and error stream events 181 | $stdoutJob = Register-ObjectEvent $process OutputDataReceived -Action ` 182 | { 183 | if( $GLOBAL:shared.afterMarker ) 184 | { 185 | $GLOBAL:output += $eventArgs.Data 186 | $split = $eventArgs.Data -split "=" 187 | $value = ($split | select -Skip 1) -join "=" 188 | Set-Content "env:\$($split[0])" $value 189 | } 190 | else 191 | { 192 | $GLOBAL:shared.afterMarker = $eventArgs.Data.Trim() -eq $GLOBAL:shared.marker 193 | if( -not $GLOBAL:shared.afterMarker ) { $GLOBAL:shared.lineQueue.Enqueue($eventArgs.Data) } 194 | } 195 | } 196 | $stderrJob = Register-ObjectEvent $process ErrorDataReceived -Action ` 197 | { 198 | $GLOBAL:shared.lineQueue.Enqueue($eventArgs.Data) 199 | } 200 | 201 | # Start process and start async read from stdout and stderr 202 | $process.Start() | Out-Null 203 | $process.BeginOutputReadLine() 204 | $process.BeginErrorReadLine() 205 | 206 | # Stopwatches that we use 207 | $totalStopwatch = [System.Diagnostics.Stopwatch]::new() 208 | $totalStopwatch.Restart() 209 | 210 | $stopwatch = [System.Diagnostics.Stopwatch]::new() 211 | $stopwatch.Restart() 212 | 213 | # Wait until process exit and dump stdout and stderr from it 214 | while( -not $process.HasExited ) 215 | { 216 | $newOutput = $false 217 | 218 | while( $lineQueue.TryDequeue([ref] $line) ) 219 | { 220 | $line 221 | 222 | $lastProgressOutput = if( [string]::IsNullOrWhiteSpace($line) ) 223 | { 224 | "..." 225 | } 226 | else 227 | { 228 | $line 229 | } 230 | 231 | Write-Progress $info $lastProgressOutput 232 | 233 | $newOutput = $true 234 | $stopwatch.Restart() 235 | } 236 | 237 | Start-Sleep -Milliseconds 100 238 | 239 | if( -not $newOutput ) 240 | { 241 | if( $PSVersionTable.PSVersion -ge 7.2 ) 242 | { 243 | $totalText = $totalStopwatch.Elapsed.ToString("hh\:mm\:ss\.f") 244 | $localText = $stopwatch.Elapsed.ToString("hh\:mm\:ss\.f") 245 | $output = "Total $totalText | Current $localText" 246 | Write-Progress $info $output 247 | } 248 | else 249 | { 250 | Write-Progress $info $lastProgressOutput -CurrentOperation $stopwatch.Elapsed.ToString("hh\:mm\:ss\.f") 251 | } 252 | } 253 | } 254 | } 255 | finally 256 | { 257 | # Cleanup that would work even if Ctrl+C is hit 258 | $process.CancelOutputRead() 259 | $process.CancelErrorRead() 260 | $process.Close() 261 | Remove-Job $stdoutJob -Force 262 | Remove-Job $stderrJob -Force 263 | $GLOBAL:shared = $preserved 264 | } 265 | 266 | # Draining line queue 267 | while( $lineQueue.TryDequeue([ref] $line) ) 268 | { 269 | Write-Progress $info $lastProgressOutput 270 | $line 271 | } 272 | 273 | # Restore PSModulePath 274 | if( $PreservePSModulePath ) 275 | { 276 | $env:PSModulePath = $preservedPSModulePath 277 | } 278 | 279 | Write-Progress $info "Done" -Completed 280 | } -------------------------------------------------------------------------------- /Xml.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function New-XName 5 | { 6 | <# 7 | .SYNOPSIS 8 | Create XName object with specified name 9 | 10 | .EXAMPLE 11 | xname some_xname 12 | #> 13 | 14 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 15 | 'PSUseShouldProcessForStateChangingFunctions', '', 16 | Justification = 'Intended to be this way')] 17 | param 18 | ( 19 | [Parameter(Mandatory = $true)] 20 | [string] $Name 21 | ) 22 | 23 | [Xml.Linq.XName] $name 24 | } 25 | 26 | function New-XAttribute 27 | { 28 | <# 29 | .SYNOPSIS 30 | Create XAttribute object with specified name and value 31 | 32 | .EXAMPLE 33 | ls | xelem "Files" ` 34 | {xattr Name $psitem.Name}, 35 | {xattr Length $psitem.Length}, 36 | #> 37 | 38 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 39 | 'PSUseShouldProcessForStateChangingFunctions', '', 40 | Justification = 'Intended to be this way')] 41 | param 42 | ( 43 | [Parameter(Mandatory = $true)] 44 | [string] $Name, 45 | [string] $Value 46 | ) 47 | 48 | New-Object Xml.Linq.XAttribute $name, $value 49 | } 50 | 51 | function New-Xmlns 52 | { 53 | <# 54 | .SYNOPSIS 55 | Create Xmlns object with specified namespace and value 56 | 57 | .EXAMPLE 58 | "Some", "Collection" | xelem "Element" ` 59 | {xmlns xsd "http://www.w3.org/2001/XMLSchema"}, 60 | {xmlns xsi "http://www.w3.org/2001/XMLSchema-instance"} 61 | #> 62 | 63 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 64 | 'PSUseShouldProcessForStateChangingFunctions', '', 65 | Justification = 'Intended to be this way')] 66 | param 67 | ( 68 | [Parameter(Mandatory = $true)] 69 | [string] $Namespace, 70 | [Parameter(Mandatory = $true)] 71 | [string] $Value 72 | ) 73 | 74 | New-XAttribute ([Xml.Linq.XNamespace]::Xmlns + $namespace) $value 75 | } 76 | 77 | function New-XComment 78 | { 79 | <# 80 | .SYNOPSIS 81 | Create XComment object with specified value 82 | 83 | .EXAMPLE 84 | ls | xelem "File" ` 85 | {xcomm " Length: $($psitem.Length) "}, 86 | {xcomm " BaseName: $($psitem.BaseName) "} 87 | #> 88 | 89 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 90 | 'PSUseShouldProcessForStateChangingFunctions', '', 91 | Justification = 'Intended to be this way')] 92 | param 93 | ( 94 | [Parameter(Mandatory = $true)] 95 | [string] $Value 96 | ) 97 | 98 | [Xml.Linq.XComment] $value 99 | } 100 | 101 | filter New-XElement 102 | { 103 | <# 104 | .SYNOPSIS 105 | Create XElement object and attach specified via 106 | script blocks other XObjects in a hierarchal form 107 | 108 | .EXAMPLE 109 | $psitem | xelem "AclListRoot" ` 110 | {xmlns xsd "http://www.w3.org/2001/XMLSchema"}, 111 | {xmlns xsi "http://www.w3.org/2001/XMLSchema-instance"}, 112 | {xattr Version "latest"}, 113 | {xcomm " Syntax: $($psitem.Syntax)"}, 114 | {xcomm " Devices: $($psitem.Devices.Name -join ';') "}, 115 | {xcomm " Clusters: $($psitem.Clusters -join ';') "}, 116 | {$psitem | xelem "AccessControlList" ` 117 | {xattr Device $psitem.Sku}, 118 | {xattr Firmware $psitem.Firmware}, 119 | {$psitem | xelem "AclGroup" ` 120 | {xattr Name "Global"}, 121 | {xattr Type "AccessList"}, 122 | {$psitem.Policies | xelem "Policy" ` 123 | {xattr Name $psitem.Name}, 124 | {$psitem.Rules | xelem Rule} 125 | } 126 | } 127 | } 128 | #> 129 | 130 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 131 | 'PSUseShouldProcessForStateChangingFunctions', '', 132 | Justification = 'Intended to be this way')] 133 | param 134 | ( 135 | [string] $Name, 136 | [scriptblock[]] $Scripts = {$psitem} 137 | ) 138 | 139 | $rendered = $psitem 140 | $arguments = , (xname $name) + @($scripts | foreach{ $script = $psitem; $rendered | foreach $script }) 141 | New-Object Xml.Linq.XElement $arguments 142 | } 143 | 144 | -------------------------------------------------------------------------------- /media/source-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PSToolset/288c7a45e7930c6a347f35dba1415e8f4a5e5a4a/media/source-output.png --------------------------------------------------------------------------------