开发者

Drag and Drop to a Powershell script

I thought I had an answer to this, but the more I play with it, the more I see it as a design flaw of Powershell.

I would like to drag and drop (or use the Send-To mechanism) to pass multiple files and/or folders as a array to a Powershell script.

Test Script

#Test.ps1
param ( [string[]] $Paths, [string] $ExampleParamete开发者_StackOverflow中文版r )
"Paths"
$Paths
"args"
$args

Attempt #1

I created a shortcut with the following command line and dragged some files on to it. The files come across as individual parameters which first match the script parameters positionally, with the remainder being placed in the $args array.

Shortcut for Attempt #1

powershell.exe -noprofile -noexit -file c:\Test.ps1

Wrapper Script

I found that I can do this with a wrapper script...

#TestWrapper.ps1
& .\Test.ps1 -Paths $args

Shortcut for Wrapper Script

powershell.exe -noprofile -noexit -file c:\TestWrapper.ps1

Batch File Wrapper Script

And it works through a batch file wrapper script...

REM TestWrapper.bat
SET args='%1'
:More
SHIFT
IF '%1' == '' GOTO Done
SET args=%args%,'%1'
GOTO More
:Done
Powershell.exe -noprofile -noexit -command "& {c:\test.ps1 %args%}"

Attempted Answers

Keith Hill made the excellent suggestion to use the following shortcut command line, however it did not pass the arguments correctly. Paths with spaces were split apart when they arrived at the Test.ps1 script.

powershell.exe -noprofile -noexit -command "& {c:\test1.ps1 $args}"

Has anyone found a way to do this without the extra script?


I realize this question is a couple of years old now, but since it's still the top result for a lot of Google queries relating to running PowerShell via an Explorer drag-and-drop I figured I'd post what I came up with today in the hopes it helps others.

I was wanting to be able to drag-and-drop files onto PowerShell (ps1) scripts and couldn't find any good solutions (nothing that didn't involve an extra script or shortcut). I started poking around the file associates in the Registry, and I came up with something that seems to work perfectly.

First we need to add a DropHandler entry for .PS1 files so that Explorer knows PS1 files should accept drag-drop operations. Do that with this Registry change. You'll probably have to create the ShellEx and DropHandler subkeys.

HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\ShellEx\DropHandler\
(Default) = {60254CA5-953B-11CF-8C96-00AA00B8708C}

This drop handler is the one used by Windows Scripting Host. It's my first choice because it supports long filenames. If you run into trouble with spaces or something else, you can try the standard Shell32 executable (.exe, .bat, etc) drop handler: {86C86720-42A0-1069-A2E8-08002B30309D}.

Both of these drop handlers work by simply invoking the default verb (what happens when you double-click a file) for the file type (seen as the (Default) value of the Shell key). In the case of PS1 files, this is Open. By default this verb displays the PowerShell script in Notepad -- not very helpful.

Change this behavior by modifying the Open verb for PS1 files:

HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\Open\Command\
(Default) = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -NoExit -File "%1" %*

This will run the script in PowerShell when opened as well as pass it all the parameters (dropped files). I have it set to stay open after the script completes, but you can change this by removing the -NoExit option.

That's it. I haven't done any extensive testing, but so far it seems to be working very well. I can drop single files/folders as well as groups. The order of the files in the parameter list isn't always what you'd expect (a quirk of how Explorer orders selected files), but other than that it seems ideal. You can also create a shortcut to a PS1 file in Shell:Sendto, allowing you to pass files using the Send To menu.

Here's both changes in REG file format:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\ShellEx\DropHandler]
@="{60254CA5-953B-11CF-8C96-00AA00B8708C}"

[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\Open\Command]
@="\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoExit -File \"%1\" %*"

Notes:

  • As a result of these changes double-clicking a PS1 file in Explorer will execute the script; to edit instead of open you'll have to use the right-click menu. And just as a suggestion (sadly learned from bitter experience :), you might consider a confirmation/sanity-check guard if you have scripts which take damaging actions.

  • Scripts run via drag-drop actions will have a default starting working directory of C:\Windows\system32\. Another reason to be careful.

  • Remember you will need to change your Execution Policy (Set-ExecutionPolicy) unless you're using signed scripts.

  • If you have adjusted handling of .PS1 files on Windows 10, you need to delete the registry key HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice which will override the default handling.


The easiest way to pass files or folders to a Powershell script is a wrapper script like the following:

@echo off
rem the name of the script is drive path name of the Parameter %0
rem (= the batch file) but with the extension ".ps1"
set PSScript=%~dpn0.ps1
set args=%1
:More
shift
if '%1'=='' goto Done
set args=%args%, %1
goto More
:Done
powershell.exe -NoExit -Command "& '%PSScript%' '%args%'"

All you have to do is

make a copy of the .bat File

give it the same name as the script file but with the extension .bat

For example "hello.ps1" <--> "hello.bat"

Drop the files/folders onto the batch file and it will pass them to the script.

A sample script code may look like this:

"hello world"
$args
$args.Gettype().Fullname


The secret all this time, since it was made available, has been the parameter attribute "ValueFromRemainingArguments".

Shortcut, Batch or CMD file to receive the dropped files

Version 5.1 or earlier

powershell.exe -noexit -file c:\Test.ps1

Powershell 6 and newer

pwsh.exe -noexit -file c:\Test.ps1

Test.ps1

[CmdletBinding()]
param (
    [Parameter(ValueFromRemainingArguments=$true)]
    $Path
)

$Path

TestExpandDirectories.ps1

[CmdletBinding()]
param (
    [Parameter(ValueFromRemainingArguments=$true)]
    $Path
)

foreach ($aPath in $Path) {
    $aPath
    if (Test-Path $aPath -PathType Container) {
        Get-ChildItem $aPath -Recurse | Select-Object -ExpandProperty FullName
    }
}

Also, if the script does not have a param block and is ran with Powershell -File ScriptName.ps1, the $args variable will contain exactly what is expected, an array of file paths.


Change your shortcut to this and try it:

powershell.exe -noprofile -noexit -command "& {c:\test1.ps1 $args}"


Dragging and Dropping Files onto a PowerShell Script --- Additional Information and Solutions

The solution posted above by Nathan Hartley using a shortcut file works well. (See: stackoverflow.com/a/45838761/80161)

It turns out that you can just as easily use a .BAT file, with the same results, using either to call the same underlying .ps1 script files.

Below is a summary of the two techniques. The sample scripts below have been tested on Windows 10 using both Windows PowerShell 5.1 (powershell.exe) and PowerShell 7.1 (pwsh.exe).

Multiple consecutive spaces in file and directory names are preserved with either technique below, which was a problem with some of the other above mentioned solutions. (Nathan's solution works well in this regard. I include his solution, along with my .bat file solution, in my summary below.)

Sample Shortcut file:

Enter something like the following as the "Target" in the Shortcut's properties:

powershell.exe -noexit -File "C:\Users\User\Documents\PS Scripts\your script.ps1"

You can also simply type powershell or pwsh when entering the above into the Shortcut, assuming the PowerShell program is in your path. When you Save or Apply the Shortcut it will automatically expand the name to a full absolute file name. Also, if the -File parameter path and filename have no spaces you can omit the quotation marks around the called .ps1 script name.

Sample .BAT file:

@echo off
set PSScript=%~dpn0.ps1
powershell.exe -NoExit -File "%PSScript%" %*

As in the posting by another author above, give the .BAT file the same base name as the corresponding PowerShell script. Of course you can, instead, expressly code the called script name into the .BAT file if you prefer, but the above makes it convenient to move the wrapper and .ps1 scripts around as a pair.

The quotes around "%PSScript%" are important, but %* (which passes all parameters passed to the .BAT script to the PowerShell script) must not be enclosed in quotes.


The below example .ps1 scripts can be called by either of the above:

Option 1: Collecting passed path names in a 'named' parameter using the 'ValueFromRemainingArguments' specification (this is basically Nathan's example above):

param (
    [Parameter(ValueFromRemainingArguments=$true)]
    $Paths
)

'Paths:'
$Paths   # lists the dropped path names

Option 2: Picking up parameters in the $Args array:

# Simply reference the dropped path names in the $Args array.
# The following will list all the dropped path names.
# Yes, this is a one line .ps1 script!

$Args

The following demonstrates "Option 2" with some added simple code accessing passed path names individually:

$Args
''
'Example of accessing passed Args individually:'
write-host "=====There are a total of $($args.count) arguments:"
for ( $i = 0; $i -lt $args.count; $i++ ) {
    write-host "Argument  $i is $($args[$i])"
}

Update 6/2021: See follow-up post below for enhanced code that will allow you to include and expand dropped directories as well.

Update 10/2021: Use of either the Windows shortcut approach or the .bat file approach generally works well. However note the following tradeoffs: The .bat file is super easy to set up for a new script--just make a copy of a previous one and give it the same base name as your new .ps1 script. On the other hand I found that you can drop more files on the Windows shortcut. As an example I found, in one of my comparison tests, I could drop about 90 files on my .bat file and about 380 files on my shortcut. Of course if the files are in a directory, and you drop the directory name, then you avoid the limitation altogether.


Notes about the code options shared above:

  • The [CmdletBinding()] specification shown in Nathan's example is not necessary and is overridden by the [ValueFromRemainingArguments=$true] specification, which causes all remaining dropped file names not already picked up by any preceding parameters to be gathered into the named $Paths parameter to which the specification is attached.

  • The [CmdletBinding()] specification must not be used when accessing the dropped file names via the default $Args variable in the example above, otherwise you'll get an error since this specification demands the all passed parameters have matching parameter specifications.

  • If neither “[CmdletBinding()]” nor “[Parameter(ValueFromRemainingArguments=$true)]” are specified in the PowerShell script and more files are dropped and passed to the PowerShell script than there are named parameters, the file names are passed first via the named parameters, and any remaining file names are pass as $Args parameters, starting with $Args[0].

  • A key element in the solutions above is the use of the -File parameter rather than the -Command parameter in the PowerShell command that calls the respective .ps1 script.

  • As mentioned above, pwsh.exe (PowerShell 7) can be used instead of powershell.exe (Windows PowerShell) in the Shortcut file or Batch file.


Unfortunately, $args will split your arguments on spaces, thus working incorrectly on filenames containing them. However, you still can process $args inside your .ps1 without an external wrapper script!

Since everything you drag-n-drop on a shortcut (or select for "Send To") will be passed to your script as space-joined list of absolute paths, you can parse $args for all absolute path occurrences using regex and pipe it to foreach:

$pattern = '([a-zA-Z]:\\(((?![<>:"/\\|?*]).)+((?<![ .])\\)?)*)(\s|$)'

Select-String "$pattern" -input "$args" -AllMatches | Foreach {$_.matches} | ForEach-Object { 
    $path = "$($_.Groups.Groups[1].Value)"
    # ...
} 

Where each $path is exactly one absolute path (including those with spaces inside)

The absolute file pattern is taken from here.

I wrapped it in parentheses (to match the first group with the actual path) and added a (\s|$) group at the end, which is "whitespace or end of string", needed because shortcut arguments will be joined with single spaces; $ is required to match the last path occurrence in the argument list.


Dragging and Dropping Files onto a PowerShell Script --- Follow-up: Expanding Dropped Directories

This is a follow-up to my posting above: “Dragging and Dropping Files onto a PowerShell Script --- Additional Information and Solutions”.

The below PowerShell script can be used with either the .bat file or Shortcut file technique described in my earlier posting, and provides additional flexibility in dropping files in Windows Explorer. The script can also be used from the PowerShell Command prompt.

  • This will allow dropping an intermixed list of directory and file names. Directories will be expanded.

  • When run manually from the command line, wildcard characters can also be used to specify files (but not directories).

  • Any passed relative file names will be converted to fully qualified names, which can be important for some applications.

param (
    [Parameter(ValueFromRemainingArguments=$true)]
    $DroppedFiles
)

if ( $DroppedFiles.count -eq 0 ) {
    Write-Host "Supply directory and/or file names. Wildcards are allowed." -ForegroundColor Red
    Exit
}

[bool]$err = $False
foreach ($f in $DroppedFiles) {
    if ( ( [string]::IsNullOrWhiteSpace($f) ) -or ( (Test-Path -Path $f) -eq $False ) ) {
    Write-host "ERROR--File or directory does not exist: " -NoNewline  -ForegroundColor Red
    Write-host $f
    $err = $True
    }
}
if ( $err ) { Exit }

$Droppedfiles = Get-ChildItem -Path $DroppedFiles -File -Depth 0 -Recurse -ErrorAction Stop

$i = 0
foreach ($File in $DroppedFiles) {
    Write-Host "Processing:" $File.FullName
    $i++
}
Write-Output "$i file(s) processed."

Notes:

  • This will process multiple intermixed directory and file names.

  • Directories expand only to the first level by default (set by "-Depth 0"). You may increase this or remove the limitation altogether.

  • You can restrict the produced file list by adding an -Include parameter to the Get-ChildItem line, for example: -Include: *.doc,*.docx

  • When calling the script from the command line, any files specified that do not exist return nothing (no error is reported), so a preliminary check for non-existent files or directories is included.

  • The script as written works well with PowerShell 7.1 (pwsh.exe).

  • Update 10/2021: I found a bug in PowerShell 5.1 that affects operation of the code when dropping multilevel directories and using the Include parameter at the same time. See additional detail and comments about the nature of the issue and possible solutions, if this is an issue for you, in my next post about my "Test+Expand-Dropped-Files" function which encapsulates the above code.

Additional comments about the code above:

  • If your PowerShell script will be used only for dropping files from Windows Explorer on either a .bat file or Shortcut file as described in my previous post (as opposed to calling your script and passing parameters on the command line), you should be able to omit the two initial test blocks since Windows Explorer will pass fully qualified names known to exist as you "drop" files. If you intend to (or might accidentally) execute the script from the command line the additional tests prevent Get-ChildItem from picking up all files in the current directory by default if no parameter is supplied, and also make sure that any user typed parameters do exist.


For those looking for a quick good-enough solution for drag n dropping a single file to a powershell script expecting a string parameter, you can use this as your shortcut:

powershell.exe -noprofile -noexit -command "& {.\myscript.ps1 ($args -join ' ')}"

It could break if you had a file path with multiple consecutive spaces, but this works well for me as I have never encountered such a file where I needed to use this technique.


Function to check and expand file names dropped on a PowerShell Script

Not to belabor the points of my two prior posts, but to simplify the creation of multiple PowerShell scripts upon which I want to be able to drop files and directories, I created a function to do the checks and directory expansions.

Below is what I came up with:

<#
Function: Test+Expand-Dropped-Files
Lewis Newton, 6/2021

This function will process an intermixed list of directory and file names passed to a 
PowerShell script when dropped in Windows Explorer on a properly configured accompanying
Windows shortcut or .bat file.  The PowerShell script or .bat file can also be called directly
from the command line.

-- This function will accept multiple intermixed directory and file names.  
-- It will check that the files exist, and expand any directories recursively to the depth
   specified (it defaults to 0, the top level).
-- The 'Include' parameter allows limiting what is included, for example: -Include *.doc,*.docx
   It defaults to a $Null string array which will include everything found.  
-- Relative file names and wildcard characters are accepted. 
-- The function returns fully qualified file objects.
-- Update 10/2021:  This works well with PowerShell 7.1.  I found a bug in PowerShell 5.1
   that affects operation when dropping multi-level directories if you supply an "-Include"
   parameter at the same time; the "-Depth 0" parameter is not honored in the "Get-ChildItem"
   command and you get results for any lower sub-directories as well. This is not
   an issue if you are not using "-Include", or if you only drop files or single level
   directories. See additional notes about possible solutions or workarounds (if this is an
   issue for you) toward the end of my web posting about this script on "stackoverflow".
-- Note: "-Include $Null" (which is set as default) acts as if no parameter is present
   and in that case the "-Depth" parameter is honored in PowerShell 5.1 (this is not the
   case for "-Include '' ").
#>

function Test+Expand-Dropped-Files {
    param(
        $DroppedFiles,
        [string]$Depth = "0",
        [string[]]$Include = $Null
  )

if ( $DroppedFiles.count -eq 0 ) {
    Write-Host "Supply directory and/or file names. Wildcards are allowed." -ForegroundColor Red
    Exit
    # Makes sure there is at least one parameter, or Get-ChildItem defaults to all in current directory.
    }

[bool]$err = $False
foreach ($f in $DroppedFiles) {
    if ( ( [string]::IsNullOrWhiteSpace($f) ) -or ( (Test-Path -Path $f) -eq $False ) ) {
    Write-host "ERROR--File or directory does not exist: " -NoNewline  -ForegroundColor Red
    Write-host $f
    $err = $True
    }
}
if ( $err ) { Exit }

$Droppedfiles = Get-ChildItem -Path $DroppedFiles -File -Recurse -Depth $Depth -Include $Include -ErrorAction Stop
# '-Depth 0' limits expansion of files in directories to the top level.
# '-Include $Null' will include everything.

Write-Output $DroppedFiles
}

To provide access to the function in my scripts, I put the above in a file named "function_Test+Expand-Dropped-Files.ps1", and then I dot-source this from a directory in my system path.

Here is an example of what I put at the beginning to my scripts to call the function to facilitate picking up dropped files and directories:

param ( [Parameter(ValueFromRemainingArguments=$true)] $DroppedFiles )

. function_Test+Expand-Dropped-Files
$DroppedFiles = Test+Expand-Dropped-Files $DroppedFiles  -Depth 0 -Include $Null
# '-Depth 0' limits directory expansion to first level.
# '-Include' can specify a string array of specifications, such as: -Include *.doc,*.docx 
# '-Depth' and '-Include' are optional and default to 0 and $Null (includes all) respectively.

Here is a sample loop that shows picking up and processing the dropped file names passed in the $DroppedFiles parameter:

$i = 0
foreach ($File in $DroppedFiles) {
    Write-Host "Processing:" $File.FullName
    $i++
}
Write-Output "$i file(s) processed.`n"

Notes:

  • Now you can create a .bat file or Windows shortcut file (as described in my previous post) that calls your script, onto which you can drag and drop files and directories in Windows Explorer. Of course you can also call your .ps1 script directly from within PowerShell, supplying filenames (including wildcards) and directory names on the command line.

  • Another alternative to dot-sourcing the above function as suggested above is to put the "Test+Expand-Dropped-Files" function in your PowerShell profile, or set the function up as a PowerShell Module.

Update--Oct 2021: Issue discovered in using this with PowerShell 5.1:

(I also added a note about this issue in the comments within the function's script code above.)

Bottom Line: If you are using PowerShell 7.1 the function works well as is. If you are using PowerShell 5.1 the function will recurse into lower level directories if any are present in any directory names you pass to the function, but only if you use the "-Include" parameter. See the gory details below along with some suggestions for dealing with this if it is an issue for you.

  • I found there was a bug in the operation of “Get-ChildItem” (which I call in the function) when used with PowerShell 5.1. The “-Recurse” option is needed to descend into a passed directory when “-Include” is also used, yet if you try to limit the depth (as in “-Depth 0” to just the first level), and at the same time supply an “-Include” parameter, the depth is ignored and it recurses down the tree in PowerShell 5.1. Note that in PowerShell 7.1 the “-Depth” parameter works properly, and the depth is limited to the specified level in this case.

  • Using a “-Filter” parameter as an alternative to using the “-Include” parameter does honor the depth parameter, but is not as flexible in some ways. I also noted that “-Filter *.doc” will also pick up “.docx” files in 5.1 but not in 7.1. In 7.1 I could use “-Filter *.doc*” to pick up both types of files, but that also potentially (while perhaps not a likely situation) picks up such a string earlier in a file name.

  • One clarifying note on the above, with either version of PowerShell, as implied above, if you pass a directory name to “Get-ChildItem” without using an “-Include” parameter, and without specifying “-Recurse”, then in this case you will get the top level files of the passed directory. This is also true if you use “-Filter”. However when you use an “-Include”, as noted above, you need to the “-Recurse” and “-Depth 0” to get the top level files.

  • Another option for creating a function that would work with either version of PowerShell would to be to use the “Get-ChildItem” without the “-Include” so that the “-Depth” parameter is honored, and then expressly follow up with a simple code loop to pull out the files you want to include.

  • As long as I use PowerShell 7.1, I’m good with the function as it is, and my function works as intended. Maybe at some point the bug in PowerShell 5.1 will be fixed.

  • Summarizing: This only an issue with PowerShell 5.1, and is not an issue if you are not using "-Include", or if you only drop files or single level directories.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜