I have some script to modify hosts. It's something like this:
#echo off
Powershell.exe -NoProfile -Command "& { $var = cat c:\...\hosts;
$var = $var -replace '....','....'
try {
Set-Content c:\...\hosts $var -ErrorAction Stop
} catch {
echo 'CAN`T WRITE'; pause; exit 2;
}
}"
exit $LASTEXITCODE
This is only an example, real script is more complex.
The problem is that sometimes script shows CAN'T WRITE error, but hosts file becomes empty, all the content is gone.
Any suggestions on how i can prevent losing file content on Set-Content error?
When you execute a statement in Try block, it is actually executed. Then if $var is $null, then the text file gets empty. And you see the error message using Catch block. Then you can save the content before, and if error occurs return the original file back in the Catch block:
#echo off
Powershell.exe -NoProfile -Command "& { $var = cat c:\...\hosts;
$oldvar = $var
$var = $var -replace '....','....'
try {
Set-Content c:\...\hosts $var -ErrorAction Stop
} catch {
echo 'CAN`T WRITE'; pause; exit 2;
$oldvar | Set-Content C:\...\hosts
New-Item "Backup.txt" -Type File -Value $oldvar
}
}"
exit $LASTEXITCODE
$oldvar will contain the the original content of the text file which can you use later. And to be more safe, i have added to write the content into a backup file (Backup.txt) if Writing again to the file fails.
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 1 year ago.
Improve this question
When I try to execute %* inside batch script by using powershell, it gives me an error. I need something to be replaced for %* so that it works for the following code snippet:
%* ^| ForEach-Object { Write-Host $_ ; $filepath = 'loggerfile.log' ; $output_file =$(Get-Date).ToString() + ' ' + $_ ; Write-Output $output_file ^| Out-File -FilePath $filepath -Append -NoClobber}
First, know why this is not valid code:
%* $| ForEach-Object { Write-Host $_ ; $filepath = 'loggerfile.log' ; $output_file =$(Get-Date).ToString() + ' ' + $_ ; Write-Output $output_file ^| Out-File -FilePath $filepath -Append -NoClobber}
Get-Alias -Definition ForEach-Object |
Format-Table -AutoSize
# Results
<#
CommandType Name Version Source
----------- ---- ------- ------
Alias % -> ForEach-Object
Alias foreach -> ForEach-Object
#>
This is why you get...
%*
%*: The term '%*' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
... when you use it.
And this ...
^
... is a termination character
Second, as well as your code is not a one-liner, as I commented above. What you really have is this...
ForEach-Object '*' ^|
ForEach-Object {
Write-Host $PSItem
$filepath = 'loggerfile.log'
$output_file = $(Get-Date).ToString() + ' ' + $PSItem
Write-Output $output_file ^|
Out-File -FilePath $filepath -Append -NoClobber
}
... and this.
ForEach-Object '*' ^
is not valid at all. I am curious as to where you got that from. As well as this...
^|
... as that is not a thing I've ever seen in any PowerShell docs, help files, repo code. This is also not aliases for anything natively.
Did you mean to do this...
$^
which means - The first token of the last command. Note though, it 'Does NOT' refer to the whole command.
The | pipe is of course pass results from the left of it to the right of it. What are you expecting this...
ForEach-Object '*' ^|
...to do? Even if you did, that is not valid and would error off with.
ForEach-Object: You cannot call a method on a null-valued expression.
So, did you mean to do this...
%* $^
... which is still something I've never seen in 10+ years I've been messing with Monad/Windows PowerShell/PowerShell Core.
To know what your code is doing, you take two steps.
Use PSSCriptANalyzer to validate what you are doing according to the PowerShell ruleset
Invoke-ScriptAnalyzer "$PWD\SomeScriptName.ps1"
Trace the execution, so you can see the stack.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/trace-command?view=powershell-7.1
Trace-Command -Name metadata,parameterbinding,cmdlet -Expression {
%* ^| ForEach-Object { Write-Host $_ ; $filepath = 'loggerfile.log' ; $output_file =$(Get-Date).ToString() + ' ' + $_ ; Write-Output $output_file ^| Out-File -FilePath $filepath -Append -NoClobber}
} -PSHost
Update2:
Now, when I know, that x32 is the problem I debugged into the script using powershell_ise_x32 and found out, that $Word.Documents is null.
So Powershell-API for Word has a different behaviour in x32 PowerShell, then in 64bit.
Update:
The error occurs, when using PowerShell x32 and occurs NOT on PowerShell 64bit. That was really it. Powershell x32 was executed because I started it from the Total Commander 32bit.
The question is now - why 32bit and 64bit PowerShell have different behaviour?
Initial Question:
I wrote a powershell script, to convert my WordDocuments and merge them to one.
I wrote a Batch script, to start this powershell script.
When I execute the script directly in "Powershell ISE" the script works fine.
When I execute the batch script as Administrator via context menu, the script reports errors. In this case the C:\WINDOWS\SysWOW64\cmd.exe is executed.
When I execute another cmd.exe found on my system as Administrator - everything works fine:
"C:\Windows\WinSxS\amd64_microsoft-windows-commandprompt_31bf3856ad364e35_10.0.15063.0_none_9c209ff6532b42d7\cmd.exe"
Why do I have different behaviour in different cmd.exe? What are those different cmd.exe?
Batch Script:
cd /d "%~dp0"
powershell.exe -noprofile -executionpolicy bypass -file "%~dp0%DocxToPdf.ps1"
pause
Powershell Script
$FilePath = $PSScriptRoot
$Pdfsam = "D:\Programme\PDFsam\bin\run-console.bat"
$Files = Get-ChildItem "$FilePath\*.docx"
$Word = New-Object -ComObject Word.Application
if(-not $?){
throw "Failed to open Word"
}
# Convert all docx files to pdf
Foreach ($File in $Files) {
Write-Host "Word Object: " $Word
Write-Host "File Object: " $Word $File
Write-Host "FullName prop:" $File.FullName
# open a Word document, filename from the directory
$Doc = $Word.Documents.Open($File.FullName)
# Swap out DOCX with PDF in the Filename
$Name=($Doc.FullName).Replace("docx","pdf")
# Save this File as a PDF in Word 2010/2013
$Doc.SaveAs([ref] $Name, [ref] 17)
$Doc.Close()
}
# check errors
if(-not $?){
Write-Host("Stop because an error occurred")
pause
exit 0
}
# wait until the conversion is done
Start-Sleep -s 15
# Now concat all pdfs to one single pdf
$Files = Get-ChildItem "$FilePath\*.pdf" | Sort-Object
Write-Host $Files.Count
if ($Files.Count -gt 0) {
$command = ""
Foreach ($File in $Files) {
$command += " -f "
$command += "`"" + $File.FullName + "`""
}
$command += " -o `"$FilePath\Letter of application.pdf`" -overwrite concat"
$command = $Pdfsam + $command
echo $command
$path = Split-Path -Path $Pdfsam -Parent
cd $path
cmd /c $command
}else{
Write-Host "No PDFs found for concatenation"
}
Write-Host -NoNewLine "Press any key to continue...";
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown");
I've found $PSScriptRoot to be unreliable.
$FilePath = $PSScriptRoot;
$CurLocation = Get-Location;
$ScriptLocation = Split-Path $MyInvocation.MyCommand.Path
Write-Host "FilePath = [$FilePath]";
Write-Host "CurLocation = [$CurLocation]";
Write-Host "ScriptLocation = [$ScriptLocation]";
Results:
O:\Data>powershell ..\Script\t.ps1
FilePath = []
CurLocation = [O:\Data]
ScriptLocation = [O:\Script]
As to the differences between the various cmd.exe implementations, I can't really answer that. I should have thought they'd be functionally identical, but maybe there's 32/64-bit differences that matter.
The error occurs, when using PowerShell x32 and occurs NOT on PowerShell 64bit.
I debugged into the script using powershell_ise_x32 and found out, that $Word.Documents is null.
This is because on my system Word 64bit is installed.
Well as the title states i made a PowerShell script that had such a horrible performance that it overextended the server ressources and crashed it.
The script reads an entire.xml file and appends a text at the beginning and at the end of the file. Also it changes the name of the file accroding to what is located in my filename.txt.
The .xml files are around 500 MB big and have over 4.7 million rows. Is there a way, that i don't have to read the entire file but not loose information?
function start-jobhere([scriptblock]$block){
start-job -argumentlist (get-location),$block { set-location $args[0]; invoke-expression $args[1] }
}
$handler_button1_Click= {
Try{
$job3 = start-jobhere {
#Text that should be at filebeginning
#('<?xml version="1.0" encoding="UTF-8"?>
<ids:ControlInfo>
<ids:ObjectFormat>CSV</ids:ObjectFormat>
<ids:SeparatorForCSV>;</ids:SeparatorForCSV>
</ids:ControlInfo>
<ids:BatchDeltaUntil></ids:BatchDeltaUntil>
</ids:BatchInfo>
</ids:Header>
<ids:Body>'
) + (get-content ZUB_Lokalisation.xml) | set-content ZUB_Lokalisation.xml
#Text that should be at file end
Add-Content ZUB_Lokalisation.xml -Value "</ids:Body>`n</ids:SimpleOperation>"
#Information that goes into the header of the file but has to be extracted from the filename inside a .txt
$filename = Select-String filename.txt -Pattern "Lokalisation"
$nameoffile = [System.IO.Path]::GetFileName($filename)
$split = $nameoffile.split('_')
$finalid = $split[5]
$content = Get-Content ZUB_Lokalisation.xml
$content[8] = ' <ids:BatchInfo ids:BatchID="{0}">' -f $finalid
$content | Set-Content ZUB_Lokalisation.xml
#Rename the file
Rename-Item ZUB_Lokalisation.xml -NewName $filename}
}catch [System.Exception]{zeigen
[System.Windows.Forms.MessageBox]::Show("ZUB_LOK_ERROR", "ERROR")}
}
Get-Job | Wait-Job | Where State -eq "Running"
}
Create files containing the start and end fragments that you want.
Then run this in a dos window or batch file:
COPY StartFile.TXT + YourXMLFile.TXT + EndFile.TXT OutputFile.TXT
This sticks the three files together and saves them as OutputFile.TXT
I am writing a super-easy script in PowerShell. The target of this script is to read a list of server names from a txt file and a command block from another txt file. The result of the operation shold be a third txt file containing the information.
Here some code:
cls
$usr = Read-Host "Please insert username, you'll be asked for password later"
$path = Read-Host "Insert a valid path for ServerList.txt file"
$serverList = Get-Content -Path $path | Out-String
$path = Read-Host "Insert a valid path fom Command.txt file"
$commandBlock = Get-Content -Path $path | Out-String
echo "Command: " $commandBlock "will be executed on " $serverList
echo "Press CTRL+Z to abort or"
pause
Invoke-Command -ComputerName $serverList -ScriptBlock { $commandBlock } -credential $usr
Serverlist.txt is a plain text containing something like "server1,server2,server3" and command.txt contain only this "Get-WmiObject Win32_BIOS | Select-Object SerialNumber"
Why the error is Invoke-Command : One or more computer names are not valid. If you are trying to pass a URI, use the -ConnectionUri parameter, or pass URI objects
instead of strings. ?
I even tried to substitute $serverlist with $serverlist.toString() but it's not working. I read somewhere that in this case $serverlist is an Array, how do I do to make everything work?
Consider that https://technet.microsoft.com/en-us/library/hh849719.aspx Invoke-Commands work with "server1,server2,server3" format if you put the string via console.
Your $serverList isn't a list, it's a single string of server1,server2 etc. To make it into an array, you can use -split to split the string by commas.
$serverList = Get-Content -Path $path | Out-String
$serverList = $serverList -split ","
For further understanding of why this doesn't work as you expect, please see the parsing and command syntax help files:
Get-Help about_Parsing
Get-Help about_Command_Syntax
$serverlist
When your text file contains the line server1,server2,server3, this command:
Get-Content -Path .\file.txt | Out-String
Just results in the string server1,server2,server3 and a newline - that's not a valid hostname.
Either format your text file like this (Get-Content automatically splits on line breaks):
server1
server2
server3
or split the string(s) from the file yourself:
$Serverlist = Get-Content -Path $Path | ForEach-Object { $_ -split "," }
$commandblock
For the command block part to work, you can't just drop a string into a ScriptBlock and expect it to execute - you need to recreate it as executable code:
$Code = Get-Content -Path $path -Raw
$CommandBlock = [scriptblock]::Create($Code)
# Now you can do this
Invoke-Command -ScriptBlock $CommandBlock
Is it possible to redirect stdout from an external program to a variable and stderr from external programs to another variable in one run?
For example:
$global:ERRORS = #();
$global:PROGERR = #();
function test() {
# Can we redirect errors to $PROGERR here, leaving stdout for $OUTPUT?
$OUTPUT = (& myprogram.exe 'argv[0]', 'argv[1]');
if ( $OUTPUT | select-string -Pattern "foo" ) {
# do stuff
} else {
$global:ERRORS += "test(): oh noes! 'foo' missing!";
}
}
test;
if ( #($global:ERRORS).length -gt 0 ) {
Write-Host "Script specific error occurred";
foreach ( $err in $global:ERRORS ) {
$host.ui.WriteErrorLine("err: $err");
}
} else {
Write-Host "Script ran fine!";
}
if ( #($global:PROGERR).length -gt 0 ) {
# do stuff
} else {
Write-Host "External program ran fine!";
}
A dull example however I am wondering if that is possible?
One option is to combine the output of stdout and stderr into a single stream, then filter.
Data from stdout will be strings, while stderr produces System.Management.Automation.ErrorRecord objects.
$allOutput = & myprogram.exe 2>&1
$stderr = $allOutput | ?{ $_ -is [System.Management.Automation.ErrorRecord] }
$stdout = $allOutput | ?{ $_ -isnot [System.Management.Automation.ErrorRecord] }
The easiest way to do this is to use a file for the stderr output, e.g.:
$output = & myprogram.exe 'argv[0]', 'argv[1]' 2>stderr.txt
$err = get-content stderr.txt
if ($LastExitCode -ne 0) { ... handle error ... }
I would also use $LastExitCode to check for errors from native console EXE files.
You should be using Start-Process with -RedirectStandardError -RedirectStandardOutput options. This other post has a great example of how to do this (sampled from that post below):
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "ping.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "localhost"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
This is also an alternative that I have used to redirect stdout and stderr of a command line while still showing the output during PowerShell execution:
$command = "myexecutable.exe my command line params"
Invoke-Expression $command -OutVariable output -ErrorVariable errors
Write-Host "STDOUT"
Write-Host $output
Write-Host "STDERR"
Write-Host $errors
It is just another possibility to supplement what was already given.
Keep in mind this may not always work depending upon how the script is invoked. I have had problems with -OutVariable and -ErrorVariable when invoked from a standard command line rather than a PowerShell command line like this:
PowerShell -File ".\FileName.ps1"
An alternative that seems to work under most circumstances is this:
$stdOutAndError = Invoke-Expression "$command 2>&1"
Unfortunately, you will lose output to the command line during execution of the script and would have to Write-Host $stdOutAndError after the command returns to make it "a part of the record" (like a part of a Jenkins batch file run). And unfortunately it doesn't separate stdout and stderr.
In case you want to get any from a PowerShell script and to pass a function name followed by any arguments you can use dot sourcing to call the function name and its parameters.
Then using part of James answer to get the $output or the $errors.
The .ps1 file is called W:\Path With Spaces\Get-Something.ps1 with a function inside named Get-It and a parameter FilePath.
Both the paths are wrapped in quotes to prevent spaces in the paths breaking the command.
$command = '. "C:\Path Spaces\Get-Something.ps1"; Get-It -FilePath "W:\Apps\settings.json"'
Invoke-Expression $command -OutVariable output -ErrorVariable errors | Out-Null
# This will get its output.
$output
# This will output the errors.
$errors
Copied from my answer on how to capture both output and verbose information in different variables.
Using Where-Object(The alias is symbol ?) is an obvious method, but it's a bit too cumbersome. It needs a lot of code.
In this way, it will not only take longer time, but also increase the probability of error.
In fact, there is a more concise method that separate different streams to different variable in PowerShell(it came to me by accident).
# First, declare a method that outputs both streams at the same time.
function thisFunc {
[cmdletbinding()]
param()
Write-Output 'Output'
Write-Verbose 'Verbose'
}
# The separation is done in a single statement.Our goal has been achieved.
$VerboseStream = (thisFunc -Verbose | Tee-Object -Variable 'String' | Out-Null) 4>&1
Then we verify the contents of these two variables
$VerboseStream.getType().FullName
$String.getType().FullName
The following information should appear on the console:
PS> System.Management.Automation.VerboseRecord
System.String
'4>&1' means to redirect the verboseStream to the success stream, which can then be saved to a variable, of course you can change this number to any number between 2 and 5.
Separately, preserving formatting
cls
function GetAnsVal {
param([Parameter(Mandatory=$true, ValueFromPipeline=$true)][System.Object[]][AllowEmptyString()]$Output,
[Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$firstEncNew="UTF-8",
[Parameter(Mandatory=$false, ValueFromPipeline=$true)][System.String]$secondEncNew="CP866"
)
function ConvertTo-Encoding ([string]$From, [string]$To){#"UTF-8" "CP866" "ASCII" "windows-1251"
Begin{
$encFrom = [System.Text.Encoding]::GetEncoding($from)
$encTo = [System.Text.Encoding]::GetEncoding($to)
}
Process{
$Text=($_).ToString()
$bytes = $encTo.GetBytes($Text)
$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes)
$encTo.GetString($bytes)
}
}
$all = New-Object System.Collections.Generic.List[System.Object];
$exception = New-Object System.Collections.Generic.List[System.Object];
$stderr = New-Object System.Collections.Generic.List[System.Object];
$stdout = New-Object System.Collections.Generic.List[System.Object]
$i = 0;$Output | % {
if ($_ -ne $null){
if ($_.GetType().FullName -ne 'System.Management.Automation.ErrorRecord'){
if ($_.Exception.message -ne $null){$Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$exception.Add($Temp)}
elseif ($_ -ne $null){$Temp=$_ | ConvertTo-Encoding $firstEncNew $secondEncNew;$all.Add($Temp);$stdout.Add($Temp)}
} else {
#if (MyNonTerminatingError.Exception is AccessDeniedException)
$Temp=$_.Exception.message | ConvertTo-Encoding $firstEncNew $secondEncNew;
$all.Add($Temp);$stderr.Add($Temp)
}
}
$i++
}
[hashtable]$return = #{}
$return.Meta0=$all;$return.Meta1=$exception;$return.Meta2=$stderr;$return.Meta3=$stdout;
return $return
}
Add-Type -AssemblyName System.Windows.Forms;
& C:\Windows\System32\curl.exe 'api.ipify.org/?format=plain' 2>&1 | set-variable Output;
$r = & GetAnsVal $Output
$Meta2=""
foreach ($el in $r.Meta2){
$Meta2+=$el
}
$Meta2=($Meta2 -split "[`r`n]") -join "`n"
$Meta2=($Meta2 -split "[`n]{2,}") -join "`n"
[Console]::Write("stderr:`n");
[Console]::Write($Meta2);
[Console]::Write("`n");
$Meta3=""
foreach ($el in $r.Meta3){
$Meta3+=$el
}
$Meta3=($Meta3 -split "[`r`n]") -join "`n"
$Meta3=($Meta3 -split "[`n]{2,}") -join "`n"
[Console]::Write("stdout:`n");
[Console]::Write($Meta3);
[Console]::Write("`n");