Search code examples
windowspowershellshortcutwscript.shell

How to create a Windows Shortcut in Powershell to a file with special characters in filename?


I usually never have programming problems because I can easily find the answer to most of them. But I am at my wits' end on this issue.

The well known way of creating a Windows shortcut in Powershell is as follows:

$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("F:\path\to\shortcut.lnk")
$Shortcut.TargetPath = "F:\some\other\path\to\targetfile.txt"
$Shortcut.Save()

However, this method has a shortcoming which I'm starting to be bothered by more and more: it does not work when the filename has special characters eg a smiley face 😍 in filename:

"targetfile 😍.txt"

I researched the issue and discovered here that the WshShortcut object and WScript cannot accept unicode in the filenames. It only seems to work for a simple set of characters. Of course, when you right-click the file in Windows and select "Create Shortcut" Windows has no problems creating the shortcut with the special character in it.

Someone scripted in C# an alternative way to create shortcuts using Shell32 but I don't know if it can be done in Powershell. And it looks like an old method which might not work in newer builds of Windows.

Would someone please help me with this issue? How to create a Windows shortcut in Powershell to a file whose filename has special characters in it?


Solution

  • When in doubt, use C#:

    $ShellLinkCSharp = @'
    namespace Shox
    {
        using System;
        using System.Runtime.InteropServices;
        using System.Runtime.InteropServices.ComTypes;
        using System.Text;
    
        [ComImport]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [Guid("000214F9-0000-0000-C000-000000000046")]
        [CoClass(typeof(CShellLinkW))]
        interface IShellLinkW
        {
            void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
            IntPtr GetIDList();
            void SetIDList(IntPtr pidl);
            void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxName);
            void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
            void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
            void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
            void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
            void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
            ushort GetHotKey();
            void SetHotKey(ushort wHotKey);
            uint GetShowCmd();
            void SetShowCmd(uint iShowCmd);
            void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
            void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
            void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, [Optional] uint dwReserved);
            void Resolve(IntPtr hwnd, uint fFlags);
            void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
        }
    
        [ComImport]
        [Guid("00021401-0000-0000-C000-000000000046")]
        [ClassInterface(ClassInterfaceType.None)]
        class CShellLinkW { }
    
        public static class ShellLink
        {
            public static void CreateShortcut(
                string lnkPath,
                string targetPath,
                string description,
                string workingDirectory)
            {
                if (string.IsNullOrWhiteSpace(lnkPath))
                    throw new ArgumentNullException("lnkPath");
    
                if (string.IsNullOrWhiteSpace(targetPath))
                    throw new ArgumentNullException("targetPath");
    
                IShellLinkW link = new IShellLinkW();
    
                link.SetPath(targetPath);
    
                if (!string.IsNullOrWhiteSpace(description))
                {
                    link.SetDescription(description);
                }
    
                if (!string.IsNullOrWhiteSpace(workingDirectory))
                {
                    link.SetWorkingDirectory(workingDirectory);
                }
    
                IPersistFile file = (IPersistFile)link;
                file.Save(lnkPath, true);
    
                Marshal.FinalReleaseComObject(file);
                Marshal.FinalReleaseComObject(link);
            }
    
            // Get target with arguments
            public static string GetTarget(string lnkPath)
            {
                if (string.IsNullOrWhiteSpace(lnkPath))
                    throw new ArgumentNullException("lnkPath");
    
                IShellLinkW link = new IShellLinkW();
                IPersistFile file = (IPersistFile)link;
                file.Load(lnkPath, 0);
    
                const int MAX_PATH = 260;
                const int INFOTIPSIZE = 1024;
                StringBuilder targetPath = new StringBuilder(MAX_PATH + 1);
                StringBuilder arguments = new StringBuilder(INFOTIPSIZE + 1);
                try
                {
                    const int SLGP_RAWPATH = 4;
                    link.GetPath(targetPath, targetPath.Capacity, IntPtr.Zero, SLGP_RAWPATH);
                    link.GetArguments(arguments, arguments.Capacity);
                    return string.Format("\"{0}\" {1}", targetPath, arguments);
                }
                finally
                {
                    Marshal.FinalReleaseComObject(file);
                    Marshal.FinalReleaseComObject(link);
                }
            }
        }
    }
    '@
    
    # Check if Shox.ShellLink class already exists; if not, import it:
    if (-not ([System.Management.Automation.PSTypeName]'Shox.ShellLink').Type)
    {
        Add-Type -TypeDefinition $ShellLinkCSharp
    }
    
    
    [Shox.ShellLink]::CreateShortcut(
        'F:\path\to\shortcut1.lnk',
        'F:\some\other\path\to\targetfile1 😍.txt',
        '😍😍 my description 😍😍',
        'F:\some\another\path\my_working_directory')
    
    [Shox.ShellLink]::CreateShortcut(
        'F:\path\to\shortcut2.lnk',
        'F:\some\other\path\to\targetfile2 😍.txt',
        $null,  # No description
        $null)  # No working directory
    
    [Shox.ShellLink]::GetTarget('F:\path\to\shortcut2.lnk')