122

For years, I have used the cmd/DOS/Windows shell and passed command-line arguments to batch files. For example, I have a file, zuzu.bat and in it, I access %1, %2, etc. Now, I want to do the same when I call a PowerShell script when I am in a Cmd.exe shell. I have a script, xuxu.ps1 (and I've added PS1 to my PATHEXT variable and associated PS1 files with PowerShell). But no matter what I do, I seem unable to get anything from the $args variable. It always has length 0.

If I am in a PowerShell shell, instead of cmd.exe, it works (of course). But I'm not yet comfortable enough to live in the PowerShell environment full time. I don't want to type powershell.exe -command xuxu.ps1 p1 p2 p3 p4. I want to type xuxu p1 p2 p3 p4.

Is this possible, and if so, how?

The sample I cannot get to work is trivial, foo.ps1:

Write-Host "Num Args:" $args.Length;
foreach ($arg in $args) {
    Write-Host "Arg: $arg";
}

The results are always like this:

C:\temp> foo
Num Args: 0
C:\temp> foo a b c d
Num Args: 0
c:\temp>
1
  • It works for me. You probably need to put the full path of the script. Commented Aug 14, 2024 at 17:24

9 Answers 9

53

This article helps. In particular, this section:

-File

Runs the specified script in the local scope ("dot-sourced"), so that the functions and variables that the script creates are available in the current session. Enter the script file path and any parameters. File must be the last parameter in the command, because all characters typed after the File parameter name are interpreted as the script file path followed by the script parameters.

i.e.

powershell.exe -File "C:\myfile.ps1" arg1 arg2 arg3

means run the file myfile.ps1 and arg1 arg2 & arg3 are the parameters for the PowerShell script.

Sign up to request clarification or add additional context in comments.

4 Comments

This still doesn't help with what the op wants ("I want to type xuxu p1 p2 p3 p4").
but what about the gaps? if arg1 it's a path with space.
@AndreiKrasutski The escaping mechanism would depend on how the script is being executed. Since it's expressed as "powershell.exe" I would assume that this would be run in a CMD.EXE (Windows) shell. In that case, you can just put quotes around the path, in the same way that the "C:\myfile.ps1" is escaped. Following the same example: powershell.exe -File "C:\myfile.ps1" "arg1a arg1b" arg2 arg3 would return: Num Args: 3 Arg: arg1a arg1b \r\n Arg: arg2 \r\n Arg: arg3 (I can't put new lines in a comment)
@BenKeene Unfortunately, -File breaks down when the arguments contain a space AND a trailing backslash. For example: powershell.exe -file "C:\myfile.ps1" "C:\F 1\" "C:\F 2\" returns: C:\F 1" C:\F \r\n 2"
32

After digging through the PowerShell documentation, I discovered some useful information about this issue. You can't use the $args if you used the param(...) at the beginning of your file; instead you will need to use $PSBoundParameters. I copy/pasted your code into a PowerShell script, and it worked as you'd expect in PowerShell version 2 (I am not sure what version you were on when you ran into this issue).

If you are using $PSBoundParameters (and this ONLY works if you are using param(...) at the beginning of your script), then it is not an array, it is a hash table, so you will need to reference it using the key / value pair.

param($p1, $p2, $p3, $p4)
$Script:args=""
write-host "Num Args: " $PSBoundParameters.Keys.Count
foreach ($key in $PSBoundParameters.keys) {
    $Script:args+= "`$$key=" + $PSBoundParameters["$key"] + "  "
}
write-host $Script:args

And when called with...

PS> ./foo.ps1 a b c d

The result is...

Num Args:  4
$p1=a  $p2=b  $p3=c  $p4=d

9 Comments

This doesn't account for OP starting his command line with powershell.exe or pwsh. The behavior changes when OP does that.
@EricHansen I don't know what you mean, I get the same result either way: ` Powershell>powershell.exe .\ParamTest.ps1 val1 val2 val3 val4 Num Args: 4 $p1=val1 $p2=val2 $p3=val3 $p4=val4 `
@RandallBrock The behavior of the arguments changes for me. If I'm in CMD/batch, and I do something like pwsh .\ParamTest.ps1 -arg1 val1 -listOfArgs val2 val3 val4, it really doesn't like that. On the other hand, if I'm in PowerShell and do .\ParamTest.ps1 -arg1 val1 -listOfArgs val2 val3 val4, that works like I'd expect. I've heard that this is how it's intended to function due to "security reasons."
@EricHansen I wonder if it is a version thing. For me, pwsh launches 6.2.0, but powershell.exe launches 5.1.17134.858, both of which produce the same results listed: Powershell>pwsh .\ParamTest.ps1 val1 val2 val3 val4 yields: Num Args: 4 $p1=val1 $p2=val2 $p3=val3 $p4=val4
@Timo I don't know what you are doing exactly, but param is a language construct, however it must be the first thing in the file. Did you declare a variable or something else before it? More info here: learn.microsoft.com/en-us/powershell/module/…
|
19

OK, so first this is breaking a basic security feature in PowerShell. With that understanding, here is how you can do it:

  1. Open an Windows Explorer window
  2. Menu Tools -> Folder Options -> tab File Types
  3. Find the PS1 file type and click the advanced button
  4. Click the New button
  5. For Action put: Open
  6. For the Application put: "C:\WINNT\system32\WindowsPowerShell\v1.0\powershell.exe" "-file" "%1" %*

You may want to put a -NoProfile argument in there too depending on what your profile does.

8 Comments

I think the key is in your step 6 where you pass in the parameters to powershell.exe. Daniel says he has associated PS1 files with PowerShell but that doesn't pass arguments without the extra %1 %* specs. Also note that the -File parameter is not available in V1. It is new to V2.
Good catch regarding the -file parameter I had forgotten that.
I'll have to install V2 before I can try your suggestion. Thanks. When you say this is breaking a basic security feature, what does "this" you mean? Calling a PowerShell script from Cmd.exe as though it were a .com/.bat/.exe file? Passing parameters to the script?
Sorry I should have been more clear. The calling the script without explicitly calling powershell.exe. I'm not saying it is a significant security feature for you personally and it is security through obscurity which I am not always a fan of anyway.
To add to EBGreen's comment, the basic security problem that PowerShell's tries to avoid is folks double-clicking on PS1 files attached to email and having the script run. That's why PS1 files are only associated with an editor by default. Microsoft really doesn't want a PowerShell version of the ILoveYou virus e.g. "LOVE-LETTER-FOR-YOU.TXT.ps1"
|
15

You could declare your parameters in the file, like param:

[string]$param1
[string]$param2

And then call the PowerShell file like so .\temp.ps1 param1 param2....param10, etc.

(For more information, see here, e.g.: https://www.red-gate.com/simple-talk/sysadmin/powershell/how-to-use-parameters-in-powershell/#heading-1)

Comments

6

Maybe you can wrap the PowerShell invocation in a .bat file like so:

rem ps.bat
@echo off
powershell.exe -command "%*"

If you then placed this file under a folder in your PATH, you could call PowerShell scripts like this:

ps foo 1 2 3

Quoting can get a little messy, though:

ps write-host """hello from cmd!""" -foregroundcolor green

2 Comments

+1 for showing the triple quotes. that had me stuck for a while.
Just for completeness - regarding quoting (and passing tab chars & interoperability); if you have a 'SetTabEV.cmd' named Command Prompt script, to set a "Tab" named environment variable to be set to/have a value of a tab character - 'Set Tab= ', run the script, then you can also use it with 4 quotes; e.g. from the Command Prompt: ``` SetTab.cmd powershell MyScript.ps1 -dol """"1%Tab%Dennis%Tab%2047"""" ``` But only 3/triple-quoting is required if you use/pass the PowerShell tab character escape-sequence instead - 't': ``` powershell MyScript.ps1 -dol """1tDennis`t2047""" ```
1

if you want to invoke ps1 scripts from cmd and pass arguments without invoking the script like

powershell.exe script.ps1 -c test
script -c test ( wont work )

you can do the following

setx PATHEXT "%PATHEXT%;.PS1;" /m
assoc .ps1=Microsoft.PowerShellScript.1
ftype Microsoft.PowerShellScript.1=powershell.exe "%1" %*

This is assuming powershell.exe is in your path

https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ftype

1 Comment

Worth noting that this requires running the cmd prompt as Administrator. Also, I am struggling to actually make it work on Windows 10 version 1903 (18362.778) - all commands run successfully but the arguments still don't get passed in. I think wrapping with a .bat file is the most portable solution.
1

You may not get "xuxu p1 p2 p3 p4" as it seems. But when you are in PowerShell and you set

PS > Set-ExecutionPolicy Unrestricted -Scope CurrentUser

You can run those scripts like this:

./xuxu p1 p2 p3 p4

or

.\xuxu p1 p2 p3 p4

or

./xuxu.ps1 p1 p2 p3 p4

I hope that makes you a bit more comfortable with PowerShell.

Comments

0

I wanted to automate the "Unblocking" of files downloaded from the internet in Windows, so I got my .ps1 file working, but when I wanted to make it as a "right-click" menu option, it never worked despite trying all possible combinations (passing params to cmd alone, then to powershell alone and to ps1, using single/double quotes, etc) !

After struggling for some hours, I was able to make it work by using both cmd and powershell in the same command like this :

cmd.exe /c start powershell "C:\folder\script.ps1 -folder '%1'"

where folder is a parameter referring to the right-clicked folder, and here is the ps1 file if someone is interested :

param (
    [string]$folder = "$env:userprofile\Downloads"
    #defaults to the current user Downloads folder
)
# Remove Recurse if you don't want to unblock inner folders
get-childitem "$folder" -Recurse | unblock-file

You can then make a .reg file to add this as a right-click option to unblock all files at once :

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Directory\shell\UnblockDL]
@="Unblock DL Files"

[HKEY_CLASSES_ROOT\Directory\shell\UnblockDL\command]
@="cmd.exe /c start powershell \"C:\\folder\\script.ps1 -folder '%1'\""

Remember to adjust "C:\folder\script.ps1" to your own ps1 file, and make sure its name/path don't contain any spaces (don't ask me why !).

Comments

0

I also wanted to do this, and I also wanted to be able to carefully control which python virtual environment is used when whatever script I call actually runs. The solution below, although rather extensive, or ridiculous, it may be, it should work with any number of arguments or flags passed to the python script, handling things appropriately and returning an appropriate execution status. The python script should run inside a virtual python environment it will work in and leave the current environment unscathed once completed. Also, when the project directory is on your path it will also still execute in your current working directory seamlessly, just like any other command.

If you need or want to see what the power shell script is really doing, modify the setting $VerbosePreference to your needs. This also happens to turn on debugging in the activate.ps1 script as well, which is rather handy.

I did have to change the windows execution policy on my system to get this to work, however you have to do that anyway if you want to run the .\venv\scripts\activate.ps1 script.

The least intrusive, and least permanent way I've found so far to do this, is to just set the current power shell you are using with Set-ExecutionPolicy Unrestricted -Scope Process. Which can go in a specific terminal initialization file and probably needs some admin rights for that. There are also forms of this command that allow scripts to run in a more sweeping, and more permanent ways as well. I would recommend running a form of this command over editing the registry directly. For more info on that see: 'virtualenv' won't activate on Windows.

I definitely recommend using a virtual environment, but that means the script should use the correct one when it runs, particularly if it needs packages installed that other virtual environments do not. I use python 3.x and here is an example of is how I always build all of mine:

mkdir xuxu ;cd xuxu 
python -m venv venv # creates venv environment named venv in current directory .\venv\Scripts\activate.ps1 # activates local venv for this project
 python -m pip install --upgrade pip # Important - Upgrade your pip first!
 pip install [modules for project]

The solution depends on this being true and also that all of your venv environments will be named 'venv' and live in a subdirectory of the location of your xuxu.ps & xuxu.py. It does not support Conda or other python environment managers.

The thing about these venv environment scripts is they change the environment of the current shell and child shells. We don't want to have two environments conflicting with each other and we don't want the script to leave the current environment polluted with its runtime environment. Therefore, the only clean way I could think of, and there may be a better one, is to have two modes the script can work with a virtual environment.

This is done using one of two modes determined by setting the variable $venvEnvironmentPreference to either useExisting or isolated. In useExisting mode, if there is an active venv, it will use it and won't deactivate it after it's finished. If you don't have a current venv, it will look for a venv in the subdirectory under the directory the script is in, which needs to be on the path if it's not the current directory.

In isolated mode, if you are in a different venv than the xuxu venv, it will deactivate that, then activate the xuxu venv, run xuxu.py with its own specific venv, and then deactivate it and reactivate the venv you were originally in.

In either mode, if you are not in a venv when you execute it, it will use the xuxu venv and deactivate it when completed, leaving your power shell environment untainted.

With this solution, you'll need a xuxu.ps1 like this for each command xuxu.py you have, or you won't be able to xuxu --flags arg1..argN. and everyone wants to xuxu without being bothered, including me. The other reason is you may want to have specific differences in these scripts as they may be different depending on what you are doing with them at the time, or other specific requirements.

Also, if the xuxu directory is not on your path and that is where you are when you run it, then you will need to add .\ on your path or you will still have to issue: .\xuxu ..args.. and nobody wants that. A more secure and less sweeping option is to add it to the path temporarily for the current shell and all children with something nonexplosive either when you first get started, or in your shell initialization script like this:

 $currentDir = $PWD.Path if (($env:PATH -split ';') -notcontains $currentDir) { $env:PATH = "$env:PATH:$currentDir" }
# xuxu.ps1 - PowerShell execution wrapper for xuxu.py

# --- Configuration ---
#$venvEnvironmentPreference = "useExisting" 
$venvEnvironmentPreference = "isolated" 

# $VerbosePreference = "SilentlyContinue"
$VerbosePreference = "Continue" # Enable verbose output for debugging

# --- Script Start ---
$ScriptDir = $PSScriptRoot
$PythonScriptPath = Join-Path -Path $ScriptDir -ChildPath "xuxu.py"
$VenvDir = Join-Path -Path $ScriptDir -ChildPath "venv"
$VenvActivateScript = Join-Path -Path $VenvDir -ChildPath "Scripts\Activate.ps1"

$invokedLocalVenv = $false
$originalVenvPath = $null



# Check if the Python script exists first
if (-not (Test-Path $PythonScriptPath -PathType Leaf)) {
    Write-Error "Error: Python script not found at '$PythonScriptPath'."
    exit 1
}

# --- Virtual Environment Handling ---

Write-Verbose "Virtual environment preference set to: '$venvEnvironmentPreference'"

# Check if the local venv activation script exists (needed for both modes eventually)
if (-not (Test-Path $VenvActivateScript -PathType Leaf)) {
    Write-Error "Error: Local virtual environment activation script not found at '$VenvActivateScript'."
    Write-Error "Please ensure a virtual environment named 'venv' exists in the script directory: $ScriptDir"
    exit 1
}

# Handle based on preference
if ($venvEnvironmentPreference -eq "isolated") {
    Write-Verbose "Mode: Isolated. Preparing local venv."
    $originalVenvPath = $env:VIRTUAL_ENV

    if ($originalVenvPath) {
        Write-Verbose "External virtual environment detected: $originalVenvPath. Attempting deactivation."
        if (Get-Command deactivate -ErrorAction SilentlyContinue) {
            try {
                deactivate
                Write-Verbose "Deactivated external venv successfully."
                 if ($env:VIRTUAL_ENV) { Remove-Item Env:\VIRTUAL_ENV }
            } catch {
                Write-Warning "Warning: Failed to deactivate the external virtual environment. Attempting to proceed. Details: $($_.Exception.Message)"
            }
        } else {
            Write-Warning "Warning: 'deactivate' command not found. Cannot deactivate external venv '$originalVenvPath' automatically. Attempting to proceed."
        }
    } else {
        Write-Verbose "No external virtual environment detected."
    }

    Write-Verbose "Attempting to activate local venv: $VenvActivateScript"
    try {
        . $VenvActivateScript
        $invokedLocalVenv = $true
        Write-Verbose "Local venv activated successfully for isolated mode."
    } catch {
        Write-Error "Error activating the local virtual environment for isolated mode. Details: $($_.Exception.Message)"
        exit 1
    }

} elseif ($venvEnvironmentPreference -eq "useExisting") {
    Write-Verbose "Mode: useExisting. Checking for active venv."
    if ($env:VIRTUAL_ENV) {
        Write-Verbose "External virtual environment detected: $env:VIRTUAL_ENV. Using existing venv."
        $currentVenvPathNormalized = $null
        $localVenvPathNormalized = $null
        try { $currentVenvPathNormalized = (Resolve-Path $env:VIRTUAL_ENV).Path } catch {}
        try { $localVenvPathNormalized = (Resolve-Path $VenvDir).Path } catch {}
        if ($currentVenvPathNormalized -and $localVenvPathNormalized -and ($currentVenvPathNormalized -eq $localVenvPathNormalized)) {
             Write-Verbose "The active virtual environment appears to be the local one."
        }
    } else {
        Write-Verbose "No external virtual environment detected. Attempting to activate local venv."
        try {
            . $VenvActivateScript
            $invokedLocalVenv = $true
            Write-Verbose "Local venv activated successfully for useExisting mode."
        } catch {
            Write-Error "Error activating the local virtual environment. Details: $($_.Exception.Message)"
            exit 1
        }
    }
} else {
    Write-Warning "Warning: Invalid value '$venvEnvironmentPreference' for \$venvEnvironmentPreference. Defaulting to 'useExisting'."
    if ($env:VIRTUAL_ENV) {
        Write-Verbose "External virtual environment detected: $env:VIRTUAL_ENV. Using existing venv."
    } else {
        Write-Verbose "No external virtual environment detected. Attempting to activate local venv."
        try {
            . $VenvActivateScript
            $invokedLocalVenv = $true
            Write-Verbose "Local venv activated successfully (defaulted from invalid preference)."
        } catch {
            Write-Error "Error activating the local virtual environment. Details: $($_.Exception.Message)"
            exit 1
        }
    }
}

# --- Execute Python Script ---
$pythonExitCode = 1
try {
    Write-Verbose "Executing: python ""$PythonScriptPath"" $args"
    $command = "python ""$PythonScriptPath"" $args"
    Invoke-Expression $command
    $pythonExitCode = $LASTEXITCODE
    Write-Verbose "Python script finished with exit code: $pythonExitCode"
} catch {
    $ErrorMessage = if ($_.Exception) { $_.Exception.Message } else { $_ }
    Write-Error "Error executing Python script '$PythonScriptPath'. Details: $ErrorMessage"
}

# --- Cleanup ---
$reactivationNeeded = $false
$originalVenvActivateScript = $null

if ($venvEnvironmentPreference -eq "isolated" -and $originalVenvPath) {
    $reactivationNeeded = $true
    $originalVenvActivateScript = Join-Path -Path $originalVenvPath -ChildPath "Scripts\Activate.ps1"
    Write-Verbose "Original venv path stored: $originalVenvPath. Reactivation will be attempted using: $originalVenvActivateScript"
}

if ($invokedLocalVenv) {
    Write-Verbose "Attempting to deactivate locally activated virtual environment."
    if (Get-Command deactivate -ErrorAction SilentlyContinue) {
        try {
            deactivate
            Write-Verbose "Local venv deactivated."
            if ($env:VIRTUAL_ENV) { Remove-Item Env:\VIRTUAL_ENV }
        } catch {
            Write-Warning "Warning: 'deactivate' command failed for the local venv. The environment might be in an inconsistent state. Details: $($_.Exception.Message)"
            $reactivationNeeded = $false
        }
    } else {
        Write-Warning "Warning: 'deactivate' command not found after script execution. Cannot deactivate local venv automatically."
        $reactivationNeeded = $false
    }
} else {
    Write-Verbose "Skipping deactivation (script did not activate the local venv)."
}

if ($reactivationNeeded) {
    Write-Verbose "Attempting to reactivate original virtual environment: $originalVenvPath"
    if ($originalVenvActivateScript -and (Test-Path $originalVenvActivateScript -PathType Leaf)) {
        try {
            . $originalVenvActivateScript
            Write-Verbose "Original virtual environment reactivated successfully."
             if ($env:VIRTUAL_ENV -ne $originalVenvPath) {
                 Write-Verbose "Warning: VIRTUAL_ENV variable might not be correctly restored after reactivation."
             }
        } catch {
            Write-Warning "Error reactivating the original virtual environment '$originalVenvPath' using '$originalVenvActivateScript'. You may need to reactivate it manually. Details: $($_.Exception.Message)"
        }
    } else {
        Write-Warning "Cannot find the assumed activation script '$originalVenvActivateScript' for the original virtual environment '$originalVenvPath'. You may need to reactivate it manually."
    }
} else {
     Write-Verbose "Reactivation of an original venv was not required or not possible."
}

Write-Verbose "Script finished. Exiting with Python script's exit code: $pythonExitCode"
exit $pythonExitCode

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.