This question already has answers here:
Runtime of Foreach-Object vs Foreach loop
(3 answers)
Closed 6 years ago.
I would like to ask you pretty easy question about foreach and output,
I noticed that while we are trying to run foreach and then export it by out-file it is more complicated, my question is what is the right way to export the content with foreach ?
For instance I prepare this:
Get-Content C:\Windows.old\1.txt
$output =
Foreach ($line in $file)
{
$line + " "+ $line.Length + " is the lengh of you user name"
}
And I know that is incorrect, I am looking for good explanation
Why is it more complicated? I would use the Foreach-Object cmdlet (but you can also go with the loop) and pipe the result to the Set-Content cmdlet (but you can also go with Out-File:
$filePath = 'C:\Windows.old\1.txt'
$content = Get-Content $filePath
$content | ForEach-Object {
"$_ $($_.Length) is the lengh of you user name"
} | Set-Content $filePath
Note: You may need to set the encoding.
Edit: Without changing the script:
Get-Content C:\Windows.old\1.txt
$output = Foreach ($line in $file)
{
$line + " "+ $line.Length + " is the lengh of you user name"
}
$output| Out-File 'your-new-filepath.txt'
Related
This question already has answers here:
Expanding variables in file contents
(3 answers)
Closed 3 years ago.
I have a number of .txt files that I use as standardized templates for Arista switch deployments, but I want to update them rapidly using PowerShell.
I replaced all the necessary values in these templates with $variables and I now am attempting to write a script to replace them.
I found a solution that I liked, but it is not working for me. I am not sure what I am doing wrong here.
https://stackoverflow.com/a/9326779/
This is a snippet from the source file :
router bgp $asn
router-id 10.1.1.1
bgp listen range 192.168.$id.0/25 peer-group cluster$id remote-as $asn
neighbor cluster$id peer-group
neighbor cluster$id update-source Loopback0
neighbor cluster$id description cluster$id-BGP
neighbor cluster$id ebgp-multihop 3
neighbor cluster$id maximum-routes 12000
network 10.1.1.1/32
exit
Here is a snippet from the powershell script :
$newvars = #{
'$id' = '101'
'$asn' = '12345'
}
$template = '.\Arista\arista.txt'
$destination_file = '.\switchconfig' + $id + '.txt'
Get-Content -Path $template | ForEach-Object {
$line = $_
$newvars.GetEnumerator() | ForEach-Object {
if ($line -match $_.Key)
{
$line = $line -replace $_.Key, $_.Value
}
}
$line
} | Set-Content -Path $destination_file
What I want is to have a group of variables defined (upwards of 30), and then replace each instance of that variable in the text file with the value contained in the script.
This solution seemed good, since it would avoid doing a "replace" over and over, but it just prints the file as it originally was.
Since powershell uses $ as an identifier for variables (reserved), you have to properly escape that when running your method. Following is a little off but does what you are looking for. update your dictionary with \ before your $ sign to replace text including $.
$newvars = #{
'\$id' = '101'
'\$asn' = '12345'
}
$template = "C:\temp\new.txt"
$destination_file = "C:\temp\replaced.txt"
$data = #()
foreach($line in Get-Content $template) {
foreach($key in $newvars.Keys) {
if ($line -match $key) {
$line = $line -replace $key, $newvars[$key]
}
}
$data += $line
}
$data | Out-File $destination_file
Another thing to note.. in your file you are defining $id as the name of the file. I am not sure where but that variable would always be null as its not defined yet (unless your snippet here is different from your actual code.
If you want to use Invoke-Expression, you can use it in the following way,
$id = '101'
$asn = '12345'
$template = (Get-Content "C:\temp\new.txt") | out-string
$data = Invoke-Expression "`"$template`""
$data | Out-File "C:\Temp\test.txt"
You'll have to make sure your variables ($id, $asn) have a value to replace when evaluating the variables within your text file.
I want to do this
read the file
go through each line
if the line matches the pattern, do some changes with that line
save the content to another file
For now I use this script:
$file = [System.IO.File]::ReadLines("C:\path\to\some\file1.txt")
$output = "C:\path\to\some\file2.txt"
ForEach ($line in $file) {
if($line -match 'some_regex_expression') {
$line = $line.replace("some","great")
}
Out-File -append -filepath $output -inputobject $line
}
As you can see, here I write line by line. Is it possible to write the whole file at once ?
Good example is provided here :
(Get-Content c:\temp\test.txt) -replace '\[MYID\]', 'MyValue' | Set-Content c:\temp\test.txt
But my problem is that I have additional IF statement...
So, what could I do to improve my script ?
You could do it like that:
Get-Content -Path "C:\path\to\some\file1.txt" | foreach {
if($_ -match 'some_regex_expression') {
$_.replace("some","great")
}
else {
$_
}
} | Out-File -filepath "C:\path\to\some\file2.txt"
Get-Content reads a file line by line (array of strings) by default so you can just pipe it into a foreach loop, process each line within the loop and pipe the whole output into your file2.txt.
In this case Arrays or Array List(lists are better for large arrays) would be the most elegant solution. Simply add strings in array until ForEach loop ends. After that just flush array to a file.
This is Array List example
$file = [System.IO.File]::ReadLines("C:\path\to\some\file1.txt")
$output = "C:\path\to\some\file2.txt"
$outputData = New-Object System.Collections.ArrayList
ForEach ($line in $file) {
if($line -match 'some_regex_expression') {
$line = $line.replace("some","great")
}
$outputData.Add($line)
}
$outputData |Out-File $output
I think the if statement can be avoided in a lot of cases by using regular expression groups (e.g. (.*) and placeholders (e.g. $1, $2 etc.).
As in your example:
(Get-Content .\File1.txt) -Replace 'some(_regex_expression)', 'great$1' | Set-Content .\File2.txt
And for the good example" where [MYID\] might be somewhere inline:
(Get-Content c:\temp\test.txt) -Replace '^(.*)\[MYID\](.*)$', '$1MyValue$2' | Set-Content c:\temp\test.txt
(see also How to replace first and last part of each line with powershell)
Guys i'm having some issues converting my Perl script to powershell, I need some help. In the host file of our machines, we have all of the URL's to our test environments blocked. In my PERL script, based on which environment is selected, it will comment out the line of the environment selected to allow access and block others so the testers can't mistakenly do things in the wrong environment.
I need help converting to powershell
Below is what I have in PERL:
sub editHosts {
print "Editing hosts file...\n";
my $file = 'C:\\Windows\\System32\\Drivers\\etc\\hosts';
my $data = readFile($file);
my #lines = split /\n/, $data;
my $row = '1';
open (FILE, ">$file") or die "Cannot open $file\n";
foreach my $line (#lines) {
if ($line =~ m/$web/) {
print FILE '#'."$line\n"; }
else {
if ($row > '21') {
$line =~ s/^\#*127\.0\.0\.1/127\.0\.0\.1/;
$line =~ s/[#;].*$//s; }
print FILE "$line\n"; }
$row++;
}
close(FILE);
}
Here is what i've tried in Powershell:
foreach ($line in get-content "C:\windows\system32\drivers\etc\hosts") {
if ($line -contains $web) {
$line + "#"
}
I've tried variation including set-content with what used to be in the host file, etc.
Any help would be appreciated!
Thanks,
Grant
-contains is a "set" operator, not a substring operator. Try .Contains() or -like.
This will comment out lines matching the variable $word, while removing # from non-matches (except the header):
function Edit-Hosts ([string]$Web, $File = "C:\windows\system32\drivers\etc\hosts") {
#If file exists and $web is not empty/whitespace
if((Test-Path -Path $file -PathType Leaf) -and $web.Trim()) {
$row = 1
(Get-Content -Path $file) | ForEach-Object {
if($_ -like "*$web*") {
#Matched PROD, comment out line
"#$($_)"
} else {
#No match. If past header = remove comment
if($row -gt 21) { $_ -replace '^#' } else { $_ }
}
$row++
} | Set-Content -Path $file
} else {
Write-Error -Category InvalidArgument -Message "'$file' doesn't exist or Web-parameter is empty"
}
}
Usage:
Edit-Hosts -Web "PROD"
This is a similar answer to Frode F.'s answer, but I'm not yet able to comment to add my 2c worth, so have to provide an alternative answer instead.
It looks like one of the gotchas moving from perl to PowerShell, in this example, is that when we get the content of the file using Get-Content it is an "offline" copy, i.e. any edits are not made directly to the file itself. One approach is to compile the new content to the whole file and then write that back to disk.
I suppose that the print FILE "some text\n"; construct in perl might be similar to "some text" | Out-File $filename -Encoding ascii -Append in PowerShell, albeit you would use the latter either (1) to write line-by-line to a new/empty file or (2) accept that you are appending to existing content.
Two other things about editing the hosts file:
Be sure to make sure that your hosts file is ASCII encoded; I have caused a major outage for a key enterprise application (50k+ users) in learning that...
You may need to remember to run your PowerShell / PowerShell ISE by right-clicking and choosing Run as Administrator else you might not be able to modify the file.
Anyway, here's a version of the previous answer using Out-File:
$FileName = "C:\windows\system32\drivers\etc\hosts"
$web = "PROD"
# Get "offline" copy of file contents
$FileContent = Get-Content $FileName
# The following creates an empty file and returns a file
# object (type [System.IO.FileInfo])
$EmptyFile = New-Item -Path $FileName -ItemType File -Force
foreach($Line in $FileContent) {
if($Line -match "$web") {
"# $Line" | Out-File $EmptyFile -Append -Encoding ascii
} else {
"$Line" | Out-File $EmptyFile -Append -Encoding ascii
}
}
Edit
The ($Line -match "$web") takes whatever is in the $web variable and treats it as a regular expression. In my example I was assuming that you were just wanting to match a simple text string, but you might well be trying to match an IP address, etc. You have a couple of options:
Use ($Line -like "*$web*") instead.
Convert what is in $web to be an escaped regex, i.e. one that will match literally. Do this with ($Line -match [Regex]::Escape($web)).
You also wanted to strip off comments from any line past row 21 of the hosts file, should that line not match $web. In perl you have used the s substitution operator; the PowerShell equivalent is -replace.
So... here is an updated version of that foreach loop:
$LineCount = 1
foreach($Line in $FileContent) {
if($Line -match [Regex]::Escape($web) {
# ADD comment to any matched line
$Line = "#" + $Line
} elseif($LineCount -gt 21) {
# Uncomment the other lines
$Line = $Line -replace '^[# ]+',''
}
# Remove 'stacked up' comment characters, if any
$Line = $Line -replace '[#]+','#'
$Line | Out-File $EmptyFile -Append -Encoding ascii
$LineCount++
}
More Information
Are there good references for moving from Perl to Powershell?
How to use operator '-replace' in PowerShell to replace strings of texts with special characters and replace successfully
about_Comparison_Operators
http://www.comp.leeds.ac.uk/Perl/sandtr.html
If you wanted to verify what was in there and then add entries, you could use the below which is designed to be ran interactively and returns any existing entries you specify in the varibles:
Note: the `t is powershell's in script method for 'Tab' command.
$hostscontent
# Script to Verify and Add Host File Entries
$hostfile = gc 'C:\Windows\System32\drivers\etc\hosts'
$hostscontent1 = $hostfile | select-string "autodiscover.XXX.co.uk"
$hostscontent2 = $hostfile | select-string "webmail.XXX.co.uk"
$1 = "XX.XX.XXX.XX`tautodiscover.XXX.co.uk"
$2 = "webmail.XXX.co.uk"
# Replace this machines path with a path to your list of machines e.g. $machines = gc \\machine\machines.txt
$machines = gc 'c:\mytestmachine.txt'
ForEach ($machine in $machines) {
If ($hostscontent1 -ne $null) {
Start-Sleep -Seconds 1
Write-Host "$machine Already has Entry $1" -ForegroundColor Green
} Else {
Write-Host "Adding Entry $1 for $machine" -ForegroundColor Green
Start-Sleep -Seconds 1
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "XX.XX.XXX.XX`tautodiscover.XXX.co.uk" -Force
}
If ($hostscontent2 -ne $null) {
Start-Sleep -Seconds 1
Write-Host "$machine Already has Entry $2" -ForegroundColor Green
} Else {
Write-Host "Adding Entry $2 for $machine" -ForegroundColor Green
Start-Sleep -Seconds 1
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "XX.XX.XXX.XX`twebmail.XXX.co.uk" -Force
}
}
$check = $args[1]
$numArgs = $($args.count)
$totMatch = 0
#reset variables for counting
for ( $i = 2; $i -lt $numArgs; $i++ )
{
$file = $args[$i]
if ( Test-Path $file ) {
#echo "The input file was named $file"
$match = #(Select-String $check $file -AllMatches | Select -Expand Matches | Select -Expand Value).count
echo "There were $match Matches in $file"
echo "There were $match Matches in $file" >> Output.txt
$totMatch = $totMatch + $match
}
else {
echo "File $file does not exist"
echo "File $file does not exist" >> Output.txt
}
}
echo "Total Matches Found: $totMatch"
Esentially i created a quick app to find the word searched and check the instances in the file, would anyone know how to edit this to send the whole Line that the word was found in to the Ouput.txt file, So rather on top of instances add the whole line itself? Thanks in advance
I can't see that your code works properly; even though you don't say how it's supposed to work (why is $check taken from args[1] instead of args[0]?).
Your Select-String line is getting the matching lines, then doing some selecting which throws away the line data you want, but doesn't seem to be necessary.
I've reworked it as:
$check = $args[0]
$totalMatches = 0
foreach ( $file in $args[1..$args.Length] )
{
if ( Test-Path $file ) {
$matches = Select-String $check $file -AllMatches -SimpleMatch
Write-Output "There were $($matches.Count) Matches in $file" | Tee-Object -FilePath "output.txt" -Append
foreach ($match in $matches) {
Write-Output $match.Line | Tee-Object -FilePath "output.txt" -Append
}
Write-Host
$totalMatches = $totalMatches + $matches.Count
}
else {
Write-Output "File $file does not exist" | Tee-Object -FilePath "output.txt" -Append
}
}
echo "Total Matches Found: $totalMatches"
Changes:
Take the $check as the first argument
Iterate over the arguments directly instead of counting through them
Added -SimpleMatch so it doesn't work with regexes, since you didn't mention them
Removed the select-object -expand bits, just grab the select-string results
Loop through the results and get the line from $match.line
Added Tee-Object which both writes to screen and to file in one line
I'm trying to get all of this to be on one line, but everything I try doesn't work. I tried `b but that doesn't backspace you to the line above. I tried putting the condition inline with the first Add-Content, but that didn't work either.
Add-Content $txtpath "p: $strPhone x $strXTEN | "
if ($strMobile -ne $null) { Add-Content $txtpath "`m: strMobile | " }
Add-Content $txtpath "$strFax"
You can't use multiple Add-Content calls because each one will append a newline. I logged a suggestion a long time ago for a NoNewline parameter on Add-Content. You can vote for it here.
You can use a StringBuilder for this and then output its contents via Add-Content or Set-Content:
$sb = new system.text.stringbuilder
[void]$sb.Append("p: $strPhone x $strXTEN | ")
if ($strMobile -ne $null) { [void]$sb.Append("`m: strMobile | ") }
[void]$sb.Append($strFax)
Add-Content $txtPath $sb.ToString()
One way to do what I think you are wanting to do is to use the -f format operator, like so:
$text = "p: $strPhone x $strXTEN {0}" -f $( if ($strMobile) {"| m: $strMobile"})
Add-Content $txtpath $text
It looks like Microsoft listened and added the -NoNewLine Parameter to Add-Content back in 2015:
https://devblogs.microsoft.com/scripting/the-powershell-5-nonewline-parameter/