diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..36bd853 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [StartAutomating] diff --git a/.github/workflows/BuildVector.yml b/.github/workflows/BuildVector.yml new file mode 100644 index 0000000..e0dabd4 --- /dev/null +++ b/.github/workflows/BuildVector.yml @@ -0,0 +1,501 @@ + +name: Build Vector Module +on: + push: + pull_request: + workflow_dispatch: +jobs: + TestPowerShellOnLinux: + runs-on: ubuntu-latest + steps: + - name: InstallPester + id: InstallPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Installs Pester + .Description + Installs Pester + #> + param( + # The maximum pester version. Defaults to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99' + ) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber + Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters + - name: Check out repository + uses: actions/checkout@v2 + - name: RunPester + id: RunPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + $Parameters.NoCoverage = ${env:NoCoverage} + $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Runs Pester + .Description + Runs Pester tests after importing a PowerShell module + #> + param( + # The module path. If not provided, will default to the second half of the repository ID. + [string] + $ModulePath, + # The Pester max version. By default, this is pinned to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99', + + # If set, will not collect code coverage. + [switch] + $NoCoverage + ) + + $global:ErrorActionPreference = 'continue' + $global:ProgressPreference = 'silentlycontinue' + + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } + $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion + $importedModule = Import-Module $ModulePath -Force -PassThru + $importedPester, $importedModule | Out-Host + + $codeCoverageParameters = @{ + CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" + CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" + } + + if ($NoCoverage) { + $codeCoverageParameters = @{} + } + + + $result = + Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters + + "::set-output name=TotalCount::$($result.TotalCount)", + "::set-output name=PassedCount::$($result.PassedCount)", + "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host + if ($result.FailedCount -gt 0) { + "::debug:: $($result.FailedCount) tests failed" + foreach ($r in $result.TestResult) { + if (-not $r.Passed) { + "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" + } + } + throw "::error:: $($result.FailedCount) tests failed" + } + } @Parameters + - name: PublishTestResults + uses: actions/upload-artifact@main + with: + name: PesterResults + path: '**.TestResults.xml' + if: ${{always()}} + TagReleaseAndPublish: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: TagModuleVersion + id: TagModuleVersion + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The tag version format (default value: '$($imported.Name) $(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Tagging" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $existingTags = git tag --list + + @" + Target Version: $targetVersion + + Existing Tags: + $($existingTags -join [Environment]::NewLine) + "@ | Out-Host + + $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } + + if ($versionTagExists) { + "::warning::Version $($versionTagExists)" + return + } + + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } + git config --global user.email $UserEmail + git config --global user.name $UserName + + git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) + git push origin --tags + + if ($env:GITHUB_ACTOR) { + exit 0 + }} @Parameters + - name: ReleaseModule + id: ReleaseModule + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} + $Parameters.ReleaseAsset = ${env:ReleaseAsset} + $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The release name format (default value: '$($imported.Name) $($imported.Version)') + [string] + $ReleaseNameFormat = '$($imported.Name) $($imported.Version)', + + # Any assets to attach to the release. Can be a wildcard or file name. + [string[]] + $ReleaseAsset + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $targetReleaseName = $targetVersion + $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' + "Release URL: $releasesURL" | Out-Host + $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + + $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion + + if ($releaseExists) { + "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host + $releasedIt = $releaseExists + } else { + $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( + [Ordered]@{ + owner = '${{github.owner}}' + repo = '${{github.repository}}' + tag_name = $targetVersion + name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) + body = + if ($env:RELEASENOTES) { + $env:RELEASENOTES + } elseif ($imported.PrivateData.PSData.ReleaseNotes) { + $imported.PrivateData.PSData.ReleaseNotes + } else { + "$($imported.Name) $targetVersion" + } + draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } + prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } + } | ConvertTo-Json + ) -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Content-type" = "application/json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + } + + + + + + if (-not $releasedIt) { + throw "Release failed" + } else { + $releasedIt | Out-Host + } + + $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' + + if ($ReleaseAsset) { + $fileList = Get-ChildItem -Recurse + $filesToRelease = + @(:nextFile foreach ($file in $fileList) { + foreach ($relAsset in $ReleaseAsset) { + if ($relAsset -match '[\*\?]') { + if ($file.Name -like $relAsset) { + $file; continue nextFile + } + } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) { + $file; continue nextFile + } + } + }) + + $releasedFiles = @{} + foreach ($file in $filesToRelease) { + if ($releasedFiles[$file.Name]) { + Write-Warning "Already attached file $($file.Name)" + continue + } else { + $fileBytes = [IO.File]::ReadAllBytes($file.FullName) + $releasedFiles[$file.Name] = + Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ + "Accept" = "application/vnd.github+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } -Body $fileBytes -ContentType Application/octet-stream + $releasedFiles[$file.Name] + } + } + + "Attached $($releasedFiles.Count) file(s) to release" | Out-Host + } + + + + } @Parameters + - name: PublishPowerShellGallery + id: PublishPowerShellGallery + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.Exclude = ${env:Exclude} + $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + [string[]] + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') + ) + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + if (-not $Exclude) { + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') + } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + @" + ::group::PSBoundParameters + $($PSBoundParameters | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host + return + } + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} + + if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { + "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host + } else { + + $gk = '${{secrets.GALLERYKEY}}' + + $rn = Get-Random + $moduleTempFolder = Join-Path $pwd "$rn" + $moduleTempPath = Join-Path $moduleTempFolder $moduleName + New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host + + Write-Host "Staging Directory: $ModuleTempPath" + + $imported | Split-Path | + Get-ChildItem -Force | + Where-Object Name -NE $rn | + Copy-Item -Destination $moduleTempPath -Recurse + + $moduleGitPath = Join-Path $moduleTempPath '.git' + Write-Host "Removing .git directory" + if (Test-Path $moduleGitPath) { + Remove-Item -Recurse -Force $moduleGitPath + } + + if ($Exclude) { + "::notice::Attempting to Exlcude $exclude" | Out-Host + Get-ChildItem $moduleTempPath -Recurse | + Where-Object { + foreach ($ex in $exclude) { + if ($_.FullName -like $ex) { + "::notice::Excluding $($_.FullName)" | Out-Host + return $true + } + } + } | + Remove-Item + } + + Write-Host "Module Files:" + Get-ChildItem $moduleTempPath -Recurse + Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" + Publish-Module -Path $moduleTempPath -NuGetApiKey $gk + if ($?) { + Write-Host "Published to Gallery" + } else { + Write-Host "Gallery Publish Failed" + exit 1 + } + } + } @Parameters + BuildVector: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@main + - name: UseEZOut + uses: StartAutomating/EZOut@master +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Jobs/BuildVector.psd1 b/Build/GitHub/Jobs/BuildVector.psd1 new file mode 100644 index 0000000..7a6a1d3 --- /dev/null +++ b/Build/GitHub/Jobs/BuildVector.psd1 @@ -0,0 +1,18 @@ +@{ + "runs-on" = "ubuntu-latest" + if = '${{ success() }}' + steps = @( + @{ + name = 'Check out repository' + uses = 'actions/checkout@main' + }, + 'RunEZOut' # , + <#@{ + name = 'Run Vector (on branch)' + if = '${{github.ref_name != ''main''}}' + uses = './' + id = 'VectorAction' + }#> + # 'BuildAndPublishContainer' + ) +} \ No newline at end of file diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1 new file mode 100644 index 0000000..e8111e8 --- /dev/null +++ b/Build/GitHub/Steps/PublishTestResults.psd1 @@ -0,0 +1,10 @@ +@{ + name = 'PublishTestResults' + uses = 'actions/upload-artifact@main' + with = @{ + name = 'PesterResults' + path = '**.TestResults.xml' + } + if = '${{always()}}' +} + diff --git a/Build/Vector.GitHubWorkflow.PSDevOps.ps1 b/Build/Vector.GitHubWorkflow.PSDevOps.ps1 new file mode 100644 index 0000000..53f291c --- /dev/null +++ b/Build/Vector.GitHubWorkflow.PSDevOps.ps1 @@ -0,0 +1,15 @@ +#requires -Module PSDevOps +Import-BuildStep -SourcePath ( + Join-Path $PSScriptRoot 'GitHub' +) -BuildSystem GitHubWorkflow + +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubWorkflow -Name "Build Vector Module" -On Push, + PullRequest, + Demand -Job TestPowerShellOnLinux, + TagReleaseAndPublish, BuildVector -Environment ([Ordered]@{ + REGISTRY = 'ghcr.io' + IMAGE_NAME = '${{ github.repository }}' + }) -OutputPath .\.github\workflows\BuildVector.yml + +Pop-Location \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..803f211 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +## Vector 0.1: + +* Initial Release of Vector module +* Commands: + * Get-Vector (#1) + * Get-Vector2 (#2) + * Get-Vector3 (#3) + * Get-Vector4 (#4) +* Vector Workflow (#5) +* Vector Tests (#6) +* Vector Docs + * Demo (#7) + * README (#8) + * FUNDING (#9) + * CODE_OF_CONDUCT (#10) + * CONTRIBUTING (#11) + * SECURITY (#12) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a132093 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Code of Conduct + +We have a simple subjective code of conduct: + +1. Be Respectful +2. Be Helpful +3. Do No Harm + +Failure to follow the code of conduct may result in blocks or banishment. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7a690ce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Contibuting + +We welcome suggestions and careful contributions. + +To suggest something, please [open an issue](https://github.com/PowerShellWeb/Vector/issues) or start a [discussion](https://github.com/PowerShellWeb/Vector/discussion) + +To add a feature, please open an issue and create a pull request. + +## Contributing Examples + +Examples are more than welcome! To contribute an example, please open an issue describing your example and create a pull request. diff --git a/Commands/Get-Vector.ps1 b/Commands/Get-Vector.ps1 new file mode 100644 index 0000000..8002ada --- /dev/null +++ b/Commands/Get-Vector.ps1 @@ -0,0 +1,119 @@ +function Get-Vector +{ + <# + .SYNOPSIS + Gets a one dimensional vector + .DESCRIPTION + Gets a one dimensional vector (or, more simply, a list of numbers) + + This will convert a variety of types into numbers. + .NOTES + This attempts to convert any type into a number. + + Some types are special: + + * Primitive types will be casted to float + * `[Numerics.Vector2]`,`[Numerics.Vector3]`,`[Numerics.Vector4]` output each component + * `[string]`s that match a range (`$start..$end`) will output that range + * `[Version]`s will output each numeric component + * `[semver]`s will output each numeric component, followed by the bytes of a release type + * `[DateTime]` and `[DateTimeOffset]` will become a series of 12 numbers + * `year`,`month`,`day` + * `hour`, `minute`, `second` + * `millisecond`, `microsecond`, `nanosecond` + * `offset.hours`, `offset.minutes`, `offset.seconds` + * `[string]s` will return their bytes in the current `$outputEncoding` + * Anything unknown will be stringified and the bytes will be returned + #> + [Alias('Vector','Vector1','V1')] + param() + + filter toVector { + $arg = $_ + # Return primitive types + if ($arg.GetType -and $arg.GetType().IsPrimitive) { + # casted to float + return ($arg -as [float]) + } + # Return vector components + if ($arg -is [ValueType]) { + if ($arg -is [Numerics.Vector2]) { + return $arg.X,$arg.Y + } + elseif ($arg -is [Numerics.Vector3]) { + return $arg.X,$arg.Y,$arg.Z + } + elseif ($arg -is [Numerics.Vector4]) { + return $arg.X,$arg.Y,$arg.Z, $arg.W + } + } + # Look for inline ranges. + if ($arg -is [string]) { + if ($arg -match '^\d..\d') { + $start, $end = $arg -split '\..', 2 + $startInt = ($start -as [int]) + $endInt = ($end -as [int]) + if ($null -ne $startInt -and $null -ne $endInt) { + # If found, return them expanded. + return ($startInt..$endInt) + } + } + if ($arg -as [float]) { + return $arg -as [float] + } + } + + + # If the arg is a version, get each number of the version + if ($arg -is [version]) {return $arg.Major,$arg.Minor,$arg.Build,$arg.Revision} + + # If we support semver and the arg is semver + if (('semver' -as [type]) -and $arg -is [semver]) { + # Return the numeric parts of the semver + $arg.Major,$arg.Minor,$arg.Patch + # and turn any string portions to bytes + if ($arg.PreReleaseLabel) { + # make sure to include a leading dash for pre-releases + $OutputEncoding.GetBytes("-$($arg.PreReleaseLabel)") + } + + if ($arg.BuildLabel) { + # make sure to include a leading plus for build labels + $OutputEncoding.GetBytes("+$($arg.BuildLabel)") + } + return + } + + # If the arg is a datetime or datetimeoffset + if ($arg -is [DateTime] -or $arg -is [DateTimeOffset]) { + # make it an offset, and then output 12 values + $dateArg = $arg -as [DateTimeOffset] + # * `year` `month` `day` + $dateArg.Year, $dateArg.Month, $dateArg.Day, + # * `hour` `minute` `second` + $dateArg.Hour, $dateArg.Minute, $dateArg.Second, + # * `millisecond`, `microsecond`, `nanosecond` + $dateArg.Millisecond, $dateArg.Microsecond, $dateArg.Nanosecond, + # * `offset hours`, `offset minutes`, `offset seconds` + $dateArg.Offset.Hours,$dateArg.Offset.Minutes,$dateArg.Offset.Seconds + return + } + # If the arg is a string + if ($arg -is [string]) { + # return its bytes + return $OutputEncoding.GetBytes($arg) + } + # any input we have not caught, stringify and turn to bytes + return $OutputEncoding.GetBytes("$arg") + } + + + # Collect all of our input and arguments + $allIn = @($input) + @( + foreach ($arg in $args) { + $arg + } + ) + + return $allIn | toVector +} \ No newline at end of file diff --git a/Commands/Get-Vector2.ps1 b/Commands/Get-Vector2.ps1 new file mode 100644 index 0000000..f3d2c6b --- /dev/null +++ b/Commands/Get-Vector2.ps1 @@ -0,0 +1,46 @@ +function Get-Vector2 { + <# + .SYNOPSIS + Gets a Vector2 + .DESCRIPTION + Gets any input and arguments as a Vector2 + .LINK + https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector2?wt.mc_id=MVP_321542 + .EXAMPLE + # Create a vector out of two numbers + Vector2 1 2 + .EXAMPLE + (Vector2 1 2) + (Vector2 2 1) + .EXAMPLE + (Vector2 1 2) - (Vector2 2 1) + .EXAMPLE + # Create a thousand vectors + $vectors = Vector2 1..2kb + .EXAMPLE + # Create a thousand vectors in random order, using the pipeline + $vectors = 1..2kb | Get-Random -Count 2kb | Vector2 + .EXAMPLE + # Create a vector from a string + $vector = Vector2 "hi" + #> + [Alias('V2','Vector2')] + param() + # Collect all of our input and arguments + $allIn = @($input) + @( + foreach ($arg in $args) { + $arg + } + ) + + # and expand them + $expandAllIn = @($allIn | Vector) + + For ($n = 0; $n -lt $expandAllIn.Length; $n+=2) { + $argSet = $expandAllIn[$n..($n+1)] -as [float[]] + if ($argSet.Length -eq 1) { + [Numerics.Vector2]::new($argSet[0]) + } else { + [Numerics.Vector2]::new($argSet) + } + } +} diff --git a/Commands/Get-Vector3.ps1 b/Commands/Get-Vector3.ps1 new file mode 100644 index 0000000..4dc1a25 --- /dev/null +++ b/Commands/Get-Vector3.ps1 @@ -0,0 +1,57 @@ +function Get-Vector3 { + <# + .SYNOPSIS + Gets a Vector3 + .DESCRIPTION + Gets any input and arguments as a Vector3 + .LINK + https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector3?wt.mc_id=MVP_321542 + .EXAMPLE + # Create a vector out of two numbers + Vector3 1 2 3 + .EXAMPLE + (Vector3 1 2 3 ) + (Vector3 3 2 1) + .EXAMPLE + (Vector3 1 2 3 ) - (Vector3 3 2 1) + .EXAMPLE + # Create a thousand vectors + $vectors = Vector3 1..3kb + .EXAMPLE + # Create a thousand vectors in random order, using the pipeline + $vectors = 1..3kb | Get-Random -Count 3kb | Vector3 + .EXAMPLE + # Create a vector from a string + $vector = Vector3 "hi" + .NOTES + This script is self contained so that it can be easily dropped into any project + #> + [Alias('Vector3','V3')] + param() + + + # Collect all of our input and arguments + $allIn = @($input) + @( + foreach ($arg in $args) { + $arg + } + ) + + # and expand them + $expandAllIn = @($allIn | Vector) + + # Go over our arguments three at a time + For ($n = 0; $n -lt $expandAllIn.Length; $n+=3) { + $argSet = $expandAllIn[$n..($n+2)] -as [float[]] + switch ($argSet.Length) { + 1 { + [Numerics.Vector3]::new($argSet[0]) + } + 2 { + [Numerics.Vector3]::new([Numerics.Vector2]::new($argSet[0],$argSet[1]), 1) + } + 3 { + [Numerics.Vector3]::new($argSet) + } + } + } +} diff --git a/Commands/Get-Vector4.ps1 b/Commands/Get-Vector4.ps1 new file mode 100644 index 0000000..b2d0d69 --- /dev/null +++ b/Commands/Get-Vector4.ps1 @@ -0,0 +1,52 @@ +function Get-Vector4 { + <# + .SYNOPSIS + Gets a Vector4 + .DESCRIPTION + Gets any input and arguments as a Vector4 + .LINK + https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector4?wt.mc_id=MVP_321542 + .EXAMPLE + # Create a vector out of four numbers + Vector4 1 2 3 4 + .EXAMPLE + (Vector4 1 2 3 4 ) + (Vector4 4 3 2 1 ) + .EXAMPLE + (Vector4 1 2 3 4 ) - (Vector4 4 3 2 1) + .EXAMPLE + # Create a thousand vectors + $vectors = Vector4 1..4kb + .EXAMPLE + # Create a thousand vectors in random order, using the pipeline + $vectors = 1..4kb | Get-Random -Count 4kb | Vector4 + .EXAMPLE + # Create vectors from a string + Vector4 "hi" + #> + [Alias('v4','Vector4')] + param() + # Collect all of our input and arguments + $allIn = @($input) + @( + foreach ($arg in $args) { + $arg + } + ) + + # and expand them + $expandAllIn = @($allIn | vector) + For ($n = 0; $n -lt $expandAllIn.Length; $n+=4) { + $argSet = $expandAllIn[$n..($n+3)] -as [float[]] + switch ($argSet.Length) { + 1 {[Numerics.Vector4]::new($argSet[0]) } + 2 { + [Numerics.Vector4]::new([Numerics.Vector2]::new($argSet[0],$argSet[1]), 1, 1) + } + 3 { + [Numerics.Vector4]::new([Numerics.Vector3]::new($argSet[0],$argSet[1],$argSet[2]), 1) + } + 4 { + [Numerics.Vector4]::new($argSet) + } + } + } +} \ No newline at end of file diff --git a/Demos/Vector-101-Intro-to-Vectors.demo.ps1 b/Demos/Vector-101-Intro-to-Vectors.demo.ps1 new file mode 100644 index 0000000..3f9c569 --- /dev/null +++ b/Demos/Vector-101-Intro-to-Vectors.demo.ps1 @@ -0,0 +1,106 @@ +#1. Getting Vectors + +# Numbers are great! + +# When we measure things with one number, it's technically called a scalar. + +# When we measure things with more than one number, it's called a vector + +# We can do lots of things with vectors. We can add or substract them, multiply and divide them. + +# Vectors are very useful. + +# Let's see how we can get a vector: + +# Create a 2D vector +[Numerics.Vector2]::new(1,2) +# Create a 3D vector +[Numerics.Vector3]::new(1,2,3) +# Create a 4D vector +[Numerics.Vector4]::new(1,2,3,4) + + +# The Vector module gives us three vector commands: +Get-Vector2 1 2 +Get-Vector3 1 2 3 +Get-Vector4 1 2 3 4 + +# We can drop the `get` +Vector2 1 2 +Vector3 1 2 3 +Vector4 1 2 3 4 + +# We can use the shorthand `v2`, `v3`, `v4` +v2 1 2 +v3 1 2 3 +v4 1 2 3 4 + +# We can create vectors from a number +v2 1 +v3 1 +v4 1 + +# Strings can be vectors, too (we just get the bytes) +v2 "hi" +v3 "hi" +v4 "hi" + +# Let's start with addition. +# We can add a scalar to a vector. +(v2 1 2) + 1 +(v3 1 2 3) + 1 +(v4 1 2 3 4) + 1 + +# Let's try substraction: +(v2 1 2) - 1 +(v3 1 2 3) - 1 +(v4 1 2 3 4) - 1 + +# How about multiplication? +(v2 1 2) * 2 +(v3 1 2 3) * 2 +(v4 1 2 3 4) * 2 + +# What about division? +(v2 1 2) / 2 +(v3 1 2 3) / 2 +(v4 1 2 3 4) / 2 + +# We can also work with other vectors: + +# Adding vectors: +(v2 1 2) + (v2 1 2) +(v3 1 2 3) + (v3 1 2 3) +(v4 1 2 3 4) + (v4 1 2 3 4) + +# Subtracting vectors: +(v2 1 2) - (v2 1 2) +(v3 1 2 3) - (v3 1 2 3) +(v4 1 2 3 4) - (v4 1 2 3 4) + +# Multiplying vectors: +(v2 1 2) * (v2 1 2) +(v3 1 2 3) * (v3 1 2 3) +(v4 1 2 3 4) * (v4 1 2 3 4) + +# Dividing vectors: +(v2 1 2) / (v2 1 2) +(v3 1 2 3) / (v3 1 2 3) +(v4 1 2 3 4) / (v4 1 2 3 4) + + +# We can also negate a vector: +-(v2 1 2) +-(v3 1 2 3) +-(v4 1 2 3 4) + +# We can compare two vectors to see if they are equal +(v2 1 2) -eq (v2 1 2) +(v3 1 2 3) -eq (v3 1 2 3) +(v4 1 2 3 4) -eq (v4 1 2 3 4) + +# We can also see if they are not equal +(v2 1 2) -ne (v2 1 2) +(v3 1 2 3) -ne (v3 1 2 3) +(v4 1 2 3 4) -ne (v4 1 2 3 4) + diff --git a/README.md b/README.md index aa27482..eac6d0d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,187 @@ # Vector -Vectors in PowerShell + +Numbers are great! + +When we measure things with one number, it's technically called a scalar. + +When we measure things with more than one number, it's called a [vector](https://en.wikipedia.org/wiki/Vector_%28mathematics_and_physics%29) + +We can do lots of things with vectors. We can add or substract them, multiply and divide them. + +Vectors are very useful. + +This module helps you use Vectors in PowerShell + +## Vectors in PowerShell + +Vectors are actually built into PowerShell. + +Because PowerShell is built atop of the .NET Framework, +and the .NET Framework has had vector support for over a decade, +PowerShell has had vectors for over a decade. + +~~~PowerShell +# Create a 2D vector +[Numerics.Vector2]::new(1,2) +# Create a 3D vector +[Numerics.Vector3]::new(1,2,3) +# Create a 4D vector +[Numerics.Vector4]::new(1,2,3,4) +~~~ + +This module exists to make vectors a bit more useful by providing commands to construct them. + +### Installing and Importing + +We can install the Vector module from the gallery: + +~~~PowerShell +# Install the module from the PowerShell gallery +Install-Module Vector +~~~ + +Once installed, we can import the Vector module with Import-Module: + +~~~PowerShell +Import-Module Vector +~~~ + +### Getting Vectors + +There are a few commands in this module: + +* `Get-Vector2` +* `Get-Vector3` +* `Get-Vector4` + +Each command constructs a vector of the corresponding size. + +We can also drop the `Get` and just refer to them by vector number + +~~~PowerShell +Vector2 1 2 +Vector3 1 2 3 +Vector4 1 2 3 4 +~~~ + +We can be even shorter, and use `V2`, `V3`, and `V4` + +~~~PowerShell +v2 1 2 +v3 1 2 3 +v4 1 2 3 4 +~~~ + +We can turn anything into a series of vectors. + +~~~PowerShell +v2 1 +v3 1 +v4 1 +~~~ + +Strings can become vectors, too! (after all, each byte is already a number) + +~~~PowerShell +v2 "hi" +v3 "hi" +v4 "hi" +~~~ + +### Vector Operators + +.NET vectors are _very_ powerful, and overload many operators. + +For example, we can add, subtract, multiply, or divide by a scalar. + +~~~PowerShell +# Let's start with addition. +# We can add a scalar to a vector. +(v2 1 2) + 1 +(v3 1 2 3) + 1 +(v4 1 2 3 4) + 1 + +# Let's try substraction: +(v2 1 2) - 1 +(v3 1 2 3) - 1 +(v4 1 2 3 4) - 1 + +# How about multiplication? +(v2 1 2) * 2 +(v3 1 2 3) * 2 +(v4 1 2 3 4) * 2 + +# What about division? +(v2 1 2) / 2 +(v3 1 2 3) / 2 +(v4 1 2 3 4) / 2 +~~~ + +We can also work with other vectors: + +~~~PowerShell +# Adding vectors: +(v2 1 2) + (v2 1 2) +(v3 1 2 3) + (v3 1 2 3) +(v4 1 2 3 4) + (v4 1 2 3 4) + +# Subtracting vectors: +(v2 1 2) - (v2 1 2) +(v3 1 2 3) - (v3 1 2 3) +(v4 1 2 3 4) - (v4 1 2 3 4) + +# Multiplying vectors: +(v2 1 2) * (v2 1 2) +(v3 1 2 3) * (v3 1 2 3) +(v4 1 2 3 4) * (v4 1 2 3 4) + +# Dividing vectors: +(v2 1 2) / (v2 1 2) +(v3 1 2 3) / (v3 1 2 3) +(v4 1 2 3 4) / (v4 1 2 3 4) +~~~ + +### Vector Methods + +Vectors have a large number of methods to work with. + +Let's start simple, by calculating the length of a given vector. + +~~~PowerShell +(v2 1 1).Length() +(v3 1 1 1).Length() +(v4 1 1 1 1).Length() +~~~ + +Many of the most useful things we can do with a vector are exposed as a static methods: + +~~~PowerShell +(v2 1 1) | Get-Member -Static +(v3 1 1 1) | Get-Member -Static +(v4 1 1 1 1) | Get-Member -Static +~~~ + + +We can access static method with `::` + +For a small example, let's find the distance between vectors: + +~~~PowerShell +$vector1 = v2 1 2 +$vector2 = v2 2 1 +$vector1::Distance($vector1, $vector2) +~~~ + +For another simple example, let's find a few point between two points, using [Linear Interpolation `lerp`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector2.lerp?wt.mc_id=MVP_321542) + +~~~PowerShell +$vector1 = v2 1 5 +$vector2 = v2 1 -5 +$vector1::Lerp($vector1, $vector2, 0.25) +$vector1::Lerp($vector1, $vector2, 0.5) +$vector1::Lerp($vector1, $vector2, 0.75) +~~~ + +All of this would not be possible without the great work of the .NET team to build such incredibly useful data structures. + +Hopefully this module helps us all work with vectors! \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7f4c180 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security + +We take security seriously. If you believe you have discovered a vulnerability, please [file an issue](https://github.com/PowerShellWeb/Vector/issues). + +## Special Security Considerations + +As this module does little more than expose .NET framework functionality, it should not have any unique special security considerations. If times proves this assertion wrong, please file an issue. \ No newline at end of file diff --git a/Vector.psd1 b/Vector.psd1 new file mode 100644 index 0000000..3e83cbf --- /dev/null +++ b/Vector.psd1 @@ -0,0 +1,146 @@ +# +# Module manifest for module 'Vector' +# +# Generated on: 10/4/2025 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Vector.psm1' + +# Version number of this module. +ModuleVersion = '0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '2110de70-cbfb-4e01-b273-e7db122a4a53' + +# Author of this module +Author = 'James Brundage' + +# Company or vendor of this module +CompanyName = 'Start-Automating' + +# Copyright statement for this module +Copyright = '2025 Start-Automating' + +# Description of the functionality provided by this module +Description = 'Vectors in PowerShell' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Get-Vector', 'Get-Vector2', 'Get-Vector3', 'Get-Vector4' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = 'V1', 'V2', 'V3', 'V4', 'Vector1', 'Vector2', 'Vector3', 'Vector4', 'Vector' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'PowerShell', 'Vector', 'Math', 'VectorMath' + # A URL to the main website for this project. + ProjectURI = 'https://github.com/PowerShellWeb/Vector' + # A URL to the license for this module. + LicenseURI = 'https://github.com/PowerShellWeb/Vector/blob/main/LICENSE' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = @' +## Vector 0.1: + +* Initial Release of Vector module +* Commands: + * Get-Vector (#1) + * Get-Vector2 (#2) + * Get-Vector3 (#3) + * Get-Vector4 (#4) +* Vector Workflow (#5) +* Vector Tests (#6) +* Vector Docs + * Demo (#7) + * README (#8) + * FUNDING (#9) + * CODE_OF_CONDUCT (#10) + * CONTRIBUTING (#11) + * SECURITY (#12) +'@ + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Vector.psm1 b/Vector.psm1 new file mode 100644 index 0000000..ebecaec --- /dev/null +++ b/Vector.psm1 @@ -0,0 +1,39 @@ +$commandsPath = Join-Path $PSScriptRoot Commands +:ToIncludeFiles foreach ($file in (Get-ChildItem -Path "$commandsPath" -Filter "*-*" -Recurse)) { + if ($file.Extension -ne '.ps1') { continue } # Skip if the extension is not .ps1 + foreach ($exclusion in '\.[^\.]+\.ps1$') { + if (-not $exclusion) { continue } + if ($file.Name -match $exclusion) { + continue ToIncludeFiles # Skip excluded files + } + } + . $file.FullName +} + +$myModule = $MyInvocation.MyCommand.ScriptBlock.Module +$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule) +$myModule.pstypenames.insert(0, $myModule.Name) + +New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore + +if ($home) { + $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name + if (-not (Test-Path $MyModuleProfileDirectory)) { + $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force + } + New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore +} + +# Set a script variable of this, set to the module +# (so all scripts in this scope default to the correct `$this`) +$script:this = $myModule + +#region Custom +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + Remove-TypeData -ErrorAction Ignore -TypeName 'System.Numerics.Vector2' + Remove-TypeData -ErrorAction Ignore -TypeName 'System.Numerics.Vector3' + Remove-TypeData -ErrorAction Ignore -TypeName 'System.Numerics.Vector4' +} +#endregion Custom + +Export-ModuleMember -Alias * -Function * -Variable $myModule.Name diff --git a/Vector.tests.ps1 b/Vector.tests.ps1 new file mode 100644 index 0000000..1422003 --- /dev/null +++ b/Vector.tests.ps1 @@ -0,0 +1,43 @@ +describe Vector { + it 'It a collection of points' { + $vector = Vector 1..4 + $vector.GetType() | Should -Be ([object[]]) + $vector | + Should -BeOfType ([int]) + } + + it 'Can be a two dimensional vector' { + Vector2 3,4 | Should -BeOfType ([Numerics.Vector2]) + } + + it 'Can be a three dimensional vector' { + Vector3 1,2,3 | Should -BeOfType ([Numerics.Vector3]) + } + + it 'Can be a four dimensional vector' { + Vector4 1,2,3,4 | Should -BeOfType ([Numerics.Vector4]) + } + + context 'Vector Math' { + it 'Can subtract vectors' { + $subtract = (v2 1 2) - (v2 1 2) + $subtract.X | Should -Be 0 + $subtract.Y | Should -Be 0 + } + it 'Can add vectors' { + $add = (v2 1 2) + (v2 1 2) + $add.X | Should -Be 2 + $add.Y | Should -Be 4 + } + it 'Can multiply vectors' { + $multiply = (v2 1 2) * (v2 1 2) + $multiply.X | Should -Be 1 + $multiply.Y | Should -Be 4 + } + it 'Can divide vectors' { + $divide = (v2 1 2) / (v2 1 2) + $divide.X | Should -Be 1 + $divide.Y | Should -Be 1 + } + } +}