Mutually exclusive powershell parameters - powershell

SCENARIO
I'm writing a cmdlet for Powershell 2.0 using Visual Studio 2008 and .NET 3.5
the cmdlet requires 3 arguments.
my intended grammar of the cmdlet is something like this:
cmdletname [foo|bar] p1, p2
That reads as the user must give a value for "-foo" or "-bar" but can't give both together.
EXAMPLE OF VALID INPUT
cmdletname -foo xxx -p1 hello -p2 world
cmdletname -bar yyy -p1 hello -p2 world
EXAMPLE OF INVALID INPUT
cmdletname -foo xxx -bar yyy -p1 hello -p2 world
MY QUESTION
My question is on how to do this in powershell so that it does all the checking for me - or if that is possible at all.
I know I can use just have two optional parameters for foo and bar and simply do the error checking manually. That's how I have it implemented currently.
Alternatively, I am interested in suggestions for different approaches.

You can use the parameter attribute to declare multiple parameter sets. You then simply assign parameters that are mutually exclusive to different parameter sets.
EDIT:
This is also documented in 'about_Functions_Advanced_Parameters', under the section "ParameterSetName Named Argument". This is how different sets of parameters is handled with cmdlets like Get-Random (which has mutually exclusive parameters):
> get-random -input 4 -max 77
Get-Random : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:11
+ get-random <<<< -input 4 -max 77
+ CategoryInfo : InvalidArgument: (:) [Get-Random], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Microsoft.PowerShell.Commands.GetRandomCommand
Here's an example of doing it in a function:
function exclusive_params() {
param(
[parameter(ParameterSetName="seta")]$one,
[parameter(ParameterSetName="setb")]$two,
$three
)
"one: $one"; "two: $two"; "three: $three"
}
The parameters one and two are in different parameter sets, so they cannot be specified together:
> exclusive_params -one foo -two bar -three third
exclusive_params : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:17
+ exclusive_params <<<< -one foo -two bar -three third
+ CategoryInfo : InvalidArgument: (:) [exclusive_params], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,exclusive_params
Which is the same error I got with Get-Random. But I can use the parameters independently:
> exclusive_params -one foo -three third
one: foo
two:
three: third
...or:
> exclusive_params -two bar -three third
one:
two: bar
three: third

Here's an example of using ParameterSetName taken from a cmdlet in the PowerShell Community Extensions. BTW, for ideas you can browse the PSCX source code.
[Cmdlet(VerbsCommon.Set, PscxNouns.Clipboard,
DefaultParameterSetName = ParamSetText)]
[Description("Copies the item in the system clipboard.")]
[RelatedLink(typeof(GetClipboardCommand))]
[RelatedLink(typeof(OutClipboardCommand))]
[RelatedLink(typeof(WriteClipboardCommand))]
public class SetClipboardCommand : ClipboardCommandBase
{
... fields elided
const string ParamSetRtf = "Rtf";
const string ParamSetHtml = "Html";
const string ParamSetText = "Text";
const string ParamSetFiles = "Files";
const string ParamSetImage = "Image";
.
[AllowNull]
[Parameter(ValueFromPipeline = true, ParameterSetName = ParamSetImage)]
public Image Image { get; set; }
.
[AllowNull]
[AllowEmptyCollection]
[Parameter(ValueFromPipeline = true, ValueFromRemainingArguments = true,
ParameterSetName = ParamSetFiles)]
public FileSystemInfo[] Files { get; set; }
.
[AllowNull]
[AllowEmptyString]
[Parameter(ValueFromPipeline = true, ValueFromRemainingArguments = true,
ParameterSetName = ParamSetText)]
public string Text { get; set; }
.
[Parameter(ValueFromPipeline = true, ValueFromRemainingArguments = true,
ParameterSetName = ParamSetHtml)]
public string Html { get; set; }
.
[Parameter(ValueFromPipeline = true, ValueFromRemainingArguments = true,
ParameterSetName = ParamSetRtf)]
public string Rtf { get; set; }
.
protected override void ProcessRecord()
{
...
}
.
protected override void EndProcessing()
{
ExecuteWrite(delegate
{
switch (ParameterSetName)
{
case ParamSetFiles:
if (Paths.Count == 0)
WinFormsClipboard.Clear();
else
WinFormsClipboard.SetFileDropList(_paths);
break;
case ParamSetImage:
if (Image == null)
WinFormsClipboard.Clear();
else
WinFormsClipboard.SetImage(_image);
break;
case ParamSetRtf:
SetTextContents(Rtf, TextDataFormat.Rtf);
break;
case ParamSetHtml:
SetTextContents(Html, TextDataFormat.Html);
break;
default:
SetTextContents(Text, TextDataFormat.UnicodeText);
break;
}
});
}
...
}
Note that the cmdlet typically declares a default ParameterSetName that helps PowerShell determine the "default" parameter set to use when there is ambiguity. Later on, if needed, you can determine which parameter set is in force by querying this.ParameterSetName as the switch statement does above in the EndProcessing() override.

I came here but with an additional requirement: Optional mutual exclusive parameters.
This post here helped me to find half of the answer. So I thought to post here the full answer in case someone has the same requirements.
The code below can be used at the top of a Powershell script to have 4 optional parameters of which LaunchAsAdmin and LaunchAsCouponBrowser are mutually exclusive while token and WorkstationName are also optional but can be combined with any other parameter.
[CmdletBinding(DefaultParametersetName="default")]
Param(
[string]$token,
[string]$WorkstationName,
[parameter(ParameterSetName="seta")][switch]$LaunchAsAdmin,
[parameter(ParameterSetName="setb")][switch]$LaunchAsCouponBrowser
)

Related

PowerShell run exe file and pass args as string var

I'm creating a power shall scrip to run exe file with arguments. The list of args are constructed in a way that if value of the arg is empty or null, the parameter shall not be passed
Below is my script
$runnerCommand = " "
[string]$splitRun = "20:1"
[string]$maxTestWorkers = "777"
[string]$retryTimes = "9"
[string]$testFilterInXmlFormat = "<filter><cat>XX</cat></filter>"
#$runnerCommand += '--testDllPath ' + $testDllPath + " "
if ($splitRun){
$runnerCommand+= "--splitRun '$splitRun' "
}
if ($maxTestWorkers){
$runnerCommand+= "--maxTestWorkers '$maxTestWorkers' "
}
if ($retryTimes){
$runnerCommand+= "--retryTimes '$retryTimes' "
}
if ($testFilterInXmlFormat){
$runnerCommand+= "--testFilterInXmlFormat '$testFilterInXmlFormat' "
}
$cmdPath = "C:\AutoTests\TestAutomation.Runner\bin\Debug\TestAutomation.Runner.exe"
& $cmdPath --testDllPath C:/AutoTests/Build/TestAutomation.TestsGUI.dll $runnerCommand
It looks like that PowerShell do a 'new line' before $runnerCommand in the last line of code that results in not passing the args from $runnerCommand
Please suggest how to solve the problem.
I tried different approaches
You're currently passing all of the arguments as a single string. You should instead use an array with each argument as a separate element. I can't actually test this, but something like this should work:
[string]$splitRun = "20:1"
[string]$maxTestWorkers = "777"
[string]$retryTimes = "9"
[string]$testFilterInXmlFormat = "<filter><cat>XX</cat></filter>"
$runnerArgs = #(
'--testDllPath', 'C:/AutoTests/Build/TestAutomation.TestsGUI.dll'
if ($splitRun) {
'--splitRun', $splitRun
}
if ($maxTestWorkers) {
'--maxTestWorkers', $maxTestWorkers
}
if ($retryTimes) {
'--retryTimes', $retryTimes
}
if ($testFilterInXmlFormat) {
'--testFilterInXmlFormat', $testFilterInXmlFormat
}
)
$cmdPath = "C:\AutoTests\TestAutomation.Runner\bin\Debug\TestAutomation.Runner.exe"
& $cmdPath $runnerArgs
Note that PowerShell allows expressions, including if-expressions, inside the array sub-expression operator (#()).

converting javascript "new RegExp" into powershell [regex]::new()

how would i implement the following javascript code snippet in powershell?
String.prototype.regexCount = function (pattern) {
if (pattern.flags.indexOf("g") < 0) {
pattern = new RegExp(pattern.source, pattern.flags + "g");
}
return (this.match(pattern) || []).length;
};
I'm thinking its something like this:
$regexCount = {
param(
$pattern
)
# ??????
if ($pattern.flags.indexOf("g") -lt 0) {
# ????
# $pattern = new RegExp(pattern.source, pattern.flags + "g");
$pattern = [regex]::new($pattern)
}
# ????
return ($this.match($pattern) || []).length;
}
I have almost the entire script converted into powershell except for this little nugget of code... Actually, i'm a little bit clueless when javascript starts creating lambda functions with regular expression objects...
for instance what's the significants of string.prototype.somename? wouldn't you just save the lambda to any variable name?
Using Update-TypeData, create a type-level ScriptMethod ETS member for the .NET string type (System.String):
Update-TypeData -TypeName System.String -MemberName RegexCount -MemberType ScriptMethod -Value {
param([regex] $Regex)
$Regex.Matches($this).Count
}
Now you can call the .RegexCount() method on any string instance, analogous to what your JavaScript code does.
Sample call:
'foo'.RegexCount('.') # -> 3
That is, 3 matches for regex . were found in the input string.

Accessing fixed size array elements from C# in PowerShell

Looking for a way to access individual array elements (continuation of this question)
$CSharpCode = #"
using System;
namespace TestStructure
{
public struct TestStructure
{
public byte Field1;
public unsafe fixed byte Field2[4];
}
}
"#
$cp = New-Object System.CodeDom.Compiler.CompilerParameters
$cp.CompilerOptions = '/unsafe'
Add-Type -TypeDefinition $CSharpCode -CompilerParameters $cp
function ConvertTo-Struct
{
# Only accept struct types (sealed value types that are neither primitive nor enum)
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ $_.IsValueType -and $_.IsSealed -and -not($_.IsPrimitive -or $_.IsEnum) })]
[Type]$TargetType,
[Parameter(Mandatory = $true)]
[byte[]]$BinaryData
)
# Start by calculating minimum size of the underlying memory allocation for the new struct
$memSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type]$TargetType)
# Make sure user actually passed enough data to initialize struct
if($memSize -gt $BinaryData.Length){
Write-Error "Not enough binary data to create an instance of [$($TargetType.FullName)]"
return
}
# now we just need some unmanaged memory in which to create our struct instance
$memPtr = [IntPtr]::Zero
try {
$memPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($memSize)
# copy byte array to allocated unmanaged memory from previous step
[System.Runtime.InteropServices.Marshal]::Copy($BinaryData, 0, $memPtr, $memSize)
# then marshal the new memory region as a struct and return
return [System.Runtime.InteropServices.Marshal]::PtrToStructure($memPtr, [type]$TargetType)
}
finally {
# and finally remember to clean up the allocated memory
if($memPtr -ne [IntPtr]::Zero){
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($memPtr)
}
}
}
$testStructure = ConvertTo-Struct -TargetType ([TestStructure.TestStructure]) -BinaryData (1..100 -as [byte[]])
$testStructure.Field1
$testStructure.Field2
this produces the following output:
1
FixedElementField
-----------------
2
only the first array element of $Field2 is visible, can't access others using $testStructure.Field2[x]
Looking for a way to iterate over FixedBuffer of known type / size
$testStructure.Field2.GetType() says <Field2>e__FixedBuffer0
$testStructure.Field2.FixedElementField.GetType() is byte
can't see a way to access other elements of the array.
PowerShell's type adapter doesn't really have anything in place to handle unsafe fixed byte[] fields, effectively raw pointers from the underlying type system's point of view.
The declared size of the underlying memory allocation is only stored in metadata, which you can locate as follows:
# Locate appropriate field metadata
$fieldInfo = $testStructure.GetType().GetField('Field2')
# Discover fixed buffer attribute
$fixedBufferAttribute = $fieldInfo.CustomAttributes.Where({$_.AttributeType -eq [System.Runtime.CompilerServices.FixedBufferAttribute]}) |Select -First 1
Now we can figure out the size and which array element type is expected:
if($fixedBufferAttribute)
{
# Grab array element type + size from FixedBuffer attribute
$elemType,$size = $fixedBufferAttribute.ConstructorArguments
# Create array of appropriate size
$values = $elemType.MakeArrayType()::new($size)
# Copy values from fixed buffer pointer to managed array
try {
$fixedBufferHandle = [System.Runtime.InteropServices.GCHandle]::Alloc($TestStructure.Field2, 'Pinned')
[System.Runtime.InteropServices.Marshal]::Copy($fixedBufferHandle.AddrOfPinnedObject(), $values, 0, $size)
return $values
}
finally {
$fixedBufferHandle.Free()
}
}
You'll find $values now contains the expected byte values 2, 3, 4 and 5

RegistryValueKind NONE

How can I set a none data type.[Microsoft.Win32.RegistryValueKind]::None in Powershell 2.0?
Powershell 2.0 has types "Unknown, String, ExpandString, Binary, DWord, MultiString, QWord".
learn.microsoft.com
Add-Type -TypeDefinition #"
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.Win32;
using Microsoft.Win32.SafeHandles;
namespace AdvapiSolution
{
public class Advapi
{
public enum HKEY : uint
{
HKEY_CLASSES_ROOT = 0x80000000,
HKEY_CURRENT_USER = 0x80000001,
HKEY_LOCAL_MACHINE = 0x80000002,
HKEY_USERS = 0x80000003,
HKEY_PERFORMANCE_DATA = 0x80000004,
HKEY_PERFORMANCE_TEXT = 0x80000050,
HKEY_PERFORMANCE_NLSTEXT = 0x80000060,
HKEY_CURRENT_CONFIG = 0x80000005
}
private enum VALUE_TYPE : uint
{
REG_NONE= 0,
REG_SZ = 1,
REG_EXPAND_SZ = 2,
REG_BINARY = 3,
REG_DWORD = 4,
REG_DWORD_LITTLE_ENDIAN = 4,
REG_DWORD_BIG_ENDIAN = 5,
REG_LINK = 6,
REG_MULTI_SZ = 7,
REG_RESOURCE_LIST = 8,
REG_FULL_RESOURCE_DESCRIPTOR = 9,
REG_RESOURCE_REQUIREMENTS_LIST = 10,
REG_QWORD_LITTLE_ENDIAN = 11
}
[DllImport("advapi32.dll", CharSet = CharSet.Auto, BestFitMapping = false)]
private static extern int RegSetKeyValueW (
HKEY hkey,
string lpSubKey,
string lpValueName,
VALUE_TYPE type,
byte[] data,
uint dataLength);
public int set_key(HKEY hkey, string subkey, string valuename){
return RegSetKeyValueW(hkey, subkey, valuename, VALUE_TYPE.REG_NONE, null, 0);
}
}
}
"#
In my case data equals null and type is REG_NONE. You can change set_key as you like with object:
$Advapiobject = New-Object 'AdvapiSolution.Advapi'
$Advapiobject.set_key('HKEY_CURRENT_USER',$yoursubkey, $yourvaluename, 'REG_NONE')
Another way is using a static method set_key.
Change set_key to public static int set_key:
[AdvapiSolution.Advapi]::set_key('HKEY_CURRENT_USER',$yoursubkey, $yourvaluename, 'REG_NONE').
If it returns 0, it's okay. If the function fails, the return value is a nonzero error code defined in Winerror.h.
System Error Codes
(As implied in your question), [Microsoft.Win32.RegistryValueKind]::None (which translates to a REG_NONE registry value) requires at least .NET Framework 4, whereas Windows PowerShell v2 is built on .NET Framework 2. (Only Windows PowerShell v3 and above are built on .NET Framework 4 and above).
Unlike C#, PowerShell does not allow you to pass the numerical value of an enum-typed parameter if that value doesn't represent an officially defined member of that enumeration (at that time, in the underlying framework), so attempting to pass (-1), the value of [Microsoft.Win32.RegistryValueKind]::None, does not work in Windows PowerShell v2 - neither with New-ItemProperty / Set-ItemProperty's -Type parameter, nor with .NET API calls ([Microsoft.Win32.Registry]::SetValue(...)).
Therefore, in Windows PowerShell v2, calling the Windows API via P/Invoke declarations implemented via Add-Type is probably your only option - see the RegSetKeyValue() WinAPI function (among others).
Building on your own helpful answer, here's a streamlined solution, which:
uses Add-Type's -MemberDefinition parameter to simplify creation of types that wrap P/Invoke calls.
Exposes a static [WinApiHelper.Registry]::SetNoneValue() method, which:
focuses only on creating REG_NONE values
also supports creating such values with byte data ([byte[]] arrays)
has no return value, but throws a Win32Exception should an error occur.
Add-Type -Namespace WinApiHelper -Name Registry -MemberDefinition #'
public enum HKEY : uint
{
HKEY_CLASSES_ROOT = 0x80000000,
HKEY_CURRENT_CONFIG = 0x80000005,
HKEY_CURRENT_USER = 0x80000001,
HKEY_LOCAL_MACHINE = 0x80000002,
HKEY_USERS = 0x80000003
}
[DllImport("advapi32.dll")]
private static extern int RegSetKeyValue(
HKEY hkey,
string lpSubKey,
string lpValueName,
UInt32 type,
byte[] data,
UInt32 dataLength
);
public static void SetNoneValue(HKEY hkey, string subkey, string valuename, byte[] bytes)
{
int rc = RegSetKeyValue(hkey, subkey, valuename, 0 /* REG_NONE */, bytes, (UInt32) (bytes == null ? 0 : bytes.Length));
if (rc != 0) {
// Access the error code in PowerShell with $Error[0].Exception.InnerException.NativeErrorCode
throw new System.ComponentModel.Win32Exception(rc);
}
}
// Overload that creates an empty value.
// Note: .NET 2 doesn't support optional parameters, so an explicit overload is required.
public static void SetNoneValue(HKEY hkey, string subkey, string valuename) {
SetNoneValue(hkey, subkey, valuename, null);
}
'#
Sample calls:
# Create an empty REG_NONE value named 'bar1' in key HKEY_CURRENT_USER\foo.
[WinApiHelper.Registry]::SetNoneValue('HKEY_CURRENT_USER', 'foo', 'bar1')
# Create a REG_NONE value named 'bar2' with a byte array.
[WinApiHelper.Registry]::SetNoneValue('HKEY_CURRENT_USER', 'foo', 'bar2', [byte[]] (42, 43))

PowerShell Passing Variables

I am attempting to call a function in powershell, but pass it variables so I don't need to repeat the funxtion X amount of times with different values. I would like it so that, when I call the function I simply type:
Foo(Red,12,1.8)
And the generic function, foo, as follows:
function Foo(Colour, Age, Height){
<# Function does something with data here #>
}
Is this possible using powershell, and if so, would any changes made to those variables be saved after the function has completed?
Yes you can use parameters in functions.
By default the parameters only change within the scope of the function, but you can choose to return the same parameters as results if you like.
example (uses splatting)-
function Foo{
param(
[string]$Color,
[int]$Age,
[decimal]$Height
)
$Age += 1
$Height += 1
$ExampleOutput = [ordered]#{
Color = $Color
Age = $Age
Height = $Height
}
return ($ExampleOutput)
}
$Parameters = #{
Color = "Red"
Age = 12
Height = 1.8
}
Foo #Parameters
Yes try default values:
function Foo($Colour='Red',$Age=12,$Height=1.8){
<# Function does something with data here #>
}