2

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?

shox
  • 33
  • 5
  • 2
    Why do you have files with emojis in the name anyways? – Fitzgery Nov 27 '21 at 04:25
  • @fit You'd run into the same issue with other Unicode code points. If you take offense looking at emojis in filenames just pretend you're instead dealing with files in the user's Documents folder, where the user name uses a complex script. – IInspectable Nov 27 '21 at 06:55
  • @Fitzgery I download stuff from Youtube and other platforms, then organize them after download, creating shortcuts to files and stuff. Sometimes, the titles on Youtube have emojis in them, meaning the filenames end up with emojis in them. What can I do? It's just the way it is. I need a way get past Powershell's limitations. – shox Nov 27 '21 at 10:08

1 Answers1

2

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')
Giorgi Chakhidze
  • 3,351
  • 3
  • 22
  • 19
  • WOW Giorgi!! Thank you so much for your generous answer. Did you just write this up or did you have it ready made from a while back? Does it indeed work? Is this ready to be pasted and executed in Powershell? $ShellLinkCSharp = @'...'@ means the bit in between is C#? And the rest is Powershell code? – shox Nov 27 '21 at 23:18
  • I tried it in Windows 10. Instead of "targetfile1 .lnk" I get "targetfile1 ðŸ˜.lnk". The special characters/emojis are not rendered properly. What does the ".., ''" mean in the second last line. What is the third argument to CreateShortcut? – shox Nov 28 '21 at 00:41
  • I've copied it from [.NET WPF source](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationFramework/MS/Internal/AppModel/ShellProvider.cs#L360). And yes, it works, I've tested on both Windows 8.1 and 10. – Giorgi Chakhidze Nov 28 '21 at 12:32
  • I am guessing you've pasted it in PowerShell console, which by default does not support Unicode. Try saving as a file and executing that way. Or use PowerShell ISE. Third argument is just a shortcut description (Can be seen when you open shortcut properties). – Giorgi Chakhidze Nov 28 '21 at 12:34
  • @shox Yes, `@' ... '@` encloses C# code and `Add-Type` cmdlet imports it into PowerShell. See [PowerShell Here-strings](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.2#here-strings) and [Add-Type](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-7.2). – Giorgi Chakhidze Nov 28 '21 at 13:44
  • I DID copy and paste into a file, and renamed it with extension .ps1. It didn't work for me. I haven't tried Powershell ISE. What Powershell do you normally use? I use the standard Powershell 5.1.19041.1237, though I've wanted to upgrade to v7 for some time now. What Windows version do you use? Is it possible that you have extra fonts/packs installed which provides recognition for special characters like emojis? – shox Nov 29 '21 at 02:43
  • UPDATE: I tried Powershell ISE. Didn't work there either. The emoji is not rendered properly. – shox Nov 29 '21 at 02:56
  • I tested this script in both Windows PowerShell 5.1.14409.1018 and PowerShell 7.2 (on Windows 8.1 and 10). I have no special fonts, just make sure to select proper encoding when saving a file (Unicode or UTF-8). – Giorgi Chakhidze Nov 29 '21 at 07:34
  • I saved with UTF-8 BOM encoding and now it works. But encoding with UTF-8 doesn't work. Why? I used two different text editors - Notepad++ and SublimeText - and they both produce the same result. I though BOM was obsolete, yet here it is, required for this thing to work on my system. Why do you think this is? Also, what argument do you add to CreateShortcut in order to get the shortcut file to have the folderpath in the "Start in" field (when you right-click and select Properties). – shox Nov 29 '21 at 09:11
  • @shox Looks like PowerShell 5.1 has problems with UTF-8 without BOM. Works fine in PowerShell 7.2. I've updated my answer and example code — added fourth parameter to CreateShortcut(), `workingDirectory` (which corresponds to "Start in" field). – Giorgi Chakhidze Nov 29 '21 at 09:51
  • Many, many Thanks Giorgi. Voting this solution as having solved my problem! This did indeed solve my problem, though I still hold out hope for a solution implemented in Powershell without C#. But maybe it's beyond Powershell's limitations. – shox Nov 29 '21 at 23:20
  • @shox You're welcome. Sadly, IShellLink does not implement [IDispatch](https://learn.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-idispatch), and, because of this, it is not yet possible to automate it from "pure" PowerShell (for now). – Giorgi Chakhidze Nov 30 '21 at 07:55
  • Excuse me, but I need to retrieve the target path from a shortcut that has Unicode characters in it, e.g. `(New-Object -COM WScript.Shell).CreateShortcut("$pwd\FΘΘBДR.lnk").TargetPath` – but, of course, this doesn't work. I stumbled on your code while looking for a solution; though, it doesn't seem the code you've posted here works the same way. Could you please add a method for retrieving the details of a shortcut? – Vopel Jul 29 '23 at 20:12
  • @GiorgiChakhidze Follow-up to my previous comment, I found this, which works in PowerShell 5.1 but not in 7.3: https://github.com/mmessano/PowerShell/blob/master/Get-Link.ps1 – but, I was able to get it working in both by butchering out all the code related to console colors: https://pastebin.com/0pLQPaqp However, it doesn't list arguments. Also, is long path support at all possible? This line here: `[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]` seems to suggest that might not be doable. – Vopel Jul 29 '23 at 23:28
  • 1
    @Vopel Hi, I've added `GetTarget` function to my code sample. It will retrieve target and arguments. For example, on my system `[Shox.ShellLink]::GetTarget("C:\Users\Default\Desktop\Google Chrome.lnk");` prints `"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-dhcp-wpad --disable-ipv6` – Giorgi Chakhidze Jul 30 '23 at 11:21
  • As for long paths, according to documentation, `IShellLink` interface does not support them. – Giorgi Chakhidze Jul 30 '23 at 11:28
  • Thanks a ton. Shame about the long paths though. – Vopel Jul 31 '23 at 06:53