<# .Synopsis Simple folder based script execution environment. .DESCRIPTION Simple folder based script execution environment designed for modularity. Suitable for the automation tasks as a better alternative to a Behemoth-class monolithic scripts. Script execution order is a simple alphanumeric sort by the file name. .EXAMPLE ./start.ps1 # execute in the current directory .EXAMPLE ./start.ps1 -MainScriptRoot /mnt/tukayyid # expect everything are located at the specified path .EXAMPLE ./start.ps1 -PSModule /data/ironhold/psmodule # load the PS modules form the specified path #> [CmdletBinding()] param ( # Path where the scripts and data for the execution are located $Root = $PSScriptRoot, # Configuration storage path $Config, # Data storage path $Data, # .NET assemblies directory $Lib, # PowerShell modules path (would be added to PSModulesPath for the auto-magik) $PSModule, # Custom arguments [Parameter( ValueFromRemainingArguments=$true )] $CustomArgs ) # this is a global variable which would be accessbile anywhere in the session $global:BS = @{ Path = @{ PSScriptRoot = $PSScriptRoot Root = $null } Config = @{ } } # workaround for the interactive script launch (ie no defined $Root) if ($PSBoundParameters['Root']) { $global:BS.Path.Root = $PSBoundParameters['Root'] } else { $global:BS.Path.Root = $PSScriptRoot } # add any custom args to the configuration var if ($PSBoundParameters['CustomArgs']) { $global:BS.Config.Add('CustomArgs', $PSBoundParameters['CustomArgs']) } # auto configure the main paths # list of the variables @( 'config' 'data' 'lib' 'psmodule' ) | % { if ($value = $PSBoundParameters[$_]) { # if this path was supplied at the start then add it as is $global:BS.Path.Add($_, $value) } else { # otherwise assume it's under the main script root path # no attemps are made for the path validation, it's upon you $global:BS.Path.Add($_, (Join-Path $BS.Path.Root $_)) } } # Adding ./psmodule to $PSModulePath allows the module auto loading without explicitly # calling Import-Module with the absolute or relative paths # Note, if you prefer for the bundled modules not to be loaded before the ones available in the system, # swap '{0}{1}{2}' to '{2}{1}{0}' if (Test-Path $BS.Path.PSModule) { # if the path exists if ($Env:PSModulePath -match [regex]::Escape($BS.Path.PSModule)) { # nothing to do, the path was already in PSModulePath } else { $Env:PSModulePath = '{0}{1}{2}' -f $BS.Path.PSModule, [System.IO.Path]::PathSeparator, $Env:PSModulePath } } # load any configiguration to the global variable if (Test-Path $BS.Path.config) { foreach ($file in (gci $BS.Path.config -Filter *.json)) { try { $json = Get-Content $file.fullname | ConvertFrom-json $BS.config.add($file.basename, $json) } catch { write-warning ('Failed to load configuration data from the file "{0}"' -f $file.fullname) } } } # auto load .NET assemblies function Register-Assembly { <# .Synopsis Load .NET assembly .DESCRIPTION Load .NET assembly or assemblies .EXAMPLE Register-Assembly Load any .dll in the current path .EXAMPLE Register-Assembly -Path ./lib/nestandard99/kewllib.dll Load a dll from the explicit path #> [CmdletBinding()] [Alias('Register-Assemblies')] Param ( # Path to *.dll [Alias('AssemblyPath')] $Path = $PWD ) function loadassembly { param ($pathToDll) try { [System.Reflection.Assembly]::LoadFile($pathToDll) Write-Verbose ("Loaded assembly: {0}" -f $pathToDll) } catch { Write-Error $_ } } try { $thisPath = Get-Item $Path -ErrorAction Stop if ($thisPath.PSIsContainer) { Get-ChildItem -Filter '*.dll' | % { loadassembly $_.FullName } } # this whould be triggered both for when PSIsContainer is false (ie this is a file) and when this property doesn't exists else { loadassembly $thisPath.FullName } } Catch { Write-Error $_ } } # end function Register-Assembly # $PSVersionTable.PSVersion # Windows PowerShell 5.x if ($PSVersionTable.PSVersion.Major -eq 5) { $subFolder = 'net4' } # PowerShell 6 elseif ($PSVersionTable.PSVersion.Major -eq 6) { $subFolder = 'netstandard2' } # PowerShell 7.0 elseif ($PSVersionTable.PSVersion -lt [version]'7.2.0') { $subFolder = 'netstandard2' } # PowerShell 7.2 elseif ($PSVersionTable.PSVersion -lt [version]'7.3.0') { $subFolder = 'net6' } # PowerShell 7.3+ else { $subFolder = 'net7' } $pathToLoadFrom = Join-Path $global:BS.Path.lib $subFolder if (Test-Path $pathToLoadFrom) { Write-Verbose ("Loading .NET assemblies from the path: {0}" -f $pathToLoadFrom) Register-Assembly -AssemblyPath $pathToLoadFrom } Remove-Variable subFolder, pathToLoadFrom -ErrorAction SilentlyContinue # end load .NET assemblies # use only dirs named like 01-Something etc $dirsToProcess = gci -Path $BS.Path.Root -Directory | ? Name -Match '\d+-\w+' | Sort-Object Name foreach ($thisDir in $dirsToProcess) { $scriptFiles = gci $thisDir.FullName -Filter '*.ps1' | Sort-Object Name foreach ($thisFile in $scriptFiles) { 'Processing {0}\{1}' -f $thisDir.Name, $thisFile.Name | Write-Verbose . $thisFile.FullName } }