private static Context ParseArgs(string[] args) { var context = new Context(); var resolvedArgs = args .SelectMany(ResolveOptionsFromFile) .ToArray(); var options = new[] { new Option { Description = "Export configuration settings. Changes will trigger a full resync:" }, new Option { Key = nameof(context.Config.AquariusServer), Setter = value => context.Config.AquariusServer = value, Getter = () => context.Config.AquariusServer, Description = "AQTS server name" }, new Option { Key = nameof(context.Config.AquariusUsername), Setter = value => context.Config.AquariusUsername = value, Getter = () => context.Config.AquariusUsername, Description = "AQTS username" }, new Option { Key = nameof(context.Config.AquariusPassword), Setter = value => context.Config.AquariusPassword = value, Getter = () => context.Config.AquariusPassword, Description = "AQTS password" }, new Option { Key = nameof(context.Config.SosServer), Setter = value => context.Config.SosServer = value, Getter = () => context.Config.SosServer, Description = "SOS server name" }, new Option { Key = nameof(context.Config.SosUsername), Setter = value => context.Config.SosUsername = value, Getter = () => context.Config.SosUsername, Description = "SOS username" }, new Option { Key = nameof(context.Config.SosPassword), Setter = value => context.Config.SosPassword = value, Getter = () => context.Config.SosPassword, Description = "SOS password" }, new Option(), new Option { Description = "/Publish/v2/GetTimeSeriesUniqueIdList settings. Changes will trigger a full resync:" }, new Option { Key = nameof(context.Config.LocationIdentifier), Setter = value => context.Config.LocationIdentifier = value, Getter = () => context.Config.LocationIdentifier, Description = "Optional location filter." }, new Option { Key = nameof(context.Config.Publish), Setter = value => context.Config.Publish = string.IsNullOrEmpty(value) ? (bool?)null : bool.Parse(value), Getter = () => context.Config.Publish?.ToString(), Description = "Optional publish filter." }, new Option { Key = nameof(context.Config.ChangeEventType), Setter = value => context.Config.ChangeEventType = (ChangeEventType)Enum.Parse(typeof(ChangeEventType), value, true), Getter = () => context.Config.ChangeEventType?.ToString(), Description = $"Optional change event type filter. One of {string.Join(", ", Enum.GetNames(typeof(ChangeEventType)))}" }, new Option { Key = nameof(context.Config.Parameter), Setter = value => context.Config.Parameter = value, Getter = () => context.Config.Parameter, Description = "Optional parameter filter." }, new Option { Key = nameof(context.Config.ComputationIdentifier), Setter = value => context.Config.ComputationIdentifier = value, Getter = () => context.Config.ComputationIdentifier, Description = "Optional computation filter." }, new Option { Key = nameof(context.Config.ComputationPeriodIdentifier), Setter = value => context.Config.ComputationPeriodIdentifier = value, Getter = () => context.Config.ComputationPeriodIdentifier, Description = "Optional computation period filter." }, new Option { Key = nameof(context.Config.ExtendedFilters), Setter = value => { var split = value.Split('='); if (split.Length != 2) { throw new ExpectedException($"Can't parse '{value}' as Name=Value extended attribute filter"); } context.Config.ExtendedFilters.Add(new ExtendedAttributeFilter { FilterName = split[0], FilterValue = split[1] }); }, Getter = () => string.Empty, Description = "Extended attribute filter in Name=Value format. Can be set multiple times." }, new Option(), new Option { Description = "Aggressive time-series filtering. Changes will trigger a full resync:" }, new Option { Key = nameof(context.Config.TimeSeries), Setter = value => context.Config.TimeSeries.Add(ParseTimeSeriesFilter(value)), Getter = () => string.Empty, Description = "Time-series identifier regular expression filter. Can be specified multiple times." }, new Option { Key = "TimeSeriesDescription", Setter = value => context.Config.TimeSeriesDescriptions.Add(ParseTimeSeriesFilter(value)), Getter = () => string.Empty, Description = "Time-series description regular expression filter. Can be specified multiple times." }, new Option { Key = nameof(context.Config.Approvals), Setter = value => context.Config.Approvals.Add(ParseApprovalFilter(value)), Getter = () => string.Empty, Description = "Filter points by approval level or name. Can be specified multiple times." }, new Option { Key = nameof(context.Config.Grades), Setter = value => context.Config.Grades.Add(ParseGradeFilter(value)), Getter = () => string.Empty, Description = "Filter points by grade code or name. Can be specified multiple times." }, new Option { Key = nameof(context.Config.Qualifiers), Setter = value => context.Config.Qualifiers.Add(ParseQualifierFilter(value)), Getter = () => string.Empty, Description = "Filter points by qualifier. Can be specified multiple times." }, new Option(), new Option { Description = "Maximum time range of points to upload: Changes will trigger a full resync:" }, new Option { Key = nameof(context.Config.MaximumPointDays), Setter = value => { var components = value.Split('='); if (components.Length == 2) { var periodText = components[0]; var daysText = components[1]; var period = (ComputationPeriod)Enum.Parse(typeof(ComputationPeriod), periodText, true); var days = daysText.Equals("All", StringComparison.InvariantCultureIgnoreCase) ? -1 : int.Parse(daysText); context.Config.MaximumPointDays[period] = days; } }, Getter = () => "\n " + string.Join("\n ", context.Config.MaximumPointDays.Select(kvp => { var daysText = kvp.Value > 0 ? kvp.Value.ToString() : "All"; return($"{kvp.Key,-8} = {daysText}"); })) + "\n ", Description = "Days since the last point to upload, in Frequency=Value format." }, new Option(), new Option { Description = "Other options: (Changing these values won't trigger a full resync)" }, new Option { Key = nameof(context.ConfigurationName), Setter = value => context.ConfigurationName = value, Getter = () => context.ConfigurationName, Description = "The name of the export configuration, to be saved in the AQTS global settings." }, new Option { Key = nameof(context.DryRun), Setter = value => context.DryRun = bool.Parse(value), Getter = () => context.DryRun.ToString(), Description = "When true, don't export to SOS. Only log the changes that would have been performed." }, new Option { Key = nameof(context.ForceResync), Setter = value => context.ForceResync = bool.Parse(value), Getter = () => context.ForceResync.ToString(), Description = "When true, force a full resync of all time-series." }, new Option { Key = nameof(context.NeverResync), Setter = value => context.NeverResync = bool.Parse(value), Getter = () => context.NeverResync.ToString(), Description = "When true, avoid full time-series resync, even when the algorithm recommends it." }, new Option { Key = nameof(context.ChangesSince), Setter = value => context.ChangesSince = DateTimeOffset.Parse(value), Getter = () => string.Empty, Description = "The starting changes-since time in ISO 8601 format. Defaults to the saved AQTS global setting value." }, new Option { Key = nameof(context.ApplyRounding), Setter = value => context.ApplyRounding = bool.Parse(value), Getter = () => context.ApplyRounding.ToString(), Description = "When true, export the rounded point values." }, new Option { Key = nameof(context.MaximumPointsPerObservation), Setter = value => context.MaximumPointsPerObservation = int.Parse(value), Getter = () => context.MaximumPointsPerObservation.ToString(), Description = "The maximum number of points per SOS observation" }, new Option { Key = nameof(context.MaximumExportDuration), Setter = value => context.MaximumExportDuration = TimeSpan.Parse(value, CultureInfo.InvariantCulture), Getter = () => context.MaximumExportDuration?.Humanize(2), Description = "The maximum duration before polling AQTS for more changes, in hh:mm:ss format. Defaults to the AQTS global setting." }, new Option { Key = nameof(context.Timeout), Setter = value => context.Timeout = TimeSpan.Parse(value, CultureInfo.InvariantCulture), Getter = () => context.Timeout.Humanize(2), Description = "The timeout used for all web requests, in hh:mm:ss format." } }; var usageMessage = $"Export time-series changes in AQTS time-series to an OGC SOS server." + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] ..." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n {string.Join("\n ", options.Select(o => o.UsageText()))}" + $"\n" + $"\nISO 8601 timestamps use a yyyy'-'mm'-'dd'T'HH':'mm':'ss'.'fffffffzzz format." + $"\n" + $"\n The 7 fractional seconds digits are optional." + $"\n The zzz timezone can be 'Z' for UTC, or +HH:MM, or -HH:MM" + $"\n" + $"\n Eg: 2017-04-01T00:00:00Z represents April 1st, 2017 in UTC." + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace are ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help page\n\n{usageMessage}"); } throw new ExpectedException($"Unknown command line argument: {arg}"); } var key = match.Groups["key"].Value.ToLower(); var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key != null && o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option == null) { throw new ExpectedException($"Unknown -option=value: {arg}\n\n{usageMessage}"); } option.Setter(value); } if (string.IsNullOrWhiteSpace(context.Config.AquariusServer) || string.IsNullOrEmpty(context.Config.AquariusUsername) || string.IsNullOrEmpty(context.Config.AquariusPassword)) { throw new ExpectedException($"Ensure your AQTS server credentials are set."); } if (string.IsNullOrWhiteSpace(context.Config.SosServer) || string.IsNullOrEmpty(context.Config.SosUsername) || string.IsNullOrEmpty(context.Config.SosPassword)) { throw new ExpectedException($"Ensure your SOS server credentials are set."); } return(context); }
private void ParseArgs(string[] args) { var resolvedArgs = args .SelectMany(ResolveOptionsFromFile) .ToArray(); var options = new[] { new Option { Key = nameof(Context.Server), Setter = value => Context.Server = value, Getter = () => Context.Server, Description = "The AQTS app server from which time-series data will be retrieved." }, new Option { Key = nameof(Context.Username), Setter = value => Context.Username = value, Getter = () => Context.Username, Description = "AQTS username." }, new Option { Key = nameof(Context.Password), Setter = value => Context.Password = value, Getter = () => Context.Password, Description = "AQTS credentials." }, new Option { Key = nameof(Context.TemplatePath), Setter = value => Context.TemplatePath = value, Getter = () => Context.TemplatePath, Description = "Path of the SharpShooter Report template file (*.RST)" }, new Option { Key = nameof(Context.OutputPath), Setter = value => Context.OutputPath = value, Getter = () => Context.OutputPath, Description = "Path to the generated report output. Only PDF output is supported." }, new Option { Key = nameof(Context.LaunchReportDesigner), Setter = value => Context.LaunchReportDesigner = bool.Parse(value), Getter = () => Context.LaunchReportDesigner.ToString(), Description = "When true, launch the SharpShooter Report Designer." }, new Option { Key = "TimeSeries", Setter = value => Context.TimeSeries.Add(ParseTimeSeries(value)), Getter = () => string.Empty, Description = "Load the specified time-series as a dataset." }, new Option { Key = "RatingModel", Setter = value => Context.RatingModels.Add(ParseRatingModel(value)), Getter = () => string.Empty, Description = "Load the specified rating-model as a dataset." }, new Option { Key = "ExternalDataSet", Setter = value => Context.ExternalDataSets.Add(ParseExternalDataSet(value)), Getter = () => string.Empty, Description = "Load the external DataSet XML file." }, new Option { Key = nameof(Context.UploadedReportLocation), Setter = value => Context.UploadedReportLocation = value, Getter = () => Context.UploadedReportLocation, Description = "Upload the generated report to this AQTS location identifier." }, new Option { Key = nameof(Context.UploadedReportTitle), Setter = value => Context.UploadedReportTitle = value, Getter = () => Context.UploadedReportTitle, Description = "Upload the generated report with this title. Defaults to the -OutputPath value." }, }; var usageMessage = $"Run a SharpShooter Reports template with AQTS data." + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] ..." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n -{string.Join("\n -", options.Select(o => o.UsageText()))}" + $"\n" + $"\nRetrieving time-series data from AQTS: (more than one -TimeSeries=value option can be specified)" + $"\n" + $"\n -TimeSeries=identifierOrUniqueId[,From=date][,To=date][,Unit=outputUnit][,GroupBy=option]" + $"\n" + $"\n =identifierOrUniqueId - Use either the uniqueId or the <parameter>.<label>@<location> syntax." + $"\n ,From=date - Retrieve data from this date. [default: Beginning of record]" + $"\n ,To=date - Retrieve data until this date. [default; End of record]" + $"\n ,Unit=outputUnit - Convert the values to the unit. [default: The default unit of the time-series]" + $"\n ,GroupBy=option - Groups data by {string.Join("|", Enum.GetNames(typeof(GroupBy)))} [default: Year]" + $"\n" + $"\n Dates specified as yyyy-MM-ddThh:mm:ss.fff. Only the year component is required." + $"\n" + $"\nRetrieving rating model info from AQTS: (more than one -RatingModel=value option can be specified)" + $"\n" + $"\n -RatingModel=identifierOrUniqueId[,From=date][,To=date][,Unit=outputUnit][,GroupBy=option]" + $"\n" + $"\n =identifierOrUniqueId - Use either the uniqueId or the <InputParameter>-<OutputParameter>.<label>@<location> syntax." + $"\n ,From=date - Retrieve data from this date. [default: Beginning of record]" + $"\n ,To=date - Retrieve data until this date. [default: End of record]" + $"\n ,StepSize=increment - Set the expanded table step size. [default: 0.1]" + $"\n" + $"\n Dates specified as yyyy-MM-ddThh:mm:ss.fff. Only the year component is required." + $"\n" + $"\nUsing external data sets: (more than one -ExternalDataSet=value option can be specified)" + $"\n" + $"\n -ExternalDataSet=pathToXml[,Name=datasetName]" + $"\n" + $"\n =pathToXml - A standard .NET DataSet, serialized to XML." + $"\n ,Name=datasetName - Override the name of the dataset. [default: The name stored within the XML]" + $"\n" + $"\nUnknown -name=value options will be merged with the appropriate data set and table." + $"\n" + $"\n Simple -name=value options like -MySetting=MyValue will be added to the Common.CommandLineParameters table." + $"\n" + $"\n Dotted -name=value options like -ReportParameters.Parameters.Description=MyValue will be merged into the named dataset.table.column." + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace is ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help.\n\n{usageMessage}"); } throw new ExpectedException($"Unknown argument '{arg}'."); } var key = match.Groups["key"].Value.ToLower(); var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option == null) { AddReportParameter(match.Groups["key"].Value, value); continue; } option.Setter(value); } }
private static void ParseArgsIntoContext(Context context, string[] resolvedArgs) { var options = new[] { new Option { Key = nameof(context.Files), Description = "Parse the NEM12 file. Can be set multiple times.", Setter = value => context.Files.Add(value), Getter = () => string.Empty, }, }; var usageMessage = $"Converts the NEM12 CSV file into a single CSV row per point." + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] NEM12File1 NEM12File2 ..." + $"\n" + $"\nIf no NEM12 CSV file is specified, the standard input will be used." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n {string.Join("\n ", options.Select(o => o.UsageText()))}" + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace are ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help page\n\n{usageMessage}"); } if (File.Exists(arg)) { context.Files.Add(arg); continue; } throw new ExpectedException($"Unknown command line argument: {arg}"); } var key = match.Groups["key"].Value.ToLower(); var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key != null && o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option == null) { throw new ExpectedException($"Unknown -option=value: {arg}\n\n{usageMessage}"); } option.Setter(value); } }
private static Context ParseArgs(string[] args) { var context = new Context(); var resolvedArgs = args .SelectMany(ResolveOptionsFromFile) .ToArray(); var options = new[] { new Option { Key = nameof(context.Server), Setter = value => context.Server = value, Getter = () => context.Server, Description = "AQTS server name" }, new Option { Key = nameof(context.Username), Setter = value => context.Username = value, Getter = () => context.Username, Description = "AQTS username" }, new Option { Key = nameof(context.Password), Setter = value => context.Password = value, Getter = () => context.Password, Description = "AQTS password" }, new Option { Key = nameof(context.SessionToken), Setter = value => context.SessionToken = value, Getter = () => context.SessionToken, }, new Option { Key = nameof(context.LocationIdentifier), Setter = value => context.LocationIdentifier = value, Getter = () => context.LocationIdentifier, Description = "Optional location filter." }, new Option { Key = nameof(context.Publish), Setter = value => context.Publish = bool.Parse(value), Getter = () => context.Publish?.ToString(), Description = "Optional publish filter." }, new Option { Key = nameof(context.ChangeEventType), Setter = value => context.ChangeEventType = (ChangeEventType)Enum.Parse(typeof(ChangeEventType), value, true), Getter = () => context.ChangeEventType?.ToString(), Description = $"Optional change event type filter. One of {string.Join(", ", Enum.GetNames(typeof(ChangeEventType)))}" }, new Option { Key = nameof(context.Parameter), Setter = value => context.Parameter = value, Getter = () => context.Parameter, Description = "Optional parameter filter." }, new Option { Key = nameof(context.ComputationIdentifier), Setter = value => context.ComputationIdentifier = value, Getter = () => context.ComputationIdentifier, Description = "Optional computation filter." }, new Option { Key = nameof(context.ComputationPeriodIdentifier), Setter = value => context.ComputationPeriodIdentifier = value, Getter = () => context.ComputationPeriodIdentifier, Description = "Optional computation period filter." }, new Option { Key = nameof(context.ExtendedFilters), Setter = value => { var split = value.Split('='); if (split.Length != 2) { throw new ExpectedException($"Can't parse '{value}' as Name=Value extended attribute filter"); } context.ExtendedFilters.Add(new ExtendedAttributeFilter { FilterName = split[0], FilterValue = split[1] }); }, Getter = () => string.Empty, Description = "Optional extended attribute filter in Name=Value format. Can be set multiple times." }, new Option { Key = nameof(context.TimeSeries), Setter = value => context.TimeSeries.Add(value), Getter = () => string.Empty, Description = "Optional time-series to monitor. Can be set multiple times." }, new Option { Key = nameof(context.ChangesSinceTime), Setter = value => context.ChangesSinceTime = InstantPattern.ExtendedIsoPattern.Parse(value).GetValueOrThrow(), Getter = () => string.Empty, Description = "The starting changes-since time in ISO 8601 format. Defaults to 'right now'" }, new Option { Key = nameof(context.PollInterval), Setter = value => context.PollInterval = value.ToUpperInvariant().ParseDuration(), Getter = () => context.PollInterval.SerializeToString(), Description = "The polling interval in ISO 8601 Duration format." }, new Option { Key = nameof(context.MaximumChangeCount), Setter = value => context.MaximumChangeCount = int.Parse(value), Getter = () => context.MaximumChangeCount.ToString(), Description = "When greater than 0, exit after detecting this many changed time-series." }, new Option { Key = nameof(context.AllowQuickPolling), Setter = value => context.AllowQuickPolling = bool.Parse(value), Getter = () => context.AllowQuickPolling.ToString(), Description = "Allows very quick polling. Good for testing, bad for production." }, new Option { Key = nameof(context.SavedChangesSinceJson), Setter = value => context.SavedChangesSinceJson = value, Getter = () => context.SavedChangesSinceJson, Description = $"Loads the /{nameof(context.ChangesSinceTime)} value from this JSON file." }, new Option { Key = nameof(context.DetectedChangesCsv), Setter = value => context.DetectedChangesCsv = value, Getter = () => context.DetectedChangesCsv, Description = $"When set, save all detected changes to this CSV file and exit." }, }; var usageMessage = $"Monitor time-series changes in an AQTS time-series." + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] [location] [timeSeriesIdentifierOrGuid] ..." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n -{string.Join("\n -", options.Select(o => o.UsageText()))}" + $"\n" + $"\nISO 8601 timestamps use a yyyy'-'mm'-'dd'T'HH':'mm':'ss'.'fffffffzzz format." + $"\n" + $"\n The 7 fractional seconds digits are optional." + $"\n The zzz timezone can be 'Z' for UTC, or +HH:MM, or -HH:MM" + $"\n" + $"\n Eg: 2017-04-01T00:00:00Z represents April 1st, 2017 in UTC." + $"\n" + $"\nISO 8601 durations use a 'PT'[nnH][nnM][nnS] format." + $"\n" + $"\n Only the required components are needed." + $"\n" + $"\n Eg: PT5M represents 5 minutes." + $"\n PT90S represents 90 seconds (1.5 minutes)" + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace is ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help page\n\n{usageMessage}"); } // Try positional arguments: [locationIdentifier] [timeSeriesIdentifier] if (Guid.TryParse(arg, out var _)) { context.TimeSeries.Add(arg); continue; } if (TimeSeriesIdentifier.TryParse(arg, out var _)) { context.TimeSeries.Add(arg); continue; } if (arg.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase)) { context.SavedChangesSinceJson = arg; continue; } if (arg.EndsWith(".csv", StringComparison.InvariantCultureIgnoreCase)) { context.DetectedChangesCsv = arg; continue; } context.LocationIdentifier = arg; continue; } var key = match.Groups["key"].Value.ToLower(); var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option == null) { throw new ExpectedException($"Unknown -option=value: {arg}\n\n{usageMessage}"); } option.Setter(value); } if (string.IsNullOrWhiteSpace(context.Server)) { throw new ExpectedException($"A /{nameof(context.Server)} option is required.\n\n{usageMessage}"); } return(context); }
private void ParseArgs(string[] args) { var resolvedArgs = args .SelectMany(ResolveOptionsFromFile) .ToArray(); var options = new[] { new Option { Description = "AQUARIUS Time-Series connection options:" }, new Option { Key = nameof(Context.Server), Setter = value => Context.Server = value, Getter = () => Context.Server , Description = "The AQTS app server from which time-series data will be retrieved." }, new Option { Key = nameof(Context.Username), Setter = value => Context.Username = value, Getter = () => Context.Username, Description = "AQTS username." }, new Option { Key = nameof(Context.Password), Setter = value => Context.Password = value, Getter = () => Context.Password, Description = "AQTS credentials." }, new Option(), new Option { Description = "SharpShooter Reports options:" }, new Option { Key = nameof(Context.TemplatePath), Setter = value => Context.TemplatePath = value, Getter = () => Context.TemplatePath, Description = "Path of the SharpShooter Reports template (*.RST) or Aquarius Report template (*.ART) file." }, new Option { Key = nameof(Context.OutputPath), Setter = value => Context.OutputPath = value, Getter = () => Context.OutputPath, Description = "Path to the generated report output. Only PDF output is supported." }, new Option { Key = nameof(Context.LaunchReportDesigner), Setter = value => Context.LaunchReportDesigner = bool.Parse(value), Getter = () => Context.LaunchReportDesigner.ToString(), Description = "When true, launch the SharpShooter Report Designer." }, new Option(), new Option { Description = "Dataset options:" }, new Option { Key = nameof(Context.QueryFrom), Setter = value => Context.QueryFrom = value, Getter = () => Context.QueryFrom, Description = "The starting point for all time-series. Can be overriden by individual series. [default: Beginning of record]" }, new Option { Key = nameof(Context.QueryTo), Setter = value => Context.QueryTo = value, Getter = () => Context.QueryTo, Description = "The ending point for all time-series. Can be overriden by individual series. [default: End of record]" }, new Option { Key = nameof(Context.GroupBy), Setter = value => Context.GroupBy = ParseEnum <GroupBy>(value), Getter = () => $"{Context.GroupBy}", Description = $"The grouping for all time-series. One of {string.Join(", ", Enum.GetNames(typeof(GroupBy)))}. Can be overriden by individual series." }, new Option { Key = "TimeSeries", Setter = value => Context.TimeSeries.Add(ParseTimeSeries(value)), Getter = () => string.Empty, Description = "Load the specified time-series as a dataset." }, new Option { Key = "RatingModel", Setter = value => Context.RatingModels.Add(ParseRatingModel(value)), Getter = () => string.Empty, Description = "Load the specified rating-model as a dataset." }, new Option { Key = "ExternalDataSet", Setter = value => Context.ExternalDataSets.Add(ParseExternalDataSet(value)), Getter = () => string.Empty, Description = "Load the external DataSet XML file." }, new Option(), new Option { Description = "Report uploading options:" }, new Option { Key = nameof(Context.UploadedReportLocation), Setter = value => Context.UploadedReportLocation = value, Getter = () => Context.UploadedReportLocation, Description = "Upload the generated report to this AQTS location identifier. If empty, no report will be uploaded." }, new Option { Key = nameof(Context.UploadedReportTitle), Setter = value => Context.UploadedReportTitle = value, Getter = () => Context.UploadedReportTitle, Description = $"Upload the generated report with this title. Defaults to the -{nameof(Context.OutputPath)} base filename." }, }; var usageMessage = $"Run a SharpShooter Reports template with AQTS data." + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] ..." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n {string.Join("\n ", options.Select(o => o.UsageText()))}" + $"\n" + $"\nRetrieving time-series data from AQTS: (more than one -TimeSeries=value option can be specified)" + $"\n" + $"\n -TimeSeries=identifierOrUniqueId[,From=date][,To=date][,Unit=outputUnit][,GroupBy=option][,DataSetName=dataSetName]" + $"\n" + $"\n =identifierOrUniqueId - Use either the uniqueId or the <parameter>.<label>@<location> syntax." + $"\n ,From=date - Retrieve data from this date. [default: Beginning of record]" + $"\n ,To=date - Retrieve data until this date. [default: End of record]" + $"\n ,Unit=outputUnit - Convert the values to the unit. [default: The default unit of the time-series]" + $"\n ,GroupBy=option - Groups data by {string.Join("|", Enum.GetNames(typeof(GroupBy)))} [default: {Context.GroupBy}]" + $"\n ,DataSetName=datasetName - Override the name of the dataset. [default: 'TimeSeries#' where # is the 1-based index of the time-series]" + $"\n" + $"\n Use the -TimeSeries4=... or -TimeSeries[4]=... syntax to force the dataset name to a specific index." + $"\n" + $"\n Dates specified as yyyy-MM-ddThh:mm:ss.fff. Only the year component is required." + $"\n" + $"\nRetrieving rating model info from AQTS: (more than one -RatingModel=value option can be specified)" + $"\n" + $"\n -RatingModel=identifierOrUniqueId[,From=date][,To=date][,Unit=outputUnit][,GroupBy=option][,DataSetName=dataSetName]" + $"\n" + $"\n =identifierOrUniqueId - Use either the uniqueId or the <InputParameter>-<OutputParameter>.<label>@<location> syntax." + $"\n ,From=date - Retrieve data from this date. [default: Beginning of record]" + $"\n ,To=date - Retrieve data until this date. [default: End of record]" + $"\n ,StepSize=increment - Set the expanded table step size. [default: 0.1]" + $"\n ,DataSetName=datasetName - Override the name of the dataset. [default: 'RatingCurve#' where # is the 1-based index of the rating model]" + $"\n" + $"\n Use the -RatingModel4=... or -RatingModel[4]=... syntax to force the dataset name to a specific index." + $"\n" + $"\n Dates specified as yyyy-MM-ddThh:mm:ss.fff. Only the year component is required." + $"\n" + $"\nUsing external data sets: (more than one -ExternalDataSet=value option can be specified)" + $"\n" + $"\n -ExternalDataSet=pathToXml[,DataSetName=dataSetName]" + $"\n" + $"\n =pathToXml - A standard .NET DataSet, serialized to XML." + $"\n ,DataSetName=datasetName - Override the name of the dataset. [default: The name stored within the XML]" + $"\n" + $"\nUnknown -name=value options will be merged with the appropriate data set and table." + $"\n" + $"\n Simple -name=value options like -MySetting=MyValue will be added to the Common.CommandLineParameters table." + $"\n" + $"\n Dotted -name=value options like -ReportParameters.Parameters.Description=MyValue will be merged into the named dataset.table.column." + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace is ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help.\n\n{usageMessage}"); } throw new ExpectedException($"Unknown argument '{arg}'."); } var key = match.Groups["key"].Value; var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key != null && o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option != null) { option.Setter(value); continue; } match = IndexedDataSetRegex.Match(key); if (!match.Success) { AddReportParameter(key, value); continue; } var category = match.Groups["category"].Value; var index = int.Parse(match.Groups["index"].Value); if (index <= 0) { throw new ExpectedException($"'{arg}' has an index of {index}. A value greater than zero is required."); } if (category.StartsWith(nameof(Context.TimeSeries), StringComparison.InvariantCultureIgnoreCase)) { var timeSeries = ParseTimeSeries(value); ForceDataSetName(timeSeries, $"TimeSeries{index}"); Context.TimeSeries.Add(timeSeries); } else { var ratingModel = ParseRatingModel(value); ForceDataSetName(ratingModel, $"RatingCurve{index}"); Context.RatingModels.Add(ratingModel); } } }
private static void ParseArgsIntoContext(Context context, string[] resolvedArgs) { var options = new[] { new Option { Description = "https://waterwatch.io credentials" }, new Option { Key = nameof(context.WaterWatchOrgId), Description = "WaterWatch.io organisation Id", Setter = value => context.WaterWatchOrgId = value, Getter = () => context.WaterWatchOrgId, }, new Option { Key = nameof(context.WaterWatchApiKey), Description = "WaterWatch.io API key", Setter = value => context.WaterWatchApiKey = value, Getter = () => context.WaterWatchApiKey, }, new Option { Key = nameof(context.WaterWatchApiToken), Description = "WaterWatch.io API token", Setter = value => context.WaterWatchApiToken = value, Getter = () => context.WaterWatchApiToken, }, new Option(), new Option { Description = "Configuration options" }, new Option { Key = nameof(context.OutputMode), Description = $"Measurement value output mode. One of {string.Join(", ", Enum.GetNames(typeof(OutputMode)))}.", Setter = value => context.OutputMode = (OutputMode)Enum.Parse(typeof(OutputMode), value, true), Getter = () => context.OutputMode.ToString(), }, new Option { Key = nameof(context.OutputDivisor), Description = "Divisor applied to all output values.", Setter = value => context.OutputDivisor = double.Parse(value), Getter = () => context.OutputDivisor.ToString("G"), }, new Option { Key = nameof(context.SaveStatePath), Description = "Path to persisted state file", Setter = value => context.SaveStatePath = value, Getter = () => context.SaveStatePath, }, new Option { Key = nameof(context.SyncFromUtc), Description = "Optional UTC sync time. [default: last known sensor time]", Setter = value => context.SyncFromUtc = ParseDateTime(value), }, new Option { Key = nameof(context.NewSensorSyncDays), Description = "Number of days to sync data when a new sensor is detected.", Setter = value => context.NewSensorSyncDays = int.Parse(value), Getter = () => context.NewSensorSyncDays.ToString(), }, new Option(), new Option { Description = "Sensor filtering options" }, new Option { Key = "SensorName", Description = "Sensor name regular expression filter. Can be specified multiple times.", Setter = value => context.SensorNameFilters.Add(ParseRegexFilter(value)) }, new Option { Key = "SensorSerial", Description = "Sensor serial number regular expression filter. Can be specified multiple times.", Setter = value => context.SensorSerialFilters.Add(ParseRegexFilter(value)) }, }; var usageMessage = $"Extract the latest sensor readings from a https://waterwatch.io account" + $"\n" + $"\nusage: {GetProgramName()} [-option=value] [@optionsFile] ..." + $"\n" + $"\nSupported -option=value settings (/option=value works too):\n\n {string.Join("\n ", options.Select(o => o.UsageText()))}" + $"\n" + $"\nSupported /{nameof(context.SyncFromUtc)} date formats:" + $"\n" + $"\n {string.Join("\n ", SupportedDateFormats)}" + $"\n" + $"\nUse the @optionsFile syntax to read more options from a file." + $"\n" + $"\n Each line in the file is treated as a command line option." + $"\n Blank lines and leading/trailing whitespace are ignored." + $"\n Comment lines begin with a # or // marker." ; foreach (var arg in resolvedArgs) { var match = ArgRegex.Match(arg); if (!match.Success) { if (HelpKeywords.Contains(arg)) { throw new ExpectedException($"Showing help page\n\n{usageMessage}"); } if (File.Exists(arg)) { // This is the magic which allows the preprocessor to be used in AQUARIUS DAS and EnviroSCADA 2018.1-or-earlier. // Those products require that a preprocessor has one and only one argument, which is a "script file". // This recursive call interprets any existing file as an @options.txt argument list. // This is not necessary for EnviroSCADA 2019.1+ or Connect, since both of those allow arbitrary preprocessor command line arguments. ParseArgsIntoContext(context, LoadArgsFromFile(arg).ToArray()); continue; } throw new ExpectedException($"Unknown command line argument: {arg}"); } var key = match.Groups["key"].Value.ToLower(); var value = match.Groups["value"].Value; var option = options.FirstOrDefault(o => o.Key != null && o.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); if (option == null) { throw new ExpectedException($"Unknown -option=value: {arg}\n\n{usageMessage}"); } option.Setter(value); } }