Skip to content

Commit

Permalink
Add pipes to write pretty PowerShell #53
Browse files Browse the repository at this point in the history
This commit introduces two pipes: `inlinePowerShell`,
`escapeDoubleQuotes`. The types when used together allows writing adding
clean and real PowerShell scripts as they are (without inlinining or
escaping them), removing the need to have hard-coded inlining/escaping.

It enables writing better PowerShell, makes it easier to maintain and
extend PowerShell scripts. Also allows writing more stable code with
less "unseen" bugs due to manual escaping/inlining. This commit
naturally reveals and fixes double quotes not being escaped in "Empty
trash bin" script.

This is solved by unifying the use of RunPowerShell function by all
scripts using PowerShell. The function inlines and escapes the scripts
as compile time to be send them to PowerShell.exe as an argument and
then invokes PowerShell.exe with generated ugly code.
  • Loading branch information
undergroundwires committed Sep 13, 2021
1 parent ddf417a commit 5217b0b
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 59 deletions.
6 changes: 6 additions & 0 deletions docs/templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,9 @@ A function can call other functions such as:
- Pipes are provided and defined by the compiler and consumed by collection files.
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
- ❗ Pipe names must be camelCase without any space or special characters.
- **Existing pipes**
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
- `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
- **Example usages**
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IPipe } from '../IPipe';

export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
return raw?.replaceAll('"', '\\"');
/*
In batch, it also works with 4 double quotes but looks bloated
An easy test: PowerShell -Command "Write-Host '\"Hello World\"'"
*/
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IPipe } from '../IPipe';

export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell';
public apply(raw: string): string {
return raw
?.split(/\r\n|\r|\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('; ');
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IPipe } from './IPipe';
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';

const RegisteredPipes = [ ];
const RegisteredPipes = [
new EscapeDoubleQuotes(),
new InlinePowerShell(),
];

export interface IPipeFactory {
get(pipeName: string): IPipe;
Expand Down
120 changes: 62 additions & 58 deletions src/application/collections/windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,10 @@ actions:
call:
function: RunPowerShell
parameters:
code:
$bin = (New-Object -ComObject Shell.Application).NameSpace(10);
code: |-
$bin = (New-Object -ComObject Shell.Application).NameSpace(10)
$bin.items() | ForEach {
Write-Host "Deleting $($_.Name) from Recycle Bin";
Write-Host "Deleting $($_.Name) from Recycle Bin"
Remove-Item $_.Path -Recurse -Force
}
-
Expand Down Expand Up @@ -2884,15 +2884,15 @@ actions:
call:
function: RunPowerShell
parameters:
code:
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
Get-ChildItem $key | foreach {
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose
code: |-
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'
Get-ChildItem $key | ForEach {
Set-ItemProperty -Path "$key\$($_.PSChildName)" -Name NetbiosOptions -Value 2 -Verbose
}
revertCode:
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
Get-ChildItem $key | foreach {
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose
revertCode: |-
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'
Get-ChildItem $key | ForEach {
Set-ItemProperty -Path "$key\$($_.PSChildName)" -Name NetbiosOptions -Value 0 -Verbose
}
-
category: Remove bloatware
Expand Down Expand Up @@ -3814,13 +3814,13 @@ actions:
call:
function: RunPowerShell
parameters:
code:
$installer = (Get-ChildItem \"$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe\");
code: |-
$installer = (Get-ChildItem "$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe")
if (!$installer) {
Write-Host Could not find the installer;
Write-Host 'Could not find the installer'
} else {
& $installer.FullName -Uninstall -System-Level -Verbose-Logging -Force-Uninstall
};
}
-
category: Disable built-in Windows features
children:
Expand Down Expand Up @@ -4416,13 +4416,13 @@ functions:
function: RunPowerShell
parameters:
code: Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage
revertCode:
$package = Get-AppxPackage -AllUsers '{{ $packageName }}';
revertCode: |-
$package = Get-AppxPackage -AllUsers '{{ $packageName }}'
if (!$package) {
Write-Error \"Cannot reinstall '{{ $packageName }}'\" -ErrorAction Stop
Write-Error "Cannot reinstall '{{ $packageName }}'" -ErrorAction Stop
}
$manifest = $package.InstallLocation + '\AppxManifest.xml';
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\"
$manifest = $package.InstallLocation + '\AppxManifest.xml'
Add-AppxPackage -DisableDevelopmentMode -Register "$manifest"
-
name: UninstallSystemApp
parameters:
Expand All @@ -4433,40 +4433,44 @@ functions:
call:
function: RunPowerShell
parameters:
code:
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
code: |-
$package = Get-AppxPackage -AllUsers '{{ $packageName }}'
if (!$package) {
Write-Host 'Not installed';
exit 0;
Write-Host 'Not installed'
exit 0
}
$directories = @($package.InstallLocation, \"$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)\");
$directories = @($package.InstallLocation, "$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)")
foreach($dir in $directories) {
if ( !$dir -Or !(Test-Path \"$dir\") ) { continue; }
cmd /c ('takeown /f \"' + $dir + '\" /r /d y 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
cmd /c ('icacls \"' + $dir + '\" /grant administrators:F /t 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
$files = Get-ChildItem -File -Path $dir -Recurse -Force;
if ( !$dir -Or !(Test-Path "$dir") ) { continue }
cmd /c ('takeown /f "' + $dir + '" /r /d y 1> nul')
if($LASTEXITCODE) { throw 'Failed to take ownership' }
cmd /c ('icacls "' + $dir + '" /grant administrators:F /t 1> nul')
if($LASTEXITCODE) { throw 'Failed to take ownership' }
$files = Get-ChildItem -File -Path $dir -Recurse -Force
foreach($file in $files) {
if($file.Name.EndsWith('.OLD')) { continue; }
$newName = $file.FullName + '.OLD';
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
if($file.Name.EndsWith('.OLD')) { continue }
$newName = $file.FullName + '.OLD'
Write-Host "Rename '$($file.FullName)' to '$newName'"
Move-Item -LiteralPath "$($file.FullName)" -Destination "$newName" -Force
}
}
revertCode:
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
revertCode: |-
$package = Get-AppxPackage -AllUsers '{{ $packageName }}'
if (!$package) {
Write-Error 'App could not be found' -ErrorAction Stop;
Write-Error 'App could not be found' -ErrorAction Stop
}
$directories = @($package.InstallLocation, \"$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)\");
$directories = @($package.InstallLocation, "$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)")
foreach($dir in $directories) {
if ( !$dir -Or !(Test-Path \"$dir\") ) { continue; }
cmd /c ('takeown /f \"' + $dir + '\" /r /d y 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
cmd /c ('icacls \"' + $dir + '\" /grant administrators:F /t 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
$files = Get-ChildItem -File -Path \"$dir\*.OLD\" -Recurse -Force;
if ( !$dir -Or !(Test-Path "$dir") ) { continue; }
cmd /c ('takeown /f "' + $dir + '" /r /d y 1> nul')
if($LASTEXITCODE) { throw 'Failed to take ownership' }
cmd /c ('icacls "' + $dir + '" /grant administrators:F /t 1> nul')
if($LASTEXITCODE) { throw 'Failed to take ownership' }
$files = Get-ChildItem -File -Path "$dir\*.OLD" -Recurse -Force
foreach($file in $files) {
$newName = $file.FullName.Substring(0, $file.FullName.Length - 4);
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
$newName = $file.FullName.Substring(0, $file.FullName.Length - 4)
Write-Host "Rename '$($file.FullName)' to '$newName'"
Move-Item -LiteralPath "$($file.FullName)" -Destination "$newName" -Force
}
}
-
Expand All @@ -4477,9 +4481,9 @@ functions:
function: RunPowerShell
parameters:
code: Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online
revertCode:
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*';
Add-WindowsCapability -Name \"$capability.Name\" -Online
revertCode: |-
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'
Add-WindowsCapability -Name "$capability.Name" -Online
-
name: RenameSystemFile
parameters:
Expand Down Expand Up @@ -4516,25 +4520,25 @@ functions:
Write-Host \"No updates. Settings file was not at $jsonfile\";
exit 0;
}
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
$json | ConvertTo-Json | Set-Content $jsonfile;
revertCode:
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force
$json | ConvertTo-Json | Set-Content $jsonfile
revertCode: |-
$jsonfile = "$env:APPDATA\Code\User\settings.json"
if (!(Test-Path $jsonfile -PathType Leaf)) {
Write-Error \"Settings file could not be found at $jsonfile\" -ErrorAction Stop;
Write-Error "Settings file could not be found at $jsonfile" -ErrorAction Stop
}
$json = Get-Content $jsonfile | ConvertFrom-Json;
$json.PSObject.Properties.Remove('{{ $setting }}');
$json | ConvertTo-Json | Set-Content $jsonfile;
$json = Get-Content $jsonfile | ConvertFrom-Json
$json.PSObject.Properties.Remove('{{ $setting }}')
$json | ConvertTo-Json | Set-Content $jsonfile
-
name: RunPowerShell
parameters:
- name: code
- name: revertCode
optional: true
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code | inlinePowerShell | escapeDoubleQuotes }}"
revertCode: |-
{{ with $revertCode }}
PowerShell -ExecutionPolicy Unrestricted -Command "{{ . }}"
PowerShell -ExecutionPolicy Unrestricted -Command "{{ . | inlinePowerShell | escapeDoubleQuotes }}"
{{ end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'mocha';
import { runPipeTests } from './PipeTestRunner';
import { EscapeDoubleQuotes } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/EscapeDoubleQuotes';

describe('EscapeDoubleQuotes', () => {
// arrange
const sut = new EscapeDoubleQuotes();
// act
runPipeTests(sut, [
{
name: 'using "',
input: 'hello "world"',
expectedOutput: 'hello \\"world\\"',
},
{
name: 'not using any double quotes',
input: 'hello world',
expectedOutput: 'hello world',
},
{
name: 'consecutive double quotes',
input: '""hello world""',
expectedOutput: '\\"\\"hello world\\"\\"',
},
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'mocha';
import { InlinePowerShell } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell';
import { runPipeTests } from './PipeTestRunner';

describe('InlinePowerShell', () => {
// arrange
const sut = new InlinePowerShell();
// act
runPipeTests(sut, [
{
name: 'no new line',
input: 'Write-Host \'Hello, World!\'',
expectedOutput: 'Write-Host \'Hello, World!\'',
},
{
name: '\\n new line',
input: '$things = Get-ChildItem C:\\Windows\\\nforeach ($thing in $things) {\nWrite-Host $thing.Name -ForegroundColor Magenta\n}',
expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }',
},
{
name: '\\n double empty lines are ignored',
input: '$things = Get-ChildItem C:\\Windows\\\n\nforeach ($thing in $things) {\n\nWrite-Host $thing.Name -ForegroundColor Magenta\n\n\n}',
expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }',
},
{
name: '\\r new line',
input: '$things = Get-ChildItem C:\\Windows\\\rforeach ($thing in $things) {\rWrite-Host $thing.Name -ForegroundColor Magenta\r}',
expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }',
},
{
name: '\\r and \\n newlines combined',
input: '$things = Get-ChildItem C:\\Windows\\\r\nforeach ($thing in $things) {\n\rWrite-Host $thing.Name -ForegroundColor Magenta\n\r}',
expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }',
},
{
name: 'trims whitespaces on lines',
input: ' $things = Get-ChildItem C:\\Windows\\ \nforeach ($thing in $things) {\n\tWrite-Host $thing.Name -ForegroundColor Magenta\r \n}',
expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }',
},
{
name: 'returns undefined when if input is undefined',
input: undefined,
expectedOutput: undefined,
},
]);
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'mocha';
import { expect } from 'chai';
import { IPipe } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipe';

interface ITestCase {
readonly name: string;
readonly input: string;
readonly expectedOutput: string;
}

export function runPipeTests(sut: IPipe, testCases: ITestCase[]) {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const actual = sut.apply(testCase.input);
// assert
expect(actual).to.equal(testCase.expectedOutput);
});
}
}

0 comments on commit 5217b0b

Please sign in to comment.