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 { }