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

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.

Related

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.

Prevent process seeing existing instance

Here's the situation.
I have an application which for all intents and purposes I have to treat like a black box.
I need to be able to open multiple instances of this application each with a set of files. The syntax for opening this is executable.exe file1.ext file2.ext.
If I run executable.exe x amount of times with no arguments, new instances open fine.
If I run executable.exe file1.ext followed by executable.exe file2.ext then the second call opens file 2 in the existing window rather than creating a new instance. This interferes with the rest of my solution and is the problem.
My solution wraps this application and performs various management operations on it, here's one of my wrapper classes:
public class myWrapper
{
public event EventHandler<IntPtr> SplashFinished;
public event EventHandler ProcessExited;
private const string aaTrendLocation = #"redacted";
//private const string aaTrendLocation = "notepad";
private readonly Process _process;
private readonly Logger _logger;
public myWrapper(string[] args, Logger logger =null)
{
_logger = logger;
_logger?.WriteLine("Intiialising new wrapper object...");
if (args == null || args.Length < 1) args = new[] {""};
ProcessStartInfo info = new ProcessStartInfo(aaTrendLocation,args.Aggregate((s,c)=>$"{s} {c}"));
_process = new Process{StartInfo = info};
}
public void Start()
{
_logger?.WriteLine("Starting process...");
_logger?.WriteLine($"Process: {_process.StartInfo.FileName} || Args: {_process.StartInfo.Arguments}");
_process.Start();
Task.Run(()=>MonitorSplash());
Task.Run(() => MonitorLifeTime());
}
private void MonitorLifeTime()
{
_logger?.WriteLine("Monitoring lifetime...");
while (!_process.HasExited)
{
_process.Refresh();
Thread.Sleep(50);
}
_logger?.WriteLine("Process exited!");
_logger?.WriteLine("Invoking!");
ProcessExited?.BeginInvoke(this, null, null, null);
}
private void MonitorSplash()
{
_logger?.WriteLine("Monitoring Splash...");
while (!_process.MainWindowTitle.Contains("Trend"))
{
_process.Refresh();
Thread.Sleep(500);
}
_logger?.WriteLine("Splash finished!");
_logger?.WriteLine("Invoking...");
SplashFinished?.BeginInvoke(this,_process.MainWindowHandle,null,null);
}
public void Stop()
{
_logger?.WriteLine("Killing trend...");
_process.Kill();
}
public IntPtr GetHandle()
{
_logger?.WriteLine("Fetching handle...");
_process.Refresh();
return _process.MainWindowHandle;
}
public string GetMainTitle()
{
_logger?.WriteLine("Fetching Title...");
_process.Refresh();
return _process.MainWindowTitle;
}
}
My wrapper class all works fine until I start providing file arguments and this unexpected instancing behaviour kicks in.
I can't modify the target application and nor do I have access to its source to determine whether this instancing is managed with Mutexes or through some other feature. Consequently, I need to determine if there is a way to prevent the new instance seeing the existing one. Would anyone have any suggestions?
TLDR: How do I prevent an application that is limited to a single instance determining that there is already an instance running
To clarify (following suspicious comments), my company's R&D team wrote executable.exe but I don't have time to wait for their help in this matter (I have days not months) and have permission to do whatever required to deliver the required functionality (there's a lot more to my solution than this question mentions) swiftly.
With some decompiling work I can see that the following is being used to find the existing instance.
Process[] processesByName = Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName);
Is there any way to mess with this short of creating multiple copies of the application with different names? I looked into renaming the Process on the fly but apparently this isn't possible short of writing kernel exploits...
I have solved this problem in the past by creating copies of the source executable. In your case, you could:
Save the 'original.exe' in a specific location.
Each time you need to call it, create a copy of original.exe and name it 'instance_xxxx.exe', where xxxx is a unique number.
Execute your new instance exe as required, and when it completes you can delete it
You could possibly even re-use the instances by creating a pool of them
Building on Dave Lucre's answer I solved it by creating new instances of the executable bound to my wrapper class. Initially, I inherited IDisposable and removed the temporary files in the Disposer but for some reason that was causing issues where the cleanup would block the application, so now my main program performs cleanup at the end.
My constructor now looks like:
public AaTrend(string[] args, ILogger logger = null)
{
_logger = logger;
_logger?.WriteLine("Initialising new aaTrend object...");
if (args == null || args.Length < 1) args = new[] { "" };
_tempFilePath = GenerateTempFileName();
CreateTempCopy(); //Needed to bypass lazy single instance checks
HideTempFile(); //Stops users worrying
ProcessStartInfo info = new ProcessStartInfo(_tempFilePath, args.Aggregate((s, c) => $"{s} {c}"));
_process = new Process { StartInfo = info };
}
With the two new methods:
private void CreateTempCopy()
{
_logger?.WriteLine("Creating temporary file...");
_logger?.WriteLine(_tempFilePath);
File.Copy(AaTrendLocation, _tempFilePath);
}
private string GenerateTempFileName(int increment = 0)
{
string directory = Path.GetDirectoryName(AaTrendLocation); //Obtain pass components.
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(AaTrendLocation);
string extension = Path.GetExtension(AaTrendLocation);
string tempName = $"{directory}\\{fileNameWithoutExtension}-{increment}{extension}"; //Re-assemble path with increment inserted.
return File.Exists(tempName) ? GenerateTempFileName(++increment) : tempName; //If this name is already used, increment an recurse otherwise return new path.
}
Then in my main program:
private static void DeleteTempFiles()
{
string dir = Path.GetDirectoryName(AaTrend.AaTrendLocation);
foreach (string file in Directory.GetFiles(dir, "aaTrend-*.exe", SearchOption.TopDirectoryOnly))
{
File.Delete(file);
}
}
As a side-note, this approach will only work for applications with (lazy) methods of determining instancing that rely on Process.GetProcessByName(); it won't work if a Mutex is used or if the executable name is explicitly set in the manifests.

Execute the C# console exe with command line arguments from VB Project

How to call an exe written in C# which accepts command line arguments from VB.NET application.
For Example, Let's assume the C# exe name is "SendEmail.exe" and its 4 arguments are From ,TO ,Subject and Message and If I have placed the exe in the C drive. This is how I call from the command prompt
C:\SendEmail from#email.com,to#email.com,test subject, "Email Message " & vbTab & vbTab & "After two tabs" & vbCrLf & "I am next line"
I would like to call this "SendEmail" exe from the VB.NET application and pass the command line arguments from VB (arguments will be using vb Syntax like vbCrLf, VBTab etc). This problem may look silly but I am trying to divide the complex problem into series of smaller issues and conquer it.
Because your question has the C# tag, I'll suggest a C# solution you can re-spin in your preferred language.
/// <summary>
/// This will run the EXE for the user. If arguments are passed, then arguments will be used.
/// </summary>
/// <param name="incomingShortcutItem"></param>
/// <param name="xtraArguments"></param>
public static void RunEXE(string incomingExePath, List<string> xtraArguments = null)
{
if (File.Exists(incomingExePath))
{
System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo();
System.Diagnostics.Process proc = new System.Diagnostics.Process();
if (xtraArguments != null)
{
info.Arguments = " " + string.Join(" ", xtraArguments);
}
info.WorkingDirectory = System.IO.Path.GetDirectoryName(incomingExePath);
info.FileName = incomingExePath;
proc.StartInfo = info;
proc.Start();
}
else
{
//do your else thing here
}
}
You might not need to call it via the console. If it's done in C# and marked public instead of internal or private, or if it relies on a public type, you may be able to add it as a reference in your VB.Net solution and call the method you want directly.
This is so much cleaner and better because you don't have to worry about things like escaping spaces or quotes in your subject or body arguments.
If you have control over the SendMail program, you make it accessible with a few simple changes. By default, a C# Console project gives you something like this:
using ....
// several using blocks at the top
// class name
class Program
{
//static Main() method
static void Main(string[] args)
{
//...
}
}
You can make it available to use from VB.Net like this:
using ....
// several using blocks at the top
//Make sure an explicit namespace is declared
namespace Foo
{
// make the class public
public class Program
{
//make the method public
static void Main(string[] args)
{
//...
}
}
}
That's it. Again, just add the reference in your project, Import it at the top of your VB.Net file, and you can call the Main() method directly, without going through the console. It doesn't matter that's it an .exe instead of a .dll. In the .Net world, they're all just assemblies you can use.

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

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

Categories