namespace Omika.LanguesProbe;
///
/// Arguments d'une commande, séparés en positionnels et options nommées.
///
///
/// L'analyse est volontairement stricte : une option inconnue est refusée
/// au lieu d'être ignorée. Sur un poste de développement, une faute de frappe
/// silencieuse (--dossiers, --sens 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.
///
///
///
/// 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.
///
///
internal sealed class Arguments
{
private readonly List _positionnels = [];
private readonly Dictionary _options = new(StringComparer.OrdinalIgnoreCase);
private Arguments()
{
}
///
/// Analyse les arguments d'une commande.
///
/// Arguments suivant le nom de la commande.
/// Options nommées acceptées, sans les tirets.
/// --aide ou -h est présent.
/// Option inconnue, ou option sans valeur.
public static Arguments Analyser(string[] args, params string[] optionsAttendues)
{
var resultat = new Arguments();
var connues = new HashSet(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;
}
/// Argument positionnel, ou erreur d'usage s'il manque.
public string Positionnel(int index, string description)
=> index < _positionnels.Count
? _positionnels[index]
: throw new ErreurUsageException($"argument manquant : {description}");
/// Valeur d'une option, ou si elle est absente.
public string? Option(string nom)
=> _options.TryGetValue(nom, out string? valeur) ? valeur : null;
/// Valeur d'une option, ou si elle est absente.
public string Option(string nom, string defaut) => Option(nom) ?? defaut;
/// Positionnels à partir d'un index (un texte peut arriver en plusieurs arguments).
public IReadOnlyList Reste(int depuis)
{
if (depuis >= _positionnels.Count)
{
return [];
}
return _positionnels.GetRange(depuis, _positionnels.Count - depuis);
}
///
/// 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.
///
private static string SansGuillemets(string valeur)
=> valeur.Length >= 2 && valeur[0] == '"' && valeur[^1] == '"' ? valeur[1..^1] : valeur;
}
/// Argument manquant, option inconnue : l'appel est incorrect (code de sortie 1).
internal sealed class ErreurUsageException : Exception
{
public ErreurUsageException(string message)
: base(message)
{
}
}
/// --aide demandé : ce n'est pas une erreur (code de sortie 0).
internal sealed class AideDemandeeException : Exception
{
}