I have the following script:
function Export-sql-toExcel {
[CmdletBinding()]
Param (
[string]$scriptFile,
[string]$excelFile,
[string]$serverInstance,
[string]$database,
[string]$userName,
[string]$password
)
$tokens = ( [system.io.file]::ReadAllText( $scriptFile ) -split '(?:\bGO\b)' )
foreach ($token in $tokens) {
$token = $token.Trim()
if ($token -ne "") {
$lines = $token -split '\n'
$title = $lines[0]
if ($title.StartsWith("--")) {
$title = $title.Substring(2)
$title
}
Invoke-Sqlcmd -ServerInstance $serverInstance -Database $database -Username $userName -Password $password -Query $token |
Export-Excel -Path $excelFile -WorkSheetname $title -FreezeTopRow -ExcludeProperty RowError,RowState,Table,ItemArray,HasErrors
}
}
}
I have installed this function as a module. When I invoke the command from, for example, desktop folder, like this:
PS D:\Usuarios\mnieto\Desktop> Export-sql-toExcel -scriptFile .\EXPORT.txt -excelFile Excel.xlsx
I get the following error (the export.txt file is in the desktop folder):
Exception calling "ReadAllText" with "1" argument(s): "Can't find the file 'D:\Usuarios\<MyUserName>\EXPORT.txt'."
EDITED
if I debug and try [system.environment]::CurrentDirectory, it returns
'D:\Usuarios\<MyUserName>
That is because my script fails. NET functions and powershell don't share the 'current directory'
Any other way to get the content and parse the $scriptFile file?
I got the solution changing the NET call by a powershell command at this line
$content = Get-Content $scriptFile -Raw
$tokens = ( $content -split '(?:\bGO\b)' )
the trick was in the -Raw parameter, so the file is read as a single string
To my experience .dot NET functions don't like relative path's.
I'd use
$scriptfile = (Get-Item $Scriptfile).FullName
to resolve to a full path in the function just ahead :
$tokens = ( [system.io.file]::ReadAllText( $scriptFile ) -split '(?:\bGO\b)' )
I had the same situation, it was resolved when i added a dot "." to call the csv
$path2 = $path + ".\file.csv"
Or check for spaces at the $excelFile variable.
Related
My PowerShell script takes an argument to indicate if the output should have a header so I currently do this:
if ($null -eq $noHeader) {
Send-SQLDataToExcel -Connection $connection -mssqlserver -SQL $SQL -Path $output
} else {
Send-SQLDataToExcel -Connection $connection -mssqlserver -SQL $SQL -Path $output -NoHeader
}
Is there a better way that doesn't repeat code?
Thank you.
Yes, take a look at splatting to handle different parameter scenarios e.g.:
$splatHt = #{
connection=$connection
mssqlserver=$true
sql=$sql
path=$output
}
If ($noHeader){
$splatHt.add('NoHeader',$true)
}
Send-SQLDataToExcel #splatHt
As the only difference currently is a SWITCH parameter you could also do this:
$header = $false
If ($noHeader){
$header = $true
}
Send-SQLDataToExcel -Connection $connection -mssqlserver -SQL $SQL -Path $output -NoHeader:$header
I am trying since hourse to unpack an passwort-protected rar-archive. The passwort is unknown and should be determined based on the contents of a json file.
$content = Get-Content "C:\JDownloader
v2.0\cfg\org.jdownloader.extensions.extraction.ExtractionExtension.passwordlist.json"
$passwords = ConvertFrom-Json
$content $7ZipPath = '"C:\Program
Files\7-Zip\7z.exe"'
$zipFile = Get-Clipboard $output = Split-Path
$zipFile
Write-Host(Get-Clipboard);
foreach ($password in $passwords) {
7zip x -o$output -p$password $zipFile
}
}
If i instead use instead of the variable $password the plain-text-password, everyting is working as expected.
I am trying to update text inside a web.config file with powershell, but I am having a very, very difficult time.
The problem is this line
<add key="Models.Catalog.Repository" value=Server23 />
You see where the Server23 is? this value sometimes can be empty, which will be represented as "" or sometimes it can have a different name like "\Server25\path"
How do I tell PowerShell to replace/insert data if the field is empty/has some text in it?
here is the full code
$GP_AP= "Server1"
$NewMD = "\\JohnWick"
Invoke-Command -ComputerName $GP_AP -ScriptBlock {
$date= Get-Date -Format "dd_MM_yyyy_HHMM_ss"
$webfile = "C:\Program Files\TEST\IIS\Web.config"
(Copy-Item -Path $webfile -Destination $webfile_Test_$Date)
#[xml]$webconfigfile = Get-Content "C:\Program Files\TEST\IIS\Web.config"
#[xml]$2 = Get-Content "\\Server1\C$\Program Files\TEST\IIS\Web.config"
$olddata = "*"
#(Get-Content $webfile -raw).Replace("<add key=`"Models.Catalog.Repository`" value=$olddata />","<add key=`"Models.Catalog.Repository`" value=$Using:NewMD />") `
#| Set-Content "C:\Program Files\TEST\IIS\Web.config" -Force -Encoding UTF8 -ErrorAction Stop
(Get-Content $webfile -raw).Replace('<add key="Models.Catalog.Repository" value=Server23 />',"<add key=`"Models.Catalog.Repository`" value=$Using:NewMD />") `
| Set-Content "C:\Program Files\TEST\IIS\Web.config" -Force -Encoding UTF8 -ErrorAction Stop
} # end of script block
The script below will allow you to set values in a web config's <appSettings> based on key names. All the nodes must exist. This script will not create them. You'll obviously need to edit the script's default params.
param(
[string] $pathToWebConfig = "$PSScriptRoot\web.config"
[System.Collections.Hashtable] $appSettings = #{
someKey = 'somevalue';
someOtherKey = 'someOtherValue'
}
)
function UpdateXmlConfigAppSettings(
[Parameter(mandatory=$true)]
[string] $configFilePath,
[Parameter(mandatory=$true)]
[System.Collections.Hashtable] $appSettingsDict
) {
[xml] $webConfig = Get-Content $configFilePath
foreach($key in $appSettingsDict.Keys) {
$node = $webConfig.SelectSingleNode("//appSettings/add[#key = '$key']")
if ($null -ne $node) {
$node.SetAttribute('value', $appSettingsDict[$key])
}
else {
throw "$appSettingsDict[$key] is not a valid key in AppSettings"
}
}
$webConfig.Save($configFilePath)
}
UpdateXmlConfigAppSettings -configFilePath $pathToWebConfig -appSettingsDict $appSettings
$path = '\\server\c$\Program Files\TEST\IIS\web.config'
[xml]$config = Get-Content -Path $path -Raw
$stuffIwant = "\\Blah-244"
$config.SelectNodes("//add[#key='Models.Catalog.Repository']") | `
ForEach-Object { $_.SetAttribute("value", $stuffIwant) }
$config.Save($path)
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");
I have a script that I've been working on to provide parsing of SCCM log files. This script takes a computername and a location on disk to build a dynamic parameter list and then present it to the user to choose the log file they want to parse. Trouble is I cannot seem to get the ValidateSet portion of the dynamic parameter to provide values to the user. In addition the script won't display the -log dynamic parameter when attempting to call the function.
When you run it for the first time you are not presented with the dynamic parameter Log as I mentioned above. If you then use -log and then hit tab you’ll get the command completer for the files in the directory you are in. Not what you’d expect; you'd expect that it would present you the Logfile names that were gathered during the dynamic parameter execution.
PSVersion 5.1.14409.1012
So the question is how do I get PowerShell to present the proper Validate set items to the user?
If you issue one of the items in the error log you get the proper behavior:
Here are the two functions that i use to make this possible:
function Get-CCMLog
{
[CmdletBinding()]
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
if(test-path $FilePath)
{
$logs = gci "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
}
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
# Your code goes here
#dir -Path $Path
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
[PSCustomObject]#{"$($log)Log"=$results}
}
}
function Get-CMLog
{
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path -Path $File -Leaf
if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-cotnet -path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]#{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
The problem is that you have all the dynamic logic inside scriptblock in the if statement, and it handles the parameter addition only if the path provided contains a semicolon (':').
You could change it to something like:
if($path.ToCharArray() -contains ':') {
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
} else {
$FilePath = $path
}
and continue your code from there
PS 6 can do a dynamic [ValidateSet] with a class:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#dynamic-validateset-values