diff --git a/.cspell.json b/.cspell.json index f67e1152..46e72d43 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,10 @@ ".devcontainer/devcontainer.json", ".github/workflows/build-and-test-powershell-module.yml", ".github/workflows/build-test-and-deploy-powershell-module.yml", - "src/tiPS/PowerShellTips.json" + ".gitignore", + "src/tiPS/PowerShellTips.json", + "**/bin/**", // Ignore C# build output files. + "**/obj/**" // Ignore C# build output files. ], "words": [ "colours", @@ -24,6 +27,7 @@ "dawidd", // GitHub action author "deadlydog", // Username "devcontainer", // VS Code devcontainer + "DontShow", // PowerShell keyword "gittools", // GitHub action author "Hmmss", // Time format "jacoco", // Java code coverage @@ -39,6 +43,8 @@ "Wordle" // Game ], "ignoreRegExpList": [ - "^\\$tip\\.Author = .*$" // Ignore tip Author names + "^\\$tip\\.Author = .*$", // Ignore tip Author names. + " + Your display name and GitHub email address. Must be in the GitHub co-author commit format: DisplayName \. e.g. Daniel Schroeder \. This allows you to get attribution in the git history and GitHub. If you do not want to use your public GitHub email address, you can use your private GitHub email address, which can be enabled and found at https://github.com/settings/emails. Leave blank if you prefer to remain anonymous in the git history. + placeholder: e.g. Daniel Schroeder + + - type: markdown + attributes: + value: | + #### By submitting this tip, you agree to make it available under the project's license (MIT). diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e4db35be..a283f5b9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,11 @@ +If this PR is simply to add a new Tip, delete all of this template text and just mention you are adding a new tip. + +If you are making changes to the module's functionality, please fill out the template below. + ### Summary A brief summary of the change this PR brings. -If this PR is simply to add a new Tip, you can remove all of the sections below, as they do not apply. - ### Checklist Addresses issue #ISSUE_NUMBER (if applicable) diff --git a/.github/workflows/ProcessNewPowerShellTipIssueFunctions.ps1 b/.github/workflows/ProcessNewPowerShellTipIssueFunctions.ps1 new file mode 100644 index 00000000..482373a2 --- /dev/null +++ b/.github/workflows/ProcessNewPowerShellTipIssueFunctions.ps1 @@ -0,0 +1,75 @@ +function New-PowerShellTipFile { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + param( + [Parameter(Mandatory = $true, HelpMessage = 'The title of the tip. Must be 75 characters or less.')] + [ValidateNotNullOrWhiteSpace()] + [string] $TipTitle, + + [Parameter(Mandatory = $true, HelpMessage = 'The text of the tip.')] + [ValidateNotNullOrWhiteSpace()] + [string] $TipText, + + [Parameter(Mandatory = $false, HelpMessage = 'The example code for the tip. Use an empty string if no example is provided.')] + [string] $TipExample = '', + + [Parameter(Mandatory = $false, HelpMessage = 'The URLs for the tip. Use an empty array if no URLs are provided.')] + [string[]] $TipUrls = @(), + + [Parameter(Mandatory = $true, HelpMessage = 'The category of the tip. Must be one of the following: Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other.')] + [ValidateSet('Community', 'Editor', 'Module', 'NativeCmdlet', 'Performance', 'Security', 'Syntax', 'Terminal', 'Other')] + [string] $TipCategory, + + [Parameter(Mandatory = $false, HelpMessage = 'The author of the tip. Use an empty string if no author is provided.')] + [string] $TipAuthor = '', + + [Parameter(Mandatory = $false, HelpMessage = 'The expiry date of the tip. Format: YYYY-MM-DD. Use an empty string if no expiry date is provided.')] + [string] $TipExpiryDate = '' + ) + + Write-Information "Building tiPS C# assemblies and importing module..." -InformationAction Continue + $buildOutput = . "$PSScriptRoot/../../tools/Helpers/ImportBuiltModule.ps1" + + # Write the build output as Information messages so it doesn't affect the function's return value. + foreach ($entry in $buildOutput) { + Write-Information $entry -InformationAction Continue + } + + # The Tip filename is based on the ID, which is based on the date and title, so load a dummy PowerShellTip to get the filename to use. + $dummyTip = [tiPS.PowerShellTip]::new() + $dummyTip.CreatedDate = [DateTime]::Today + $dummyTip.Title = $TipTitle.Trim() + + [string] $createdDate = $dummyTip.CreatedDate.ToString('yyyy-MM-dd') + [string] $powerShellTipsFilesDirectoryPath = Resolve-Path -Path "$PSScriptRoot/../../src/PowerShellTips" + [string] $newTipFileName = $dummyTip.Id + '.ps1' + [string] $newTipFilePath = Join-Path -Path $powerShellTipsFilesDirectoryPath -ChildPath $newTipFileName + + [string] $newTipFileContent = @" +`$tip = [tiPS.PowerShellTip]::new() +`$tip.CreatedDate = [DateTime]::Parse('$createdDate') +`$tip.Title = '$($TipTitle.Replace("'", "''"))' +`$tip.TipText = @' +$TipText +'@ +`$tip.Example = @' +$TipExample +'@ +`$tip.Urls = @('$($TipUrls -join "','")') +`$tip.Category = [tiPS.TipCategory]::$TipCategory # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +`$tip.Author = '$TipAuthor' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#`$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Tip submitted via GitHub issue workflow. +"@ + + # If a TipExpiryDate is provided, uncomment the TipExpiryDate line. + if (-not [string]::IsNullOrWhiteSpace($TipExpiryDate)) { + [string] $expiryTextToMatch = '$tip.ExpiryDate = [DateTime]::Parse(' + $newTipFileContent = $newTipFileContent.Replace("#$expiryTextToMatch", $expiryTextToMatch) + } + + Write-Information "Creating new tip file '$newTipFilePath'..." -InformationAction Continue + Set-Content -Path $newTipFilePath -Value $newTipFileContent -Encoding utf8 -Force + + return $newTipFilePath +} diff --git a/.github/workflows/process-new-powershell-tip-issue.yml b/.github/workflows/process-new-powershell-tip-issue.yml new file mode 100644 index 00000000..af95a300 --- /dev/null +++ b/.github/workflows/process-new-powershell-tip-issue.yml @@ -0,0 +1,369 @@ +name: process-new-powershell-tip-issue + +# Trigger whenever a new issue is opened, and ensure (in the job) it has the automation label meaning we should process it. +on: + issues: + types: [opened, edited] + +jobs: + process-new-tip-issue: + if: contains(github.event.issue.labels.*.name, 'automation-new-tip-issue-do-not-use') + runs-on: windows-latest # Use Windows to ensure dotnet SDK is installed to build the assemblies. + permissions: + contents: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch all history for all branches and tags to ensure we can rebase the branch on main. + + - name: Comment on issue letting user know we are processing it + shell: pwsh + run: | + [string] $runUrl = "$Env:GITHUB_SERVER_URL/${{ github.repository }}/actions/runs/${{ github.run_id }}" + [string] $message = @" + Thank you for your submission! 🙏 + + A pull request is being created for your tip via the following GitHub Actions workflow run and should be linked to this issue shortly... ⏱️ + + $runUrl + "@ + + Write-Output "Adding comment with link to workflow run to issue..." + gh issue comment "${{ github.event.issue.number }}" --body $message + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout branch for PR + id: create-branch + shell: pwsh + run: | + Write-Output "Setting up git user information for automated workflow..." + git config --global user.name 'GitHub Action - tiPS Automation' + git config --global user.email 'tiPSAutomation@DoesNotExist.com' + + [string] $branchName = 'new-tip/issue-${{ github.event.issue.number }}' + + Write-Output "Creating and checking out local branch: $branchName" + git checkout -b "$branchName" + + Write-Output "Checking if branch '$branchName' already exists on remote..." + [string] $remoteBranchName = "origin/$branchName" + [string] $remoteBranchSha = (& git show-ref "$remoteBranchName") + [bool] $remoteBranchExists = -not ([string]::IsNullOrWhitespace($remoteBranchSha)) + if ($remoteBranchExists) { + Write-Output "Branch '$branchName' already exists on remote. Getting remote changes..." + git reset --hard "$remoteBranchName" + + Write-Output "Rebasing branch '$branchName' on main to ensure it has latest main branch changes..." + git rebase main + } else { + # If the remote branch doesn't exist, the 'git show-ref' command will have returned a non-zero exit code. + # Reset the PowerShell LastExitCode variable to 0 to avoid failing the workflow run. + $LastExitCode = 0 + } + + Write-Output "Writing variables to environment variables for later steps..." + "branch_name=$branchName" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Extract tip information from issue and create new tip file + id: create-tip-file + shell: pwsh + env: + # Avoid script injection by retrieving the issue body via an environment variable instead of injecting the text directly into the script. + ISSUE_BODY: ${{ github.event.issue.body }} + run: | + Write-Output "Reading information from GitHub issue..." + $body = $Env:ISSUE_BODY + + Write-Output "Displaying issue body for troubleshooting purposes:" + Write-Output "----------------------------------------" + Write-Output $body + Write-Output "----------------------------------------" + + Write-Output "Extracting information from issue body to local variables..." + [string] $tipTitle = [string]::Empty + [string] $tipText = [string]::Empty + [string] $tipExample = [string]::Empty + [string] $tipCategory = [string]::Empty + [string[]] $tipUrls = @() + [string] $tipAuthor = [string]::Empty + [string] $tipExpiryDate = [string]::Empty + [string] $gitHubCoAuthor = [string]::Empty + + # Extract the data from the markdown representation of the issue. + # GitHub Forms generates structured markdown with specific section headers we can match against. + # Below is a sample of what the body data may look like: + # + # ### Tip Title + # + # Testing title + # + # ### Tip Text + # + # Test description + # + # ### Example Code (optional) + # + # ```powershell + # Example code + # ``` + # + # ### Category + # + # Syntax + # + # ### URL 1 (optional) + # + # https://example.com + # + # ### URL 2 (optional) + # + # https://anotherExample.com + # + # ### URL 3 (optional) + # + # _No response_ + # + # ### Author (optional) + # + # Daniel Schroeder (deadlydog) + # + # ### Expiry Date (optional) + # + # _No response_ + # + # ### Git Display Name and GitHub Email (optional) + # + # Daniel Schroeder + + # The default text that GitHub Issues uses for optional fields that are not filled in. + [string] $noResponseText = '_No response_' + + # Extract Title. + [regex] $titleRegex = '### Tip Title\s+(?.*?)\s+### Tip Text' + if ($body -match $titleRegex) { + $tipTitle = $Matches['Title'].Trim() + + # Trim title to 75 characters max. + # Normally we would let the class validation logic handle this, but we want to do as much as possible + # to ensure the PR creation doesn't fail, as this flow is only triggered on issue creation and we don't + # want to force the user to create a whole new issue just to adjust the title length. + if ($tipTitle.Length -gt 75) { + $tipTitle = $tipTitle.Substring(0, 75) + } + + Write-Output "Extracted Title: $tipTitle" + } + + # Extract Tip Text. Use '(?s)' for multiline match mode, since these are multiline strings. + [regex] $tipTextRegex = '(?s)### Tip Text\s+(?<TipText>.*?)\s+### Example Code' + if ($body -match $tipTextRegex) { + $tipText = $Matches['TipText'].Trim() + Write-Output "Extracted Tip Text: $tipText" + } + + # Extract Example Code. Use '(?s)' for multiline match mode, since these are multiline strings. + [regex] $exampleRegex = '(?s)### Example Code \(optional\)\s+```powershell\s+(?<ExampleCode>.*?)```\s+### Category' + if ($body -match $exampleRegex) { + $tipExample = $Matches['ExampleCode'].Trim() + Write-Output "Extracted Example Code: $tipExample" + } + + # Extract Category. + [regex] $categoryRegex = '### Category\s+(?<Category>.*?)\s+### URL 1' + if ($body -match $categoryRegex) { + $tipCategory = $Matches['Category'].Trim() + Write-Output "Extracted Category: $tipCategory" + } + + # Extract URLs (up to 3). + [regex] $url1Regex = '### URL 1 \(optional\)\s+(?<Url1>.*?)\s+### URL 2' + if ($body -match $url1Regex) { + $url = $Matches['Url1'].Replace($noResponseText, '').Trim() + if (-not [string]::IsNullOrWhiteSpace($url)) { + # Ensure URL starts with http(s)://. + if (-not ($url.StartsWith('http://') -or $url.StartsWith('https://'))) { + $url = "https://$url" + } + $tipUrls += $url + Write-Output "Extracted URL 1: $url" + } + } + + [regex] $url2Regex = '### URL 2 \(optional\)\s+(?<Url2>.*?)\s+### URL 3' + if ($body -match $url2Regex) { + $url = $Matches['Url2'].Replace($noResponseText, '').Trim() + if (-not [string]::IsNullOrWhiteSpace($url)) { + # Ensure URL starts with http(s)://. + if (-not ($url.StartsWith('http://') -or $url.StartsWith('https://'))) { + $url = "https://$url" + } + $tipUrls += $url + Write-Output "Extracted URL 2: $url" + } + } + + [regex] $url3Regex = '### URL 3 \(optional\)\s+(?<Url3>.*?)\s+### Author' + if ($body -match $url3Regex) { + $url = $Matches['Url3'].Replace($noResponseText, '').Trim() + if (-not [string]::IsNullOrWhiteSpace($url)) { + # Ensure URL starts with http(s)://. + if (-not ($url.StartsWith('http://') -or $url.StartsWith('https://'))) { + $url = "https://$url" + } + $tipUrls += $url + Write-Output "Extracted URL 3: $url" + } + } + + # Extract Author. + [regex] $authorRegex = '### Author \(optional\)\s+(?<Author>.*?)\s+### Expiry Date' + if ($body -match $authorRegex) { + $tipAuthor = $Matches['Author'].Trim() + Write-Output "Extracted Author: $tipAuthor" + } + + # Extract Expiry Date. + [regex] $expiryDateRegex = '### Expiry Date \(optional\)\s+(?<ExpiryDate>.*?)\s+### Git Display Name and GitHub Email' + if ($body -match $expiryDateRegex) { + $tipExpiryDate = $Matches['ExpiryDate'].Trim() + Write-Output "Extracted Expiry Date: $tipExpiryDate" + } + + # Extract GitHub Co-Author. + [regex] $gitHubCoAuthorRegex = '### Git Display Name and GitHub Email \(optional\)\s+(?<GitHubCoAuthor>.*?)$' + if ($body -match $gitHubCoAuthorRegex) { + $gitHubCoAuthor = $Matches['GitHubCoAuthor'].Trim() + Write-Output "Extracted GitHub Co-Author: $gitHubCoAuthor" + } + + Write-Output "Dot-sourcing utility script used to create the new tip file..." + [string] $gitRootDirectoryPath = $Env:GITHUB_WORKSPACE + . "$gitRootDirectoryPath/.github/workflows/ProcessNewPowerShellTipIssueFunctions.ps1" + + Write-Output "Calling function to create new tip file..." + $newTipFileParameters = @{ + TipTitle = $tipTitle + TipText = $tipText + TipExample = $tipExample.Replace($noResponseText, '').Trim() + TipUrls = $tipUrls + TipCategory = $tipCategory.Replace($noResponseText, '').Trim() + TipAuthor = $tipAuthor.Replace($noResponseText, '').Trim() + TipExpiryDate = $tipExpiryDate.Replace($noResponseText, '').Trim() + } + [string] $newTipFilePath = New-PowerShellTipFile @newTipFileParameters + + # Sanitize the GitHub Co-Author string if none was provided. + $gitHubCoAuthor = $gitHubCoAuthor.Replace($noResponseText, '').Trim() + + Write-Output "Writing variables to environment variables for later steps..." + "tip_file_path=$newTipFilePath" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + "tip_title=$tipTitle" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + "github_coauthor=$gitHubCoAuthor" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Push changes and create PR + id: create-pr + shell: pwsh + run: | + [string] $commitMessage = "New tip: ${{ steps.create-tip-file.outputs.tip_title }}" + + # Use GitHub's Co-Author feature to add the user as a committer on the git commit. + # GitHub docs: https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors + [string] $gitHubCoAuthor = '${{ steps.create-tip-file.outputs.github_coauthor }}' + if (-not ([string]::IsNullOrWhiteSpace($gitHubCoAuthor))) { + [string] $coAuthorCommitMessage = "Co-authored-by: $gitHubCoAuthor" + Write-Output "Adding co-author to commit message: $coAuthorCommitMessage" + $commitMessage += [Environment]::NewLine + [Environment]::NewLine + $coAuthorCommitMessage + } + + Write-Output "Adding new tip file to git staging area..." + git add "${{ steps.create-tip-file.outputs.tip_file_path }}" + + Write-Output "Committing changes to local branch..." + git commit -m "$commitMessage" + + Write-Output "Pushing changes to remote branch..." + # Use force push because we rebased the branch on main, and the commit history may have changed. + git push --set-upstream origin "${{ steps.create-branch.outputs.branch_name }}" --force-with-lease + + if ($LastExitCode -ne 0) { + throw "Git push to remote branch failed. Exiting workflow run." + } + + Write-Output "Checking if a PR already exists for this branch..." + $prOutput = (& gh pr list --head "${{ steps.create-branch.outputs.branch_name }}") + + # Get the PR number from the output, if it exists. + # The output to the console will look like this when a PR is found for the branch: + # Showing 1 of 1 pull request in deadlydog/PowerShell.tiPS that matches your search + # ID TITLE BRANCH CREATED AT + # #110 Tip: Test title new-tip/issue-109 about 10 hours ago + # But the actual output captured by the variable will look like this: + # 110 Tip: Test title new-tip/issue-109 OPEN 2025-04-04T03:45:47Z + [string] $prNumber = [string]::Empty + [regex] $prNumberRegex = '^(?<PRNumber>\d+)' + if ($prOutput -match $prNumberRegex) { + $prNumber = $Matches['PRNumber'].Trim() + Write-Output "PR already exists. PR number: $prNumber" + } + + # If the PR number was not found, create a new PR. + [string] $prUrl = [string]::Empty + if ([string]::IsNullOrWhiteSpace($prNumber)) { + [string] $message = @" + This PR adds the new PowerShell tip submitted via issue #${{ github.event.issue.number }}. + + Next steps are to wait for the PR checks to pass, and for the maintainer to review and merge this PR. + + Subscribe to this PR to receive notifications for any comments and to see when your tip has been merged into tiPS. + + If the PR check fails due to a spelling mistake, or you want to update the tip information, you can edit the issue description and it will automatically re-run the workflow to update this PR. + + 🎉 Thank you for contributing to tiPS! 🎉 + + This PR was created automatically using a GitHub Action to resolve #${{ github.event.issue.number }}. + "@ + # Using "resolve #issue_number" in the PR message above will automatically close the issue when the PR is merged. + # For more information see: https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword + + Write-Output "Creating pull request..." + $prUrl = gh pr create ` + --title "Tip: ${{ steps.create-tip-file.outputs.tip_title }}" ` + --body $message ` + --label "automation-new-tip-pr-do-not-use" + } else { + $prUrl = "https://github.com/${{ github.repository }}/pull/$prNumber" + } + + Write-Output "PR URL is: $prUrl" + + Write-Output "Writing variables to environment variables for later steps..." + "pr_url=$prUrl" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + env: + # Must use a user token instead of built-in GITHUB_TOKEN so that workflows will be triggered on the created PR. + # For more information see: https://github.com/peter-evans/create-pull-request/issues/48 + GITHUB_TOKEN: ${{ secrets.TIPS_GITHUB_ACTIONS_PR_CREATION_TOKEN }} + + - name: Comment on issue with link to PR + shell: pwsh + run: | + [string] $message = @" + Thanks for contributing! 🙌 + + A pull request has been created for your tip. You can view it at PR ${{ steps.create-pr.outputs.pr_url }}. + + Subscribe to the PR to receive notifications for any comments and to see when your tip has been merged into tiPS. + + If the PR check fails due to a spelling mistake, or you want to update the tip information, you can edit the issue description and it will automatically re-run the workflow to update the PR. + + This issue will be automatically closed when the PR is merged. + "@ + + Write-Output "Adding comment with link to PR to issue..." + gh issue comment "${{ github.event.issue.number }}" --body $message + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-readme-contributors.yml b/.github/workflows/update-readme-contributors.yml new file mode 100644 index 00000000..d4e5c237 --- /dev/null +++ b/.github/workflows/update-readme-contributors.yml @@ -0,0 +1,125 @@ +name: Update README Contributors + +on: + # Run weekly on Sunday at 00:00 UTC + schedule: + - cron: '0 0 * * 0' + + # Run when changes are merged to main + push: + branches: + - main + paths-ignore: + - 'ReadMe.md' # Skip when only the ReadMe.md changes to avoid infinite loops + - '.github/workflows/update-readme-contributors.yml' # Skip when this workflow file changes + + # Allows manual triggering from the Actions tab + workflow_dispatch: + +jobs: + update-contributors: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + with: + # Fetch with token that has write access to the repo + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update README with Contributors + id: update-readme + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const path = require('path'); + + try { + // Fetch contributors from the repository + console.log('Fetching contributors from GitHub API...'); + const contributorsResponse = await github.rest.repos.listContributors({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100 + }); + + const contributors = contributorsResponse.data; + console.log(`Found ${contributors.length} contributors.`); + + if (contributors.length === 0) { + console.log('No contributors found. Skipping update.'); + return false; + } + + // Read the README file + const readmePath = path.join(process.env.GITHUB_WORKSPACE, 'ReadMe.md'); + let readmeContent = fs.readFileSync(readmePath, 'utf8'); + + // Create the Contributors section + let contributorsSection = `## 👥 Contributors + + Thanks to these wonderful people who have contributed to tiPS: + + <p align="center"> + `; + + // Add each contributor to the section + for (const contributor of contributors) { + // Skip GitHub Actions bot or other bots + if (contributor.login === 'github-actions[bot]' || + contributor.login === 'dependabot[bot]' || + contributor.login === 'Copilot') { + continue; + } + + contributorsSection += ` <a href="${contributor.html_url}" title="${contributor.login}"><img src="${contributor.avatar_url}" width="50" height="50" style="border-radius:50%;margin:5px;" alt="${contributor.login}"></a>\n`; + } + + contributorsSection += `</p> + + `; + + // Check if the README already has a Contributors section + if (readmeContent.includes('## 👥 Contributors')) { + // Replace the existing section using regex to capture everything between the Contributors heading and the next heading + const pattern = /(## 👥 Contributors[\s\S]*?)(## 🛣️ Roadmap)/; + readmeContent = readmeContent.replace(pattern, contributorsSection + '$2'); + } else { + // Insert before the Roadmap section + readmeContent = readmeContent.replace(/(## 🛣️ Roadmap)/, contributorsSection + '$1'); + } + + // Write the updated content back to the README file + fs.writeFileSync(readmePath, readmeContent); + + console.log('README updated with contributors.'); + + // Set output for next step - return true if we made changes + return true; + } catch (error) { + console.error(`Error updating README: ${error}`); + core.setFailed(`Failed to update README: ${error.message}`); + return false; + } + result-encoding: string + + - name: Create Pull Request if README was updated + if: steps.update-readme.outputs.result == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "docs: update contributors in README [skip ci]" + title: "docs: Update contributors in README" + body: | + This PR updates the README.md file with the latest contributors to the project. + + *This is an automated PR created by the update-readme-contributors workflow.* + branch: auto-update-readme-contributors + branch-suffix: timestamp + delete-branch: true + base: main diff --git a/.gitignore b/.gitignore index fc344c6f..be06620f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -# Ignore Pester code coverage report file +# Ignore Pester code coverage report file. coverage.xml +# Ignore NPM files. +package-lock.json +package.json + # Created by https://www.gitignore.io/api/powershell,visualstudio,visualstudiocode # Edit at https://www.gitignore.io/?templates=powershell,visualstudio,visualstudiocode diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4afda726..f9f05383 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,6 +10,7 @@ }, "dependsOn": [ "Run PSScriptAnalyzer linter", + "Run CSpell spell checker", "Convert PowerShellTips files to JSON file" ] }, @@ -47,6 +48,31 @@ "$func-powershell-watch" ] }, + { + "label": "Run CSpell spell checker", + "type": "shell", + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + }, + // If npx is not available, warn that Node.js is not installed. If we cannot run cspell, try to install and run it, and warn if we still cannot run it. + "command": "try { & npx -v > $null } catch {}; if (-not $?) { Write-Warning 'Node.js is not installed, so cannot download and run npx cspell.' } else { try { & npx cspell . } catch {}; if (-not $?) { & npm install cspell; & npx cspell . }; if (-not $?) { Write-Warning 'There was a problem installing or running cspell' } }", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true, + "group": "build" + }, + "problemMatcher": [ + "$func-powershell-watch" + ] + }, { "label": "Convert PowerShellTips files to JSON file", "type": "shell", diff --git a/Changelog.md b/Changelog.md index 11f3662f..9ae699e6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,12 @@ This page is a list of _notable_ changes made in each version. Every time a tip is added the patch version is incremented, so there will be a lot of patch version changes not documented here. +## v1.4.0 - September 20, 2025 + +Features: + +- Added `Biweekly` and `Monthly` options to the `AutomaticallyWritePowerShellTip` configuration setting to allow more flexibility in how often tips are automatically shown. + ## v1.3.7 - May 20, 2024 Fixes: diff --git a/ReadMe.md b/ReadMe.md index b0801172..2617babc 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -36,7 +36,7 @@ tiPS provides a low-effort way to learn new things about PowerShell, and help yo tiPS is community driven. Have a tip, module, blog post, or community event that you think others may find valuable? -See the Contributing section below for how to add your tips to tiPS. +See [the Contributing section below](#-contribute-a-tip) for how to easily add your own tips to tiPS. If you enjoy this module, consider giving it a GitHub star ⭐ to show your support. @@ -47,7 +47,7 @@ This module brings tips to you with minimal effort required on your part. You can configure tiPS to show a tip every time you open your PowerShell terminal, or show a tip on demand by running the `tips` command. -## 🖼 Screenshots +## 🖼️ Screenshots Calling the `tips` command to show a tip in the terminal: @@ -89,7 +89,7 @@ Add-TiPSImportToPowerShellProfile > You can remove the import statement from your PowerShell profile by running the `Remove-TiPSImportFromPowerShellProfile` command. -### ⚙ Recommended configuration +### ⚙️ Recommended configuration The following configuration is a good balance for displaying new tips automatically and not being overwhelmed by them. @@ -135,12 +135,12 @@ To have a tip automatically displayed every time you open your PowerShell termin Set-TiPSConfiguration -AutomaticallyWritePowerShellTip EverySession ``` -Possible values for the `-AutomaticallyWritePowerShellTip` parameter are `Never`, `EverySession`, `Daily`, and `Weekly`. +Possible values for the `-AutomaticallyWritePowerShellTip` parameter are `Never` (default), `EverySession`, `Daily`, `Weekly`, `Biweekly`, and `Monthly`. Tips will only be automatically shown in interactive PowerShell sessions. This prevents them from appearing unexpectedly when running scripts or other automated processes. -### ⬆ Automatic updates +### ⬆️ Automatic updates New tips are obtained by updating the tiPS module. Instead of remembering to manually update the module, you can have the module automatically update itself by running: @@ -149,12 +149,24 @@ Instead of remembering to manually update the module, you can have the module au Set-TiPSConfiguration -AutomaticallyUpdateModule Weekly ``` -Possible values for the `-AutomaticallyUpdateModule` parameter are `Never`, `Daily`, `Weekly`, `Biweekly`, and `Monthly`. +Possible values for the `-AutomaticallyUpdateModule` parameter are `Never` (default), `Daily`, `Weekly`, `Biweekly`, and `Monthly`. Automatic updates are performed in a background job, so they will not block your PowerShell session from starting. The updated module will be loaded the next time you open a PowerShell terminal or import tiPS. Old versions of the module are automatically deleted after a successful update. +### 🔀 Tip retrieval order + +By default, the most recent tips are shown first. +This means that if a new tip is submitted, it will be shown next. +You can change the order in which tips are retrieved by running: + +```powershell +Set-TiPSConfiguration -TipOrder Random +``` + +Possible values for the `-TipOrder` parameter are `NewestFirst` (default), `OldestFirst`, and `Random`. + ### ❌ Uninstalling tiPS If you imported tiPS into your PowerShell profile, the import statement will not be removed if you just uninstall the tiPS module. @@ -194,26 +206,44 @@ tiPS is open source, and contributions are not only welcome, they are encouraged Have a PowerShell tip you want to share? Know of a great module, blog post, or community event that you think others should know about? -Follow these steps to add your tip to tiPS: +There are 2 ways to submit your tip to tiPS: -1. Fork this repo to your account ([See GitHub docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo)). -1. Clone the repo to your local machine, or open it in GitHub Codespaces, and create a new branch. -1. Run the [New-PowerShellTip script](/tools/New-PowerShellTip.ps1) to create your new tip file in the [PowerShellTips directory](/src/PowerShellTips/). -1. Modify it with your tip info and commit your changes. -1. Submit a pull request from your fork back to this repo ([See GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)). +1. Use [the GitHub issue template](https://github.com/deadlydog/PowerShell.tiPS/issues/new?template=new_powershell_tip.yml) to create a PR for your new tip. + This is the easiest way to submit a tip. +1. Fork the repo and create a PR for your tip the traditional way. + You will need to fork the repo if you want to contribute other changes to the module, besides just new tips. -🎦 [This video](https://youtu.be/OBg1KJf8sC4?si=CXEtMmaaZ3ybQAVF) walks through the above steps, showing how to contribute a new tip 🎦 + Follow the steps below to fork the repo and manually submit your PR: + 1. Fork this repo to your account ([See GitHub docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo)). + 1. Clone the repo to your local machine, or open it in GitHub Codespaces, and create a new branch. + 1. Run the [New-PowerShellTip script](/tools/New-PowerShellTip.ps1) to create your new tip file in the [PowerShellTips directory](/src/PowerShellTips/). + 1. Modify it with your tip info and commit your changes. + 1. Submit a pull request from your fork back to this repo ([See GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)). + + 🎦 [This video](https://youtu.be/OBg1KJf8sC4?si=CXEtMmaaZ3ybQAVF) walks through the above steps, showing how to fork the repo to contribute a new tip 🎦 tiPS is meant to be a community driven project, so please help make it better by contributing your tips. Issues, Discussions, and Pull Requests are welcome. See [the Contributing page](/docs/Contributing.md) for more details. -## 🛣 Roadmap +## 👥 Contributors + +Thanks to these wonderful people who have contributed to tiPS: + +<p align="center"> + <a href="https://github.com/deadlydog" title="deadlydog"><img src="https://avatars.githubusercontent.com/u/1187140?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="deadlydog"></a> + <a href="https://github.com/belibug" title="belibug"><img src="https://avatars.githubusercontent.com/u/61643561?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="belibug"></a> + <a href="https://github.com/ruudmens" title="ruudmens"><img src="https://avatars.githubusercontent.com/u/20253186?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="ruudmens"></a> + <a href="https://github.com/santisq" title="santisq"><img src="https://avatars.githubusercontent.com/u/23342410?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="santisq"></a> + <a href="https://github.com/adrimus" title="adrimus"><img src="https://avatars.githubusercontent.com/u/37830358?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="adrimus"></a> + <a href="https://github.com/ehmiiz" title="ehmiiz"><img src="https://avatars.githubusercontent.com/u/43292173?v=4" width="50" height="50" style="border-radius:50%;margin:5px;" alt="ehmiiz"></a> +</p> + +## 🛣️ Roadmap Below is a short list of planned enhancements for tiPS: -- An easier way to add new tips. e.g. a simple web form that can be filled out. - A way to customize the look of the tip when it is displayed. e.g. add a border around it, change the colors, etc. - Add a `Write-PowerShellTip -Previous` switch to output the last shown tip. @@ -224,7 +254,7 @@ Have other ideas? See what's changed in the module over time by viewing [the changelog](Changelog.md). -## ❤ Donate to support this project +## ❤️ Donate to support this project Buy me a bagel for providing this module open source and for free 🙂 diff --git a/docs/Contributing.md b/docs/Contributing.md index e1ee956c..1917fdef 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -72,6 +72,8 @@ The build process performs the following operations: 1. Generate the PowerShellTips.json file from the files in the PowerShellTips directory. 1. [This ADR](/docs/ArchitectureDecisionRecords/004-Save-all-tips-to-a-single-file.md) explains why we save all of the tips to a single file. 1. Run the PSScriptAnalyzer linter on all of the PowerShell files. +1. Run the CSpell spell checker on all files to find any spelling mistakes. + 1. If CSpell flags a word that is not actually misspelled, you can add it to the `.cspell.json` file in the root of the repository. 1. (Pipeline build only) Concatenate all of the PowerShell file contents into the psm1 file, and delete the ps1 files. 1. [This ADR](/docs/ArchitectureDecisionRecords/005-How-to-dot-source-files-into-the-module-psm1-file.md) explains why we concatenate the files into the psm1 file. diff --git a/src/CSharpClasses/tiPSClasses/Configuration.cs b/src/CSharpClasses/tiPSClasses/Configuration.cs index ee2a5f89..95dd6d33 100644 --- a/src/CSharpClasses/tiPSClasses/Configuration.cs +++ b/src/CSharpClasses/tiPSClasses/Configuration.cs @@ -14,7 +14,9 @@ public enum WritePowerShellTipCadence Never = 0, EverySession = 1, Daily = 2, - Weekly = 3 + Weekly = 3, + Biweekly = 4, + Monthly = 5 } public enum TipRetrievalOrder diff --git a/src/CSharpClasses/tiPSClasses/PowerShellTip.cs b/src/CSharpClasses/tiPSClasses/PowerShellTip.cs index 9a4dfd3a..47ac1cde 100644 --- a/src/CSharpClasses/tiPSClasses/PowerShellTip.cs +++ b/src/CSharpClasses/tiPSClasses/PowerShellTip.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace tiPS { @@ -72,6 +73,7 @@ public void TrimAllProperties() { Urls[i] = Urls[i].Trim(); } + Urls = Urls.Where(url => !string.IsNullOrWhiteSpace(url)).ToArray(); // Remove empty URLs. Author = Author.Trim(); } diff --git a/src/PowerShellTips/2024-11-06-join-the-research-triangle-powershell-user-group.ps1 b/src/PowerShellTips/2024-11-06-join-the-research-triangle-powershell-user-group.ps1 index 70bde6e4..3a1639cf 100644 --- a/src/PowerShellTips/2024-11-06-join-the-research-triangle-powershell-user-group.ps1 +++ b/src/PowerShellTips/2024-11-06-join-the-research-triangle-powershell-user-group.ps1 @@ -6,9 +6,9 @@ The Research Triangle User Group (RTPSUG) meets virtually, typically once or twi "@ $tip.Example = '' $tip.Urls = @( + 'https://rtpsug.com' 'https://www.meetup.com/research-triangle-powershell-users-group/' 'https://www.youtube.com/c/RTPSUG/' - 'https://rtpsug.com' ) $tip.Category = [tiPS.TipCategory]::Community # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. $tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. diff --git a/src/PowerShellTips/2024-11-15-use-set-psreadlinekeyhandler-to-change-keybindings.ps1 b/src/PowerShellTips/2024-11-15-use-set-psreadlinekeyhandler-to-change-keybindings.ps1 new file mode 100644 index 00000000..c7211d40 --- /dev/null +++ b/src/PowerShellTips/2024-11-15-use-set-psreadlinekeyhandler-to-change-keybindings.ps1 @@ -0,0 +1,33 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-11-15') +$tip.Title = 'Use Set-PSReadLineKeyHandler to change keybindings' +$tip.TipText = @' +You can use the Set-PSReadLineKeyHandler cmdlet to change key bindings. + +A handy shortcut to set is for AcceptNextSuggestionWord which is built within ForwardWord function. This function is bound to the key chord Ctrl + F on Unix but not on Windows. The following is a way to enable that mapping on Windows. +'@ +$tip.Example = @' +# With the following example you will be able to set the Ctrl+f to accept the next word of an inline suggestion. +Set-PSReadLineKeyHandler -Chord "Ctrl+f" -Function ForwardWord + +# You could change the Right arrow to accept the next word instead of the whole suggestion line. +Set-PSReadLineKeyHandler -Chord "RightArrow" -Function ForwardWord +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/using-predictors?view=powershell-7.4#changing-keybindings' + 'https://learn.microsoft.com/en-us/powershell/module/psreadline/set-psreadlinekeyhandler?view=powershell-7.4' +) +$tip.Category = [tiPS.TipCategory]::Terminal # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Adrian Muscat (adrimus)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2024-11-18-get-the-members-of-an-array.ps1 b/src/PowerShellTips/2024-11-18-get-the-members-of-an-array.ps1 new file mode 100644 index 00000000..e5be7f5b --- /dev/null +++ b/src/PowerShellTips/2024-11-18-get-the-members-of-an-array.ps1 @@ -0,0 +1,46 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-11-18') +$tip.Title = 'Get the members of an array' +$tip.TipText = @' +PowerShell sends the items in an array one at a time when you pipe an array to Get-Member and it ignores duplicates +'@ +$tip.Example = @' +# When you pipe to Get-Member PowerShell enumerates the array and you get the properties of the items inside the array, in this case a string +PS C:\> [array]$myArray = @('one','two','three') +PS C:\> $myArray | Get-Member + + TypeName: System.String + +# This example will output the members of the array +PS C:\> Get-Member -InputObject $myArray + + TypeName: System.Object[] + +# This will also do the same by making the array the second item in an array of arrays +,$myArray | Get-Member + +# To see what type of object is in a variable use the GetType method. +PS C:\> $myArray.GetType() + +IsPublic IsSerial Name BaseType +-------- -------- ---- -------- +True True Object[] System.Array +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.4' + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.4#get-the-members-of-an-array' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Adrian Muscat (adrimus)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2024-11-27-use-erroraction-to-change-what-happens-when-an-error-occurs.ps1 b/src/PowerShellTips/2024-11-27-use-erroraction-to-change-what-happens-when-an-error-occurs.ps1 new file mode 100644 index 00000000..e857d6d0 --- /dev/null +++ b/src/PowerShellTips/2024-11-27-use-erroraction-to-change-what-happens-when-an-error-occurs.ps1 @@ -0,0 +1,48 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-11-27') +$tip.Title = 'Use ErrorAction to change what happens when an error occurs' +$tip.TipText = @' +The `-ErrorAction` common parameter allows you to change what happens when a non-terminating error occurs in a cmdlet or script, such as when `Write-Error` is used. The default behavior is to display an error message and continue executing the script. You can change this behavior to: + +- `Stop`: Display the error message and stop executing the script. That is, treat it as a terminating error. +- `Continue`: Display the error message and continue executing the script. This is the default. +- `SilentlyContinue`: Suppress the error message (so it is not written to the error stream) and continue executing the script. +- `Ignore`: Suppress the error message and continue executing the script. Unlike SilentlyContinue, Ignore doesn't add the error message to the $Error automatic variable. +- `Inquire`: Display the error message and prompt the user to continue or stop executing the script. +- `Break`: Display the error message and enter the debugger. Also breaks into the debugger when a terminating error occurs. + +You can set the global behavior for the current scope by setting the `$ErrorActionPreference` variable. This will be the default value for all cmdlets called that don't have the `-ErrorAction` parameter specified. +'@ +$tip.Example = @' +function Test-ErrorAction { + [CmdletBinding()] # This attribute is required to use the `-ErrorAction` common parameter. + param() + + Write-Error "This is an error message" +} + +Test-ErrorAction # Displays the error message and continues executing the script. +Test-ErrorAction -ErrorAction Stop # Displays the error message and stops executing the script. + +$ErrorActionPreference = "SilentlyContinue" # Sets the global error action preference to suppress error messages. +Test-ErrorAction # Suppresses the error message and continues executing the script, because the global setting is SilentlyContinue. +Test-ErrorAction -ErrorAction Stop # Displays the error message and stops executing the script, because the `-ErrorAction` parameter overrides the global setting. +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters#-erroraction' + 'https://devblogs.microsoft.com/scripting/handling-errors-the-powershell-way/' +) +$tip.Category = [tiPS.TipCategory]::Syntax # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2024-11-27-use-errorvariable-to-save-cmdlet-errors-to-a-variable.ps1 b/src/PowerShellTips/2024-11-27-use-errorvariable-to-save-cmdlet-errors-to-a-variable.ps1 new file mode 100644 index 00000000..b2e8eab6 --- /dev/null +++ b/src/PowerShellTips/2024-11-27-use-errorvariable-to-save-cmdlet-errors-to-a-variable.ps1 @@ -0,0 +1,53 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-11-27') +$tip.Title = 'Use ErrorVariable to save cmdlet errors to a variable' +$tip.TipText = @' +When running a cmdlet in PowerShell, you can use the `-ErrorVariable` parameter to save any errors that occur during the cmdlet's execution to a variable. This can be useful for capturing errors and handling them programmatically. There is also a `-WarningVariable` parameter that works similarly for warnings. + +You can also couple these with the `-ErrorAction` and `-WarningAction` common parameters to control how errors and warnings are handled. For example, you can set `-ErrorAction` to `SilentlyContinue` to suppress errors from being displayed on the console, while still capturing them in the error variable. +'@ +$tip.Example = @' +function Test-ErrorVariable { + [CmdletBinding()] # This attribute is required to use the `-ErrorVariable` common parameter. + param() + + Write-Error "This is an error message that should be IGNORED" + Write-Error "This is another error message" +} + +# You don't need to initialize the error and warning variable, but it's a good practice. +$errors = @() +$warnings = @() + +# Use ErrorAction SilentlyContinue to suppress errors from being displayed on the console while still capturing them in the errors variable. +Test-ErrorVariable -WarningVariable warnings -ErrorVariable errors -ErrorAction SilentlyContinue + +if ($errors) { + # Here you can inspect the errors and handle them as needed. + $errors | ForEach-Object { + if ($_ -like '*IGNORED*') { + Write-Verbose "Ignoring error: $_" + } else { + Write-Error "Error: $_" + } + } +} +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters#-errorvariable' + 'https://stuart-moore.com/powershell-using-errorvariable-and-warningvariable/' +) +$tip.Category = [tiPS.TipCategory]::Syntax # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2024-12-26-attend-the-powershell--devops-global-summit.ps1 b/src/PowerShellTips/2024-12-26-attend-the-powershell--devops-global-summit.ps1 new file mode 100644 index 00000000..e93ce730 --- /dev/null +++ b/src/PowerShellTips/2024-12-26-attend-the-powershell--devops-global-summit.ps1 @@ -0,0 +1,28 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-12-26') +$tip.Title = 'Attend the PowerShell + DevOps Global Summit' +$tip.TipText = @' +The PowerShell + DevOps Global Summit happens every year somewhere in the United States, usually in April. The conference is a great place to learn about PowerShell and DevOps, and to meet and network with other people in the PowerShell community, including Microsoft MVPs and the Microsoft PowerShell team. Discuss the latest trends, best practices, and tips and tricks with other PowerShell enthusiasts! + +Want to speak at the conference? The call for speakers is typically open during October and November, where you can submit your session ideas for consideration. Speakers often receive a free ticket to the conference and sometimes additional reimbursement. It's a great opportunity to share your knowledge with the community. + +Check the website for the latest information on the conference dates, location, sessions, and registration details. +'@ +$tip.Example = 'Start-Process https://www.powershellsummit.org' +$tip.Urls = @( + 'https://www.powershellsummit.org' +) +$tip.Category = [tiPS.TipCategory]::Community # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2024-12-26-automate-azure-devops-tasks-with-vsteam.ps1 b/src/PowerShellTips/2024-12-26-automate-azure-devops-tasks-with-vsteam.ps1 new file mode 100644 index 00000000..70767cad --- /dev/null +++ b/src/PowerShellTips/2024-12-26-automate-azure-devops-tasks-with-vsteam.ps1 @@ -0,0 +1,38 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2024-12-26') +$tip.Title = 'Automate Azure DevOps tasks with VSTeam' +$tip.TipText = @' +Many organizations use Azure DevOps as their application lifecycle management (ALM) tool. VSTeam is a PowerShell module that provides cmdlets for many Azure DevOps tasks, which can be easier than calling the Azure DevOps APIs directly. It's a great way to simplify and automate many Azure DevOps tasks, such as retrieving and updating work items, teams, git repos, pull requests, release definitions, and more. +'@ +$tip.Example = @' +# List all commits in the demo project for a specific repository. +Get-VSTeamGitCommit -ProjectName demo -RepositoryId 118C262F-0D4C-4B76-BD9B-7DD8CA12F196 + +# List of all agent pools. +Get-VSTeamPool + +# Gets work items with IDs 47 and 48. +Get-VSTeamWorkItem -Id 47,48 + +# Updates the title of work item 1. +Update-VSTeamWorkItem -WorkItemId 1 -Title "Updated Work Item Title" +'@ +$tip.Urls = @( + 'https://methodsandpractices.github.io/vsteam-docs/' + 'https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/' + 'https://github.com/MethodsAndPractices/vsteam-docs' +) +$tip.Category = [tiPS.TipCategory]::Module # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-01-08-use-start-transcript-for-a-quick-and-easy-log-file.ps1 b/src/PowerShellTips/2025-01-08-use-start-transcript-for-a-quick-and-easy-log-file.ps1 new file mode 100644 index 00000000..3bfcf78b --- /dev/null +++ b/src/PowerShellTips/2025-01-08-use-start-transcript-for-a-quick-and-easy-log-file.ps1 @@ -0,0 +1,43 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-01-08') +$tip.Title = 'Use Start-Transcript for a quick and easy log file' +$tip.TipText = @' +You can use the Start-Transcript cmdlet to easily create a log file of your PowerShell session. This is useful for keeping a record of what you did during an interactive session, or in your scripts to log any output for troubleshooting purposes. + +If you do not specify the log file path, the log file will be created in the user's Home/Documents directory with a timestamped filename. The log file will remain locked by the PowerShell process until Stop-Transcript is called, so be sure to call Stop-Transcript when you are done. + +It is often preferable to log output to a central logging system, however, that often requires much more code and may be overkill for some scenarios. Logging to a local file with Start-Transcript is a quick and easy alternative to capture output for later reference. +'@ +$tip.Example = @' +try { + # Log all script output to a file for easy reference later if needed. + # $PSCommandPath will be the file path of the PowerShell script that is running. + # Include the date and time in the log file name to make it unique instead of overwriting it every run. + [string] $lastRunLogFilePath = "$PSCommandPath.LastRun.log" + Start-Transcript -Path $lastRunLogFilePath + + # Put your script code here... +} +finally { + Stop-Transcript +} +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript' + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/stop-transcript' + 'https://lazyadmin.nl/powershell/start-transcript/' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-03-17-avoid-null-checks-with-the-null-coalescing-operators.ps1 b/src/PowerShellTips/2025-03-17-avoid-null-checks-with-the-null-coalescing-operators.ps1 new file mode 100644 index 00000000..35e658ec --- /dev/null +++ b/src/PowerShellTips/2025-03-17-avoid-null-checks-with-the-null-coalescing-operators.ps1 @@ -0,0 +1,54 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-03-17') +$tip.Title = 'Avoid null checks with the null-coalescing operators' +$tip.TipText = @' +PowerShell 7 introduced the null-coalescing operators `??` and `??=`. These operators allow you to simplify your code by avoiding explicit null checks. + +The null-coalescing operator `??` returns the left-hand operand if it is not null; otherwise, it returns the right-hand operand. This is useful for providing default values. + +The null-coalescing assignment operator `??=` assigns the right-hand operand to the left-hand operand only if the left-hand operand is null. This is useful to help ensure a variable has a value. +'@ +$tip.Example = @' +# Example of using the null-coalescing operator. +$nullValue = $null +$realValue = 'Real Value' +$defaultValue = 'Default Value' +$result = $nullValue ?? $defaultValue # $result will be 'Default Value'. +$result = $realValue ?? $defaultValue # $result will be 'Real Value'. + +# Example of using the null-coalescing assignment operator. +$existingValue = $null +$existingValue ??= 'Assigned Value' # $existingValue will be 'Assigned Value'. +$existingValue ??= 'Another Value' # $existingValue will still be 'Assigned Value'. + +# Example of using the null-coalescing operator with a function. +function Get-Value { + param ([string]$inputValue) + return $inputValue ?? 'No Value Provided' +} + +# Example of using the null-coalescing assignment operator with a function. +function Set-Value { + param ([string]$inputValue) + $inputValue ??= 'Default Value' + return $inputValue +} +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators#null-coalescing-operator-' + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators#null-coalescing-assignment-operator-' +) +$tip.Category = [tiPS.TipCategory]::Syntax # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-03-17-extend-types-with-your-own-properties-and-methods.ps1 b/src/PowerShellTips/2025-03-17-extend-types-with-your-own-properties-and-methods.ps1 new file mode 100644 index 00000000..b195c8cf --- /dev/null +++ b/src/PowerShellTips/2025-03-17-extend-types-with-your-own-properties-and-methods.ps1 @@ -0,0 +1,57 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-03-17') +$tip.Title = 'Extend types with your own properties and methods' +$tip.TipText = @' +The `Update-TypeData` cmdlet can be used to add custom properties and methods to a type. This is useful when you want to extend the functionality of a type that you did not define, such as a built-in .NET class. + +The `Update-TypeData` cmdlet has a `-MemberType` parameter that can be used to specify the type of member to add. The most common types are `NoteProperty` and `ScriptMethod`. The `NoteProperty` type is used to add a new property to an object, while the `ScriptMethod` type is used to add a method to an object to perform some action. + +This is similar to using the `Add-Member` cmdlet, but `Update-TypeData` modifies the type data for all instances of the type, while `Add-Member` modifies only the instance you used it on. +'@ +$tip.Example = @' +# Define the new function logic that we want add to the type. +[scriptblock] $GetValueOrDefaultFunction = { + param($key, $defaultValue) + if ($this.ContainsKey($key)) { + return $this[$key] + } else { + return $defaultValue + } +} + +# Define that we want to add the new method to the Hashtable type, and call it GetValueOrDefault. +$extendedTypeData = @{ + TypeName = 'System.Collections.Hashtable' + MemberType = 'ScriptMethod' + MemberName = 'GetValueOrDefault' + Value = $GetValueOrDefaultFunction +} + +# Add the new method to the Hashtable type, via splatting. +Update-TypeData @extendedTypeData + +# Now we can use the new method on any Hashtable object. +[hashtable] $myHashTable = @{ 'key1' = 'value1' } +$myHashTable.GetValueOrDefault('key1', 'unknown') # Returns 'value1'. +$myHashTable.GetValueOrDefault('key2', 'unknown') # Returns 'unknown'. +$myHashTable['key2'] # Returns $null. +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/update-typedata' + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes_properties#defining-instance-properties-with-update-typedata' + 'https://x.com/blackboxcoder/status/1716585384102985949?t=-Ox1iPV67-4Vqb8wIZ275A' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-04-04-submit-your-own-tips-tips-from-the-web-form.ps1 b/src/PowerShellTips/2025-04-04-submit-your-own-tips-tips-from-the-web-form.ps1 new file mode 100644 index 00000000..241aea5e --- /dev/null +++ b/src/PowerShellTips/2025-04-04-submit-your-own-tips-tips-from-the-web-form.ps1 @@ -0,0 +1,17 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-04-04') +$tip.Title = 'Submit your own tiPS tips from the web form' +$tip.TipText = @' +You can now easily submit your own PowerShell tips for the tiPS module right from the GitHub repository website! There is a new GitHub issue template called "PowerShell tip submission". When you open an issue you will be prompted for your tip's information, such as the tip title, text, category, example code, and URLs. You can even enter your GitHub email address so that you are tagged as a co-author on the commit, allowing you to still get credit in the git history. + +This tip was submitted using a GitHub issue. Visit the website and submit a tip today! +'@ +$tip.Example = @' +Start-Process 'https://github.com/deadlydog/PowerShell.tiPS/issues/new?template=new_powershell_tip.yml' +'@ +$tip.Urls = @('https://github.com/deadlydog/PowerShell.tiPS/issues/new?template=new_powershell_tip.yml','https://github.com/deadlydog/PowerShell.tiPS#-contribute-a-tip') +$tip.Category = [tiPS.TipCategory]::Module # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Tip submitted via GitHub issue workflow. diff --git a/src/PowerShellTips/2025-04-09-reduce-null-checks-by-using-null-conditional-operators.ps1 b/src/PowerShellTips/2025-04-09-reduce-null-checks-by-using-null-conditional-operators.ps1 new file mode 100644 index 00000000..05c4f2d1 --- /dev/null +++ b/src/PowerShellTips/2025-04-09-reduce-null-checks-by-using-null-conditional-operators.ps1 @@ -0,0 +1,52 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-04-09') +$tip.Title = 'Reduce null checks by using null-conditional operators' +$tip.TipText = @' +PowerShell 7.1 introduced the null-conditional operators `?.` and `?[]`. These operators allow you to simplify your code by avoiding explicit null checks when accessing properties or elements of objects or arrays that may be null. + +By default, if you try to index into a null array (e.g. $nullArray[0]), an exception will be thrown. Similarly, if you have Strict Mode enabled and try to access a property of a null object (e.g. $nullObject.SomeProperty), an exception will be thrown. The null-conditional operators allow you to avoid these exceptions and return null instead. You can even chain these together when accessing nested properties. + +One slight caveat is that because '?' can be part of a variable name, you must use braces '{}' around the variable name. e.g. ${someVariable}?.Property or ${someVariable}?[0]. +'@ +$tip.Example = @' +# Example of using the null-conditional operator with an array. +$nullArray = $null +$element = $nullArray[0] # Throws exception 'InvalidOperation: Cannot index into a null array.' +$element = ${nullArray}?[0] # $element will be null without throwing an exception. + +$array = @('Element1', 'Element2') +$element = ${array}?[0] # $element will be 'Element1'. + +# Example of using the null-conditional operator with a property. +$nullObject = $null +$property = $nullObject.Property # $property will be null without throwing an exception, since Strict Mode is off. +$property = ${nullObject}?.Property # $property will be null without throwing an exception. + +Set-StrictMode -Version Latest +$property = $nullObject.Property # Throws a PropertyNotFoundException, since Strict Mode is on. +$property = ${nullObject}?.Property # $property will be null without throwing an exception. + +$object = [PSCustomObject]@{ Property = 'Value' } +$property = ${object}?.Property # $property will be 'Value'. + +# Example with a REST API call and potentially null nested properties. +$result = Invoke-RestMethod -Uri https://dummyjson.com/products +$productWidthOrNull = ${result}?.{products}?[0]?.{dimensions}?.width +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.4#null-conditional-operators--and-' +) +$tip.Category = [tiPS.TipCategory]::Syntax # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-04-10-find-the-net-version-powershell-is-using.ps1 b/src/PowerShellTips/2025-04-10-find-the-net-version-powershell-is-using.ps1 new file mode 100644 index 00000000..5de12e6c --- /dev/null +++ b/src/PowerShellTips/2025-04-10-find-the-net-version-powershell-is-using.ps1 @@ -0,0 +1,34 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-04-10') +$tip.Title = 'Find the .NET version PowerShell is using' +$tip.TipText = @' +$PSVersionTable will tell you which version of PowerShell is running, but it won't tell you which version of .NET is being used. This is important as sometimes you want to call .NET methods from PowerShell, or use .NET types, but they may have changed from one .NET version to the next. + +Minor Pwsh versions currently map to major .NET versions. For example, Pwsh 7.0 uses .NET Core 3.1, 7.1 uses .NET 5, 7.2 uses .NET 6, 7.3 uses .NET 7, 7.4 uses .NET 8, and 7.5 uses .NET 9. This isn't necessarily easy to remember though, and the convention may change in the future. + +Rather than relying on remembering a convention, we can use the following command to display the version of .NET being used: +[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription + +Note: This property exists for all .NET Core versions, but was not introduced in .NET Framework until .NET Framework 4.7.1, so it may not work in Windows PowerShell versions using a lower version of .NET Framework. +'@ +$tip.Example = @' +# Display the .NET version the current PowerShell session is using. +[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.runtimeinformation.frameworkdescription' +) +$tip.Category = [tiPS.TipCategory]::Other # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-04-18-capture-superfluous-parameters-passed-to-your-functions.ps1 b/src/PowerShellTips/2025-04-18-capture-superfluous-parameters-passed-to-your-functions.ps1 new file mode 100644 index 00000000..ff553b6e --- /dev/null +++ b/src/PowerShellTips/2025-04-18-capture-superfluous-parameters-passed-to-your-functions.ps1 @@ -0,0 +1,21 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-04-18') +$tip.Title = 'Capture superfluous parameters passed to your function(s)' +$tip.TipText = @' +Capture any parsed parameter to prevent a function from bombing out when being passed unknown / misspelled variables. +Comes in handy when parsing the $PSBoundParameters from a calling script with just a subset of parameters that are appropriate / needed by your (custom) function. +'@ +$tip.Example = @' +[CmdletBinding()] +param( +[Parameter(DontShow, ValueFromRemainingArguments)]$Superfluous +) + +Write-Verbose -Message "Ignoring superfluous params: $($Superfluous -join ' ')" +'@ +$tip.Urls = @('https://github.com/ChristelVDH/SyncAD2AAD/blob/main/ConnectTo-Graph.ps1','https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.parameterattribute.valuefromremainingarguments','https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.parameterattribute.dontshow') +$tip.Category = [tiPS.TipCategory]::Syntax # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Christel VdH' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Tip submitted via GitHub issue workflow. diff --git a/src/PowerShellTips/2025-04-29-use-measure-object-to-get-stats-about-objects.ps1 b/src/PowerShellTips/2025-04-29-use-measure-object-to-get-stats-about-objects.ps1 new file mode 100644 index 00000000..e2046f76 --- /dev/null +++ b/src/PowerShellTips/2025-04-29-use-measure-object-to-get-stats-about-objects.ps1 @@ -0,0 +1,45 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-04-29') +$tip.Title = 'Use Measure-Object to get stats about objects' +$tip.TipText = @' +You can use the Measure-Object cmdlet to get statistics about objects in PowerShell. This cmdlet can be used to calculate the sum, average, minimum, maximum, and count of numeric properties in objects. When used with text input, it can count characters, words, and lines. + +The cmdlet returns an object containing properties for each statistic, but the statistic is only actually calculated if you provided the switch for it. For example, if you only provide the -Sum switch, the Sum property will contain the sum of the values, but the Average property will be null since the -Average switch was not provided. +'@ +$tip.Example = @' +# Get all statistics about a range of numbers. +1..10 | Measure-Object -Average -Sum -Minimum -Maximum -StandardDeviation + +# Get all statistics about a string. +"Hello there" | Measure-Object -Character -Word -Line + +# Count the number of words in a file. +Get-Content 'C:\path\to\file.txt' | Measure-Object -Word + +# Calculate the total size (Length) of all files in the current directory. +Get-ChildItem | Measure-Object -Property Length -Sum + +# In an array of objects, find the one with the maximum value of the Num property. +@{num=3}, @{num=4}, @{num=5} | Measure-Object -Maximum Num + +# Get the total and maximum CPU time and paged memory size of all processes. +Get-Process | Measure-Object -Property CPU,PagedMemorySize -Sum -Maximum +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/measure-object' + 'https://adamtheautomator.com/powershell-measure-object/' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-05-05-use-join-path-and-split-path-to-create-cross-platform-paths.ps1 b/src/PowerShellTips/2025-05-05-use-join-path-and-split-path-to-create-cross-platform-paths.ps1 new file mode 100644 index 00000000..ed8b8924 --- /dev/null +++ b/src/PowerShellTips/2025-05-05-use-join-path-and-split-path-to-create-cross-platform-paths.ps1 @@ -0,0 +1,53 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-05-05') +$tip.Title = 'Use Join-Path and Split-Path to create cross-platform paths' +$tip.TipText = @' +When creating file paths in PowerShell, use the `Join-Path` cmdlet instead of string concatenation. This ensures that the correct path separator is used for the current platform (e.g. `\` on Windows and `/` on Linux/macOS). PowerShell 6 introduced the -AdditionalChildPath parameter, which allows you to specify multiple child paths to join. + +Similarly, you can use the `Split-Path` cmdlet to split a path into its components. This is useful for extracting the directory or file name from a full path. +'@ +$tip.Example = @' +# Don't do this, as it may not work on all platforms. +[string] $configFilePath = "$HOME/Config/config.json" + +# Do this instead, as it works on all platforms. +[string] $configDirectoryPath = Join-Path -Path $HOME -ChildPath 'Config' +[string] $configFilePath = Join-Path $configDirectoryPath 'config.json' # Excludes -Path and -ChildPath for brevity. + +# You can use System.IO.Path to easily join multiple paths. Helpful in Windows PowerShell. +[string] $configFilePath = [System.IO.Path]::Combine($HOME, 'Config', 'config.json') + +# In PowerShell 6+ you can join multiple child paths at once using -AdditionalChildPath. +[string] $configFilePath = Join-Path -Path $HOME -AdditionalChildPath 'Config' 'config.json' +[string] $xmlFilePath = Join-Path $HOME 'Config' 'config.xml' # Excludes parameter names for brevity. + +# Use -Resolve to ensure we get an absolute path, and error if the path does not exist. +[string] $configFilePath = Join-Path $HOME 'Config' 'config.json' -Resolve + +# Get the name of the file with and without the extension, it's parent directory path, and it's parent directory name. +[string] $fileName = Split-Path -Path $configFilePath -Leaf +[string] $fileNameWithoutExtension = Split-Path -Path $configFilePath -LeafBase +[string] $directoryPath = Split-Path -Path $configFilePath -Parent +[string] $directoryName = Split-Path -Path $directoryPath -Leaf + +# Change the working directory to the folder containing your PowerShell profile. +Set-Location (Split-Path -Path $PROFILE) +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/join-path' + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/split-path' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-09-03-array-lists.ps1 b/src/PowerShellTips/2025-09-03-array-lists.ps1 new file mode 100644 index 00000000..beb77fc8 --- /dev/null +++ b/src/PowerShellTips/2025-09-03-array-lists.ps1 @@ -0,0 +1,38 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-09-03') +$tip.Title = 'Array Lists' +$tip.TipText = @' +An Array list is similar to an array, but it does not have a fixed size like an array does. + +With a fixed-sized array and you add an item to the array, the array is actually recreated with the additional item. This can impact performance, when working with thousands of items. + +Another concern with fixed-size arrays is that there's no simple method to remove an item. + +'@ +$tip.Example = @' +# In PowerShell, you can create an Array list using the `System.Collections.ArrayList` class. Here's how you can create and use an Array list: + +[System.Collections.ArrayList]$computers = @('Server1', 'Server2', 'Server3') + +# To create an empty array ready to add items + +$computers=New-Object System.Collections.ArrayList +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/training/modules/work-arrays-hash-tables-window-powershell-scripts/3-work-array-lists-windows' + 'https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.5#arraylist' +) +$tip.Category = [tiPS.TipCategory]::Performance # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Adrian Muscat (adrimus)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-09-20-adjust-tips-display-frequency.ps1 b/src/PowerShellTips/2025-09-20-adjust-tips-display-frequency.ps1 new file mode 100644 index 00000000..9100c875 --- /dev/null +++ b/src/PowerShellTips/2025-09-20-adjust-tips-display-frequency.ps1 @@ -0,0 +1,31 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-09-20') +$tip.Title = 'Adjust tiPS display frequency' +$tip.TipText = @' +You can adjust how often tips are automatically shown by using the `Set-TiPSConfiguration` command. + +If you find that you are seeing the same tips over and over, it means that you've viewed all of the tips currently in the tiPS module. +While new tips do get added to the module over time, you may want to adjust how often tips are shown, such as changing the frequency from Daily to Weekly. + +By default, tips are shown from newest to oldest, so even if you reduce the frequency you will still see newly added tips next. +'@ +$tip.Example = @' +Set-TiPSConfiguration -AutomaticallyWritePowerShellTip Biweekly +'@ +$tip.Urls = @( + 'https://github.com/deadlydog/PowerShell.tiPS?tab=readme-ov-file#-commands' +) +$tip.Category = [tiPS.TipCategory]::Module # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-09-20-use-get-verb-to-see-approved-verbs.ps1 b/src/PowerShellTips/2025-09-20-use-get-verb-to-see-approved-verbs.ps1 new file mode 100644 index 00000000..4301cf6f --- /dev/null +++ b/src/PowerShellTips/2025-09-20-use-get-verb-to-see-approved-verbs.ps1 @@ -0,0 +1,30 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-09-20') +$tip.Title = 'Use Get-Verb to see approved verbs' +$tip.TipText = @' +Use the Get-Verb cmdlet to see a list of all approved verbs in PowerShell. This is useful when creating your own functions or cmdlets, as using approved verbs helps ensure consistency and discoverability. You know right away what a function does simply by the verb it uses. + +Get-Verb is also great for learning about new verbs you may not have known about, including the typical alias abbreviations. +'@ +$tip.Example = @' +# List all approved verbs and their descriptions. +Get-Verb +'@ +$tip.Urls = @( + 'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-verb' + 'https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands' +) +$tip.Category = [tiPS.TipCategory]::NativeCmdlet # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/PowerShellTips/2025-10-01-use-psdates-to-easily-work-with-time-zones.ps1 b/src/PowerShellTips/2025-10-01-use-psdates-to-easily-work-with-time-zones.ps1 new file mode 100644 index 00000000..215a345c --- /dev/null +++ b/src/PowerShellTips/2025-10-01-use-psdates-to-easily-work-with-time-zones.ps1 @@ -0,0 +1,42 @@ +$tip = [tiPS.PowerShellTip]::new() +$tip.CreatedDate = [DateTime]::Parse('2025-10-01') +$tip.Title = 'Use PSDates to easily work with time zones' +$tip.TipText = @' +The PSDates module makes it easy to work with dates and times across different time zones. It contains functions to help you find and convert date formats, get certain dates based on other dates (first/last day of the month or year, patch Tuesday, etc). It includes functions for working with timezones, unix time, WMI time, crontab schedules, and more. +'@ +$tip.Example = @' +# Convert the local system time to GMT Standard Time. +Convert-TimeZone -ToTimeZone "GMT Standard Time" + +# Convert the date and time 11/17/2017 12:34 AM from 'China Standard Time' to 'US Mountain Standard Time'. +Convert-TimeZone -date '11/17/2017 12:34 AM' -FromTimeZone "China Standard Time" -ToTimeZone "US Mountain Standard Time" + +# Get the datetime for the Unix time 1509512400. +ConvertFrom-UnixTime -UnixTime 1509512400 + +# Get the datetime for the Wmi time 20190912173652.000000-300. +ConvertFrom-WmiDateTime -WmiTime '20190912173652.000000-300' + +# Explain the crontab expression '0 17 * * 1'. Output: At 05:00 PM, only on Monday +Get-CronDescription -Crontab '0 17 * * 1' + +# Get the next occurrence of the crontab from the current time. +Get-CronNextOccurrence -Crontab '0 17 * * *' +'@ +$tip.Urls = @( + 'https://github.com/mdowst/PSDates' +) +$tip.Category = [tiPS.TipCategory]::Module # Community, Editor, Module, NativeCmdlet, Performance, Security, Syntax, Terminal, or Other. +$tip.Author = 'Daniel Schroeder (deadlydog)' # Optional. Get credit for your tip. e.g. 'Daniel Schroeder (deadlydog)'. +#$tip.ExpiryDate = [DateTime]::Parse('2024-10-30') # Optional. If the tip is not relevant after a certain date, set the expiration date. e.g. Announcing a conference or event. + +# Category meanings: +# Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. +# Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. +# NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. +# Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. +# Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. +# Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. +# Other: Tips that don't fit into any of the other categories. diff --git a/src/tiPS/Classes/PowerShellTip.Tests.ps1 b/src/tiPS/Classes/PowerShellTip.Tests.ps1 index fb094175..4796f4b6 100644 --- a/src/tiPS/Classes/PowerShellTip.Tests.ps1 +++ b/src/tiPS/Classes/PowerShellTip.Tests.ps1 @@ -57,6 +57,16 @@ Describe 'Trimming all tip properties' { $tip.Urls | Should -Be $urls $tip.Author | Should -Be $author } + + It 'Should remove empty URLs from the URL array' { + [tiPS.PowerShellTip] $tip = $ValidTip + [string[]] $urls = @('https://Url1.com', '', 'http://Url2.com') + $tip.Urls = $urls + + { $tip.TrimAllProperties() } | Should -Not -Throw + + $tip.Urls | Should -Be @('https://Url1.com', 'http://Url2.com') + } } Context 'Given the properties need whitespace trimmed' { diff --git a/src/tiPS/PowerShellTips.json b/src/tiPS/PowerShellTips.json index 6628fa38..abe02a32 100644 --- a/src/tiPS/PowerShellTips.json +++ b/src/tiPS/PowerShellTips.json @@ -762,5 +762,265 @@ "Category": 0, "ExpiryDate": "9999-12-31T23:59:59.9999999", "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2024-11-06T00:00:00", + "Title": "Join the Research Triangle PowerShell User Group!", + "TipText": "The Research Triangle User Group (RTPSUG) meets virtually, typically once or twice a month, to share ideas and discuss all things PowerShell. Community members often demo modules they've built or PowerShell things they have learned. It's free to attend and everyone is welcome. You can even reach out to the organizers to present something you've built or learned. Presentations are often uploaded to YouTube, allowing you to catch up on sessions you've missed. RTPSUG is an excellent way to stay up to date with both PowerShell technology and the PowerShell community.", + "Example": "", + "Urls": [ + "https://rtpsug.com", + "https://www.meetup.com/research-triangle-powershell-users-group/", + "https://www.youtube.com/c/RTPSUG/" + ], + "Category": 0, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2024-11-15T00:00:00", + "Title": "Use Set-PSReadLineKeyHandler to change keybindings", + "TipText": "You can use the Set-PSReadLineKeyHandler cmdlet to change key bindings.\r\n\r\nA handy shortcut to set is for AcceptNextSuggestionWord which is built within ForwardWord function. This function is bound to the key chord Ctrl + F on Unix but not on Windows. The following is a way to enable that mapping on Windows.", + "Example": "# With the following example you will be able to set the Ctrl+f to accept the next word of an inline suggestion.\r\nSet-PSReadLineKeyHandler -Chord \"Ctrl+f\" -Function ForwardWord\r\n\r\n# You could change the Right arrow to accept the next word instead of the whole suggestion line.\r\nSet-PSReadLineKeyHandler -Chord \"RightArrow\" -Function ForwardWord", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/using-predictors?view=powershell-7.4#changing-keybindings", + "https://learn.microsoft.com/en-us/powershell/module/psreadline/set-psreadlinekeyhandler?view=powershell-7.4" + ], + "Category": 7, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Adrian Muscat (adrimus)" + }, + { + "CreatedDate": "2024-11-18T00:00:00", + "Title": "Get the members of an array", + "TipText": "PowerShell sends the items in an array one at a time when you pipe an array to Get-Member and it ignores duplicates", + "Example": "# When you pipe to Get-Member PowerShell enumerates the array and you get the properties of the items inside the array, in this case a string\r\nPS C:\\> [array]$myArray = @('one','two','three')\r\nPS C:\\> $myArray | Get-Member\r\n\r\n TypeName: System.String\r\n\r\n# This example will output the members of the array\r\nPS C:\\> Get-Member -InputObject $myArray\r\n\r\n TypeName: System.Object[]\r\n\r\n# This will also do the same by making the array the second item in an array of arrays\r\n,$myArray | Get-Member\r\n\r\n# To see what type of object is in a variable use the GetType method.\r\nPS C:\\> $myArray.GetType()\r\n\r\nIsPublic IsSerial Name BaseType\r\n-------- -------- ---- --------\r\nTrue True Object[] System.Array", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.4", + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.4#get-the-members-of-an-array" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Adrian Muscat (adrimus)" + }, + { + "CreatedDate": "2024-11-27T00:00:00", + "Title": "Use ErrorAction to change what happens when an error occurs", + "TipText": "The `-ErrorAction` common parameter allows you to change what happens when a non-terminating error occurs in a cmdlet or script, such as when `Write-Error` is used. The default behavior is to display an error message and continue executing the script. You can change this behavior to:\r\n\r\n- `Stop`: Display the error message and stop executing the script. That is, treat it as a terminating error.\r\n- `Continue`: Display the error message and continue executing the script. This is the default.\r\n- `SilentlyContinue`: Suppress the error message (so it is not written to the error stream) and continue executing the script.\r\n- `Ignore`: Suppress the error message and continue executing the script. Unlike SilentlyContinue, Ignore doesn't add the error message to the $Error automatic variable.\r\n- `Inquire`: Display the error message and prompt the user to continue or stop executing the script.\r\n- `Break`: Display the error message and enter the debugger. Also breaks into the debugger when a terminating error occurs.\r\n\r\nYou can set the global behavior for the current scope by setting the `$ErrorActionPreference` variable. This will be the default value for all cmdlets called that don't have the `-ErrorAction` parameter specified.", + "Example": "function Test-ErrorAction {\r\n [CmdletBinding()] # This attribute is required to use the `-ErrorAction` common parameter.\r\n param()\r\n\r\n Write-Error \"This is an error message\"\r\n}\r\n\r\nTest-ErrorAction # Displays the error message and continues executing the script.\r\nTest-ErrorAction -ErrorAction Stop # Displays the error message and stops executing the script.\r\n\r\n$ErrorActionPreference = \"SilentlyContinue\" # Sets the global error action preference to suppress error messages.\r\nTest-ErrorAction # Suppresses the error message and continues executing the script, because the global setting is SilentlyContinue.\r\nTest-ErrorAction -ErrorAction Stop # Displays the error message and stops executing the script, because the `-ErrorAction` parameter overrides the global setting.", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters#-erroraction", + "https://devblogs.microsoft.com/scripting/handling-errors-the-powershell-way/" + ], + "Category": 6, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2024-11-27T00:00:00", + "Title": "Use ErrorVariable to save cmdlet errors to a variable", + "TipText": "When running a cmdlet in PowerShell, you can use the `-ErrorVariable` parameter to save any errors that occur during the cmdlet's execution to a variable. This can be useful for capturing errors and handling them programmatically. There is also a `-WarningVariable` parameter that works similarly for warnings.\r\n\r\nYou can also couple these with the `-ErrorAction` and `-WarningAction` common parameters to control how errors and warnings are handled. For example, you can set `-ErrorAction` to `SilentlyContinue` to suppress errors from being displayed on the console, while still capturing them in the error variable.", + "Example": "function Test-ErrorVariable {\r\n [CmdletBinding()] # This attribute is required to use the `-ErrorVariable` common parameter.\r\n param()\r\n\r\n Write-Error \"This is an error message that should be IGNORED\"\r\n Write-Error \"This is another error message\"\r\n}\r\n\r\n# You don't need to initialize the error and warning variable, but it's a good practice.\r\n$errors = @()\r\n$warnings = @()\r\n\r\n# Use ErrorAction SilentlyContinue to suppress errors from being displayed on the console while still capturing them in the errors variable.\r\nTest-ErrorVariable -WarningVariable warnings -ErrorVariable errors -ErrorAction SilentlyContinue\r\n\r\nif ($errors) {\r\n # Here you can inspect the errors and handle them as needed.\r\n $errors | ForEach-Object {\r\n if ($_ -like '*IGNORED*') {\r\n Write-Verbose \"Ignoring error: $_\"\r\n } else {\r\n Write-Error \"Error: $_\"\r\n }\r\n }\r\n}", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters#-errorvariable", + "https://stuart-moore.com/powershell-using-errorvariable-and-warningvariable/" + ], + "Category": 6, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2024-12-26T00:00:00", + "Title": "Attend the PowerShell + DevOps Global Summit", + "TipText": "The PowerShell + DevOps Global Summit happens every year somewhere in the United States, usually in April. The conference is a great place to learn about PowerShell and DevOps, and to meet and network with other people in the PowerShell community, including Microsoft MVPs and the Microsoft PowerShell team. Discuss the latest trends, best practices, and tips and tricks with other PowerShell enthusiasts!\r\n\r\nWant to speak at the conference? The call for speakers is typically open during October and November, where you can submit your session ideas for consideration. Speakers often receive a free ticket to the conference and sometimes additional reimbursement. It's a great opportunity to share your knowledge with the community.\r\n\r\nCheck the website for the latest information on the conference dates, location, sessions, and registration details.", + "Example": "Start-Process https://www.powershellsummit.org", + "Urls": [ + "https://www.powershellsummit.org" + ], + "Category": 0, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2024-12-26T00:00:00", + "Title": "Automate Azure DevOps tasks with VSTeam", + "TipText": "Many organizations use Azure DevOps as their application lifecycle management (ALM) tool. VSTeam is a PowerShell module that provides cmdlets for many Azure DevOps tasks, which can be easier than calling the Azure DevOps APIs directly. It's a great way to simplify and automate many Azure DevOps tasks, such as retrieving and updating work items, teams, git repos, pull requests, release definitions, and more.", + "Example": "# List all commits in the demo project for a specific repository.\r\nGet-VSTeamGitCommit -ProjectName demo -RepositoryId 118C262F-0D4C-4B76-BD9B-7DD8CA12F196\r\n\r\n# List of all agent pools.\r\nGet-VSTeamPool\r\n\r\n# Gets work items with IDs 47 and 48.\r\nGet-VSTeamWorkItem -Id 47,48\r\n\r\n# Updates the title of work item 1.\r\nUpdate-VSTeamWorkItem -WorkItemId 1 -Title \"Updated Work Item Title\"", + "Urls": [ + "https://methodsandpractices.github.io/vsteam-docs/", + "https://methodsandpractices.github.io/vsteam-docs/docs/modules/vsteam/commands/", + "https://github.com/MethodsAndPractices/vsteam-docs" + ], + "Category": 2, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-01-08T00:00:00", + "Title": "Use Start-Transcript for a quick and easy log file", + "TipText": "You can use the Start-Transcript cmdlet to easily create a log file of your PowerShell session. This is useful for keeping a record of what you did during an interactive session, or in your scripts to log any output for troubleshooting purposes.\r\n\r\nIf you do not specify the log file path, the log file will be created in the user's Home/Documents directory with a timestamped filename. The log file will remain locked by the PowerShell process until Stop-Transcript is called, so be sure to call Stop-Transcript when you are done.\r\n\r\nIt is often preferable to log output to a central logging system, however, that often requires much more code and may be overkill for some scenarios. Logging to a local file with Start-Transcript is a quick and easy alternative to capture output for later reference.", + "Example": "try {\r\n # Log all script output to a file for easy reference later if needed.\r\n # $PSCommandPath will be the file path of the PowerShell script that is running.\r\n # Include the date and time in the log file name to make it unique instead of overwriting it every run.\r\n [string] $lastRunLogFilePath = \"$PSCommandPath.LastRun.log\"\r\n Start-Transcript -Path $lastRunLogFilePath\r\n\r\n # Put your script code here...\r\n}\r\nfinally {\r\n Stop-Transcript\r\n}", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript", + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/stop-transcript", + "https://lazyadmin.nl/powershell/start-transcript/" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-03-17T00:00:00", + "Title": "Avoid null checks with the null-coalescing operators", + "TipText": "PowerShell 7 introduced the null-coalescing operators `??` and `??=`. These operators allow you to simplify your code by avoiding explicit null checks.\r\n\r\nThe null-coalescing operator `??` returns the left-hand operand if it is not null; otherwise, it returns the right-hand operand. This is useful for providing default values.\r\n\r\nThe null-coalescing assignment operator `??=` assigns the right-hand operand to the left-hand operand only if the left-hand operand is null. This is useful to help ensure a variable has a value.", + "Example": "# Example of using the null-coalescing operator.\r\n$nullValue = $null\r\n$realValue = 'Real Value'\r\n$defaultValue = 'Default Value'\r\n$result = $nullValue ?? $defaultValue # $result will be 'Default Value'.\r\n$result = $realValue ?? $defaultValue # $result will be 'Real Value'.\r\n\r\n# Example of using the null-coalescing assignment operator.\r\n$existingValue = $null\r\n$existingValue ??= 'Assigned Value' # $existingValue will be 'Assigned Value'.\r\n$existingValue ??= 'Another Value' # $existingValue will still be 'Assigned Value'.\r\n\r\n# Example of using the null-coalescing operator with a function.\r\nfunction Get-Value {\r\n\tparam ([string]$inputValue)\r\n\treturn $inputValue ?? 'No Value Provided'\r\n}\r\n\r\n# Example of using the null-coalescing assignment operator with a function.\r\nfunction Set-Value {\r\n\tparam ([string]$inputValue)\r\n\t$inputValue ??= 'Default Value'\r\n\treturn $inputValue\r\n}", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators#null-coalescing-operator-", + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators#null-coalescing-assignment-operator-" + ], + "Category": 6, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-03-17T00:00:00", + "Title": "Extend types with your own properties and methods", + "TipText": "The `Update-TypeData` cmdlet can be used to add custom properties and methods to a type. This is useful when you want to extend the functionality of a type that you did not define, such as a built-in .NET class.\r\n\r\nThe `Update-TypeData` cmdlet has a `-MemberType` parameter that can be used to specify the type of member to add. The most common types are `NoteProperty` and `ScriptMethod`. The `NoteProperty` type is used to add a new property to an object, while the `ScriptMethod` type is used to add a method to an object to perform some action.\r\n\r\nThis is similar to using the `Add-Member` cmdlet, but `Update-TypeData` modifies the type data for all instances of the type, while `Add-Member` modifies only the instance you used it on.", + "Example": "# Define the new function logic that we want add to the type.\r\n[scriptblock] $GetValueOrDefaultFunction = {\r\n param($key, $defaultValue)\r\n if ($this.ContainsKey($key)) {\r\n return $this[$key]\r\n } else {\r\n return $defaultValue\r\n }\r\n}\r\n\r\n# Define that we want to add the new method to the Hashtable type, and call it GetValueOrDefault.\r\n$extendedTypeData = @{\r\n TypeName = 'System.Collections.Hashtable'\r\n MemberType = 'ScriptMethod'\r\n MemberName = 'GetValueOrDefault'\r\n Value = $GetValueOrDefaultFunction\r\n}\r\n\r\n# Add the new method to the Hashtable type, via splatting.\r\nUpdate-TypeData @extendedTypeData\r\n\r\n# Now we can use the new method on any Hashtable object.\r\n[hashtable] $myHashTable = @{ 'key1' = 'value1' }\r\n$myHashTable.GetValueOrDefault('key1', 'unknown') # Returns 'value1'.\r\n$myHashTable.GetValueOrDefault('key2', 'unknown') # Returns 'unknown'.\r\n$myHashTable['key2'] # Returns $null.", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/update-typedata", + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes_properties#defining-instance-properties-with-update-typedata", + "https://x.com/blackboxcoder/status/1716585384102985949?t=-Ox1iPV67-4Vqb8wIZ275A" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-04-04T00:00:00", + "Title": "Submit your own tiPS tips from the web form", + "TipText": "You can now easily submit your own PowerShell tips for the tiPS module right from the GitHub repository website! There is a new GitHub issue template called \"PowerShell tip submission\". When you open an issue you will be prompted for your tip's information, such as the tip title, text, category, example code, and URLs. You can even enter your GitHub email address so that you are tagged as a co-author on the commit, allowing you to still get credit in the git history.\r\n\r\nThis tip was submitted using a GitHub issue. Visit the website and submit a tip today!", + "Example": "Start-Process 'https://github.com/deadlydog/PowerShell.tiPS/issues/new?template=new_powershell_tip.yml'", + "Urls": [ + "https://github.com/deadlydog/PowerShell.tiPS/issues/new?template=new_powershell_tip.yml", + "https://github.com/deadlydog/PowerShell.tiPS#-contribute-a-tip" + ], + "Category": 2, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-04-09T00:00:00", + "Title": "Reduce null checks by using null-conditional operators", + "TipText": "PowerShell 7.1 introduced the null-conditional operators `?.` and `?[]`. These operators allow you to simplify your code by avoiding explicit null checks when accessing properties or elements of objects or arrays that may be null.\r\n\r\nBy default, if you try to index into a null array (e.g. $nullArray[0]), an exception will be thrown. Similarly, if you have Strict Mode enabled and try to access a property of a null object (e.g. $nullObject.SomeProperty), an exception will be thrown. The null-conditional operators allow you to avoid these exceptions and return null instead. You can even chain these together when accessing nested properties.\r\n\r\nOne slight caveat is that because '?' can be part of a variable name, you must use braces '{}' around the variable name. e.g. ${someVariable}?.Property or ${someVariable}?[0].", + "Example": "# Example of using the null-conditional operator with an array.\r\n$nullArray = $null\r\n$element = $nullArray[0] # Throws exception 'InvalidOperation: Cannot index into a null array.'\r\n$element = ${nullArray}?[0] # $element will be null without throwing an exception.\r\n\r\n$array = @('Element1', 'Element2')\r\n$element = ${array}?[0] # $element will be 'Element1'.\r\n\r\n# Example of using the null-conditional operator with a property.\r\n$nullObject = $null\r\n$property = $nullObject.Property # $property will be null without throwing an exception, since Strict Mode is off.\r\n$property = ${nullObject}?.Property # $property will be null without throwing an exception.\r\n\r\nSet-StrictMode -Version Latest\r\n$property = $nullObject.Property # Throws a PropertyNotFoundException, since Strict Mode is on.\r\n$property = ${nullObject}?.Property # $property will be null without throwing an exception.\r\n\r\n$object = [PSCustomObject]@{ Property = 'Value' }\r\n$property = ${object}?.Property # $property will be 'Value'.\r\n\r\n# Example with a REST API call and potentially null nested properties.\r\n$result = Invoke-RestMethod -Uri https://dummyjson.com/products\r\n$productWidthOrNull = ${result}?.{products}?[0]?.{dimensions}?.width", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.4#null-conditional-operators--and-" + ], + "Category": 6, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-04-10T00:00:00", + "Title": "Find the .NET version PowerShell is using", + "TipText": "$PSVersionTable will tell you which version of PowerShell is running, but it won't tell you which version of .NET is being used. This is important as sometimes you want to call .NET methods from PowerShell, or use .NET types, but they may have changed from one .NET version to the next.\r\n\r\nMinor Pwsh versions currently map to major .NET versions. For example, Pwsh 7.0 uses .NET Core 3.1, 7.1 uses .NET 5, 7.2 uses .NET 6, 7.3 uses .NET 7, 7.4 uses .NET 8, and 7.5 uses .NET 9. This isn't necessarily easy to remember though, and the convention may change in the future.\r\n\r\nRather than relying on remembering a convention, we can use the following command to display the version of .NET being used:\r\n[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription\r\n\r\nNote: This property exists for all .NET Core versions, but was not introduced in .NET Framework until .NET Framework 4.7.1, so it may not work in Windows PowerShell versions using a lower version of .NET Framework.", + "Example": "# Display the .NET version the current PowerShell session is using.\r\n[System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription", + "Urls": [ + "https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.runtimeinformation.frameworkdescription" + ], + "Category": 8, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-04-18T00:00:00", + "Title": "Capture superfluous parameters passed to your function(s)", + "TipText": "Capture any parsed parameter to prevent a function from bombing out when being passed unknown / misspelled variables.\r\nComes in handy when parsing the $PSBoundParameters from a calling script with just a subset of parameters that are appropriate / needed by your (custom) function.", + "Example": "[CmdletBinding()]\r\nparam(\r\n[Parameter(DontShow, ValueFromRemainingArguments)]$Superfluous\r\n)\r\n\r\nWrite-Verbose -Message \"Ignoring superfluous params: $($Superfluous -join ' ')\"", + "Urls": [ + "https://github.com/ChristelVDH/SyncAD2AAD/blob/main/ConnectTo-Graph.ps1", + "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.parameterattribute.valuefromremainingarguments", + "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.parameterattribute.dontshow" + ], + "Category": 6, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Christel VdH" + }, + { + "CreatedDate": "2025-04-29T00:00:00", + "Title": "Use Measure-Object to get stats about objects", + "TipText": "You can use the Measure-Object cmdlet to get statistics about objects in PowerShell. This cmdlet can be used to calculate the sum, average, minimum, maximum, and count of numeric properties in objects. When used with text input, it can count characters, words, and lines.\r\n\r\nThe cmdlet returns an object containing properties for each statistic, but the statistic is only actually calculated if you provided the switch for it. For example, if you only provide the -Sum switch, the Sum property will contain the sum of the values, but the Average property will be null since the -Average switch was not provided.", + "Example": "# Get all statistics about a range of numbers.\r\n1..10 | Measure-Object -Average -Sum -Minimum -Maximum -StandardDeviation\r\n\r\n# Get all statistics about a string.\r\n\"Hello there\" | Measure-Object -Character -Word -Line\r\n\r\n# Count the number of words in a file.\r\nGet-Content 'C:\\path\\to\\file.txt' | Measure-Object -Word\r\n\r\n# Calculate the total size (Length) of all files in the current directory.\r\nGet-ChildItem | Measure-Object -Property Length -Sum\r\n\r\n# In an array of objects, find the one with the maximum value of the Num property.\r\n@{num=3}, @{num=4}, @{num=5} | Measure-Object -Maximum Num\r\n\r\n# Get the total and maximum CPU time and paged memory size of all processes.\r\nGet-Process | Measure-Object -Property CPU,PagedMemorySize -Sum -Maximum", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/measure-object", + "https://adamtheautomator.com/powershell-measure-object/" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-05-05T00:00:00", + "Title": "Use Join-Path and Split-Path to create cross-platform paths", + "TipText": "When creating file paths in PowerShell, use the `Join-Path` cmdlet instead of string concatenation. This ensures that the correct path separator is used for the current platform (e.g. `\\` on Windows and `/` on Linux/macOS). PowerShell 6 introduced the -AdditionalChildPath parameter, which allows you to specify multiple child paths to join.\r\n\r\nSimilarly, you can use the `Split-Path` cmdlet to split a path into its components. This is useful for extracting the directory or file name from a full path.", + "Example": "# Don't do this, as it may not work on all platforms.\r\n[string] $configFilePath = \"$HOME/Config/config.json\"\r\n\r\n# Do this instead, as it works on all platforms.\r\n[string] $configDirectoryPath = Join-Path -Path $HOME -ChildPath 'Config'\r\n[string] $configFilePath = Join-Path $configDirectoryPath 'config.json' # Excludes -Path and -ChildPath for brevity.\r\n\r\n# You can use System.IO.Path to easily join multiple paths. Helpful in Windows PowerShell.\r\n[string] $configFilePath = [System.IO.Path]::Combine($HOME, 'Config', 'config.json')\r\n\r\n# In PowerShell 6+ you can join multiple child paths at once using -AdditionalChildPath.\r\n[string] $configFilePath = Join-Path -Path $HOME -AdditionalChildPath 'Config' 'config.json'\r\n[string] $xmlFilePath = Join-Path $HOME 'Config' 'config.xml' # Excludes parameter names for brevity.\r\n\r\n# Use -Resolve to ensure we get an absolute path, and error if the path does not exist.\r\n[string] $configFilePath = Join-Path $HOME 'Config' 'config.json' -Resolve\r\n\r\n# Get the name of the file with and without the extension, it's parent directory path, and it's parent directory name.\r\n[string] $fileName = Split-Path -Path $configFilePath -Leaf\r\n[string] $fileNameWithoutExtension = Split-Path -Path $configFilePath -LeafBase\r\n[string] $directoryPath = Split-Path -Path $configFilePath -Parent\r\n[string] $directoryName = Split-Path -Path $directoryPath -Leaf\r\n\r\n# Change the working directory to the folder containing your PowerShell profile.\r\nSet-Location (Split-Path -Path $PROFILE)", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/join-path", + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/split-path" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-09-03T00:00:00", + "Title": "Array Lists", + "TipText": "An Array list is similar to an array, but it does not have a fixed size like an array does.\r\n\r\nWith a fixed-sized array and you add an item to the array, the array is actually recreated with the additional item. This can impact performance, when working with thousands of items.\r\n\r\nAnother concern with fixed-size arrays is that there's no simple method to remove an item.", + "Example": "# In PowerShell, you can create an Array list using the `System.Collections.ArrayList` class. Here's how you can create and use an Array list:\r\n\r\n[System.Collections.ArrayList]$computers = @('Server1', 'Server2', 'Server3')\r\n\r\n# To create an empty array ready to add items\r\n\r\n$computers=New-Object System.Collections.ArrayList", + "Urls": [ + "https://learn.microsoft.com/en-us/training/modules/work-arrays-hash-tables-window-powershell-scripts/3-work-array-lists-windows", + "https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.5#arraylist" + ], + "Category": 4, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Adrian Muscat (adrimus)" + }, + { + "CreatedDate": "2025-09-20T00:00:00", + "Title": "Adjust tiPS display frequency", + "TipText": "You can adjust how often tips are automatically shown by using the `Set-TiPSConfiguration` command.\r\n\r\nIf you find that you are seeing the same tips over and over, it means that you've viewed all of the tips currently in the tiPS module.\r\nWhile new tips do get added to the module over time, you may want to adjust how often tips are shown, such as changing the frequency from Daily to Weekly.\r\n\r\nBy default, tips are shown from newest to oldest, so even if you reduce the frequency you will still see newly added tips next.", + "Example": "Set-TiPSConfiguration -AutomaticallyWritePowerShellTip Biweekly", + "Urls": [ + "https://github.com/deadlydog/PowerShell.tiPS?tab=readme-ov-file#-commands" + ], + "Category": 2, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-09-20T00:00:00", + "Title": "Use Get-Verb to see approved verbs", + "TipText": "Use the Get-Verb cmdlet to see a list of all approved verbs in PowerShell. This is useful when creating your own functions or cmdlets, as using approved verbs helps ensure consistency and discoverability. You know right away what a function does simply by the verb it uses.\r\n\r\nGet-Verb is also great for learning about new verbs you may not have known about, including the typical alias abbreviations.", + "Example": "# List all approved verbs and their descriptions.\r\nGet-Verb", + "Urls": [ + "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-verb", + "https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands" + ], + "Category": 3, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" + }, + { + "CreatedDate": "2025-10-01T00:00:00", + "Title": "Use PSDates to easily work with time zones", + "TipText": "The PSDates module makes it easy to work with dates and times across different time zones. It contains functions to help you find and convert date formats, get certain dates based on other dates (first/last day of the month or year, patch Tuesday, etc). It includes functions for working with timezones, unix time, WMI time, crontab schedules, and more.", + "Example": "# Convert the local system time to GMT Standard Time.\r\nConvert-TimeZone -ToTimeZone \"GMT Standard Time\"\r\n\r\n# Convert the date and time 11/17/2017 12:34 AM from 'China Standard Time' to 'US Mountain Standard Time'.\r\nConvert-TimeZone -date '11/17/2017 12:34 AM' -FromTimeZone \"China Standard Time\" -ToTimeZone \"US Mountain Standard Time\"\r\n\r\n# Get the datetime for the Unix time 1509512400.\r\nConvertFrom-UnixTime -UnixTime 1509512400\r\n\r\n# Get the datetime for the Wmi time 20190912173652.000000-300.\r\nConvertFrom-WmiDateTime -WmiTime '20190912173652.000000-300'\r\n\r\n# Explain the crontab expression '0 17 * * 1'. Output: At 05:00 PM, only on Monday\r\nGet-CronDescription -Crontab '0 17 * * 1'\r\n\r\n# Get the next occurrence of the crontab from the current time.\r\nGet-CronNextOccurrence -Crontab '0 17 * * *'", + "Urls": [ + "https://github.com/mdowst/PSDates" + ], + "Category": 2, + "ExpiryDate": "9999-12-31T23:59:59.9999999", + "Author": "Daniel Schroeder (deadlydog)" } ] diff --git a/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.Tests.ps1 b/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.Tests.ps1 index 160090d8..3196ba05 100644 --- a/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.Tests.ps1 +++ b/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.Tests.ps1 @@ -83,6 +83,50 @@ InModuleScope -ModuleName tiPS { # Must use InModuleScope to call private functi } } + Context 'When the WritePowerShellTipCadence is Biweekly' { + It 'Should update the module if the last update was more than 14 days ago' { + $config = [tiPS.Configuration]::new() + $config.AutoWritePowerShellTipCadence = [tiPS.WritePowerShellTipCadence]::Biweekly + WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate ([DateTime]::Now.Date.AddDays(-15)) + + WriteAutomaticPowerShellTipIfNeeded -Config $config + + Assert-MockCalled WriteAutomaticPowerShellTip -Times 1 -Exactly + } + + It 'Should not update the module if the last update was less than 14 days ago' { + $config = [tiPS.Configuration]::new() + $config.AutoWritePowerShellTipCadence = [tiPS.WritePowerShellTipCadence]::Biweekly + WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate ([DateTime]::Now.Date.AddDays(-13)) + + WriteAutomaticPowerShellTipIfNeeded -Config $config + + Assert-MockCalled WriteAutomaticPowerShellTip -Times 0 -Exactly + } + } + + Context 'When the WritePowerShellTipCadence is Monthly' { + It 'Should update the module if the last update was more than 30 days ago' { + $config = [tiPS.Configuration]::new() + $config.AutoWritePowerShellTipCadence = [tiPS.WritePowerShellTipCadence]::Monthly + WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate ([DateTime]::Now.Date.AddDays(-31)) + + WriteAutomaticPowerShellTipIfNeeded -Config $config + + Assert-MockCalled WriteAutomaticPowerShellTip -Times 1 -Exactly + } + + It 'Should not update the module if the last update was less than 30 days ago' { + $config = [tiPS.Configuration]::new() + $config.AutoWritePowerShellTipCadence = [tiPS.WritePowerShellTipCadence]::Monthly + WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate ([DateTime]::Now.Date.AddDays(-29)) + + WriteAutomaticPowerShellTipIfNeeded -Config $config + + Assert-MockCalled WriteAutomaticPowerShellTip -Times 0 -Exactly + } + } + Context 'When the PowerShell session is not interactive' { BeforeEach { Mock -CommandName TestPowerShellSessionIsInteractive -MockWith { return $false } diff --git a/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.ps1 b/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.ps1 index aa0e1c87..b7005a24 100644 --- a/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.ps1 +++ b/src/tiPS/Private/AutomaticWritePowerShellTipFunctions.ps1 @@ -26,6 +26,8 @@ function WriteAutomaticPowerShellTipIfNeeded ([tiPS.WritePowerShellTipCadence]::EverySession) { $shouldShowTip = $true; break } ([tiPS.WritePowerShellTipCadence]::Daily) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 1; break } ([tiPS.WritePowerShellTipCadence]::Weekly) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 7; break } + ([tiPS.WritePowerShellTipCadence]::Biweekly) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 14; break } + ([tiPS.WritePowerShellTipCadence]::Monthly) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 30; break } } if ($shouldShowTip) diff --git a/src/tiPS/Public/Set-TiPSConfiguration.ps1 b/src/tiPS/Public/Set-TiPSConfiguration.ps1 index 40c27360..68c70cd4 100644 --- a/src/tiPS/Public/Set-TiPSConfiguration.ps1 +++ b/src/tiPS/Public/Set-TiPSConfiguration.ps1 @@ -18,11 +18,11 @@ function Set-TiPSConfiguration This also means that the new module version will not be used until the next time the module is imported, or the next time a PowerShell session is started. Old versions of the module are automatically deleted after a successful update. - Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never. + Valid values are Never, Daily, Weekly, Biweekly, and Monthly. Default is Never. .PARAMETER AutomaticallyWritePowerShellTip Whether to automatically write a PowerShell tip at session startup. - Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never. + Valid values are Never, EverySession, Daily, Weekly, Biweekly, and Monthly. Default is Never. .PARAMETER TipRetrievalOrder The order in which to retrieve PowerShell tips. diff --git a/tools/New-PowerShellTip.ps1 b/tools/New-PowerShellTip.ps1 index 6723215c..dfcdc781 100644 --- a/tools/New-PowerShellTip.ps1 +++ b/tools/New-PowerShellTip.ps1 @@ -29,13 +29,13 @@ $dummyTip = [tiPS.PowerShellTip]::new() $dummyTip.CreatedDate = [DateTime]::Today $dummyTip.Title = $tipTitle.Trim() -[string] $today = $dummyTip.CreatedDate.ToString('yyyy-MM-dd') +[string] $createdDate = $dummyTip.CreatedDate.ToString('yyyy-MM-dd') [string] $powerShellTipsFilesDirectoryPath = Resolve-Path -Path "$PSScriptRoot/../src/PowerShellTips" [string] $newTipFileName = $dummyTip.Id + '.ps1' [string] $newTipFilePath = Join-Path -Path $powerShellTipsFilesDirectoryPath -ChildPath $newTipFileName [string] $tipTemplateFileContents = @" `$tip = [tiPS.PowerShellTip]::new() -`$tip.CreatedDate = [DateTime]::Parse('$today') +`$tip.CreatedDate = [DateTime]::Parse('$createdDate') `$tip.Title = '$($tipTitle.Replace("'", "''"))' `$tip.TipText = @' A short description of the tip. @@ -58,18 +58,18 @@ Example code to demonstrate the tip. This can also be multiple lines if needed. # Category meanings: # Community: Social events and community resources. e.g. PowerShell Summit, podcasts, etc. -# Editor: Editor tips and extensions. e.g. VSCode, ISE, etc. +# Editor: Editor tips and extensions. e.g. VS Code, ISE, etc. # Module: Modules and module tips. e.g. PSScriptAnalyzer, Pester, etc. # NativeCmdlet: Native cmdlet tips. e.g. Get-Process, Get-ChildItem, Get-Content, etc. # Performance: Tips to improve runtime performance. e.g. foreach vs ForEach-Object, ForEach-Object -Parallel, etc. # Security: Security tips. e.g. ExecutionPolicy, Constrained Language Mode, passwords, etc. -# Syntax: Syntax tips. e.g. splatting, pipeline, etc. +# Syntax: Syntax tips. e.g. splatting, pipeline, keywords, etc. # Terminal: Terminal shortcuts and tips. e.g. PSReadLine, Windows Terminal, ConEmu, etc. # Other: Tips that don't fit into any of the other categories. "@ Write-Output "Creating new PowerShell Tip file and opening it: $newTipFilePath" -Set-Content -Path $newTipFilePath -Value $tipTemplateFileContents -Force +Set-Content -Path $newTipFilePath -Value $tipTemplateFileContents -Encoding utf8 -Force try { Invoke-Item -Path $newTipFilePath -ErrorVariable openTipFileError