Powershell: невозможно найти тип при использовании классов PS 5
Я использую классы в PS с WinSCP Powershell Assembly. В одном из методов я использую различные типы из WinSCP.
Это работает нормально, пока у меня уже есть добавленная сборка - однако из-за способа, которым Powershell читает сценарий при использовании классов (я полагаю?), Выдается ошибка, прежде чем сборка может быть загружена.
На самом деле, даже если я поставлю Write-Host сверху, он не будет загружаться.
Есть ли способ заставить что-то запускаться до того, как будет проанализирован остальной файл?
Transfer() {
$this.Logger = [Logger]::new()
try {
Add-Type -Path $this.Paths.WinSCP
$ConnectionType = $this.FtpSettings.Protocol.ToString()
$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = [WinSCP.Protocol]::$ConnectionType
HostName = $this.FtpSettings.Server
UserName = $this.FtpSettings.Username
Password = $this.FtpSettings.Password
}
Protocol = [WinSCP.Protocol]:: $ ConnectionType
Невозможно найти тип [WinSCP.Protocol].
5 ответов
Как вы обнаружили, PowerShell отказывается запускать сценарии, содержащие определения классов, которые ссылаются на недоступные (еще не загруженные) типы - этап разбора сценария завершается неудачно.
- Начиная с PSv5.1, даже
using assembly
оператор в верхней части скрипта в этом случае не помогает, потому что в вашем случае на тип ссылаются в контексте определения класса PS - однако это может быть исправлено в PowerShell Core; требуемая работа отслеживается в этом выпуске GitHub.
Правильным решением является создание скриптового модуля ( *.psm1
) чей связанный манифест ( *.psd1
) объявляет сборку, содержащую ссылочные типы, обязательным условием через RequiredAssemblies
ключ.
Посмотрите альтернативное решение внизу, если использование модулей не вариант.
Вот упрощенное прохождение:
Создать тестовый модуль tm
следующим образом:
Создать папку модуля
./tm
и манифест (*.psd1
) в этом:# Create module folder mkdir ./tm # Create manifest file that declares the WinSCP assembly a prerequisite. # Modify the path to the assembly as needed; you may specify a relative path, but # note that the path must not contain variable references (e.g., $HOME). New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 ` -RequiredAssemblies C:\path\to\WinSCPnet.dll
Создать файл модуля скрипта (
*.psm1
) в папке модуля:Создать файл
./tm/tm.psm1
с вашим определением класса; например:class Foo { # Simply return the full name of the WinSCP type. [string] Bar() { return [WinSCP.Protocol].FullName } }
Примечание. В реальном мире модули обычно размещаются в одном из стандартных мест, определенных в
$env:PSMODULEPATH
, так что на модуль можно ссылаться только по имени, без необходимости указывать (относительный) путь.
Используйте модуль:
PS> using module ./tm; (New-Object Foo).Bar()
WinSCP.Protocol
using module
оператор импортирует модуль и - в отличие Import-Module
- также делает класс, определенный в модуле, доступным для текущего сеанса.
После импорта модуля неявно загружена сборка WinSCP благодаря RequiredAssemblies
введите манифест модуля, создание экземпляра класса Foo
, который ссылается на типы сборки, успешно.
Если ваш вариант использования не позволяет использовать модули, вы можете использовать Invoke-Expression
в крайнем случае, но учтите, что обычно лучше избегать Invoke-Expression
в интересах надежности и во избежание угроз безопасности [1].
# Adjust this path as needed.
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll
# By placing the class definition in a string that is invoked at *runtime*
# via Invoke-Expression, *after* the WinSCP assembly has been loaded, the
# class definition succeeds.
Invoke-Expression @'
class Foo {
# Simply return the full name of the WinSCP type.
[string] Bar() {
return [WinSCP.Protocol].FullName
}
}
'@
(New-Object Foo).Bar()
[1] Это не проблема в данном случае, но в целом, учитывая, что Invoke-Expression
может вызвать любую команду, хранящуюся в строке, применение ее к строкам, не полностью контролируемым вами, может привести к выполнению вредоносных команд. Это предостережение относится к другим языкам аналогично, например, к встроенному в Bash eval
команда.
Дополнительное решение - поместить логику Add-Type в отдельный файл .ps1 (назовите его
Во-первых, я бы порекомендовал ответ mklement0.
Тем не менее, вы можете немного побегать, чтобы получить тот же эффект с немного меньшим объемом работы, что может быть полезно в небольших проектах или на ранних стадиях.
Можно просто. создайте другой файл ps1 в своем коде, который содержит ваши классы, ссылающиеся на еще не загруженную библиотеку после загрузки указанной сборки.
##########
MyClasses.ps1
Class myClass
{
[3rdParty.Fancy.Object] $MyFancyObject
}
Затем вы можете вызвать свою собственную библиотеку классов из основного скрипта с расширением.
#######
MyMainScriptFile.ps1
#Load fancy object's library
Import-Module Fancy.Module #If it's in a module
Add-Type -Path "c:\Path\To\FancyLibrary.dll" #if it's in a dll you have to reference
. C:\Path\to\MyClasses.ps1
Исходный синтаксический анализ пройдет проверку, скрипт запустится, ваша ссылка будет добавлена, а затем, по мере продолжения скрипта, файл. Исходный файл будет прочитан и проанализирован, добавив ваши пользовательские классы без проблем, поскольку их справочная библиотека находится в памяти к моменту анализа кода.
По-прежнему намного лучше создать и использовать модуль с надлежащим манифестом, но это легко пройдет, и его очень легко запомнить и использовать.
Хотя это не решение само по себе, я обошел его. Тем не менее, я оставлю вопрос открытым, поскольку он все еще стоит
Вместо использования WinSCP-типов я просто использую строки. Поскольку у меня уже есть перечисления, идентичные WinSCP.Protocol
Enum Protocols {
Sftp
Ftp
Ftps
}
И установили протокол в FtpSettings
$FtpSettings.Protocol = [Protocols]::Sftp
Я могу установить протокол так
$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
Protocol = $this.FtpSettings.Protocol.ToString()
HostName = $this.FtpSettings.Server
UserName = $this.FtpSettings.Username
Password = $this.FtpSettings.Password
}
Я использовал подобное на [WinSCP.TransferMode]
$TransferOptions.TransferMode = "Binary" #[WinSCP.TransferMode]::Binary
Add-Type -LiteralPath C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll $psclasses = GC "C:\Windows\Temp\foobarclass.ps1" -Raw Invoke-Expression $psclasses [Foo]::new().Bar()