昨天提到的 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 真香! 哈。

Introduce to the concepts of stdout/stderr and how to use them in .NET.


Comments

Be the first to post a comment

Post a comment