PowerShell: SQL実行結果(TSV/CSV)をテキスト整形

概要

  • Azure Data Studio等のツールでSQLの実行結果を取得(コピー)するとタブ区切りの文字列になります。テキストベースの媒体にそのまま張り付けると、表示が崩れてしまうので、空白ベースに整形するツールを作成してみました。
  • Windows 10(64bit)+ PowerShell 5.1.19041の環境で動作確認しています。

スクリプトの仕様

機能概要

  • タブ区切りのファイルをテキストに整形するPowerShellのスクリプト(TabToText.ps1)です。スクリプトを修正することで、CSVファイルを整形することもできます。
  • 既定では、「単純整形」「表形式整形」の2つの整形方法を提供しますが、スクリプトの修正や追加でカスタマイズすることもできます。
    名称     データベースID 照合順序
    -------- -------------- --------------------------------
    master   1              SQL_Latin1_General_CP1_CI_AS
    tempdb   2              SQL_Latin1_General_CP1_CI_AS
    Test1    6              Japanese_CI_AS
    最大                    123456789013579123456789012
    半角超過 8              1234567890135791234567890..
    全角超過 9              135791357913579...
    |名称    |データベースID|照合順序                        |
    +--------+--------------+--------------------------------+
    |master  |1             |SQL_Latin1_General_CP1_CI_AS    |
    |tempdb  |2             |SQL_Latin1_General_CP1_CI_AS    |
    |Test1   |6             |Japanese_CI_AS                  |
    |最大    |              |123456789013579123456789012|
    |半角超過|8             |1234567890135791234567890..|
    |全角超過|9             |135791357913579...|
    • 列の値の最大幅をその列の幅(列幅)にします。
    • 値の幅が最大幅(既定では32文字)を超える場合、”..”で省略して整形します。
  • 引数を指定して、入力・出力ファイルを指定することもできます。
    • 入力・出力ファイルのエンコーディングはUTF-8を使用します。
    • 既定では、カレントフォルダの”input.tsv”ファイルを読み込み、整形した結果を出力します。

使用方法

  1. スクリプトをgithubからダウンロードし、任意のフォルダに配置します。
    スクリプト名説明
    メインスクリプトTabToText.ps1メインのスクリプト
    単純整形用スクリプトTabToText_simple.ps1単純整形のための(ハンドラ)関数の定義
    表形式整形用スクリプトTabToText_table.ps1表形式整形のための(ハンドラ)関数の定義
    サンプル入力ファイルinput.tsvサンプルとなるTSVファイル
  2. PowerShellを起動し、TabToText.ps1を実行します。
    PS > .\TabToText.ps1
    |名称    |データベースID|照合順序                        |
    +--------+--------------+--------------------------------+
    |master  |1             |SQL_Latin1_General_CP1_CI_AS    |
    |tempdb  |2             |SQL_Latin1_General_CP1_CI_AS    |
    
    PS > .\TabToText.ps1 -InFile input2.tsv -OutFile output.txt
    
    PS > .\TabToText.ps1 | ForEach-Object { Write-Output "> $_" }
    > |名称    |データベースID|照合順序                        |
    > +--------+--------------+--------------------------------+
    > |master  |1             |SQL_Latin1_General_CP1_CI_AS    |
    > |tempdb  |2             |SQL_Latin1_General_CP1_CI_AS    |
    • 入力ファイルのパスを”-InFile”、整形結果の出力先ファイルのパスを”-OutFile”で指定します。
    • 入力ファイルの指定がない場合、カレントフォルダの”input.tsv”を読み込みます。
    • 出力先ファイルの指定がない場合、画面(標準出力)に整形結果を出力します。

    (私の場合、SQLの実行結果を都度整形したいので、input.tsvをエディタで開いたままにし、SQL等の実行結果をコピペして保存後にスクリプトを実行する整形、という使い方を想定しています。)

カスタマイズの参考

  • カンマ区切り(CSV)を整形したい場合、メインのTabToText.ps1の冒頭の変数($DELI)を”,”に変更します。
  • 整形方法を変更したい場合、メインスクリプトに読み込む整形用スクリプト(単純整形用スクリプト、または、表形式整形用スクリプト)を切り替えます。
  • 単純整形、または、表形式整形の動作を変更したい場合、それぞれのスクリプト(TabToText_simple.ps1, TabToText_table.ps1)のハンドラ関数を修正します。
  • 新しい整形方法を追加したい場合、新規のスクリプトファイルを作成して、そこに独自のハンドラ関数を実装することをお薦めします。
  • 想定しているハンドラ関数は次の通りです。
    ハンドラ関数名実行単位(回数)説明
    CalcColWidth入力の行・列(M x N)対象行・列の幅を返却します。(各列の幅を決定するための)
    ProcHeader整形開始前(1)

    整形開始前の出力内容を返却します。
    ProcRowBefore入力の行(M)整形結果を出力する前の出力内容を返却します。
    ProcRow入力行に対応する整形結果を返却します。
    ProcRowAfter整形結果を出力した後の出力内容を返却します。
    ProcFooter整形終了後(1)整形開始後の出力内容を返却します。
    • 入力の各行・各列毎にCalcColWidthを実行します。返却された値の最大値が各列の幅(列幅)になります。
    • 整形の開始前後でProcHeader, ProcFooterを実行します。整形が開始されると、各行毎にProcRowBefore, ProcRow, ProcRowAfterを順番に実行します。
    • ProcRowBefore, ProcRow, ProcRowAfterに引き渡される各列の値(配列)は、列幅までパディングされた値になっています。列幅を超えた値の終端は切り捨てられた状態(終端が”..”の省略記号)になっています。

ソースコード

  • 完全なソースコードはgithubで公開しています。
  • 説明の都合でソースコードにあるコメントは一部削除して掲載しています。

メインスクリプト

メインの処理を実装したスクリプトです。

  • 入力ファイルとしてCSVファイルを使用する場合、$DELIの値を”,”に変更してください。
  • 列幅を超える値の終端は切り捨て、省略記号”..”に置き換えます。切り捨て時に全角文字の有無によって、省略記号の数が変わる場合があります。
  • 整形方法(13-14行目)に関して、「単純」を使用する場合はTabToText_simple.ps1を有効化、「表形式」を使用する場合はTabToText_table.ps1を有効化してください。
Param(
    [parameter(mandatory = $false)][String]$InFile,
    [parameter(mandatory = $false)][String]$OutFile
)

# 定数定義
$DELI = "`t"            # 列区切り(CSV: ",", TSV: "`t")
$EL_LEN = 2             # 省略時の記号の長さ("...")

$SJIS_ENC = [System.Text.Encoding]::GetEncoding("shift_jis")

# 整形方法の選択
. ${PSScriptRoot}\TabToText_simple.ps1 # 単純な整形
#. ${PSScriptRoot}\TabToText_table.ps1 # 表形式の整形

# メイン処理
Function ProcMain([String]$InFile, [String]$OutFile) {

    # データの読み取り
    if ([string]::IsNullOrEmpty($InFile)) {
        $InFile = ".\input.tsv"
    }
    $content = Get-Content $InFile -Encoding UTF8

    # 一旦データを読み取って各列幅を決定
    $widths = @() # 列幅配列(各列の幅を保持)
    $row = 0
    $content | ForEach-Object {
        $vals = $_ -split $DELI
        for ($col = 0; $col -lt $vals.Length; $col++) {
            # 列の値に基づいて列幅を算出(上限あり)
            $valw = GetValWidth -Value $vals[$col]
            $colw = CalcColWidth -Row $row -Col $col -ValueWidth $valw
            # 既存より列が増えた場合、列幅配列を追加
            if ( $widths.Length -lt ($col + 1) ) { $widths += 0 }
            # 既存より列幅が大きい場合は、列幅配列の値を更新
            if ( $widths[$col] -lt $colw ) { $widths[$col] = $colw }
        }
        $row++
    }

    # 列幅配列に基づいて各行・列のデータを出力
    # ※ProcXXX関数(ハンドラ関数)は、冒頭の「整形方式の選択」(外部のps1)で定義される前提
    $row = 0; $lines = @()
    $lines += ProcHeader -Widths $widths
    $content | ForEach-Object {
        $vals = $_ -split $DELI
        $outvals = @()
        for ($col = 0; $col -lt $vals.Length; $col++) {
            # 値が列幅以内であれば空白を埋める(超過時は切り捨て)
            $val = $vals[$col]; $wid = $widths[$col]
            $valw = GetValWidth -Value $val
            if ( $valw -le $wid ) {
                $padw = $wid - $valw
                $outvals += $val + " " * $padw
            }
            else {
                $outvals += Truncate -Value $val -Max $wid
            }
        }
        $lines += ProcRowBefore -Row $row -Column $col -Widths $widths -Values $outvals
        $lines += ProcRow       -Row $row -Column $col -Widths $widths -Values $outvals
        $lines += ProcRowAfter  -Row $row -Column $col -Widths $widths -Values $outvals
        $row++
    }
    $lines += ProcFooter -Widths $widths

    # 実行結果をファイルに出力(ファイル指定がない場合は標準出力へ)
    $result = ($lines | Where-Object { $null -ne $_ }) # ハンドラ関数で未出力の行を除外
    if ([string]::IsNullOrEmpty($OutFile)) {
        Write-Output $result # パイプライン後段処理のために配列のまま出力
    }
    else {
        $result -join "`r`n" | Out-File $OutFile
    }
}

# 文字列の超過部分を切り捨てる。
# ※全角文字のを中途半端に切り捨てて文字化けしないよう位置を調整
Function Truncate([string]$Value, [int]$Max) {
    # 省略記号を埋める前提で、値を残す最大幅を決定
    # (最大幅が小さすぎる場合は省略記号なし)
    $vmax = $Max
    if ( $EL_LEN -lt $Max ) { $vmax = $Max - $EL_LEN }
    # 値を残す最大幅までの文字列を生成
    # ※最大幅を超えないよう1文字(1or2バイト)づつ文字連結を試行
    $result = ""; $count = 0
    foreach ($ch in $Value.ToCharArray()) {
        $size = $SJIS_ENC.GetByteCount($ch)
        if ( $vmax -lt ($count + $size) ) { break }
        $result += $ch; $count += $size
    }
    # 最大幅まで省略記号を埋める
    $padlen = $max - $count
    if ( 0 -lt $padlen ) { $result += "." * $padlen }
    return $result
}

# 画面上での文字列幅を取得
# ※画面上の文字数とバイト数が一致するSJISを使用
#   (UTF-8/16等では画面上の文字数とバイト数が一致しない)
Function GetValWidth($Value) {
    return $SJIS_ENC.GetByteCount($Value)
}


# メイン処理の実行
ProcMain -InFile $InFile -OutFile $OutFile

単純整形用スクリプト

  • 各列の幅を整え、最大列幅($MAX_WIDTH)を超えた値は切り捨てます。
  • 1行目はカラム名、2行目以降は列値、のようなSQL実行結果(TSV)を想定しており、1行目直後にセパレータを挿入します。
  • ヘッダ名が異常に長いと無駄に列幅が広がる場合があります。$IGNORE_HEADERを有効すると、カラム名の幅を無視した列幅になります。
$MAX_WIDTH = 32         # 最大列幅
$IGNORE_HEADER = $false # 先頭行の列幅を無視

# 各列の最大幅を定義  ※戻り値が各列の最大幅になります!
Function CalcColWidth([int]$Row, [int]$Col, [int]$ValueWidth) {
    # ヘッダ行の幅は無視
    if ( $IGNORE_HEADER -and $Row -eq 0 ) { return 1 }
    # 列幅は最大列幅以下に調整
    $actual = $ValueWidth
    if ( $actual -lt $MAX_WIDTH) {
        return $actual
    }
    else {
        return $MAX_WIDTH
    }
}

# 処理前に出力する内容を定義
Function ProcHeader([int[]]$Widths) { return $null }

# 行出力の直前に出力する内容を定義
Function ProcRowBefore([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) { 
    return $null 
}

# 行出力する内容を定義
Function ProcRow([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) { 
    return $Values -join " " 
}

# 行出力の直後に出力する内容を定義
Function ProcRowAfter([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) {
    # 1行目の下にセパレータを出力
    if ( $Row -gt 0 ) { return $null }
    $line = ""
    foreach ($w in $Widths) {
        $line += "-" * $w + " "
    }
    return $line
}

# 処理後に出力する内容を定義
Function ProcFooter([int[]]$Widths) { return $null }

表形式整形用スクリプト

  • 動作は前述の単純整形用スクリプトと同様です。
  • 行・列の値に飾り付けするサンプルとして実装しました。
$MAX_WIDTH = 32     # 最大列幅

$SEP = "-"          # セパレータ(ヘッダ・データの仕切り)
$SEP_LEFT = "+"     # セパレータ行: 左側
$SEP_DIV = "+"      # セパレータ行: 列区切り
$SEP_RIGHT = "+"    # セパレータ行: 右側
$ROW_LEFT = "|"     # データ行: 左側
$ROW_DIV = "|"      # データ行: 列区切り
$ROW_RIGHT = "|"    # データ行: 右側

Function CalcColWidth([int]$Row, [int]$Col, [int]$ValueWidth) {
    # 列幅は最大列幅以下に調整
    $actual = $ValueWidth
    if ( $actual -lt $MAX_WIDTH) {
        return $actual
    }
    else {
        return $MAX_WIDTH
    }
}

Function ProcHeader([int[]]$Widths) { return $null }

Function ProcRowBefore([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) { 
    return $null 
}

Function ProcRow([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) {
    return $ROW_LEFT + ($Values -join $ROW_DIV) + $ROW_RIGHT
}

Function ProcRowAfter([int]$Row, [int]$Col, [int[]]$Widths, [string[]]$Values) {
    if ( $Row -gt 0 ) { return $null }
    $sepvals = @()
    foreach ($w in $Widths) {
        $sepvals += $SEP * $w
    }
    return $SEP_LEFT + ($sepvals -join $SEP_DIV) + $SEP_RIGHT
}

Function ProcFooter([int[]]$Widths) { return $null }