touch Function in PowerShell - powershell

I recently added a touch function in PowerShell profile file
PS> notepad $profile
function touch {Set-Content -Path ($args[0]) -Value ($null)}
Saved it and ran a test for
touch myfile.txt
error returned:
touch : The term 'touch' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.
At line:1 char:1
+ touch myfile
+ ~~~~~
+ CategoryInfo : ObjectNotFound: (touch:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

With PowerShell there are naming conventions for functions. It is higly recommended to stick with that if only to stop getting warnings about it if you put those functions in a module and import that.
A good read about naming converntion can be found here.
Having said that, Powershell DOES offer you the feature of Aliasing and that is what you can see here in the function below.
As Jeroen Mostert and the others have already explained, a Touch function is NOT about destroying the content, but only to set the LastWriteTine property to the current date.
This function alows you to specify a date yourself in parameter NewDate, but if you leave it out it will default to the current date and time.
function Set-FileDate {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory = $false, Position = 1)]
[datetime]$NewDate = (Get-Date),
[switch]$Force
)
Get-Item $Path -Force:$Force | ForEach-Object { $_.LastWriteTime = $NewDate }
}
Set-Alias Touch Set-FileDate -Description "Updates the LastWriteTime for the file(s)"
Now, the function has a name PowerShell won't object to, but by using the Set-Alias you can reference it in your code by calling it touch

Here is a version that creates a new file if it does not exist or updates the timestamp if it does exist.
Function Touch-File
{
$file = $args[0]
if($file -eq $null) {
throw "No filename supplied"
}
if(Test-Path $file)
{
(Get-ChildItem $file).LastWriteTime = Get-Date
}
else
{
echo $null > $file
}
}
If you have a set of your own custom functions stored in a .ps1 file, you must first import them before you can use them, e.g.
Import-module .\MyFunctions.ps1 -Force

To avoid confusion:
If you have placed your function definition in your $PROFILE file, it will be available in future PowerShell sessions - unless you run . $PROFILE in the current session to reload the updated profile.
Also note that loading of $PROFILE (all profiles) can be suppressed by starting a session with powershell.exe -NoProfile (Windows PowerShell) / pwsh -NoProfile (PowerShell (Core)).
As Jeroen Mostert points out in a comment on the question, naming your function touch is problematic, because your function unconditionally truncates an existing target file (discards its content), whereas the standard touch utility on Unix-like platforms leaves the content of existing files alone and only updates their last-write (and last-access) timestamps.
See this answer for more information about the touch utility and how to implement equivalent behavior in PowerShell.

Related

How to load variables automatically in PowerShell for every session? [duplicate]

On Windows, not counting ISE or x86, there are four (4) profile scripts.
AllUsersAllHosts # C:\Program Files\PowerShell\6\profile.ps1
AllUsersCurrentHost # C:\Program Files\PowerShell\6\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts # C:\Users\lit\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost # C:\Users\lit\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
On Linux with pwsh 6.2.0 I can find only two locations.
CurrentUserAllHosts # ~/.config/powershell/Microsoft.PowerShell_profile.ps1
CurrentUserCurrentHost # ~/.config/powershell/profile.ps1
Are there any "AllUsers" profile scripts on Linux? If so, where are they?
tl;dr (also applies to Windows):
The conceptual about_Profiles help topic describes PowerShell's profiles (initialization files).
The automatic $PROFILE variable contains a string that is the path of the initialization file for the current user and the current PowerShell host environment (typically, the terminal a.k.a console).
Additional profile files are defined - along the dimensions of (a) all-users vs. current-user and (b) all host environments vs. the current one - which are exposed via properties that the $PROFILE string variable is decorated with, which makes them nontrivial to discover - see below.
None of the profile files exist by default, and in some case even their parent directories may not; the bottom section of this answer shows programmatic on-demand creation and updating of the $PROFILE file.
Olaf provided the crucial pointer in comment:
$PROFILE | select * # short for: $profile | Select-Object -Property *
shows all profile file locations, whether or not the individual profile files exist.
E.g., on my Ubuntu machine with PowerShell installed in /home/jdoe/.powershell, I get:
AllUsersAllHosts : /home/jdoe/.powershell/profile.ps1
AllUsersCurrentHost : /home/jdoe/.powershell/Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : /home/jdoe/.config/powershell/profile.ps1
CurrentUserCurrentHost : /home/jdoe/.config/powershell/Microsoft.PowerShell_profile.ps1
Length : 62
Note the presence of the [string] type's native Length property, which you could omit if you used $PROFILE | select *host* instead.
That you can get the profile locations that way is not obvious, given that $PROFILE is a string variable (type [string]).
PowerShell decorates that [string] instance with NoteProperty members reflecting all profile locations, which is why select (Select-Object) is able to extract them.
Outputting just $PROFILE - i.e. the string value - yields /home/jdoe/.config/powershell/Microsoft.PowerShell_profile.ps1, i.e. the same path as its CurrentUserCurrentHost property, i.e. the path of the user-specific profile file specific to the current PowerShell host environment (typically, the terminal aka console).[1]
You can verify the presence of these properties with reflection as follows, (which reveals their values too):
$PROFILE | Get-Member -Type NoteProperty
This means that you can also use regular property access and tab completion to retrieve individual profile locations; e.g.:
# Use tab-completion to find a specific profile location.
# Expands to .Length first, then cycles through the profile-location properties.
$profile.<tab>
# Open the all-users, all-hosts profiles for editing.
# Note: Only works if the file already exists.
# Also, you must typically run as admin to modify all-user profiles.
Invoke-Item $profile.AllUsersAllHosts
Convenience functions for getting profile locations and opening profiles for editing:
The code below defines:
Get-Profile enumerates profiles, showing their location and whether they exist on a given machine.
Edit-Profile opens profile(s) for editing (use -Force to create them on demand); note that modifying all-user profiles typically requires running as admin.
function Get-Profile {
<#
.SYNOPSIS
Gets the location of PowerShell profile files and shows whether they exist.
#>
[CmdletBinding(PositionalBinding=$false)]
param (
[Parameter(Position=0)]
[ValidateSet('AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost')]
[string[]] $Scope
)
if (-not $Scope) {
$Scope = 'AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost'
}
foreach ($thisScope in $Scope) {
[pscustomobject] #{
Scope = $thisScope
FilePath = $PROFILE.$thisScope
Exists = (Test-Path -PathType Leaf -LiteralPath $PROFILE.$thisScope)
}
}
}
function Edit-Profile {
<#
.SYNOPSIS
Opens PowerShell profile files for editing. Add -Force to create them on demand.
#>
[CmdletBinding(PositionalBinding=$false, DefaultParameterSetName='Select')]
param (
[Parameter(Position=0, ValueFromPipelineByPropertyName, ParameterSetName='Select')]
[ValidateSet('AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost')]
[string[]] $Scope = 'CurrentUserCurrentHost'
,
[Parameter(ParameterSetName='All')]
[switch] $All
,
[switch] $Force
)
begin {
$scopes = New-Object Collections.Generic.List[string]
if ($All) {
$scopes = 'AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost'
}
}
process {
if (-not $All) { $scopes.Add($Scope) }
}
end {
$filePaths = foreach ($sc in $scopes) { $PROFILE.$sc }
$extantFilePaths = foreach ($filePath in $filePaths) {
if (-not (Test-Path -LiteralPath $filePath)) {
if ($Force) {
if ((New-Item -Force -Type Directory -Path (Split-Path -LiteralPath $filePath)) -and (New-Item -Force -Type File -Path $filePath)) {
$filePath
}
} else {
Write-Verbose "Skipping nonexistent profile: $filePath"
}
} else {
$filePath
}
}
if ($extantFilePaths.Count) {
Write-Verbose "Opening for editing: $extantFilePaths"
Invoke-Item -LiteralPath $extantFilePaths
} else {
Write-Warning "The implied or specified profile file(s) do not exist yet. To force their creation, pass -Force."
}
}
}
[1] PowerShell considers the current-user, current-host profile the profile of interest, which is why $PROFILE's string value contains that value. Note that in order to decorate a [string] instance with note properties, Add-Member alone is not enough; you must use the following idiom: $decoratedString = $string | Add-Member -PassThru propName propValue - see the Add-Member help topic.

Saving XML on remote machine using Invoke-Command

Basically, I'm loading the XML from my local machine, and then I'm trying to save it on a remote machine using Invoke-Command.
I know I can use Copy-item via UNC path, but it takes too long on some machines, and Invoke-Command is faster - I tested this already.
However, I think I'm passing the argument wrong?
The error I get is:
Method invocation failed because [System.String] does not contain a method named 'Save'.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName : -
This is how I'm passing it:
foreach ($serverPath in $serverLocations) {
if ($null -ne $serverPath) {
$generatedPath = "$(Get-Location)\Generated.ManageSQLJobs.xml"
[Xml]$generatedFile = Get-Content $generatedPath
Write-Log "INFO" "Checking on $serverPath" $ExecutionLogFullPath
$testPath = Invoke-Command -ComputerName "$serverPath" -ArgumentList [Xml]$generatedFile -ScriptBlock {
param (
$value
)
Test-Path -Path "C:\AppData\MonitoringConfig\"
if (!$testPath) {
$destinationPath = New-Item -Path "C:\AppData\" -Name "MonitoringConfig" -ItemType Directory
}
if ($testPath) {
$destinationPath = "C:\AppData\MonitoringConfig"
#Write-Log "INFO" "Exists on $serverPath." $ExecutionLogFullPath
}
$GetPathToDeleteXML = "C:\AppData\MonitoringConfig\Generated.ManageSQLJobs.xml"
if (Test-Path -Path $GetPathToDeleteXML) {
Remove-Item -Path * #-Filter Generated.ManageSQLJobs.xml
}
$GetPathForXML = "C:\AppData\MonitoringConfig\Generated.ManageSQLJobs.xml"
$value.Save($GetPathForXML.fullname)
}
}
}
-ArgumentList [Xml]$generatedFile
should be (note the (...)):
-ArgumentList ([Xml]$generatedFile)
[Xml]$generatedFile isn't recognized as an expression, because when PowerShell parses in argument mode (commands with arguments, shell-style), an initial [ isn't special.
In effect, your argument is interpreted as an expandable string, i.e. as if you had passed
"[Xml]$generatedFile".
Therefore, $value in your remotely executed script block received a [string] instance, not an [xml] instance, and strings don't have a .Save() method, which explains the error message.
Enclosing your argument in (...) forces its interpretation as an expression.
See this answer for a comprehensive overview of how PowerShell parses unquoted tokens in argument mode.
A general caveat re passing complex objects as arguments to code executed remotely / in background jobs:
Arguments passed to remote / background script blocks must undergo XML-based serialization and deserialization, because they pass computer / process boundaries.
Only a limited set of known types are deserialized faithfully (deserialized as the original type), others are emulated.
While [xml] instances, [string] instances and .NET primitive types such as [int] are faithfully deserialized, most other types are not.
See this answer for more information.

ValidateScript unexpectedly returning false when condition is true

I have a PowerShell function I'm writing to build and execute a variety of logman.exe commands for me so I don't have to reference the provider GUIDs and type up the command each time I want to capture from a different source. One of the parameters is the file name and I am performing some validation on the parameter. Originally I used -match '.+?\.etl$' to check that the file name had the .etl extension and additionally did some validation on the path. I later decided to remove the path validation but neglected to change the validation attribute to ValidatePattern.
What I discovered was that while it worked perfectly on the machine I was using to author and validate it, on my Server 2016 Core machine it seemed to misbehave when calling the function but that if I just ran the same check at the prompt it worked as expected.
The PowerShell:
[Parameter(ParameterSetName="Server", Mandatory=$true)]
[Parameter(ParameterSetName="Client", Mandatory=$true)]
[ValidateScript({$FileName -match '.+?\.etl$'}]
[string] $FileName = $null
The Output:
PS C:\Users\Administrator> Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
PS C:\Users\Administrator> Start-TBLogging : Cannot validate argument on parameter 'FileName'. The "$FileName -match '.+?\.etl$'" validation script
for the argument with value ".\TestLog.etl" did not return a result of True. Determine why the validation script failed,
and then try the command again.
At line:1 char:50
+ Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Start-TBLogging], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Start-TBLogging
Trying it manually worked:
PS C:\Users\Administrator> $FileName = ".\TestLog.etl"
PS C:\Users\Administrator> $FileName -match '.+?\.etl$'
True
After changing the function to use ValidatePattern it works just fine everywhere but I was wondering if anyone could shed light on the discontinuity.
As Joshua Shearer points out in a comment on a question, you must use automatic variable $_ (or its alias form, $PSItem), not the parameter variable to refer to the argument to validate inside [ValidateScript({ ... })].
Therefore, instead of:
# !! WRONG: The argument at hand has NOT yet been assigned to parameter
# variable $FileName; by design, that assignment
# doesn't happen until AFTER (successful) validation.
[ValidateScript({ $FileName -match '.+?\.etl$' }]
[string] $FileName
use:
# OK: $_ (or $PSItem) represents the argument to validate inside { ... }
[ValidateScript({ $_ -match '.+?\.etl$' })]
[string] $FileName
As briantist points out in another comment on the question, inside the script block $FileName will have the value, if any, from the caller's scope (or its ancestral scopes).

How to return the name of the calling script from a Powershell Module?

I have two Powershell files, a module and a script that calls the module.
Module: test.psm1
Function Get-Info {
$MyInvocation.MyCommand.Name
}
Script: myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
When I run ./myTest.ps1 I get
Get-Info
I want to return the name of the calling script (test.ps1). How can I do that?
Use PSCommandPath instead in your module:
Example test.psm1
function Get-Info{
$MyInvocation.PSCommandPath
}
Example myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
Output:
C:\Users\moomin\Documents\myTest.ps1
If you want only the name of the script that could be managed by doing
GCI $MyInvocation.PSCommandPath | Select -Expand Name
That would output:
myTest.ps1
I believe you could use the Get-PSCallStack cmdlet, which returns an array of stack frame objects. You can use this to identify the calling script down to the line of code.
Module: test.psm1
Function Get-Info {
$callstack = Get-PSCallStack
$callstack[1].Location
}
Output:
myTest.ps1: Line 2
Using the $MyInvocation.MyCommand is relative to it's scope.
A simple example (Of a script located : C:\Dev\Test-Script.ps1):
$name = $MyInvocation.MyCommand.Name;
$path = $MyInvocation.MyCommand.Path;
function Get-Invocation(){
$path = $MyInvocation.MyCommand.Path;
$cmd = $MyInvocation.MyCommand.Name;
write-host "Command : $cmd - Path : $path";
}
write-host "Command : $cmd - Path : $path";
Get-Invocation;
The output when running .\c:\Dev\Test-Script.ps1 :
Command : C:\Dev\Test-Script.ps1 - Path : C:\Dev\Test-Script.ps1
Command : Get-Invocation - Path :
As you see, the $MyInvocation is relative to the scoping. If you want the path of your script, do not enclose it in a function. If you want the invocation of the command, then you wrap it.
You could also use the callstack as suggested, but be aware of scoping rules.
I used this today after trying a couple of techniques.
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$ScriptName = $MyInvocation.MyCommand | select -ExpandProperty Name
Invoke-Expression ". $Script\$ScriptName"
To refer to the invocation info of the calling script, use:
#(Get-PSCallStack)[1].InvocationInfo
e.g.:
#(Get-PSCallStack)[1].InvocationInfo.MyCommand.Name
This provides the script path with trailing backslash as one variable and the script name as another.
The path works with Powershell 2.0 and 3.0 and 4.0 and probably 5.0
Where with Posershell $PSscriptroot is now available.
$_INST = $myinvocation.mycommand.path.substring(0,($myinvocation.mycommand.path.length - $MyInvocation.mycommand.name.length))
$_ScriptName = $myinvocation.mycommand.path.substring($MyInvocation.MyCommand.Definition.LastIndexOf('\'),($MyInvocation.mycommand.name.length +1))
$_ScriptName = $_ScriptName.TrimStart('\')
If you want a more reusable approach, you can use:
function Get-CallingFileName
{
$cStack = #(Get-PSCallStack)
$cStack[$cStack.Length-1].InvocationInfo.MyCommand.Name
}
The challenge I had was having a function that could be reused within the module. Everything else assumed that the script was calling the module function directly and if it was removed even 1 step, then the result would be the module file name. If, however, the source script is calling a function in the module which is, in turn, calling another function in the module, then this is the only answer I've seen that can ensure you're getting the source script info.
Of course, this approach is based on what #iRon and #James posted.
For you googlers looking for quick copy paste solution,
here is what works in Powershell 5.1
Inside your module:
$Script = (Get-PSCallStack)[2].Command
This will output just the script name (ScriptName.ps1) which invoked a function located in module.
I use this in my module:
function Get-ScriptPath {
[CmdletBinding()]
param (
[string]
$Extension = '.ps1'
)
# Allow module to inherit '-Verbose' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Verbose'))) {
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
# Allow module to inherit '-Debug' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Debug'))) {
$DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
}
$callstack = Get-PSCallStack
$i = 0
$max = 100
while ($true) {
if (!$callstack[$i]) {
Write-Verbose "Cannot detect callstack frame '$i' in 'Get-ScriptPath'."
return $null
}
$path = $callstack[$i].ScriptName
if ($path) {
Write-Verbose "Callstack frame '$i': '$path'."
$ext = [IO.Path]::GetExtension($path)
if (($ext) -and $ext -eq $Extension) {
return $path
}
}
$i++
if ($i -gt $max) {
Write-Verbose "Exceeded the maximum of '$max' callstack frames in 'Get-ScriptPath'."
return $null
}
}
return $null
}
You can grab the automatic variable MyInvocation from the parent scope and get the name from there.
Get-Variable -Scope:1 -Name:MyInvocation -ValueOnly
I did a basic test to check to see if it would always just get the direct parent scope and it worked like a treat and is extremely fast as opposed to Get-PSCallStack
function ScopeTest () {
Write-Information -Message:'ScopeTest'
}
Write-nLog -Message:'nLog' -Type:110 -SetLevel:Verbose
ScopeTest

Passing an argument to a powershell script to be used for the Test-Path -include option?

this is my first time asking a question so bear with me. I am teaching myself powershell by writing a few basic maintenance scripts. My question is in regard to a clean up script I am writing which accepts arguments to determine the target directory and files to delete.
The Problem:
The script accepts an optional argument for a list of file extensions to look for when processing the deletion of files. I am trying to test for the existence of the files prior to actually running the delete. I use test-path with the –include parameter to run the check within a ValidateScript block. It works if I pass in a single file extension or no file extensions, however when I try to pass in more than one file extension it fails.
I have tried using the following variations on the code inside the script:
[ValidateScript({ Test-Path $targetDirChk -include $_ })]
[ValidateScript({ Test-Path $targetDirChk -include "$_" })]
[ValidateScript({ Test-Path $targetDirChk -include ‘$_’ })]
For each of the above possibilities I have run the script from the command line using the following variations for the multi extension file list:
& G:\batch\DeleteFilesByDate.ps1 30 G:\log *.log,*.ext
& G:\batch\DeleteFilesByDate.ps1 30 G:\log “*.log, *.ext”
& G:\batch\DeleteFilesByDate.ps1 30 G:\log ‘*.log, *.ext’
Example of the error message:
chkParams : Cannot validate argument on parameter 'includeList'. The " Test-Path $targetDirChk -include "$_" " validation script for the argument with value "*.log, *.ext" did not return true. Determine why the validation script failed and then try the command again.
At G:\batch\DeleteFilesByDate.ps1:81 char:10
+ chkParams <<<< #args
+ CategoryInfo : InvalidData: (:) [chkParams], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,chkParams
The full script is below. I have not yet added the actual code to delete files, because I am still working on accepting and validating the arguments passed in.
I have searched google and stackoverflow but I have not found a solution to this particular problem. I assume I am either doing something wrong with the code, or there is a better way to accomplish what I want to do.
Note:
I should mention that I also tried running the test-path with multiple file extensions outside of the script with no problems:
PS G:\batch\powershell> test-path G:\log\* -include *.log
True
PS G:\batch\powershell> test-path G:\log\* -include *.log, *.ext
True
Script:
# Check that the proper number of arguments have been supplied and if not provide usage statement.
# The first two arguments are required and the third is optional.
if ($args.Length -lt 2 -or $args.Length -gt 3 ){
#Get the name of the script currently executing.
$ScriptName = $MyInvocation.MyCommand.Name
$ScriptInstruction = #"
usage: $ScriptName <Number of Days> <Directory> [File Extensions]
This script deletes files from a given directory based on the file date.
Required Paramaters:
<Number of Days>:
This is an integer representing the number of days worth of files
that should be kept. Anything older than <Number of Days> will be deleted.
<Directory>:
This is the full path to the target folder.
Optional Paramaters:
[File Extensions]
This is the set of file extensions that will be targeted for processing.
If nothing is passed all files will be processed.
"#
write-output $ScriptInstruction
break
}
#Function to validate arguments passed in.
function chkParams()
{
Param(
[Parameter(Mandatory=$true,
HelpMessage="Enter a valid number of days between 1 and 999")]
#Ensure the value passed is between 1 and 999.
#[ValidatePattern({^[1-9][0-9]{0,2}$})]
[ValidateRange(1,999)]
[Int]
$numberOfDays,
[Parameter(Mandatory=$true,
HelpMessage="Enter a valid target directory.")]
#Check that the target directory exists.
[ValidateScript({Test-Path $_ -PathType 'Container'})]
[String]
$targetDirectory,
[Parameter(Mandatory=$false,
HelpMessage="Enter the list of file extensions.")]
#If the parameter is passed, check that files with the passed extension(s) exist.
[ValidateScript({ Test-Path $targetDirChk -include "$_" })]
[String]
$includeList
)
#If no extensions are passed check to see if any files exist in the directory.
if (! $includeList ){
$testResult = Test-path $targetDirChk
if (! $testResult ){
write-output "No files found in $targetDirectory"
exit
}
}
}
#
if ($args[1].EndsWith('\')){
$targetDirChk = $args[1] + '*'
} else {
$targetDirChk = $args[1] + '\*'
}
chkParams #args
-Include on Test-Path is a string[]. You probably want to mirror that definition:
[ValidateScript({ Test-Path $targetDirChk -include $_ })]
[String[]]
$includeList
And drop the "" from there because they will force the argument to be a string and thus trying to match a file that looks like `foo.log blah.ext.
You also have to either put parentheses around that argument when calling the function or remove the space.