13

命令列工具的 stdout, stderr 輸出與 .NET 整合應用

 3 years ago
source link: https://blog.darkthread.net/blog/stdout-stderr/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
命令列工具的 stdout, stderr 輸出與 .NET 整合應用-黑暗執行緒

昨天提到的 Linux 掃描工具 - scanimage,剛好有個經典輸出分流行為,scanimage 將圖檔傳到標準輸出(Standard Output),故可加上 > tab.tiff 轉存成檔案,加上 -v -p 參數,過程會顯示偵錯資訊及執行進度,則是顯示在主控端(Console):

這恰好展示了 DOS、Linux 及大部分命令列環境的 Standard Output 與 Standard Error 輸出分流概念,UNIX/C 世界的術語是 stdout、stderr,對映 .NET 與 Process 溝通的 StandardOutputStandardError 屬性。

依據 GNU C Library 定義,除了輸出錯誤訊息,診斷相關資訊也是從 stderr 輸出:參考

Variable: FILE * stdout
The standard output stream, which is used for normal output from the program.
Variable: FILE * stderr
The standard error stream, which is used for error messages and diagnostics issued by the program.

因此,scanimage 接收掃描結果會從 stdout 輸出被導向檔案,診斷資訊則走 stderr 輸出到主控端。指令最後加個 2> msg.txt 則會把 stderr 導向 msg.txt,畫面將看不到診斷訊息,而是被轉存到 msg.txt:

如果你批次指令寫得夠多,可能看過 "2>&1" 像咒語一樣的寫法,它的意思是將 stderr 也合併到 stdout,但加的位置很關鍵。例如:

# 2>&1 先寫,result.txt 只會有 robots.txt 的內容
curl -v https://dotnet.microsoft.com/robots.txt 2>&1 >result.txt
# 2>&1 放在最後,result.txt 才會有包含診斷訊息及 robots.txt 
curl -v https://dotnet.microsoft.com/robots.txt >result.txt 2>&1

如果有點難理解,可想成 2(stderr), 1(stdout) 是 Reference 變數。
2>&1 >result.txt,1 一開始指向主控端,先 2>&1 把 2 也導向向主控端輸出,之後將 1 導向 result.txt 時 2 不受影響。
>result.txt 2>&1,先將 1 導向 result.txt,2>&1 將 2 也導向 1 輸出的 result.txt,故二者結果會合併。

在寫 .NET Console 程式時,怎麼決定輸出到 stdout 還是 stderr 呢?很簡單,使用 Console.Error.Write()/WriteLine() 就好,以下 .NET 6 Console 程式會分別輸出到 stderr 及 stdout,正常執行時二者都是顯示在螢幕上,但加上 >stdout.txt 2>stderr.txt 就能觀察到差異。

那,要怎麼做出像 scanimage 一樣進度數字在原地跳動的效果呢?

江湖一點訣,說破不值五毛錢,不要 WriteLine(),改為 Write() 並在最前方加一個 "\r" 讓游標移到第一欄即可。

如果從 .NET 程式執行外部程式時,要怎麼接收 stdout 與 stderr?

就以 scanimage 為對象,我試寫了一個範例:參考

using System.Diagnostics;

var si = new ProcessStartInfo() 
{
	FileName = "scanimage",
	Arguments = "-v -p --format tiff -d \"brother4:net1;dev0\" -x 100 -y 100 --source FlatBed --resolution 150",
	UseShellExecute = false,
	RedirectStandardOutput = true,
	RedirectStandardError = true,
	CreateNoWindow = true
};

using (var p = new Process()) 
{
	var progress = false;
	p.ErrorDataReceived += new DataReceivedEventHandler(
		(sender, e) => { 
			var s = e.Data;
			if (!string.IsNullOrEmpty(s)) 
			{
				//when redirected, no \r included in progress update, add it
				if (s.Contains("%")) 
				{					
					Console.Write("\r" + s);
					progress = true;
				}
				else
				{
					if (progress) 
					{
						Console.WriteLine();
						progress = false;
					}
					Console.WriteLine(s); 
				}
			}
		});
	p.StartInfo = si;
	p.Start();
	p.BeginErrorReadLine();
	using (var ms = new MemoryStream()) 
	{
		byte[] buff = new byte[8192];
		int len = 0;
		do 
		{
			len = p.StandardOutput.BaseStream.Read(buff, 0, buff.Length);
			if (len > 0) ms.Write(buff, 0, len);
		} while (len > 0);
		File.WriteAllBytes("image.tiff", ms.ToArray());
	}
	p.WaitForExit();
}

為了即時顯示進度,StandardError 用了 BeginErrorReadLine(),而其中有個小眉角,當偵測到 stderr 被導向時,scanimage 輸出進度百分比時不會前置 \r 以配合 Log 檔案輸出,我用了點技巧補上 \r 使其呈現原有的效果。執行結果如下:

希望以上的分享對常用或常寫命令列程式的朋友有些幫助。

題外話,原本覺得 .NET 6 把 Program.cs 簡化到 namespace、class、void Main(string[] args) 丟光光是公然偷懶,道德淪喪的行為,害 C# 都不 C# 了,但寫過幾支測試用小程式之後我決定改口 - Top-Level Statements 真香! 哈。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK