122 lines
4.6 KiB
C#
122 lines
4.6 KiB
C#
namespace Omika.LanguesProbe;
|
|
|
|
/// <summary>
|
|
/// Arguments d'une commande, séparés en positionnels et options nommées.
|
|
///
|
|
/// <para>
|
|
/// L'analyse est volontairement stricte : une option inconnue est <b>refusée</b>
|
|
/// au lieu d'être ignorée. Sur un poste de développement, une faute de frappe
|
|
/// silencieuse (<c>--dossiers</c>, <c>--sens</c> mal orthographié) ferait
|
|
/// travailler l'outil sur autre chose que ce que le développeur croit, et le
|
|
/// diagnostic serait faux sans que rien ne le signale.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Les valeurs ne sont jamais redécoupées : un chemin contenant des espaces
|
|
/// arrive en un seul argument, grâce aux guillemets du shell, et le reste.
|
|
/// </para>
|
|
/// </summary>
|
|
internal sealed class Arguments
|
|
{
|
|
private readonly List<string> _positionnels = [];
|
|
private readonly Dictionary<string, string> _options = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private Arguments()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Analyse les arguments d'une commande.
|
|
/// </summary>
|
|
/// <param name="args">Arguments suivant le nom de la commande.</param>
|
|
/// <param name="optionsAttendues">Options nommées acceptées, sans les tirets.</param>
|
|
/// <exception cref="AideDemandeeException"><c>--aide</c> ou <c>-h</c> est présent.</exception>
|
|
/// <exception cref="ErreurUsageException">Option inconnue, ou option sans valeur.</exception>
|
|
public static Arguments Analyser(string[] args, params string[] optionsAttendues)
|
|
{
|
|
var resultat = new Arguments();
|
|
var connues = new HashSet<string>(optionsAttendues, StringComparer.OrdinalIgnoreCase);
|
|
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
string argument = args[i];
|
|
|
|
if (argument is "--aide" or "-h")
|
|
{
|
|
// Reconnu à n'importe quelle position : « traduire --aide » doit expliquer la
|
|
// commande, pas réclamer --dossier.
|
|
throw new AideDemandeeException();
|
|
}
|
|
|
|
if (!argument.StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
resultat._positionnels.Add(SansGuillemets(argument));
|
|
continue;
|
|
}
|
|
|
|
string nom = argument[2..];
|
|
if (!connues.Contains(nom))
|
|
{
|
|
throw new ErreurUsageException($"option inconnue « {argument} »");
|
|
}
|
|
|
|
if (i + 1 >= args.Length)
|
|
{
|
|
throw new ErreurUsageException($"l'option « {argument} » attend une valeur");
|
|
}
|
|
|
|
// La valeur est consommée telle quelle, même si elle commence par « -- » : un texte à
|
|
// traduire peut légitimement commencer par un tiret.
|
|
resultat._options[nom] = SansGuillemets(args[++i]);
|
|
}
|
|
|
|
return resultat;
|
|
}
|
|
|
|
/// <summary>Argument positionnel, ou erreur d'usage s'il manque.</summary>
|
|
public string Positionnel(int index, string description)
|
|
=> index < _positionnels.Count
|
|
? _positionnels[index]
|
|
: throw new ErreurUsageException($"argument manquant : {description}");
|
|
|
|
/// <summary>Valeur d'une option, ou <see langword="null"/> si elle est absente.</summary>
|
|
public string? Option(string nom)
|
|
=> _options.TryGetValue(nom, out string? valeur) ? valeur : null;
|
|
|
|
/// <summary>Valeur d'une option, ou <paramref name="defaut"/> si elle est absente.</summary>
|
|
public string Option(string nom, string defaut) => Option(nom) ?? defaut;
|
|
|
|
/// <summary>Positionnels à partir d'un index (un texte peut arriver en plusieurs arguments).</summary>
|
|
public IReadOnlyList<string> Reste(int depuis)
|
|
{
|
|
if (depuis >= _positionnels.Count)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return _positionnels.GetRange(depuis, _positionnels.Count - depuis);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retire une paire de guillemets entourant une valeur : un copier-coller
|
|
/// depuis un document, ou un shell qui transmet les guillemets eux-mêmes,
|
|
/// produirait sinon un chemin de fichier introuvable.
|
|
/// </summary>
|
|
private static string SansGuillemets(string valeur)
|
|
=> valeur.Length >= 2 && valeur[0] == '"' && valeur[^1] == '"' ? valeur[1..^1] : valeur;
|
|
}
|
|
|
|
/// <summary>Argument manquant, option inconnue : l'appel est incorrect (code de sortie 1).</summary>
|
|
internal sealed class ErreurUsageException : Exception
|
|
{
|
|
public ErreurUsageException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary><c>--aide</c> demandé : ce n'est pas une erreur (code de sortie 0).</summary>
|
|
internal sealed class AideDemandeeException : Exception
|
|
{
|
|
}
|