Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: PowerShell new syntax: gsudo { ScriptBlock } #178

Merged
merged 20 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,32 @@ jobs:
with:
fetch-depth: 0
- name: Run Tests
id: tests
run: ./build/02-test.ps1
- name: Test Report DotNet
uses: dorny/test-reporter@v1
if: success() || failure() # run this step even if previous step failed
uses: dorny/test-reporter@v1.5.0
if: success() || failure()
with:
name: TestsResults (dotnet)
path: "**/TestResults*.trx"
reporter: dotnet-trx
fail-on-error: false
fail-on-error: true
- name: Test Report PowerShell v5
uses: zyborg/pester-tests-report@v1.5.0 # https://github.com/zyborg/pester-tests-report#inputs
if: success() || failure() # run this step even if previous step failed
uses: dorny/test-reporter@v1.5.0
if: success() || failure()
with:
test_results_path: ./testResults_PS5.xml
report_name: TestResults PowerShell v5.x
report_title: PowerShell v5 Tests
github_token: ${{ secrets.GITHUB_TOKEN }}

name: TestsResults (PowerShell v5)
path: ./testResults_PS5.xml
reporter: java-junit
fail-on-error: true
- name: Test Report PowerShell v7
uses: zyborg/pester-tests-report@v1.5.0 # https://github.com/zyborg/pester-tests-report#inputs
if: success() || failure() # run this step even if previous step failed
uses: dorny/test-reporter@v1.5.0
if: success() || failure()
with:
test_results_path: ./testResults_PS7.xml
report_name: TestResults PowerShell Core (v7.x)
report_title: PowerShell v7 Tests
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Fail on error
if: failure() # run this step only if any of the previous steps failed
run: exit 1
name: TestsResults (PowerShell v7)
path: ./testResults_PS7.xml
reporter: java-junit
fail-on-error: true
build:
name: Build
runs-on: windows-latest
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- 'docs/**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
workflow_call:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ jobs:
with:
name: Binaries
path: ./artifacts

docs:
needs: release
uses: ./.github/workflows/docs.yml
permissions:
id-token: write
contents: read
pages: write
2 changes: 1 addition & 1 deletion build/02-test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ $script = {
$configuration.Run.Path = "src"
$configuration.TestResult.Enabled = $true
$configuration.TestResult.OutputPath = "TestResults_PS$($PSVersionTable.PSVersion.Major).xml"
$configuration.TestResult.OutputFormat = "NUnitXml"
$configuration.TestResult.OutputFormat = "JUnitXml"
# $configuration.Should.ErrorAction = 'Continue'
# $configuration.CodeCoverage.Enabled = $true

Expand Down
179 changes: 112 additions & 67 deletions docs/docs/usage/powershell.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,112 @@ title: Usage from PowerShell
---
## Usage from PowerShell

When the current shell is `PowerShell`, there are three ways to elevate PS commands.
- Run `gsudo` to start an elevated PowerShell session.
- Run `gsudo {command}` to elevate one command. It accepts and returns strings
- Run `Invoke-gsudo { ScriptBlock }` for native ScriptBlock syntax and auto serialization of inputs, outputs and pipeline objects.


:::warning

If you installed `PowerShell Core` using `dotnet tool install PowerShell` [you will have issues](https://github.com/PowerShell/PowerShell/issues/11747). Please install with any another installation method such as: `winget install Microsoft.PowerShell` / `choco install pwsh` / [Download from GitHub](https://github.com/PowerShell/PowerShell/releases/latest) / [Microsoft Store](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D)
If you installed `PowerShell Core` as a `dotnet global tool` (using `dotnet tool install PowerShell`) [you will have issues](https://github.com/PowerShell/PowerShell/issues/11747). Please install with any another installation method such as: `winget install Microsoft.PowerShell` / `choco install pwsh` / [Download from GitHub](https://github.com/PowerShell/PowerShell/releases/latest) / [Microsoft Store](https://apps.microsoft.com/store/detail/powershell/9MZ1SNWT0N5D)

:::

### `gsudo` Command
When the current shell is `PowerShell`, gsudo can be used in the following ways:

- Call `gsudo` to start an elevated PowerShell session.
- To elevate a command, use:
- [`gsudo { ScriptBlock }`](#using-gsudo-scriptblock-syntax) => New, suggested syntax.
- [`gsudo 'string command'`](#using-gsudo-command-syntax) => Old, legacy syntax.
- [`Invoke-gsudo` CmdLet](#using-invoke-gsudo-cmdlet) is a wrapper with better serialization.

- You can [add `gsudo` PowerShell Module](#powershell-profile-config) to your `$PROFILE`
- This enables to use `gsudo !!` to elevate last command.

- In a pipeline of commands, `gsudo` only elevates one command.

`command1 | gsudo elevatedCommand2 | command3`

Or you can elevate the whole pipeline if you put it inside a [script block](#using-gsudo-scriptblock-syntax).
---

`gsudo` detects if it's invoked from PowerShell and elevates PS commands (unless `-d` is used to elevate CMD commands).
### Using `gsudo {ScriptBlock}` syntax

- New! *recommended* way (added in gsudo v1.6.0)
- Express the command to elevate as a PowerShell ScriptBlock, between `{braces}`. PowerShell will parse it and auto-complete commands.
- The ScriptBlock can use literals, but can't access parent or global scope variables (remember it runs in another process). To parametrize the script, you can pass values with `-args` parameter and access them via `$args` array. If you find this painfull, try [`Invoke-gsudo`](#using-invoke-gsudo-cmdlet).

``` powershell
gsudo { Get-Process "chrome" }
gsudo { Get-Process $args } -args "chrome"
gsudo { echo $args[0] $args[1] } -args "Hello", "World"
```

- Output can be captured as PSObjects.
``` powershell
$services = gsudo { Get-Service 'WSearch', 'Winmgmt'}
Write-Output $services.DisplayName
```

- Pipeline input:
- Must be explicitly mapped with `$input`
- If marshaling doesn't work as intended, try [`Invoke-gsudo`](#using-invoke-gsudo-cmdlet)

``` powershell
get-process winword | gsudo { $input | Stop-Process }
```

Examples:

``` powershell
$file='C:\My Secret.txt';
$algorithm='md5';

$hash = gsudo {(Get-FileHash $args[0] -Algorithm $args[1]).Hash} -args $file, $algorithm
```

```powershell
gsudo {PowerShell command to elevate}
gsudo -d {Cmd command to elevate}
```
---

But, the command must be escaped:
### Using `gsudo 'command'` syntax

- If your command doesn't include symbols `()|& <>'`, just prepend `gsudo`.
- This is the old syntax. Is still supported but not recommended.
- Express the command to elevate as a string literal (between `'quotes'`). (And properly escaping your quotes, if needed).
- Outputs are strings, not PSObjects.
- The command can use literals, but can't access parent or global scope variables. To parametrize the script, you can use string substitution:

``` powershell
$file='C:\My Secret.txt';
$algorithm='md5';

```powershell
PS C:\> gsudo Get-Content .\MySecret.txt
$hash = gsudo "(Get-FileHash '$file' -Algorithm $algorithm).Hash"
```

- Or use `gsudo --%` (PowerShell's stop-parsing token) IF your command doesn't include the pipeline op `|`.

``` powershell
PS C:\> gsudo --% (Get-FileHash "\PrivateFolder\MySecret.txt").hash
```
---

- Otherwise put your command to elevate inside a **string literal**:
### Using `Invoke-gsudo` function

``` powershell
PS C:\> gsudo '(Get-FileHash "C:\PrivateFolder\MySecret.txt").hash'
```
**`Invoke-gsudo`** is a wrapper function of `gsudo` with the following benefits:

:::info
The `gsudo` command returns a string that can be captured, not powershell objects. For object serialization, look at the [`Invoke-gsudo cmdlet`](#invoke-gsudo-cmdlet).
:::
- Automatic serialization of inputs, outputs and pipeline objects. The results are serialized and returned (as a `PSObject` or `PSObject[]`).
- The command can't access parent or global scope variables. To parametrize the script, you can:
- Mention your `$variable` as `$using:variableName` and its serialized value will be applied.
- Pass values with `-args` parameter and access them via `$args` array.
- Current Location is preserved for non-FileSystem providers.
- `$ErrorActionPreference` is preserved.
- If your command requires accessing a function on your `$PROFILE` add the `-LoadProfile` parameter. [See More](#loading-your-ps-profile-on-command-elevations).

**Examples:**
Examples:

``` powershell
# Variable substitutions example:
$file='C:\My Secret.txt'; $algorithm='md5';
$hash = gsudo "(Get-FileHash '$file' -Algorithm $algorithm).Hash"
# or
$hash = gsudo "(Get-FileHash ""$file"" -Algorithm $algorithm).Hash"
# Accepts pipeline input.
Get-process SpoolSv | Invoke-gsudo { Stop-Process -Force }

# Variable usage
$folder = "C:\ProtectedFolder"
Invoke-gsudo { Remove-Item $using:folder }

# The result is serialized (PSObject) with properties.
(Invoke-gsudo { Get-ChildItem $using:folder }).LastWriteTime
```

# Skip PowerShell wrapper (with -d): run an .EXE or a CMD command directly (optional, faster)
gsudo -d notepad
### Test elevation success

``` powershell
# Test gsudo success (optional)
if ($LastExitCode -eq 999 ) {
'gsudo failed to elevate!'
Expand All @@ -70,53 +119,49 @@ if ($LastExitCode -eq 999 ) {
} else { 'Success!' }
```

### `Invoke-gsudo` cmdlet
### Elevate CMD Commands

Use **`Invoke-gsudo` CmdLet** to elevate a ScriptBlock (take advantage of better syntax validation and auto-complete), with **auto serialization of inputs, outputs and pipeline objects.**

The ScriptBlock will ran elevated in a different process and lexical scope, so it can't access your existing `$variables`. You if you use the `$using:variableName` syntax, it´s serialized value will be applied. The results are serialized and returned (as a PSObject or PSObject[]).
Use `gsudo -d {command}` to tell gsudo that your command does not requires a new instance of PowerShell to interpret it.

``` powershell
# Accepts pipeline input.
Get-process SpoolSv | Invoke-gsudo { Stop-Process -Force }

# Variable usage
$folder = "C:\ProtectedFolder"
Invoke-gsudo { Remove-Item $using:folder }

# The result is serialized (PSObject) with properties.
(Invoke-gsudo { Get-ChildItem $using:folder }).LastWriteTime
gsudo -d dir C:\
```

## PowerShell Profile Config

- For an enhanced experience, import module `gsudoModule.psd1`. This is optional and enables `gsudo !!`, and param auto-complete for `Invoke-Gsudo` cmdlet.
## gsudo PowerShell Module

For an enhanced experience, import `gsudoModule.psd1`. This is optional and enables `gsudo !!`, and param auto-complete for `Invoke-Gsudo` command.

Add the following line to your $PROFILE (replace with full path)
``` powershell
Import-Module 'C:\FullPathTo\gsudoModule.psd1'

``` powershell
Import-Module 'C:\FullPathTo\gsudoModule.psd1'

# Or let the following line do it for you run:
Get-Command gsudoModule.psd1 | % { Write-Output "`nImport-Module `"$($_.Source)`"" | Add-Content $PROFILE }
```
Get-Command gsudoModule.psd1 | % { Write-Output "`nImport-Module `"$($_.Source)`"" | Add-Content $PROFILE }
```

:::tip
- You can create a custom alias for gsudo or Invoke-gsudo by adding one of these lines to your `$PROFILE`:
- `Set-Alias 'sudo' 'gsudo'` <br/>or
- `Set-Alias 'sudo' 'Invoke-gsudo'`
:::

:::caution
<!-- :::caution
- Windows PowerShell (5.x) and PowerShell Core (>6.x) have different `$PROFILE` configuration files, so follow this steps on the version that you use, or both.
:::
-->
## Loading your PS Profile on command elevations

## Profile loading
When elevating commands, elevation is called with the `-NoProfile` argument. This means the elevated instance won't load your `$PROFILE`. If your command requires your PowerShell profile loaded you can:

For faster performance, elevation is called with the `-NoProfile` argument. If your command requires your PowerShell profile loaded you can:
- Per command, when using `gsudo`, infix `--loadProfile`:

``` powershell
PS C:\> gsudo --loadProfile { echo (1+1) }
```

When using `gsudo`, infix `--loadProfile`:
- `PS C:\> gsudo --loadProfile echo (1+1)`
- Set as a permanent setting with `gsudo config PowerShellLoadProfile true`
- Per command, when using `Invoke-gsudo`, add `-LoadProfile`:
PS C:\> Invoke-Gsudo { echo (1+1) } -LoadProfile

When using `Invoke-gsudo`, add `-LoadProfile`:
- `PS C:\> Invoke-Gsudo { echo (1+1) } -LoadProfile`
- Set as a permanent setting adding `$gsudoLoadProfile=$true` in your `$PROFILE` after `Import-Module C:\FullPathTo\gsudoModule.psd1`
- Set as a permanent setting with: `gsudo config PowerShellLoadProfile true`
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"write-heading-ids": "docusaurus write-heading-ids docs"
},
"dependencies": {
"@docusaurus/core": "^2.0.1",
"@docusaurus/plugin-google-gtag": "^2.0.1",
"@docusaurus/plugin-sitemap": "^2.0.1",
"@docusaurus/preset-classic": "^2.0.1",
"@docusaurus/core": "^2.1.0",
"@docusaurus/plugin-google-gtag": "^2.1.0",
"@docusaurus/plugin-sitemap": "^2.1.0",
"@docusaurus/preset-classic": "^2.1.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"prism-react-renderer": "^1.3.1",
Expand Down
2 changes: 1 addition & 1 deletion src/gsudo.Wrappers.Tests/Invoke-gsudo.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Describe "PS Invoke-Gsudo (v$($PSVersionTable.PSVersion.ToString()))" {
Describe "PS Invoke-Gsudo (PSv$($PSVersionTable.PSVersion.Major))" {
BeforeAll {
$env:Path = (Get-Item (Join-Path $PSScriptRoot "..\gsudo.Wrappers")).FullName + ";" + $env:Path
}
Expand Down
15 changes: 14 additions & 1 deletion src/gsudo.Wrappers.Tests/gsudo.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Describe "PS Gsudo (v$($PSVersionTable.PSVersion.ToString()))" {
Describe "PS Gsudo (PSv$($PSVersionTable.PSVersion.Major))" {
BeforeAll {
$env:Path = (Get-Item (Join-Path $PSScriptRoot "..\gsudo.Wrappers")).FullName + ";" + $env:Path
$Path = (Get-Item (Join-Path $PSScriptRoot "..\gsudo.Wrappers\gsudoModule.psm1")).FullName
Expand Down Expand Up @@ -26,4 +26,17 @@ Describe "PS Gsudo (v$($PSVersionTable.PSVersion.ToString()))" {

gsudo !! | Should -Be 'Hello World'
}

It "Elevates a ScriptBlock." {
$result = gsudo { (1+1) }
$result | Should -Be "2"
$result -is [System.Int32] | Should -Be $true
}

It "Elevates a ScriptBlock with arguments." {
$result = gsudo { "$($args[1]) $($args[0])" } -args "World", "Hello"
$result | Should -Be "Hello World"
$result -is [System.String] | Should -Be $true
}

}
18 changes: 14 additions & 4 deletions src/gsudo.Wrappers/Invoke-gsudo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ param
[Parameter()]
[switch]
$LoadProfile = $false,

[Parameter()]
[switch]
$NoProfile = $false,

#test mode
[Parameter()]
Expand Down Expand Up @@ -153,9 +157,14 @@ if($NoElevate) {
$windowTitle = $host.ui.RawUI.WindowTitle;

$dbg = if ($debug) {"--debug "} else {" "}
$NoProfile = if ($gsudoLoadProfile -or $LoadProfile) {""} else {"-NoProfile "}

$arguments = "-d $dbg--LogLevel Error $pwsh -nologo $NoProfile-NonInteractive -OutputFormat Xml -InputFormat Text -encodedCommand IAAoACQAaQBuAHAAdQB0ACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAKQAgAHwAIABpAGUAeAAgAA==".Split(" ")
if ($LoadProfile -or ((gsudo.exe --loglevel None config Powershellloadprofile).Split(" = ")[1] -like "*true*" -and -not $NoProfile)) {
$sNoProfile = ""
} else {
$sNoProfile = "-NoProfile "
}

$arguments = "-d --LogLevel Error $dbg$pwsh -nologo $sNoProfile-NonInteractive -OutputFormat Xml -InputFormat Text -encodedCommand IAAoACQAaQBuAHAAdQB0ACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAKQAgAHwAIABpAGUAeAAgAA==".Split(" ")

# Must Read: https://stackoverflow.com/questions/68136128/how-do-i-call-the-powershell-cli-robustly-with-respect-to-character-encoding-i?noredirect=1&lq=1
$result = $remoteCmd | & gsudo.exe $arguments *>&1
Expand All @@ -166,8 +175,9 @@ if($NoElevate) {
ForEach ($item in $result)
{
if (
$item.Exception.SerializedRemoteException.WasThrownFromThrowStatement -or
$item.Exception.WasThrownFromThrowStatement
$item.psobject.Properties['Exception'] -and
($item.Exception.SerializedRemoteException.WasThrownFromThrowStatement -or
$item.Exception.WasThrownFromThrowStatement)
)
{
throw $item
Expand Down
Loading