Louis Rhys
Louis Rhys

Reputation: 35637

Can I get the arguments to my application in the original form (e.g including quotes)?

I have a .Net application that take a bunch of command line arguments, process some of it, and use the rest as arguments for another application

E.g.

MyApp.exe foo1 App2.exe arg1 arg2 ...

MyApp.exe is my application, foo1 is a parameter that my application care. App2.exe is another application, and my application will run App2 with arg1 arg2, etc. as arguments.

Currently my application just run App2.exe using something like this

Process.Start(args[1], String.Join(" ", args.Skip(2)). So the command above will correctly run: App2.exe with arguments "arg1 arg2". However, consider something like this

MyApp.exe foo1 notepad.exe "C:\Program Files\readme.txt"

The code above will not be aware of the quotes, and will run notepad.exe with arguments C:\Program Files\readme.txt (without quotes). How can I fix this problem?

Upvotes: 4

Views: 2741

Answers (9)

mp3ferret
mp3ferret

Reputation: 1193

Environment.CommandLine

will give you the exact command line - you'll have to parse out the path to your app but otherwise works like a charm - @idle_mind alluded to this earlier (kind of)

Edited to move example into answer (because people are obviously still looking for this answer). Note that when debuging vshost messes up the command line a little.

#if DEBUG             
    int bodgeLen = "\"vshost.\"".Length; 
#else             
    int bodgeLen = "\"\"".Length; 
#endif              
string a = Environment.CommandLine.Substring(Assembly.GetExecutingAssembly().Location.Lengt‌​h+bodgeLen).Trim();

Upvotes: 8

Daniel Gimenez
Daniel Gimenez

Reputation: 20564

Credit to @mp3ferret for having the right idea. But there was no example of a solution using Environment.CommandLine, so I went ahead and created a OriginalCommandLine class that will get the Command Line arguments as originally entered.

An argument is defined in the tokenizer regex as being a double quoted string of any type of character, or an unquoted string of non-whitespace characters. Within the quoted strings, the quote character can be escaped by a backslash. However a trailing backslash followed by a double quote and then white space will not be escaped.

There reason I chose the exception of the escape due to whitespace was to accommodate quoted paths that end with a backslash. I believe it is far less likely that you'll encounter a situation where you'd actually want the escaped double quote.

Code

static public class OriginalCommandLine
{
    static Regex tokenizer = new Regex(@"""(?:\\""(?!\s)|[^""])*""|[^\s]+");
    static Regex unescaper = new Regex(@"\\("")(?!\s|$)");
    static Regex unquoter = new Regex(@"^\s*""|""\s*$");
    static Regex quoteTester = new Regex(@"^\s*""(?:\\""|[^""])*""\s*$");

    static public string[] Parse(string commandLine = null)
    {
        return tokenizer.Matches(commandLine ?? Environment.CommandLine).Cast<Match>()
            .Skip(1).Select(m => unescaper.Replace(m.Value, @"""")).ToArray();
    }

    static public string UnQuote(string text)
    {
        return (IsQuoted(text)) ? unquoter.Replace(text, "") : text;
    }

    static public bool IsQuoted(string text)
    {
        return text != null && quoteTester.Match(text).Success;
    }
}

Results

As you can see from the results below the above method fixes maintains the quotes, while more gracefully handling a realistic scenario you might encounter.

Test:
    ConsoleApp1.exe foo1 notepad.exe "C:\Progra\"m Files\MyDocuments\"  "C:\Program Files\bar.txt"

    args[]:
[0]: foo1
[1]: notepad.exe
[2]: C:\Progra"m Files\MyDocuments"  C:\Program
[3]: Files\bar.txt

    CommandLine.Parse():
[0]: foo1
[1]: notepad.exe
[2]: "C:\Progra"m Files\MyDocuments\"
[3]: "C:\Program Files\bar.txt"

Finally

I debated using an alternative scheme for escaping double quotes. I feel that using "" is better given that command lines so often deal with backslashes. I kept the backslash escaping method because it is backwards compatible with how command line arguments are normally processed.

If you want to use that scheme make the following changes to the regexes:

static Regex tokenizer = new Regex(@"""(?:""""|[^""])*""|[^\s]+");
static Regex unescaper = new Regex(@"""""");
static Regex unquoter = new Regex(@"^\s*""|""\s*$");
static Regex quoteTester = new Regex(@"^\s*""(?:""""|[^""])*""\s*$");

If you want to get closer to what you expect from args but with the quotes intact, change the two regexes. There is still a minor difference, "abc"d will return abcd from args but [0] = "abc", [1] = d from my solution.

static Regex tokenizer = new Regex(@"""(?:\\""|[^""])*""|[^\s]+");
static Regex unescaper = new Regex(@"\\("")");

If you really, really want to get the same number of elements as args, use the following:

static Regex tokenizer = new Regex(@"(?:[^\s""]*""(?:\\""|[^""])*"")+|[^\s]+");
static Regex unescaper = new Regex(@"\\("")");

Result of exact match

Test: "zzz"zz"Zzz" asdasd zz"zzz" "zzz"

args               OriginalCommandLine
-------------      -------------------
[0]: zzzzzZzz      [0]: "zzz"zz"Zzz"
[1]: asdasd        [1]: asdasd
[2]: zzzzz         [2]: zz"zzz"
[3]: zzz           [3]: "zzz"

Upvotes: 1

csg
csg

Reputation: 2107

You will need to modify MyApp to enclose any arguments with quotes.

Short story, the new code should be this:

var argsToPass = args.Skip(2).Select(o => "\"" + o.Replace("\"", "\\\"") + "\"");
Process.Start(args[1], String.Join(" ", argsToPass);

The logic is this:

  1. each argument should be enclosed with quotes, so if you are calling with :

    MyApp.exe foo1 notepad.exe "C:\Program Files\readme.txt"

    The app will get called this way:

    notepad.exe "C:\Program Files\readme.txt"

  2. each argument should escape the quotes (if any), so if you are calling with:

    MyApp.exe foo1 notepad.exe "C:\Program Files\Some Path with \"Quote\" here\readme.txt"

    The app will get called this way:

    notepad.exe "C:\Program Files\Some Path with \"Quote\" here\readme.txt"

Upvotes: 4

aleppke
aleppke

Reputation: 496

One solution might be to try using Command Line Parser, a free 3rd-party tool, to set up your application to take specific flags.

For example, you could define the accepted options as follows:

    internal sealed class Options
    {
        [Option('a', "mainArguments", Required=true, HelpText="The arguments for the main application")]
        public String MainArguments { get; set; }

        [Option('t', "targetApplication", Required = true, HelpText = "The second application to run.")]
        public String TargetApplication { get; set; }

        [Option('p', "targetParameters", Required = true, HelpText = "The arguments to pass to the target application.")]
        public String targetParameters { get; set; }

        [ParserState]
        public IParserState LastParserState { get; set; }


        [HelpOption]
        public string GetUsage()
        {
            return HelpText.AutoBuild(this, current => HelpText.DefaultParsingErrorsHandler(this, current));
        }
    }

Which can then be used in your Program.cs as follows:

    static void Main(string[] args)
    {
        Options options = new Options();
        var parser = new CommandLine.Parser();

        if (parser.ParseArgumentsStrict(args, options, () => Environment.Exit(-2)))
        {
            Run(options);
        }
    }


private static void Run(Options options)
{
    String mainArguments = options.MainArguments;
    // Do whatever you want with your main arguments.

    String quotedTargetParameters = String.Format("\"{0}\"", options.TargetParameters);   
    Process targetProcess = Process.Start(options.TargetApplication, quotedTargetParameters);
}

You would then call it on the command line like this:

myApp -a mainArgs -t targetApp -p "target app parameters"

This takes all the guesswork out of trying to figure out what's an argument for which app while also allowing your user to specify them in whatever order they want. And if you decide to add in another argument down the road, you can easily do so without breaking everything.

EDIT: Updated Run method to include ability to add quotes around the target parameters.

Upvotes: 0

soydachi
soydachi

Reputation: 901

Try with "\"". I have to pass as arguments url too, this is the way:

_filenameDestin and _zip are urls. I hope it helps.

string ph = "\"";
var psi = new ProcessStartInfo();
psi.Arguments = "a -r " + ph + _filenameDestin + ".zip " + ph + _filenameDestin + ph;
psi.FileName = _zip;
var p = new Process();
p.StartInfo = psi;
p.Start();
p.WaitForExit();

Upvotes: 0

Ilan
Ilan

Reputation: 664

Try the following.

This code preserved the double quotes characters as well as giving the option to escape the \ and " characters (see comments in the code below).

static void Main(string[] args)
{
    // This project should be compiled with "unsafe" flag!
    Console.WriteLine(GetRawCommandLine());
    var prms = GetRawArguments();
    foreach (var prm in prms)
    {
        Console.WriteLine(prm);
    }
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern System.IntPtr GetCommandLine();
public static string GetRawCommandLine()
{
    // Win32 API
    string s = Marshal.PtrToStringAuto(GetCommandLine());

    // or better, managed code as suggested by @mp3ferret
    // string s = Environment.CommandLine;
    return s.Substring(s.IndexOf('"', 1) + 1).Trim();
}

public static string[] GetRawArguments()
{
    string cmdline = GetRawCommandLine();

    // Now let's split the arguments. 
    // Lets assume the fllowing possible escape sequence:
    // \" = "
    // \\ = \
    // \ with any other character will be treated as \
    //
    // You may choose other rules and implement them!

    var args = new ArrayList();
    bool inQuote = false;
    int pos = 0;
    StringBuilder currArg = new StringBuilder();
    while (pos < cmdline.Length)
    {
        char currChar = cmdline[pos];

        if (currChar == '"')
        {
            currArg.Append(currChar);
            inQuote = !inQuote;
        }
        else if (currChar == '\\')
        {
            char nextChar = pos < cmdline.Length - 1 ? cmdline[pos + 1] : '\0';
            if (nextChar == '\\' || nextChar == '"')
            {
                currArg.Append(nextChar);
                pos += 2;
                continue;
            }
            else
            {
                currArg.Append(currChar);
            }
        }
        else if (inQuote || !char.IsWhiteSpace(currChar))
        {
            currArg.Append(currChar);
        }
        if (!inQuote && char.IsWhiteSpace(currChar) && currArg.Length > 0)
        {
            args.Add(currArg.ToString());
            currArg.Clear();
        }
        pos++;
    }

    if (currArg.Length > 0)
    {
        args.Add(currArg.ToString());
        currArg.Clear();
    }
    return (string[])args.ToArray(typeof(string));
}

Upvotes: 0

Idle_Mind
Idle_Mind

Reputation: 39142

Use Environment.GetCommandLine() as it will keep the parameter in quotes together as one argument.

Upvotes: 2

Damith
Damith

Reputation: 63075

You can use backslashes for escape quotes. below will work

MyApp.exe foo1 notepad.exe \"C:\Program Files\readme.txt\"

Above will be the best solution if you are don't have idea about which other exes going to run and what are the arguments they expecting. In that case you can't add quotes from your program.

give instructions to add backslashes when there is quotes when running your application

Upvotes: 1

Theo Belaire
Theo Belaire

Reputation: 3020

Well, the simple answer is to just wrap every argument in quotes when calling MyApp2.exe. It doesn't hurt to wrap arguments that are one word, and it will fix the case that it has spaces in the argument.

The only thing that might go wrong is if the argument has an escaped quote in it already.

Upvotes: 1

Related Questions