CLIプログレスバーを作る(複数行対応/色付き)[Node.js/C#/VB.Net/Python3]

コンソールで一行用のプログレスバーを作る時は"\r"(Cr、キャリッジリターン)が使用されるのは割と有名ですが、これでは一行を過ぎると戻らなくなってしまいます。
ここでは、1行目にゲージを、2行目以降にメッセージを表現するプログレスバーの例を記述します。
![progress1.gif](https://qiita-image-store.s3.amazonaws.com/0/147573/525ec07e-ab28-c142-dde1-111df793b22a.gif)
![progress3.gif](https://qiita-image-store.s3.amazonaws.com/0/147573/593cf96b-12d7-4c9d-8f06-63ad8ab0282b.gif)
![progress2.gif](https://qiita-image-store.s3.amazonaws.com/0/147573/e9bd02ec-bc11-ea68-250e-fc5ebaf2db9c.gif)


# Node.js
```js:ProgressBar.js
"use strict";
const readline=require("readline");

const ProgressBar=(()=>{
	const PB={};
	const lenB=Symbol();

	//モノクロ版プログレスバー
	PB.Progress=class{
		constructor(width,parMax){
			//最大の桁数
			this.columns=process.stdout.columns;
			//プログレスバーの長さ
			this.width=width;
			//進捗度
			this.par=0;
			//目標進捗度
			this.parMax=parMax;
		}

		//プログレスバーの更新
		update(message){
			const parcent=this.par/this.parMax;
			const widthNow=Math.floor(this.width*parcent);
			const rowCnt=Math.floor(this[lenB](message)/this.columns)+2;

			const gauge=">".repeat(widthNow)+" ".repeat(this.width-widthNow);
			const status=`(${(parcent*100).toFixed(1)}%<-${this.par}/${this.parMax})`;

			console.error(`#[${gauge}]#${status}`);
			readline.clearScreenDown(process.stdout);

			console.error(message);
			readline.moveCursor(process.stdout,0,-rowCnt);
			this.par++;
		}

		//プログレスバーの完了
		done(doneAlert){
			const sideLen=Math.floor((this.width-doneAlert.length)/2);

			var gauge="=".repeat(sideLen)+doneAlert;
			gauge+="=".repeat(this.width-gauge.length);
			const status=`(100%<-${this.parMax}/${this.parMax})`;

			readline.clearScreenDown(process.stdout);
			console.error(`#[${gauge}]#${status}`);
		}
	};
	//バイト(文字幅)数計算
	PB.Progress.prototype[lenB]=function(message){
		var len=0;
		for(let val of message){
			let cc=val.charCodeAt(0);
			if( 0x0000<=cc && cc<=0x024F &&
				cc!=0x0085 && cc!=0x089 && cc!=0x00A7 && cc!=0x00B0 &&
				cc!=0x00B1 && cc!=0x00D7 && cc!=0x00F7 ||
				cc==0xA5 || cc==0x203E || cc==0xF8F0 ||
				0xFF61<=cc && cc<=0xFFDC ||
				0xFFE8<=cc && cc<=0xFFEE
			){
				len=0|len+1;
			}
			else{
				len=0|len+2;
			}
		}
		return len;
	};

	//カラー版プログレスバー
	PB.ProgressColor=class extends PB.Progress{
		//プログレスバーの更新
		update(message){
			const parcent=this.par/this.parMax;
			const widthNow=Math.floor(this.width*parcent);
			const rowCnt=Math.floor(this[lenB](message)/this.columns)+2;

			const status=`(${(parcent*100).toFixed(1)}%<-${this.par}/${this.parMax})`;

			readline.clearScreenDown(process.stderr);
			console.error(
				"\u001b[43m\u001b[5m"+  /*BackLightYellow*/
				"\u001b[33m"+			/*ForeDarkYellow*/
				"{"+
				"\u001b[46m"+  			/*BackLightCyan*/
				" ".repeat(widthNow)+
				"\u001b[25m"+ 			/*BackDarkCyan*/
				" ".repeat(this.width-widthNow)+
				"\u001b[43m\u001b[5m"+	/*BackLightYellow*/
				"}"+
				"\u001b[0m"+			/*ResetColor*/
				status+
				"\n"+
				message
			);
			readline.moveCursor(process.stderr,0,-rowCnt);
			this.par=0|this.par+1
		}

		//プログレスバーの完了
		done(doneAlert){
			const sideLen=Math.floor((this.width-doneAlert.length)/2);

			var gauge=" ".repeat(sideLen)+doneAlert;
			gauge+=" ".repeat(this.width-gauge.length);
			const status=`(100%<-${this.parMax}/${this.parMax})`;

			readline.clearScreenDown(process.stderr);

			console.error(
				"\u001b[43m\u001b[5m"+	/*BackLightYellow*/
				"\u001b[33m"+			/*ForeDarkYellow*/
				"{"+
				"\u001b[42m"+			/*BackLightGreen*/
				"\u001b[31m\u001b[1m"+  /*ForeLightRed*/
				gauge+
				"\u001b[43m"+			/*BackLightYellow*/
				"\u001b[33m\u001b[22m"+	/*ForeDarkYellow*/
				"}"+
				"\u001b[0m"+			/*ColorReset*/
				status
			);
		}
	};

	return PB;
})();
Object.freeze(ProgressBar);
module.exports=ProgressBar;
```
```js:main.js
"use strict";
const readline=require("readline");
readline.emitKeypressEvents(process.stdin);
const rl=readline.createInterface({input:process.stdin,output:process.stdout,terminal:false});
process.stdin.setRawMode(true);

const {Progress,ProgressColor}=require("./ProgressBar");

const g=(function*(){
	const firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ";
	const secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ";
	const thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ";
	process.stdout.write("READY...");
	yield process.stdin.once("keypress",key=>g.next(key));
	console.log("\rSTART!  ");

	const width=55;
	const works=270;
	//モノクロ版
	//const prg=new Progress(width,works);
	//カラー版
	const prg=new ProgressColor(width,works);

	for(let i=0;i<=works;i++){
		yield setTimeout(()=>g.next(),20);
		if(i<130){
			prg.update(firstMsg);
		}
		else if(i<210){
			prg.update(secondMsg);
		}
		else{
			prg.update(thirdMsg);
		}
	}
	prg.done("Done!");
	console.log("終了しました!");
	process.stdin.setRawMode(false);
	yield rl.once("line",val=>g.next(val));
	rl.pause();
})();
g.next();
```


# C<span>#</span>
```csharp:ProgressBar.cs
using System;
using static System.Console;

namespace ProgressBar{
	class Progress{
		//最大の桁数
		public int columns;
		//プログレスバーの長さ
		public int width;
		//進捗度
		public int par=0;
		//目標進捗度
		public int parMax;
		//最後に出力したカーソルの行
		protected int rowLate=CursorTop;

		//モノクロ版プログレスバー
		public Progress(int width,int parMax){
			this.columns=WindowWidth;
			this.width=width;
			this.parMax=parMax;
		}

		//プログレスバーの更新
		public virtual void update(string message){
			int row0=CursorTop;

			float parcent=(float)par/parMax;
			int widthNow=(int)Math.Floor(width*parcent);

			string gauge=new string('>',widthNow)+new string(' ',width-widthNow);
			string status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})";

			Error.WriteLine($"#[{gauge}]#{status}");
			clearScreenDown();

			Error.WriteLine(message);
			rowLate=CursorTop;
			SetCursorPosition(0,row0);
			par++;
		}

		//プログレスバーの完了
		public virtual void done(string doneAlert){
			int sideLen=(int)Math.Floor((float)(width-doneAlert.Length)/2);

			string gauge=new string('=',sideLen)+doneAlert;
			gauge+=new string('=',width-gauge.Length);
			string status=$"(100%<-{parMax}/{parMax})";

			clearScreenDown();
			Error.WriteLine($"#[{gauge}]#{status}");
		}

		//コンソール表示の掃除
		protected void clearScreenDown(){
			int clearRange=rowLate-(CursorTop-1);
			Error.Write(new string(' ',columns*clearRange));
			SetCursorPosition(CursorLeft,CursorTop-clearRange);
		}
	}

	//カラー版プログレスバー
	class ProgressColor:Progress{
		public ProgressColor(int width,int parMax):base(width,parMax){}

		//プログレスバーの更新
		public override void update(string message){
			int row0=CursorTop;
			float parcent=(float)par/parMax;
			int widthNow=(int)Math.Floor(width*parcent);

			string status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})";

			BackgroundColor=ConsoleColor.Yellow;
			ForegroundColor=ConsoleColor.DarkYellow;
			Error.Write("{");
			BackgroundColor=ConsoleColor.Cyan;
			Error.Write(new string(' ',widthNow));
			BackgroundColor=ConsoleColor.DarkCyan;
			Error.Write(new string(' ',width-widthNow));
			BackgroundColor=ConsoleColor.Yellow;
			Error.Write("}");
			ResetColor();
			Error.WriteLine(status);
			clearScreenDown();

			Error.WriteLine(message);
			rowLate=CursorTop;
			SetCursorPosition(0,row0);
			par++;
		}

		//プログレスバーの完了
		public override void done(string doneAlert){
			int sideLen=(int)Math.Floor((float)(width-doneAlert.Length)/2);

			string gauge=new string(' ',sideLen)+doneAlert;
			gauge+=new string(' ',width-gauge.Length);
			string status=$"(100%<-{parMax}/{parMax})";

			clearScreenDown();

			BackgroundColor=ConsoleColor.Yellow;
			ForegroundColor=ConsoleColor.DarkYellow;
			Error.Write("{");
			BackgroundColor=ConsoleColor.Green;
			ForegroundColor=ConsoleColor.Red;
			Error.Write(gauge);
			BackgroundColor=ConsoleColor.Yellow;
			ForegroundColor=ConsoleColor.DarkYellow;
			Error.Write("}");
			ResetColor();
			Error.WriteLine(status);
		}
	}
}
```
```csharp:main.cs
using System;
using System.Threading;
using static System.Console;
using ProgressBar;

class Program{
	static void Main(){
		const string firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ";
		const string secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ";
		const string thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ";
		Write("READY...");
		ReadKey();
		WriteLine("\rSTART!  ");

		const int width=55;
		const int works=270;
		//モノクロ版
		//var prg=new Progress(width,works);
		//カラー版
		var prg=new ProgressColor(width,works);

		for(var i=0;i<=works;i++){
			Thread.Sleep(20);
			if(i<130){
				prg.update(firstMsg);
			}
			else if(i<210){
				prg.update(secondMsg);
			}
			else{
				prg.update(thirdMsg);
			}
		}
		prg.done("Done!");
		WriteLine("終了しました!");
		ReadLine();
	}
}
```

# VB.Net
```vb:ProgressBar.vb
Option Strict On
Option Infer On
Imports System.Console
Imports CL=System.Console

Namespace ProgressBar
	'モノクロ版プログレスバー
	Class Progress
		'最大の桁数
		Public columns As Integer
		'プログレスバーの長さ
		Public width As Integer
		'進捗度
		Public par As Integer=0
		'目標進捗度
		Public parMax As Integer
		'最後に出力したカーソルの行
		Protected rowLate As Integer=CursorTop

		Sub New(width As Integer,parMax As Integer)
			Me.columns=WindowWidth
			Me.width=width
			Me.parMax=parMax
		End Sub

		'プログレスバーの更新
		Overridable Sub update(message As String)
			Dim row0=CursorTop

			Dim parcent=CSng(par/parMax)
			Dim widthNow=CInt(Math.Floor(width*parcent))

			Dim gauge=New String(">"c,widthNow) & New String(" "c,width-widthNow)
			Dim status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})"

			CL.Error.WriteLine($"#[{gauge}]#{status}")
			clearScreenDown()

			CL.Error.WriteLine(message)
			rowLate=CursorTop
			SetCursorPosition(0,row0)
			par+=1
		End Sub

		'プログレスバーの完了
		Overridable Sub done(doneAlert As String)
			Dim sideLen=(width-doneAlert.Length)\2

			Dim gauge=New String("="c,sideLen) & doneAlert
			gauge &=New String("="c,width-gauge.Length)
			Dim status=$"(100%<-{parMax}/{parMax})"

			clearScreenDown()
			CL.Error.WriteLine($"#[{gauge}]#{status}")
		End Sub

		'コンソール表示の掃除
		Protected Sub clearScreenDown()
			Dim clearRange=rowLate-(CursorTop-1)
			CL.Error.Write(New String(" "c,columns*clearRange))
			SetCursorPosition(0,CursorTop-clearRange)
		End Sub
	End Class

	'カラー版プログレスバー
	Class ProgressColor: Inherits Progress
		Sub New(width As Integer,parMax As Integer)
			MyBase.New(width,parMax)
		End Sub

		'プログレスバーの更新
		Overrides Sub update(message As String)
			Dim row0=CursorTop
			Dim parcent=CSng(par/parMax)
			Dim widthNow=CInt(Math.Floor(width*parcent))

			Dim status=$"({(parcent*100).ToString("f1")}%<-{par}/{parMax})"

			BackgroundColor=ConsoleColor.Yellow
			ForegroundColor=ConsoleColor.DarkYellow
			CL.Error.Write("{")
			BackgroundColor=ConsoleColor.Cyan
			CL.Error.Write(New String(" "c,widthNow))
			BackgroundColor=ConsoleColor.DarkCyan
			CL.Error.Write(New String(" "c,width-widthNow))
			BackgroundColor=ConsoleColor.Yellow
			CL.Error.Write("}")
			ResetColor()
			CL.Error.WriteLine(status)
			clearScreenDown()

			CL.Error.WriteLine(message)
			rowLate=CursorTop
			SetCursorPosition(0,row0)
			par+=1
		End Sub

		'プログレスバーの完了
		Overrides Sub done(doneAlert As String)
			Dim sideLen=(width-doneAlert.Length)\2

			Dim gauge=New String(" "c,sideLen) & doneAlert
			gauge &=New String(" "c,width-gauge.Length)
			Dim status=$"(100%<-{parMax}/{parMax})"

			clearScreenDown()

			BackgroundColor=ConsoleColor.Yellow
			ForegroundColor=ConsoleColor.DarkYellow
			CL.Error.Write("{")
			BackgroundColor=ConsoleColor.Green
			ForegroundColor=ConsoleColor.Red
			CL.Error.Write(gauge)
			BackgroundColor=ConsoleColor.Yellow
			ForegroundColor=ConsoleColor.DarkYellow
			CL.Error.Write("}")
			ResetColor()
			CL.Error.WriteLine(status)
		End Sub
	End Class
End Namespace
```
```vb:main.vb
Option Strict On
Imports System.Console
Imports System.Threading
Imports ProgressBar

Module Program
	Sub Main()
		Const firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ"
		Const secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ"
		Const thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ"
		Write("READY...")
		ReadKey()
		WriteLine(vbCr & "START!  ")

		Const width=CInt(55)
		Const works=CInt(270)
		'モノクロ版
		'Dim prg As New Progress(width,works)
		'カラー版
		Dim prg As New ProgressColor(width,works)

		For i=0 To works
			Thread.Sleep(20)
			If i<130 Then
				prg.update(firstMsg)
			ElseIf i<210 Then
				prg.update(secondMsg)
			Else
				prg.update(thirdMsg)
			End If
		Next
		prg.done("Done!")
		WriteLine("終了しました!")
		ReadLine()
	End Sub
End Module
```

# Python3
```py3:ProgressBar.py
import sys
import math
import os
import unicodedata
import colorama
from colorama import Fore,Back,Style
colorama.init()

#モノクロ版プログレスバー
class Progress:
	def __init__(self,width,parMax):
		#最大の行数
		self.columns=os.get_terminal_size().columns
		#プログレスバーの長さ
		self.width=width
		#進捗度
		self.par=0
		#目標進捗度
		self.parMax=parMax

	#プログレスバーの更新
	def update(self,message):
		parcent=self.par/self.parMax
		widthNow=math.floor(self.width*parcent)
		rowCnt=math.floor(self.__lenB(message)/self.columns)+2

		gauge=">"*widthNow+" "*(self.width-widthNow)
		status=f"({round(parcent*100,1)}%<-{self.par}/{self.parMax})"

		sys.stderr.write(
			f"#[{gauge}]#{status}\n"+
			"\u001b[0J"+			#clearScreenDown
			message+
			f"\u001b[{rowCnt}A\r\n" #moveCursorUp
		)
		self.par=self.par+1

	#プログレスバーの完了
	def done(self,doneAlert):
		sideLen=math.floor((self.width-len(doneAlert))/2)
		gauge="="*sideLen+doneAlert
		gauge+="="*(self.width-len(gauge))

		status=f"(100%<-{self.parMax}/{self.parMax})"

		sys.stderr.write(
			"\u001b[0J"+	#clearScreenDown
			f"#[{gauge}]#{status}\n"
		)

	#バイト(文字幅)数計算
	def __lenB(self,str):
		len=0
		for val in str:
			cw=unicodedata.east_asian_width(val)
			if cw in u"WFA":
				len=len+2
			else:
				len=len+1
		return len

#カラー版プログレスバー
class ProgressColor(Progress):
	#プログレスバーの更新
	def update(self,message):
		parcent=self.par/self.parMax
		widthNow=math.floor(self.width*parcent)
		rowCnt=math.floor(self._Progress__lenB(message)/self.columns)+2

		status=f"({round(parcent*100,1)}%<-{self.par}/{self.parMax})"

		sys.stderr.write(
			Style.BRIGHT+
			Back.YELLOW+
			Fore.YELLOW+
			"{"+
			Back.CYAN+
			Fore.CYAN+
			"▤"*widthNow+
			" "*(self.width-widthNow)+
			Back.YELLOW+
			Fore.YELLOW+
			"}"+
			Style.RESET_ALL+
			status+
			"\n"+
			"\u001b[0J"+			#clearScreenDown
			message+
			f"\u001b[{rowCnt}A\r\n" #moveCursorUp
		)
		self.par=self.par+1

	#プログレスバーの完了
	def done(self,doneAlert):
		sideLen=math.floor((self.width-len(doneAlert))/2)
		doneAlert=" "+doneAlert+" "
		gauge=(
			Fore.GREEN+
			"▤"*sideLen+
			Fore.RED+
			doneAlert+
			Fore.GREEN+
			"▤"*(self.width-len("#"*sideLen+doneAlert))
		)
		status=f"(100%<-{self.parMax}/{self.parMax})"

		sys.stderr.write(
			"\u001b[0J"+	#clearScreenDown
			Style.BRIGHT+
			Back.YELLOW+
			Fore.YELLOW+
			"{"+
			Back.GREEN+
			gauge+
			Back.YELLOW+
			Fore.YELLOW+
			"}"+
			Style.RESET_ALL+
			status+
			"\n"
		)
```
```py3:main.py
import os
from time import sleep
from ProgressBar import Progress,ProgressColor

if __name__ in "__main__":
	firstMsg="1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ 1stステップ"
	secondMsg="2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ 2ndステップ"
	thirdMsg="3rdステップ 3rdステップ 3rdステップ 3rdステップ 3rdステップ"
	print("READY...",end="")
	if os.name=="nt":
		import msvcrt
		msvcrt.getch()
	else:
		input()
	print("\rSTART!  ")

	width=55
	works=270
	#モノクロ版
	#prg=Progress(width,works)
	#カラー版
	prg=ProgressColor(width,works)

	for i in range(0,works+1):
		sleep(0.02)
		if i<130:
			prg.update(firstMsg)
		elif i<210:
			prg.update(secondMsg)
		else:
			prg.update(thirdMsg)
	prg.done("Done!")
	print("終了しました!")
	input()
```

# 何をやってるの?
コンソールのカーソルの位置を記憶して、標準出力後に戻してます。
Node.jsではカーソルの位置の取得ができなかったので、コンソールの最大桁数と出力する文字バイト幅から何行分に相当するか計算して、相対座標で移動しています。
Node.jsのバイト数の計算はこちらの[Unicode表](http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/index.html)に基づいて、半角全角判別するメソッドを書いて使用しています。(とりあえず、半角英数記号に加えてラテンBまでと半角カナ、半角ハングルまでを半角(1バイト)として計算。この区分分けについてベターと言えそうなものがなかったので自分で実装。)
Pythonの方は文字のコードから文字種の判別できるようなので、そレを利用しています。

なお、Python3については標準じゃ厳しかったのでcoloramaを使用しています。
インストールはpipから次のように行っています。

```bat:
pip3.6 install colorama
```

## 色つき版補足
Node.jsではWindowsでもANSIエスケープコードが使えるみたいですね。
流石replやnpmがやたらカラフルなだけあって感心したのですが、拡張色選ぼうとしたらバグったので使えるのは7色だけのようです。

なお、Python3でだけcolorama使用しても何故か背景色が変わりませんでした...
他の言語と見た目が若干変わるのは妥協の証。