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