I've been working on a powershell script. Because I want to modularize/organize my code a little bit I decided to try making some modules to split up my script.
I am running powershell 5.1.14393.1066
Now I have one main script which imports the modules that it needs, but it can't seem to find my function Is-Template-Name-Set:
Is-Template-Name-Set : The term 'Is-Template-Name-Set' 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 C:\Users\niels\test1\TemplateScript.ps1:6 char:5
+ If (Is-Template-Name-Set) {
+ ~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Is-Template-Name-Set:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Is-Template-Name-Set is defined in UtilityTemplateModule. Module file (UtilityTemplateModule.psm1):
Function Get-ScriptDirectory() {
return Split-Path $script:MyInvocation.MyCommand.Path
}
Function Is-Template-Name-Set($templateName) {
$templateName -And $templateName -is [String] -And -Not $templateName -eq ""
}
Export-ModuleMember -Function 'Get-*'
Export-ModuleMember -Function 'Is-*'
UtilityTemplateModule description (UtilityTemplateModule.psd1 file):
#{
ModuleVersion = '1.0'
GUID = '082fc8f7-4c7f-4d9f-84a8-b36c3610cdfe'
Author = 'Niels Bokmans'
CompanyName = 'Bluenotion'
Copyright = 'Copyright (c) 2017'
Description = 'General utilities used by the other template modules.'
# Functions to export from this module
FunctionsToExport = '*'
# Cmdlets to export from this module
CmdletsToExport = '*'
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module
AliasesToExport = '*'
}
My main script:
param([String]$templateName="")
Import-Module BuildTemplateModule
Import-Module CdnTemplateModule
Import-Module UtilityTemplateModule
If (Is-TemplateNameSet -templateName $templateName) {
echo "Template name is set!"
$templateId = GetTemplateId -templateName $templateName
if (-Not $templateId -eq -1) {
Restore-Packages
Run-Build-Tasks
Minify-Require-Js
Clean-Downloaded-Dependencies
Copy-Offline-Page
#Deploy $response.templateId
}
} else {
echo "Template name not set!"
exit
}
I'm very new to any powershell work and I'm not sure why the function isn't recognized. I think I'm doing it right, exporting functions in the module itself and also just exporting everything in the description file (?, the .psd1 file).
Any suggestions would be greatly appreciated.
Based on your comment about the system32 powershell modules directory I think you may be putting your modules in the wrong directory.
Per: https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx
$PSHome\Modules (%Windir%\System32\WindowsPowerShell\v1.0\Modules)
This location is reserved for modules that ship with Windows. Do not install modules to this location.
I suggest moving your modules to:
$Home\Documents\WindowsPowerShell\Modules (%UserProfile%\Documents\WindowsPowerShell\Modules)
Or
$Env:ProgramFiles\WindowsPowerShell\Modules (%ProgramFiles%\WindowsPowerShell\Modules)
Creating any directories in either of those paths that are missing (e.g under Documents the \WindowsPowerShell\Modules folders don't usually exist by default).
Alternatively you could put modules in a custom path and add that path to the PSModulePath environment variable.
Related
Take these settings for the program Win-PS2EXE:
This is so that the console will show when the exe file is clicked on.
And this code:
$inf_file = "$PSScriptRoot\setup-files\install.inf"
write-host """$inf_file"""
timeout 10
Let us say that the path of the new executable is W:\Apps\Install Scheme.exe
Which means the $inf_file is here W:\Apps\setup-files\install.inf
When I click the converted exe file I get this.
Is there any way to get the correct path of W:\Apps\setup-files\install.inf so that the executable recognises the location of itself when clicked.
I thought that $PSScriptRoot would work.
I'm lost as to how to get around this as the exe file will eventually depend on knowing its location.
Here's the code that can accomplish that.
Function Get-PSScriptPath {
<#
.SYNOPSIS
Returns the current filepath of the .ps1 or compiled .exe with Win-PS2EXE.
.DESCRIPTION
This will return the path of the file. This will work when the .ps1 file is
converted with Win-PS2EXE
.NOTES
Author: Ste
Date Created: 2021.05.03
Tested with PowerShell 5.1 and 7.1.
Posted here: https://stackoverflow.com/q/60121313/8262102
.PARAMETER None
NA
.INPUTS
None. You cannot pipe objects to Get-PSScriptPath.
.OUTPUTS
Returns the current filepath of the .ps1 or compiled .exe with Win-PS2EXE.
.EXAMPLE (When run from a .ps1 file)
PS> Get-PSScriptPath
PS> C:\Users\Desktop\temp.ps1
.EXAMPLE (When run from a compiled .exe file with Win-PS2EXE.
PS> Get-PSScriptPath
PS> C:\Users\Desktop\temp.exe
#>
if ([System.IO.Path]::GetExtension($PSCommandPath) -eq '.ps1') {
$psScriptPath = $PSCommandPath
} else {
# This enables the script to be compiles and get the directory of it.
$psScriptPath = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
}
return $psScriptPath
}
Get-PSScriptPath
To offer a pragmatic, concise alternative (PSv3+) that always reports the script path as a full path:
One-liner:
$scriptDir = if (-not $PSScriptRoot) { Split-Path -Parent (Convert-Path ([environment]::GetCommandLineArgs()[0])) } else { $PSScriptRoot }
Annotated form:
$scriptDir = if (-not $PSScriptRoot) { # $PSScriptRoot not defined?
# Get the path of the executable *as invoked*, via
# [environment]::GetCommandLineArgs()[0],
# resolve it to a full path with Convert-Path, then get its directory path
Split-Path -Parent (Convert-Path ([environment]::GetCommandLineArgs()[0]))
}
else {
# Use the automatic variable.
$PSScriptRoot
}
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.
When I import my module I can't access the exposed members.
Placed my module in C:\Program Files\WindowsPowerShell\Modules.
When I import my module in powershell by:
Import-Module StuiterModule -Verbose
and then enter Invoke-Reboot it gives the following error:
Invoke-Reboot : The term 'Invoke-Reboot' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, 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
+ Invoke-Reboot
+ ~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Invoke-Reboot:String [], CommandNotFoundExeption
+ FullyQualifiedErrorId : CommandNotFoundException
Does anyone have an idea what I'm doing wrong?
Update
When I put -Force behind the Import Module everything works. Why is that and how can I fix this?
Code:
StuiterModule.psm1
$Public = #( Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" )
$Private = #( Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" )
#($Public + $Private) | ForEach-Object {
Try {
. $_.FullName
} Catch {
Write-Error -Message "Failed to import function $($_.FullName): $_"
}
}
Export-ModuleMember -Function $Public.BaseName
StuiterModule.psd1
#
# Module manifest for module 'StuiterModule'
#
# Generated by: StuiterSlurf
#
# Generated on: 29-5-2018
#
#{
# Script module or binary module file associated with this manifest.
RootModule = 'StuiterModule.psm1'
# Version number of this module.
ModuleVersion = '1.0.0'
# ID used to uniquely identify this module
GUID = '0254592e-b712-4d70-844c-6e38cec20ee5'
# Author of this module
Author = 'StuiterSlurf'
# Copyright statement for this module
Copyright = '(c) 2018 StuiterSlurf. All rights reserved.'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.0'
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = 'Invoke-Reboot', 'Start-Program', 'Update-Program'
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = #()
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = #()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = #{
PSData = #{}
}
}
Public/Invoke-Reboot.ps1
# Reboot system
Function Invoke-Reboot {
[cmdletbinding()]
Param()
Write-Verbose "Starting reboot"
Restart-Computer
}
Background
I am creating a script, using PowerCLI, to perform mass OVF exports using VMware's ovftool. The script works by performing the following functions:
Through PowerCLI arguments, take in the vCenter address, naming scheme of the VMs to export, and where the OVFs should be exported
Connect to the vCenter instance
Put all VMs that follow the specified naming scheme into a list
Loop through the list and export each VM to an ovf using the ovftool and the built path to the VM
The Script: VMs-to-OVF.ps1
# Take in vCenter address, naming scheme of VMs to be exported, and where the OVFs should be exported
param (
[string]$vcenter = $(throw "
vCenter address required.`n
Example:`n
.\VMs-to-OVF.ps1 -vcenter <192.168.1.200>`n
.\VMs-to-OVF.ps1 -vcenter <vcenter.test.com>"),
[string]$vmNamingScheme = $(throw "
VM naming scheme required.`n
Example:`n
.\VMs-to-OVF.ps1 -vcenter <vcenterIP/DNS> -vmPath </DATACENTER/vm/`n
test/> -vmNamingScheme <test-vm-1>`n
.\VMs-to-OVF.ps1 -vcenter <vcenterIP/DNS> -vmPath </DATACENTER/vm/`n
test/> -vmNamingScheme <test-vm-*>`n"),
[string]$exportLocation = $(throw "
Export location required.`n
Example:`n
.\VMs-to-OVF.ps1 -vcenter <vcenterIP/DNS> -vmPath </DATACENTER/vm/`n
test/> -vmNamingScheme <test-vm-1> -exportLocation 192.168.1.100:\`n
.\VMs-to-OVF.ps1 -vcenter <vcenterIP/DNS> -vmPath </DATACENTER/vm/`n
test/> -vmNamingScheme <test-vm-*> -exportLocation X:\`n")
)
# Connect to vCenter
Connect-VIServer -Server $vcenter
# $VMs is an array of VM names that will be exported
# $vmNamingScheme gives the VM naming pattern we are looking for
$VMs = $(get-vm -name $vmNamingScheme | select name | format-list | out-string)
$VMs = $VMs.replace("Name : ","")
$VMs = $VMs.replace(" ","")
$VMs = $VMs.split("`n")
$VMs = $VMs|?{$_ -ne $VMs[1]}
# This loop iterates through the $VMs array and performs an OVF export, to the location
# specified in $exportLocation, for each VM name in the array
# $vmPath is comprised of the path to the VM and $VM holds the actual VM name
foreach ($VM in $VMs){
if ($VMs -ne $null){
# ***THIS SCRIPT ASSUMES THE get-vmfolderpath SCRIPT IS IN THE SAME DIRECTORY AS ITSELF***
# get the folder path of the VM using the get-vmfolderpath script (this will align with
# the path in vSphere Folders and Templates view)
$vmPath = "get-vm -name $VM | .\get-vmfolderpath.ps1"
&$vmPath
# ***THIS SCRIPT ASSUMES THE DEFAULT INSTALL PATH FOR THE ovftool PROGRAM
# run the ovftool command with the proper arguments
&'C:\Program Files\VMware\VMware OVF Tool\ovftool.exe' vi://$vcenter$vmPath$VM $exportLocation
}
}
Accompanying Script: get-vmfolderpath
param(
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[string]$folderid,
[switch]$moref
)
$folderparent=get-view $folderid
if ($folderparent.name -ne 'vm'){
if($moref){
$path=$folderparent.moref.toString()+'\'+$path
}
else{
$path=$folderparent.name+'\'+$path
}
if ($folderparent.parent){
if($moref){
get-vmfolderpath $folderparent.parent.tostring() -moref
}
else{
get-vmfolderpath($folderparent.parent.tostring())
}
}
}
else {
if ($moref){
return (get-view $folderparent.parent).moref.tostring()+"\"+$folderparent.moref.tostring()+"\"+$path
}
else {
return (get-view $folderparent.parent).name.toString()+'\'+$folderparent.name.toString()+'\'+$path
}
}
Errors
The ovftool command built in each iteration of the for loop will work if you copy and paste the text into the PowerCLI console, but not when run directly from the script. The following errors are produced when the custom ovftool command is run from within the script:
The term 'get-vm -name VM-NAME | .\get-vmfolderpath.ps1' 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
C:\Users\username\Desktop\VMs-to-OVF.ps1:85 char:4
+ &$vmPath
+ ~~~~~~~
+ CategoryInfo : ObjectNotFound: (get-vm -name CA...mfolderpath.p
s1:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Things I Have Checked:
The output of "get-vm -name vm_name | .\get-vmfolderpath.ps1" run directly in the PowerCLI console returns the proper VM path
All variables output the expected values
If the exact ovftool command generated in the script is run in the PowerCLI console then it will properly export the VM
Just closing the loop on this. I found a solution to my issue. My guess is that something is lost when you manipulate the list of VMs returned by the Get-VM cmdlet. In the VMs-to-OVFs script, if all of the "$VM =" lines are replaced by the single line below, then that information is retained.
$VMs = get-vm -name $vmNamingScheme
In the for loop use the ".Name" attribute to pass each individual VM object to the get-vmfolderpath script.
$vmPath = get-vm -name $VM.Name | .\get-vmfolderpath.ps1
With PowerShell being built on top of the .NET framework, can I write my own custom class using PowerShell?
I'm not talking about instantiating .NET classes... that part is plain enough. I want to write my own custom classes using PowerShell scripts. Is it possible? So far my research leads me to say that this isn't possible.
I'm still learning PowerShell, and so far I haven't found an answer on this website, despite a few searches.
Take a look at the Add-Type cmdlet. It lets you write C# and other code in PowerShell.
For example (from the above link), in a PowerShell window,
$source = #"
public class BasicTest
{
public static int Add(int a, int b)
{
return (a + b);
}
public int Multiply(int a, int b)
{
return (a * b);
}
}
"#
Add-Type -TypeDefinition $source
[BasicTest]::Add(4, 3)
$basicTestObject = New-Object BasicTest
$basicTestObject.Multiply(5, 2)
I suspect that the solution that you are looking for is PowerShell Modules. They perform the roles that classes typically perform in other languages. They give you a very simple, yet structured, way to reuse your code.
Here is how to get the functionality of classes in PowerShell using modules. At the command line you could do this:
New-Module -ScriptBlock {function add($a,$b){return $a + $b}; function multiply($a,$b){return $a * $b}; function supersecret($a,$b){return multiply $a $b}; export-modulemember -function add, supersecret}
Then you would be able to:
PS C:\> add 2 4
6
PS C:\> multiply 2 4
The term 'multiply' 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:9
+ multiply <<<< 2 4
+ CategoryInfo : ObjectNotFound: (multiply:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
PS C:\> supersecret 2 4
8
As you can see multiply is private within the module. More traditionally you would instantiate an object that is an instance of the module. That is done via the -AsCustomObject parameter:
$m = New-Module -ScriptBlock {function add($a,$b){return $a + $b}; function multiply($a,$b){return $a * $b}; function supersecret($a,$b){return multiply $a $b}; export-modulemember -function add, supersecret} -AsCustomObject
Then you could:
PS C:\> $m.add(2,4)
6
PS C:\> $m.multiply(2,4)
Method invocation failed because [System.Management.Automation.PSCustomObject] doesn't contain a method named 'multiply'.
At line:1 char:12
+ $m.multiply <<<< (2,4)
+ CategoryInfo : InvalidOperation: (multiply:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
PS C:\> $m.supersecret(2,4)
8
This all demonstrates the use of dynamic modules meaning nothing is stored to disk for reuse. It is fine for very simple functionality. If you want to actually be able to read the code and reuse it in future sessions or scripts, however, you would want to store it in a .psm1 file and then store that file in a folder with the same name (minus the extension) as the file. Then you can import the module into your session at the command line or into another script.
As an example of this, let's say I took this code:
function Add{
param(
$a,
$b
)
return $a + $b
}
function Multiply{
param(
$a,
$b
)
return $a + $b
}
function SuperSecret{
param(
$a,
$b
)
return Multiply $a $b
}
Export-ModuleMember -Function Add, SuperSecret
And saved it to a file called TestModule.psm1 in the folder: C:\Windows\System32\WindowsPowerShell\v1.0\Modules\TestModule
The Modules folder in the PowerShell install folder is a magic folder and any modules stored there are visible to the Import-Module cmdlet without having to specify a path. Now if we run Get-Module -List at the command line we see:
ModuleType Name ExportedCommands
---------- ---- ----------------
Script DotNet {}
Manifest FileSystem {Get-FreeDiskSpace, New-Zip, Resolve-ShortcutFile, Mount-SpecialFolder...}
Manifest IsePack {Push-CurrentFileLocation, Select-CurrentTextAsVariable, ConvertTo-Short...
Manifest PowerShellPack {New-ByteAnimationUsingKeyFrames, New-TiffBitmapEncoder, New-Viewbox, Ne...
Manifest PSCodeGen {New-Enum, New-ScriptCmdlet, New-PInvoke}
Manifest PSImageTools {Add-CropFilter, Add-RotateFlipFilter, Add-OverlayFilter, Set-ImageFilte...
Manifest PSRss {Read-Article, New-Feed, Remove-Article, Remove-Feed...}
Manifest PSSystemTools {Test-32Bit, Get-USB, Get-OSVersion, Get-MultiTouchMaximum...}
Manifest PSUserTools {Start-ProcessAsAdministrator, Get-CurrentUser, Test-IsAdministrator, Ge...
Manifest TaskScheduler {Remove-Task, Get-ScheduledTask, Stop-Task, Add-TaskTrigger...}
Manifest WPK {Get-DependencyProperty, New-ModelVisual3D, New-DiscreteVector3DKeyFrame...
Manifest AppLocker {}
Manifest BitsTransfer {}
Manifest PSDiagnostics {}
Script **TestModule** {}
Manifest TroubleshootingPack {}
Manifest Citrix.XenApp.Commands... {}
We can see that our module is ready to import. We can import it into the session and use it in the raw using:
Import-Module TestModule
Or once again we can instantiate an object:
$m = Import-Module TestModule -AsCustomObject
You can use the class keyword that was introduced in PowerShell 5.0
Here is an example by Trevor Sullivan. (Archived here.)
##################################################
####### WMF 5.0 November 2014 Preview ###########
##################################################
class Beer {
# Property: Holds the current size of the beer.
[Uint32] $Size;
# Property: Holds the name of the beer's owner.
[String] $Name;
# Constructor: Creates a new Beer object, with the specified
# size and name / owner.
Beer([UInt32] $NewSize, [String] $NewName) {
# Set the Beer size
$this.Size = $NewSize;
# Set the Beer name
$this.Name = $NewName;
}
# Method: Drink the specified amount of beer.
# Parameter: $Amount = The amount of beer to drink, as an
# unsigned 32-bit integer.
[void] Drink([UInt32] $Amount) {
try {
$this.Size = $this.Size - $Amount;
}
catch {
Write-Warning -Message 'You tried to drink more beer than was available!';
}
}
# Method: BreakGlass resets the beer size to 0.
[void] BreakGlass() {
Write-Warning -Message 'The beer glass has been broken. Resetting size to 0.';
$this.Size = 0;
}
}
This works on Windows 10 Pro.
Test drive it like this:
# Create a new 33 centilitre beer, named 'Chimay'
$chimay = [Beer]::new(33, 'Chimay');
$chimay.Drink(10)
$chimay.Drink(10)
# Need more beer!
$chimay.Drink(200)
$chimay.BreakGlass()