Log PowerShell commands to LogFile - powershell

I have basic PowerShell script with logging function and some commands to run.
I'm looking for a solution of logging into log file commands what are executed.
For now I only know this, but its quite annoying to copy + paste all command to have been logged:
$LogPath = "C:\Logs"
$FileName = (Get-Item $PSCommandPath).Basename
$LogFile = $LogPath + "\" + $FileName + ".log"
Function WriteLog
{
Param ([string]$LogString)
$Stamp = (Get-Date).toString("yyyy-MM-dd HH:mm:ss")
$LogMessage = "$Stamp $LogString"
Add-content $LogFile -value $LogMessage
}
WriteLog "***********************"
WriteLog ""
WriteLog "Command1"
Command1
WriteLog "Command2"
Command2
WriteLog "Command3"
Command3
WriteLog "Command4"
Command4
WriteLog "Command5"
Command5
WriteLog ""
WriteLog "***********************"

I suggest the following:
Modify your function to alternatively accept a script block ({ ... }) representing the command to execute.
If a script block is given, use its string representation as the log message, and then execute it.
# Create the logging function in a *dynamic module* (see below).
# Place this at the top of your script.
$null = New-Module {
# Define the log-file path.
$LogPath = 'C:\Logs'
$FileName = (Get-Item $PSCommandPath).Basename
$LogFile = $LogPath + '\' + $FileName + '.log'
# Create / truncate the file.
New-Item -Force -ErrorAction Stop $LogFile
function Add-LogMessage {
[CmdletBinding(DefaultParameterSetName = 'String')]
param(
[Parameter(Position = 0, Mandatory, ParameterSetName = 'ScriptBlock')]
[scriptblock] $ScriptBlock
,
[Parameter(Position = 0, ParameterSetName = 'String')]
[string] $String
)
# If a script block was given, use its string representation
# as the log string.
if ($ScriptBlock) {
# Make the string representation single-line by replacing newlines
# (and line-leading whitespace) with "; "
$String = $ScriptBlock.ToString().Trim() -replace '\r?\n *', '; '
}
# Create a timestamped message and append it to the log file.
$stamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$logMessage = "$stamp $String"
Add-Content -LiteralPath $LogFile -Value $logMessage
# If a script block was given, execute it now.
if ($ScriptBlock) {
# Because this function is defined in a (dynamic) module,
# its invocation doesn't create a child scope of the *caller's* scope,
# and invoking the given script block, which is bound to the caller's scope,
# with . (dot-sourcing) runs it *directly in the caller's scope*.
. $ScriptBlock
}
}
}
Note:
The function name adheres to PowerShell's verb-noun convention, using Add, which is an approved verb; however, for brevity the aspect of situationally also performing execution (for which Invoke would be the approved verb) is not reflected in the name.
Your script would then look something like this:
Add-LogMessage "***********************"
Add-LogMessage ""
Add-LogMessage { Command1 }
Add-LogMessage { Command2 }
# ...
Add-LogMessage "***********************"
Note:
By placing the function inside a (dynamic, transient) module created via New-Module, its invocation does not create a child scope of the caller's scope.
When a script block created by a literal ({ ... }) in the caller's scope is passed, it can then be invoked with ., the dot-sourcing operator, which executes it directly in the caller's scope, which means that the script block's code is free to modify the script's variables, the same way that placing that code directly in the script would.
If you want the function to also log a given script block's output (while still printing it to the display), you can use Tee-Object as follows (for simplicity I'm assuming the same target log file, adjust as needed):
. $ScriptBlock | Tee-Object -Append -FilePath $LogFile
Caveat: As of PowerShell 7.2.x, Tee-Object uses a fixed character encoding, namely UTF-16LE ("Unicode") in Windows PowerShell and BOM-less UTF-8 in PowerShell (Core) 7+. GitHub issue #11104 suggests adding an -Encoding parameter (which only future PowerShell (Core) versions would benefit from).
Therefore, if you're using Windows PowerShell and you're targeting the same log file for capturing the output, be sure to modify the Add-Content call with -Encoding Unicode as follows:
Add-Content -Encoding Unicode -LiteralPath $LogFile -Value $logMessage
Alternatively, if you want to avoid UTF-16LE ("Unicode") files for their size (with all-ASCII characters, they're twice the size of ANSI and UTF-8 files), you can use one of the workarounds discussed in this answer.

Related

How to pass the full path of a file as an argument to a new instance of Powershell?

This is the code of my script that calls another script:
$LocalFolder = "$env:USERPROFILE\Desktop\Banana Productions"
$RenderClient = "$LocalFolder\Render\Modelo 02\Cliente 02"
$CutFolder = "$RenderClient\Cortar"
$FFMpegScript = "$CutFolder\ffmpeg crop.ps1"
gci "$RenderClient\Cortar" *.mp4 -File -Recurse | ForEach-Object {
$FilePath = $_.FullName
start PowerShell "-NoExit", "-File", "`"$FFMpegScript`"", "$FilePath"
Write-Host $FilePath
}
The issue is that I am not able to pass the argument with the value of $_.FullName to the new instance, I get an error message in the new instance with the message:
Cannot process argument because the value of argument "name" is not valid
This is all that's in the script I'm calling:
param($args)
Write-Host $args[0]
Read-Host -Prompt "Press Enter to continue"
How can I resolve this?
Due to a long-standing bug in Start-Process - see GitHub issue #5576 - it is best to pass a single string argument to the (positionally implied) -ArgumentList parameter, which allows you to control the process command line explicitly:
The following uses an expandable here-string for syntactic convenience:
Get-ChildItem "$RenderClient\Cortar" -Filter *.mp4 -File -Recurse |
ForEach-Object {
$FilePath = $_.FullName
Start-Process PowerShell #"
-NoExit -File "$FFMpegScript" "$FilePath"
"# # Note: This must be at the very start of the line.
}
Additionally, do not use the automatic $args variable as a custom variable.
In fact, if you want your script to receive positional arguments only, there is no need for a formal param(...) declaration - just use the array-valued $args variable as-is:
# Without a param(...) declaration, $args *implicitly* contains
# any arguments passed, as an array.
$args[0] # output the 1st argument passed.
Read-Host -Prompt "Press Enter to continue"

How to generate Powershell registry import script?

Basically I simply want to right-click on a branch in Regedit and say 'Generate Powershell script for import'. So that instead of a .reg file I get a PS script which will import/create elsewhere the entire selected registry branch with all keys/values etc.
I thought this would be a standard thing somewhere but I can't find anything, nor anyone with the same question, which surprises me.
Of course I could code it all out in PS but I'm feeling really lazy...
What you're looking for would indeed be convenient, but, as of this writing:
There is no official mechanism for customizing the regedit.exe utility's GUI that I'm aware of - unlike the (registry-based) mechanism for customizing File Explorer's shortcut menus.
Conceivably, specialized tools / advanced WinAPI-based techniques exist to achieve that.
Separately, there's no packaged PowerShell solution that I'm aware of that creates self-contained .ps1 scripts that bundle registry-import code with the data to import.
Leaving the regedit.exe GUI-integration aspect out of the picture, the building blocks of what you're looking for are:
(a) Using reg.exe export to export a given registry key's subtree to a .reg file.
(b) Later using reg.exe import to import such a file.
PowerShell code that combines (a) and (b) as follows:
It performs (a) ...
... and embeds the resulting .reg file's content in a dynamically generated script (.ps1) ...
which, when executed on a given machine, imports the embedded data into the registry, via (b).
Below is function New-RegistryImportScript, which implements the steps above; here's a sample invocation:
Get-Item HKCU:\Console | New-RegistryImportScript -OutPath .
The above creates script .\Import-RegKey_HKEY_CURRENT_USER_Console.ps1, which has the data from the HKEY_CURRENT_USER\Console registry key (subtree) embedded and, when executed, imports that data into the registry.
The script file name was auto-generated, from the given key path, because only an output directory was specified to -OutPath (. to target the current dir.), but you may specify a file path instead, so as to use a file name of choice.
As for regedit.exe integration: Invoke shortcut-menu command Copy Key Name on the key of interest, and then pass it as an argument to New-RegistryImportScript; e.g.:
# 'HKEY_CURRENT_USER\Console' is an example path copied from regedit.exe
New-RegistryImportScript HKEY_CURRENT_USER\Console .
New-RegistryImportScript source code:
function New-RegistryImportScript {
<#
.SYNOPSIS
Generates a self-contained registry-import script.
.DESCRIPTION
Generates a self-contained registry-import script that bundles the
data exported from a given registry key (subtree), using `reg.exe`
behind the scenes.
By default, the content of the generated script is output; redirect
it to a file as needed.
Alternatively, use -OutPath to directly save it to a file.
If you specify a *directory*, a file name is auto-generated as
Import-RegKey_<sanitized_key_path>.ps1, where <sanitized_key_path>
is the input key path with all non-alphanumeric characters replaced with
"_".
If you provide multiple key paths via the pipeline, a *single* output file
is created if you pass a *file* path to -OutPath.
With a *directory* path, an auto-named script is generate for each
input key path.
.EXAMPLE
Get-Item HKCU:\Console | New-RegistryImportScript -OutPath .
Creates automatically named script .\Import-RegKey_HKEY_CURRENT_USER_Console.ps1
with the data exported from HKEY_CURRENT_USER\Console embeded in it.
#>
param(
[Alias('PSPath')]
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $KeyPath,
[string] $OutPath
)
begin {
# Code to add at the top and bottom of the generated script
$scriptProlog = #'
[CmdletBinding()] param()
$tempFile = "$env:TEMP\" + [System.IO.Path]::GetRandomFileName() + '.reg'
& {
'#
$scriptEpilog = #'
} | Set-Content -Encoding Unicode -LiteralPath $tempFile
reg.exe import $tempFile
Remove-Item -LiteralPath $tempFile
exit $LASTEXITCODE
'#
if ($env:OS -ne 'Windows_NT') { throw "This command runs on Windows only." }
# Note: For cross-PS-edition compatibility we ensure that UTF-8 files *with BOM* are created.
$enc = if ($IsCoreCLR) { 'utf8BOM'} else { 'utf8 '}
$autoGenerateName = $OutPath -and (Test-Path -Type Container -LiteralPath $OutPath)
if (-not $OutPath) {
$scriptProlog # Output the prolog to the success output stream.
} elseif (-not $autoGenerateName) {
if (($parentPath = (Split-Path -Parent $OutPath)) -and -not (Test-Path -Type Container -LiteralPath $parentPath)) {
throw "Cannot find part of the output path: $OutPath"
}
Write-Verbose "Generating script `"$($outFile.FullName)`"..."
# Initialize the single output file.
$scriptProlog | Set-Content -LiteralPath $OutPath -Encoding $enc
}
}
process {
# First, try to convert to a full, provider-native path.
$nativeRegPath = Convert-Path -ErrorAction Ignore -LiteralPath $KeyPath
if (-not $nativeRegPath) { $nativeRegPath = $KeyPath } # Assume that a native registry path was directly given.
# Resolve it to a full, native registry path via a Get-Item call.
# By using "Microsoft.PowerShell.Core\Registry::" as the prefix, we rule out non-registry paths.
# !! Sadly, even the .Name property does NOT contain the *case-exact* form of the key path - it reflects the case *as specified*.
# !! However, given that the registry is inherently case-INsensitive, this should not matter.
$nativeRegPath = (Get-Item -ErrorAction Ignore -LiteralPath "Microsoft.PowerShell.Core\Registry::$nativeRegPath").Name
if (-not $nativeRegPath) {
"Not an (existing) registry path: `"$KeyPath`""
return
}
Write-Verbose "Targeting registry key `"$nativeRegPath`""
# Export the target key's subtree from the registry.
$tempFile = New-TemporaryFile
reg.exe export $nativeRegPath $tempFile /y >$null # Creates a UTF-16LE file.
if ($LASTEXITCODE) {
Write-Error "Export of registry key `"$nativeRegPath`" failed."
return
}
$regFileContent = Get-Content -Raw $tempFile
$tempFile | Remove-Item
# Create the part of the generated script that has the exported
# data embedded as a here-string.
$scriptEmbeddedData = #"
Write-Verbose "Importing into ``"$nativeRegPath``"..."
#'
$regFileContent
'#
"#
if (-not $OutPath) {
$scriptEmbeddedData # output to the success output stream
}
else {
if ($autoGenerateName) {
# Auto-generate a filename for the key path at hand.
$OutFile = Join-Path $OutPath ('Import-RegKey_' + ($nativeRegPath -replace '[^\p{L}\d]', '_') + '.ps1')
Write-Verbose -Verbose "Generating auto-named script `"$OutFile`"..."
$scriptProlog, $scriptEmbeddedData, $scriptEpilog | Set-Content -Encoding $enc $OutFile
} else {
# Append the embedded data to the single output script.
$scriptEmbeddedData | Add-Content -Encoding $enc $OutPath
}
}
}
end {
if (-not $OutPath) {
# Output the the epilog.
$scriptEpilog
}
elseif (-not $autoGenerateName) {
# Single output file? Append the epilog.
$scriptEpilog | Add-Content -Encoding $enc $OutPath
}
}
}

Hidden variable Password parameter - powershell 4.0

I am creating a release definition with a powershell script to replace a file with env variables from the release definition it works but It doesn't seem to catch the password variable which is hidden in the release definition. is there a way to tell powershell to look for hidden variables?
UPDATE: Here is the script it finds all the variables in $paramsFilePath that are not hidden my password in In environmental variables in Release definition is hidden and the script doesn't find it.
param(
[string]$paramsFilePath,
)
Write-Verbose -Verbose "Entering script Replace-SetParameters.ps1"
Write-Verbose -Verbose ("Path to SetParametersFile: {0}" -f $paramsFilePath)
# get the environment variables
$vars = Get-ChildItem -path env:*
# read in the setParameters file
$contents = Get-Content -Path $paramsFilePath
# perform a regex replacement
$newContents = "";
$contents | % {
$line = $_
if ($_ -match "__(\w+)__") {
$setting = Get-ChildItem -path env:* | ? { $_.Name -eq $Matches[1] }
if ($setting) {
Write-Verbose -Verbose ("Replacing key {0} with value from environment" -f $setting.Name)
$line = $_ -replace "__(\w+)__", $setting.Value
}
}
$newContents += $line + [Environment]::NewLine
}
Write-Verbose -Verbose "Overwriting SetParameters file with new values"
Set-Content $paramsFilePath -Value $newContents
Write-Verbose -Verbose "Exiting script Replace-SetParameters.ps1"
Unlike the normal variable, the password you are trying to get is secret variable.
Secret Variables
We recommend that you make the variable Secret if it contains a
password, keys, or some other kind of data that you need to avoid
exposing.
The variable replacement we do is on the inputs on the tasks, we don't parse the scripts. To use secret variables you will have to take those as inputs into your script we explicitly do not populate those into the environment. You could take a look at this discuss: Use hidden / secret variables in commands

How to pipe embedded script to powershell

I adapted some code from a solution here, but it doesn't work for me when executed as a bat script. Executed within a bat script, it gives the error below. Executed on the command line (with the correct line number as calculated by the script in place of %Line%, it works fine.
The key line is
more +%Line% %0 | powershell -c –
The error is
û : The term 'û' is not recognized as the name of a cmdlet, function, script file, or operable program.
The full Bat script is
#echo off
set fn=Archive-Extract
set fnp0=C:\RMT\VCS\GIT\Games\soulfu\build\dependencies2\libogg-1.2.2.zip
set fnp1=C:\RMT\VCS\GIT\Games\soulfu\build\dependencies2
::::::::::::::::::::::::::::::::::::
REM Set %A to the line number of all lines starting with ':', this leaves %A with the line number of the last ':'.
for /f "delims=:" %%a In ('findstr /Bn ":" %0') do set /A Line=%%a
REM Send the content of this script past the last line starting with ':' to powershell.
more +%Line% %0 | powershell -c –
::::::::::::::::::::::::::::::::::::
dir *.bat
pause & exit /b
::::::::::::::::::::::::::::::::::::
function Archive-Extract([string]$zipFilePath, [string]$destinationPath) {
# This will get added when paths are joined, and path comparison will need it to be absent.
$destinationPath = $destinationPath.TrimEnd("\");
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem');
$zipfile = [IO.Compression.ZipFile]::OpenRead($zipFilePath);
# Determine how many top level entries there are.
$ziplevel0files = #{};
$zipfile.Entries | foreach {
$s = ($_.FullName.TrimEnd("/") -split "/")[0];
if ($ziplevel0files.ContainsKey($s)) {
$ziplevel0files[$s] = $ziplevel0files[$s] + 1;
} else {
$ziplevel0files[$s] = 0;
}
}
if ($ziplevel0files.count -ne 1) {
Write-Host "Zip archives are (at this time) expected to contain one top-level directory, and all content within it.";
return 1; # Failure
}
$zipDirPath = Join-Path -Path $destinationPath -ChildPath $ziplevel0files.Keys[0];
# If the directory does not exist, extract the zip archive into the current folder.
if (Test-Path -LiteralPath $zipDirPath) {
Write-Host "Top-level extraction directory already exists.";
return 2; # Failure
}
$zipfile.Entries | foreach {
$extractFilePath = Join-Path -Path $destinationPath -ChildPath $_.FullName;
$extractFileDirPath = Split-Path -Parent $extractFilePath;
# Skip the top-level directory everything comes under.
if ($extractFileDirPath -ne $destinationPath) {
if (-not (Test-Path -LiteralPath $extractFileDirPath -PathType Container)) {
New-Item -Path $extractFileDirPath -Type Directory | Out-Null;
}
# Sometimes a directory comes after a file within the directory (the latter causes it to be created implicitly above).
if (-not $extractFilePath.EndsWith("\")) {
try {
[IO.Compression.ZipFileExtensions]::ExtractToFile($_, $extractFilePath, $true);
} catch {
Write-Host "Failed to extract file:" $extractFilePath;
return 3; # Failure
}
}
}
}
return 0; # Success
}
# Anything that calls should execute the powershell and set the parameters.
$fn = (Get-ChildItem Env:fn).Value;
$f = (get-item -path function:$fn);
Write-Host "sss1" $fn;
if ($fn -eq "Archive-Extract") {
Write-Host "sss2";
$archivepath = (Get-ChildItem Env:fnp0).Value;
$destinationpath = (Get-ChildItem Env:fnp1).Value;
$err = & $f.ScriptBlock $archivepath $destinationpath;
exit $err;
} else {
Write-Host "sss3";
Write-Error "Failed to match function: "+ $fn;
exit 1000;
}
What needs to be added to the BAT script to execute the same as when the code is executed line-by-line on the command-line?
EDIT: Note that when I adapted this script to follow the multi-line powerscript commenting approach recommended by npocmaka, the existing code above based on more but with no skipped line numbers worked. However, I am not entirely convinced that this solves the problem. I believe the above code worked fine at one point, as is, and worked for the person who came up with the original base code to begin with.
<# :
#echo off
echo --START POWERSHELL--
:: $cmdargs variable will contain the command line.
powershell $cmdargs="""%*""";$exp=$(Get-Content '%~f0') -replace """`n""",""";""" ;invoke-expression """$exp"""
echo --START BATCH--
dir *.bat
pause & exit /b
#>
Write-Host "sss";
exit;
Try this.I think it's better way for hybridization and allows something like argument passing to powershell script.Just have in mind that every line should be terminated with ;

Pipe all Write-Output to the same Out-File in PowerShell

As the title suggests, how do you make it so all of the Write-Outputs - no matter where they appear - automatically append to your defined log file? That way the script will be nicer to read and it removes a tiny bit of work!
Little example below, id like to see none of the "| Out-File" if possible, yet have them still output to that file!
$Author = 'Max'
$Time = Get-Date -Format "HH:mm:ss.fff"
$Title = "Illegal Software Removal"
$LogName = "Illegal_Remove_$($env:COMPUTERNAME).log"
$Log = "C:\Windows\Logs\Software" + "\" + $LogName
$RemoteLog = "\\Server\Adobe Illegal Software Removal"
Set-PSBreakpoint -Variable Time -Mode Read -Action { $global:Time = Get-Date -format "HH:mm:ss.fff" } | Out-Null
If((Test-Path $Log) -eq $False){ New-Item $Log -ItemType "File" -Force | Out-Null }
Else { $Null }
"[$Time][Startup] $Title : Created by $Author" | Out-File $Log -Append
"[$Time][Startup] Configuring initial variables required before run..." | Out-File $Log -Append
EDIT: This needs to work on PS v2.0, I don't want the output to appear on screen at all only in the log. So I have the same functionality, but the script would look like so...
"[$Time][Startup] $Title : Created by $Author"
"[$Time][Startup] Configuring initial variables required before run..."
You have two options, one is to do the redirection at the point the script is invoked e.g.:
PowerShell.exe -Command "& {c:\myscript.ps1}" > c:\myscript.log
Or you can use the Start-Transcript command to record everything (except exe output) the shell sees. After the script is done call Stop-Transcript.