Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user