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

[powershell]巨大なテキストファイルの最後の方だけ見るのに時間がかかる

PS> dir \\リモートのPC\巨大ファイル

    ディレクトリ: \\xxxx\xxx\bin\logs


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/01/12     20:25       68290811 巨大ファイル.log

サーバのログの監視などでしょっちゅう状態を知りたいときなどに、
Linuxのtailの代わりにこれをやると

date
Get-Content \\リモートのPC\巨大ファイル -tail 20
date
20221013日 木曜日 20:23:29
~略~
20221013日 木曜日 20:26:03

2分半くらいかかった。もっと素早くぱぱっと見たい。

FileStreamのSeekを使ってから、StreamReaderに引き渡す。
byte数指定なので、仮に10000byte

using namespace System.IO
using namespace System.Text
date
$infile="\\リモートのPC\巨大ファイル" 
$fs = [FileStream]::new($infile, [FileMode]::Open, [FileAccess]::Read, [FileShare]::ReadWrite) #Seek()を使いたい + FileShare指定したいのでこれで開く
$fs.Seek(-10000, [SeekOrigin]::End)
$st = [StreamReader]::new($fs, [Encoding]::GetEncoding("shift_jis")) #ReadToEnd(), ReadLine()などを使いたいので
$st.ReadToEnd()
$st.close()
$fs.close()
date
20221013日 木曜日 20:22:00
~略~
20221013日 木曜日 20:22:14

14秒。
[FileShare]::ReadWrite でサーバ側の更新処理を邪魔しないようにしている(はず)

powershell Excelにファイル一覧

立ち上げたExcelにカレントディレクトリのファイル名一覧を書き出す

($sheet = (New-Object -ComObject excel.application).workbooks.add().sheets.add()).Application.Visible=$true
$r = $sheet.range("a1")
dir -Name | foreach { $r.value = $_; $r=$r.Offset(1,0) }

最後の行をこれにすればサブディレクトリも含む全て

dir -Name -Recurse | foreach { $r.value = $_; $r=$r.Offset(1,0) }

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
	}
}

Powershell class, インスタンス, List 不思議な挙動

Aというクラスがあるとして

class A {
	[string]$s1
}

.Net のList型を用意

using namespace System.Collections.Generic
$alist = New-Object List[A]

状態を見ると、List型にはAddというメソッドがあり、 A型を1つ入れられることが分かる。
$alist.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

$alist.Add

OverloadDefinitions
-------------------
void Add(A item)
void ICollection[A].Add(A item)
int IList.Add(System.Object value)

1つ入れてみる
$alist.Add( [A]::new())

型はもちろんそのまま
$alist.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     List`1                                   System.Object

個数は当然 1
$alist.Count

1

ここから本題。
powershellの配列には +=演算子(要素追加) がある。それを使ってみる。
$alist += New-Object A
$alist.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

形がObject[]に変わってしまった!しかし個数は正しいw
$alist.Count

2

List型にはその演算子はありません!って言ってほしい。

powershellでExcel操作+画像ファイルをクリップボード経由でシート貼り付け

ショートカット
cmd.exe /c powershell -noexit \app\scripts\toExcel2.ps1
を作り、そのショートカットへ複数フォルダをドラッグアンドドロップすると、
フォルダ毎別シートになって、ファイル名一覧をシートに書き込む。
拡張子が.DATまたは.csvのものは内容をシートに書き込む。
拡張子が.jpgのものは画像をシートに貼る。

#"\app\scripts\toExcel2.ps1"として保存
#ショートカット
#cmd.exe /c powershell -noexit \app\scripts\toExcel2.ps1
#フォルダを複数ドラッグアンドドロップ
using namespace System.Drawing;
using namespace System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms

$excel = New-Object -ComObject Excel.Application
$book = $excel.Workbooks.Add()
$excel.Visible = "True"
$excel.EnableEvents = "False"
$excel.DisplayAlerts = "False"

function execFolder($fol){
	cd $fol
	$sh = $book.Sheets.Add()
	$sh.Name = (Get-Item .).BaseName
echo $sh.Name
	$r = $sh.Cells(1,1)
	$files = dir -name
	$files.ForEach({ 
		$r.Value = $_
		$ext = (Get-Item $_).Extension
echo $ext
		if($ext -eq ".DAT" -or $ext -eq ".csv") {
			$r.Offset(0,3).Value = cat $_
		}
		$r = $r.Offset(1,0)
	})
echo $files
	$files = (dir *.jpg).Fullname
	$files.ForEach({ 
echo $r.Address()
		[Clipboard]::SetImage([Image]::FromFile($_))
		[Application]::DoEvents()
		$sh.Paste($r)
		$r = $r.Offset(1,0)
	})
}
$args.ForEach({
	execFolder($_)
})

$excel.EnableEvents = "True"
$excel.DisplayAlerts = "True"

C++Builder UnicodeStringとchar*文字列の連結

これがあるとき

UnicodeString s = "xxx"

これはできるのに

UnicodeString where = " abc" + s + " = :" + s;

これは出来ない

UnicodeString where = "and " + " abc" + s + " = :" + s;

理由:

char*とUnicodeStringをつなぐときには+演算子と自動変換が include\windows\rtl\ustring.h に定義してあるが、

  UnicodeString operator +(const char*, const UnicodeString&);
  UnicodeString operator +(const wchar_t*, const UnicodeString&);
  UnicodeString operator +(const char16_t*, const UnicodeString&);
  UnicodeString operator +(const char32_t*, const UnicodeString&);
    UnicodeString(const char* src);
    UnicodeString(const UnicodeString& src);
    UnicodeString(const WideChar* src, int len);
    UnicodeString(const char* src, int len);
    UnicodeString(const WideChar* src);
#if defined(WIDECHAR_IS_WCHAR)
    UnicodeString(const char16_t* src, int numChar16 = -1);
#endif
    UnicodeString(const char32_t* src, int numChar32 = -1);

#if defined(WIDECHAR_IS_CHAR16)
    UnicodeString(const wchar_t* src, int numWChar = -1);
#endif

char*が連続すると、char*とchar*では演算子が無いためコンパイルできない。

しかし、これはできる

UnicodeString where = "and " " abc" + s + " = :" + s;

理由:
"and " " abc" はプリプロセッサによってコンパイルよりも前に連結されるので + をつけなければ良い。

Sharepointのパスをエクスプローラで開く

https://xxxx.sharepoint.com/sites/xxxxxxxxxxxxxxxx/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2Fxxxxxxxxxxxxxxxxxxxx&viewid=xxxxxxxxxxxxxxxxx

このようにsites という言葉が2回でてくるパターンの場合、ブラウザのアドレスバーからURLをコピーし、ブラウザ内を右クリック
→ Edgeなら「開発者ツール」, Chromeだと「検証」
→「console」または「コンソール」タブでdecodeURIComponent関数を使い、

decodeURIComponent("https://xxxx.sharepoint.com/sites/xxxxxxxxxxxxxxxx/Shared%20Documents/Forms/AllItems.aspx?id=%2Fsites%2Fxxxxxxxxxxxxxxxxxxxx&viewid=xxxxxxxxxxxxxxxxx")

と実行すると、日本語を含んだパスがでてくるので、最後の"&viewid=xxxxxxxxxxxxxxxxx"の部分を取り除く

https://xxxx.sharepoint.com/sites/xxxxxxxxxxxxxxxx/Shared Documents/Forms/AllItems.aspx?id=/sites/xxxxxxxxxxxxxxxxxxxx

さらに、1つ目の"/sites/"と2つ目の"/sites/"の間の文字列を整理して

https://xxxx.sharepoint.com/sites/xxxxxxxxxxxxxxxxxxxx

このかたちにすれば、エクスプローラ(explorer.exe)で開くことができる。

さっきのdecodeURIComponent関数を使うところは

decodeURIComponent(location.href);

としても同じ結果になる。