PSToolLauncher.psm1

#Requires -Version 5.1

using namespace System.IO;
using namespace System.Management.Automation;

# +-----------------------------------------------------------------------------------------------------------
# | Public functions
# |
<#
#>

function Start-Tool
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolKey,
        
        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    begin
    {
        Write-Verbose 'BEGIN Start-Tool';
    }
    process
    {
        $toolInfo = Get-ToolInstallation $ToolKey; 
        if($toolInfo.IsInstalled -ne $true) {
            throw [InvalidOperationException] "'$($toolInfo.Definition.$KeyToolName)' is not installed.";
        }
        
        Write-Verbose "Executing '$($toolInfo.Definition.$KeyToolName) version '$($toolInfo.DefaultVersion.Version)'.";
        & $toolInfo.DefaultVersion.Command $Arguments;

        Write-Verbose 'SUCCESS Start-Tool';
    }
    end
    {
        Write-Verbose 'FINISH Start-Tool';
    }
}

function Start-NotepadPlusPlus
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    process
    {
        Write-Verbose 'EXECUTING Start-NotepadPlusPlus.';
        Start-Tool -ToolKey $KeyNotepadPlusPlus -Arguments $Arguments;
    }
}

function Start-SqlAdmin
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    process
    {
        Write-Verbose 'EXECUTING Start-SqlAdmin.';
        Start-Tool -ToolKey $KeySqlAdmin -Arguments $Arguments;
    }
}

# +-----------------------------------------------------------------------------------------------------------
# | Private functions
# |
function Find-ToolFile
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([FileInfo])]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [DirectoryInfo[]] $ProductDirectories,

        [Parameter(Position = 1, Mandatory)]
        [string[]] $ExecutableNames,

        [Parameter()]
        [switch] $FindOnPath
    )
    
    begin
    {
        Write-Verbose 'BEGIN Find-ToolFile.';
        
        $exeNameList = $ExecutableNames -join ', ';
    }
    process
    {
        $allSearchDirectories = @();
        $allSearchDirectories += $ProductDirectories;
        
        if($FindOnPath) {
            $allSearchDirectories += $Env:Path -split ';' `
                                   | Select-Object -Unique `
                                   | Get-Item -ErrorAction SilentlyContinue;
        }

        $allSearchDirectories `
        | Select-Object -Unique `
        | ForEach-Object {
            Write-Verbose "Searching product directory '$_' for executables '$exeNameList'.";
            
            # return
            Get-ChildItem -Path $_ -File `
            | Where-Object {
                $ExecutableNames -contains $_.Name;
            }
        }
        
        Write-Verbose 'SUCCESS Find-ToolFile.';
    }
    end
    {
        Write-Verbose 'FINISH Find-ToolFile.';
    }
}

function Get-ProgramFilesDirectory
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([DirectoryInfo])]
    param()

    begin
    {
        Write-Verbose 'BEGIN Get-ProgramFilesDirectory.';
    }
    process
    {
        $toolDirs = Get-ChildItem -Path Env:ProgramFiles* `
                  | Select-Object -ExpandProperty Value;

        # return
        $toolDirs | Select-Object -Unique `
                  | Get-Item -ErrorAction SilentlyContinue;

        Write-Verbose 'SUCCESS Get-ProgramFilesDirectory.';
    }   
    end
    {
        Write-Verbose 'FINISH Get-ProgramFilesDirectory.';
    }
}

function Get-ToolInstallation
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Installation')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolKey,
        
        [Parameter()]
        [switch] $Force
    )

    begin
    {
        Write-Verbose 'BEGIN Get-ToolInstallation.';
    }
    process
    {
        Write-Verbose "Retrieving tool installation information for '$ToolKey'.";

        if($Force -or $ToolInstallations.ContainsKey($ToolKey) -eq $false) {
            Write-Verbose "Acquiring installation information for '$ToolKey'.";
            
            $toolDef = $ToolDefinitions[$ToolKey];
            if($null -eq $toolDef) {
                throw [InvalidOperation] "Tool '$ToolKey' is not a known common tool, available common tools are: (list of common tools).";
            }
            
            $productDirectories = & $toolDef.$KeyGetProductDirectory;
            
            $findOnPath = $toolDef.$KeyIgnorePath -ne $true;
            $toolVersions = Find-ToolFile $productDirectories $toolDef.$KeyExecutableNames -FindOnPath:$findOnPath `
                          | Get-ToolVersion;

            $ToolInstallations[$ToolKey] = New-ToolInstallation -ToolDefinition $toolDef -ToolVersions $toolVersions;
        }
        
        # return
        $ToolInstallations[$ToolKey];

        Write-Verbose 'SUCCESS Get-ToolInstallation.';
    }
    end
    {
        Write-Verbose 'FINISH Get-ToolInstallation.';
    }
}

function Get-ToolVersion
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.ToolVersion')]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [FileInfo[]] $ToolFiles
    )
    
    begin
    {
        Write-Verbose 'BEGIN Get-ToolVersion.';
    }
    process
    {
        $sortableVersionFormat = '{0}{1:D6}.{2:D6}.{3:D6}.{4:D6}.{5:D6}';
        
        $sequenceNumber = $ToolFiles.Length;
        $ToolFiles | ForEach-Object {
            Write-Verbose "Get tool version information for executable '$_'.";
            
            $versionInfo = $_.VersionInfo;
            if($null -eq $versionInfo) {
                $exeDescription         = $_.BaseName;
                $printableVersionNumber = 'Unknown';
                $sortableProductVersion = $sortableVersionFormat -f 'X', 0, 0, 0, 0, $sequenceNumber
                $sortableFileVersion    = $sortableVersionFormat -f 'X', 0, 0, 0, 0, $sequenceNumber
            }
            else {
                $exeDescription         = $versionInfo.FileDescription;
                $printableVersionNumber = '{0:D}.{1:D}.{2:D}.{3:D}' -f $versionInfo.ProductMajorPart, `
                                                                       $versionInfo.ProductMinorPart, `
                                                                       $versionInfo.ProductBuildPart, `
                                                                       $versionInfo.ProductPrivatePart;
                $sortableProductVersion = $sortableVersionFormat -f 'V', $versionInfo.ProductMajorPart, `
                                                                         $versionInfo.ProductMinorPart, `
                                                                         $versionInfo.ProductBuildPart, `
                                                                         $versionInfo.ProductPrivatePart, `
                                                                         $sequenceNumber;
                $sortableFileVersion    = $sortableVersionFormat -f 'V', $versionInfo.FileMajorPart, `
                                                                         $versionInfo.FileMinorPart, `
                                                                         $versionInfo.FileBuildPart, `
                                                                         $versionInfo.FilePrivatePart, `
                                                                         $sequenceNumber;
            }
            
            Write-Verbose "Tool '$exeDescription' is version '$printableVersionNumber'.";

            # return
            New-Object PSObject -Property @{
                Command               = (Get-Command $_);
                Version               = $printableVersionNumber;
                OrderedProductVersion = $sortableProductVersion;
                OrderedFileVersion    = $sortableFileVersion;
            } `
            | ForEach-Object {
                $_.PSTypeNames.Insert(0, 'PSToolLauncher.ToolVersion');
                $_;
            };
            
            $sequenceNumber--;
        }

        Write-Verbose 'SUCCESS Get-ToolVersion.';
    }
    end
    {
        Write-Verbose 'FINISH Get-ToolVersion.';
    }
}

function New-ToolDefinition
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Definition')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolName,

        [Parameter(Mandatory)]
        [string] $NounPrefix,

        [Parameter(Mandatory)]
        [string[]] $ExecutableNames,

        [Parameter()]
        [CommandInfo] $GetProductDirectory,

        [Parameter()]
        [switch] $IgnorePath,

        [Parameter()]
        [string[]] $Aliases
    )

    begin
    {
        Write-Verbose 'BEGIN New-ToolDefinition.';
    }
    process
    {
        Write-Verbose "Creating ToolDefinition for '$ToolName'.";

        if($null -eq $GetProductDirectory) {
            $GetProductDirectory = Get-Command -Name "Get-$($NounPrefix)ProductDirectory";
        }

        $toolDef = New-Object PSObject -Property @{
            $KeyToolName            = $ToolName;
            $KeyNounPrefix          = $NounPrefix;
            $KeyGetProductDirectory = $GetProductDirectory;
            $KeyExecutableNames     = $ExecutableNames;
            $KeyIgnorePath          = $IgnorePath -eq $true;
            $KeyAliases             = $Aliases;
        };

        $toolDef.PSTypeNames.Insert(0, 'PSToolLauncher.Definition');

        # return
        $toolDef;

        Write-Verbose 'SUCCESS New-ToolDefinition.';
    }
    end
    {
        Write-Verbose 'FINISH New-ToolDefinition.';
    }
}

function New-ToolInstallation
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Installation')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [PSTypeName('PSToolLauncher.Definition')]
        [object] $ToolDefinition,
        
        [Parameter(Mandatory)]
        [PSTypeName('PSToolLauncher.ToolVersion')]
        [object[]] $ToolVersions
    )

    begin
    {
        Write-Verbose 'BEGIN New-ToolInstallation.';
    }
    process
    {
        Write-Verbose "Creating tool installation information for '$($ToolDefinition.$KeyToolName)'.";

        $isInstalled = $ToolVersions.Count -gt 0;
        $defaultVersion = $ToolVersions `
                        | Sort-Object -Property OrderedFileVersion, OrderedPropertVersion `
                        | Select-Object -First 1;
        
        # return
        New-Object PSObject -Property @{
            Definition     = $ToolDefinition;
            Versions       = $ToolVersions;
            IsInstalled    = $isInstalled;
            DefaultVersion = $defaultVersion;
        } `
        | ForEach-Object {
            $_.PSTypeNames.Insert(0, 'PSToolLauncher.Installation');
            $_;
        };

        Write-Verbose 'SUCCESS New-ToolInstallation.';
    }
    end
    {
        Write-Verbose 'FINISH New-ToolInstallation.';
    }
}

function Get-NotepadPlusPlusProductDirectory
{
    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([DirectoryInfo])]
    param()

    begin
    {
        Write-Verbose 'BEGIN Get-NotepadPlusPlusProductDirectory.';
    }
    process
    {
        Get-ProgramFilesDirectory `
        | ForEach-Object {
            $productDir = Join-Path $_ 'Notepad++';
            
            # return
            Get-Item -Path $productDir -ErrorAction SilentlyContinue;
        }

        Write-Verbose 'SUCCESS Get-NotepadPlusPlusProductDirectory.';
    }
    end
    {
        Write-Verbose 'FINISH Get-NotepadPlusPlusProductDirectory.';
    }
}

function Get-SqlAdminProductDirectory
{
    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([DirectoryInfo])]
    param()

    begin
    {
        Write-Verbose 'BEGIN Get-SqlAdminProductDirectory.';
    }
    process
    {
        Get-ProgramFilesDirectory `
        | ForEach-Object {
            $programFilesDir = $_;
            
            # return
            Get-ChildItem -Path $programFilesDir -Filter 'Microsoft SQL Server Management Studio*' `
            | Get-ChildItem -Directory -Recurse;
            
            # return
            Get-ChildItem -Path $programFilesDir -Filter 'Microsoft SQL Server' `
            | Get-ChildItem -Directory `
            | Where-Object { $_.Name -match '^[1-9][0-9]*$' } `
            | Get-ChildItem -Directory -Recurse;
        }

        Write-Verbose 'SUCCESS Get-SqlAdminProductDirectory.';
    }
    end
    {
        Write-Verbose 'FINISH Get-SqlAdminProductDirectory.';
    }
}

# ##############################################
#
# This section contains the function and alias
# that are actually exported from this module
#
New-Variable -Name KeyToolName -Option Constant `
             -Value 'ToolName';
New-Variable -Name KeyNounPrefix -Option Constant `
             -Value 'NounPrefix';
New-Variable -Name KeyGetProductDirectory -Option Constant `
             -Value 'Get-ProductDirectory';
New-Variable -Name KeyExecutableNames -Option Constant `
             -Value 'ExeNames';
New-Variable -Name KeyIgnorePath -Option Constant `
             -Value 'IgnorePath';
New-Variable -Name KeyAliases -Option Constant `
             -Value 'Aliases';

New-Variable -Name KeyNotepadPlusPlus -Option Constant `
             -Value 'notepad-plusplus';
New-Variable -Name ToolDefNotepadPlusPlus -Option Constant `
             -Value (New-ToolDefinition -ToolName 'Notepad++' `
                                        -NounPrefix 'NotepadPlusPlus' `
                                        -ExecutableNames 'notepad++.exe' `
                                        -Aliases '_notepad++');

New-Variable -Name KeySqlAdmin -Option Constant `
             -Value 'sqladmin-ide';
New-Variable -Name ToolDefSqlAdmin -Option Constant `
             -Value (New-ToolDefinition -ToolName 'SQL Server Management Studio' `
                                        -NounPrefix 'SqlAdmin' `
                                        -ExecutableNames 'ssms.exe', 'sqlwb.exe' `
                                        -Aliases '_sqladmin');

New-Variable -Name ToolDefinitions -Option ReadOnly `
             -Value @{
                 $KeyNotepadPlusPlus = $ToolDefNotepadPlusPlus;
                 $KeySqlAdmin        = $ToolDefSqlAdmin;
             };
New-Variable -Name ToolInstallations `
             -Value @{};

$PublicAliases = @();

$ToolDefinitions.Keys `
| ForEach-Object {
    $toolDef = $ToolDefinitions[$_];

    foreach($alias in $toolDef.$KeyAliases) {
        Set-Alias -Name $alias -Value "Start-$($toolDef.$KeyNounPrefix)" -Force;
        $PublicAliases += $alias;
    }
};

$PublicFunctions = @(
    'Start-NotepadPlusPlus',
    'Start-SqlAdmin',
    'Start-Tool'
);

if($global:DebugMode) {
    Write-Host 'Adding all cmdlets for debug purposes.';

    $PublicFunctions += @(
        'Get-NotepadPlusPlusProductDirectory',
        'Get-ProgramFilesDirectory',
        'Get-SqlAdminProductDirectory',
        'Get-ToolInstallation',
        'Get-ToolVersion',
        'Find-ToolFile',
        'New-ToolDefinition',
        'New-ToolInstallation'
    );
}

Export-ModuleMember -Function $PublicFunctions `
                    -Alias $PublicAliases;