alter .txt file and save it with PowerShell - powershell

I am doing classic find&replace in some .txt file with powershell.
How can I "save" file at the end? I tried with | Set-Content but nothing happens.
Maybe I need to Add-Content first?
#Find what?
$optionBuilderStringToFind = "optionsBuilder.UseSqlServer"
$findUsingKeywordString = "using Microsoft."
#Replace with
$namespaceAdd = "using Microsoft.Extensions.Configuration;"
$optionBuilderConfigurable ="optionsBuilder.UseSqlServer(_configuration.GetConnectionString(`"Database`")
);"
gc -Path .\APSContext.cs | % {
if ($_ -match "using System;") {
$_ = $_ + "`n" + $namespaceAdd
#write-host $_
}
if ($_ -match "optionsBuilder.UseSqlServer") {
$_ = $optionBuilderConfigurable
#write-host $_
}
} | Set-Content -Path .\test.cs
Update: Here is testFile where I am looking to alter it. The content of the file is not important. I want to add another reference like "using Something.Something" and in the middle of the file to replace "optionsBuilder.UseSqlServer("dfjidfjljfiejf88");" with "optionsBuilder.UseSqlServer("_configtest");":
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("dfjidfjljfiejf88");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}
Maybe I need to use Out-String instead?

Any script block in Powershell can produce values. This is done by not storing a value in a variable:
{ $foo = 3 } produces nothing
{ 3 } produces the integer 3.
In the same way
ForEach-Object { $_ = "something" } produces nothing
ForEach-Object { $_ = "something"; $_ } produces the string "something"
Your loop body does not output anything, it's like the upper example above. Therefore, Set-Content has nothing to do. Modify the block to actually return the changed value of $_:
$replacements = #(
#{regex='using (System|Potentially|Others);'; replacement='using Microsoft.$1;' }
#{regex='optionsBuilder\.UseSqlServer\("[^"]*"\)'; replacement='optionsBuilder.UseSqlServer("Database")' }
# more search/replace pairs
)
Get-Content .\APSContext.cs -Encoding UTF8 | ForEach-Object {
foreach ($item in $replacements) {
$_ = $_ -replace $item.regex, $item.replacement
}
$_ # <--- this is what produces the line
} | Set-Content -Path .\test.cs -Encoding UTF8
That being said, never load or save text files without specifying their encoding. For C# source code files, I think UTF-8 is the default.
And that being said, modifying source code with regular expressions is not a good thing to do. If this is a one-off, fine. If you plan to do this on a regular basis, you are doing something wrong. Work with configuration files or environment variables instead of keeping in your codebase literal values that are subject to regular change.

as i mentioned - and as Tomalak pointed out - your mucking with the current pipeline object is the source of the glitch. [grin] this code fixes a logic error in your IF cascade [which may be better done with a switch block], removes the unwise $_ fiddling, and then ensures there is always some output.
# fake reading in a text file
# in real life, use Get-Content
$InStuff = #'
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("dfjidfjljfiejf88");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}
'# -split [environment]::NewLine
#Find what?
$optionBuilderStringToFind = "optionsBuilder.UseSqlServer"
$findUsingKeywordString = "using Microsoft."
#Replace with
$namespaceAdd = 'using Microsoft.Extensions.Configuration;'
$optionBuilderConfigurable ='optionsBuilder.UseSqlServer(_configuration.GetConnectionString("Database"));'
$InStuff | ForEach-Object {
if ($_ -match "using System;")
{
$_ + "`n" + $namespaceAdd
#write-host $_
}
elseif ($_ -match "optionsBuilder.UseSqlServer")
{
$optionBuilderConfigurable
#write-host $_
}
else
{
$_
}
}
output ...
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(_configuration.GetConnectionString("Database"));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}

Related

Anonymous Recursive Function

Is it possible to create an anonymous Recursive Function in PowerShell? (if yes, how?)
I have a recursive object and using a recursive function to drill down through the properties, like:
$Object = ConvertFrom-Json '
{
"Name" : "Level1",
"Folder" : {
"Name" : "Level2",
"Folder" : {
Name : "Level3"
}
}
}'
Function GetPath($Object) {
$Object.Name
if ($Object.Folder) { GetPath $Object.Folder }
}
(GetPath($Object)) -Join '\'
Level1\Level2\Level3
The function is relative small and only required ones, therefore I would like to directly invoke it as an anonymous function, some like:
(&{
$Object.Name
if ($Object.Folder) { ???? $Object.Folder }
}) -Join '\'
Is this possible in PowerShell?
If yes, how can I (as clean as possible) refer to the current function at ?????
Unfortunately there is not much documentation on this topic but you could execute the anonymous script block via $MyInvocation automatic variable, specifically it's ScriptInfo.ScriptBlock Property.
A simple example:
& {
param([int] $i)
if($i -eq 10) { return $i }
($i++)
& $MyInvocation.MyCommand.ScriptBlock $i
}
# Results in 0..10
Using your current code and Json provided in question:
(& {
param($s)
$s.Name; if ($s.Folder) { & $MyInvocation.MyCommand.ScriptBlock $s.Folder }
} $Object) -join '\'
# Results in Level1\Level2\Level3
Same as the above but using pipeline processing instead:
($Object | & {
process {
$_.Name; if($_.Folder) { $_.Folder | & $MyInvocation.MyCommand.ScriptBlock }
}
}) -join '\'
A bit more code but the same can be accomplished using a Collections.Queue instead of recursion, which is likely to be more resource efficient:
$(
$queue = [System.Collections.Queue]::new()
$queue.Enqueue($object)
while($queue.Count) {
$node = $queue.Dequeue()
$node.Name
if($node.Folder) { $queue.Enqueue($node.Folder) }
}
) -join '\'
#Santiago's helpful answer was exactly where I was initially looking for.
Nevertheless, it doesn't always require a recursive function to crawl through a recursive object.
As in the mcve, I could just have done:
#(
do {
$Object.Name
$Object = $Object.Folder
} while ($Object)
) -Join '\'

Powershell Global Variable usage as parameter to argument

$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.

Reference Static member in Static Method In a Powershell Class

Why does the direct access of a static fail, but the indirect works? Note that the file loaded is valid in both examples.
Failure Using Direct To Static
class OpPrj {
[string] $ProjectPath
static [string] $configFile = 'settings.json';
[OpPrj] static GetSettings(){
return [OpPrj](Get-Content [OpPrj]::configFile | Out-String|ConvertFrom-Json);
}
Works By Assigning to Local
class OpPrj {
[string] $ProjectPath
static [string] $configFile = 'settings.json';
[OpPrj] static GetSettings(){
$file = [OpPrj]::configFile
Write-Host $file # outputs settings.json
return [OpPrj](Get-Content $file | Out-String | ConvertFrom-Json);
}
You have a syntax error in your call to Get-Content:
Get-Content [OpPrj]::configFile
The PowerShell parser is unable to determine where this ends (I am uncertain of the reason), so you need to explicitly wrap it in parentheses (I also recommend being explicit about the parameter you're passing, especially in scripts, for readability):
Get-Content -Path ([OpPrj]::configFile)
You will need to follow this syntax for enums and static class members.
In all (your call to Out-String is unnecessary):
class OpPrj
{
[string] $ProjectPath
static [string] $ConfigFile = 'settings.json'
static [OpPrj] GetSettings()
{
return [OpPrj](Get-Content -Path ([OpPrj]::ConfigFile) -Raw | ConvertFrom-Json)
}
}

Using a variables string value in variable name

It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}

How remove accents in PowerShell?

I have a script which creates users in Microsoft Exchange Server and Active Directory. So, though it's commmon that user's names have accents or ñ in Spain, I want to avoid them for the username to not to cause any incompatibilities in old systems.
So, how could I clean a string like this?
$name = "Ramón"
To be like that? :
$name = "Ramon"
As per ip.'s answer, here is the Powershell version.
function Remove-Diacritics {
param ([String]$src = [String]::Empty)
$normalized = $src.Normalize( [Text.NormalizationForm]::FormD )
$sb = new-object Text.StringBuilder
$normalized.ToCharArray() | % {
if( [Globalization.CharUnicodeInfo]::GetUnicodeCategory($_) -ne [Globalization.UnicodeCategory]::NonSpacingMark) {
[void]$sb.Append($_)
}
}
$sb.ToString()
}
# Test data
#("Rhône", "Basíl", "Åbo", "", "Gräsäntörmä") | % { Remove-Diacritics $_ }
Output:
Rhone
Basil
Abo
Grasantorma
Well I can help you with some of the code.....
I used this recently in a c# project to strip from email addresses:
static string RemoveDiacritics(string input)
{
string inputFormD = (input ?? string.Empty).Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();
for (var i = 0; i < inputFormD.Length; i++)
{
UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(inputFormD[i]);
if (uc != UnicodeCategory.NonSpacingMark)
{
sb.Append(inputFormD[i]);
}
}
return (sb.ToString().Normalize(NormalizationForm.FormC));
}
I guess I can now say 'extending into a PowerShell script/form is left to the reader'.... hope it helps....
Another PowerShell translation of #ip for non C# coders ;o)
function Remove-Diacritics
{
param ([String]$sToModify = [String]::Empty)
foreach ($s in $sToModify) # Param may be a string or a list of strings
{
if ($sToModify -eq $null) {return [string]::Empty}
$sNormalized = $sToModify.Normalize("FormD")
foreach ($c in [Char[]]$sNormalized)
{
$uCategory = [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($c)
if ($uCategory -ne "NonSpacingMark") {$res += $c}
}
return $res
}
}
Clear-Host
$name = "Un été de Raphaël"
Write-Host (Remove-Diacritics $name )
$test = ("äâûê", "éèà", "ùçä")
$test | % {Remove-Diacritics $_}
Remove-Diacritics $test
With the help of the above examples I use this "one-liner:" in pipe (tested only in Win10):
"öüóőúéáűí".Normalize("FormD") -replace '\p{M}', ''
Result:
ouooueeui
PS> [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding(1251).GetBytes("Ramón"))
Ramon
PS>
Another solution... quickly "reuse" your C# in PowerShell (C# code credits lost somewhere on the net).
Add-Type -TypeDefinition #"
using System.Text;
using System.Globalization;
public class Utils
{
public static string RemoveDiacritics(string stIn)
{
string stFormD = stIn.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();
for (int ich = 0; ich < stFormD.Length; ich++)
{
UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(stFormD[ich]);
if (uc != UnicodeCategory.NonSpacingMark)
{
sb.Append(stFormD[ich]);
}
}
return (sb.ToString().Normalize(NormalizationForm.FormC));
}
}
"# | Out-Null
[Utils]::RemoveDiacritics("ABC-abc-ČŠŽ-čšž")
Instead of creating a stringbuilder and looping over characters, you can just use -replace on the NFD string to remove combining marks:
function Remove-Diacritics {
param ([String]$src = [String]::Empty)
$normalized = $src.Normalize( [Text.NormalizationForm]::FormD )
($normalized -replace '\p{M}', '')
}