Search code examples
windowspowershellemojifile-copyingshortcut-file

Copy the target of a shortcut file (*.lnk) when the target path contains emoji characters


My goal is to write a simple Powershell script that will take one mandatory argument, that argument must be a full file path to a shortcut (.lnk) file, then the script will resolve the shortcut's target item (a file or a directory) and copy it into the current working directory of the script.

The problem I found is when testing a shortcut whose target item points to a file or folder that contains emoji chars in the path, like for example:

"C:\Movies\• Unidentified\[🇪🇸]\Amor, curiosidad, prozak y dudas (2001)\File.mkv"

Firstly I've tried with Copy-Item cmdlet, and after that I tried with Shell.NameSpace + Folder.CopyHere() method from Windows Shell Scripting as shown in this example:

https://stackoverflow.com/a/33760315/1248295

That methodology is what I finally pretend to use for this script instead of Copy-Item cmdlet, because it displays the default file progress UI and I prefer it for this reason.

Note that I'm not very experienced with PowerShell, but in both cases the Copy-Item cmdlet and the CopyHere method are executed without giving any exception message, it just does not perform the file copy operation.

If the item path of the shortcut's target item does not contain emoji chars, it works fine.

I'm not sure if it's some kind of encoding issue. My default O.S encoding is Windows-1252.

What I'm doing wrong and how can I fix this issue?.

# Takes 1 mandatory argument pointing to a shortcut (.lnk) file, 
# resolves the shortcut's target item (a file or directory), 
# and copies that target item to the specified destination folder 
# using Windows default IFileOperation progress UI.

# - File copy method took from here:
#   https://stackoverflow.com/a/33760315/1248295

# - "Shell.NameSpace" method and "Folder" object Docs:
#   https://learn.microsoft.com/en-us/windows/win32/shell/shell-namespace
#   https://learn.microsoft.com/en-us/windows/win32/shell/folder

param (
    [Parameter(
        Position=0,
        Mandatory, 
        ValueFromPipeline, 
        HelpMessage="Enter the full path to a shortcut (.lnk) file.")
    ] [string] $linkFile = "",
    [Parameter(
        Position=1,
        ValueFromPipeline, 
        HelpMessage="Enter the full path to the destination folder.")
    ] [string] $destinationFolder = $(Get-Location)
)

$wsShell    = New-Object -ComObject WScript.Shell
$shellApp   = New-Object -ComObject Shell.Application
$targetItem = $wsShell.CreateShortcut($linkFile).TargetPath

Write-Host [i] Link File..: ($linkFile)
Write-Host [i] Target Item: ($targetItem)
Write-Host [i] Destination: ($destinationFolder)
Write-Host [i] Copying target item to destination folder...
$shellApp.NameSpace("$destinationFolder").CopyHere("$targetItem")
Write-Host [i] Copy operation completed.

#[System.Console]::WriteLine("Press any key to exit...")
#[System.Console]::ReadKey($true)
Exit(0)

UPDATE

I've put all this after the param block and nothing has changed:

[Text.Encoding] $encoding                      = [Text.Encoding]::UTF8
[console]::InputEncoding                       = $encoding
[console]::OutputEncoding                      = $encoding
$OutputEncoding                                = $encoding
$PSDefaultParameterValues['Out-File:Encoding'] = $encoding
$PSDefaultParameterValues['*:Encoding']        = $encoding

Solution

  • As discussed in comments, the Shell.CreateShortcut method seems to have an encoding issue when it comes to emoji (the root cause is propably missing support of UTF-16 surrogate pairs). The value of the variable $targetItem already contains ?? in place of the emoji character. The proof is that this does not only show in the console, but also if you write the value to an UTF-8 encoded file.

    As a workaround, you may use the FolderItem2.ExtendedProperty(String) method. This allows you to query a plethora of shell properties. The one we are interested in is System.Link.TargetParsingPath.

    Function Get-ShellProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
            [Alias('FullName', 'PSPath')]
            [string[]] $LiteralPath,
    
            [Parameter(Mandatory)]
            [string[]] $Property
        )
        begin{
            $Shell = New-Object -ComObject Shell.Application
        }
        process{
            foreach( $filePath in $LiteralPath ) {
                $fsPath = Convert-Path -LiteralPath $filePath
                $nameSpace = $Shell.NameSpace(( Split-Path $fsPath ))       
                $file = $nameSpace.ParseName(( Split-Path $fsPath -Leaf ))
    
                # Query the given shell properties and output them as a new object
                $ht = [ordered] @{ Path = $filePath }
                foreach( $propId in $Property ) {
                    $ht[ $propId ] = $file.ExtendedProperty( $propId )
                }
                [PSCustomObject] $ht
            }
        }
    }
    

    Usage:

    $properties = Get-ShellProperty $linkFile -Property System.Link.TargetParsingPath
    $targetItem = $properties.'System.Link.TargetParsingPath'
    

    You may also query multiple properties with one call:

    $properties = Get-ShellProperty $linkFile -Property System.Link.TargetParsingPath, System.Link.Arguments
    $targetItem = $properties.'System.Link.TargetParsingPath'
    $arguments  = $properties.'System.Link.Arguments'