I develop an application with command line parameters and use it in cmd shell and powershell. There it is obvious that the arguments are received differently in main() of my application.
static void Main(string[] args)
{
// In cmd shell: args[0] == "-ipaddress=127.0.0.1"
// In powershell: args[0] == "-ipaddress=127"
// and args[1] == ".0.0.1"
}
Example:
myApp.exe -ipaddress=127.0.0.1
In my C# application the arguments are interpreted differently depending on the shell I start the app in.
In cmd shell: arg[0]=-ipaddress=127.0.0.1
In powershell: arg[0]=-ipaddress=127 and arg[1]=.0.0.1
What is best practice here?
Should I join all args[] and parse the arguments in my application?
Should I rely on the shell's parser?
I would abandon cmd shell support completely and just created proper PowerShell cmdlet. Writing a Windows PowerShell Cmdlet. But I don't know your exact requirements.
In general calling executable from cmd and PowerShell should work same way. For example line, "> ping -n 3 google.com" just works fine no matter where you put it.
tl;dr:
When calling from PowerShell, enclose the entire argument in '...' (single quotes) to ensure that it is passed as-is:
myApp.exe '-ipaddress=127.0.0.1'
You've run into a parsing bug[1]
where PowerShell breaks an unquoted argument that starts with - in two at the first .
The following simplified example demonstrates that:
# Helper function that echoes all arguments.
function Out-Argument { $i = 0; $Args | % { 'arg[{0}]: {1}' -f ($i++), $_ }}
# Pass an unquoted argument that starts with "-" and has an embedded "."
Out-Argument -foo=bar.baz
The above yields:
arg[0]: -foo=bar
arg[1]: .baz
[1] The presumptive bug is present as of Windows PowerShell v5.1 / PowerShell Core v6.0.1 and has been reported on GitHub.
A PSv2 variation of the bug affects . when enclosed in "..." in the interior of the argument - see this answer of mine.
Related
I have a C# class library that provides a number of interfaces that can be called from PowerShell scripts (.PS1) and Advanced Modules (.PSM1). I have a static method to write verbose and debug messages to the console using the System.Console class:
public class Write
{
public static void Verbose(string msg, string source)
{
if (Config.EnableVerbose)
{
ConsoleColor originalForeGroundColor = Console.ForegroundColor;
ConsoleColor originalBackGroundColor = Console.BackgroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.BackgroundColor = ConsoleColor.Black;
Console.Write("VERBOSE: {0} {1}{2}", source, msg, Environment.NewLine);
Console.ForegroundColor = originalForeGroundColor;
Console.BackgroundColor = originalBackGroundColor;
}
}
}
However, when those messages are displayed in a PowerShell console, they cannot be captured using redirection, like with Out-File, >&0 or even with Start-Transcript.
I have read about_Redirection, and using the redirect modifiers does not capture the console output. For instance, using a PowerShell Advanced Function (aka Cmdlet) I have written:
Get-CommandTrace -ScriptBlock { Get-Resource } *> C:\Temp\capture.log
The Get-CommandTrace Cmdlet sets the $VerbosePreference = 'Continue' during the ScriptBlock execution, and does capture the verbose from Get-Resource output there. But does not capture the Console output from my C# library.
So, my question is simply: Can a C# class that is not a Cmdlet class, nor inherited class, be able to write output to the existing PowerShell runspace it is being called from?
Note:
This is not a complete answer, because it has severe limitations - though it may work for specific use cases.
The original form of this answer used since-deprecated PowerShell SDK method .CreateNestedPipeline(), which cannot be used anymore if you're writing your code against the PowerShellStandard library for cross-platform and cross-edition compatibility (code that should run in both Windows PowerShell and PowerShell Core, on all supported platforms).
Chris (the OP) himself found a compatible alternative, which the current form of this answer is based on.
The challenge is to write to the invoking pipeline's output streams (as you've observed writing via Console is unrelated to PowerShell's output streams and prints directly to the console, with no ability to capture or redirect such output in PowerShell).
While I you can obtain a reference to the invoking runspace, there is no way I know of to obtain a reference to the running pipeline.
Using the invoking runspace you can write to PowerShell's output streams via a new pipeline, but that comes with severe limitations:
You cannot write to the caller's success output stream (1) that way; that is, while you can call cmdlets that target the other streams, such as Write-Verbose, Write-Output does not work.
Capturing the output from this nested pipeline in a variable or sending it through the pipeline requires enclosing the method call in (...) (or $(...) or #(...)) in addition to applying the appropriate redirection to the success output stream (e.g., 4>&1 for the verbose stream).
See the code comments for details.
Add-Type -TypeDefinition #'
using System.Management.Automation;
public class Write
{
public static void Verbose(string msg)
{
using (PowerShell ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) {
// IMPORTANT: Use .AddScript(), not .AddCommand().
// Even though .AddCommand() + .AddParameter() is arguably a cleaner way to
// formulate the command, it results in output that cannot be captured.
ps.AddScript("Write-Output '" + msg.Replace("'", "''") + "'").Invoke();
}
}
}
'#
#"
$VerbosePreference = 'Continue'
# Regular output to the verbose stream.
Write-Verbose 'msg1'
# Verbose output via the custom type.
[Write]::Verbose('msg2')
# SUPPRESSING and REDIRECTING TO A FILE work.
[Write]::Verbose('msg3') 4> $null
[Write]::Verbose('msg4') 4> t.txt
# By default, REDIRECTING TO THE STANDARD OUTPUT STREAM (1)
# works only for the OUTSIDE, i.e. for CALLERS of this script.
[Write]::Verbose('msg5') 4>&1
# To REDIRECT TO THE STANDARD OUTPUT STREAM (1) in order to:
# * CAPTURE the result INSIDE your script
# * SEND THE RESULT THROUGH THE PIPELINE,
# additionally invoke the method call enclosed in (...) or $(...) or #(...)
$out = ([Write]::Verbose('msg6') 4>&1); "captured: [$out]"
([Write]::Verbose('msg7') 4>&1) | ForEach-Object { "piped: [$_]" }
While the answer above from mklement0 is a good one, it will not work if you attempt to use it with PowerShellCore or targeting .NetStandard 2.0 as the CreateNestedPipeline API is deprecated. (See this thread on the PowerShellStandard GitHub repo.)
So, instead, I have this working code:
Add-Type -TypeDefinition #'
using System.Management.Automation;
using System.Management.Automation.Runspaces;
public class Write
{
public static void Verbose(string msg)
{
using (PowerShell initialPowerShell = PowerShell.Create(RunspaceMode.CurrentRunspace))
{
initialPowerShell.Commands.AddScript("Write-Verbose " + msg.Replace("\"", "\"\"") + "\" -v");
initialPowerShell.Invoke();
}
}
}
'#
$VerbosePreference = 'Continue'
# Regular output to the verbose stream.
Write-Verbose 'msg1'
# Verbose output via the custom type.
# !! This can NOT be redirected from the outside.
[Write]::Verbose('msg2')
# !! SUPPRESSING or REDIRECTING TO A FILE only works
# !! when DIRECTLY APPLIED to the method call.
[Write]::Verbose('msg3') 4> $null
[Write]::Verbose('msg4') 4> t.txt
# !! REDIRECTING TO THE STANDARD OUTPUT STREAM (1) for the OUTSIDE works,
# !! but obviously it then merges with success output.
[Write]::Verbose('msg5') 4>&1
# !! To REDIRECT TO THE STANDARD OUTPUT STREAM (1) and capture the result
# !! INSIDE your script, invoke the method call in (...) or $(...) or #(...)
$out = ([Write]::Verbose('msg6') 4>&1)
"[$out]"
Which works with PowerShell 5.1 for Windows, PowerShell Core 6.2.2 for Windows and Linux (Ubuntu/Debian).
I will still leave mklement0 reply marked as the answer to the original question. I'm just adding another one based on research I had compiled over the past few days on migrating my class library to .NetStandard 2.0.
This seems trivial, but google only gives me powershell-related stuff and how to implement pipelines within programs.
I'm trying to consume input arguments from the (standard command processor cmd.exe for Windows 7, not powershell) command line pipeline on windows like 'more' does.
I'm trying to get this to work with two self-created .exe files:
randgen (creates a random number and writes it to the console)
wordwrite (the c# program that I want to consume this random number from the pipeline)
And using two common windows executables to test and model the behaviour:
echo (writes input to console)
more (displays output one screen at a time, can consume input from the pipeline)
Here's the current content of my Main function in wordwrite:
static void Main(string[] args)
{
string ouname = args[0];
DocWrite(ouname);
}
and the behaviour trying to naively pipe output from randgen into wordwrite:
D:\code>randgen | wordwrite
Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.
at ADDoc.ADOUDoc.Main(String[] args)
(plus a popup telling me wordwrite.exe has stopped working)
Which is the behaviour of trying to consume a null array of arguments as input:
D:\code>wordwrite
Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.
at ADDoc.ADOUDoc.Main(String[] args)
So the output of randgen isn't being read from the pipeline correctly as an argument to the wordwrite executable's args array (thanks #Peter Duniho for explaining the difference between stdin and how argument assignation works). Contrast that with the behaviour of the 'more' executable:
D:\code>randgen | more
56929227
That said, this 'pipeline-awareness' seems to be inherent to the 'more' executable itself, as another common windows executable, echo.exe, is just as pipeline-blind as wordwrite.exe:
D:\code>randgen | echo
ECHO is on.
D:\code>echo blah
blah
D:\code>echo
ECHO is on.
Here we can see that the attempt to use the pipeline is effectively the same as providing no arguments for echo.exe (contrasted in the middle with behaviour when an argument is provided).
Which suggests there is something I can add the the body of my Main function in wordwrite.exe to enable pipeline-consuming behaviour like the 'more' executable does. Does anyone know how this is done?
You can read data 'from the pipeline' (effectively, the stdin stream*) by using the Console.ReadLine() method, like so:
static void Main()
{
string ouname = Console.ReadLine();
DocWrite(ouname);
}
*The pipeline is a tool to direct the output of one process (stdout) to the input (stdin) of another one. So when you think of 'consuming input from the pipeline', think 'assign stdin to a variable that you then do stuff with'.
Reference:
C# Console receive input with pipe
I've created a .NET console application that gets some command line arguments.
When I pass args with white spaces, I use quotes to embrace these arguments so that they are not splitted by cmd:
C:\MyAppDir> MyApp argument1 "argument 2" "the third argument"
If I execute the app in Windows XP it works fine: it gets 3 arguments:
argument1
argument 2
the third argument
However, if i execute it in Windows Server 2008 it seems to ignore quotes: it gets 6 arguments:
argument1
"argument
2"
"the
third
argument"
Any ideas why?
NOTE: I printed arguments just when Main starts execution using this code:
Console.WriteLine("Command line arguments:");
foreach (string arg in args)
{
Console.WriteLine("# " + arg);
}
Make sure the character you are typing is indeed the double quote ".
Maybe it's a character that looks like it.
I know my Greek language settings produce a " but it's not read that way.
Please Try it.
C:\MyAppDir> MyApp argument1 \"argument 2\" \"the third argument\"
You can try, put each arguments between Quota"" and in the paths put double backslash for example like that:
generadorPlantillasPDF.exe "C:\GDI\desarrollos\celula canales\proyectos\Progreso\curso xml\" generadorprogreso.xml C:\Temp\ BVI "C:\GDI\desarrollos\celula canales\proyectos\Progreso\plantilla\" "C:\GDI\desarrollos\celula canales\proyectos\Progreso\" "Formulario Progreso Version 2.0.docx" C:\Temp\
In this article http://nagios.sourceforge.net/docs/3_0/eventhandlers.html#example they show how to get arguments from nagios to shell script. I have done this. Now in this article they show how to receive nagios parameters in .bat files. You can access nagios parameters like so:
(::echo 1: %1 2: %2 3: %3 4: %4)
In shell script I would access the variables like this: echo $1, echo $2 ect...
How do I access this variables in c# in .exe file?
It looks like Nagios just passes the arguments via command line. Assuming your C# project is just a console application, the command line arguments will be passed to Main. So the following will print 3 arguments:
class Program
{
static void Main(string[] args)
{
Console.WriteLine(args[0]); // echo $1
Console.WriteLine(args[1]); // echo $2
Console.WriteLine(args[2]); // echo $3
}
}
Note, before retrieving the command line arguments make sure to check that the indices are valid.
I found the following snippet of the code:
using System;
using System.Diagnostics;
public class RedirectingProcessOutput
{
public static void Main()
{
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c dir *.cs";
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Console.WriteLine("Output:");
Console.WriteLine(output);
}
}
but I can't figure out what this p.StartInfo.Arguments = "/c dir *.cs"; is doing? thanks in advance for any explanation
It's passing command line arguments to the process that will be launched.
In this particular case, the process is the Windows shell (cmd.exe). Passing a command line to it will cause it to execute this command when started; then, because of the /c parameter at the beginning it will terminate itself.
So the output of the process will be exactly what you will get if you open a command prompt and enter the command dir *.cs.
In the beginning was exec(3) and its friends, which accept the path to an executable and a variable length list of pointers to arguments. In sane operating systems, The process that gets started receives a point to the argument list, each word of which contains a pointer to and individual string. Sane shells parse the command line and populate the argument list required by exec(3).
You can see a direct correlation between the argument list accepted by exec(3):
exec ("some.executable.file", "arg1" , "arg2" , "arg3" , ... ) ;
and what gets passed to the entrypoint of the process:
int main ( char *arg[] ) { ... }
where argv[0] is the executable name, and argv[1]—argv[n-2] are the individual arguments, and argv[n-1] is a NULL pointer to indicate the end of the argument list.
Both conceptually simple and simple to implement.
CP/M didn't do it that way (I assume because of limited memory). It passed the started process the address of the raw command line from the shell and left its parsing up the process.
DOS followed in 1982 as clone of CP/M, handing a started process the address of the raw command line as well.
Windows hasn't deviated from that model since its inception. The Win32 CreateProcess() function
BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,
__inout_opt LPTSTR lpCommandLine,
...
);
still does the same thing, passing the raw command line to be passed to the program. The C runtime library, of course, takes care of the command line parsing for you...more or less.
So...in the CLR/.Net world, because of all this history, and because the CLR was designed to be dependent on the Win32 APIs, you have pass a complete command line to the process to be started. Why they didn't let you pass a params string[], instead and have the CLR build the command line is something that Microsoft's developers kept close to their chest.
Building the command line required by the started program is simple. You just join each argument into a single string with an SP character separating the arguments. Easy!
...until one of your arguments contains whitespace or a double quote character (").
Then you have to quote one or all of the arguments. Should be easy, but due to the bizarre quote rules, there are a lot of edge conditions that can trip you up.
A Windows command-line is broken up into words separated by whitespace, optionally quoted with double-quoted ("). Partly because Windows also got the path separater wrong (\ rather than /), the quoting rules are...byzantine. If you dig into the Windows CRT source code (the file is something like {VisualStudioInstallLocation}\VC\crt\src\stdargv.c), you'll find the command line parsing code.
That line just gives an argument to that proccess.