2025-05-13 00:52:19 +02:00
using MadMilkman.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>
/// Cmdlet for updating an INI file value. This cmdlet updates, adds, or removes a key-value pair within a specified section
/// 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>
/// Gets or sets a switch parameter that specifies a new key-value pair will always be appended,
/// 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 19:49:03 +02:00
// Check if the specified INI file exists; if not, exit the method.
2025-05-13 00:52:19 +02:00
if ( ! File . Exists ( FilePath ) )
return ;
2025-05-13 19:49:03 +02:00
// Configure INI file options based on provided parameters.
2025-05-13 00:52:19 +02:00
var iniOptions = new IniOptions ( )
2024-08-05 18:02:01 -05:00
{
2025-05-13 00:52:19 +02:00
Encoding = GetEncoding ( ) ,
SectionDuplicate = KeepSectionDuplicates ? IniDuplication . Allowed : IniDuplication . Ignored ,
KeyDuplicate = KeepKeyDuplicates ? IniDuplication . Allowed : IniDuplication . Ignored ,
} ;
2025-05-13 19:49:03 +02:00
// Load the INI file with the specified options.
2025-05-13 00:52:19 +02:00
var ini = new IniFile ( iniOptions ) ;
ini . Load ( FilePath ) ;
2024-08-05 18:02:01 -05:00
2025-05-13 19:49:03 +02:00
// Retrieve the specified section; if not found and appending/updating is not allowed, exit.
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 ;
2025-05-13 19:49:03 +02:00
// Create a new section if it does not exist.
2025-05-13 19:44:52 +02:00
if ( iniSection = = null )
{
iniSection = new IniSection ( ini , Section ) ;
ini . Sections . Add ( iniSection ) ;
}
2025-05-13 19:49:03 +02: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 ) ;
2025-05-13 19:49:03 +02: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
2025-05-13 20:15:35 +02: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 )
{
// assuming most of the engines interpret INI files from top to bottom using the last value of a multiple existing key, we update the last found key
var firstMatch = iniSection . Keys . LastOrDefault ( keyMatcher ) ;
2025-05-13 20:15:35 +02:00
var iniValue = Value ;
// Adjust the value's surrounding quotes based on the WrapValueInQuotes parameter.
iniValue = ApplyQuoteWrapping ( iniValue , firstMatch ? . Value ) ;
2025-05-13 19:49:03 +02: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 )
{
2025-05-13 19:49:03 +02:00
// Handle updating an existing key, optionally repositioning it if an insertion index is specified.
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 ) ;
}
2025-05-13 20:15:35 +02:00
// Update the existing key's value.
firstMatch . Value = iniValue ;
2025-05-13 00:52:19 +02:00
}
2025-05-13 19:49:03 +02:00
// No else clause – updating without adding a new key should be possible.
2025-05-13 00:52:19 +02:00
}
2025-05-13 19:49:03 +02:00
// Save the modified INI file.
2025-05-13 00:52:19 +02:00
ini . Save ( FilePath ) ;
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.
/// <list type="bullet">
/// <item>
/// <description>If <c>null</c>: uses the quoting format of the existing INI value if available.</description>
/// </item>
/// <item>
/// <description>If <c>true</c>: enforces quotes around the value.</description>
/// </item>
/// <item>
/// <description>If <c>false</c>: ensures the value is stored without quotes.</description>
/// </item>
/// </list>
/// </summary>
/// <param name="newValue">The new value to be processed.</param>
/// <param name="existingValue">The existing INI value (if any) for reference.</param>
/// <returns>The processed string with quotes applied or removed according to the settings.</returns>
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 )
{
// If null, use the quoting style of the existing value if available.
return isExistingValueQuoted ? $"\" { newValue } \ "" : newValue ;
}
else if ( WrapValueInQuotes . Value )
{
// If true, enforce quotes.
return isNewValueQuoted ? newValue : $"\" { newValue } \ "" ;
}
else
{
// If false, remove any surrounding quotes.
return isNewValueQuoted ? newValue . Substring ( 1 , newValue . Length - 2 ) : newValue ;
}
}
2024-08-05 18:02:01 -05:00
}
}