C#: How do I add a response to "/?" in my program - c#

Basically, I know that some apps when called in command line with "/?" spit back a formatted list of how to call the app with params from the command line. Also, these apps sometimes even popup a box alerting the user that the program can only be run with certain params passed in and give this detailed formatted params (similar to the command prompt output).
How do they do this (The /? is more important for me than the popup box)?

The Main method takes string[] parameter with the command line args.
You can also call the Environment.GetCommandLineArgs method.
You can then check whether the array contains "/?".

Try looking at NDesk.Options. It's a single source file embeddable C# library that provides argument parsing. You can parse your arguments quickly:
public static void Main(string[] args)
{
string data = null;
bool help = false;
int verbose = 0;
var p = new OptionSet () {
{ "file=", "The {FILE} to work on", v => data = v },
{ "v|verbose", "Prints out extra status messages", v => { ++verbose } },
{ "h|?|help", "Show this message and exit", v => help = v != null },
};
List<string> extra = p.Parse(args);
}
It can write out the help screen in a professional looking format easily as well:
if (help)
{
Console.WriteLine("Usage: {0} [OPTIONS]", EXECUTABLE_NAME);
Console.WriteLine("This is a sample program.");
Console.WriteLine();
Console.WriteLine("Options:");
p.WriteOptionDescriptions(Console.Out);
}
This gives output like so:
C:\>program.exe /?
Usage: program [OPTIONS]
This is a sample program.
Options:
-file, --file=FILE The FILE to work on
-v, -verbose Prints out extra status messages
-h, -?, --help Show this message and exit

Related

How to get separate arguments from single arguments string like ProcessStartInfo.Arguments?

Let's say I have a shell command line. A single line of text, exactly as you would type that in shell. Splitting command from arguments is trivial. It's always a space after the command, in every shell in the world. I know, the command theoretically can contain spaces, but let's assume it's a normal command for a while.
When I just pass the arguments as string to the ProcessStartInfo.Arguments string - it will work. But this is not what I want.
I want the arguments to be separated.
Each one. When an argument is like #"C:\Program Files" - it should be a one argument.
Getting that manually is just tricky as (s)hell. Because it's done differently for different OSes and shells. However - .NET does it somehow internally and I need to get to it not to reinvent the wheel.
What I REALLY want to do is to build an argument list first like this:
new[] { "dir", #"'C:\Program Files'" }
And then like this:
new[] { "cmd", "/C", #"dir 'C:\Program Files'" }
So it goes both ways - I extract arguments from a string, then I build an arguments string from separate arguments. All according for the OS-specific rules. My code should work on both Linux and Windows.
The catch is, it has to work multi-platform, so instead of "cmd /C" it could be "bash -c" on Linux, you get the idea.
The main point is to make the .NET to do all the quoting / unqouting itself, and AFAIK it does it properly on both Windows and Linux.
I really did search the Google and Stack Overflow for this - it seems like there's nothing.
I tried to get ArgumentList from ProcessStartInfo after settings Arguments property. It doesn't work. Setting one property doesn't set the other one. You can actually set both and get the exception when trying to start the process.
I bet the problem is far from being trivial. Any suggestions?
UPDATE
I've done more research based on digging in .NET sources and GitHub discussions. It looks really bad (or really good depending on a point of view). It seems like there's nothing like this. So I basically need one method to quote one argument for the target OS, and one method to parse a command line into unquoted parts. For parsing I'll go standard, using FSM (finite state machine algorithm), quoting is trivial, it's just adding OS / shell specific quote symbols.
Anyway, if it's somewhere in .NET just hiding and giggling, please let me know ;)
So it seems like it was not there yet, so I made a little braindead simple hack for it:
Pre-requisite:
public class SpaceDelimitedStringParser {
public char Quote { get; set; } = '"';
public IEnumerable<string> Split(string input) {
bool isQuoting = false;
for (int i = 0, s = 0, n = input.Length; i < n; i++) {
var c = input[i];
var isWhiteSpace = c is ' ' or '\t';
var isQuote = c == Quote;
var isBreak = i == n - 1 || isWhiteSpace && !isQuoting;
if (isBreak) {
yield return input[s..i];
s = i + 1;
continue;
}
if (isWhiteSpace && !isQuoting) continue;
if (isQuote) {
if (!isQuoting) {
s = i + 1;
isQuoting = true;
}
else {
yield return input[s..i];
s = i + 1;
isQuoting = false;
}
}
}
}
public string Join(IEnumerable<string> items)
=> String.Join(' ', items.Select(i => i.Contains(' ') || i.Contains('\t') ? (Quote + i + Quote) : i));
}
It just splits and joins space delimited strings using a quoting character: double quoute. I tested that is treated similarily on Windows and Linux.
Then another nice helper class:
public class ShellStartInfo {
public ShellStartInfo(string command, bool direct = false) {
var (shell, exec) =
OS.IsLinux ? ("bash", "-c") :
OS.IsWindows ? ("cmd", "/C") :
throw new PlatformNotSupportedException();
var arguments = new[] { exec, command };
StartInfo = new ProcessStartInfo(shell, new SpaceDelimitedStringParser().Join(arguments));
if (!direct) {
StartInfo.RedirectStandardInput = true;
StartInfo.RedirectStandardOutput = true;
StartInfo.RedirectStandardError = true;
StartInfo.StandardOutputEncoding = Encoding.UTF8;
StartInfo.StandardErrorEncoding = Encoding.UTF8;
}
}
public static implicit operator ProcessStartInfo(ShellStartInfo startInfo) => startInfo.StartInfo;
private readonly ProcessStartInfo StartInfo;
}
And another little helper:
public static class OS {
public static bool IsLinux => _IsLinux ?? (_IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)).Value;
public static bool IsWindows => _IsWindows ?? (_IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)).Value;
private static bool? _IsLinux;
private static bool? _IsWindows;
}
And we can test it:
var command =
OS.IsLinux ? #"cat 'File name with spaces.txt'" :
OS.IsWindows ? #"type ""File name with spaces.txt""" :
throw new PlatformNotSupportedException();
var process = Process.Start(new ShellStartInfo(command));
if (process is null) throw new InvalidOperationException();
await process.WaitForExitAsync();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
if (output.Length > 0) Console.WriteLine($"OUTPUT:\n{output}\nEND.");
if (error.Length > 0) Console.WriteLine($"ERROR:\n{error}\nEND.");
Works on Windows, works on Linux. Done and done. The split function is unnecessary here. I thought it can be useful to pass arguments as array somewhere, but well.
Passing arguments through ArgumentList property of ProcessStartInfo class is a little broken. It just doesn't work as inteneded, especially when I pass the arguments as one shell execute argument. Why? IDK, works when passed like in the example.
BTW, my ShellStartInfo class also sets some other properties of internal ProcessStartInfo that makes it useful to read the command's output.
In my remote shell it just works, the local terminal behaves as the remote one. It's not full blown Putty clone, it's just a tiny tool to have a little piece of "ssh" to a client device that doesn't even have external IP, open SSH port or may even not accept TCP connections. In fact - the client is a small IoT device that runs Linux.

C# passing array to array but still getting "cannot convert from 'string[]' to 'string'” error

(using VS Community 2019 v16.10.4 on Win 10 Pro 202H)
I'm working on a C# console/winforms desktop app which monitors some local drive properties and displays a status message. Status message display is executed via Task Scheduler (the task is programmatically defined and registered). The UI is executed from the console app using the Process method. My intention is to pass an argument from Scheduler to the console app, perform some conditional logic and then pass the result to the UI entry (Program.cs) for further processing.
I’m testing passing an argument from the console app to the UI entry point and I’m getting a “Argument 1: cannot convert from 'string[]' to 'string'” error.
Code is:
class Program_Console
{
public static void Main(string[] tsArgs)
{
// based on MSDN example
tsArgs = new string[] { "Test Pass0", "TP1", "TP2" };
Process p = new Process();
try
{
p.StartInfo.FileName = BURS_Dir;
p.Start(tsArgs); // error here
}
public class Program_UI
{
[STAThread]
public void Main(string[] tsArgs)
{
Isn’t "tsArgs" consistently an array?
EDIT:
For clarity I’m using .NET Framework 4.7.2. The problem was not with consistency of what I am passing but in the Process.Start(String, IEnumerable String) overload. I believed “IEnumerable String” included string[ ]; it obviously does not since I was able to pass a plain string (not a string variable -- that also failed – just a hardcoded string).
In case it’s useful to somebody, my work-around is saving the arguments to a SQLite table in the console app and loading them into a List in the UI app. I’m sure a more proficient programmer could do it more efficiently.
Start doesn' have a costractor with string array. if you look at msdn document youi will see that you can use something the closest to your example
public static Start (string fileName, IEnumerable<string> arguments);
so you can try
p.Start( filename,tsArgs );
and replace filename with yours
The only Start() method taking arguments as an array also needs the filename: Start(). You can't set the Filename via StartInfo and then omit that parameter in the method call.
The following should work for you:
p.Start(BURS_Dir, tsArgs);
In .Net 5.0+, and .Net Core and Standard 2.1+, you can use a ProcessStartInfo for multiple command-line arguments
tsArgs = new string[] { "Test Pass0", "TP1", "TP2" };
Process p = new Process();
try
{
p.StartInfo.FileName = BURS_Dir;
foreach (var arg in tsArgs)
p.StartInfo.ArgumentList.Add(arg);
p.Start();
}
catch
{ //
}
Alternatively, just add them directly
Process p = new Process
{
StartInfo =
{
FileName = BURS_Dir,
ArgumentList = { "Test Pass0", "TP1", "TP2" },
}
};
try
{
p.Start();
}
catch
{ //
}

How to recognize dashed commands from Console user input?

let's say user enters -path D:\TestFolder\Test.txt -output c -formula x+1-20 in console.
That means he wants to process file which is located in that path, output results to console and apply that formula to each number in file.
what is the proper way to recognize commands if they start with dash?
for example I want to build an object from that string:
public UserInput(string input)
{
public string Path { get; set; }
public string OutputParam { get; set; }
public string Formula { get; set; }
}
Depending on your target framework, and depending on the complexity of the input parameters, you may want to consider using a 3rd party package to do the parsing for you. There are plenty to choose from each with their own quirks and best use cases (e.g. one example is Command Line Parser which can work with .NET Standard. If you are using .NET Core, there is a built in System.CommandLine that can do what you need as well). Each of these will have their own particular implementations and specifics on how to use, so showing examples might not be as helpful if you're not interested in parsing complex user input.
If you are specifically simply trying to parse only the string -path <path> -output <file> -formula <formula>, you could simply write a helper function to return an array of values after parsing, rather than creating a class (and having to deal with static and all that in your Main function). If you want to create a custom class to handle things like mapping your mathematical formula to something, or manipulate the data in some way, you should probably refactor the example below.
Helper:
private static string[] ParseInput(string[] input)
{
try
{
var results = new string[3];
// shift over one index based on the position of the arguments
// get the 'path' value
var index = input.FindIndex(x => x == "-path") + 1;
results[0] = input[index];
// get the 'output' value
index = input.FindIndex(x => x == "-output") + 1;
results[1] = input[index];
// get the formula value
index = input.FindIndex(x => x == "-formula") + 1;
results[2] = input[index];
return results;
}
catch { throw new ArgumentException("Input string was not formatted correctly.")
}
You can then use this in the main program:
...
static void Main(string[] args)
{
...
try
{
var results = ParseInput(args);
// do stuff with results[0] through [2]
...
}
catch (ArgumentException ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
return;
}
...
}
Note: In general I would avoid hardcoding stuff like this, because it makes your application nearly impossible to extend as you get new requirements. For example, if you ever decide you want a -formula2 input argument, well now you need to recode your entire helper function, and any dependencies to consume that information. On the flip side, I also do not advise to install large 3rd party packages that "do everything" if you have a very specific requirement that (you assume) will never change. On this one it's really up to your requirements and scope to decide if you need to use a larger solution to solve your problem.

Set Service Start Parameter using Topshelf

I have a service with multiple instances with different parameters for each instance, at the moment I'm setting these parameters manually (in another code to be exact) to Image Path of the service in Registry (e.g. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\MyService$i00). so our service installation is done in two steps.
I'm really interested to merge these steps in Topshelf installation for example like
MyService.exe install -instance "i00" -config "C:\i00Config.json"
First Try
I tried AddCommandLineDefinition from TopShelf but it seems it only works during installation and running through console not the service itself (will not add anything to service Image Path).
Second Try
I tried to see if its possible to do this with AfterInstall from Topshelf without any luck. here is a test code to see if it going to work or not (but unfortunately Topshelf overwrites the registry after AfterInstall call).
HostFactory.Run(x =>
{
x.UseNLog();
x.Service<MyService>(sc =>
{
sc.ConstructUsing(hs => new MyService(hs));
sc.WhenStarted((s, h) => s.Start(h));
sc.WhenStopped((s, h) => s.Stop(h));
});
x.AfterInstall(s =>
{
using (var system = Registry.LocalMachine.OpenSubKey("SYSTEM"))
using (var controlSet = system.OpenSubKey("CurrentControlSet"))
using (var services = controlSet.OpenSubKey("services"))
using (var service = services.OpenSubKey(string.IsNullOrEmpty(s.InstanceName)
? s.ServiceName
: s.ServiceName + "$" + s.InstanceName, true))
{
if (service == null)
return;
var imagePath = service.GetValue("ImagePath") as string;
if (string.IsNullOrEmpty(imagePath))
return;
var appendix = string.Format(" -{0} \"{1}\"", "config", "C:\i00config.json"); //only a test to see if it is possible at all or not
imagePath = imagePath + appendix;
service.SetValue("ImagePath", imagePath);
}
});
x.SetServiceName("MyService");
x.SetDisplayName("My Service");
x.SetDescription("My Service Sample");
x.StartAutomatically();
x.RunAsLocalSystem();
x.EnableServiceRecovery(r =>
{
r.OnCrashOnly();
r.RestartService(1); //first
r.RestartService(1); //second
r.RestartService(1); //subsequents
r.SetResetPeriod(0);
});
});
I couldn't find any relevant information about how it can be done using TopShelf so the question is, is it possible to do this with TopShelf?
Ok, as Travis mentioned, It seems there is no built-in feature or simple workaround for this problem. so I wrote a little extension for Topshelf based on a Custom Environment Builder (most of the code is borrowed form Topshelf project itself).
I posted the code on Github, in case others may find it useful, here is the Topshelf.StartParameters extension.
based on the extension my code would be like:
HostFactory.Run(x =>
{
x.EnableStartParameters();
x.UseNLog();
x.Service<MyService>(sc =>
{
sc.ConstructUsing(hs => new MyService(hs));
sc.WhenStarted((s, h) => s.Start(h));
sc.WhenStopped((s, h) => s.Stop(h));
});
x.WithStartParameter("config",a =>{/*we can use parameter here*/});
x.SetServiceName("MyService");
x.SetDisplayName("My Service");
x.SetDescription("My Service Sample");
x.StartAutomatically();
x.RunAsLocalSystem();
x.EnableServiceRecovery(r =>
{
r.OnCrashOnly();
r.RestartService(1); //first
r.RestartService(1); //second
r.RestartService(1); //subsequents
r.SetResetPeriod(0);
});
});
and I can simply set it with:
MyService.exe install -instance "i00" -config "C:\i00Config.json"
To answer you question, no this isn't possible with Topshelf. I am excited you figured out how to manage the ImagePath. But that's the crux of the problem, there's been some discussion on the mailing list (https://groups.google.com/d/msg/topshelf-discuss/Xu4XR6wGWxw/8mAtyJFATq8J) on this topic and issues about it in the past.
The big problem is that managing expectations of behavior when applying custom arguments to the ImagePath will be unintuitive. For example, what happens when you call start with custom command line arguments? I'm open to implementing this or accepting a PR if we get something that doesn't confuse me just thinking about it, let alone trying to use. Right now, I strongly encourage you to use configuration, not command line arguments, to manage this, even if it means duplicating code on disk.
The following work-around is nothing more than a registry update. The update operation expects the privileges the installer requires in order to write our extended arguments.
Basically, we're responding to the AfterInstall() event. As of Topshelf v4.0.3, calling the AppendImageArgs() work-around from within the event will cause your args to appear before the TS args. If the call is deferred, your args will appear after the TS args.
The work-around
private static void AppendImageArgs(string serviceName, IEnumerable<Tuple<string, object>> args)
{
try
{
using (var service = Registry.LocalMachine.OpenSubKey($#"System\CurrentControlSet\Services\{serviceName}", true))
{
const string imagePath = "ImagePath";
var value = service?.GetValue(imagePath) as string;
if (value == null)
return;
foreach (var arg in args)
if (arg.Item2 == null)
value += $" -{arg.Item1}";
else
value += $" -{arg.Item1} \"{arg.Item2}\"";
service.SetValue(imagePath, value);
}
}
catch (Exception e)
{
Log.Error(e);
}
}
An example call
private static void AppendImageArgs(string serviceName)
{
var args = new[]
{
new Tuple<string, object>("param1", "Hello"),
new Tuple<string, object>("param2", 1),
new Tuple<string, object>("Color", ConsoleColor.Cyan),
};
AppendImageArgs(serviceName, args);
}
And the resulting args that would appear in the ImagePath:
-displayname "MyService Display Name" -servicename "MyServiceName" -param1 "Hello" -param2 "1" -Color "Cyan"
Notice the args appeared after the TS args, -displayname & -servicename. In this example, the AppendImageArgs() call was invoked after TS finished its installation business.
Command line args can be specified normally using Topshelf methods such as AddCommandLineDefinition(). To force processing of the args, call ApplyCommandLine().

Finding the word in every line c#

so I'm writing a program that takes in a text line by line and then is supposed to output yes or no if the line contains the word problem.
The program is working, but I have to press enter twice to get the output.
The first enter I get, which is for the last line. And the second enter is so the while loop can break out.
Any suggestions to how I can improve this and not need the second enter?
using System;
using System.Collections.Generic;
namespace Tester
{
internal class Program
{
private static void Main(string[] args)
{
List<string> stringList = new List<string>();
string input = "";
while ((input = Console.ReadLine()) != string.Empty)
{
var s = input.ToLower();
stringList.Add(s.Contains("problem") ? "yes" : "no");
}
foreach (var str in stringList)
Console.WriteLine(str);
Console.ReadKey();
}
}
}
Well, for the last output you will type something. That's when (input = Console.ReadLine()) != string.Empty kicks in and the condition will pass.
The loop will come back to this line and block until you give it new input. Then supposedly you just type enter and in that case the loop will just exist. This is expected behaviour.
I'm not sure what upsets you about this. If you reaaaally wanted to get rid of the second enter, maybe you can put some token in your line (line /q or whatever) and whenever that is found in your line you know that you should break out of the loop.
Alternatively you can count how many inputs you get and make sure you get exactly 10 or 20 or whatever. When that number is reached, the loop will exit after the last input is processed.
Welcome to SO. :)
You can safely get rid of the last ReadKey. Given that you're creating a console application, you would normally rut it... in a console - as such, consoles don't close themselves after a program is done running. It's different if you run a console application in Windows OUTSIDE of a console - in this case, Windows will open a temporary console, run the app, and then close the console.
Also, if you're using Visual Studio, you can make VS wait for you by using the "start without debug" option (Ctrl+F5). VS will then add a "press enter to close" on it's own, at the end, to prevent the window from closing too fast, allowing you to check your outputs / exceptions.
One simple solution could be to output the "yes" or "no" values per line, rather than all at once at the end of your app.
Another way (which would require a bit more coding) would be to read individual keys, rather than lines - then you could react to the user pressing Esc, for example, rather than relying on an empty string ("extra" enter press).
You can use string.IsNullOrEmpty() and ToUpper() method is in general more accurate than ToLower(). So i'd probably refactor your code to something like :
private static void Main(string[] args)
{
List<string> stringList = new List<string>();
string input = "";
while(!string.IsNullOrEmpty(input = Console.ReadLine()))
stringList.Add(input.ToUpper().Contains("PROBLEM") ? "yes" : "no");
foreach (var str in stringList)
Console.WriteLine(str);
Console.ReadKey();
}
By the way welcome to SO... ;)
ok,do you mean this
static void Main(string[] args)
{
string input = "";
input = Console.ReadLine();
while (input != string.Empty)
{
if (input.Contains("problem"))
{
Console.WriteLine("yes");
}
else
{
Console.WriteLine("no");
}
input = Console.ReadLine();
}
Console.ReadKey();
}
maybe I know your intention.But if you want to break the while(input!=string.Empty) you must press an empty line.

Categories