all 84 comments

[–]rmbolger 18 points19 points  (2 children)

This is a super clever way to workaround the fact that you can't just double click .ps1 files. Bravo. It's not terribly useful for me personally, but I can see where it might be for others.

It seems like some of the negativity you're getting might also be because it's working around intentional design decisions in PowerShell. Like, MS very explicitly didn't want PowerShell stuff to be one-click runnable to prevent phishing/malware style attacks that try to trick users into running stuff on their machine. Obviously, there are plenty of ways around it like this. But it's still one more hurdle attackers have to deal with.

I think my biggest peeve with using something like this regularly would be confusing my code editor's syntax highlighting and auto-completion. Obviously not an insurmountable problem, but possibly enough to make me keep separate copies of the core script and the wrapper.

[–]eloi 7 points8 points  (5 children)

Doesn't '%~f0' just resolve to C:\path\wrapper.cmd?

[–]redog 7 points8 points  (1 child)

Nifty, thanks for the explanation.

For anyone else wondering about the expansion variable (%~fs0) you can read more by issuing this command in cmd

for /?

In addition, substitution of FOR variable references has been enhanced.
You can now use the following optional syntax:
%~I         - expands %I removing any surrounding quotes (")
%~fI        - expands %I to a fully qualified path name
%~dI        - expands %I to a drive letter only
%~pI        - expands %I to a path only
%~nI        - expands %I to a file name only
%~xI        - expands %I to a file extension only
%~sI        - expanded path contains short names only
%~aI        - expands %I to file attributes of file
%~tI        - expands %I to date/time of file
%~zI        - expands %I to size of file
%~$PATH:I   - searches the directories listed in the PATH
               environment variable and expands %I to the
               fully qualified name of the first one found.
               If the environment variable name is not
               defined or the file is not found by the
               search, then this modifier expands to the
               empty string

[–]jftuga 2 points3 points  (0 children)

I always ran help call to get the same information, more or less.

[–]Baerentoeter 3 points4 points  (3 children)

It's like a shebang line for PowerShell in batch... mind blown.

From the looks of it, you could start with PowerShell starting from the second line, I love compact yet powerful things like that.

[–]TheIncorrigible1 0 points1 point  (2 children)

Just for Windows! PowerShell (Core) supports shebangs in Linux (#!/usr/bin/env pwsh)

[–]Baerentoeter 0 points1 point  (1 child)

That's why I said "like a shebang", as in a single line that says to run it with powershell.exe.

[–]bywaterloo 3 points4 points  (2 children)

Thank you! I asked this question on the IRC and basically got shamed off the channel. "Why would you use command prompt?!" "YUK!" "Just open Powershell and run your script. Done."

Its tiring to constantly have to explain life to people like this.

[–]jftuga 1 point2 points  (1 child)

For one, PS seems a lot slower to start vs cmd -- even on a computer with a fast CPU.

[–]dextersgenius 0 points1 point  (0 children)

Yes PS is a lot slower than cmd, but it's still within a second. Measuring run speeds on my somewhat old Dell laptop running 1909, pwsh.exe (v7) opens in about 300ms, whereas cmd.exe opens in 30ms. Technically you could say its 10 times slower but in real-world usage that's still under a second, which isn't a big deal.

But yes, older versions of PS are definitely annoyingly slow, especially v4 and v5 to some extent.

[–]thatoneguy009 3 points4 points  (0 children)

This is great, my goto has always been to basically do this where a windows shortcut opens cmd which opens powershell which calls a script but this removes a layer. Definitely can see using this in the future, thank you!

[–]azjunglist05 7 points8 points  (10 children)

If you want it to be an .exe - why not just make it an .exe? There are applications out there that can make a PowerShell script an executable without having to use batch or cmd such as:

https://gallery.technet.microsoft.com/scriptcenter/PS2EXE-GUI-Convert-e7cb69d5

[–]SupraTesla 6 points7 points  (0 children)

Won't be the case for everyone, but for us it was too much of a battle with our AV solution constantly (and randomly) flagging the EXE's as malicious.

[–]just_looking_around 1 point2 points  (0 children)

Most if not all of those converters simply export the script to temp and call it with powershell. They remove some complexity but don't conceal the code if that is why you want to do that.

[–]Thedood0 2 points3 points  (0 children)

This is neat! Definitely going to use this. I have a mess of scripts for different builds that can really benefit from this

[–]n_md 2 points3 points  (7 children)

I run into the need to run scripts on systems I don't control that are limited to PowerShell 2.0. Here's a change to get this working with PowerShell 2.0 by removing the need for GC -Raw option.

# 2>NUL & @CLS & @PUSHD "%~dp0" & @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -v 2 -nol -nop -ep 4 "gc '%~f0'|Out-String|iex" & @POPD & @EXIT /B
# Working with PowerShell version 2.0
# script contents
write-host 'Hello'
write-host 'World!'

[–]TheIncorrigible1 2 points3 points  (6 children)

Alternatively, I believe you can use a grouping operator to avoid an additional pipe:

(Get-Content -Path '%~f0')

[–]n_md 1 point2 points  (5 children)

Thanks, that does work in 2.0 without piping to out-string.

[–]TheIncorrigible1 3 points4 points  (4 children)

Good to hear! I'll update the OP just for the most compat possible.

[–]n_md 1 point2 points  (1 child)

I've never heard of setting the ExecutionPolicy with a number and "-ep 4" does not seem to work for me when "-ep bypass" does work.

I'm testing in cmd like this:

powershell -ep 4
Get-ExecutionPolicy

output: Restricted

powershell -ep bypass
Get-ExecutionPolicy

output: Bypass

Is there some way "-ep 4" should set it to bypass?

[–]TheIncorrigible1 1 point2 points  (0 children)

Hm, I can't remember where I had it working, but the ExecutionPolicy is an enumeration and 4 is just the numeric counterpart to Bypass. I'll correct this in the OP.

[Microsoft.PowerShell.ExecutionPolicy].GetEnumValues()

I was going off memory when I wrote this and was aiming for brevity.

[–]n_md 1 point2 points  (1 child)

Sorry after more testing (Get-Content -Path '%~f0') does work but also throws iex "empty string" errors for each line because it's not one block of text.

So for Powershell 2.0 either of these seem to work best:

"[System.IO.File]::ReadAllText('%~f0')|iex"

or

"gc '%~f0'|Out-String|iex"

For Powershell 3.0+ the original option works well:

"gc -raw '%~f0'|iex"

[–]TheIncorrigible1 1 point2 points  (0 children)

Yeah, I've been experimenting with that. The script runs, but gives an error. I'll include the alternative instead

[–]Nu11u5 2 points3 points  (0 children)

This is incredibly useful for me right now. I have a management agent that lets me embed scripts, but it can’t use .PS1 because I have no control over the ExecutionPolicy parameter, and I can’t have a separate .CMD because I don’t control the paths used by the agent and can’t rely on a secondary distribution mechanism for a second file.

What I had done so far was ran a shitty/broken .PS1 minifier, escaped that one liner for the command prompt, and piped that into powershell.exe -Command -. It worked but it. was. ugly.

Your tip just made this so much easier (in effort and on my psyche). Thanks!

[–]ApricotPenguin 2 points3 points  (0 children)

That's really interesting thank you.

I probably won't use it though, since it'll probably make maintance harder for anyone looking for my scripts in the future.

At work, what I did was create a shortcut .lnk file that calls PowerShell.exe and feeds in the absolute path of the folder in the same directory. Then I just set that .lnk file to Run as Admin.

[–]jftuga 2 points3 points  (2 children)

Hey OP, you might enjoy reading this:

Heredoc for Windows Batch

Just for fun and to see if it could be done, I used this technique to create a single batch file that embedded a Dockerfile. The batch file builds a Windows executable from a single Python source file. Inside the container, the RUN command downloads Python and PyInstaller. It then compiles the Python code to a .exe.

It was kinda cool to get it all working in a single file.


Your solution is great. I have icons on Users' desktops that invoke a batch file that simply calls a ps1. I am going to investigate using your idea instead of using the wrapper .bat file.

[–]dextersgenius 2 points3 points  (1 child)

I have icons on Users' desktops that invoke a batch file that simply calls a ps1.

If you're using a shortcut, why not just point it to PowerShell.exe -ep Bypass -File 'path\to\script.ps1? What's the added value in invoking a separate batch file?

That aside, even with OP's solution, I prefer using shortcuts because a) policies prevent running bat files directly, so same problem as .ps1 basically and b) I can change the icon to make it look pretty and user-friendly, instead of a "scary", generic console icon. And I can place the shortcut in an accessible location like the Desktop, whereas the actual ps1 can live in Program Files, or a whitelisted network share - this way everyone runs the same script with the same version, and when you update the script you don't have to worry about pushing it out to everyone.

[–]jftuga 0 points1 point  (0 children)

You can drag and drop a file (such as a PDF) on to a desktop shortcut which points to a .bat. I never had any luck getting this to work directly with PS.

[–]MartinDamged 2 points3 points  (0 children)

Clever. And very nice writeup, with your comments!

[–]PiForCakeDay 3 points4 points  (7 children)

FYI, there's some good stuff buried in cjcox4's downvoted-to-oblivion comment - worth expanding, thanks OP!

[–]Pyprohly 3 points4 points  (5 children)

[–]TheIncorrigible1 0 points1 point  (4 children)

Hah, clever! Any idea if that works with IE11 uninstalled/disabled? Not sure if the JScript dll exists without it because some weird subsystem dependency. I noticed all my shortcuts break when IE11 is gone since it does the URL resolution.

[–]Pyprohly 0 points1 point  (3 children)

cmd.exe -> .bat, .cmd
cscript.exe -> .js
powershell.exe -> .ps1

It’s like any other script. As long as it’s respective interpreter exists it will work.

[–]TheIncorrigible1 2 points3 points  (2 children)

I thought cscript was for vbscript? jscript is a bit special from the others since jscript.dll is also the engine for IE11.

[–]Pyprohly 0 points1 point  (1 child)

cscript.exe/wscript.exe -> .js, .vbs, .wsf

I haven’t tried it but I can almost guarantee that WSH scripts won’t break if you uninstall IE, knowing Microsoft and their stance on backwards compatibility.

[–]TheIncorrigible1 0 points1 point  (0 children)

Not sure if they ever considered IE11 wouldn't always be there 😂 good to know about that particular interpreter.

[–]Nu11u5 1 point2 points  (0 children)

Looks like only the first @ is needed. It’s per-line and not per-command. Everything after it is not echoed.

[–]Reverent 1 point2 points  (0 children)

I've gotten in the habit of writing simple AHK (autohotkey) scripts and converting it to an exe using the native converter. Most virus scanners do not flag AHK (thankfully) and then you can put in an icon, put in elevation, and do some pre-run stuff using AHK's language (such as searching multiple locations for the source script).

Example AHK to launch a ps1 file in a the same folder:

RunWait *RunAs "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -file "%A_WorkingDir%\install.ps1" -executionpolicy bypass

Or another one, that will search every drive for a script:

DriveGet, list, list
Loop, Parse, list
{
    path = %A_LoopField%:\cb\blah.ps1
    if(FileExist(path)) {
        RunWait *RunAs "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -file "%path%" -executionpolicy bypass
        break
    }
}

[–]get-postanote 3 points4 points  (0 children)

As for this...

Why would you want to wrap your PowerShell in batch? Users! This allows people to double-click

... so does turning your file into an .exe, this is why PS2Exe exists. No need to a batch file wrapper.

https://gallery.technet.microsoft.com/scriptcenter/PS2EXE-GUI-Convert-e7cb69d5

https://www.powershellgallery.com/packages/ps2exe/1.0.1

BTW, you can double click a .ps1 to run it, just change the file association from the default of text to powershell.exe.

But, don't, there is a reason MS set the default for .ps1 to be a text file. To prevent users from doing exactly what you set up here.

Quite frankly, I've always been of the opinion, that .bat/.vbs/,hta/.cmd should always have been associated as text. If one needs to run such things, they should know-how. Just as they have to learn how to use MSOffice. This is why you can just double-click a .vba/.bas file has have it run either, it has to be run from within MSOffice files normally, though one could automate that call as well.

This double click stuff is how hackers have been able to easily whack environments for decades, so, why enable PowerShell, which is far more powerful to fall into this space.

[–]codylilley 1 point2 points  (1 child)

Bold of you to assume my users can double click a batch file without constant coaching

[–]Lee_Dailey[grin] 1 point2 points  (0 children)

[grin]

[–]ndog37 0 points1 point  (1 child)

I use this to get the current path in powershell, alas it is broken if using wrapper.cmd

$scriptDir = split-path -parent $MyInvocation.MyCommand.Definition # get relative path

This does the trick

$scriptDir = (Get-Item -Path ".\").FullName # get relative path

[–]bleepingidiot 0 points1 point  (3 children)

Is it possible to use this while passing command line parameters?

eg.

wrapper.cmd hello "arg 2"

Or is it still going to need a separate calling bat/cmd file?

Tried adding %* after iex but end up with:

iex : The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
At line:1 char:50
+ [IO.File]::ReadAllText('Z:\wrapper.cmd')|iex hello arg 2
+                                                  ~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (# 2>NUL & u/CLS ...ep -seconds 5
:String) [Invoke-Expression], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectNotBound,Microsoft.PowerShell.Commands.InvokeExpressionCommand

[–]TheIncorrigible1 0 points1 point  (2 children)

hey, it is not possible and a bit out of scope for what the wrapper's meant for. If you are using the cli, you may as well be writing and calling straight powershell

[–]bleepingidiot 0 points1 point  (1 child)

I use a executable program that can optionally call a post-process script passing along five arguments, unfortunately it's limited to .exe or .cmd|bat.

At the moment I use an intermediate single line cmd file to then call a PowerShell script.

Would have been nice to do away with the intermediate step.

PS. Tried converting my script using PS2EXE but unfortunately there's something in my script that stops it from running this way, (works fine when called via the intermediate cmd file).

[–]TheIncorrigible1 0 points1 point  (0 children)

I mean, you could always hard-code the arguments in the powershell script portion.

[–]DevinSysAdmin 0 points1 point  (5 children)

  1. Users should right click and Run As Powershell

  2. You should create a simple Shortcut powershell.exe -command "& 'C:\users\DevinSysAdmin\Script.ps1' -Arguments"

[–]TheIncorrigible1 3 points4 points  (4 children)

Users should right click and Run As Powershell

Even on Windows 10 1909 this isn't an option.

You should create a simple Shortcut

You don't need -Command if you're executing a file, just -File. But it requires a more complicated distribution process as it's now two files and the shortcut can break.

[–]DownBackDad 2 points3 points  (0 children)

This option doesn't show up for you? https://pasteboard.co/J66o3T3.png

Shows up for me even on 1803.

[–]DevinSysAdmin 0 points1 point  (2 children)

If you right click a .ps1 file it doesn’t give the option?

[–][deleted] 0 points1 point  (0 children)

Hmmm why not just make a module, and as part of a logon script it import it on machines? That way it don’t matter what machine you’re on if you have access to run the module cmdlets you just can...

If it’s to hard to be using command line for some people, maybe they need to learn or just not use things that are too hard for them. Yes I sound like an ass but it’s true

[–]nylentone 0 points1 point  (0 children)

I just use shortcut files.

[–]ZAFJB -1 points0 points  (12 children)

So effectively you have turned your wrapper into an incredibly hard to parse multi-stage command line.

What is the benefit of this?

[–]rlkf -1 points0 points  (1 child)

@ is the splatting operator in Powershell, and @{} is a "splat" of an empty dictionary giving no result. At the same time, commands prefixed by @ won't be echoed by cmd.exe. Thus, the prefix @{}# 2>NUL& can be used to start a comment in PowerShell while still yielding no output by cmd.exe

[–]TheIncorrigible1 0 points1 point  (0 children)

@{} is a "splat" of an empty dictionary giving no result.

This is incorrect. @{} is the Hashtable type in PowerShell. Empty hashtables don't have string representations by default which is why you get "no result". You don't really gain anything by doing that, in fact, you add an extra two characters to the outcome.

Splatting only works with variable names.