[Windows] 파워셸 스크립팅: 기본

참고 문서

개요

파워셸에서 스크립트를 작성하고 사용하는 방법과 문법 등을 정리한 글.

파워셸 최신 버전 설치하기

iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI"

환경 변수

ℹ️ Windows Terminal은 새 탭이나 새 창을 열어도 환경 변수가 갱신되지 않으니 필요하면 앱을 재시작할 것

환경 변수 조회

# 모든 환경 변수 보기,
Get-ChildItem Env:

# 위와 같음
ls env:

# 환경 변수 중 'path' 출력,
Write-Output $env:path

# 위와 같음
echo $env:path

# 이렇게만 쳐도 됨 (암시적인 출력은 Write-Output이 처리함)
$env:path

명령어 설명은 여기에서.

로컬 환경 변수 추가/삭제

# 환경 변수 test 추가
$env:test = 1234

# 환경 변수 test2 추가
[Environment]::SetEnvironmentVariable("test2", "1234", "Process")

# 환경 변수 'test' 삭제
Remove-Item Env:\test

글로벌 환경 변수 추가/삭제

# 로그인한 사용자의 환경 변수로 'test' 추가
[Environment]::SetEnvironmentVariable("test", "1234", "User")

# 사용자 환경 변수 'test' 삭제
[Environment]::SetEnvironmentVariable("test", $null, "User")

# PATH 덧붙이기
[Environment]::SetEnvironmentVariable("PATH", "$env:PATH;원하는경로", "User")

# PATH 덧붙이기 #2
[Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";원하는경로", "User")

# 시스템 환경 변수로 'test2' 추가. (관리자 권한 필요)
[Environment]::SetEnvironmentVariable("test2", "1234", "Machine")

# 시스템 환경 변수 'test2' 삭제. (관리자 권한 필요)
[Environment]::SetEnvironmentVariable("test2", $null, "Machine")

스크립트 파일의 바로가기 만들기

아래는 Sound Switch 프로세스를 강제로 재시작하는 스크립트다:

# restart-soundswitch.ps1
Stop-Process -Name 'SoundSwitch'
Start-Process -FilePath 'C:\Program Files\SoundSwitch\SoundSwitch.exe'

이 스크립트를 실행하면 되는데, 문제는 파워셸 스크립트 파일은 터미널 환경이 아니면 직접 실행할 수 없다는 것. 그래서 배치 파일을 추가로 만들고 거기서 파워셸 스크립트를 실행한다:

# restart-soundswitch.bat
pwsh -executionpolicy remotesigned -File .\restart-soundswitch.ps1

이제 배치 파일의 바로가기를 만들어서 적절한 곳에 두면 끝. 혹시라도 pwsh가 안 되면 Powershell 혹은 Powershell.exe로 바꾸면 됨.

기본 문법

변수선언과 사용

$abc = 1234

$abc
# 1234

gv abc
# Name      Value
# ----      -----
# abc       1234

관련 Cmdlet은 저 밑에 링크에서 확인.

따옴표 "" ''

파워셸에서 작은따옴표''로 감싸진 문자열은 (큰따옴표""와 다르게) 문자 그대로 취급되며, 이 안에 포함된 변수나 표현식은 평가되지 않고 그대로 출력된다:

$i = 5
"The value of $i is $i."
# The value of 5 is 5.

'The value of $i is $i.'
# The value of $i is $i.

"The value of $(2+3) is 5."
# The value of 5 is 5.

'The value of $(2+3) is 5.'
# The value of $(2+3) is 5.

"$env:LOCALAPPDATA\abcd"
# C:\Users\fixalot\AppData\Local\abcd

'$env:LOCALAPPDATA\abcd'
# $env:LOCALAPPDATA\abcd

따옴표간 차이는 이 정도고, 나머지는 대체로 동일한 의미로 쓰인다.

중괄호 {}

TODO

파이프라인 입력 Pipeline Input

TODO

줄 바꿈

TODO

코멘트 처리

# 한 줄 코멘트
Get-ChildItem # 이것도 한 줄 코멘트
<#
   이것은 여러줄 코멘트
#>

연산자

블로그 내부 링크 | 파워셸 스크립팅: 연산자

명령어 Cmdlet

블로그 내부 링크 | 파워셸 스크립팅: 자주 사용하는 명령어

CmdletBinding()

TODO

제어문

if

if (<test1>)
  {<statement list 1>}
[elseif (<test2>)
  {<statement list 2>}]
[else
  {<statement list 3>}]

else if가 아니라 elseif인 점과 연산자를 제외하면 다른 언어와 크게 다르지 않음.

$condition = $true
if ($condition) {
  echo "Hello there!"
} else {
  echo "I'm Waldo."
}

if 조건식에서 사용하는 비교 연산자:

  • -eq: case-insensitive equality
  • -ieq: case-insensitive equality
  • -ceq: case-sensitive equality
  • -ne: case-insensitive not equal
  • -ine: case-insensitive not equal
  • -cne: case-sensitive not equal
  • -gt: greater than
  • -igt: greater than, case-insensitive
  • -cgt: greater than, case-sensitive
  • -ge: greater than or equal
  • -ige: greater than or equal, case-insensitive
  • -cge: greater than or equal, case-sensitive
  • -lt: less than
  • -ilt: less than, case-insensitive
  • -clt: less than, case-sensitive
  • -le: less than or equal
  • -ile: less than or equal, case-insensitive
  • -cle: less than or equal, case-sensitive
$con = 123
if (123 -eq $con) {
  echo 'Hi'
}

이 외에 라이크 검색, 정규식 논리 연산자 등이 있으니 여기를 볼 것.

switch

switch (<test-expression>)
{
    <result1-to-be-matched> {<action>}
    <result2-to-be-matched> {<action>}
}

switch [-regex | -wildcard | -exact] [-casesensitive] (<test-expression>)
{
    "string" | number | variable | { <value-scriptblock> } { <action-scriptblock> }
    default { <action-scriptblock> } # optional
}
switch ('a') {
  'a' { echo "It's a." }
  'b' { echo "It's b." }
  'c' { echo "It's c." }
}

break도 쓸 수 있는데, 파워셸에선 자바나 자바스크립트와 다르게 다음 실행문으로 이어지는 것을 막는게 아니라 switch를 중단하는 것을 의미한다. 웬 뜬금없이 중단이냐 하겠지만 파워셸의 switch는 파라미터의 수 만큼 반복한다.

아래를 실행해 보면:

switch (1, 3) {
  1 {"It is one."}
  2 {"It is two."}
  3 {"It is three."; break}
  4 {"It is four."}
  3 {"Three again."}
}

switch (3, 1) {
  1 {"It is one."}
  2 {"It is two."}
  3 {"It is three."; break}
  4 {"It is four."}
  3 {"Three again."}
}

첫 번째 switch는 "It is one. It is three." 둘 다 출력하지만 두 번째 switch는 "It is three."만 출력한다. break를 만나서 switch를 중단했기 때문이다.

함수 Functions

함수 정의

일단 생김새는 이렇다:

function Get-PSVersion {
  $PSVersionTable.PSVersion
}

함수 정의는 단순히 커맨드라인에서 함수 리터럴을 입력하면 된다. 하지만 이렇게 하면 현재 세션에만 유효한 함수가 되므로, 파워셸 프로파일에 작성하거나 스크립트 모듈로 등록한다.

프로파일은 일단 한 번 추가하면 $PROFILE 변수에서 파일 경로를 찾을 수 있음:

Write-Output $PROFILE
# C:\Users\fixalot\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

함수의 이름은 Cmdlet의 작명 규칙을 따르는게 좋다.

메모리에 함수가 로드됐는지는 'Function PSDrive'란 곳에서 볼 수 있다고 함:

# Function PSDrive에서 'Get-'으로 시작하고 'Version'으로 끝나는 함수 조회
Get-ChildItem -Path Function:\Get-*Version

# ABC 함수를 Function PSDrive에서 제거
Get-ChildItem -Path Function:\ABC | Remove-Item

파라미터

function Test-Fn {
  param (
    $Param1
  )
  echo $Param1
}

함수 블록 내에 param(a, b, c, ...)으로 파라미터를 정의한다. 단축 표기는 아래와 같다:

function Test-Fn($Param1) {
  echo $Param1
}

Test-Fn 123
# 123

Test-Fn -Param1 789
# 789

파라미터가 여러 개일 경우 다음처럼 정의하고:

function Test-Fn($Param1, $Param2) {
  Write-Host '$Param1:'$Param1
  Write-Host '$Param2:'$Param2
}

호출할 때는 파라미터 구분을 -파라미터이름으로 지정하거나, 아니면 아예 생략한 뒤 순서에 맞춰 공백으로 구분한다:

Test-Fn abc def
# $Param1: abc
# $Param2: def

Test-Fn -Param2 abc -Param1 def
# $Param1: def
# $Param2: abc

하나의 파라미터는 쉼표,로 구분된 값 여러 개를 전달할 수 있다. 이 때 해당 파라미터는 배열처럼 다룰 수 있다:

function Test-Fn($Param1) {
  Write-Host '$Param1:'$Param1
}

Test-Fn 1, 2, 3
# $Param1: 1 2 3

Test-Fn 1,2,3
# $Param1: 1 2 3

function Test-Fn2($Param1) {
  Write-Host '$Param1[0]:'$Param1[0]
  foreach ($ele in $Param1) {
    Write-Host '$ele:'$ele
  }
}

Test-Fn2 4, 5, 6
# $Param1[0]: 4
# $ele: 4
# $ele: 5
# $ele: 6

타입 제한

파라미터의 이름 앞에 대괄호[]로 감싸고 타입을 지정한다. 이렇게 하면 해당 타입이 아닌 값이 전달됐을 때 메시지와 함께 에러가 발생한다:

function Test-Fn3([string]$Param1) {}

Test-Fn3 123
Test-Fn3 a

function Test-Fn4([number]$Param1) {}

Test-Fn3 123
Test-Fn3 a
# InvalidOperation: Unable to find type [number].

function Test-Fn5([boolean]$Param1) {}

Test-Fn5 true
# Test-Fn5: Cannot process argument transformation on parameter 'Param1'. Cannot convert value "System.String" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

Test-Fn5 True
# Test-Fn5: Cannot process argument transformation on parameter 'Param1'. Cannot convert value "System.String" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

Test-Fn5 $True
Test-Fn5 $False

string 타입은 배열도 되는데 딱히 뭔가를 제한 기능은 음슴(왜냐면 길이 1짜리 배열도 있으니께):

function Test-Fn6([string[]]$Arr) {}

Test-Fn6 1
Test-Fn6 $True
Test-Fn6 a, b

필수 파라미터로 지정

Parameter(Mandatory) 혹은 [Parameter(Mandatory=$true)]로 해당 파라미터가 필수임을 지정한다:

# 단축 표기로 해도 되지만 가독성을 위해서 풀어서 씀
function Test-Fn7 {
  param (
    [Parameter(Mandatory)]
    [string]$Param1
  )
  Write-Host '$Param1:'$Param1
}

Test-Fn7
# cmdlet Test-Fn7 at command pipeline position 1
# Supply values for the following parameters:
# Param1:
# Test-Fn7: Cannot bind argument to parameter 'Param1' because it is an empty string.

필수 파라미터를 전달하지 않으면 이제라도 입력하라는 프롬프트가 뜬다. 이 때 ctrl + c하면 에러가 뙇.

Parameter(Mandatory)의 부수효과로 데이터 개수 제한이 있다. 위 예시에서 $Param1의 데이터 타입은 string인데 배열이 아니라서 딱 하나의 값만 허용한다:

Test-Fn7 -Param1 1
$Param1: 1

Test-Fn7 -Param1 1, 2, 3
# Test-Fn7: Cannot process argument transformation on parameter 'Param1'. Cannot convert value to type System.String.

기본값 지정

기본값은 ValidateNotNullOrEmpty()과 함께 등호=로 할당한다:

function Test-Fn9 {
  param (
    [ValidateNotNullOrEmpty()]
    [string]$Param1 = 'default value'
  )
  Write-Host '$Param1:'$Param1
}

Test-Fn9
# $Param1: default value

유효한 데이터 지정

특정 파라미터의 값으로 허용된 값만 받도록 제한하는 방법이다. ValidateSet()으로 지정한다:

function Test-Fn10 {
  param (
    [Parameter(Mandatory)]
    [ValidateSet('production', 'stage', 'test', 'vpn')]
    [string]$Server
  )
  Write-Host 'Chosen one:'$Server
}

Test-Fn10 -Server production
# Chosen one: production

Test-Fn10 -Server my-server
# Test-Fn10: Cannot validate argument on parameter 'Server'. The argument "my-server" does not belong to the set "production,stage,test,vpn" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.

이렇게 지정한 값은 탭 키를 눌러 자동완성이 된다:

아래 예시는 ValidateSet()와 해시테이블을 응용해서 ssh로 서버에 접속할 때 쓰는 함수다:

$hashtable = @{
  production = '1.2.3.4';
  stage = '2.3.4.5';
  test = '3.4.5.6';
  vpn = '4.5.6.7'
}

function Connect-RemoteServer {
  param (
    [Parameter(Mandatory)]
    [ValidateSet('production', 'stage', 'test', 'vpn')]
    [string]$Server
  )
  $target = $hashtable.$Server;
  echo $target
  ssh "ubuntu@$target"
}

Connect-RemoteServer -Server vpn

@hashtable.keysValidateSet()에서 받아주면 좋겠는데 에러가 남:

function Connect-RemoteServer {
  param (
    [ValidateSet($hashtable.keys)]
    # ... 생략
  )
}

ParserError: C:\Users\fixalot\Documents\PowerShell\Microsoft.PowerShell_profile.ps1:24
Line |
  24 |      [ValidateSet($hashtable.keys)]
     |                   ~~~~~~~~~~~~~~~
     | Attribute argument must be a constant or a script block.

데이터 타입

해시 테이블

$hash = [ordered]@{ Number = 1; Shape = "Square"; Color = "Blue"}

$hash
# Name                           Value
# ----                           -----
# Number                         1
# Color                          Blue
# Shape                          Square

$hash.keys
# Number
# Shape
# Color

$hash.values
# 1
# Square
# Blue

$hash.Number
# 1

TODO

작성자 저장용 스크립트

자주 쓰는 거 저장해 둠.

Set-Alias ex explorer
Set-Alias sb 'C:\Program Files\Sublime Text\subl.exe'
Set-Alias dk 'docker'
Set-Alias -Name grep -Value findstr
Set-Alias -Name-Value git

########################

function Get-Child-Item-Force { 
  Get-ChildItem -Force 
}
Set-Alias -Name ll -Value Get-Child-Item-Force

function Remove-Item-Recurse-Force { 
  Remove-Item -Recurse -Force @args
}
Set-Alias -Name rmrf -Value Remove-Item-Recurse-Force

########################

$RemoteIp = @{ 
  production = '1.2.3.4';
  stage = '1.2.3.4';
  dev = '1.2.3.4';
  'dev-old' = '1.2.3.4';
}

function Connect-RemoteServer {
  param (
    [Parameter(Mandatory)]
    [ValidateSet('production', 'stage', 'dev', 'dev-old')]
    [string]$Server
  )

  $target = $RemoteIp.$Server;

  switch ($Server) {
    'production' { $keyfile = "C:\dev\ssh-keys\infra_production.pem" }
    'stage' { $keyfile = "C:\dev\ssh-keys\infra_stage.pem" }
    default { $keyfile = "C:\dev\ssh-keys\default.pem" }
  }

  # if ('production' -eq $Server) {
  #   $keyfile = "C:\dev\ssh-keys\infra_production.pem"
  # } elseif ('stage' -eq $Server) {
  #   $keyfile = "C:\dev\ssh-keys\infra_stage.pem"
  # }

  ssh -i $keyfile "ubuntu@$target"
}

Set-Alias -Name sshrs -Value Connect-RemoteServer