3

Based on a related question and some Googling/testing, it is pretty clear that the maximum length of a command line in cmd.exe is 8,191 characters. However...my script seems to successfully exceed that length, and I can't figure out why it works.

So I have:

  • A script written/saved as a .ps1
  • I'm converting it to a .bat using the method from Converting PowerShell to Batch
  • If I run the .bat - there don't seem to be any issues
  • If I try to echo the $encoded command or just remove @echo off before running the .bat - it is easy to see that the length/content of $encoded has been truncated...yet it works as expected

I realize this is a bit of a strange question since I'm trying to ascertain some kind of hidden truth about something that works vs. something that is broken, but I'd like to understand why it is working!

Note: The primary reason I'm creating a .bat for my script is because I have a number of small 'programs' that I need to run on my work computer and share with others. Our company's execution policies don't allow running scripts, and it is much easier for others to use something I've written if it is a .bat since they won't need to understand/use PowerShell. They can just place the .bat in the right place, give it two clicks, and then sit back and relax until something happens.

EDIT: Here's the truncated command in the cmd.exe window. If I convert from Base64 back to readable text, it is clearly missing about half of the script. Yet - I'm not noticing any problems Truncated Command

aschipfl
  • 33,626
  • 12
  • 54
  • 99
immobile2
  • 489
  • 2
  • 15
  • 3
    The script can be a million characters long, but the _command line_ (including arguments) cannot exceed 8191 chars. Even though your script is 10K chars, the command line for the process will still just be `powershell -File myscript.ps1` <- which in itself is obviously under the limit – Mathias R. Jessen Sep 23 '21 at 13:03
  • Right - but in this case, I'm not calling a `.ps1` because it wouldn't be able to run, so here are the first 200 or so characters from my command line: `powershell.exe -NoExit -encodedCommand ZgB1AG4AYwB0AGkAbwBuACAAVwByAGkAdABlAC0ASABvAHMAdABQAHIAbwBtAHAAdAAgAHsADQAKACAAIAAgACAAWwBDAG0AZABsAGUAdABCAGkAbgBkAGkAbgBnACgAKQBdAA0ACgAgACAAIAAgAHAAYQByAGEA...` it goes on and it looks like the full string is `14,488` which should be well over the limit – immobile2 Sep 23 '21 at 13:16
  • 2
    And you're launching this 14488 char long command directly from `cmd.exe`? – Mathias R. Jessen Sep 23 '21 at 13:19
  • It is a .bat so the code in the .bat itself is: `@echo off powershell.exe -NoExit -encodedCommand ZgB1AG4AYwB0AGkAbwBuACAAVwByAGkAdABlAC0ASABvAHMAdABQAHIAbwBtAHAAdAAgAHsADQAKACAAIAAgACAAWwBDAG0AZABsAGUAdABCAGkAbgBkAGkAbgBnACgAKQBdAA0ACgAgACAAIAAgAHAAYQByAGEAbQANAAoAIAAgACAAIAAoAA0ACgAgACAAIAAgACAAIAAgACAAIwAgAEMAYQBwAHQAaQBvAG4AIAB0AG8AIABwAHIAZQBjAGUAZQBk...` you get the idea. I'll add a screenshot, but if you remove @echo off then you can clearly see the prompt cutoff early (and I decoded what was there, it was only part of the script), yet it seems to work fine – immobile2 Sep 23 '21 at 15:02

1 Answers1

5
  • The 8191-character limit applies to the interactive command line and calls to cmd.exe's CLI (via cmd /c)

  • It does not apply to commands invoked from batch files - there the limit is close to[1] 32KiB (32,768) characters.

    • However, it still seems to apply selectively to cmd.exe's internal commands, such as echo; by contrast, a call to an external program such as powershell.exe is not affected (though it is possible for specific external programs to have their own, lower limits).

As an aside:

As clever as the linked method of converting PowerShell scripts to batch files is, the resulting batch files lack support for arguments to be passed through to the PowerShell code, and adding support for that would be impractical (it would require Base64-encoding the arguments too, at invocation time, which isn't possible from a batch file except with the help of non-built-in utilities).

  • If your script / batch file is written to either require no arguments on invocation or to prompt for them interactively (as in your case), this limitation is not a concern.

  • However, you may want argument support for any of the following reasons:

    • To allow calling the batch file from cmd.exe (Command Prompt) with arguments.
    • To allow calling with arguments from other environments that support passing arguments, notably Task Scheduler and the Windows Run dialog (WinKey-R).
    • To make the batch file support drag-and-drop (which implicitly passes the dropped files' paths as arguments).

As for the overall reason to wrap PowerShell code in batch files:

  • Batch files (.cmd, .bat) can be launched directly, as if they were executables, system-wide.
  • By contrast, this is not supported for PowerShell scripts (.ps1), which from outside PowerShell must be invoked via an explicit call to the PowerShell CLI; note that systems may be configured to categorically prevent execution of .ps1 scripts - see next section.

If you do need argument support in your batch file, the best - but inconvenient - solution is to distribute two files:

  • the original *.ps1 file...
  • and a companion batch file, with the same base file name, to be placed in the same directory - which can be invoked from outside PowerShell - whose sole purpose is to invoke the *.ps1 file via powershell.exe with pass-through arguments.

E.g. foo.cmd would accompany foo.ps1, with the following - invariant - content:

@powershell.exe -NoProfile -File "%~dpn0.ps1" %*

Note:

  • Important: The above assumes that the effective PowerShell execution policy allows execution of script files (.ps1).

    • If you cannot make this assumption, place -ExecutionPolicy RemoteSigned before -NoProfile above (or, to deactivate all checks, -ExecutionPolicy Bypass), but note that an execution policy based on GPO (Group Policy Objects) may still prevent script execution.
  • To call PowerShell (Core) 7+ instead, use pwsh.exe instead of powershell.exe.

  • %~dpn0 expands to the full path of the enclosing batch file itself without its filename extension; appending .ps1 therefore targets the companion PowerShell script in the same directory; run cmd /c call /? for an explanation of the syntax.

  • %* passes any and all arguments received by the batch file on.


[1] In practice, the limits appear to be: 32,767 characters in batch files, and - lower by 3 chars. - 32,764 for Powershell (both on the command line / via the CLI and in *.ps1 scripts). In PowerShell, the limit may in practice be even lower than that, because PowerShell expands the names of executables it locates via $env:PATH to their full paths in the command lines constructed behind the scenes, which cmd.exe doesn't do.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Could you possibly expand on the content of `foo.ps1` or how arguments would get passed to and through `foo.cmd` in your example? I know that `%*` will expand to represent all arguments passed to `foo.cmd`; but does the header of `foo.ps1` need to parse `arg[n]`? Also, how/when would arguments be passed to `foo.cmd` in the first place? Dropping files onto a `.bat` is one way, but alternatively - would this method imply calling `foo.cmd`/`foo.bat` from a shell with arguments to begin with? If so, what does that look like and, importantly, how does `foo.ps1` handle the expanded form of `%*`? – immobile2 Oct 06 '21 at 14:35
  • @immobile2, please see my update, also re execution policies. Re `%*`: It is just a pass-through mechanism, so your `.ps1` file should handle arguments the way it always does, as in direct invocation (use `$args` if you don't want to declare parameters explicitly, but preferably use a `param(...)` block to declare them) - see [the docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Functions#functions-with-parameters) – mklement0 Oct 06 '21 at 15:25