-1

I want to execute a PowerShell Command from VBA using WScript.Shell

Here is the string that gets sent to WScript.Shell:

Powershell -ExecutionPolicy Bypass -Command $FilesInPathway = get-childitem -path "C:\Users\Me\Desktop\NewFolder" -recurse -attributes !directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace(".TXT",".txt"); Rename-Item $file.fullname $tempName}

And here is the PowerShell Command I want to execute. Note this command does successfully execute in PowerShell.

$FilesInPathway = get-childitem -path "C:\Users\Me\Desktop\NewFolder" -recurse -attributes !directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace(".TXT",".txt"); Rename-Item $file.fullname $tempName}

Do not get information that there is an issue executing PowerShell Command, but the Command does not execute and files are not renamed. Msgbox returns no information. Blank form. Want to avoid switching to a PowerShell script or writing this using DIR and NAME.

strCommand = "Powershell -ExecutionPolicy Bypass -Command $FilesInPathway = get-childitem -path " & """" & dataPacket.filePath & """" & " -recurse -Attributes !Directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace(" & """" & dataPacket.stringOld & """" & "," & """" & dataPacket.stringNew & """" & "); Rename-Item $file.fullname $tempName}"
Set WshShell = CreateObject("WScript.Shell")
Set WshShellExec = WshShell.Exec(strCommand)
strOutput = WshShellExec.StdOut.ReadAll
MsgBox strOutput

UPDATE I

New Command does traverse the directory tree and returns correct number of files in that tree. It must be the foreach function I am passing that is not executing. But when I run the debug.print version of string directly in Powershell, it executes OK - all files are renamed.

strCommand = "Powershell $FilesInPathway = get-childitem -path " & """" & dataPacket.filePath & """" & " -recurse -attributes !directory; $FilesInPathway.count"
Set WshShell = CreateObject("WScript.Shell")
Set WshShellExec = WshShell.Exec(strCommand)
Do While WshShellExec.Status = 0
     Application.Wait (Now() + TimeValue("0:00:01"))
Loop
strOutput = WshShellExec.StdOut.ReadAll
MsgBox strOutput

The string that is passed to WshShell:

Powershell $FilesInPathway = get-childitem -path "C:\Users\Me\Desktop\NewFolder" -recurse -attributes !directory; $FilesInPathway.count

The string that is passed to PowerShell:

$FilesInPathway = get-childitem -path "C:\Users\Me\Desktop\NewFolder" -recurse -attributes !directory; $FilesInPathway.count

Note I have tried to use the StdIn.Write method to isolate Commands but am getting error.

WshShellExec.StdIn.Write "$FilesInPathway.count"

enter image description here

UPDATE II

Running Shell() with a type or enum declared may generate malicious macro warning and kill excel application? Have not isolated code that causes this.

enter image description here

The following will enter infinite loop if -noexit included with Command Do While WshShellExec.Status = 0 Application.Wait (Now() + TimeValue("0:00:01")) Loop

ANSWER

Of course this would be a syntax problem. Need to enclose parameters of Replace method with ' '. If filepath contains a whitespace, it too needs enclosure with ' '. If entered directly into PowerShell, enclosure with " " works fine. So the Command is:

strCommand = "Powershell $FilesInPathway = get-childitem -path " & "'" & dataPacket.filePath & "'" & " -recurse -attributes !directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace('" & dataPacket.stringOld & "','" & dataPacket.stringNew & "'); Rename-Item $file.fullname $tempName}"

This seems to explain why: https://stackoverflow.com/a/45762288/9721351

learnAsWeGo
  • 2,252
  • 2
  • 13
  • 19
  • Perhaps you explain what you’re trying to do – Doug Maurer Sep 27 '20 at 03:54
  • Are you calling this from a macro/vba in Word, Excel, et al? Where are you running this from VBA notwithstanding? This [" & """" & cataPacket.filePath & """" & "] is not a format that I've ever seen, for variable use, and is not a proper expanded in parlane. If you are running this form PowerShell, I am suprised it is successful at all as those as quoated are simple strings not UNC paths. Yet, ABL (always be learning) is my mantra. – postanote Sep 27 '20 at 04:49
  • Script needs some time to fail, bur [`Exec` method](https://www.vbsedit.com/html/5593b353-ef4b-4c99-8ae1-f963bac48929.asp) doesn't wait. Use[`.Run` method](https://www.vbsedit.com/html/6f28899c-d653-4555-8a59-49640b0e32ea.asp). Btw whats wrong with `FSO` or `Dir` unless you process a lot of files? – ComputerVersteher Sep 27 '20 at 05:36
  • @DougMaurer I want to execute a PowerShell Command from VBA using WScript.Shell – learnAsWeGo Sep 27 '20 at 15:36
  • @postanote from vba in excel. sorry for confusion. see edited post for string that gets sent to Wscript and then to Powershell. – learnAsWeGo Sep 27 '20 at 15:37
  • @ComputerVersteher would be neat to be able to use PowerShell in VBA. Also Dir doesnt support -recurse and it seems annoying to return directories to array then traverse those directories and so on and so on. – learnAsWeGo Sep 27 '20 at 15:40
  • @ComputerVersteher .run did not work either – learnAsWeGo Sep 27 '20 at 15:45
  • 1
    `returned = WshShell.Run(strCommand, 4, True)` failed? Although Dir has no recurse option, VBA has see [Loop Through All Subfolders Using VBA](https://stackoverflow.com/questions/22645347/loop-through-all-subfolders-using-vba) and check out FSO – ComputerVersteher Sep 27 '20 at 15:49
  • @ComputerVersteher it did. will take a look at Loop Through All Subfolders Using VBA. Thank you. – learnAsWeGo Sep 27 '20 at 17:52
  • 1
    This in not an answer if you want to use WScript.Shell but might help you debug your powershell line. And, this is not from Excel but run from an Access module (but VBA so I think it should work from Excel). I didn't want to test by renaming my own files so this just uses a posh session to list the folders under the directory in which the session is opened. Two examples, one might work for you - 1) write output to a file, 2) write output to the screen but keep the session open so you can read the screen and then close it manually. – JD Pavek Sep 27 '20 at 21:38
  • 1
    I don't know markdown - let me try and post readable code in a few minutes – JD Pavek Sep 27 '20 at 21:53
  • The false positive malicous code warnung is another reason to use WSxript.Shell! -noexit option doesn't create an infinite loop,as it will end as soon as you close the WShell window, but :Exec doesn't show it. So don't use -noexit with .Exec (use .Run instaed) and if done that use task manager to close script! Btw, I updated my answer: code is tested now! – ComputerVersteher Sep 28 '20 at 03:13

3 Answers3

2

@learnAsWeGo I didn't want to post this as an answer (see my prior comments) but had trouble marking it as code and getting it into a comment. That said, if this runs for you then you can focus on editing strPoshLine so it does what you want. Might be an alternative for you.

Dim strPoshLine As String
Dim Retval As Variant

'Example1 to execute a powershell line from VBA and write host output to a file
    strPoshLine = "get-childitem -directory | Select-object fullname | out-file 'c:\PoshFromVbaTeat.txt'"
    Retval = Shell("Powershell.exe -noexit -Command " & strPoshLine, 0)

'Example2 to execute a powershell line from VBA and not exit the session so
    strPoshLine = "get-childitem -directory | Select-object fullname "
    Retval = Shell("Powershell.exe -noexit -Command " & strPoshLine, 1)
    MsgBox "Enter 'exit' in powershell window to close the session."

@learnAsWeGo, based on your comments, the following code may be more flexible for you. The -Filter can only use * and ? but the -replace expression can use regular expressions. There is output in the open session window showing renamed files. The output could be piped to a format command to suit your preference. You can review the output and exit the window when done.

Public Sub VbaToPwsh()

Dim strGetFiles As String
Dim strRename As String
Dim varRetval As Variant

strGetFiles = "Get-Childitem -Path 'C:\zDev\*' -Filter '*.txt' -Recurse -File "
' Notes:  If using -Recurse then the last '\*' in the path is not needed, otherwise needed if using -Filter.
'         The use of -Filter and -File is just to reduce the number of files going through the pipeline
'           and not needed if the -replace operator provides all the constraint required.

strRename = " Rename-Item -NewName {$_.Name -replace '.txt', '.TXT'} -PassThru "
' Notes:  The -replace comparison operator is not case sensitive on selection but writes as shown.
'         If case sensitivity on selection is needed use -creplace instead.
'         Rename-Item does not provide output and that is way the -PassThru param is added.
          
varRetval = Shell("Powershell.exe -noexit -Command " & strGetFiles & "|" & strRename, 1)

MsgBox "Enter 'exit' in PowerShell window to close the session."

End Sub
JD Pavek
  • 154
  • 4
  • Interestingly enough, when make call Shell() from workbook I am developing this, excel generates malicious macro warning and kills application. Have not isolated component that triggers this, perhaps because I have a Type or Enum declared? However in another workbook Shell() with Command I want to run does work. Ultimately I had to replace the " " surrounding parameters of Replace method. Your code helped me get to error message that showed me what needed to be changed though. – learnAsWeGo Sep 28 '20 at 00:55
  • I deleted a comment with a line of code that didn't work. Question @learnAsWeGo, are you interested in renaming only the extension or any part of the file name? And, is the output you are looking for the old file name and the new on the same line? – JD Pavek Sep 28 '20 at 23:49
  • msgbox was meant to get a sense of what was happening in PowerShell. Command is meant to rename files, could be extension or any part of the file name. – learnAsWeGo Sep 29 '20 at 01:20
1

1. Wait until script has finished execution

You have to wait till WshShell.Exec has finished execution, as it won't signal it to you. So your code moves on and as script is still running, it hasn't returned a response and your MessageBox stays empty.

Simplest approach is to use WshShell.Run method, as that provides a parameter to wait till execution is finished.

strCommand = "Powershell -ExecutionPolicy Bypass -Command $FilesInPathway = get-childitem -path " & """" & dataPacket.filePath & """" & " -recurse -Attributes !Directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace(" & """" & dataPacket.stringOld & """" & "," & """" & dataPacket.stringNew & """" & "); Rename-Item $file.fullname $tempName}"
Set WshShell = CreateObject("WScript.Shell")
strOutput = WshShell.Run(strCommand ,0 ,True)
MsgBox strOutput

It is also possible to wait for the .Execmethod till it is finished (WshShellExec.Status <> 0), by looping and sometimes check .Status

strCommand = "Powershell -ExecutionPolicy Bypass -Command $FilesInPathway = get-childitem -path " & """" & dataPacket.filePath & """" & " -recurse -Attributes !Directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace(" & """" & dataPacket.stringOld & """" & "," & """" & dataPacket.stringNew & """" & "); Rename-Item $file.fullname $tempName}"
Set WshShell = CreateObject("WScript.Shell")
Set WshShellExec = WshShell.Exec(strCommand)
Do While WshShellExec.Status = 0
     Application.Wait(Now() + TimeValue("0:00:01"))
Loop
strOutput = WshShellExec.StdOut.ReadAll
MsgBox strOutput

2. You can't pass multiple powershell commands from shell (without some tricks, read Docs)

Just create a ps1 file with the commands and then execute that script like above.

That will enable you to use a Powershell IDE for coding and you can reuse that scripts elsewhere! All dynamic code can be passed as arguments to the matching parameters you create in your script (saved as RenameFiles.ps1).

param ($FilePath, $StringOld, $StringNew)
$FilesInPathway = get-childitem -path $FilePath -recurse -Attributes !Directory 
foreach ($file in $FilesInPathway)
{
    $tempName = $file.name 
    $tempName = $tempName.replace($StringOld,$StringNew) 
    Rename-Item $file.fullname $tempName
}

Command to execute that script:

strCommand = "Powershell -ExecutionPolicy Bypass -File ""path\to\RenameFiles.ps1"" """ & dataPacket.filePath & """ """ & dataPacket.stringOld & """ """ & dataPacket.stringeNew & """"
ComputerVersteher
  • 2,638
  • 1
  • 10
  • 20
  • Awesome. Feel like I have better command of this now. Sent a "Powershell -ExecutionPolicy Bypass -Command $FilesInPathway = get-childitem -path " & """" & dataPacket.filePath & """" & " -recurse -attributes !directory; $FilesInPathway.count and msgbox returned the correct number of files. So there must be an issue with the second half of my PowerShell command, but as stated in OP the string runs when I copy it from immediate window and paste it into PowerShell. – learnAsWeGo Sep 27 '20 at 19:25
  • You provide absolute paths, as WDcript may have a different [`.CurrentFolder`](https://ss64.com/vb/cd.html)? Tried `VBA.Shell strCommand ` (but has no waitonreturn option)? – ComputerVersteher Sep 27 '20 at 19:36
  • the filePath works OK as I am able to send other Commands and return information that is correctly, such as the number of files in directory tree I am traversing. issue seems to be with the foreach function that I send to PowerShell. I am trying to use WshShellExc.StdIn.Write method to isolate Commands, but am getting errors. Regardless I am now on the right track. Appreciate it. – learnAsWeGo Sep 27 '20 at 20:16
  • Seems like you can't execute multiple powershell commands, by simply passing them to shell! You tested the commands in powershell, not command-line? You should consider executing a file with ps code, by shell, not rhe commands. As benefit you can use a powershell ide and you can reuse that files elsewhere. Then just execute the ps1 file and pass the dynamic values as parameters to it! – ComputerVersteher Sep 28 '20 at 00:14
  • apologies for making this bit of an x y problem - but I really wanted to send Command from PowerShell. In this instance I needed to wrap the parameters of .Replace method in ' ' rather than "". If filepath contains a whitespace it too must be wrapped in ' ' rather than "". An aside if -noexit is included with command, Do While WshShellExec.Status = 0 never exits, an infinite loop. – learnAsWeGo Sep 28 '20 at 01:03
  • No need for excuses! As you see I don't use a lot of powershell scripts, but it won't harm me tearning how-to (in opposite, I'll benefit). Updated answer with a script using parameters ( test it first on powershell!) – ComputerVersteher Sep 28 '20 at 01:30
  • appreciate it. going to work with shell script answer over weekend. a good sequitur into creating and calling .dll's too! thank you. – learnAsWeGo Sep 29 '20 at 01:40
0

Of course this would be a syntax problem. Need to enclose parameters of Replace method with ' '. If filepath contains a whitespace, it too needs enclosure with ' '. If entered directly into PowerShell, enclosure with " " works fine. So the Command is:

strCommand = "Powershell $FilesInPathway = get-childitem -path " & "'" & dataPacket.filePath & "'" & " -recurse -attributes !directory; foreach ($file in $FilesInPathway){$tempName = $file.name; $tempName = $tempName.replace('" & dataPacket.stringOld & "','" & dataPacket.stringNew & "'); Rename-Item $file.fullname $tempName}"

This seems to explain why: https://stackoverflow.com/a/45762288/9721351

learnAsWeGo
  • 2,252
  • 2
  • 13
  • 19