Write-Host vs Write-Information в PowerShell 5
Хорошо известно, что Write-Host
является злом.
В PowerShell 5
добавляется Write-Information
и считается заменой Write-Host
.
Но, действительно, что лучше?
Write-Host
является злом, потому что он не использует конвейер, поэтому входное сообщение не может использоваться повторно.
Но что делать Write-Host
- это просто показать что-то в консоли прямо? В каком случае мы должны повторно использовать вход?
В любом случае, если мы действительно хотим повторно использовать входные данные, почему бы просто не написать что-то вроде этого:
$foo = "Some message to be reused like saving to a file"
Write-Host $foo
$foo | Out-File -Path "D:\foo.log"
Другой минус Write-Host
заключается в том, что Write-Host
может указать, в каком цвете сообщения отображаются в консоли с помощью -ForegroundColor
и -BackgroundColor
.
С другой стороны, используя Write-Information
, входное сообщение может использоваться везде, где мы хотим, через конвейер №6. И не нужно писать дополнительные коды, как я пишу выше. Но темная сторона этого заключается в том, что если мы хотим писать сообщения на консоль и также сохраняться в файле, мы должны это сделать:
# Always set the $InformationPreference variable to "Continue"
$InformationPreference = "Continue";
# if we don't want something like this:
# ======= Example 1 =======
# File Foo.ps1
$InformationPreference = "Continue";
Write-Information "Some Message"
Write-Information "Another Message"
# File AlwaysRunThisBeforeEverything.ps1
.\Foo.ps1 6>"D:\foo.log"
# ======= End of Example 1 =======
# then we have to add '6>"D:\foo.log"' to every lines of Write-Information like this:
# ======= Example 2 =======
$InformationPreference = "Continue";
Write-Information "Some Message" 6>"D:\foo.log"
Write-Information "Another Message" 6>"D:\foo.log"
# ======= End of Example 2 =======
Немного лишний, я думаю.
Я знаю только небольшой аспект этой вещи "против", и, должно быть, что-то не в моих силах. Так есть ли что-нибудь еще, что может заставить меня поверить, что Write-Information
лучше, чем Write-Host
, пожалуйста, оставьте здесь свои добрые ответы.
Спасибо.
Ответы
Ответ 1
Командлеты Write-*
позволяют вам структурировать канал вывода вашего кода PowerShell, поэтому вы можете легко отличать сообщения различной степени тяжести друг от друга.
-
Write-Host
: отображение сообщений интерактивному пользователю на консоли. В отличие от других командлетов Write-*
это не подходит и не предназначено для целей автоматизации/перенаправления. Не зло, просто разные.
-
Write-Output
: записать "нормальный" вывод кода в выходной поток по умолчанию (успех) ( "STDOUT" ).
-
Write-Error
: записать информацию об ошибке в отдельный поток ( "STDERR" ).
-
Write-Warning
: писать сообщения, которые вы считаете предупреждениями (т.е. вещами, которые не являются ошибками, но что-то, на что должен обратить внимание пользователь) на отдельный поток.
-
Write-Verbose
: напишите информацию, которую вы считаете более подробным, чем "нормальный" вывод в отдельный поток.
-
Write-Debug
: напишите информацию, которую вы считаете релевантной для отладки вашего кода в отдельный поток.
Write-Information
является лишь продолжением этого подхода. Он позволяет вам реализовать уровни журналов в вашем выводе (Debug
, Verbose
, Information
, Warning
, Error
) и по-прежнему иметь выходной поток успеха для регулярного вывода.
Что касается того, почему Write-Host
стал оберткой вокруг Write-Information
: я не знаю фактической причины этого решения, но подозреваю, потому что большинство людей не понимают, как работает Write-Host
, т.е. для чего его можно использовать и для чего его не следует использовать.
Насколько я знаю, нет общепринятого или рекомендуемого подхода к регистрации в PowerShell. Например, вы могли бы реализовать в своем ответе одну функцию регистрации, например @JeremyMontgomery:
function Write-Log {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
Write-Log 'foo' # default log level: Information
Write-Log 'foo' 'Information' # explicit log level: Information
Write-Log 'bar' 'Debug'
или набор функций ведения журнала (по одному для каждого уровня журнала):
function Write-LogInformation {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
function Write-LogDebug {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
...
Write-LogInformation 'foo'
Write-LogDebug 'bar'
Другой вариант - создать пользовательский объект журнала:
$logger = New-Object -Type PSObject -Property @{
Filename = ''
Console = $true
}
$logger | Add-Member -Type ScriptMethod -Name Log -Value {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
$logger | Add-Member -Type ScriptMethod -Name LogDebug -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Debug')
}
$logger | Add-Member -Type ScriptMethod -Name LogInfo -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Information')
}
...
Write-Log 'foo' # default log level: Information
$logger.Log('foo') # default log level: Information
$logger.Log('foo', 'Information') # explicit log level: Information
$logger.LogInfo('foo') # (convenience) wrapper method
$logger.LogDebug('bar')
В любом случае вы можете экпортировать код регистрации
-
помещая его в отдельный файл script и dot-sourcing этот файл:
. 'C:\path\to\logger.ps1'
-
помещая его в модуль и импортируя этот модуль:
Import-Module Logger
Ответ 2
В дополнение к Ансгару полезный и всесторонний ответ:
Write-Host
стал (по сути) оберткой для
Write-Information -InformationAction Continue
в PSv5, предположительно потому, что:
-
он позволяет подавлять или перенаправлять сообщения Write-Host
, что ранее не было возможно (в PowerShell 4 или ниже, Write-Host
обходит потоки PowerShell и выводится непосредственно на хост),
-
в то время как сохраняет обратную совместимость в том, что сообщения выводятся по умолчанию - в отличие от Write-Information
, поведение по умолчанию которого должно быть тихим (поскольку оно соответствует переменной предпочтения $InformationPreference
, чья значение по умолчанию SilentlyContinue
).
В то время как Write-Host
теперь (PSv5 +) является немного неправильным - он не обязательно больше записывает на хост - он все еще имеет одно отличное преимущество над Write-Information
(поскольку вы state): он может создавать цветной выход с -ForegroundColor
и -BackgroundColor
.
Ответ Ansgar имеет общую перспективу каротажа, но командлет PowerShell Start-Transcript
может служить встроенной альтернативой (см. ниже).
Что касается вашего желания выводить сообщения на хост, а также записывать их в файл журнала:
PowerShell транскрипты сеанса - через Start-Transcript
и Stop-Transcript
- могут дать вам то, что вы хотите.
Как следует из названия, транскрипты фиксируют все распечатки на экране (без раскраски), поэтому по умолчанию включает в себя успешный вывод.
Применяется к вашему примеру:
$null = Start-Transcript "D:\foo.log"
$InformationPreference = "Continue"
Write-Information "Some Message"
Write-Information "Another Message"
$null = Stop-Transcript
Вышеприведенное будет печатать сообщения как на экране, так и на файле транскрипта; обратите внимание, что, с любопытством, только в файле будет префикс INFO:
.
(В отличие от этого, Write-Warning
, Write-Verbose
и Write-Debug
- если они настроены на создание префикса вывода - WARNING:
, VERBOSE:
, DEBUG:
как на экране, так и в файле, аналогично, Write-Error
производит "шумный" многострочный ввод как на экране, так и в файле.)
Обратите внимание на одну странность (я не знаю, есть ли ошибка или по дизайну, наблюдаемая в PSv5.1): вывод из Write-Information
отображается в файле расшифровки (но не на экране), даже если $InformationPreference
установлен на SilentlyContinue
(по умолчанию); единственный способ исключить вывод Write-Information
(через переменную предпочтения или параметр -InformationAction
) - это значение Ignore
- которое молчаливо отключает вывод - или, что любопытно, Continue
, в котором оно печатается только консоль, как указывает PetSerAl.
В двух словах вы можете использовать Start-Transcript
как удобную встроенную аппроксимацию средства ведения журнала, чья многословие вы можете контролировать извне через переменные предпочтений ($InformationPreference
, $VerbosePreference
,...) со следующими важными отличиями от обычного ведения журнала:
-
Как правило, то, что входит в файл транскрипции, также выводится на консоль (что обычно можно считать плюсом).
-
Однако выход успеха (вывод данных) по умолчанию также отправляется в стенограмму - если вы не захватили его или вообще не подавили - и вы не можете выборочно сохранить его из стенограммы:
-
Если вы захватили или подавили его, он не будет отображаться на хосте (по умолчанию, в консоли) либо [1].
-
Возможно, обратное: вы можете отправлять вывод только в транскрипцию (без эхо-сигнала в консоли), используя Out-Default -Transcript
Спасибо, PetSerAl; например,
'to transcript only' | Out-Default -Transcript
-
Как правило, внешние перенаправления сохраняют потоки из транскрипта, с двумя исключениями, с Windows PowerShell v5.1/PowerShell Core v6.0.0-beta 0,5:
-
Write-Host
, даже если используются переадресации 6>
или *>
.
- Вывод ошибок, даже если используются перенаправления
2>
или *>
.
Однако использование $ErrorActionPreference = 'SilentlyContinue'
/'Ignore'
не допускает ошибок, исключающих завершение, но не заканчивающих их.
-
Файлы транскриптов не являются ориентированными на линию (есть блок строк заголовков с информацией о вызове, и нет гарантии, что вывод, созданный script, ограничен линией), поэтому вы не можете ожидать синтаксического анализа их по очереди.
[1] PetSerAl упоминает следующее ограниченное и несколько громоздкое обходное решение (PSv5 +) для отправки результатов успеха только на консоль, что особенно препятствует отправке вывода по конвейеру или его захвату:
'to console only' | Out-String -Stream | ForEach-Object { $Host.UI.WriteLine($_) }
Ответ 3
PowerShell посвящен автоматизации.
Иногда вы запускаете script несколько раз в день, и вы не хотите видеть вывод все время.
Write-Host
не имеет возможности скрывать вывод. Он записывается на консоль, несмотря ни на что.
С помощью Write-Information
вы можете указать параметр -InformationAction
на Script. С помощью этого параметра вы можете указать, хотите ли вы видеть сообщения (-InformationAction Continue
) или нет (-InformationAction SilentlyContinue
)
Изменить:
И используйте "Some Message" | out-file D:\foo.log
для регистрации, и ни Write-Host
, ни Write-Information
Ответ 4
Вот общая версия более специализированной функции регистрации, которую я недавно использовал для script.
Сценарий для этого заключается в том, что когда мне нужно что-то делать в качестве запланированной задачи, я обычно создаю общий script или функцию в модуле, который выполняет "тяжелый подъем", а затем вызывает script, который обрабатывает особенности для конкретного задания, такие как получение аргументов из конфигурации XML, протоколирование, уведомления и т.д.
Внутренний script использует Write-Error, Write-Warning и Write-Verbose, вызывающий script перенаправляет все выводит потоки вниз по конвейеру к этой функции, которая фиксирует записи сообщений в файле csv с отметкой времени, уровнем и сообщением.
В этом случае он был нацелен на PoSh v.4, поэтому я в основном использую Write-Verbose в качестве резервной копии для Write-Information, но такую же идею. Если бы я использовал Write-Host в Some- Script.ps1 (см. Пример) вместо Write-Verbose или Write-Information, функция Add-LogEntry не могла бы записывать и записывать сообщение. Если вы хотите использовать это для надлежащего захвата большего количества потоков, добавьте записи в оператор switch, чтобы удовлетворить ваши потребности.
Переключатель -PassThru в этом случае был в основном способом адресовать то, что вы упомянули о записи в файл журнала, помимо вывода на консоль (или в другую переменную, или вниз трубопровод). В этой реализации я добавил объект "Уровень" для объекта, но, надеюсь, вы можете увидеть эту точку. Моим прецедентом для этого было передать записи журнала переменной, чтобы они могли быть проверены на наличие ошибок и использовались в уведомлении SMTP, если произошла ошибка.
function Add-LogEntry {
[CmdletBinding()]
param (
# Path to logfile
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, Position = 0)]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 0)]
[String]$Path,
# Can set a message manually if not capturing an alternate output stream via the InformationObject parameter set.
[Parameter(ParameterSetName = 'Normal', Mandatory = $true)]
[String]$Message,
# Captures objects redirected to the output channel from Verbose, Warning, and Error channels
[ValidateScript({ @("VerboseRecord", "WarningRecord", "ErrorRecord") -Contains $_.GetType().name })]
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, ValueFromPipeline = $true)]
$InformationObject,
# If using the message parameter, must specify a level, InformationObject derives level from the object.
[ValidateSet("Information", "Warning", "Error")]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 2)]
[String]$Level,
# Forward the InformationObject down the pipeline with additional level property.
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $false)]
[Switch]$PassThru
)
Process {
# If using an information object, set log entry level according to object type.
if ($PSCmdlet.ParameterSetName -eq "InformationObject") {
$Message = $InformationObject.ToString()
# Depending on the object type, set the error level,
# add entry to cover "Write-Information" output here if needed
switch -exact ($InformationObject.GetType().name) {
"VerboseRecord" { $Level = "Information" }
"WarningRecord" { $Level = "Warning" }
"ErrorRecord" { $Level = "Error" }
}
}
# Generate timestamp for log entry
$Timestamp = (get-date).Tostring("yyyy\-MM\-dd\_HH\:mm\:ss.ff")
$LogEntryProps = @{
"Timestamp" = $Timestamp;
"Level" = $Level;
"Message" = $Message
}
$LogEntry = New-Object -TypeName System.Management.Automation.PSObject -Property $LogEntryProps
$LogEntry | Select-Object Timestamp, Level, Message | Export-Csv -Path $Path -NoTypeInformation -Append
if ($PassThru) { Write-Output ($InformationObject | Add-Member @{Level = $Level } -PassThru) }
}
}
Пример использования будет
& $PSScriptRoot\Some-Script.ps1 -Param $Param -Verbose *>&1 | Add-LogEntry -Path $LogPath -PassThru
Переключатель -PassThru должен по существу записывать информационный объект в консоль, если вы не записываете вывод в переменную или не передаете его по каналу на что-то еще.
Ответ 5
Я должен признать, что я ненавижу ведение журнала PowerShell и все команды Write-*
... Поэтому я запускаю все свои скрипты с помощью той же функции:
function logto{ ## Outputs data to Folder tree
Param($D,$P,$F,$C,$filename)
$LogDebug = $false
$FDomain =[System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$SCRdir = $MyInvocation.ScriptName
$FDNSName = $FDomain.Name
$RealFile = $F
if($ScriptName -eq $null){
$ScriptName = "\LogTo\"
}
## if there is a time stamp defined make it part of the directory
if($GlobalRunTime){
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName + $GlobalRunTime + "\"
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}else{
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}
## do not write null data
if ($D -eq $null) {
If ($LogDebug) {Write-host "$RealFile :Received Null Data Exiting Function" -foregroundcolor Magenta}
Return
}
## if no path is chosen default to
if ($P -eq $null) {
$PT = $Flocaldrive
If ($LogDebug) {Write-host "Path was Null, setting to $PT" -foregroundcolor Magenta}
}else{
$PT = $Flocaldrive + $P
If ($LogDebug) {Write-host "Path detected as $p, setting path to $PT" -foregroundcolor Magenta}
}
## anything with no file goes to Catchall
If ($RealFile-eq $null) {
If ($LogDebug) {Write-host "$D :attempting to write to Null file name, redirected out to Catchall" -foregroundcolor Magenta}
$RealFile= "\Catchall.txt"
}
##If color is blank DONT write to screen
if ($C -eq $null) {
If ($LogDebug) {Write-host "Color was blank so not writing to screen" -foregroundcolor Magenta}
}else{
If ($LogDebug) {Write-host "Attempting to write to console in $C" -foregroundcolor Magenta}
write-host $D -foregroundcolor $C
}
###### Write standard format
$DataFile = $PT + $RealFile## define path with File
## Check if path Exists if not create it
If (Test-Path $PT) {
If ($LogDebug) {Write-host "$PT :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $PT -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $PT" -foregroundcolor Magenta}
}
## If file exist if not create it
If (Test-Path $DataFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$DataFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $DataFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$DataFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $DataFile -append ## Write our data to file
## Write to color coded files
if ($C -ne $null) {
$WriteSumDir = $Flocaldrive + "Log\Sorted"
$WriteSumFile = $WriteSumDir + "\Console.txt"
## Check if path Exists if not create it
If (Test-Path $WriteSumDir) {
If ($LogDebug) {Write-host "$WriteSumDir :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumDir -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $WriteSumDir" -foregroundcolor Magenta}
}
## If file does not exist create it
If (Test-Path $WriteSumFile) {
If ($LogDebug) {Write-host "$WriteSumFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteSumFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $WriteSumFile -append ## write everything to same file
## Write our data to color coded file
$WriteColorFile = $WriteSumDir + "\$C.txt"
If (Test-Path $WriteColorFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$WriteColorFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteColorFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteColorFile :File Created" -foregroundcolor Magenta}
}
## Write our data to Color coded file
$D | out-file -Filepath $WriteColorFile -append ## write everything to same file
}
## If A return was not specified
If($filename -ne $null){
Return $DataFile
}
}