What is an equivalent of *Nix 'cut' command in Powershell? - powershell

I have following content in a configuration file (sample.cfg),
Time_Zone_Variance(Mins):300
Alert_Interval(Mins):2
Server:10.0.0.9
Port:1840
I'm trying to store an each values after the : by using split in PowerShell. but i'm not able to produce require output.
Can someone tell me how to use PowerShell split for the above problem ?

You can read the contents of the file using Get-Content, then pipe each line through ForEach-Object, then use the split command on each line, taking the second item in the array as follows:
$filename = "sample.cfg"
Get-Content $filename | ForEach-Object {
$_.split(":")[1]
}
Output
300
2
10.0.0.9
1840
Update
I prefer the approach by #AnsgarWiechers, but if you really need specifically named values you could create a hashtable and replace the name with the value:
$configValues = #{
hour = "Time_Zone_Variance(Mins)"
min = "Alert_Interval(Mins)"
server = "Server"
port = "Port"
}
Get-Content $filename | ForEach-Object {
# Courtesy of Ansgar Wiechers
$key, $value = $_ -split ':', 2
foreach($configValuesKey in $($configValues.keys)) {
if ($configValues[$configValuesKey] -eq $key)
{
$configValues[$configValuesKey] = $value
}
}
}
write-host "`nAll Values:"
$configValues
write-host "`nIndividual value:"
$configValues.port
Output
All Values:
Name Value
---- -----
port 1840
min 2
server 10.0.0.9
hour 300
Individual value:
1840

How's this?
function cut {
param(
[Parameter(ValueFromPipeline=$True)] [string]$inputobject,
[string]$delimiter='\s+',
[string[]]$field
)
process {
if ($field -eq $null) { $inputobject -split $delimiter } else {
($inputobject -split $delimiter)[$field] }
}
}
PS C:\> 'hi:there' | cut -f 0 -d :
hi
PS C:\> 'hi:there' | cut -f 1 -d :
there
PS C:\> 'hi:there' | cut -f 0,1 -d :
hi
there
PS C:\> 'hi:::there' | cut -f 0 -d :+
hi
PS C:\> 'hi there' | cut
hi
there

For a more succint syntax, this will also do the trick:
((Get-Content "your-file.txt") -Split ":")[1]
So the trick to use the -Split method is to have a String object returned by Get-Content (alias cat can also be used, actually), and from the resulting String[] object you can use the brackets to extract the nth item.
Note: Using -Split without parenthesis around Get-Content won't work since -Split is not a parameter name for that command... 🤷‍♂️

I suppose you don't want to just split the lines, but actually create key/value pairs. That could be achieved like this:
$config = #{}
Get-Content 'C:\path\to\sample.cfg' | % {
$key, $value = $_ -split ':', 2
$config[$key] = $value
}
You could also use the ConvertFrom-StringData cmdlet:
Get-Content 'C:\path\to\sample.cfg' | % {
ConvertFrom-StringData ($_ -replace ':','=')
}
The -replace operation is necessary, because ConvertFrom-StringData expects key and value to be separated by =. If you could change the delimiter in the config file from : to =, you could use ConvertFrom-StringData $_ without replacement.

Related

Powershell : Find a group of text in a file then extract a specific line in that group of text

I've been on this for few days now, I'm trying to parse multiple text files containing data like this :
[Cluster1]
GatewayIp=xx.xxx.xxx.xx
IpAddress=xx.xxx.xxx.x
MTU=0000
NetMask=xxx.xxx.xxx.0
Port=xxx
Protocol=xxxx/xxxxx
Sessions=xxxxxx
Bands=xxx, xxx, x
Binding=xxxxx
GroupNumber=x
InitQueue=xxxxxx
Interface=xxxxxx
Process=xxx
SupportsCar=No
SupportsCom=Yes
SupportsPos=Yes
SupportsXvd=No
[Cluster2]
GatewayIp=xx.xxx.xxx.xx
IpAddress=xx.xxx.xxx.x
MTU=0000
NetMask=xxx.xxx.xxx.0
Port=xxx
Protocol=xxxx/xxxxx
Sessions=xxxxxx
Bands=xxx, xxx, x
Binding=xxxxx
GroupNumber=x
InitQueue=xxxxxx
Interface=xxxxxx
Process=xxx
SupportsCar=No
SupportsCom=No
SupportsPos=No
SupportsXvd=Yes
I want to extract the "IpAddress" in the section where thoses lines are present :
SupportsCom=Yes
SupportsPos=Yes
The thing is, I've tried using -context to grab the nth line after the section name "[Cluster1]", but that section name is different from file to file ...
$ip = Select-String -Path "$location" -Pattern "\[Cluster1\]" -Context 0,2 |
Foreach-Object {$_.Context.PostContext}
I've tried using the Precontext to grab the Nth line before SupportsCom=Yes, but the line position of "IpAddress=" is different from file to file ...
$ip = Select-String -Path "$location" -Pattern " SupportsCom=Yes" -Context 14,0 |
Foreach-Object { $_.Line,$_.Context.PreContext[0].Trim()}
Is there a way to grab the section containing "SupportsCom=Yes" knowing that the section is delimited by a blank line above and below, then search in that section a string that contains "IpAddress=" then return the value afterthe "=" ?
Ok, since you are not allowed to use a module (perhaps later..), this should get you what you want
# change the extension in the Filter to match that of your files
$configFiles = Get-ChildItem -Path 'X:\somewhere' -Filter '*.ini' -File
$result = foreach ($file in $configFiles) {
# initialize these variables to $null
$IpAddress = $supportsCom = $supportsPos = $null
# loop through the file line by line and try regex matches on them
switch -Regex -File $file {
'^\[([^\]]+)]' {
# did we get all wanted entries from the previous cluster?
if ($IpAddress -and $supportsCom -and $supportsPos) {
if ($supportsCom -eq 'Yes' -and $supportsPos -eq 'Yes') {
# just output the IpAddress so it gets collected in variable $result
$IpAddress
}
# reset the variables to $null
$IpAddress = $supportsCom = $supportsPos = $null
}
# start a new cluster
$cluster = $matches[1]
}
'^\s+IpAddress\s*=\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' { $IpAddress = $matches[1]}
'^\s+SupportsCom\s*=\s*(Yes|No)' { $supportsCom = $matches[1] }
'^\s+SupportsPos\s*=\s*(Yes|No)' { $supportsPos = $matches[1]}
}
}
# show results on screen
$result
# or save as text file
$result | Set-Content -Path 'X:\somewhere\IpAddresses.txt'
Updated answer:
If you don't care about the name of the section(s), where IpAddress is found in, you can use this "one-liner" (broken into multiple lines for readability):
$ip = (Get-Content $location -Raw) -split '\[.+?\]' |
ConvertFrom-StringData |
Where-Object { $_.SupportsCom -eq 'Yes' -and $_.SupportsPos -eq 'Yes' } |
ForEach-Object IpAddress
The Get-Content line reads the input file as a single multi-line string and splits it at the section headers (e. g. [Cluster1]).
ConvertFrom-StringData converts the Key = Value lines into one hashtable per section.
For each hashtable, Where-Object checks whether it contains SupportsCom=Yes and SupportsPos=Yes
ForEach-Object IpAddress is shorthand for writing Select-Object -ExpandProperty IpAddress which gives you the actual value of IpAddress instead of an object that contains a member named IpAddress.
Note that $ip can be either a single string value or an array of strings (if there are multiple matching sections).
Original answer:
You could also write a general-purpose function that converts INI sections into objects. This enables you to use the pipeline with a simple Where-Object statement to get the data you are interested in.
Generic function to output INI sections as objects, one by one:
Function Read-IniObjects {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)] [String] $Path
)
process {
$section = #{} # A hashtable that stores all properties of the currently processed INI section.
# Read file line by line and match each line by the given regular expressions.
switch -File $Path -RegEx {
'^\s*\[(.+?)\]\s*$' { # [SECTION]
# Output all data of previous section
if( $section.Count ) { [PSCustomObject] $section }
# Create new section data
$section = [ordered] #{ IniSection = $matches[ 1 ] }
}
'^\s*(.+?)\s*=\s*(.+?)\s*$' { # KEY = VALUE
$key, $value = $matches[ 1..2 ]
$section.$key = $value
}
}
# Output all data of last section
if( $section.Count ) { [PSCustomObject] $section }
}
}
Usage:
$ip = Read-IniObjects 'test.ini' |
Where-Object { $_.SupportsCom -eq 'Yes' -and $_.SupportsPos -eq 'Yes' } |
ForEach-Object IpAddress
Notes:
The INI file is parsed using the switch statement, which can directly use a file as input. This is much faster than using a Get-Content loop.
As we are using -RegEx parameter, the switch statement matches each line of the file to the given regular expressions, entering the case branches only if the current line matches.
Get detailed explanation about how the RegEx's work:
match lines like [Section] -> RegEx101
match lines like Key = Value -> RegEx101
ForEach-Object IpAddress is shorthand for writing Select-Object -ExpandProperty IpAddress which gives you the actual value of IpAddress instead of an object that contains a member named IpAddress.
Note that $ip can be either a single string value or an array of strings (if there are multiple matching sections).

Powershell Get-Content specific content inside text

I receive a text file with a multiple lists like shown below (edit: more accurate example dataset included)
# SYSTEM X
# SINGULAR
192.168.1.3
# SUB-SYSTEM V
192.168.1.4
192.168.1.5
192.168.1.6
# SYSTEM Y
# MANDATORY
192.168.1.7
192.168.1.8
192.168.1.9
192.168.1.7
192.168.1.8
192.168.1.9
Each "SYSTEM comment" means its a new set after it.
I want to read each block of content separately so each set should be assigned to an object discarding the embedded comments. I just need the IPs.
Something like:
$ipX = get-content -path [file.txt] [set X]
$ipY = get-content -path [file.txt] [set Y]
$ipZ = get-content -path [file.txt] [set Z]
But I'm not sure how to actually assign these sets separately.
Help please.
Here's one possible solution. The result will be a hashtable, each key containing any array of ips for the set:
$result = #{}
get-content file.txt | foreach {
if ($_ -match "#\s*SET\s+(\w+)") {
$result[($key = $matches.1)] = #()
}
elseif ($_ -notlike "#*") {
$result[$key] += $_
}
}
Contents of $result:
Name Value
---- -----
Y {[ip], [ip], [more ips]}
Z {[ip], [ip], [more ips]}
X {[ip], [ip], [more ips]}
Here's another approach. We will take advantage of Foreach-Object's -End block to [PSCustomObject] the final one.
Get-Content $file | Foreach-Object {
if($_ -match 'SET (.+?)'){
if($ht){[PSCustomObject]$ht}
$ht = [ordered]#{Set = $Matches.1}
}
if($_ -match '^[^#]'){
$ht["IPs"] += $_
}
} -End {if($ht){[PSCustomObject]$ht}}
Output
Set IPs
--- ---
X [ip][ip][more ips]
Y [ip][ip][more ips]
Z [ip][ip][more ips]
If you want to also ensure $ht is empty to start with you could use the -Begin block.
Get-Content $file | Foreach-Object -Begin{$ht=$null}{
if($_ -match 'SET (.+?)'){
if($ht){[PSCustomObject]$ht}
$ht = [ordered]#{Set = $Matches.1}
}
if($_ -match '^[^#]'){
$ht["IPs"] += $_
}
} -End {if($ht){[PSCustomObject]$ht}}
You can use Select-String to extract a specific section of text:
# Update $section to be the set you want to target
$section = 'Set Y'
Get-Content a.txt -Raw |
Select-String -Pattern "# $section.*\r?\n(?s)(.*?)(?=\r?\n# Set|$)" | Foreach-Object
{$_.Matches.Groups[1].Value}
Using Get-Content with -Raw reads in the file as a single string making multi-line matching easier. With PowerShell 7, Select-String includes a -Raw switch making this process a bit simpler.
This outputs capture group 1 results, which match the (.*?). If you want to capture between comments rather than between Set <something> and Set <something>, you can edit the -Pattern value at the end to only be # rather than # Set.
Regex Breakdown:
# matches the characters # literally
$section substitutes your variable value matches the value literally provided there are no regex characters in the string
.* matches any character (except for line terminators)
\r matches a carriage return
? Quantifier — Matches between zero and one times, as many times as
possible, giving back as needed (greedy)
\n matches a line-feed (newline) character
(?s) modifier: single line. Dot matches newline characters
1st Capturing Group (.*?)
.*? matches any characters lazily
Positive Lookahead (?=\r?\n# Set)
\r? matches a carriage return zero or more times
\n matches a line-feed (newline) character
# Set matches the characters # Set literally
$ matches the end of the string
If I understand the question with the new example correctly, you want to parse out the file and create single variables of that each holding an array ip IP addresses.
If that is the case, you could do:
# loop through the file line-by-line
$result = switch -Regex -File 'D:\Test\thefile.txt' {
'#\sSYSTEM\s(\w+)' {
# start a new object, output the earlier object if available
if ($obj) { $obj }
$obj = [PsCustomObject]#{ 'System' = $Matches[1]; 'Ip' = #() }
}
'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' {
# looks like an IPv4 address. Add it to the Ip property array of the object
$obj.Ip += $_
}
default {}
}
Now you have an array ob objects in $result:
System Ip
------ --
Y {192.168.1.7, 192.168.1.8, 192.168.1.9, 192.168.1.7...}
X {192.168.1.3, 192.168.1.4, 192.168.1.5, 192.168.1.6}
To make separate variables of that is as easy as:
$ipX = ($result | Where-Object { $_.System -eq 'X' }).Ip
$ipY = ($result | Where-Object { $_.System -eq 'Y' }).Ip
$ipZ = ($result | Where-Object { $_.System -eq 'Z' }).Ip
Your example has duplicate IP addresses. If you don't want these do
$ipX = ($result | Where-Object { $_.System -eq 'X' }).Ip | Select-Object -Unique (same for the others)

How can I use PowerShell to import a file, and replace all existing placeholder variables with ones defined in the script? [duplicate]

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.

Get-Content with Wait and processing each line

Using -Match gives me True/False values instead of the lines of text.
Get-Content ("\\path”) -tail 10 -wait | % {
foreach ($data in ($_ -match "Execute")) {
$First = $data.Substring(26,37).Trim()
Write-Host $First
}
}
I used below code without -tail -wait to do what I, but I can't change to parsing the file using -tail with Get-Content.
$DB = Get-Content ("\\path”) -tail -ReadCount 5000000 | foreach { $_ -match "string to match" } | foreach{ $_.Trim()}
foreach ($Data in $DB) {
$First = $Data.Substring(26,37).Trim()
$Second = $Data
Write-Host $First
Write-Host $Second
}
As a workaround, you can array-cast your $_, like this:
foreach ($data in (,$_ -match "Execute")) {
Here's the output difference:
$data=#("bla bla","foo bla","foo bar","bla bar")
PS > $data | % { foreach ($a in ($_ -match "bla")){$a}}
True
True
False
True
PS > $data | % { foreach ($a in (,$_ -match "bla")){$a}}
bla bla
foo bla
bla bar
The -match expression returns a boolean result, but also updates a special variable, $matches.
You can filter lines by using -match in a where-object expression to return only those lines which match.
$myFilteredArray = $myArray | where-object{$_ -match 'execute'}
If using it for something that simple though, you may as well use -like:
$myFilteredArray = $myArray | where-object{$_ -like '*execute*'}
If you want to be clever, you can also use regular expressions; that way $matches will hold all captured groups (including a special group named 0 which holds the original line).
Simple Example / plain text search
#(
'line 1 - please '
,'line 2 - execute'
,'line 3 - this'
,'line 4 - script'
) | where-object {$_ -match 'execute'} | foreach-object{
"-Match found the following matches:"
$matches
"The line giving this result was"
$_
}
Example using Regex / capturing group
#(
'line 1 - please '
,'line 2 - execute'
,'line 3 - this'
,'line 4 - script'
) | where-object {$_ -match '(?<capturedData>.*?)(?: - )execute'} | foreach-object{
"-Match found the following matches:"
$matches
"The line giving this result was"
$_
}
-match: https://ss64.com/ps/syntax-regex.html
-like, -match, and other comparisons: https://ss64.com/ps/syntax-compare.html
Well, -match when applied to a scalar returns a boolean result. When applied to an array it returns the matching elements. In your second example you use ReadCount to force Get-Content to send more than a line through the pipeline at a time. Of course you're then getting an array of lines every time instead of just a line, thus changing the semantics of -match.
To make your first example work, simply change foreach into if.

Retrieving second part of a line when first part matches exactly

I used the below steps to retrieve a string from file
$variable = 'abc#yahoo.com'
$test = $variable.split('#')[0];
$file = Get-Content C:\Temp\file1.txt | Where-Object { $_.Contains($test) }
$postPipePortion = $file | Foreach-Object {$_.Substring($_.IndexOf("|") + 1)}
This results in all lines that contain $test as a substring. I just want the result to contain only the lines that exactly matches $test.
For example, If a file contains
abc_def|hf#23$
abc|ohgvtre
I just want the text ohgvtre
If I understand the question correctly you probably want to use Import-Csv instead of Get-Content:
Import-Csv 'C:\Temp\file1.txt' -Delimiter '|' -Header 'foo', 'bar' |
Where-Object { $_.foo -eq $test } |
Select-Object -Expand bar
To address the exact matching, you should be testing for equality (-eq) rather than substring (.Contains()). Also, there is no need to parse the data multiple times. Here is your code, rewritten to to operate in one pass over the data using the -split operator.
$variable = 'abc#yahoo.com'
$test = $variable.split('#')[0];
$postPipePortion = (
# Iterate once over the lines in file1.txt
Get-Content C:\Temp\file1.txt | foreach {
# Split the string, keeping both parts in separate variables.
# Note the backslash - the argument to the -split operator is a regex
$first, $second = ($_ -split '\|')
# When the first half matches, output the second half.
if ($first -eq $test) {
$second
}
}
)