Never write a batch wrapper again

[deleted]

84 comments · 97% upvoted · other discussions

comments

rmbolger

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

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

[deleted]

[deleted]

eloi

So... Where do you feed it a PowerShell script to run?

TheIncorrigible1

The script goes after the comment. I left 'Hello, World!' to illustrate that, but maybe that's not clear enough.

eloi

Are you saying that you put your PowerShell script lines below the first line, inside the wrapper.cmd file? Doesn't CMD.exe attempt to execute the PowerShell code after PowerShell closes?

redog

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

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

Baerentoeter

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

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

Baerentoeter

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

TheIncorrigible1

yessir

bywaterloo

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

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

dextersgenius

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

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

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

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.

[deleted]

[deleted]

[deleted]

[deleted]

azjunglist05

I write a large portion of scripts and turn them into services or executables, and then commit the source code to a Git repository in Azure DevOps.

In that repo I also describe what application was used to create the .exe

If I ever get hit by a bus anyone with any PowerShell skills can debug and solve any issues.

[deleted]

[deleted]

azjunglist05

I definitely misstated my intent! While I commit all scripts I write to a Git repo — I don’t make all scripts a service or an .exe, as you’re right, that’s an incredible waste of time if it’s for a co-worker or someone knowledgeable.

I also don’t commit the .exe to Git — I commit the original PowerShell script, and in the Read Me I detail what application was used to make it an .exe if that was originally how we ended up using that script.

However, using a Git workflow should never be seen as a waste of time even if the underlying code/process is tedious.

jftuga

Are you using GitHub?

If so, have you ever considered publishing the .exe files to the releases section?

jftuga

How do you turn them into services? That sounds great.

TheD4rkSide

But with PS2EXE you can convert a PowerShell made EXE back to a .ps1 and figure out what’s under the hood. It’s as simple to convert the EXE back as it is to make it in the first place.

just_looking_around

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

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

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

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

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

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

TheIncorrigible1

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

n_md

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

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

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

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

Nu11u5

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

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

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

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

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

Clever. And very nice writeup, with your comments!

PiForCakeDay

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

[deleted]

[deleted]

ZAFJB

There is absolutely no difference whether you use .bat or .cmd if you are executing under cmd.exe.

The only purpose of .cmd was to to prevent script using new features of cmd.exe from being executed under command.com.

TheIncorrigible1

Thanks for the additional info. It was my workplace's habit to use .cmd vs .bat for clarity I guess.

ZAFJB

Clarity in what?

Command.com is dead and gone. Everything executes under cmd.exe.

There is no point in bothering about which extension is 'correct'.

TheIncorrigible1

When your environment was (might still be; thankfully nothing older than 2008R2) a mix of XP and 7, there certainly was.

nascentt

thankfully nothing older than 2008R2

/Cries

I know you meant 2003, but 2008 is going alive and strong (pre r2) unfortunately.

TheIncorrigible1

2008 is going alive and strong (pre r2) unfortunately.

RIIIIP

Pyprohly
TheIncorrigible1

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

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

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

Pyprohly

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

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

Nu11u5

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

Reverent

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
    }
}
[deleted]

[deleted]

TheIncorrigible1

It's possible your admins have blocked the alias since it appears in many malicious scripts.

get-postanote

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

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

Lee_Dailey

[grin]

ndog37

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

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

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

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

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

DevinSysAdmin
  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

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

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

Shows up for me even on 1803.

DevinSysAdmin

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

[deleted]

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

I just use shortcut files.

ZAFJB

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

What is the benefit of this?

[deleted]

[deleted]

ZAFJB

Hardly no dependencies.

It depends on something to hold and deliver your complex command line.

And it is dependant on cmd.exe, You cannot arbitrarily execute that command line.

[deleted]

[deleted]

ZAFJB

That's why it's called a wrapper. It's still a script, just one you can double-click.

I though you said never write a wrapper again....

rlkf

@ 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

@{} 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.

cjcox4

But, does it prevent multiple postings to reddit? No.

[deleted]

[deleted]

subsetsum

I appreciate the breakdown of the script. Trying to learn powershell now. Thanks!

Beauregard_Jones

You're doing good work. Thank you for the wrapper. Thank you for cross-posting. Thank you for recognizing that others may benefit from your work, and sharing it with the community.

/u/cjcox4 could have taken the higher road, and ignored your post. Instead, s/he made a conscious decision to be rude and hurtful. What a sad life one must lead to find joy in hurting others.

crackguy

You're an idiot

Beauregard_Jones

Some people find joy in building people up. Some people find joy in tearing them down.