คำสั่งอะไรต่างๆหลากหลายในระดับสูงในคอมมักจะต้องทำโดยผ่านคอมมานด์ไลน์
เวลาที่เขียนโปรแกรมภาษาไพธอนงานบางอย่างที่ไม่ได้อยู่ในขอบเขตของคำสั่งที่มีอยู่แล้วในไพธอนก็อาจจำเป็นต้องใช้การสั่งงานในคอมมานด์ไลน์
ไพธอนสามารถควบคุมคอมมานด์ไลน์ได้ผ่านหลายวิธี หนึ่งในวิธีที่นิยมใช้ก็คือใช้มอดูล subprocess
ซึ่งเป็นมอดูลที่มีติดตัวอยู่แล้วแต่แรกในไพธอน ไม่ต้องติดตั้งเพิ่มเติม
นอกจากนี้ยังมีวิธีเช่นใช้ฟังก์ชันในมอดูล os เช่นฟังก์ชัน os.system() หรือ os.popen() ซึ่งเมื่อก่อนอาจนิยมใช้
แต่ในปัจจุบันส่วนใหญ่คนมักจะแนะนำให้ใช้ subprocess แทนมากกว่า
ในบทความนี้จะอธิบายวิธีการใช้มอดูล subprocess เพื่อสั่งคำสั่งในคอมมานด์ไลน์
อนึ่งคอมมานด์ไลน์ใน mac หรือ linux จะเป็นเชลยูนิกซ์ ในขณะที่วินโดวส์เป็นคอมมานด์พร้อมปต์ (รายละเอียดอ่านใน
https://phyblas.hinaboshi.com/20190124)
ไม่ว่าจะแบบไหนก็สามารถใช้ subprocess ควบคุมได้เหมือนกัน ต่างกันที่คำสั่งต่างๆในเชล
บทความนี้จะยกตัวอย่างโดยใช้เชลลูนิกซ์ซึ่งใช้ใน mac หรือ linux เป็นหลัก
เพราะโดยทั่วไปแล้วมีโอกาสได้ใช้งานมากกว่าในวินโดวน์มาก
การใช้คำสั่ง run
ภายในมอดูล subprocess ประกอบไปด้วยฟังก์ชันหลายตัวที่ใช้ควบคุมคอมมานด์ไลน์ ตัวที่เป็นพื้นฐานที่สุดก็คือ
subprocess.run
การใช้ subprocess.run นั้นโดยพื้นฐานง่ายที่สุดก็คือ ถ้าเป็นคำสั่งเดี่ยวๆ ต้องการสั่งอะไรก็ใส่คำสั่งลงไปเลย เช่น
import subprocess
subprocess.run('pwd')
แล้วก็จะได้ผลการรันคำสั่งออกมา เช่นคำสั่ง pwd นี้จะให้ค้าออกมาเป็นตำแหน่งโฟลเดอร์ที่อยู่ปัจจุบัน เช่น
/home/phyblas
หากคำสั่งมีส่วนที่ต้องเว้นวรรค เช่นมีตัวเลือกเสริมหรือชื่อไฟล์ประกอบ ไม่ได้มีแค่คำสั่งเดี่ยวๆ เช่นแบบนี้ จะใส่ไปทั้งอย่างนั้นไม่ได้
subprocess.run('python -V') # ได้ FileNotFoundError: [Errno 2] No such file or directory: 'python -V': 'python -V'
ในตัวอย่างนี้เป็นการใช้คำสั่ง python ซึ่งใช้สำหรับรันไพธอนในคอมมานด์ไลน์เป็นตัวอย่าง (รายละเอียดการใช้อ่านใน
https://phyblas.hinaboshi.com/20190705) โดยเติมตัวเลือกเสริม -V เป็นการแสดงเวอร์ชันของไพธอนที่ใช้อยู่ปัจจุบัน
กรณีที่มีตัวเลือกเสริมแบบนี้จะต้องใส่เป็นลิสต์โดยแยกส่วนของแต่ละส่วนออกจากกันแทน แบบนี้
subprocess.run(['python','-V'])
ได้
Python 3.8.1
แต่หากต้องการจะใส่ติดกันไปเลยเหมือนเวลาที่พิมพ์ในคอมมานด์ไลน์จริงๆให้ใส่คีย์เวิร์ด shell=True ลงไป
subprocess.run('python -V',shell=True)
เมื่อใส่ shell=True ไปจะทำให้พิมพ์คำสั่งเหมือนเวลาเวลาอยู่ในคอมมานด์ไลน์โดยตรงเลย คือจะมีการแบ่งช่องว่างเว้นวรรคเป็นตัวแยก
หรือจะใช้ .split() เพื่อให้แยกสายอักขระเป็นลิสต์โดยเว้นตามช่องว่างให้เองก็ได้เช่นกัน
subprocess.run('python -V'.split())
ออบเจ็กต์ CompletedProcess ที่ได้จากการรันเสร็จ
ค่าคืนกลับที่ได้จาก subprocess.run คือออบเจ็กต์ CompletedProcess ซึ่งจะเก็บค่าอาร์กิวเมนต์ต่างๆที่เราใช้รัน (args)
และโค้ดคืนกลับ (returncode) สามารถเก็บใส่ตัวแปรไว้แล้วเอาไว้มาดูค่าได้
p = subprocess.run('python -V',shell=True)
print(p) # ได้ CompletedProcess(args=['python', '-V'], returncode=0)
print(p.args) # ได้ ['python', '-V']
print(p.returncode) # ได้ 0
args ก็คือคำสั่งที่เราป้อนเข้าไป
returncode คือค่าที่แสดงผลว่ารันแล้วมีข้อผิดพลาดหรือไม่ หากไม่มีข้อผิดพลาดก็จะได้ 0
แต่หากเกิดข้อผิดพลาดก็จะได้รหัสของข้อผิดพลาดต่างกันไป
เช่นลองใส่ตัวเลือกเสริมที่ไม่มีอยู่จริงในคำสั่งนั้นเข้าไป
p = subprocess.run('python -j',shell=True)
ก็จะเกิดข้อผิดพลาด ขึ้นมาว่า
Unknown option: -j
usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
Try `python -h' for more information.
จากนั้นถ้าลองมาดูค่าของ CompletedProcess ที่ได้มาก็จะได้ returncode เป็น 2 แบบนี้เป็นต้น
print(p) # ได้ CompletedProcess(args='python -j', returncode=2)
การทำให้เกิดข้อผิดพลาดตามเมื่อมีข้อผิดพลาดในคำสั่ง
จากตัวอย่างที่แล้วจะเห็นว่าแม้จะเกิดข้อผิดพลาดขึ้นในโค้ดของคอมมานด์ไลน์แบบนี้ก็ตาม
ก็จะไม่ทำให้เกิดข้อผิดพลาดขึ้นมาจริงๆในโปรแกรมไพธอน
หากต้องการให้ข้อผิดพลาดนั้นกลายมาเป็นข้อผิดพลาดในไพธอน เพื่อให้โปรแกรมชะงักลงไปด้วยให้ใส่คีย์เวิร์ด
check=True
subprocess.run('python -j',shell=True,check=True)
แบบนี้จะเกิดข้อผิดพลาด
CalledProcessError: Command 'python -j' returned non-zero exit status 2.
การเปลี่ยนโฟลเดอร์ที่รัน หรือเปลี่ยนตัวแปรสภาพแวดล้อม
ปกติคำสั่งจะถูกรันในโฟลเดอร์ที่อยู่ปัจจุบัน แต่หากต้องการกำหนดโฟลเดอร์ที่ต้องการรันก็ใส่คีย์เวิร์ด cwd
subprocess.run('ls',cwd='/home')
เพียงแต่ว่าในนี้จะใช้ ~ เพื่อแทนโฟลเดอร์บ้านไม่ได้ หากต้องการก็อาจต้องใช้ os.expandusers (รายละเอียดอ่านใน
https://phyblas.hinaboshi.com/20200304)
import os
subprocess.run('ls',cwd=os.path.expanduser('~'))
ทั้ง ~ หรือการแทนค่าตัวแปรโดยขึ้นต้นด้วย $ นั้นจะได้ผลต่อเมื่อใช้ shell=True
subprocess.run(['echo HOME: $HOME'],shell=True)
ได้
HOME: /home/phyblas
ปกติเวลาที่รันคอมมานด์ไลน์ด้วยวิธีนี้ ค่าตัวแปรสภาพแวดล้อมจะเป็นไปตามที่ตั้งไว้แต่เดิม แต่ก็สามารถเปลี่ยนแปลงหรือเพิ่มเข้าไปได้โดยใส่คีย์เวิร์ด env
เช่น
subprocess.run(['echo HOME: $HOME'],shell=True,env={'HOME': 'บ้านฉัน'})
ได้
HOME: บ้านฉัน
การกำหนดเวลาจำกัดให้คำสั่ง
หากใส่คีย์เวิร์ด timeout จะสามารถกำหนดเวลาจำกัดซึ่งถ้าคำสั่งใช้เวลาเลยกว่านั้นก็จะเกิดข้อผิดพลาดขึ้น
เช่นลองใช้คำสั่ง sleep เพื่อทำให้เกิดการรอจนทำให้สิ้นสุดการทำงานของคำสั่งไม่ทัน
subprocess.run('sleep 2',shell=True,timeout=1)
ก็จะเกิดข้อผิดพลาดแบบนี้ขึ้น
TimeoutExpired: Command 'sleep 2' timed out after 1 seconds
การเอาผลลัพธ์หรือข้อผิดพลาดที่ได้จากคำสั่ง
ปกติเมื่อใช้ subprocess.run จะแสดงผลที่ออกมาให้เห็นทันที แต่ผลที่ได้นั้นจะไม่ได้ถูกเก็บไว้ที่ไหน
นำไปใช้ต่อไม่ได้
หากต้องการจะให้ผลลัพธ์ถูกเก็บไว้เพื่อใช้ทีหลัง แทนที่จะแสดงออกมาทันที แบบนี้ให้ใส่คีย์เวิร์ด stdout=subprocess.PIPE
แล้วค่าจะถูกเก็บอยู่ใน .stdout
p = subprocess.run(['python', '-V'],stdout=subprocess.PIPE)
print(p) # ได้ CompletedProcess(args=['python', '-V'], returncode=0, stdout=b'Python 3.8.1\n')
print(p.stdout) # ได้ b'Python 3.8.1\n'
ค่าที่ได้คืนกลับมาจะเป็นข้อมูลแบบ bytes หากต้องการให้เป็นสายอักขระซึ่งเป็นยูนิโค้ดให้ใส่คีย์เวิร์ด encoding เข้าไป โดยทั่วไปก็ใช้
utf-8
p = subprocess.run(['python', '-V'],stdout=subprocess.PIPE,encoding='utf-8')
print(p.stdout) # ได้ Python 3.8.1
เพียงแต่ว่าคีย์เวิร์ด encoding นี้ใช้ได้ตั้งแต่ในไพธอน 3.6 ถ้าเป็นรุ่นเก่ากว่านั้นคือ 3.5 ลงมาอาจใช้เมธอด decode() เพื่อแปลงเป็นยูนิโค้ดอีกที แบบนี้
p = subprocess.run(['python', '-V'],stdout=subprocess.PIPE)
print(p.stdout.decode()) # ได้ Python 3.8.1
หรือถ้าเป็นไพธอน 3.7 ขึ้นไปจะใส่ text=True ก็ได้ แบบนี้ก็มีความหมายเหมือนกัน
p = subprocess.run(['python', '-V'],stdout=subprocess.PIPE,text=True)
print(p.stdout) # ได้ Python 3.8.1
แต่ถ้าเป็นไพธอน 3.6 ลงมาจะใช้ universal_newlines=True แทน
p = subprocess.run(['python', '-V'],stdout=subprocess.PIPE,universal_newlines=True)
print(p.stdout) # ได้ Python 3.8.1
subprocess.PIPE นี้จริงๆแล้วก็คือตัวเลขธรรมดาที่มีค่า -1 ดังนั้นจริงๆจะใส่เป็น stdout=-1 ไปก็ได้ แต่ปกติจะใส่เป็น subprocess.PIPE
เพื่อให้เข้าใจความหมายได้ง่าย
หากต้องการให้ผลลัพธ์ถูกโยนทิ้งหายไปเลย ไม่ได้ถูกเก็บไว้ไหน และไม่ได้แสดงผลออกมาด้วย อาจใส่ stdout=subprocess.DEVNULL
(หรือก็คือเลข -3)
p = subprocess.run(['python', '-V'],stdout=subprocess.DEVNULL)
print(p) # ได้ CompletedProcess(args=['python', '-V'], returncode=0)
print(p.stdout) # ได้ None
stdout นี้จะเอาแต่ค่าผลลัพธ์ทั่วไปเท่านั้น แต่กรณีที่เกิดข้อผิดพลาดผลจะไปออกที่ stderr
p = subprocess.run(['python', '-j'],stderr=subprocess.PIPE)
print(p) # ได้ CompletedProcess(args=['python', '-j'], returncode=2, stderr=b"Unknown option: -j\nusage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\nTry `python -h' for more information.\n")
print(p.stderr) # ได้ b"Unknown option: -j\nusage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\nTry `python -h' for more
print(p.stdout) # ได้ None
หากต้องการให้ผลของข้อผิดพลาดออกมาใน stdout ให้ใส่ stderr=subprocess.STDOUT (หรือก็คือเลข -2) และในขณะเดียวกันก็ใส่
stdout=subprocess.PIPE ด้วย
p = subprocess.run(['python', '-j'],stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
print(p) # ได้ CompletedProcess(args=['python', '-j'], returncode=2, stdout=b"Unknown option: -j\nusage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\nTry `python -h' for more information.\n")
print(p.stderr) # ได้ None
print(p.stdout) # ได้ b"Unknown option: -j\nusage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\nTry `python -h' for more information.\n"
นอกจากนี้ในไพธอน 3.7 ขึ้นไปยังมีวิธีเขียนย่อให้ง่ายขึ้น คือใส่ capture_output=True
แบบนี้ทั้งผลลัพธ์ปกติและผลจากข้อผิดพลาดก็จะถูกเก็บไว้ใน stdout และ stderr เหมือนกัน
p = subprocess.run(['python', '-V'],capture_output=True)
print(p.stdout) # ได้ b'Python 3.8.1\n'
p = subprocess.run(['python', '-j'],capture_output=True)
print(p.stderr[0]) # ได้ b"Unknown option: -j\nusage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...\nTry `python -h' for more information.\n"
การเก็บผลลัพธ์ลงไฟล์
ถ้าต้องการให้ผลลัพธ์ที่ได้จากคำสั่งถูกใส่ลงไฟล์ก็อาจใช้ open เพื่อเปิดไฟล์สำหรับเขียนขึ้นมาแล้วใส่เป็น stdout
อาจใช้ with แบบนี้
with open('pythonver.txt','w') as f:
subprocess.run(['python', '-V'],stdout=f)
หรืออาจใช้ open ใส่ลงไปใน stdout โดยตรงเลยก็ได้ คือเขียนแค่นี้
subprocess.run(['python', '-V'],stdout=open('pythonver.txt','w'))
ผลของข้อผิดพลาดก็เก็บลงไฟล์ได้เช่นกัน โดยใส่ที่ stderr เช่น
subprocess.run(['python', '-j'],stderr=open('pythonerr.txt','w'))
หรือ
subprocess.run(['python', '-j'],stdout=open('pythonerr.txt','w'),stderr=subprocess.STDOUT)
ค่าป้อนเข้าในคำสั่ง
คำสั่งบางอย่างต้องการค่าป้อนเข้า (input) เราอาจใส่ค่าลงไปในคีย์เวิร์ด input
ค่าที่ป้อนเข้านั้นต้องเป็นชนิด bytes นั่นคือสายอักขระที่มี b นำหน้า เพราะในไพธอน 3 ถ้าไม่ใส่
สายอักขระทั่วไปจะเป็นยูนิโค้ด (ถ้าเป็นไพธอน 2 สายอักขระธรรมดาจะเป็น bytes อยู่แล้ว ส่วน unicode ต้องเติม u)
เช่นลองป้อนคำสั่ง python สำหรับรันโค้ดไพธอน โดยใส่โค้ดที่เป็น
subprocess.run('python',input=b'print(1./11)')
ได้
0.09090909090909091
ถ้าต้องการใส่สายอักขระธรรมดาซึ่งเป็นยูนิโค้ดอยู่แล้วไม่ต้องใช้เป็น bytes ก็ให้เติม encoding='utf-8' หรือ text=True
ไปด้วย ตัวอย่างข้างต้นอาจเขียนใหม่เป็นแบบนี้ก็ได้
subprocess.run('python',input='print(1./11)',encoding='utf-8')
หรือ
subprocess.run('python',input='print(1./11)',text=True)
ถ้าค่าป้อนเข้านั้นมาจากไฟล์ อาจใช้คีย์เวิร์ด stdin โดยใช้ open อ่านไฟล์เข้ามา เช่น
subprocess.run('python',stdin=open('sanpo.py'))
หรือจะใช้ open เปิดไฟล์เข้ามาแล้วอ่านด้วย .read() แล้วป้อนเข้าใส่คีย์เวิร์ด input ก็ได้
subprocess.run('python',input=open('sanpo.py').read(),text=True)
สรุปคีย์เวิร์ดใน run
คีย์เวิร์ด |
ความหมาย |
ค่าตั้งต้น |
หมายเหตุ |
stdin |
กำหนดตัวป้อนเข้า |
None |
|
input |
ค่าป้อนเข้า |
None |
|
stdout |
กำหนดการแสดงผลค่าผลลัพธ์ทั่วไป |
None |
|
stderr |
กำหนดการแสดงผลค่าผลลัพธ์ที่ผิดพลาด |
None |
|
capture_output |
เก็บเอาทั้งค่าผลลัพธ์ทั่วไปและค่าผลลัพธ์ที่ผิดพลาด |
False |
เพิ่มมาในไพธอน 3.7 |
shell |
ถ้าเป็น False จะต้องป้อนค่าแยกเป็นลิสต์ |
False |
|
cwd |
กำหนดโฟลเดอร์ที่รัน |
None |
|
timeout |
กำหนดเวลาจำกัดในการรัน |
None |
|
check |
ให้ขึ้นข้อผิดพลาดในไพธอนด้วยเมื่อคำสั่งมีข้อผิดพลาด |
False |
|
encoding |
ใส่ encoding='utf-8' เพื่อให้ค่าป้อนเข้าและผลลัพธ์เป็นยูนิโค้ด |
None |
เพิ่มมาในไพธอน 3.6 |
text |
ถ้าเป็น True จะทำให้ทั้งค่าป้อนเข้าและผลลัพธ์เป็นยูนิโค้ด |
None |
เพิ่มมาในไพธอน 3.7 |
env |
กำหนดตัวแปรสภาพแวดล้อมเพิ่มเติมหรือเปลี่ยนแปลง |
None |
|
universal_newlines |
เหมือน text แต่มีมาตั้งแต่ก่อนไพธอน 3.7 อาจใช้แทน text ในไพธอน 3.6 ลงมา |
call, check_call, check_output
นอกจาก run แล้วก็มีคำสั่ง call, check_call, check_output ที่อาจสะดวกที่จะใช้แทนในงานบางอย่าง แม้ว่างานส่วนใหญ่แล้วใช้แค่ run ก็ทำได้หมด
แต่ run นั้นเพิ่งมีตั้งแต่ไพธอน 3.5 ทำให้โค้ดเก่าๆอาจยังใช้ ๓ คำสั่งนี้ อีกทั้งยังอาจทำให้เขียนสั้นขึ้นกว่าด้วย
ที่น่าจะได้ใช้มากที่สุดคือ subprocess.check_output นั่นคือคำสั่งสำหรับรันแล้วคืนค่าเอาผลที่ได้มาใช้
res = subprocess.check_output(['python','-V'])
print(res) # ได้ b'Python 3.8.1\n'
ซึ่งถ้าเขียนแทนโดยใช้ run ก็จะเท่ากับการเขียนแบบนี้
res = subprocess.run(['python','-V'],stdout=subprocess.PIPE).stdout
ส่วน subprocess.call จะเป็นการรันแล้วคืนค่า returncode ซึ่งเป็นค่าที่บอกว่าเกิดข้อผิดพลาดหรือไม่ออกมา
returncode = subprocess.call(['python','-V'])
print(returncode) # ได้ 0
ซึ่งถ้าเขียนแทนด้วย run ก็จะได้
returncode = subprocess.run(['python','-V']).returncode
ส่วน check_call จะคล้ายกับ call แต่จะเหมือนกับเติม check=True
กรณีที่ไม่มีข้อผิดพลาดใดๆ ผลที่ได้ก็ไม่ต่างจาก call
returncode = subprocess.check_call(['python','-V'])
print(returncode) # ได้ 0
แต่เมื่อมีข้อผิดพลาดในคำสั่ง ผลที่ได้จะต่างกัน โดย call_check จะทำให้เกิดข้อผิดพลาดขึ้นมาในไพธอนด้วย
returncode = subprocess.call(['python','-j'])
print(returncode) # ได้ 2
returncode = subprocess.check_call(['python','-j'])
print(returncode) # ได้ CalledProcessError: Command '['python', '-j']' returned non-zero exit status 2.
Popen
ในตัวอย่างที่ผ่านมานั้นเป็นการสั่งคำสั่งแบบง่ายๆ คือสั่งครั้งเดียวแล้วเสร็จ รอผลลัพธ์เลย
แต่สำหรับคำสั่งที่ต้องมีการใส่ข้อมูลเพิ่มเติมตอบโต้อย่างต่อเนื่องจะมีความซับซ้อนกว่า
subprocess.Popen เป็นออบเจ็กต์ที่มีไว้เพื่อสั่งคำสั่งลงในคอมมานด์ไลน์อย่างต่อเนื่อง
เนื่องจากมีรายละเอียดมากและเนื้อหาจะยาว ดังนั้นขอแยกไปเขียนต่อในอีกหน้า
https://phyblas.hinaboshi.com/20200307
อ้างอิง