Skip to content
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## Script-context helper restoration (unreleased)

- Restore Get-ScriptDirectory and Get-ScriptName; export twenty-nine commands.
- Resolve the immediately calling script rather than module-scoped invocation data.
- Support explicit literal filesystem paths and pipeline input without requiring existence.
- Return no output for interactive calls without a script path.
- Remove dependence on the externally supplied hostinvocation variable.
- Add real script, nested/dot-sourced caller and isolated interactive regression tests.

## AD helper restoration (unreleased)

- Restore Test-IsValidDn, Test-IsValidUpn and Get-ReportChain; export twenty-seven commands.
Expand Down
2 changes: 2 additions & 0 deletions IT-ToolBox.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
'Get-TimerStatus'
'Stop-Timer'
'Get-ElapsedTime'
'Get-ScriptDirectory'
'Get-ScriptName'
'Test-FileName'
'Test-IsValidPath'
'Test-IsIP'
Expand Down
2 changes: 2 additions & 0 deletions IT-ToolBox.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Export-ModuleMember -Function @(
'Get-TimerStatus'
'Stop-Timer'
'Get-ElapsedTime'
'Get-ScriptDirectory'
'Get-ScriptName'
'Test-FileName'
'Test-IsValidPath'
'Test-IsIP'
Expand Down
20 changes: 0 additions & 20 deletions Legacy/Get-ScriptDirectory.ps1

This file was deleted.

20 changes: 0 additions & 20 deletions Legacy/Get-ScriptName.ps1

This file was deleted.

8 changes: 5 additions & 3 deletions Legacy/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Historical commands

These six commands are preserved for reference and are not loaded or exported by v3.
These four commands are preserved for reference and are not loaded or exported by v3.
The string encryption design is unsuitable for new security implementations.
Exchange helpers require a separate compatibility review. Script-context helpers
require clarification of caller semantics before reuse.
Exchange helpers require a separate compatibility review.

Get-ScriptDirectory and Get-ScriptName now have supported implementations in Public/.
See the root README for caller resolution and interactive behavior.
26 changes: 26 additions & 0 deletions Private/Resolve-ITToolBoxScriptPath.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
function Resolve-ITToolBoxScriptPath {
[CmdletBinding()]
[OutputType([string])]
param (
[AllowEmptyString()]
[string]$ScriptPath,
[AllowEmptyString()]
[string]$CallerScriptPath
)

$path = if ($ScriptPath) { $ScriptPath } else { $CallerScriptPath }
if ([string]::IsNullOrEmpty($path)) { return }

$provider = $null
$drive = $null
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
$path, [ref]$provider, [ref]$drive
)
if ($provider.Name -ne 'FileSystem') {
throw 'ScriptPath must be a filesystem path.'
}
if ([string]::IsNullOrEmpty([System.IO.Path]::GetFileName($resolved))) {
throw 'ScriptPath must include a filename.'
}
$resolved
}
29 changes: 29 additions & 0 deletions Public/Get-ScriptDirectory.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function Get-ScriptDirectory {
<#
.SYNOPSIS
Return the directory of the calling script or an explicit script path.
.DESCRIPTION
Uses the immediately calling script, including dot-sourced scripts and functions
defined in a script. Interactive calls without ScriptPath produce no output.
Explicit paths are literal filesystem paths, resolved against the current location;
the target need not exist. No hostinvocation variable is required.
.PARAMETER ScriptPath
Optional explicit script filename, with an absolute or relative filesystem path.
.EXAMPLE
Get-ScriptDirectory
.EXAMPLE
Get-ScriptDirectory -ScriptPath './scripts/automation.ps1'
#>
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string]$ScriptPath
)

process {
$path = Resolve-ITToolBoxScriptPath -ScriptPath $ScriptPath -CallerScriptPath $MyInvocation.ScriptName
if ($path) { [System.IO.Path]::GetDirectoryName($path) }
}
}
29 changes: 29 additions & 0 deletions Public/Get-ScriptName.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function Get-ScriptName {
<#
.SYNOPSIS
Return the filename (including its extension) of the calling script or an explicit script path.
.DESCRIPTION
Uses the immediately calling script, including dot-sourced scripts and functions
defined in a script. Interactive calls without ScriptPath produce no output.
Explicit paths are literal filesystem paths, resolved against the current location;
the target need not exist. No hostinvocation variable is required.
.PARAMETER ScriptPath
Optional explicit script filename, with an absolute or relative filesystem path.
.EXAMPLE
Get-ScriptName
.EXAMPLE
Get-ScriptName -ScriptPath './scripts/automation.ps1'
#>
[CmdletBinding()]
[OutputType([string])]
param (
[Parameter(ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string]$ScriptPath
)

process {
$path = Resolve-ITToolBoxScriptPath -ScriptPath $ScriptPath -CallerScriptPath $MyInvocation.ScriptName
if ($path) { [System.IO.Path]::GetFileName($path) }
}
}
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The foundation imports without WinSCP, GnuPG, Active Directory or Exchange depen
| Get-TimerStatus | Check whether a Stopwatch is running |
| Stop-Timer | Stop a Stopwatch |
| Get-ElapsedTime | Retrieve elapsed time or individual components |
| Get-ScriptDirectory | Directory of the calling script or an explicit script path |
| Get-ScriptName | Filename of the calling script or an explicit script path |
| Test-FileName | Native filename validation; optional Windows-compatible rules |
| Test-IsValidPath | Native filesystem path syntax; no existence check |
| Test-IsIP | Standard IPv4/IPv6 literals; strict dotted IPv4 |
Expand Down Expand Up @@ -66,10 +68,10 @@ Redaction is opt-in and does not guarantee detection of every secret.

- SCP and GnuPG wrappers and bundled WinSCP binaries are removed. Separate modules
will own file transfer and OpenPGP; no replacement is bundled here.
- `Legacy/` retains string encryption, Exchange and script-context helpers for reference.
- `Legacy/` retains string encryption and Exchange helpers for reference.
- All commands formerly retained in `Staging/v3/` now have supported implementations.
Its README records the migration; separate legacy/staged integrations remain excluded. They are not currently exported.
- Only the twenty-seven listed commands are exported. Private helpers, variables and aliases
- Only the twenty-nine listed commands are exported. Private helpers, variables and aliases
are not exported. Existing calls to other v2 commands require the v2 release until
those commands return to the supported API.
- The module GUID and Git history are preserved.
Expand Down Expand Up @@ -373,3 +375,27 @@ No ActiveDirectory dependency is required to import IT-ToolBox or use the naming
validators. Get-ReportChain requires an available Get-ADUser command and access to
an AD endpoint when invoked; it uses the command's ambient authentication. Tests
mock AD queries and verify filter construction; no live domain query is tested.

## Script context

Get-ScriptDirectory and Get-ScriptName resolve the immediately calling script rather
than the module implementation file. Calls inside a function defined in a script
use that defining script; a nested or dot-sourced script uses its own path. Calls
made interactively without an explicit path return no output. They do not read the
historical, externally supplied hostinvocation variable.

```powershell
# Inside automation.ps1:
$scriptDirectory = Get-ScriptDirectory
$scriptName = Get-ScriptName
# Explicit paths also work interactively, including paths that do not yet exist:
Get-ScriptDirectory -ScriptPath './scripts/automation.ps1'
'./scripts/automation.ps1', './scripts/other.ps1' | Get-ScriptName
```

ScriptPath accepts literal absolute/relative filesystem filenames and pipeline
input. Relative paths use the current PowerShell location, not the caller's directory.
Wildcard characters are treated literally. Non-filesystem provider paths and paths
without a filename throw; no existence, extension or file-type check is performed.
Unlike the legacy implementations, these helpers do not depend on script-scoped
MyInvocation or silently return a module filename.
2 changes: 1 addition & 1 deletion Tests/Module.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Describe 'IT-ToolBox module boundary' {
}

It 'exports exactly the supported commands' {
$expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'Test-IsEmail', 'Test-IsUrl', 'Convert-LogonTimestamp', 'Get-OsUpTime', 'Remove-SpecialCharacters', 'Test-RegistryValue', 'Test-IsValidDn', 'Test-IsValidUpn', 'Get-ReportChain', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object
$expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Get-ScriptDirectory', 'Get-ScriptName', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'Test-IsEmail', 'Test-IsUrl', 'Convert-LogonTimestamp', 'Get-OsUpTime', 'Remove-SpecialCharacters', 'Test-RegistryValue', 'Test-IsValidDn', 'Test-IsValidUpn', 'Get-ReportChain', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object
$actual = @($module.ExportedFunctions.Keys | Sort-Object)
($actual -join ',') | Should -Be ($expected -join ',')
$module.ExportedVariables.Count | Should -Be 0
Expand Down
120 changes: 120 additions & 0 deletions Tests/ScriptContext.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
BeforeAll {
$manifestPath = (Resolve-Path (Join-Path $PSScriptRoot '../IT-ToolBox.psd1')).Path
Import-Module $manifestPath -Force -ErrorAction Stop
$fixtureDirectory = Join-Path $TestDrive 'scripts with spaces'
$null = New-Item -ItemType Directory -Path $fixtureDirectory
$callerPath = Join-Path $fixtureDirectory 'caller.ps1'
$nestedPath = Join-Path $fixtureDirectory 'nested.ps1'
$functionPath = Join-Path $fixtureDirectory 'definition.ps1'
Set-Content -LiteralPath $callerPath -Value @'
[pscustomobject]@{ Directory = Get-ScriptDirectory; Name = Get-ScriptName }
'@
Set-Content -LiteralPath $nestedPath -Value @'
[pscustomobject]@{ Directory = Get-ScriptDirectory; Name = Get-ScriptName }
'@
Set-Content -LiteralPath $functionPath -Value @'
function Get-DefinedScriptContext {
[pscustomobject]@{ Directory = Get-ScriptDirectory; Name = Get-ScriptName }
}
'@
}

Describe 'Script-context helpers' {
It 'resolves the script calling the imported module' {
$result = & $callerPath
$result.Directory | Should -Be $fixtureDirectory
$result.Name | Should -Be 'caller.ps1'
}

It 'resolves a dot-sourced script' {
$result = . $callerPath
$result.Directory | Should -Be $fixtureDirectory
$result.Name | Should -Be 'caller.ps1'
}

It 'resolves the innermost nested script' {
$outerPath = Join-Path $TestDrive 'outer.ps1'
Set-Content -LiteralPath $outerPath -Value '& (Join-Path $PSScriptRoot ''scripts with spaces/nested.ps1'')'
$result = & $outerPath
$result.Directory | Should -Be $fixtureDirectory
$result.Name | Should -Be 'nested.ps1'
}

It 'resolves the script defining a calling function' {
. $functionPath
$result = Get-DefinedScriptContext
$result.Directory | Should -Be $fixtureDirectory
$result.Name | Should -Be 'definition.ps1'
}

It 'lets an explicit path override caller context' {
$explicitPath = Join-Path $TestDrive 'not-created.ps1'
Get-ScriptDirectory -ScriptPath $explicitPath | Should -Be $TestDrive
Get-ScriptName -ScriptPath $explicitPath | Should -Be 'not-created.ps1'
Test-Path -LiteralPath $explicitPath | Should -BeFalse
}

It 'resolves relative paths against the current location' {
Push-Location $TestDrive
try {
Get-ScriptDirectory -ScriptPath './absent/example.ps1' | Should -Be (Join-Path $TestDrive 'absent')
Get-ScriptName -ScriptPath './absent/example.ps1' | Should -Be 'example.ps1'
}
finally { Pop-Location }
}

It 'treats wildcard characters literally' {
$path = Join-Path $TestDrive 'absent[12].ps1'
Get-ScriptName -ScriptPath $path | Should -Be 'absent[12].ps1'
Get-ScriptDirectory -ScriptPath $path | Should -Be $TestDrive
}

It 'returns one string per explicit pipeline path' {
$paths = @((Join-Path $TestDrive 'one.ps1'), (Join-Path $fixtureDirectory 'two.ps1'))
$names = @($paths | Get-ScriptName)
$directories = @($paths | Get-ScriptDirectory)
$names.Count | Should -Be 2
($names -join ',') | Should -Be 'one.ps1,two.ps1'
$directories.Count | Should -Be 2
$directories[0] | Should -Be $TestDrive
$directories[1] | Should -Be $fixtureDirectory
}

It 'rejects explicit empty or null paths for <Command>' -ForEach @(
@{ Command = 'Get-ScriptDirectory' }; @{ Command = 'Get-ScriptName' }
) {
{ & $Command -ScriptPath '' } | Should -Throw
{ & $Command -ScriptPath $null } | Should -Throw
}

It 'rejects a non-filesystem provider for <Command>' -ForEach @(
@{ Command = 'Get-ScriptDirectory' }; @{ Command = 'Get-ScriptName' }
) {
{ & $Command -ScriptPath 'Env:PATH' } | Should -Throw '*filesystem*'
}

It 'rejects a trailing directory separator for <Command>' -ForEach @(
@{ Command = 'Get-ScriptDirectory' }; @{ Command = 'Get-ScriptName' }
) {
{ & $Command -ScriptPath ($TestDrive + [IO.Path]::DirectorySeparatorChar) } | Should -Throw '*filename*'
}

It 'returns no interactive output and ignores hostinvocation in an isolated process' {
$escapedManifest = $manifestPath.Replace("'", "''")
$code = @"
Import-Module '$escapedManifest' -ErrorAction Stop
`$global:hostinvocation = @{ MyCommand = @{ Path = '/incorrect/host.ps1'; Name = 'host.ps1' } }
[pscustomobject]@{
DirectoryCount = @(Get-ScriptDirectory).Count
NameCount = @(Get-ScriptName).Count
} | ConvertTo-Json -Compress
"@
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($code))
$executable = Join-Path $PSHOME $(if ($IsWindows) { 'pwsh.exe' } else { 'pwsh' })
$output = & $executable -NoProfile -NonInteractive -EncodedCommand $encoded
$LASTEXITCODE | Should -Be 0
$result = $output | ConvertFrom-Json
$result.DirectoryCount | Should -Be 0
$result.NameCount | Should -Be 0
}
}
Loading