PowerShellワンライナーで解く『シェル・ワンライナー160本ノック』第1章

はじめに

Pythonワンライナーで『シェル・ワンライナー160本ノック』を第一章まで解いた方のブログに感化されて、私も負けずにPowerShellで解くことにしました。

対象読者

シェル好きの方(ワンライナーという1行で書き捨てるタイプのプログラミングが好きな方)また職場でWSLやLinuxを使いたくても職場の制限を受ける方(PowerShellの便利さを伝えたい)

作者の自己紹介

非ITエンジニアでPowerShell使いです。職場でGit BashWindowsで使ってましたが、色々と罠にハマることが多いため(文字コードやパスの区切り文字など)、PowerShellを使ってます。

注意

正しい解き方なのかは保証できかねます。

書籍情報

1日1問、半年以内に習得 シェル・ワンライナー160本ノック

https://gihyo.jp/book/2021/978-4-297-12267-6

参考記事

Pythonワンライナーで解く『シェル・ワンライナー160本ノック』第1章(問題1から問題11まで)

https://zenn.dev/yusukekato/articles/d5a389f7dbdad3

環境

今回はLinuxPowerShellで解いてみました。多分Windowsでも動きます。

$PSVersionTable
Name                           Value
----                           -----
PSVersion                      7.2.6
PSEdition                      Core
GitCommitId                    7.2.6
OS                             Linux 5.15.74-gentoo-x86_64
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

問題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」みたいにする必要あり。

別解

Get-Content .\files.txt | Where-Object{$_ -match "\.exe$" }

もしかしたら別解の方がよく使うかもしれません。 Where-Objectで特定の条件に一致する文字列を検索することができます。「$_」は特殊演算子で、パイプラインから受け取ったオブジェクトを意味します。Where-Objectだけでなく、Foreach-Objectでループ処理する際にもこの特殊演算子をよく使います。比較演算子「-match」で正規表現が使用できます。

問題2(画像ファイルの一括変換)

問題:ディレクトリ以下にあるJPEG形式の画像をPNG形式に変換する。

解答

下記サイトを参考にしました。詳細も丸投げ。ちょっと説明すると、.NETのSystem.Drawingの力を借ります。ペイントの機能らしいです。他のツールをインストールする必要がなく、便利ですね。

https://www.tekizai.net/entry/2021/09/12/063000

Add-Type -AssemblyName System.Drawing;Get-ChildItem *.jpg | foreach{$img=[System.Drawing.Image]::FromFile($_.fullname);$img.Save($(-join((join-path $_.DirectoryName $_.BaseName),".png")),[System.Drawing.Imaging.ImageFormat]::Png)}

問題3(ファイル名の一括変更)

問題:1〜1000の数字ファイル名を一括処理して、桁数を揃える

ごめんなさい、シェル芸の本では100万も空のファイル作るのですが、PowerShell遅いのでやめました。代わりに1000個の空のファイルを作りました。

解答

フォーマット演算子「-f」を使います。「{0:0000}」のコロンから右側のゼロの数が桁数です。 詳細は公式ドキュメントの演算子を見ていただければ。 (ちなみに下記の解答は、友人の「津田さん(@tsuda_ahr)」に作成いただきました。自分一人では正直いい解答が思いつかず悩んでいたので、助かりました。ありがとうございます。)

#空のファイル生成
1..1000 | foreach {New-Item $_}
#桁数を揃える「ゼロパディング」
Get-ChildItem | foreach{Rename-Item $_ ("{0:0000}" -f [int]$_.Name)}

別解

ちょっとワンライナーで書くには長いかもですが、こういう書き方もあります。 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

解答

2列目をうまく抽出するために、ConvertFrom-Csvを使いました。スペース区切りにして、各列に対して「1,2,3」とヘッダーをつけて、最後にSelect−Objectで「2」とすると、2列目だけを取り出せます。

Get-Content .\ntp.conf | Select-String "^pool" | ConvertFrom-Csv -Delimiter " " -Header @(1..3) | Select-Object "2"

問題6(端末に模様を書く)

問題:下記の出力が得られるワンライナーを作ってください。

    x
   x
  x
 x
x

解答

色々な別解が考えられそうですが、私の解答は以下です。 スペースの数を4から0までループで与えて、行末に「x」を書きます。

4..0 | foreach{ " "*$_+"x"}

問題7(消費税)

問題:2019年10月に消費税が上がったが、食品は税率が8%に据え置き。kakeibo.txtファイルの3列目の金額(税抜き)に消費税を加えてすべて足し合わせてください。

>gc ./kakeibo.txt
20190901 ゼロカップ大関 10000
20190902 *キャベツ二郎 130
20191105 外食 13000
20191106 ストロングワン 13000
20191106 *ねるねるねるねる 30
20190912 外食 13000

解答

PowerShell使いの津田さんからの解答を紹介します。一部を私が勝手に書き換えてますがご了承願います。 ConvertFrom-Csvまでは、これまでの解説で理解したという前提でいきます。まず%はForeach-ObjectのAliasです。{}の中でif文を使って、条件分岐をしています。ここでif文の条件式の中の「-or」は、論理演算子の一つで文字通り「OR(または)」を意味します。If文の中の処理の中に[decimal]は、不動小数点型を意味し、型を付けることで少数の計算結果で誤差が生じるのを防ぐことができます。誤差が生じる要因としては、コンピュータ内部で数値を2進数で扱うことが関係しているようです。link 次にMeasure-Objectの-sumでに合計値を出せますが、余計なデータもついてくるため、最後に行頭と行末をカッコで閉じて、.Sumで合計値のみを出力することができます。ちょっと説明が雑かもですね。

(gc .\kakeibo.txt |
ConvertFrom-Csv -Header @("date","food","price") -Delimiter " " |
% {
if ($_."food" -match "^\*" -or $_."date" -lt "20191001") {
[decimal]$_."price" * 1.08
} else {
[decimal]$_."price" * 1.1
}
} |
Measure-Object -Sum).Sum

問題8(ログの集計)

問題

下記のようなログから午前午後の行数を求めます。

>gc ./access.log
183.YY.129.XX - - [07/Nov/2017:22:37:38 +0900]
192.Y.220.XXX - - [08/Nov/2017:02:17:16 +0900]
66.YYY.79.XXX - - [07/Nov/2017:14:42:48 +0900]
::1 - - [07/Nov/2017:13:37:54 +0900]
133.YY.23.XX - - [07/Nov/2017:09:41:48 +0900] 

解答

シェル芸本の解答と同じく、コロンを区切り文字として、行末から攻めます。もうちょっと詳細を説明すると($_ -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行目は発言内容であるため、この一行目と二行目をフォーマット文で「発言者:発言内容」と言う形に整形してやります。ちなみにPowerShellgcしたテキストを変数に格納することで、$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

Validate