■
PowerShellワンライナーで解く『シェル・ワンライナー160本ノック』第1章
はじめに
Pythonワンライナーで『シェル・ワンライナー160本ノック』を第一章まで解いた方のブログに感化されて、私も負けずにPowerShellで解くことにしました。
対象読者
シェル好きの方(ワンライナーという1行で書き捨てるタイプのプログラミングが好きな方)また職場でWSLやLinuxを使いたくても職場の制限を受ける方(PowerShellの便利さを伝えたい)
作者の自己紹介
非ITエンジニアでPowerShell使いです。職場でGit BashをWindowsで使ってましたが、色々と罠にハマることが多いため(文字コードやパスの区切り文字など)、PowerShellを使ってます。
注意
正しい解き方なのかは保証できかねます。
書籍情報
1日1問、半年以内に習得 シェル・ワンライナー160本ノック
環境
今回はLinuxのPowerShellで解いてみました。多分Windowsでも動きます。
問題1(ファイル名の検索)
問題:file.txtを開いて、「.exe」の拡張子を持つファイルを抜き出す。
>Get-Content ./files.txt test.txt test.exe 画面仕様書_v2.0.xls 画面仕様書.xls.exe secret file.md 画面仕様書_改訂版.xlsx 画面仕様書_最新バージョン.xls README.md 秘密のファイル.exe.jpeg LICENSE execution.sh packman_exe 重要書類.doc
解答
今回はPowerShell知らない人向けで、解答にはなるべくコマンドレットのAliasは使わないようにしよう思いましたが、やはり便利なのでちょっと一部でAliasを使っています。Aliasの補足は入れておきました(※コマンドレット:PowerShellの特徴である「動詞-名詞」という命名規則のコマンド) 例えば「Get-content」のAliasは「gc」でタイプ数が少なく、テキストファイルを開けるので、ワンライナー書く時はバリバリ使います。PowerShellでスクリプト書くときは、他人が読むことを考えて、Aliasはなるべく使わないようにします。では以下解答
Get-Content .\files.txt | Select-String "\.exe$"
出力結果
test.exe 画面仕様書.xls.exe
Get-Contentでファイルを開いて、パイプラインでSelect-Stringに渡します。ちなみにパイプラインではオブジェクトが次のコマンドレットに渡されます。Select-Stringは文字列を抽出するコマンドレットです。Grepみたいに使います。普通に正規表現も使えます。今回検索したい文字列は「.exe」ですが、この文字列の中の「.(ピリオド)」は正規表現で任意の1文字を意味するため、「\(バックスラッシュ)」でエスケープします。あと最後の「$」ですが、正規表現で行末(行の終わり)を意味します。「\.exe$」とすることで、行の最後にある「.exe」に一致する文字列だけを抽出することができます。※補足:Get-Contentを使う際の注意点、WindowsでGet-contentする際には文字コードに注意、Shift-JISやUTF-8かエンコーディングを指定しないと文字化けする場合があります。例えば、「Get-Content -Encoding utf8 .\files.txt」みたいにする必要あり。
別解
問題2(画像ファイルの一括変換)
解答
下記サイトを参考にしました。詳細も丸投げ。ちょっと説明すると、.NETのSystem.Drawingの力を借ります。ペイントの機能らしいです。他のツールをインストールする必要がなく、便利ですね。
問題3(ファイル名の一括変更)
問題:1〜1000の数字ファイル名を一括処理して、桁数を揃える
ごめんなさい、シェル芸の本では100万も空のファイル作るのですが、PowerShell遅いのでやめました。代わりに1000個の空のファイルを作りました。
解答
別解
ちょっとワンライナーで書くには長いかもですが、こういう書き方もあります。 https://nasunoblog.blogspot.com/2014/08/powershell-zero-padding-sample.html
問題4(特定のファイルの削除)
問題
1000個のテキストファイルにランダムな整数を書き込んで、「10」と書かれたファイルを削除する。
解答
シェル芸本では100万個のファイル作成ですが、まずは1000個でやってみます。
# まずランダムな整数を書き込んだファイル作成 1..1000 | ForEach-Object{Set-Content $_ -Value $(Get-Random -Minimum 1 -Maximum 1000) } # 「10」と書かれたファイルを検索して、そのファイルを消す。 Select-String -Pattern ^10$ -Path * | ForEach-Object{Remove-Item ($_ -split ":")[0]}
まずSelect-Stringの説明
>Select-String -Pattern ^10$ -Path * 194:1:10 241:1:10 762:1:10
「-Pattern」を使うと左から①ファイル名と②一致する行数③検索で一致した文字列が表示されます。今回必要なのは、①のファイル名です。次にForeach-Objectで、①のファイル名は「($_ -split ":")[0]」になるので、そいつをRemove-Itemに渡してやると指定したファイルを消せます。
問題5(設定ファイルからの情報抽出)
問題:ntp.confからpoolの項目にあるサーバの名前を抜き出す。
こんな感じの出力が得られます。(一部省略)
>gc ntp.conf # Use servers from the NTP Pool Project. Approved by Ubuntu Technical Board # on 2011-02-08 (LP: #104525). See http://www.pool.ntp.org/join.html for # more information. pool 0.ubuntu.pool.ntp.org iburst pool 1.ubuntu.pool.ntp.org iburst pool 2.ubuntu.pool.ntp.org iburst pool 3.ubuntu.pool.ntp.org iburst
問題6(端末に模様を書く)
問題:下記の出力が得られるワンライナーを作ってください。
x x x x x
解答
色々な別解が考えられそうですが、私の解答は以下です。 スペースの数を4から0までループで与えて、行末に「x」を書きます。
4..0 | foreach{ " "*$_+"x"}
問題7(消費税)
問題:2019年10月に消費税が上がったが、食品は税率が8%に据え置き。kakeibo.txtファイルの3列目の金額(税抜き)に消費税を加えてすべて足し合わせてください。
解答
PowerShell使いの津田さんからの解答を紹介します。一部を私が勝手に書き換えてますがご了承願います。 ConvertFrom-Csvまでは、これまでの解説で理解したという前提でいきます。まず%はForeach-ObjectのAliasです。{}の中でif文を使って、条件分岐をしています。ここでif文の条件式の中の「-or」は、論理演算子の一つで文字通り「OR(または)」を意味します。If文の中の処理の中に[decimal]は、不動小数点型を意味し、型を付けることで少数の計算結果で誤差が生じるのを防ぐことができます。誤差が生じる要因としては、コンピュータ内部で数値を2進数で扱うことが関係しているようです。link 次にMeasure-Objectの-sumでに合計値を出せますが、余計なデータもついてくるため、最後に行頭と行末をカッコで閉じて、.Sumで合計値のみを出力することができます。ちょっと説明が雑かもですね。
問題8(ログの集計)
問題
解答
シェル芸本の解答と同じく、コロンを区切り文字として、行末から攻めます。もうちょっと詳細を説明すると($_ -split ":")[-3]の部分でコロンで区切って、後ろから3番目の要素である「時刻」だけを取り出します。12時は午前なので、if文で午前と午後を条件分岐させます。最後にGroup-Objectでそれぞれの行数を簡単に求めることができます。最後にSelect-ObjectでCountとNameという必要なオブジェクトだけを抽出します。
gc ./access.log | %{($_ -split ":")[-3]} | %{if([int]$_ -le 12){Write-Output "午前"}else{Write-Output "午後"} } | Group-Object | select Count,Name
別解 カギカッコのなかの時刻文字列を時刻として認識させ、大小関係を比較しています。
gc .\access.log | % { if ($_ -match "\[(.+)\]$") { $date = [DateTime]::ParseExact($Matches[1],"dd/MMM/yyyy:HH:mm:ss zzz",[System.Globalization.CultureInfo]::InvariantCulture) if ($date.Hour -le 12) {"午前"} else {"午後"} } } | group | select Name, Count
解説をすると、まず[DateTime]:: は、.NETのクラスライブラリのクラスやメソッドを使う記法です。PowerShellの機能だけでは、うまく書けない場合には.NET(C#)の力を借ります。次に条件式で用いている正規表現"\[(.+)\]$"の説明をします。ざっくりいうと、カギカッコ[]の中にある複数の任意の文字列を意味します。正規表現の説明をすると、[]は正規表現のメタ文字なのでバックスラッシュでそれぞれエスケープして、カギカッコを抽出できるようにします。正規表現の()はグループ化を意味し、「.+」は任意の文字の繰り返しを意味します。詳しい説明は、例えば次のリンクを参照。一致した文字列は$Matchesに格納され、[DateTime]で型付けされた後に$date変数に格納されます。あと$Matches以降に書かれているのは、時刻の認識をさせるためのカスタム書式のフォーマットです。詳しくはこちらのDateTime.ParseExact。最後に、「$date.Hour」で時刻だけを取り出して、午前午後かどうかの判断をして最後に行数を数えるということですね。説明が大変でしたというか、この説明伝わるか自信ありません。
問題9(ログの抽出)
問題
こんな感じのログから、2016年12月24日21時台から2016年12月25日3時台までのログを出力する。
192.168.60.74 - - [01/Dec/2016 00:20:09] "GET / HTTP/1.0" 200 5855 192.168.49.206 - - [01/Dec/2016 01:04:29] "GET / HTTP/1.0" 200 1518 192.168.93.125 - - [01/Dec/2016 02:21:15] "GET / HTTP/1.0" 200 8931 略 192.168.113.126 - - [29/Dec/2016 23:22:10] "GET / HTTP/1.0" 200 9948
解答
ワンライナーではありません。ごめんなさい。ワンライナーでいい解答を思いつきませんでした。ちなみに下記はChatGPTの解答です。説明になっているかわかりませんが、一応説明します。$startLineの条件に一致するまで$inMatchフラグはFalseのままで、何もしません。次に条件が一致したらフラグをTrueにして、行を変数$matchingLinesに格納します。最後に$endLine条件に一致したらフラグをFalseにして、変数に行の格納をやめます。最後の$matchingLinesで変数の中身を展開します。
$startLine = "\[24/Dec/2016 21:..:..\]" $endLine = "\[25/Dec/2016 04:..:..\]" $matchingLines = @() $inMatch = $false Get-Content log_range.log | ForEach-Object { if ($_ -match $startLine) { $inMatch = $true } if ($_ -match $endLine) { $inMatch = $false } if ($inMatch) { $matchingLines += $_ } } $matchingLines
問題10(見出し記法の変換)
問題
下記のマークダウン記法を解答例のように変換します。
> gc ./headings.md # AAA これはAAAです # BBB これはBBBです。 楽しいですね。 ## CCC これはCCCCです ## DDD これはDDDです
期待する出力
AAA === これはAAAです BBB === これはBBBです。 楽しいですね。 CCC --- これはCCCCです DDD --- これはDDDです
解答
行頭の「#」とスペースは取り除きます。「#」の数に応じて、その次の行にイコールやハイフンを入れます。イコールもしくはハイフンを入れるには、-replace演算子を使います。sedみたいに使えます(「-replace "before","after"」)これらの処理をif文で条件分岐します。
gc ./headings.md | %{if($_ -match "^# "){$_ -replace "^# ","" -replace "$","`n==="}elseif($_ -match "^##"){$_ -replace "^## ","" -replace "$","`n---"}else{$_}}
問題11
問題
下記の議事録は、発言者と発言内容がそれぞれ一行ずつ分かれています。解答例のように整形して出力する。
>gc ./gijiroku.txt すず あばばあばば さと あばばばばばばば! やま びっくりするほどユートピア!びっくりするほどユートピア! すず うひょひょひょwwwwwやまwwやまwww さと ひょおお?ひょおお??? すず それでは会議を終わります
期待する出力
鈴木:あばばあばば 佐藤:あばばばばばばば! 山本:びっくりするほどユートピア!びっくりするほどユートピア! 鈴木:うひょひょひょwwwwwやまwwやまwww 佐藤:ひょおお?ひょおお??? 鈴木:それでは会議を終わります
解答
一行目は名前、二行目は発言内容、三行目は空白と規則的な構造になっているのをうまく利用します。For文のループは、3行ずつ処理を行います。処理の内容を説明すると、3行の中の一行目の省略した名前が、連想配列のkeyに一致したら、連想配列のValueである漢字の名前に置き換わります。2行目は発言内容であるため、この一行目と二行目をフォーマット文で「発言者:発言内容」と言う形に整形してやります。ちなみにPowerShellでgcしたテキストを変数に格納することで、$gijiroku[$i]のように配列の中の要素を取り出せます。3行目の改行は「`n」で表現します。Bashだと「\n」のようにバックスラッシュ使いますが、PowerShellはバッククォートを使います。
$name = @{}; $name["すず"] = "鈴木"; $name["さと"] = "佐藤"; $name["やま"] = "山本"; $gijiroku = gc gijiroku.txt; for ($i = 0; $i -lt $gijiroku.Length; $i += 3) { "{0}: {1}`n" -f $name[$gijiroku[$i]], $gijiroku[$i + 1] }
Created: 2023-02-25 Sat 00:26