Linux/MacユーザーがPowerShellでつまずく理由——設計思想と構文の違いを徹底解説

プログラミング・IT

Linux/Macのターミナルに慣れた状態でWindows PowerShellを触ると、見覚えのあるコマンドが動くのに結果が違う、オプションの書き方がことごとく通じない、という場面に繰り返しぶつかります。

この記事では、他OS経験者がPowerShellで「なぜそうなる?」と感じるポイントを整理し、思考の切り替え方を解説します。

スポンサーリンク

まず知るべき設計思想の違い

PowerShellとBash/Zshは設計思想が根本的に異なるため、個別のコマンドを覚える前にこの違いを掴んでおくと、「なぜこう書くのか」が腑に落ちやすくなります。

命名規則——短縮名 vs 動詞-名詞

Linuxコマンドはlscprmのように短く、タイプ数を最小化する文化です。

PowerShellのコマンド(コマンドレット)はGet-ChildItemCopy-ItemRemove-Itemのように動詞-名詞の形式で統一されています。

動詞意味コマンド例
Get取得するGet-Process、Get-ChildItem
Set設定するSet-Location、Set-Content
New新規作成するNew-Item、New-LocalUser
Remove削除するRemove-Item
Start / Stop開始 / 停止Start-Service、Stop-Process

長い名前に抵抗があっても、lscdなどのLinuxコマンド名がエイリアス(別名)として登録されているため、入力自体は通ります。

筆者もPowerShellを使い始めた当初はエイリアスに頼りきっていました。
しかし、業務で使うスクリプト(.ps1ファイル)を書くようになった段階で、公式のスタイルガイドがスクリプト内でのエイリアス使用を非推奨としていることを知り、正式名称に矯正することになりました。

対話的にコマンドを打つ場面ではエイリアスで問題ありませんが、スクリプトとして残すコードは正式名称で書く、と使い分けるのが現実的です。

Tab補完も強力で、Get-Chまで打ってTabキーを押せばGet-ChildItemに補完されます。
正式名称の長さは見た目ほど負担になりません。

パイプラインの中を流れるもの——テキスト vs オブジェクト

Linux/Macのパイプ(|)はコマンド間で「テキスト」を受け渡します。
PowerShellのパイプラインは「オブジェクト(構造化されたデータ)」を受け渡します。

この違いは、移行時に最も混乱を引き起こす部分です。

Linuxの場合:

ps aux | grep nginx | awk '{print $2}'

テキスト出力をgrepで絞り込み、awkで列を抽出する、というテキスト処理の連鎖です。

PowerShellの場合:

Get-Process | Where-Object { $_.ProcessName -eq "nginx" } | Select-Object Id

Get-Processがプロセス情報をオブジェクトとして出力し、Where-Objectがプロパティ名で絞り込み、Select-Objectが必要なプロパティだけを取り出します。

筆者が最初にこの壁にぶつかったのは、ログファイル一覧から特定の拡張子だけを抜き出そうとしたときでした。
Linux感覚でls | grep ".log"と書いたところ、以下のエラーが返ってきました。

grep : 用語 'grep' は、コマンドレット、関数、スクリプト ファイル、
または操作可能なプログラムの名前として認識されません。
名前が正しく記述されていることを確認し、パスが含まれている場合は
そのパスが正しいことを確認してから、再試行してください。

PowerShellにはgrepが存在しません。
lsはエイリアスとして通るのにgrepは通らない、という非対称さに面食らったのを覚えています。

grepWhere-ObjectawkSelect-Objectsed-replace演算子。
対応関係を覚えるというよりも、「テキストを加工する」のではなく「オブジェクトのプロパティを指定する」という発想そのものを切り替える必要があります。

オブジェクトベースのパイプラインは、テキスト加工にありがちな「列がずれて意図しないデータを拾う」といった事故が起きにくく、慣れると堅実に感じられます。

オプション構文——--longも-rもない

Linux/Mac経験者がPowerShellで最も苦労するのが、オプション(パラメータ)の書き方の違いです。

LinuxPowerShell違い
-r(短縮形)-Recurse(フルネーム)1文字短縮形が存在しない
--recursive(長い形式)-Recurseハイフンは常に1つ
-l(long format)対応なしFormat-Listで出力形式を別途指定
-rf(複数オプションの結合)-Recurse -Force(個別に指定)結合構文がない

筆者にとって、この構文の違いがPowerShell移行で最もストレスの大きい部分でした。

Linuxでcp -r src dstと手癖で打っていたものが、PowerShellではCopy-Item src dst -Recurseになります。
オプションの位置も自由で、Copy-Item -Recurse src dstでも動作します。
この「オプションが引数の前にも後にも書ける」という仕様は、Linuxの慣習(オプションは引数の前)に慣れていると最初は気持ち悪く感じます。

ただし、PowerShellのパラメータ名は途中まで入力すれば一意に特定できる短縮が可能です。
例えば-Recurse-Reまで打てばTabで補完されますし、-ErrorAction-EAだけで通ります。
公式な短縮形ではなく「一意に特定できる最短文字列」が自動的に受け入れられる仕組みです。

コマンドプロンプトの/sのようなスラッシュ+1文字のオプションも使えません。
3つの環境それぞれで構文が違うため、「他の環境の手癖が邪魔をする」のが移行初期の最大の敵です。

ファイル操作で最初にハマること

ファイル操作はPowerShellで最も使用頻度の高い領域です。

ここでは、基本構文の羅列ではなく「移行者が実際にぶつかる場面」に焦点を当てて解説します。

一括リネームの実務例

大量のファイルに対して同じ規則でリネームしたい場面は頻繁にあります。

例えば、フォルダ内の画像ファイル(IMG_4521.jpgDSC0012.jpgなど)をphoto_001.jpgphoto_002.jpg…と連番に揃えるケースです。

$i = 1
Get-ChildItem -Filter *.jpg | ForEach-Object {
    Rename-Item $_.FullName -NewName ("photo_{0:D3}.jpg" -f $i)
    $i++
}

{0:D3}は3桁ゼロ埋めの書式指定です。
ファイル数が100を超える見込みがあればD4(4桁)に変えておくと、ファイルの並び順が崩れません。

Linuxならrenameコマンドやforループとパラメータ展開で書くところですが、PowerShellではGet-ChildItemでオブジェクトを取得し、ForEach-Objectで各オブジェクトのFullNameプロパティを使って操作する流れになります。

ハマりポイント: リネーム対象のファイルに日本語名が含まれている場合、PowerShell 5.1ではコンソールの文字コード設定によっては正しく処理されないことがあります。
$OutputEncoding[Console]::OutputEncodingの設定を確認してください。

rm -rf感覚のRemove-Itemは危険——-WhatIfで事前確認

Linuxでrm -rfを打つとき、多くの人は「消える」ことを明確に意識しています。
確認プロンプトも出ず、即座に削除されます。

PowerShellのRemove-Item -Recurse -Forceも同様に確認なしで削除が実行されますが、挙動に微妙な違いがあります。

# これは確認なしで即削除される
Remove-Item -Path "C:\OldProject" -Recurse -Force

PowerShellには-WhatIfという安全装置があります。
コマンドに-WhatIfを付けると、実際には実行せず「何が起きるか」だけを表示します。

# 実行せず、何が削除されるかだけ表示
Remove-Item -Path "C:\OldProject" -Recurse -Force -WhatIf

実行結果の例:

What if: 対象 "項目: C:\OldProject\src\main.py" に対して操作 "ファイルの削除" を実行します。
What if: 対象 "項目: C:\OldProject\src\utils.py" に対して操作 "ファイルの削除" を実行します。
What if: 対象 "項目: C:\OldProject\README.md" に対して操作 "ファイルの削除" を実行します。
...

-WhatIfはLinuxのrmにはない機能で、PowerShellの多くの変更系コマンド(Remove-ItemMove-ItemSet-Content等)で使えます。
特にスクリプトを本番環境で初めて実行するときに重宝します。

条件付きファイル移動——古いログの退避

「30日以上更新されていないログファイルをアーカイブフォルダに移す」という定型作業をPowerShellで自動化する例です。

Get-ChildItem -Path "C:\Logs" -Filter *.log |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
    Move-Item -Destination "C:\Logs\Archive\"

処理の流れ:

  1. Get-ChildItemでログファイルをオブジェクトとして取得
  2. Where-Objectで各オブジェクトのLastWriteTime(最終更新日時)プロパティを評価し、30日以上前のものだけ通す
  3. Move-Itemで条件を満たしたファイルをアーカイブフォルダへ移動

Linuxならfind /var/log -name "*.log" -mtime +30 -exec mv {} /archive/ \;で書くところです。
find-mtimeに相当する処理が、PowerShellではWhere-Object内でオブジェクトのプロパティ比較として表現されます。

事前にMove-ItemMove-Item -WhatIfに置き換えて確認してから、本実行に切り替えると安全です。

「ネットが繋がらない」の切り分け手順——判断ロジックつき

ネットワーク関連のコマンドを個別に紹介するのではなく、「ネットに繋がらない」という症状から原因を特定するまでの診断フローとして構成します。

コマンドそのものより、ステップ間の判断ロジックが重要です。

ステップ1:外部への疎通確認

Test-Connection -ComputerName 8.8.8.8 -Count 4

まずGoogle Public DNS(8.8.8.8)のIPアドレスに直接ping。

結果の判断:

  • 応答あり → インターネット接続は生きている。ステップ3(DNS)へ
  • 応答なし → ネットワーク自体に問題がある。ステップ2へ

ドメイン名(google.com)ではなくIPアドレスを指定するのは、DNS解決の問題と回線の問題を切り分けるためです。

ステップ2:ネットワークアダプターの状態確認

Get-NetAdapter | Select-Object Name, Status, LinkSpeed

結果の判断:

  • Status が “Disabled” → アダプターが無効化されている。Enable-NetAdapter -Name "Wi-Fi"で有効化
  • Status が “Disconnected” → 物理的な接続の問題。LANケーブルやWi-Fiの接続を確認
  • Status が “Up” なのにステップ1で疎通不可 → IPアドレス設定の問題の可能性。以下を確認
Get-NetIPAddress -AddressFamily IPv4 | Select-Object InterfaceAlias, IPAddress, PrefixLength

IPアドレスが169.254.x.x(APIPA)になっている場合、DHCPからアドレスを取得できていません。
ルーターの電源やDHCPサーバーの稼働状態を確認してください。

ステップ3:DNS解決の確認

ステップ1でIPアドレスへの疎通はあるのに、ブラウザで特定のサイトが開けない場合、DNSの問題を疑います。

Resolve-DnsName google.com

結果の判断:

  • 正常に解決される → DNS自体は動作している。ブラウザのキャッシュやプロキシ設定の問題
  • エラーが出る → DNSサーバーに到達できないか、設定が間違っている

DNSサーバーの設定を確認するには以下を実行します。

Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object InterfaceAlias, ServerAddresses

社内ネットワークで特定のDNSサーバーが指定されている場合、そのサーバーがダウンしているケースもあります。
一時的にGoogle Public DNS(8.8.8.8)に変更して疎通が復活するか確認することで、DNS原因かどうかを切り分けられます。

ステップ4:まとめて診断する

上記のステップを1つのスクリプトにまとめると、手順の抜け漏れなく素早く診断できます。

Write-Host "=== アダプター状態 ===" -ForegroundColor Cyan
Get-NetAdapter | Where-Object { $_.Status -eq "Up" } |
    Select-Object Name, Status, LinkSpeed | Format-Table

Write-Host "=== IPアドレス ===" -ForegroundColor Cyan
Get-NetIPAddress -AddressFamily IPv4 |
    Where-Object { $_.InterfaceAlias -notlike "Loopback*" } |
    Select-Object InterfaceAlias, IPAddress | Format-Table

Write-Host "=== DNS設定 ===" -ForegroundColor Cyan
Get-DnsClientServerAddress -AddressFamily IPv4 |
    Where-Object { $_.ServerAddresses.Count -gt 0 } |
    Select-Object InterfaceAlias, ServerAddresses | Format-Table

Write-Host "=== 外部疎通 ===" -ForegroundColor Cyan
$targets = @("8.8.8.8", "1.1.1.1", "google.com")
foreach ($target in $targets) {
    $result = Test-Connection -ComputerName $target -Count 1 -Quiet -ErrorAction SilentlyContinue
    [PSCustomObject]@{ Host = $target; Reachable = $result }
}

コマンドを個別に実行して目視で判断するより、スクリプトにまとめておく方が、焦っている場面(「今すぐ直してくれ」と言われている場面)では確実です。

パイプラインの実践——定番を超えた組み合わせ

パイプラインの基本構文(|でつなぐ)自体はLinuxと同じですが、オブジェクトベースであるぶん「何ができるか」の幅が異なります。

ここでは、入門記事でよく見るメモリソートやプロセス一覧ではなく、もう一歩踏み込んだ実用例を紹介します。

例1:特定フォルダ以下の巨大ファイルをCSVで出力する

ディスク容量の逼迫原因を調査するとき、100MB以上のファイルを探して一覧化する例です。

Get-ChildItem -Path C:\ -Recurse -File -ErrorAction SilentlyContinue |
    Where-Object { $_.Length -gt 100MB } |
    Sort-Object Length -Descending |
    Select-Object @{N='サイズ(MB)';E={[math]::Round($_.Length/1MB)}}, FullName, LastWriteTime |
    Export-Csv -Path "large_files.csv" -NoTypeInformation -Encoding UTF8

パイプの各段階で何が起きるか:

  1. Get-ChildItem -Recurse -File:Cドライブ全体からファイルオブジェクトを再帰取得(フォルダは除外)
  2. Where-Object:各オブジェクトのLengthプロパティが100MBを超えるものだけ通す
  3. Sort-Object:サイズの降順に並べ替え
  4. Select-Object:サイズをMB単位に変換しつつ、必要なプロパティだけ選択
  5. Export-Csv:結果をCSVファイルとして保存

-Encoding UTF8を指定している理由は後述の「5.1と7.xの違い」で解説します。

-ErrorAction SilentlyContinueを付けているのは、アクセス権限のないシステムフォルダでエラーが大量に出るためです。
この記事では初出のため説明しましたが、以降のコード例では説明なしで使用します。

例2:複数ホストの疎通状態を定期チェックしてログに残す

監視対象のサーバーやサービスが生きているかを定期的に確認し、結果をタイムスタンプ付きでログファイルに追記する例です。

$targets = @("192.168.1.1", "10.0.0.5", "google.com", "internal-app.local")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

$targets | ForEach-Object {
    $reachable = Test-Connection -ComputerName $_ -Count 1 -Quiet -ErrorAction SilentlyContinue
    [PSCustomObject]@{
        Timestamp = $timestamp
        Host      = $_
        Status    = if ($reachable) { "OK" } else { "NG" }
    }
} | Export-Csv -Path "connectivity_log.csv" -Append -NoTypeInformation -Encoding UTF8

このスクリプトをWindowsのタスクスケジューラで5分おきに実行すれば、簡易的な死活監視になります。

Linuxならping+cron+シェルスクリプトで組むところですが、PowerShellではオブジェクトをそのままCSVに出力できるため、テキスト整形の手間がありません。

例3:サービスの状態を一覧にして停止中のものだけ抽出する

Windows上で動いているサービスの中から、自動起動に設定されているのに停止しているもの(=異常の可能性があるもの)を抽出する例です。

Get-Service |
    Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" } |
    Select-Object Name, DisplayName, Status, StartType |
    Format-Table -AutoSize

実行結果の例:

Name                  DisplayName                          Status StartType
----                  -----------                          ------ ---------
wuauserv              Windows Update                      Stopped Automatic
Spooler               Print Spooler                       Stopped Automatic

Linuxではsystemctl list-units --state=failedで失敗したサービスを確認できますが、PowerShellではサービスのプロパティ(起動種別と現在の状態)を組み合わせて柔軟にフィルタリングできます。

スクリプト実行の壁——実行ポリシーという概念

パイプラインで組んだ処理を.ps1ファイルとして保存し、初めて実行しようとしたとき、ほぼ確実にぶつかる壁があります。

.\my_script.ps1 : このシステムではスクリプトの実行が無効になっているため、
ファイル C:\Users\username\my_script.ps1 を読み込むことができません。
詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。

Linuxでスクリプトを実行するときはchmod +x script.shで実行権限を付与します。
PowerShellにも「実行の許可」は存在しますが、ファイル単位ではなくシステム全体の実行ポリシー(ExecutionPolicy)で制御される点が根本的に異なります。

現在の実行ポリシーを確認する

Get-ExecutionPolicy

初期状態ではRestricted(すべてのスクリプト実行を禁止)が設定されています。

実行ポリシーを変更する

# 自分で作成したスクリプトは許可、ダウンロードしたスクリプトには署名を要求
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

-Scope CurrentUserを指定すると、管理者権限がなくても現在のユーザーだけに適用できます。

ポリシーの選択肢と判断基準:

ポリシー動作推奨度
Restrictedすべてのスクリプト実行を禁止(初期値)スクリプトを使わないなら安全
RemoteSignedローカル作成のスクリプトは許可、ダウンロードしたものは署名が必要実用上はこれが最適解
Unrestrictedすべて許可(ダウンロードファイルは警告あり)セキュリティリスクあり。非推奨
Bypass一切の制限なし、警告も出ない自動化パイプラインの一時的な用途以外では避ける

ネット上では「Set-ExecutionPolicy Unrestrictedで解決」という情報も見かけますが、出所不明のスクリプトも無警告で実行されるため、RemoteSignedで十分な場面がほとんどです。

Linuxのchmodとの考え方の違い

Linuxの実行権限はファイルごとに個別設定です。
PowerShellの実行ポリシーはシステム(またはユーザー)全体に対する一括設定であり、個々のスクリプトに対して個別に「実行許可/不許可」を切り替える仕組みではありません。

この違いは、Linuxに慣れた人ほど直感に反します。
「なぜファイルに権限を付けるのではなく、システム全体で制御するのか」と感じるのは自然な反応で、PowerShellのセキュリティモデルがWindowsのグループポリシーの思想に基づいているためです。

PowerShell 5.1と7.xの実用差分

記事中で何度か「PowerShell 5.1では」「7.x以降では」と触れましたが、ここで実用上の違いを整理します。

Windows 10/11に標準搭載されているのはPowerShell 5.1(Windows PowerShell)です。
PowerShell 7.x(PowerShell Core → PowerShell)は別途インストールが必要ですが、クロスプラットフォーム対応で機能も強化されています。

使用中のバージョンは$PSVersionTable.PSVersionで確認できます。

項目PowerShell 5.1PowerShell 7.x
OS対応WindowsのみWindows / macOS / Linux
&& / || 演算子使えない(;で代替)使える
>リダイレクトの文字コードUTF-16(BOM付き)UTF-8(BOM無し)
curlコマンドInvoke-WebRequestのエイリアス(本物のcurlと引数が違う)エイリアス削除済み。curl.exeが直接呼ばれる
wgetコマンドInvoke-WebRequestのエイリアスエイリアス削除済み
三項演算子使えない$x = $a ? "yes" : "no" が使える
null合体演算子使えない$x = $a ?? "default" が使える
null条件付き代入使えない$a ??= "default" が使える
パイプラインチェーン使えないGet-Process | ForEach-Object -Parallel { } で並列処理可能
エラー表示赤い長文(全文表示)簡潔なビュー($ErrorView = 'ConciseView'がデフォルト)

どちらを使うべきか

既存のWindows環境の管理スクリプトを書くだけなら、追加インストール不要の5.1で十分です。
ただし、以下のケースでは7.xの導入を検討する価値があります。

  • Linux/Macでも同じスクリプトを動かしたい
  • &&や三項演算子などモダンな構文を使いたい
  • curlwgetをLinuxと同じ感覚で使いたい(エイリアス衝突を回避したい)
  • UTF-8をデフォルトで使いたい(他環境とのファイルやり取りが多い場合)

5.1と7.xは共存可能です。
5.1はpowershell.exe、7.xはpwsh.exeで起動するため、用途に応じて使い分けられます。

cmd / Linux / Mac コマンド対応早見表(罠つき)

よく使う操作について、3環境の対応と「ハマりやすい罠」をまとめます。

操作cmdLinux / MacPowerShell罠・注意点
ディレクトリ一覧dirlsGet-ChildItemlsエイリアスは動くがオプション(-la等)は通らない
ディレクトリ移動cdcdSet-Locationcdエイリアスは動く。問題なし
現在地の表示cd(引数なし)pwdGet-Locationpwdエイリアスは動く
ファイルコピーcopycpCopy-Item-rは通らない。-Recurseを使う
ファイル移動movemvMove-Item同名ファイルが存在すると上書き確認なしで失敗(-Forceで上書き)
ファイル削除delrmRemove-Itemrm -rfに相当する-Recurse -Forceは確認なしで即削除。-WhatIfで事前確認を推奨
フォルダ作成mkdirmkdirNew-Item -ItemType Directorymkdirはエイリアスとして動くが、-p(親ディレクトリ自動作成)は通らない。PowerShellは親ディレクトリを自動作成する
ファイル内容表示typecatGet-Contentcatエイリアスは動くが、大きなファイルは全行メモリに読み込まれるため注意
文字列検索findstrgrepSelect-Stringgrepは存在しない。Select-String -Pattern "文字列" -Path fileで代替
プロセス一覧tasklistpsGet-Processpsエイリアスは動くが、出力がオブジェクト(Linuxとは形式が異なる)
プロセス停止taskkillkillStop-Processkillエイリアスは動く。-9のようなシグナル指定はなく、-Forceで強制停止
疎通確認pingpingTest-Connectionpingも動くが、Test-Connectionはオブジェクトを返すためパイプラインで加工しやすい
画面クリアclsclearClear-Hostclsclearともにエイリアスで動く
HTTP取得なしcurl / wgetInvoke-WebRequest5.1ではcurl/wgetがエイリアスで上書きされている。curl.exeと打てば本物のcurlを呼べる(Win10 1803以降)

エイリアスがあるコマンドは「入力は通るが、オプションの互換性はない」という点が共通の罠です。
ls -lacp -rrm -rfのようなオプション付きの書き方はそのままでは動作しないため、PowerShellのパラメータに読み替える必要があります。

まとめ

PowerShellとLinux/Macのシェルは、パイプラインの設計(テキスト vs オブジェクト)、オプション構文(短縮形 vs フルネーム)、コマンド名の文化(短縮 vs 動詞-名詞)という3つの根本的な違いがあります。

ファイル操作では-WhatIfによる事前確認がLinuxにはない安全装置として有効で、ネットワーク診断ではコマンドの知識よりもステップ間の判断ロジック(疎通NG→アダプター確認→DNS確認)の整理が実務では重要です。

スクリプト実行時に遭遇する実行ポリシーの壁は、Linuxのchmodとは根本的に異なるシステム全体の制御方式であり、RemoteSignedの設定で対処できます。

PowerShell 5.1と7.xには文字コードのデフォルト、&&演算子、curlエイリアスの有無など実用上の差異があり、他OS環境とファイルをやり取りする場面では7.xの方がトラブルが少なくなります。

エイリアスの存在で「入力は通るがオプションが通じない」という半端な互換がかえって混乱を生むため、移行初期は対応早見表を手元に置きつつ、Tab補完を活用してPowerShellの正式な構文に慣れていくのが最短ルートです。

参考情報源

タイトルとURLをコピーしました