สคริปต์ Windows มีภาษาไทยแล้วพังด้วย error หลอก ทั้งที่ syntax ถูก
error ที่ชี้ไปที่วงเล็บปีกกาทำให้เราไล่นับวงเล็บ ทั้งที่ของที่ผิดคือไบต์ที่มองไม่เห็นบนจอ ตัวอักษรไทยหนึ่งตัวในไฟล์ถูกตัวอ่านคนละตัวแปลออกมาไม่เหมือนกัน และบางตัวแปลออกมาเป็นเครื่องหมายที่ parser ถือว่ามีความหมาย
สรุปใน 1 นาที
- อาการ/ปัญหา
- สคริปต์ Windows ที่มีภาษาไทยพังด้วย error หลอก ทั้งที่ syntax ถูก
- ใครควรอ่าน
- คนเขียน .ps1 .cmd หรือ curl ที่มีข้อความไทยบน Windows
- ทางแก้แบบสั้น
- ต้นเหตุคือตัวอ่านแต่ละตัวตีความไบต์คนละตาราง ตรวจ encoding ของไฟล์ก่อนไล่วงเล็บ บทความมี 5 เคสพร้อมวิธีพิสูจน์และทางแก้
พอดีผมเขียนสคริปต์ PowerShell ไว้ตั้งค่าการ์ด LAN ของเครื่องที่หน้างาน ใส่ข้อความไทยใน Write-Host ให้คนรันอ่านรู้เรื่อง ตรวจแล้วผ่าน พอเอาไปดับเบิลคลิกรันจริงได้ Missing closing '}' in statement block ชี้ไปที่บรรทัดที่ไม่มีอะไรผิดเลย ผมไล่นับวงเล็บปีกกาอยู่นาน แก้ syntax ไปหลายรอบ สุดท้ายต้นเหตุไม่ได้อยู่ในตัวอักษรที่ผมเห็นบนจอ แต่อยู่ที่ไฟล์ถูกเซฟเป็น UTF-8 แบบไม่มี BOM
หลังจากนั้นผมย้อนดูบันทึกงานเก่า พบว่าเคยโดนเรื่องตระกูลเดียวกันมาแล้วอีก 4 แบบ คือไฟล์ .cmd การส่ง JSON ด้วย curl คำสั่ง adb และ print ของ Python ทุกเคสอาการเหมือนกันหมด คือ error ชี้ไปที่ syntax หรือที่ตัวโปรแกรม ทั้งที่ต้นเหตุคือ encoding เลยรวมไว้ที่เดียว ชื่อไฟล์ ปลายทาง และ path ในบทความนี้ผมเปลี่ยนเป็นค่าสมมุติทั้งหมด ส่วนข้อความ error คือของจริง
เรื่องนี้เป็นคนละชั้นกับ path แบบ Windows ที่ไปค้างบนเซิร์ฟเวอร์ และคนละชั้นกับ ข้อมูลไทยที่เพี้ยนในฐานข้อมูล ตรงนี้คือ encoding ของตัวไฟล์สคริปต์กับ console ฝั่ง Windows ล้วน ๆ
อาการ: Missing closing ในบรรทัดที่ไม่มีอะไรผิด
สคริปต์หน้าตาประมาณนี้ ไม่มีอะไรซับซ้อน
# set-lan.ps1 - เซฟเป็น UTF-8 ไม่มี BOM (ค่าเริ่มต้นของ editor และเครื่องมือ AI ส่วนใหญ่)
param([string]$Adapter = 'Ethernet')
if (-not (Get-NetAdapter -Name $Adapter -ErrorAction SilentlyContinue)) {
Write-Host "ไม่พบการ์ด LAN ชื่อ $Adapter"
exit 1
}
Write-Host "กำลังตั้งค่าการ์ด LAN..."
New-NetIPAddress -InterfaceAlias $Adapter -IPAddress 10.10.10.50 -PrefixLength 24
รันจาก console ปกติของเครื่อง หรือคลิกขวาสั่ง Run ได้ข้อความนี้
PS C:\work> .\set-lan.ps1
At C:\work\set-lan.ps1:4 char:...
+ if (-not (Get-NetAdapter -Name $Adapter -ErrorAction SilentlyContinue)) {
Missing closing '}' in statement block or type definition.
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : MissingEndCurlyBrace
ผมเดาผิดไปสองรอบ รอบแรกคิดว่าวงเล็บไม่ครบจริง นั่งไล่นับคู่ ย้ายบล็อก if ไปมา รอบสองคิดว่าเป็นเรื่อง PowerShell 5.1 ไม่รับ syntax ใหม่บางตัว เลยเขียนใหม่ให้เก่าลง ไม่หายทั้งสองรอบ
ที่ทำให้หลงนานคือผมตรวจสคริปต์ด้วย pwsh ซึ่งเป็น PowerShell 7 และมันผ่านทุกครั้ง แต่เครื่องที่ใช้งานจริงเปิดไฟล์ .ps1 ด้วย Windows PowerShell 5.1 ผมตรวจถูกไฟล์ แต่ตรวจผิดตัวอ่าน
PowerShell 7 ถือว่าไฟล์ที่ไม่มี BOM เป็น UTF-8 ส่วน Windows PowerShell 5.1 ถือว่าเป็น ANSI ตาม codepage ของเครื่อง ไฟล์เดียวกันสองตัวนี้เห็นตัวอักษรไม่เหมือนกัน ถ้าปลายทางรันด้วย powershell.exe ตัวตรวจก็ต้องเป็น powershell.exe ตัวเดียวกัน
ต้นเหตุ: ไบต์ชุดเดียวกัน อ่านคนละตาราง
อักษรไทยหนึ่งตัวใน UTF-8 ใช้ 3 ไบต์ ตัว ด คือ E0 B8 94 พอ PowerShell 5.1 ไม่เห็น BOM มันอ่านไฟล์เป็น ANSI ซึ่งบน Windows ภาษาไทยคือ CP874 ตารางนี้เป็นแบบหนึ่งไบต์ต่อหนึ่งตัวอักษร สามไบต์เลยกลายเป็นสามตัว คือ เ ธ และไบต์ 0x94 ซึ่งใน CP874 คือเครื่องหมายคำพูดโค้งปิด
PowerShell ยอมรับเครื่องหมายคำพูดโค้งเป็นตัวเปิดปิด string เทียบเท่าเครื่องหมายคำพูดตรง พอเจอ ” กลาง "กำลังตั้งค่าการ์ด LAN..." string เลยปิดตรงตัว ด ที่เหลือของบรรทัดถูกอ่านเป็นโค้ด และเครื่องหมายคำพูดตรงตัวสุดท้ายกลายเป็นตัวเปิด string ใหม่ที่ลากยาวกลืนบรรทัดถัดไปทั้งหมด รวมถึงวงเล็บปีกกาปิดด้วย parser เลยรายงานว่าบล็อกไม่ปิด และชี้ไปที่ตัวเปิดบล็อกซึ่งอยู่คนละบรรทัดกับของที่ผิด
พิสูจน์ได้จาก console โดยไม่ต้องพิมพ์ภาษาไทยเลย
# ไบต์ UTF-8 ของ ด (U+0E14)
[System.Text.Encoding]::UTF8.GetBytes([string][char]0x0E14) | ForEach-Object { '{0:X2}' -f $_ }
# E0
# B8
# 94
# เอาสามไบต์เดิมมาอ่านด้วย CP874 แล้วดู code point ที่ได้
[System.Text.Encoding]::GetEncoding(874).GetString([byte[]](0xE0, 0xB8, 0x94)).ToCharArray() |
ForEach-Object { 'U+{0:X4}' -f [int]$_ }
# U+0E40
# U+0E18
# U+201D
U+201D คือเครื่องหมายคำพูดโค้งปิด ข้อความไทยที่ไม่มี ด หรือ ณ อยู่ใน string จะไม่เจออาการนี้ สคริปต์บางไฟล์เลยรันได้ บางไฟล์พัง ทั้งที่เซฟแบบเดียวกัน ซึ่งทำให้ยิ่งไม่สงสัยเรื่อง encoding
แก้ .ps1: ใส่ BOM แล้วตรวจด้วยตัวอ่านตัวจริง
ทางแก้คือบรรทัดเดียว อ่านไฟล์เข้ามาแล้วเขียนกลับเป็น UTF-8 แบบมี BOM
$p = 'C:\work\set-lan.ps1'
[System.IO.File]::WriteAllText($p, [System.IO.File]::ReadAllText($p), [System.Text.UTF8Encoding]::new($true))
# ยืนยันสามไบต์แรก ต้องได้ EF BB BF
[System.IO.File]::ReadAllBytes($p)[0..2] | ForEach-Object { '{0:X2}' -f $_ }
แล้วตรวจ parse ด้วย powershell.exe ไม่ใช่ pwsh ผมทำไฟล์ตรวจไว้ตัวหนึ่ง เนื้อไฟล์เป็น ASCII ล้วนเพื่อไม่ให้ตัวตรวจติดปัญหาเดียวกันเสียเอง
# parsecheck.ps1 - ASCII only
param([Parameter(Mandatory = $true)][string]$Path)
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) | Out-Null
foreach ($e in $errors) {
'[ERR] {0}:{1} {2}' -f $e.Extent.StartLineNumber, $e.Extent.StartColumnNumber, $e.Message
}
if ($errors.Count -gt 0) { exit 1 }
'[OK] parse clean: ' + $Path
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\parsecheck.ps1 -Path C:\work\set-lan.ps1
ParseFile แค่แยกโครงสร้าง ไม่ได้รันสคริปต์ เลยใช้กับสคริปต์ที่แตะเครือข่ายหรือแตะระบบได้โดยไม่มีผลข้างเคียง
- เซฟไฟล์
.ps1ที่มีอักขระนอก ASCII เป็น UTF-8 แบบมี BOM ทุกครั้ง ถ้าเครื่องมือที่ใช้เขียนไฟล์ไม่มีตัวเลือกนี้ ให้แปลงด้วยบรรทัดWriteAllTextทันทีหลังเขียน - ตรวจสามไบต์แรกของไฟล์ว่าเป็น
EF BB BF - รัน
parsecheck.ps1ผ่านpowershell.exe -NoProfileไม่ใช่pwsh - ถ้าสคริปต์ต้องส่งให้คนอื่นดับเบิลคลิก ให้ทดสอบด้วยวิธีเดียวกับที่เขาจะเปิดจริงอีกหนึ่งรอบ
ระหว่างแก้เรื่องนี้ผมเจอของแถมอีกจุดที่เป็นความต่างของ 5.1 กับ 7 จริง ๆ ไม่เกี่ยวกับ encoding คือ 5.1 ไม่รับการขึ้นบรรทัดใหม่กลาง expression แล้วต่อด้วยการเรียก method
# 5.1 ไม่รับ: ขึ้นบรรทัดใหม่แล้วต่อด้วย ).IsInRole(...)
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
# เขียนแยกตัวแปร รับได้ทั้งสองรุ่น
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$id
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
เคส .cmd: comment ภาษาไทยถูกรันเป็นคำสั่ง
เคสนี้เกิดกับไฟล์ deploy ของเว็บหน้าเดียว เป็น .cmd ที่มี REM กับ echo เป็นภาษาไทย รันบน Windows ภาษาไทยซึ่ง console ใช้ CP874
@echo off
REM ตั้งค่าปลายทาง
set HOST=example.com
set PORT=22
set KEY=C:\keys\deploy.key
REM ลบไฟล์เก่าก่อน
ssh -i "%KEY%" -p %PORT% webuser@%HOST% "rm -f /home/webuser/tmp/*.zip"
echo อัปโหลดเสร็จแล้ว
ได้ error สามแบบในการรันรอบเดียว
'ฟล์เก่าก่อน' is not recognized as an internal or external command,
operable program or batch file.
Bad port 'rm -f /home/webuser/tmp/*.zip'
บรรทัดแรกบอกว่าครึ่งหลังของ comment ถูกเอาไปรันเป็นคำสั่ง บรรทัดที่สองบอกว่าตัวแปร PORT ว่าง อาร์กิวเมนต์ของ ssh เลยเลื่อนมาหนึ่งช่อง คำสั่งระยะไกลถูกอ่านเป็นเลขพอร์ต และเมื่อ echo คำสั่งออกมาดูจะเห็น ssh -i "" -p ตามด้วยค่าว่าง แปลว่าบรรทัด set หลายบรรทัดถูกกลืนหายไปด้วย
cmd.exe อ่านไฟล์ batch เป็นไบต์ตาม codepage ของ console ไม่ได้อ่านเป็น UTF-8 พอเจอไบต์ของอักษรไทยแบบหลายไบต์ มันตัดบรรทัดผิดที่ ผมไม่ได้ไล่ลงไปถึงระดับว่าไบต์ไหนทำให้ขอบบรรทัดเลื่อน สิ่งที่ยืนยันได้คือผลลัพธ์ บรรทัด comment ถูกผ่าครึ่ง และบรรทัด set ที่อยู่ติดกันไม่ถูกทำงาน
เดาผิดของเคสนี้คือใส่ chcp 65001 ไว้ต้นไฟล์ ไม่ช่วย เพราะ cmd parse ไฟล์ไปก่อนที่คำสั่งเปลี่ยน codepage จะมีผล
chcp 65001 ต้นไฟล์
เปลี่ยน codepage ของ console หลังจากไฟล์ถูกอ่านไปแล้ว บรรทัดที่พังยังพังเหมือนเดิม และอาการขึ้นกับว่าข้อความไทยตัวไหนอยู่ตรงไหน เลยดูเหมือนแก้ได้บ้างไม่ได้บ้าง
เนื้อไฟล์ ASCII ล้วน
REM และ echo เป็นภาษาอังกฤษทั้งหมด ไม่มีไบต์เกิน 127 ในไฟล์ ภาษาไทยใน path ของไฟล์ไม่เป็นปัญหา ปัญหาอยู่ที่เนื้อไฟล์เท่านั้น
path ที่มีภาษาไทยเรียกได้ตามปกติ ขอแค่ครอบด้วยเครื่องหมายคำพูดและเรียกผ่าน cmd.exe /c
& cmd.exe /c "C:\work\งานเว็บ\deploy.cmd"
ตัวตรวจที่ผมผูกไว้ก่อนรันไฟล์ batch ทุกตัวคือหาไบต์ที่เกิน ASCII ถ้าเจอให้หยุด
# รันจาก Git Bash - เจอไบต์นอก ASCII ให้ถือว่าไฟล์ใช้ไม่ได้
if LC_ALL=C grep -nP '[^\x00-\x7F]' deploy.cmd; then
echo "[ERR] deploy.cmd has non-ASCII bytes"; exit 1
fi
echo "[OK] deploy.cmd is ASCII only"
เคส curl และ adb: ข้อความไทยหายระหว่างทาง
เคสที่สามเกิดตอนทดสอบ API ที่รับข้อความไทย ผมส่ง JSON ด้วย curl --data แบบพิมพ์ข้อความลงในบรรทัดคำสั่งจาก Git Bash ปลายทางได้เครื่องหมาย ? ทั้งข้อความ ผมเปิดโค้ดฝั่งเซิร์ฟเวอร์หาบั๊กเรื่อง charset อยู่พักหนึ่ง ก่อนจะสังเกตว่าส่งจากเบราว์เซอร์จริงไม่มีปัญหาเลย
ข้อความถูกแปลงผ่าน codepage ของ console ก่อนจะถึงมือ curl ตัวอักษรที่แปลงไม่ได้เลยกลายเป็น ? ตั้งแต่ยังไม่ออกจากเครื่อง มันเป็นบั๊กของวิธีทดสอบ ไม่ใช่ของแอป ทางแก้คือไม่ให้ข้อความไทยผ่านบรรทัดคำสั่ง เขียนลงไฟล์ UTF-8 แล้วให้ curl อ่านจากไฟล์
# payload.json เซฟเป็น UTF-8 จาก editor ไม่ได้สร้างด้วย echo ใน shell
curl -sS -X POST https://example.com/api/message \
-H 'Content-Type: application/json; charset=utf-8' \
-H 'X-Token: <TOKEN>' \
--data-binary @payload.json
# กรณีเป็นฟอร์ม ให้ curl อ่านค่าจากไฟล์แล้ว urlencode เอง
curl -sS -X POST https://example.com/comment --data-urlencode [email protected]
เคสที่สี่คือ adb ตอนทำ UI test บนเครื่อง Android 13 สั่งพิมพ์ข้อความไทยลงช่องกรอก
$ adb shell input text 'ทดสอบ'
java.lang.NullPointerException: Attempt to get length of null array
ข้อความ error ไม่มีคำว่า encoding หรือ character เลย คำสั่ง input text แปลงข้อความเป็นลำดับการกดปุ่มผ่าน KeyCharacterMap.getEvents ซึ่งคืน null กับอักษรที่ไม่ใช่ละติน โค้ดถัดไปวนอ่านความยาวของ array ที่เป็น null เลยโยน exception ออกมาแทนที่จะบอกว่าพิมพ์อักษรนี้ไม่ได้ ทางเลี่ยงที่ผมใช้คือให้ test กรอกค่า placeholder ที่เป็น ASCII และถ้าต้องดูการแสดงผลภาษาไทยจริงก็พิมพ์จากตัวเครื่อง
ทั้ง curl และ adb ไม่ได้ทำอะไรผิดกับข้อมูลที่มันได้รับ ข้อความเสียไปตั้งแต่ชั้นที่ส่งต่อให้มัน เวลาเจอข้อความไทยเพี้ยนในการทดสอบ ให้ถามก่อนว่าเบราว์เซอร์หรือแอปจริงเจอด้วยไหม ถ้าไม่เจอ ให้สงสัยเครื่องมือทดสอบก่อนสงสัยแอป
เคส Python: ตั้ง encoding ในสคริปต์สายเกินไป
เคสสุดท้ายคือ print ข้อความแล้วล้มด้วย UnicodeEncodeError ที่อ้างถึง codec cp874 ผมแก้ด้วยการใส่ os.environ['PYTHONIOENCODING'] = 'utf-8' ไว้บรรทัดแรกของสคริปต์ ไม่หาย เพราะตัวแปลภาษาเปิด stdout พร้อม encoding ไปตั้งแต่ก่อนรันบรรทัดแรกของเราแล้ว ตัวแปร environment ต้องมีค่าตั้งแต่ตอนเรียก python
# ตั้งตอนเรียก ไม่ใช่ตั้งในสคริปต์
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 python report.py
# ทางที่ไม่ขึ้นกับ console เลย: เขียนผลลงไฟล์ UTF-8 แล้วเปิดอ่าน
PYTHONUTF8=1 python report.py > out.txt
# ในสคริปต์ ให้ระบุ encoding ทุกครั้งที่เปิดไฟล์ อย่าพึ่งค่าเริ่มต้นของเครื่อง
with open('out.txt', 'w', encoding='utf-8') as f:
f.write(text)
ถ้าเรียกจาก PowerShell ให้ตั้ง $env:PYTHONUTF8 = '1' ก่อนเรียก ส่วนตัวผมเลือกทางเขียนลงไฟล์เป็นหลัก เพราะผลที่ได้เหมือนกันทุกเครื่อง ไม่ขึ้นกับว่า console ตัวไหนเปิดอยู่
ตารางสรุป: ตัวอ่านแต่ละตัวเห็นอะไร
| ตัวอ่าน | ค่าเริ่มต้นเมื่อไม่มี BOM หรือไม่ระบุ | อาการ | ทางแก้ |
|---|---|---|---|
| Windows PowerShell 5.1 | ANSI ตาม codepage เครื่อง (CP874) | Missing closing '}' ในบรรทัดที่ไม่ผิด |
เซฟ UTF-8 มี BOM แล้ว parse-check ด้วย powershell.exe |
| pwsh 7 | UTF-8 | ไม่มีอาการ ทำให้ตรวจผิดตัว | อย่าใช้เป็นตัวตรวจแทน 5.1 |
| cmd.exe | ไบต์ตาม codepage ของ console | comment ถูกรัน ตัวแปร set ว่าง |
เนื้อไฟล์ ASCII ล้วน chcp 65001 ไม่ช่วย |
| Git Bash ส่งต่อให้ curl | แปลงผ่าน codepage ของ console | ปลายทางได้ ? |
--data-binary @file หรือ --data-urlencode name@file |
| adb shell input text | รับเฉพาะอักษรที่มีใน key map | NullPointerException |
ใช้ placeholder ASCII หรือพิมพ์จากเครื่อง |
| python | encoding ของ stdout ตาม console | UnicodeEncodeError cp874 |
ตั้ง PYTHONUTF8=1 ตอนเรียก หรือเขียนลงไฟล์ UTF-8 |
กติกาที่ผมใช้อยู่ตอนนี้
สรุปคือทุกเคสมีรูปแบบเดียวกัน ข้อความไทยเดินทางผ่านตัวกลางที่ตีความไบต์ตามตารางของตัวเอง แล้ว error ไปโผล่ในชั้นถัดไปซึ่งไม่รู้เรื่อง encoding เลย ข้อความ error เลยพูดถึงวงเล็บ พอร์ต หรือ null array แทน
ถ้า error ชี้ไปที่บรรทัดที่อ่านแล้วไม่ผิด และไฟล์หรือคำสั่งนั้นมีอักขระนอก ASCII อยู่ ให้ตรวจ encoding ก่อนอย่างอื่น ถามสามข้อ ไฟล์นี้เซฟเป็นอะไร ตัวที่อ่านมันถือว่าเป็นอะไร และผมตรวจด้วยตัวอ่านตัวเดียวกับที่ใช้รันจริงหรือเปล่า
กติกาที่ผมยึดตอนนี้มีสามข้อ ข้อแรก ไฟล์ .ps1 ที่มีภาษาไทยต้องมี BOM และต้องผ่าน parse-check ด้วย powershell.exe ข้อสอง ไฟล์ .cmd และ .bat เป็น ASCII ล้วน ไม่มีข้อยกเว้น ข้อสาม ข้อความไทยที่ต้องส่งให้โปรแกรมอื่นให้ส่งผ่านไฟล์ UTF-8 ไม่ส่งผ่านบรรทัดคำสั่ง ใครจะยังอยากใส่ข้อความไทยใน batch ก็ตามสะดวก แต่ส่วนตัวผมเลิกแล้ว เพราะเวลาที่เสียไปกับการไล่ error หลอกมากกว่าเวลาที่ประหยัดได้จากการอ่าน comment ภาษาไทยหลายเท่า ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย