Process files dragged to BAT file in PowerShell - powershell

I trying to create a script that will convert Markdown files that are dropped on a processing script.
To accomplish this, I created a (process.bat) that would pass the names of the dropped files to a PowerShell script:
powershell.exe -NoProfile -File "./process.ps1" -Document %*
#pause
The PowerShell file (process.ps1) would process each file individually:
[parameter(Mandatory=$true)]
[String[]]$Document
Write-Host $args[1]
$Document | ForEach-Object {
Write-Host "Document: $_"
# convert Markdown to Html
pandoc -o ($_ -Replace '.md', '.html') -f markdown -t html $_
}
When I drop two files on the batch file:
C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown>powershell.exe -NoProfile -File "./process.ps1" -Document "C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\FOO.md"
"C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\BAR.md"
C:\Users\XXX\Documents\WindowsPowerShell\Scripts\Markdown\BAR.md
Document:
Press any key to continue . . .
The documents are not being processed.
What's the recommended approach to passing the batch file's file list %* to PowerShell?

When powershell.exe, the PowerShell CLI, is called from the outside with the -File parameter, it doesn't support arrays - only individual arguments.[1]
(Additionally, you neglected to wrap your parameter definition in a param(...) block, which effectively caused it be ignored.)
The simplest solution is to define your parameter with the ValueFromRemainingArguments option so that it automatically collects all positional arguments in the parameter variable:
param(
[Parameter(Mandatory, ValueFromRemainingArguments)]
[String[]] $Document
)
Then invoke your script via the PowerShell CLI without -Document:
powershell.exe -NoProfile -File "./process.ps1" %*
As an alternative to using a helper batch file, you can:
define a shortcut file (*.lnk) that explicitly invokes your PowerShell script as powershell.exe -File \path\to\your\script.ps1 (without additional arguments)
and then use that as the drop target.
Note: The reason that you cannot use a PowerShell script (*.ps1) directly as a drop target is that PowerShell script files aren't directly executable - instead, opening (double-clicking) a *.ps1 file opens it for editing.
You'd then need to add pause (or something similar, such as Read-Host -Prompt 'Press Enter to exit.') to your PowerShell script, to prevent it from closing the window instantly on finishing.
Alternatively, leave the script as-is and use -NoExit (placed before -File) to keep the PowerShell session open.
[1] The same applies to the PowerShell Core CLI, pwsh.

Related

Wrong encoding with powershell shell command

I setup my powershell so I can drag and drop files on .ps1 files and start the script with the file paths as arg parameters.
I set a default key to
\HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\Open\Command
Default key
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -File "%1" %*
But this command can't handle files with cyrillic or japanese characters.
For example, create a test.ps1 file:
foreach ($arg in $args) {
Write-Host "$arg"
}
pause
and I start this script like this:
powershell -File test.ps1 "ダーク" "Олег"
I get this result:
PS C:\Users\...\Desktop> powershell -File test.ps1 "ダーク" "Олег"
???
????
Is there an alternative command I can set to start a script or is there a way to force an encoding for this command?

How do I have to change PowerShell variables code so that I can run it via CMD?

How do I have to change PowerShell code so that I can run it via CMD?
I came up with the following code:
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Count :","") > $env:APPDATA\BIOS-Benchmark\Count_only.txt
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Average :","") > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
If I copy and paste it completely into a powershell, it can run. But now I have to put the code next to other code in a batch file. How do I have to adjust the code so that the cmd.exe executes the whole thing?
I suspect setting the variables via Powershell code is problematic here.
Unfortunately, a PS1 file is out of the question for my project.
To execute PowerShell commands from a batch file / cmd.exe, you need to create a PowerShell child process, using the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+) and pass the command(s) to the -Command (-c) parameter.
However, batch-file syntax does not support multi-line strings, so you have two options (the examples use two simple sample commands):
Pass all commands as a double-quoted, single-line string:
powershell.exe -Command "Get-Date; Write-Output hello > test.txt"
Do not use quoting, which allows you to use cmd.exe's line continuations, by placing ^ at the end of each line.
powershell.exe -Command Get-Date;^
Write-Output hello ^> test.txt
Note:
In both cases multiple statements must be separated with ;, because ^ at the end of a batch-file line continues the string on the next line without a newline.
Especially with the unquoted solution, you need to carefully ^-escape individual characters that cmd.exe would otherwise interpret itself, such as & and >
See this answer for detailed guidance.
Powershell -c executes PowerShell commands. You can do this from cmd, however, it looks like it needs to be run as administrator.
PowerShell -c "$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Count :','') > $env:APPDATA\BIOS-Benchmark\Count_only.txt;
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Average :','') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt"
It is possible to execute the PowerShell code in a batch file, but technically what you are doing is pulling a copy of it out and executing it someplace else. Here are 3 methods that I know of.
mklement0's answer addresses executing a copy of it that is passed as a parameter to PowerShell.
You could build a ps1 file from CMD, and then execute that ps1 file by passing it as a parameter to PowerShell.
And the method I've worked with the most is to pass specially designed PowerShell code to PowerShell that, when it runs, will load all, or part, of the current CMD file into memory and execute it there as a ScriptBlock. I have tried loading parts of the current CMD file, but my experience has been that this gets too complicated and I just stick with loading the entire current CMD file.
That last method is what I'm presenting here. The trick is to make the batch/CMD portion of the script look like a comment that is ignored by PowerShell, but still runs without throwing error messages in CMD. I'm not sure where I first found this trick, but it goes like this:
First, place <# : at the start of script. PowerShell sees this as the start of a comment, but CMD seems to ignore this line. I think CMD is trying to redirect < the contents of a non-existing file : to a non-existing command. But what does CMD do with the #? It works, and that's the important thing.
Place your batch code in lines following the <# :.
You end the batch/CMD part with a GOTO :EOF.
You then end the PowerShell comment with #>, but visually I find it easier to find <#~#>, which does the same job.
The rest of the file is your PowerShell code.
This version treats the PowerShell code as a function with defined parameters. The batch part builds %ARGS% and passes, with double quotes intact, to a PowerShell ScriptBlock that in turn is wrapped in another ScriptBlock. The PowerShell function is called twice with the same SourceFile parameter, but different DestinationFile and TextToRemove parameters. Perhaps there is a simpler way to reliably pass double quotes " in arguments passed to a ScriptBlock from batch, but this is the method I got working.
<# :
#ECHO OFF
SET f0=%~f0
SET SourceFile=%APPDATA%\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Count_only.txt" "Count :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Durchschnitt_only.txt" "Average :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
GOTO :EOF
<#~#>
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$SourceFile,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFile,
[Parameter(Mandatory = $true, Position = 2)]
[string]$TextToRemove
)
(Get-Content $SourceFile).Replace($TextToRemove, '') > $DestinationFile
This script passes a single parameter that, in PowerShell, is used by the Switch command to decide which section of PowerShell you intend on executing. Since we are not passing double quotes " in the args, the PowerShell lines can be greatly simplified. Information could still be passed to PowerShell by defining environmental variables in batch and reading them in PowerShell.
<# :
#ECHO OFF
SET f0=%~f0
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script1
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script2
GOTO :EOF
<#~#>
switch ($args[0]) {
'Script1' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Count :", '') > $env:APPDATA\BIOS-Benchmark\Count_only.txt
break
}
'Script2' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Average :", '') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
break
}
default {}
}
The -c parameter is intended to solve this scenario.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh?view=powershell-7.2#-command---c
If possible, it would be more efficient to invoke PowerShell\Pwsh directly rather than using a cmd wrapper.

PowerShell function not running as expected

I have a curious case that I cannot fathom the reason for...
Please know I am a novice to PowerShell.
I am working on a PowerShell menu system to help automate building out new computers in my environment. I have a PS1 file that holds the script for an app install. When I use the script to reference this I am able to run it and have no issue. However, when I try inserting this into a function and referencing it does not.
This works:
4 # Microsoft Office 32-bit
{
Write-Host "`nMicrosoft Office 32-bit..." -ForegroundColor Yellow
# {installMS32Bit}
Invoke-Expression "cmd /c start powershell -NoExit -File '\\**SERVERPATH**\menuItems\ms_office\32-bit\install.ps1'"
Start-Sleep -seconds 2
}
This does not:
function installMS32Bit(){
Invoke-Expression "cmd /c start powershell -NoExit -File '\\**SERVERPATH**\menuItems\ms_office\32-bit\install.ps1'"
}
}
4 # Microsoft Office 32-bit
{
Write-Host "`nMicrosoft Office 32-bit..." -ForegroundColor Yellow
{installMS32Bit}
Start-Sleep -seconds 2}
install.ps1 file:
# Copy MS Office uninstall and setup to local then run and install 32-bit Office
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\setup.exe' -Destination 'C:\temp\' -Force
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\uninstall.xml' -Destination 'C:\temp\' -Force
Copy-Item -Path '\\**SERVERPATH**\menuItems\ms_office\32-bit\Setup.exe' -Destination 'C:\temp' -Force
Invoke-Expression ("cmd /c 'C:\temp\setup.exe' /configure 'C:\temp\uninstall.xml'")
Start-Process -FilePath 'C:\temp\Setup.exe'
Secondary question and a little explanation for Invoke-Expression...
I like to see progress and like to have secondary windows open to monitor the new process being run. I was unable to find a solution with a persistent window that worked for me to do this without Invoke-Expression.
If there is a better way to do this in PowerShell I am all ears!
{installMS32Bit}
As Mathias points out in a comment on the question, this statement doesn't call your function, it wraps it in a script block ({ ... })[1], which is a piece of reusable code (like a function pointer, loosely speaking), for later execution via &, the call (execute) operator.
To call your function, just use its name (by itself here, given that there are no arguments to pass): installMS32Bit
Invoke-Expression should generally be avoided; definitely don't use it to invoke an external program, as in your attempts.
Additionally, there's generally no need to call an external program via cmd.exe (cmd /c ...), just invoke it directly.
For instance, replace the last Invoke-Epression call from your question with:
# If the EXE path weren't quoted, you wouldn't need the &
& 'C:\temp\setup.exe' /configure 'C:\temp\uninstall.xml'
I like to see progress and like to have secondary windows open to monitor the new process being run. I was unable to find a solution with a persistent window that worked for me to do this without Invoke-Expression.
(On Windows), Start-Process by default executes a console application in a new window (unless you specify -NoNewWindow), asynchronously (unless you specify -Wait).
You cannot pass a .ps1 script directly to Start-Process (it will be treated like a document to open rather than an executable to call), but you can pass it to PowerShell's CLI via the -File parameter:
Start-Process powershell.exe '-File install.ps1'
The above is short for:
Start-Process -FilePath powershell.exe -ArgumentList '-File install.ps1'
That is, PowerShell will execute the following in a new window:
powershell.exe -File install.ps1
[1] Since you're not assigning the script block being created to a variable, it is implicitly output (printed to the display, in the absence of a redirection); a script block stringifies by its literal contents, excluding the enclosing { and }, so string installMS32Bit will print to the display.

Open a file with a PowerShell script in explorer

I wanted to use Windows built-in table viewer to open CSV file, like this SO answer shows.
So I want that when I double click on a CSV file in Explorer, the following command is run:
Import-Csv [CSV-FILE] |Out-GridView
I ended up creating two script files. One file name "read_csv.bat" which contains:
powershell.exe -ExecutionPolicy ByPass -noexit -File %~dp0read_csv.ps1 -csvfile %1
pause
Another file name "read_csv.ps1" which contains the actual script
param (
[Parameter(Mandatory=$true)][string]$csvfile
)
Import-Csv $csvfile |Out-GridView
Is there now way to do it more efficiently, so with only one script file? If I set explorer to open the CSV file with the POwerShell script directly, a blue message appears
This app can't run on your PC
To find a version for your PC, check with your published
Note:
Since powershell.exe is ultimately called, a console window will invariably (also) open when a CSV file is opened, because powershell.exe is a console-subsystem application.
Providing an alternative, GUI-subsystem executable to avoid creating a console window is the subject of this feature request on GitHub; in the meantime, there are workarounds:
A VBScript-based solution is shown in this answer.
A script-less, but complex and potentially AV-software-triggering alternative is used in this answer.
You'll need to use an adapted version of the powershell.exe command from your batch file, because *.ps1 files are by design not directly executable.
That is, in the registry definition for the CSV file type, use the following command to define the action for the Open verb (see below):
powershell.exe -ExecutionPolicy ByPass -NoExit -File c:\path\to\read_csv.ps1 -csvfile "%1"
Be sure to substitute the real path to your *.ps1 script for c:\path\to\read_csv.ps1 (double-quote it, if necessary); you can either use a literal path, or one based on cmd.exe-style environment-variable references (e.g., "%USERPROFILE%\read_csv.ps1").
Alternatively, you can make do without a script file altogether, using the PowerShell CLI's -Command parameter:
powershell.exe -ExecutionPolicy ByPass -NoExit -Command Import-Csv \"%1\" | Out-GridView
To automate the process of configuring this command for opening CSV files via File Explorer / the desktop:
The code below modifies the registry as follows:
defines a new file type, PsCsvViewer, with an Open verb (operation) that invokes the PowerShell command defined above.
associates the .csv filename extension with that new file type.
Note: The first time you open a CSV file after the redefinition, you may be prompted to confirm the intent to use a PowerShell command from now on.
creates the definitions above for the current user only, which means that you don't need admin privileges to run the code (which writing to subkeys of HKEY_CLASSES_ROOT\ would require).
# Note the need for *3* "\", because an extra layer of escaping is
# needed for reg.exe.
$cmd = 'powershell.exe -ExecutionPolicy ByPass -NoExit -Command Import-Csv \\\"%1\\\" | Out-GridView'
# Create a new file type for the PowerShell command.
reg.exe add HKCU\Software\Classes\PsCsvViewer\Shell\Open\command /ve /d $cmd /f
# Associate .csv files with the new file type.
reg.exe add HKCU\Software\Classes\.csv /ve /d PsCsvViewer /f

Need to call powershell script from batch file

I have a batch file which is in a folder called script. The script folder also contains folder called powershell which has a script called IE-Settings.ps1.
I want to execute the powershell script from the batch file and I am unable to give powershell script path in the command. What I tried is
call %windir%\system32\WindowsPowerShell\v1.0\powershell.exe -File "& '%~dp0IESettings\IE-Settings.ps1'"
But it doesn't recognize the path
call is for running other batch files in a way that they return to the current batch file after they terminate, and per your question the subdirectory name is powershell, not IESettings. Also, when using the parameter -File you just specify the path to the file.
powershell.exe -File "%~dp0powershell\IE-Settings.ps1"
The call operator (&) is used when running PowerShell code via the -Command parameter, e.g.:
powershell.exe -Command "& { Write-Host 'foo' }"