It is currently November 29th, 2021, 11:34 am

Passing Path with spaces to RunCommand

Help with creating, editing & fixing problems with skins
RedEye45
Posts: 3
Joined: June 29th, 2021, 5:18 am

Passing Path with spaces to RunCommand

Post by RedEye45 »

Hi, I am trying to use a PowerShell script to extract icons from a executable. The script takes in three arguments: Path to Exe, Output Dir, and Output Name. My issue is that I need to pass a path that contains spaces e.g: C:\Program Files\Blender Foundation\Blender 2.93\blender.exe. But PowerShell interprets the path as multiple arguments, messing up the script.

This is the PowerShell script (taken from this post https://forum.rainmeter.net/viewtopic.php?t=32404:

Code: Select all

@echo off
setlocal

set "exe_in=%~1"
set "out_dir=."
set "out_nam=%~3"

echo "ExecutablePath : %exe_in%"
echo "OutputPath : %out_dir%"
echo "OutputName : %out_nam%"

if not "%~2"=="" 2>NUL pushd "%~2" && ( call set "out_dir=%%CD%%" & popd )
set "ico_out=%out_dir%\%out_nam%.ico"

set "psCommand1="Remove-Item(\"%ico_out%\")""

set "psCommand2="[void][Reflection.Assembly]::LoadWithPartialName('System.Drawing');^
[Drawing.Icon]::ExtractAssociatedIcon(\"%exe_in%\").ToBitmap().Save(\"%ico_out%\")""

set "psCommand3="[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"%exe_in%\").FileDescription""

powershell -noprofile -noninteractive %psCommand1%

powershell -noprofile -noninteractive %psCommand2%

powershell -noprofile -noninteractive %psCommand3%
This is the RunCommand Measure.

Code: Select all

[MeasureGetIcon0]
Measure=Plugin
Plugin=RunCommand
Program =#@#Scripts\bat\get-name-and-icon.bat
Parameter="#Path0#" "#@#Icons" "Icon0"
UpdateDivider=-1
OutputFile=Command0.txt
OutputType=ANSI
DynamicVariables=1
Substitute="#CRLF#":""
FinishAction=[!Log "GetIconFinished #Path0#"][!Update][!Redraw]
The Powershell script outputs this:

Code: Select all

"ExecutablePath : C:\Program"
"OutputPath : ."
"OutputName : Foundation\Blender"

Remove-Item : Cannot find path 'C:\Users\ahsan\OneDrive\Documents\Rainmeter\Skins\Rainmeter 
Dock\Foundation\Blender.ico' because it does not exist.
I tried surrounding all the parameters in double quotes and some other suggestions:
  • Parameter=""#Path0#" "#@#Icons" "Icon0""
  • Parameter="\"#Path0#\" \"#@#Icons\" \"Icon0\""
  • Parameter="\'#Path0#\' \'#@#Icons\' \'Icon0\'"
But none of these worked.

I even tried this (The original author of the script used it in this way):

Code: Select all

[MeasureGetIcon0]
Measure=Plugin
Plugin=RunCommand
Parameter=#@#Scripts\bat\get-name-and-icon.bat "#Path0#" "#@#Icons" "Icon0"
OutputType=ANSI
OutputFile=Command0.txt
UpdateDivider=-1
DynamicVariables=1
Substitute="#CRLF#":""
FinishAction=[!Log "GetIconFinished #Path0#"][!Update][!Redraw]
While this works for the original author's example skin, it doesn't work for me even though I have the exact same directory structure. For me, it outputs a single

Code: Select all

'
and doesn't extract any icon at all.

Been stuck on this for an entire day. Would really appreciate if anyone can help :).
User avatar
death.crafter
Rainmeter Sage
Posts: 1200
Joined: April 24th, 2021, 8:13 pm

Re: Passing Path with spaces to RunCommand

Post by death.crafter »

RedEye45 wrote: June 29th, 2021, 5:51 am
Actually, I would suggest using PowershellRM. Here is the script:

Code: Select all

function Extract {
	
	$exePath=$RmAPI.VariableStr('ExePath')
	
	$icoPath=$RmAPI.VariableStr('IconPath')
	
	#extracts the name and type from full path
	$icoPath, $name, $type=if($icoPath -match '^(.+)\\([^\\]+)\.(.+)$'){$matches[1], $matches[2], $matches[3]} else {
		$icoPath,
		$($RmAPI.VariableStr('IconName')),
		$RmAPI.VariableStr('IconFormat')
	}
	
	if ($type -notin @('png', 'ico', 'bmp')) {$type = 'bmp'}

    $RmAPI.Log("$icoPath\$name.$type")
	
	#extracts the icons if all data are given, otherwise logs an error
	if ($icoPath -and $name -and $type) {
		Import-ExeIco -ExePath $exePath -Path $icoPath -Name $name -Type $type
	} else {
		$RmAPI.Log('Please provide necessary data!')
	}
}

function Import-ExeIco {
    param (
        [Parameter(Mandatory)]
        [string]
        $ExePath,
        [Parameter()]
        [string]
        $Path,
        [Parameter()]
        [string]
        $Name,
        [Parameter()]
        [string]
        $Type
    )
    Add-Type -AssemblyName System.Drawing

    try {
        $Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($ExePath)
        
        $icoPath=if ($Path -match "^.+\\$"){$Path}else{$Path+'\'}
        
        $Icon.ToBitmap().Save("$icoPath$Name.$Type")
        
        $Icon.Dispose()
    }catch{
        $RmAPI.Log('Could not extract icon')
    }
}
Now in Rainmeter, you can execute an action like:

Code: Select all

THIS MEASURE IS THE SCRIPT MEASURE:
;----------------------------------------------------------------------------------------
[PSRM]
Measure=Plugin
Plugin=PowershellRM
ScriptFile=path\to\script\file
;----------------------------------------------------------------------------------------

HOW TO USE:
;----------------------------------------------------------------------------------------
LeftMouseUpAction=[!SetVariable ExePath "path\to\exe\file"][!SetVariable IconPath "path\tosave\iconfile\name.type"][!CommandMeasure PSRM "Extract"]
; - - - - - - - - - - - - - - - - - OR
LeftMouseUpAction==[!SetVariable ExePath "path\to\exe\file"][!SetVariable IcoPath "path\tosave\iconfile"][!SetVariable IconName "iconname"][!SetVariable IconFormat "ico(/png/bmp)"][!CommandMeasure PSRM "Extract"]

EXAMPLE:
;----------------------------------------------------------------------------------------
[Variables]
App="C:\Program Files\Mozilla Firefox\firefox.exe"
IconFile=#@#Icons\firefox.ico

[SomeMeter]
LeftMouseUpAction=[!SetVariable ExePath "#App#"][!SetVariable IconPath "IconFile"][!CommandMeasure PSRM Extract]
PowershellRM documentation: https://khanhas.gitbook.io/powershellrm/

But ofc you can change the script for your own purposes. I just generalized it.

Here is the example skin:
IconExtractor_1.00.rmskin
If something doesn't work out right or you get errors feel free to reply.

Hope this helps.

Regards,
death.crafter
You do not have the required permissions to view the files attached to this post.
from the Realm of Death
RedEye45
Posts: 3
Joined: June 29th, 2021, 5:18 am

Re: Passing Path with spaces to RunCommand

Post by RedEye45 »

death.crafter wrote: June 29th, 2021, 7:47 am
Thanks a lot for the help. I have got it working for a extracting a single Icon. But I am working on a sort of dock, which has multiple icons (total number can be changed by the user), which I am updating at OnRefreshAction. So I tried incorporating a loop in the script you gave me. I don't really have any experience with PowerShell so I did a bit of Googling and just stuck in a for-loop and a bunch of -join functions.

Code: Select all

function Extract {
	
    $totalIcons=$RmAPI.VariableStr('TotalIcons')

    #Iterate over all icons
    for($i = 0; $i -lt $totalIcons; $i++)
    {
        $exePath=$RmAPI.VariableStr( -join("Path", $i))       
        $icoPath=$RmAPI.VariableStr("IconDir")

        #extracts the name and type from full path
        $icoPath, $name, $type=
        if($icoPath -match '^(.+)\\([^\\]+)\.(.+)$')
        {$matches[1], $matches[2], $matches[3]} 
        else {
            $icoPath,
            -join("Icon", $i),
            $RmAPI.VariableStr('IconFormat')
        }
        
        if ($type -notin @('png', 'ico', 'bmp')) {$type = 'ico'}
        
        #extracts the icons if all data are given, otherwise logs an error
        if ($path -and $name -and -$type) {
            Import-ExeIco -ExePath $exePath -Path $icoPath -Name $name -Type $type
        } else {
            $RmAPI.Log('Please provide necessary data!')
        }
    }
}
But I get this error:

Code: Select all

System.Management.Automation.PSInvalidOperationException: The pipeline was not run because a pipeline is already running. Pipelines cannot be run concurrently.
   at System.Management.Automation.Runspaces.PipelineBase.DoConcurrentCheck(Boolean syncCall, Object syncObject, Boolean isInLock)
   at System.Management.Automation.Runspaces.RunspaceBase.DoConcurrentCheckAndAddToRunningPipelines(PipelineBase pipeline, Boolean syncCall)
   at System.Management.Automation.Runspaces.PipelineBase.CoreInvoke(IEnumer (Rainmeter Dock\Rainmeter Dock.ini - [MeasureIconExtractor])
I'm not sure why exactly it's complaining about concurrency. Being used to regular C and C# loops, I thought this might run serially too. Anyways, is there a better way to call the extract function multiple times, each operating on a different path?
Last edited by RedEye45 on June 29th, 2021, 12:33 pm, edited 2 times in total.
User avatar
death.crafter
Rainmeter Sage
Posts: 1200
Joined: April 24th, 2021, 8:13 pm

Re: Passing Path with spaces to RunCommand

Post by death.crafter »

RedEye45 wrote: June 29th, 2021, 9:34 am
Check this if works and notice the comments.

Of Option 1 and 2 you can use either one, based on your requirements.

If you face some errors or don't understand something, I am here, :P

Code: Select all

function Extract {
	
    # to get double type values use $RmAPI.Variable() and not $RmAPI.VariableStr()
    
    $totalIcons=$RmAPI.Variable('TotalIcons')

    #Iterate over all icons
    for($i = 0; $i -lt $totalIcons; $i++)
    {
        # you don't need a join operator, "Path$i" would work just fine
            # notice the [" "](double quotes). It wouldn't work with [' '](single quotes)
            
        $exePath=$RmAPI.VariableStr("Path$i")

        # you need to declare icon path for each exe, which is kinda redundant, so just have one hardcoded
        
        $icoPath=$RmAPI.VariableStr("@")+"Icons"   # Yes you can add strings with just [+](addition operator), but both must be string
        
        $type=$RmAPI.VariableStr("IconFormat")

        ##------------------------------------------ ||||||||||||||||||||   Option 1   |||||||||||||||||||------------------------------------------

        # this gets the name from the exe

        $name=if ($exePath -match ".+\\([^\\]+)\.exe$") {$matches[1]} else {"$((Get-ChildItem -Path $icoPath).Count + 1)"}

        # checks if there is already a icon with the same name, which may be replaced unintentionally
        
        $name=if((Get-ChildItem -Path $icoPath).Name -contains "$name.$type"){$((Get-ChildItem -Path $icoPath).Count + 1)} else {$name}
        
        # extracts the icons if all data are given, otherwise logs an error
        
        if ($path -and $name -and -$type) {
            Import-ExeIco -ExePath $exePath -Path $icoPath -Name $name -Type $type
        } else {
            $RmAPI.Log('Please provide necessary data!')
        }
        #---------------------------------------------------------------

        ##------------------------------------------ ||||||||||||||||||||   Option 2   |||||||||||||||||||------------------------------------------
        
        # But it would be better if you just use numbers instead of names
        	# Also note that I removed $type and replaced it by .bmp by default, and you can change it
        	# but I would recommend to use bmp, since it is the format powershell creates

        if ($path -and $name -and -$type) {
            Import-ExeIco -ExePath $exePath -Path $icoPath -Name $i -Type '.bmp'    # notice the [-Name $i -Type '.bmp'] part
        } else {
            $RmAPI.Log('Please provide necessary data!')
        }
        
        #---------------------------------------------------------------_
        
    }
}
Still I would like to see how your skin works internally, so if you can, post an rmskin or zip of the skin folder here.

Regards,
death.crafter
from the Realm of Death
User avatar
Yincognito
Rainmeter Sage
Posts: 4101
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Passing Path with spaces to RunCommand

Post by Yincognito »

RedEye45 wrote: June 29th, 2021, 5:51 am Hi, I am trying to use a PowerShell script to extract icons from a executable. The script takes in three arguments: Path to Exe, Output Dir, and Output Name. My issue is that I need to pass a path that contains spaces e.g: C:\Program Files\Blender Foundation\Blender 2.93\blender.exe. But PowerShell interprets the path as multiple arguments, messing up the script.
Actually, it only gives the error if the icon file doesn't yet exist (i.e. you're at first execution and the icon has not yet been saved). After the 1st execution, no error comes up. The original batch file didn't check if the icon existed before trying to delete it (Remove-Item).

But then, the question is ... why would one need a batch file to run a powershell script? You can simply do...

Skin:

Code: Select all

[Variables]
Path0=C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

---Measures---

[MeasureGetIcon0]
Measure=Plugin
Plugin=RunCommand
Program=powershell
Parameter=-noprofile -noninteractive "#@#Scripts\powershell\get-name-and-icon.ps1" '#Path0#' '#@#Icons\Icon0.ico'
UpdateDivider=-1
OutputFile=Command0.txt
OutputType=ANSI
DynamicVariables=1
Substitute="#CRLF#":""
FinishAction=[!Log "GetIconFinished #Path0#"][!UpdateMeter *][!Redraw]

---Meters---

[MeterTest]
Meter=String
FontFace=Consolas
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
FontSize=16
AntiAlias=1
MeasureName=MeasureGetIcon0
Text="Output:"
LeftMouseUpAction=[!CommandMeasure MeasureGetIcon0 "Run"]
UpdateDivider=-1
DynamicVariables=1

[MeterImage]
Meter=Image
X=5R
ImageName=#@#Icons\Icon0.ico
UpdateDivider=-1
DynamicVariables=1
PS Script (located at #@#Scripts\powershell\get-name-and-icon.ps1):

Code: Select all

if (Test-Path $args[1]) {Remove-Item $args[1]}
[void][Reflection.Assembly]::LoadWithPartialName('System.Drawing');[Drawing.Icon]::ExtractAssociatedIcon($args[0]).ToBitmap().Save($args[1])
[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).FileDescription
It's basically about avoiding the need for a .bat file - much shorter and clearer codes all around. Only 2 parameters (the full path to the EXE and the ICO) are required as the PS script's parameters, they are enclosed between apostrophes (i.e. ') and if by any chance you have other apostrophes in your path, you can escape them by repeating the apostrophe, e.g. 'C:\Program Files\Apos'trophe' will become 'C:\Program Files\Apos''trophe'.

As for the automation process, I see death.crafter already took care of that. I just wanted to show that the original process can be both corrected and simplified.
RedEye45
Posts: 3
Joined: June 29th, 2021, 5:18 am

Re: Passing Path with spaces to RunCommand

Post by RedEye45 »

I have got it to work just fine using a mix of both the suggestions by you guys. Thanks a LOT for the help. Really appreciate it ;-).
User avatar
Yincognito
Rainmeter Sage
Posts: 4101
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Passing Path with spaces to RunCommand

Post by Yincognito »

RedEye45 wrote: June 29th, 2021, 3:58 pm I have got it to work just fine using a mix of both the suggestions by you guys. Thanks a LOT for the help. Really appreciate it ;-).
Excellent then. Death.crafter did most of the work though - my suggestion would have probably needed either a FOR in Powershell / some tweaking to accept a list of parameters through some wildcard system, or just run those RunCommands for each meter you wanted to have an associated icon in native Rainmeter code if you found the exclusive PowerShell approach too complex.

For the latter, a simplified version, in case someone else needs it as a base for future work (I kept the Metadata and the original authorship of StArL0rd84, as the modifications are minor, really - just simplification and some Default.ico to avoid errors if the icons don't exist yet):
Icon Extractor_3.0.0.rmskin
Icon Extractor.jpg
P.S. Using this would probably (not sure, as I have full rights already) require permission to directly run PowerShell scripts on the computer, by running PowerShell as an Administrator and entering set-executionpolicy remotesigned into it. Of course, this assumes one knows what's safe or not for him and some reasonable common sense when dealing with unknown scripts.
You do not have the required permissions to view the files attached to this post.