コンピュータや音楽の事書いてます

powershell iniファイルをテスト用などでしょっちゅう書き換えるときのツール作ってみた

powershellの練習がてら作成。
コメントや空行などを維持したままで更新してくれる。

<#
	.SYNOPSIS
		iniファイルへの変更指示をコマンドラインから受け付けて処理する
	.example
		.\iniUtil セクション名1.パラメータ1=値1 セクション名2.パラメータ2=値2
		スペース区切りなので=の両端にはスペースを入れない 値にダブルクオーテーションはあっても無くても同じ
	.outputs
		現在のiniファイル名と同じで作成する
		元ファイルは_bakタイムスタンプを付けてバックアップする
#>
using namespace System.Collections.Generic
using namespace System.IO

$inifile = [INIFile]::new(".\AAAA.ini") #iniファイル名が固定の場合。パラメータにしても良いだろう
Write-Host "input file $($inifile.name)"

$section = [Section]::new("")
$lineno = 0
Get-Content $inifile.name | ForEach-Object { 
	if($_ -match "^\[.+\]") { 
		$inifile.sections.add($section.name, $section)
		$section = [Section]::new($_)
		Write-Host "input section $_"
		return #foreach-objectではcontinueではなくreturnで次ループへ移る
	} else { 
		$line = [INILine]::new($_)
	}

	if($line.type -eq [LineType]::param){ 
		if($section.lines.containskey($line.paramname)){ "警告 --------- 重複パラメータ 無視しました $($line.paramname)"}
		else{ $section.lines.add($line.paramname, $line) }
	} else { 
		$section.lines.add($lineno, $line) 
	}
	$lineno++
}
$inifile.sections.add($section.name, $section) #最後は終わりの合図がないので強制

$changecount = 0
#変更箇所を反映
$args.ForEach({
	$match = [regex]::Match($_, "^(.+)\.(.+)=(.+)$") #sec.key=val
	if(!$match.Success) { 
		Write-Host " ---- invalid param $_"
		continue #foreachメソッドの場合は普通にこれで良い
	}
	$arg = [Arg]::new($match.Groups[1].value, $match.Groups[2].value, $match.Groups[3].value)
	$sec = $inifile.sections[$arg.sec]
	if($sec -eq $null){
		$sec = [Section]::new("[$($arg.sec)]")
		Write-Host " ---- add Section $($arg.sec)"
		$inifile.sections.add($arg.sec, $sec)
	}
	$params = $sec.lines
	$arg.val = '"' + $arg.val + '"'
	if($params.containskey($arg.key)){
		Write-Host " ---- change $($arg.sec).$($arg.key) = $($arg.val)"
		$params[$arg.key].value = $arg.val
	} else {
		Write-Host " ---- add $($arg.sec).$($arg.key) = $($arg.val)"
		$params.add($arg.key, [INILine]::new("$($arg.key) = $($arg.val)"))
		$params.add("", [INILine]::new("")) #空行も追加
	}
	$changecount++
})
if($changecount -gt 0) { #changecountがあるときのみ書き換え 
	$bakfile = $inifile.name + "_bak" + [DateTime]::now.ToString("yyyyMMddHHmmss")
	Copy-Item $inifile.name ($bakfile) #バックアップ
	Write-Host " ---- backup $bakfile"
	$inifile.save($inifile.name) 
	Write-Host " ---- save $($inifile.name)"
} 

enum LineType {
	param
	comment
	empty
}
class INILine {
	[LineType] $type
	[string] $paramname
	[string] $value
	INILine([String] $line){
		#class内ではWrite-Outputは出力されない #Write-Host "new line constructor"
		if($line -match "^[ \t]*;") {
			$this.type = [LineType]::comment 
			$this.value = $line
		} elseif($line -match "^[ \t]*$"){
			$this.type = [LineType]::empty
		} else {
			$match = [regex]::Matches($line, "^[ \t]*(.+)=(.+)")
			if($match.success) { 
				$this.type = [LineType]::param
				$this.paramname = $match.Groups[1].value.trim()
				$this.value = $match.Groups[2].value.trim()
			} else {
				Write-Host "ignore line $line"
				$this.type = [LineType]::empty
			}
		}
	}
	out([StreamWriter] $w){
		switch ($this.type) {
			([LineType]::comment) { $w.WriteLine("$($this.value)") }
			([LineType]::empty) { $w.WriteLine("") }
			([LineType]::param) { $w.WriteLine("$($this.paramname) = $($this.value)") }
		}
	}
}
class Section {
	[string] $name #nameが無ければ名前のないSection(ファイル先頭など)
	[Dictionary[[string],[INILine]]] $lines = [Dictionary[[string],[INILine]]]::new()
	Section([string] $s){
		$this.name = [regex]::Match($s, "\[(.+)\]").groups[1].Value
	}
	out([StreamWriter] $w){
		if($this.name -ne "") { $w.WriteLine("[$($this.name)]") }
		foreach($k in $this.lines.keys){
			$this.lines[$k].out($w)
		}
	}
}
class INIFile {
	[string] $name;
	[Dictionary[[string],[Section]]] $sections = [Dictionary[[string],[Section]]]::new()
	INIFile ([string] $n){
		$this.name = $n
	}
	save([string] $filename){
		$outfile = [StreamWriter]::new((Get-Location).ToString() + "\" + $filename, $false, [System.Text.Encoding]::GetEncoding("shift-jis"))
		foreach($k in $this.sections.keys) {
			$this.sections[$k].out($outfile)
		}
		$outfile.Close()
	}
}
class Arg {
	[string] $sec
	[string] $key
	[string] $val
	Arg ($s, $k, $v){
		$this.sec = $s
		$this.key = $k
		$this.val = $v
	}
}