DSCResources/cShortcut/cShortcut.psm1

Enum Ensure{
    Absent
    Present
}

Enum WindowStyle {
    undefined = 0
    normal = 1
    maximized = 3
    minimized = 7
}

function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [parameter()]
        [ValidateSet("Present", "Absent")]
        [String]
        $Ensure = [Ensure]::Present,

        [parameter(Mandatory)]
        [String]
        $Path,

        [parameter(Mandatory)]
        [String]
        $Target,

        [parameter()]
        [String]
        $WorkingDirectory,

        [parameter()]
        [String]
        $Arguments,

        [parameter()]
        [String]
        $Description,

        [parameter()]
        [String]
        $Icon,

        [parameter()]
        [String]
        $HotKey,

        [ValidateSet("normal", "maximized", "minimized")]
        [String]
        $WindowStyle = [WindowStyle]::normal
    )

    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $Path = $Path + '.lnk'
    }

    $Ensure = [Ensure]::Present

    # check file exists
    if (-not (Test-Path $Path)) {
        Write-Verbose 'File not found.'
        $Ensure = [Ensure]::Absent
    }
    else {
        $shortcut = Get-Shortcut $Path -ErrorAction Continue
    }
    $returnValue = @{
        Ensure           = $Ensure
        Path             = $Path
        Target           = $shortcut.TargetPath
        WorkingDirectory = $shortcut.WorkingDirectory
        Arguments        = $shortcut.Arguments
        Description      = $shortcut.Description
        Icon             = $shortcut.IconLocation
        HotKey           = $shortcut.Hotkey
        WindowStyle      = [WindowStyle]::undefined
    }

    if ($shortcut.WindowStyle -as [WindowStyle]) {
        $returnValue.WindowStyle = [WindowStyle]$shortcut.WindowStyle
    }

    $returnValue
} # end of Get-TargetResource

function Set-TargetResource {
    [CmdletBinding()]
    param
    (
        [parameter()]
        [ValidateSet("Present", "Absent")]
        [String]
        $Ensure = [Ensure]::Present,

        [parameter(Mandatory)]
        [String]
        $Path,

        [parameter(Mandatory)]
        [String]
        $Target,

        [parameter()]
        [String]
        $WorkingDirectory,

        [parameter()]
        [String]
        $Arguments,

        [parameter()]
        [String]
        $Description,

        [parameter()]
        [String]
        $Icon,

        [parameter()]
        [String]
        $HotKey,

        [ValidateSet("normal", "maximized", "minimized")]
        [String]
        $WindowStyle = [WindowStyle]::normal
    )

    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $Path = $Path + '.lnk'
    }

    # Ensure = "Absent"
    if ($Ensure -eq [Ensure]::Absent) {
        Write-Verbose ('Remove shortcut file "{0}"' -f $Path)
        Remove-Item $Path -Force
    }
    else {
        # Ensure = "Present"
        $arg = $PSBoundParameters
        $arg.Remove('Ensure')
        New-Shortcut @arg
    }

} # end of Set-TargetResource

function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([bool])]
    param
    (
        [parameter()]
        [ValidateSet("Present", "Absent")]
        [String]
        $Ensure = [Ensure]::Present,

        [parameter(Mandatory)]
        [String]
        $Path,

        [parameter(Mandatory)]
        [String]
        $Target,

        [parameter()]
        [String]
        $WorkingDirectory,

        [parameter()]
        [String]
        $Arguments,

        [parameter()]
        [String]
        $Description,

        [parameter()]
        [String]
        $Icon = ',0',

        [parameter()]
        [String]
        $HotKey,

        [ValidateSet("normal", "maximized", "minimized")]
        [String]
        $WindowStyle = [WindowStyle]::normal
    )

    <# 想定される状態パターンと返却するべき値
        1. ショートカットがあるべき(Present)
            1-A. ショートカットなし => 更新の必要あり($false)
            1-B. ショートカットはあるがプロパティが正しくない => 更新の必要あり($false)
            1-C. ショートカットあり、プロパティ一致 => 何もする必要なし($true)
        2. ショートカットはあるべきではない(Absent)
            2-A. ショートカットなし => 何もする必要なし($true)
            2-B. ショートカットあり => 削除の必要あり($false)
    #>


    # 拡張子つける
    if (-not $Path.EndsWith('.lnk')) {
        Write-Verbose ("File extension is not 'lnk'. Automatically add extension")
        $Path = $Path + '.lnk'
    }

    # HotKey文字列組み立て
    if ($HotKey) {
        $HotKeyStr = Format-HotKeyString $HotKey
    }
    else {
        $HotKeyStr = [string]::Empty
    }

    $ReturnValue = $false
    switch ($Ensure) {
        'Absent' {
            # ファイルがなければ$true あれば$false
            $ReturnValue = (-not (Test-Path $Path -PathType Leaf))
        }
        'Present' {
            $Info = Get-TargetResource -Ensure $Ensure -Path $Path -Target $Target
            if ($Info.Ensure -eq [Ensure]::Absent) {
                $ReturnValue = $false
            }
            else {
                $NotMatched = @()
                if ($Info.Target -ne $Target) {$NotMatched += 'Target'}
                if ($Info.WorkingDirectory -ne $WorkingDirectory) {$NotMatched += 'WorkingDirectory'}
                if ($Info.Arguments -ne $Arguments) {$NotMatched += 'Arguments'}
                if ($Info.Description -ne $Description) {$NotMatched += 'Description'}
                if ($Info.Icon -ne $Icon) {$NotMatched += 'Icon'}
                if ($Info.HotKey -ne $HotKey) {$NotMatched += 'HotKey'}
                if ($Info.WindowStyle -ne $WindowStyle) {$NotMatched += 'WindowStyle'}
                
                $ReturnValue = ($NotMatched.Count -eq 0)
                if (-not $ReturnValue) {
                    $NotMatched | ForEach-Object {
                        Write-Verbose ('{0} property is not matched!' -f $_)
                    }
                }
            }
        }
    }
    Write-Verbose "Test returns $ReturnValue"
    return $ReturnValue
} # end of Test-TargetResource

function New-Shortcut {
    [CmdletBinding()]
    [OutputType([System.__ComObject])]
    param
    (
        # Set Target full path to create shortcut
        [parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName)]
        [Alias('Target')]
        [string]$TargetPath,

        # set file path to create shortcut. If the path not ends with '.lnk', extension will be add automatically.
        [parameter(
            Position = 1,
            Mandatory,
            ValueFromPipelineByPropertyName)]
        #[validateScript({Test-Path (Split-Path $_ -Parent)})]
        [string]$Path,

        # Set Description for shortcut.
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias('Comment')]
        [string]$Description,

        # Set Arguments for shortcut.
        [parameter(ValueFromPipelineByPropertyName)]
        [string]$Arguments,

        # Set WorkingDirectory for shortcut.
        [parameter(ValueFromPipelineByPropertyName)]
        # [validateScript({Test-Path $_})]
        [string]$WorkingDirectory,

        # Set IconLocation for shortcut.
        [parameter(ValueFromPipelineByPropertyName)]
        [string]$Icon,

        [parameter(ValueFromPipelineByPropertyName)]
        [String]
        $HotKey,

        # Set WindowStyle for shortcut.
        [parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('normal', 'maximized', 'minimized')]
        [string]$WindowStyle = [WindowStyle]::normal,

        # set if you want to show create shortcut result
        [switch]$PassThru
    )

    begin {
        $extension = ".lnk"
        $wsh = New-Object -ComObject Wscript.Shell
    }

    process {
        # set Path for Shortcut
        if (-not $Path.EndsWith('.lnk')) {
            $Path = $Path + $extension
        }

        if ($HotKey) {
            $HotKeyStr = Format-HotKeyString $HotKey
        }

        if (-not (Test-Path (Split-Path $Path -Parent))) {
            Write-Verbose ("Create parent folder")
            New-Item -Path (Split-Path $Path -Parent) -ItemType Directory -Force -ErrorAction Stop
        }

        $fileName = Split-Path $Path -Leaf  # Filename of shortcut
        $Directory = Resolve-Path (Split-Path $Path -Parent) # Directory of shortcut
        $Path = Join-Path $Directory $fileName  # Fullpath of shortcut

        #Remove existing shortcut
        if (Test-Path $path) {
            Write-Verbose ("Remove existing shortcut file")
            Remove-Item $path -Force -ErrorAction SilentlyContinue
        }

        # Call Wscript to create Shortcut
        Write-Verbose ("Trying to create Shortcut for name '{0}'" -f $path)
        try {
            $shortCut = $wsh.CreateShortCut($path)
            $shortCut.TargetPath = $TargetPath
            $shortCut.Description = $Description
            $shortCut.WindowStyle = [int][WindowStyle]$WindowStyle
            $shortCut.Arguments = $Arguments
            $shortCut.WorkingDirectory = $WorkingDirectory
            if ($PSBoundParameters.ContainsKey('Icon')) {
                $shortCut.IconLocation = $Icon
            }
            if ($HotKeyStr) {
                $shortCut.Hotkey = $HotKeyStr
            }
            $shortCut.Save()
            Write-Verbose ('Shortcut file created successfully')
        }
        catch [Exception] {
            Write-Error $_.Exception
        }

        if ($PSBoundParameters.PassThru) {
            $shortCut
        }
    }

    end {}
}

function Get-Shortcut {
    [CmdletBinding()]
    [OutputType([System.__ComObject])]
    param
    (
        # Path of shortcut file
        [parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline)]
        [validateScript( {$_ | % {Test-Path $_}})]
        [string[]]$Path
    )
    begin {
        $wsh = New-Object -ComObject Wscript.Shell
    }

    Process {
        $Path.ForEach( {
                $fullPath = Resolve-Path $_
                Write-Verbose ('Trying to get file properties from "{0}"' -f $fullPath)
                $wsh.CreateShortcut($fullPath.Path)
            })
    }

    End {}
}

function Format-HotKeyString {
    [CmdletBinding()]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory, Position = 0)]
        [string[]]$HotKeyArray
    )

    $HotKeyArray = $HotKey.split('+').Trim()
    if ($HotKeyArray.Count -notin (2..4)) {
        #最短で修飾+キーの2要素、最長でAlt+Ctrl+Shift+キーの4要素
        Write-Error ('HotKey is not valid format.')
    }
    elseif ($HotKeyArray[0] -notmatch '^(Ctrl|Alt|Shift)$') {
        #修飾キーから始まっていないとダメ
        Write-Error ('HotKey is not valid format.')
    }
    else {
        #優先順位付きソート
        $sort = $HotKeyArray | % {
            switch ($_) {
                'Alt' {1}
                'Ctrl' {2}
                'Shift' {3}
                Default {4}
            }
        }
        [Array]::Sort($sort, $HotKeyArray)
        $HotKeyArray -join '+'
    }
}

Export-ModuleMember -Function *-TargetResource