2026-05-16 18:14:41 -05:00
using LANCommander.SDK.Parsers.Ini ;
2024-08-05 18:02:01 -05:00
using System ;
using System.IO ;
2025-05-13 00:52:19 +02:00
using System.Linq ;
2024-08-05 18:02:01 -05:00
using System.Management.Automation ;
2025-05-13 00:52:19 +02:00
using System.Text ;
2024-08-05 18:02:01 -05:00
namespace LANCommander.SDK.PowerShell.Cmdlets
{
2025-05-13 19:49:03 +02:00
/// <summary>
2026-05-16 18:14:41 -05:00
/// Cmdlet for updating an INI file value. This cmdlet updates, adds, or removes a key-value pair within a specified section
2025-05-13 19:49:03 +02:00
/// of an INI file, based on provided parameters.
/// </summary>
2024-08-05 18:02:01 -05:00
[Cmdlet(VerbsData.Update, "IniValue")]
[OutputType(typeof(string))]
2026-01-24 16:37:07 -06:00
public class UpdateIniValueCmdlet : Cmdlet
2024-08-05 18:02:01 -05:00
{
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the section in the INI file that contains the key to be updated.
/// </summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "Specifies the section in the INI file that contains the key to be updated.")]
2025-05-13 00:52:19 +02:00
[Alias("s")]
2024-08-05 18:02:01 -05:00
public string Section { get ; set ; }
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the key within the section whose value should be updated.
/// </summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "Specifies the key within the section whose value should be updated.")]
2025-05-13 00:52:19 +02:00
[Alias("k")]
2024-08-05 18:02:01 -05:00
public string Key { get ; set ; }
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the new value to assign to the provided key.
/// </summary>
[Parameter(Mandatory = true, Position = 2, HelpMessage = "Specifies the new value to assign to the provided key.")]
2025-05-13 00:52:19 +02:00
[Alias("v")]
2024-08-05 18:02:01 -05:00
public string Value { get ; set ; }
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the full file path to the INI file that should be processed.
/// </summary>
[Parameter(Mandatory = true, Position = 3, HelpMessage = "Specifies the file path to the INI file that should be processed.")]
2025-05-13 00:52:19 +02:00
[Alias("f")]
2024-08-05 18:02:01 -05:00
public string FilePath { get ; set ; }
2025-05-13 19:49:03 +02:00
/// <summary>
2025-05-13 20:15:35 +02:00
/// If set, controls whether the value should be wrapped in quotes.
/// If <c>null</c>, the cmdlet will add quotes if the value is not already quoted.
/// If <c>true</c>, quotes will be enforced.
/// If <c>false</c>, any surrounding quotes will be removed.
2025-05-13 19:49:03 +02:00
/// </summary>
2025-05-13 20:15:35 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, controls whether the value will be wrapped in quotes. Nullable: if null, adds quotes if missing; if true, enforces quotes; if false, removes quotes.")]
2025-05-13 00:52:19 +02:00
[Alias("wrap", "quotes")]
2025-05-13 20:15:35 +02:00
public bool? WrapValueInQuotes { get ; set ; } = null ;
2024-08-05 18:02:01 -05:00
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that updates an existing key or adds a new one if it does not exist.
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "If set, the cmdlet will update an existing key or add a new one if it does not exist.")]
2025-05-13 00:52:19 +02:00
[Alias("add")]
2025-05-13 19:42:05 +02:00
public SwitchParameter UpdateOrAdd { get ; set ; } = true ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that restricts the operation to updating existing keys only; no new key will be added.
/// </summary>
2025-05-13 19:42:05 +02:00
[Alias("update-only", "only-update")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, restricts the operation to updating existing keys only; no new key will be added.")]
2025-05-13 19:42:05 +02:00
public SwitchParameter NoAdd
{
get = > new ( ! UpdateOrAdd ) ;
set = > UpdateOrAdd = ! value . ToBool ( ) ;
}
2025-05-13 00:52:19 +02:00
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that specifies the key(s) will only be removed from the section without updating or adding any value.
/// </summary>
2025-05-13 00:52:19 +02:00
[Alias("remove-only")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, the key(s) will only be removed from the section without updating or adding any value.")]
2025-05-13 00:52:19 +02:00
public SwitchParameter OnlyRemove { get ; set ; } = false ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that clears all instances of the specified key in the section.
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "If set, all instances of the specified key in the section will be cleared.")]
2025-05-13 00:52:19 +02:00
public SwitchParameter Clear { get ; set ; } = false ;
2025-05-13 19:49:03 +02:00
/// <summary>
2026-05-16 18:14:41 -05:00
/// Gets or sets a switch parameter that specifies a new key-value pair will always be appended,
2025-05-13 19:49:03 +02:00
/// even if the key already exists.
/// </summary>
2025-05-13 00:52:19 +02:00
[Alias("append")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, a new key-value pair will always be appended, even if the key already exists.")]
2025-05-13 00:52:19 +02:00
public SwitchParameter AlwaysAppend { get ; set ; } = false ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the index position at which the new key-value pair should be inserted.
/// If not provided, the key is added at the end.
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Specifies the index position at which the new key-value pair should be inserted. If not provided, the key is added at the end.")]
2025-05-13 00:52:19 +02:00
[Alias("insert")]
public int? InsertIndex { get ; set ; } = null ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a value indicating whether duplicate keys are allowed within the section.
/// </summary>
2025-05-13 00:52:19 +02:00
[Alias("keepkey", "keydup")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If true, duplicate keys are allowed within the section.")]
2025-05-13 00:52:19 +02:00
public bool KeepKeyDuplicates { get ; set ; } = true ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that prevents duplicate keys in the section.
/// </summary>
2025-05-13 19:42:54 +02:00
[Alias("nokey", "nokeydup")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, duplicate keys in the section are prevented.")]
2025-05-13 19:42:54 +02:00
public SwitchParameter NoKeyDuplicates
{
get = > new ( ! KeepKeyDuplicates ) ;
set = > KeepKeyDuplicates = ! value . ToBool ( ) ;
}
2025-05-13 00:52:19 +02:00
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a value indicating whether duplicate sections in the INI file are allowed.
/// </summary>
2025-05-13 00:52:19 +02:00
[Alias("keepsec", "secdup")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If true, duplicate sections in the INI file are allowed.")]
2025-05-13 00:52:19 +02:00
public bool KeepSectionDuplicates { get ; set ; } = true ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets a switch parameter that prevents duplicate section names in the INI file.
/// </summary>
2025-05-13 19:42:54 +02:00
[Alias("nosec", "nosecdup")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "If set, duplicate section names are not permitted in the INI file.")]
public SwitchParameter NoSectionDuplicates
2025-05-13 19:42:54 +02:00
{
get = > new ( ! KeepSectionDuplicates ) ;
set = > KeepSectionDuplicates = ! value . ToBool ( ) ;
}
2025-05-13 00:52:19 +02:00
2025-05-13 19:49:03 +02:00
/// <summary>
/// Gets or sets the encoding (codepage) for reading and writing the INI file.
/// Examples include 'UTF8', 'Latin', 'ASCII', or 'Unicode'.
/// </summary>
2025-05-13 00:52:19 +02:00
[Alias("encoding", "enc")]
2025-05-13 19:49:03 +02:00
[Parameter(Mandatory = false, HelpMessage = "Specifies the encoding (codepage) for reading and writing the INI file (e.g., 'UTF8', 'Latin', 'ASCII', 'Unicode').")]
2025-05-13 00:52:19 +02:00
public string Codepage { get ; set ; } = "Latin" ;
2025-05-13 19:49:03 +02:00
/// <summary>
/// Returns the appropriate <see cref="Encoding"/> instance based on the specified Codepage.
/// </summary>
/// <returns>The <see cref="Encoding"/> for the specified codepage.</returns>
2025-05-13 00:52:19 +02:00
protected Encoding GetEncoding ( )
{
string page = Codepage ? . ToLower ( ) ;
switch ( page )
{
2025-05-13 19:49:03 +02:00
case "uft8" :
return Encoding . UTF8 ;
2025-05-13 00:52:19 +02:00
case "latin" :
case "latin1" :
2025-05-13 19:49:03 +02:00
case "iso-8859-1" :
2025-05-13 00:52:19 +02:00
return Encoding . Latin1 ;
case "asci" :
case "ascii" :
return Encoding . ASCII ;
case "unicode" :
return Encoding . Unicode ;
}
return Encoding . Default ;
}
2025-05-13 19:49:03 +02:00
/// <summary>
/// Processes the record by loading the specified INI file, updating, adding, or removing the specified key/value pair,
/// and then saving the file.
/// </summary>
2024-08-05 18:02:01 -05:00
protected override void ProcessRecord ( )
{
2025-05-13 00:52:19 +02:00
if ( ! File . Exists ( FilePath ) )
return ;
2026-05-16 18:14:41 -05:00
var encoding = GetEncoding ( ) ;
var options = new IniParseOptions
2024-08-05 18:02:01 -05:00
{
2026-05-16 18:14:41 -05:00
Encoding = encoding ,
AllowDuplicateKeys = KeepKeyDuplicates ,
AllowDuplicateSections = KeepSectionDuplicates ,
2025-05-13 00:52:19 +02:00
} ;
2025-05-13 19:49:03 +02:00
2026-05-16 18:14:41 -05:00
var ini = IniParser . Load ( FilePath , options ) ;
2024-08-05 18:02:01 -05:00
2026-05-16 18:14:41 -05:00
// Retrieve the specified section
2025-05-13 00:52:19 +02:00
var iniSection = ini . Sections [ Section ] ;
2025-05-13 19:44:52 +02:00
if ( iniSection = = null & & ! AlwaysAppend & & ! UpdateOrAdd )
2025-05-13 00:52:19 +02:00
return ;
2026-05-16 18:14:41 -05:00
// Create a new section if it does not exist
2025-05-13 19:44:52 +02:00
if ( iniSection = = null )
{
2026-05-16 18:14:41 -05:00
iniSection = new IniSection ( Section ) ;
2025-05-13 19:44:52 +02:00
ini . Sections . Add ( iniSection ) ;
}
2026-05-16 18:14:41 -05:00
// Function for matching a key using case-insensitive comparison
2025-05-13 00:52:19 +02:00
bool keyMatcher ( IniKey x ) = > string . Equals ( x . Name , Key , StringComparison . OrdinalIgnoreCase ) ;
2026-05-16 18:14:41 -05:00
// If the operation is to remove or clear keys, perform deletion of matching keys
2025-05-13 00:52:19 +02:00
if ( OnlyRemove | | Clear )
{
var list = iniSection . Keys . Where ( keyMatcher ) . ToList ( ) ;
list . ForEach ( x = > iniSection . Keys . Remove ( x ) ) ;
2024-08-05 18:02:01 -05:00
}
2025-05-13 00:52:19 +02:00
2026-05-16 18:14:41 -05:00
// If removal is not the sole operation, proceed with updating or adding the value
2025-05-13 00:52:19 +02:00
if ( ! OnlyRemove )
{
2026-05-16 18:14:41 -05:00
// Update the last found key (most engines use last value for duplicate keys)
2025-05-13 00:52:19 +02:00
var firstMatch = iniSection . Keys . LastOrDefault ( keyMatcher ) ;
2025-05-13 20:15:35 +02:00
var iniValue = Value ;
2026-05-16 18:14:41 -05:00
// Adjust the value's surrounding quotes based on the WrapValueInQuotes parameter
2025-05-13 20:15:35 +02:00
iniValue = ApplyQuoteWrapping ( iniValue , firstMatch ? . Value ) ;
2026-05-16 18:14:41 -05:00
// Insert a new key-value pair if appending is enforced or the key does not exist
2025-05-13 00:52:19 +02:00
if ( AlwaysAppend . ToBool ( ) | | ( UpdateOrAdd & & firstMatch = = null ) )
{
if ( InsertIndex . HasValue & & InsertIndex . Value > = 0 )
2025-05-13 20:15:35 +02:00
iniSection . Keys . Insert ( Math . Clamp ( InsertIndex . Value , 0 , iniSection . Keys . Count - 1 ) , Key , iniValue ) ;
2025-05-13 00:52:19 +02:00
else
2025-05-13 20:15:35 +02:00
iniSection . Keys . Add ( Key , iniValue ) ;
2025-05-13 00:52:19 +02:00
}
else if ( firstMatch ! = null )
{
2026-05-16 18:14:41 -05:00
// Handle updating an existing key, optionally repositioning it
2025-05-13 19:49:03 +02:00
var insertIndex = ( InsertIndex . HasValue & & InsertIndex . Value > = 0 )
? Math . Clamp ( InsertIndex . Value , 0 , iniSection . Keys . Count - 1 )
: - 1 ;
2025-05-13 00:52:19 +02:00
if ( insertIndex > = 0 )
{
iniSection . Keys . Remove ( firstMatch ) ;
iniSection . Keys . Insert ( insertIndex , firstMatch ) ;
}
2026-05-16 18:14:41 -05:00
// Update the existing key's value
2025-05-13 20:15:35 +02:00
firstMatch . Value = iniValue ;
2025-05-13 00:52:19 +02:00
}
}
2026-05-16 18:14:41 -05:00
// Save the modified INI file
IniParser . Save ( ini , FilePath , encoding ) ;
2024-08-05 18:02:01 -05:00
}
2025-05-13 20:15:35 +02:00
/// <summary>
/// Applies quote wrapping on the given string based on the <see cref="WrapValueInQuotes"/> parameter.
/// </summary>
private string ApplyQuoteWrapping ( string newValue , string existingValue )
{
if ( string . IsNullOrEmpty ( newValue ) )
return newValue ;
bool isNewValueQuoted = newValue . StartsWith ( "\"" ) & & newValue . EndsWith ( "\"" ) | |
newValue . StartsWith ( "'" ) & & newValue . EndsWith ( "'" ) ;
bool isExistingValueQuoted = ! string . IsNullOrEmpty ( existingValue ) & &
( existingValue . StartsWith ( "\"" ) & & existingValue . EndsWith ( "\"" ) | |
existingValue . StartsWith ( "'" ) & & existingValue . EndsWith ( "'" ) ) ;
if ( WrapValueInQuotes = = null )
{
return isExistingValueQuoted ? $"\" { newValue } \ "" : newValue ;
}
else if ( WrapValueInQuotes . Value )
{
return isNewValueQuoted ? newValue : $"\" { newValue } \ "" ;
}
else
{
return isNewValueQuoted ? newValue . Substring ( 1 , newValue . Length - 2 ) : newValue ;
}
}
2024-08-05 18:02:01 -05:00
}
}