summaryrefslogtreecommitdiff
path: root/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs
blob: e84445d7b01e54f50a2b43fd9368af0dd0633af0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
{
    /// <summary>Provides methods for parsing command-line arguments.</summary>
    internal class ArgumentParser : IReadOnlyList<string>
    {
        /*********
        ** Fields
        *********/
        /// <summary>The command name for errors.</summary>
        private readonly string CommandName;

        /// <summary>The arguments to parse.</summary>
        private readonly string[] Args;

        /// <summary>Writes messages to the console and log file.</summary>
        private readonly IMonitor Monitor;


        /*********
        ** Accessors
        *********/
        /// <summary>Get the number of arguments.</summary>
        public int Count => this.Args.Length;

        /// <summary>Get the argument at the specified index in the list.</summary>
        /// <param name="index">The zero-based index of the element to get.</param>
        public string this[int index] => this.Args[index];


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="commandName">The command name for errors.</param>
        /// <param name="args">The arguments to parse.</param>
        /// <param name="monitor">Writes messages to the console and log file.</param>
        public ArgumentParser(string commandName, string[] args, IMonitor monitor)
        {
            this.CommandName = commandName;
            this.Args = args;
            this.Monitor = monitor;
        }

        /// <summary>Try to read a string argument.</summary>
        /// <param name="index">The argument index.</param>
        /// <param name="name">The argument name for error messages.</param>
        /// <param name="value">The parsed value.</param>
        /// <param name="required">Whether to show an error if the argument is missing.</param>
        /// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param>
        public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null)
        {
            value = null;

            // validate
            if (this.Args.Length < index + 1)
            {
                if (required)
                    this.LogError($"Argument {index} ({name}) is required.");
                return false;
            }
            if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.OrdinalIgnoreCase))
            {
                this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}.");
                return false;
            }

            // get value
            value = this.Args[index];
            return true;
        }

        /// <summary>Try to read an integer argument.</summary>
        /// <param name="index">The argument index.</param>
        /// <param name="name">The argument name for error messages.</param>
        /// <param name="value">The parsed value.</param>
        /// <param name="required">Whether to show an error if the argument is missing.</param>
        /// <param name="min">The minimum value allowed.</param>
        /// <param name="max">The maximum value allowed.</param>
        public bool TryGetInt(int index, string name, out int value, bool required = true, int? min = null, int? max = null)
        {
            value = 0;

            // get argument
            if (!this.TryGet(index, name, out string raw, required))
                return false;

            // parse
            if (!int.TryParse(raw, out value))
            {
                this.LogIntFormatError(index, name, min, max);
                return false;
            }

            // validate
            if ((min.HasValue && value < min) || (max.HasValue && value > max))
            {
                this.LogIntFormatError(index, name, min, max);
                return false;
            }

            return true;
        }

        /// <summary>Try to read a decimal argument.</summary>
        /// <param name="index">The argument index.</param>
        /// <param name="name">The argument name for error messages.</param>
        /// <param name="value">The parsed value.</param>
        /// <param name="required">Whether to show an error if the argument is missing.</param>
        /// <param name="min">The minimum value allowed.</param>
        /// <param name="max">The maximum value allowed.</param>
        public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null)
        {
            value = 0;

            // get argument
            if (!this.TryGet(index, name, out string raw, required))
                return false;

            // parse
            if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value))
            {
                this.LogDecimalFormatError(index, name, min, max);
                return false;
            }

            // validate
            if ((min.HasValue && value < min) || (max.HasValue && value > max))
            {
                this.LogDecimalFormatError(index, name, min, max);
                return false;
            }

            return true;
        }

        /// <summary>Returns an enumerator that iterates through the collection.</summary>
        /// <returns>An enumerator that can be used to iterate through the collection.</returns>
        public IEnumerator<string> GetEnumerator()
        {
            return ((IEnumerable<string>)this.Args).GetEnumerator();
        }

        /// <summary>Returns an enumerator that iterates through a collection.</summary>
        /// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Log a usage error.</summary>
        /// <param name="message">The message describing the error.</param>
        private void LogError(string message)
        {
            this.Monitor.Log($"{message} Type 'help {this.CommandName}' for usage.", LogLevel.Error);
        }

        /// <summary>Print an error for an invalid int argument.</summary>
        /// <param name="index">The argument index.</param>
        /// <param name="name">The argument name for error messages.</param>
        /// <param name="min">The minimum value allowed.</param>
        /// <param name="max">The maximum value allowed.</param>
        private void LogIntFormatError(int index, string name, int? min, int? max)
        {
            if (min.HasValue && max.HasValue)
                this.LogError($"Argument {index} ({name}) must be an integer between {min} and {max}.");
            else if (min.HasValue)
                this.LogError($"Argument {index} ({name}) must be an integer and at least {min}.");
            else if (max.HasValue)
                this.LogError($"Argument {index} ({name}) must be an integer and at most {max}.");
            else
                this.LogError($"Argument {index} ({name}) must be an integer.");
        }

        /// <summary>Print an error for an invalid decimal argument.</summary>
        /// <param name="index">The argument index.</param>
        /// <param name="name">The argument name for error messages.</param>
        /// <param name="min">The minimum value allowed.</param>
        /// <param name="max">The maximum value allowed.</param>
        private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max)
        {
            if (min.HasValue && max.HasValue)
                this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}.");
            else if (min.HasValue)
                this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}.");
            else if (max.HasValue)
                this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}.");
            else
                this.LogError($"Argument {index} ({name}) must be a decimal.");
        }
    }
}