วิธีใช้ Bash history expansion ด้วย !! และ !$ อย่างมือโปร
เรียนรู้การใช้ !! เพื่อรันคำสั่งล่าสุดและ !$ เพื่อดึงอาร์กิวเมนต์ตัวสุดท้ายมาใช้งาน พร้อมเทคนิคการใช้ :p เพื่อตรวจสอบคำสั่งก่อนรันจริง ช่วยลดเวลาพิมพ์บน Terminal ได้จริง
การทำงานของ bash history expansion
Bash history expansion จะสร้างบรรทัดคำสั่งก่อนหน้าขึ้นมาใหม่จากประวัติการใช้งานใน shell ของคุณก่อนที่ bash จะดำเนินการคำสั่งนั้น !! ใช้สำหรับทำซ้ำคำสั่งก่อนหน้า !$ ใช้สำหรับแทรกอาร์กิวเมนต์ตัวสุดท้ายของคำสั่งก่อนหน้า การแทนที่นี้เป็นการจัดการในระดับข้อความและเกิดขึ้นเป็นลำดับแรก ก่อนที่ bash จะแยกบรรทัดคำสั่งออกเป็นคำๆ ดังนั้นสิ่งที่ได้กลับมาคืออักขระชุดเดียวกับที่คุณพิมพ์ไปในครั้งแรก
รูปแบบ 6 ประการต่อไปนี้ครอบคลุมเกือบทุกอย่างที่คุณต้องทำบนเซิร์ฟเวอร์:
!!ทำซ้ำทั้งบรรทัดก่อนหน้า และsudo !!รันคำสั่งนั้นอีกครั้งในฐานะ root!$คืออาร์กิวเมนต์ตัวสุดท้ายของบรรทัดก่อนหน้า!*คืออาร์กิวเมนต์ทั้งหมดของบรรทัดก่อนหน้า!nรันรายการประวัติคำสั่งลำดับที่ n และ!-nนับย้อนหลังจากตำแหน่งปัจจุบันของคุณ!stringรันคำสั่งล่าสุดที่ขึ้นต้นด้วยstring^old^newรันบรรทัดก่อนหน้าซ้ำโดยแทนที่oldตัวแรกด้วยnew
ทุกอย่างด้านล่างนี้เป็นการพิมพ์ที่ interactive prompt บนเซิร์ฟเวอร์ของคุณเอง โดย history expansion จะถูกปิดใช้งานในสคริปต์ ซึ่งส่วนสุดท้ายจะอธิบายรายละเอียดในเรื่องนี้
เส้นทางยาวที่พิมพ์เพียงครั้งเดียว
นี่คือกรณีที่คุ้มค่ากับการใช้งาน คุณกำลังเตรียมไดเรกทอรีสำหรับ release และเส้นทางนั้นยาวพอที่จะทำให้การพิมพ์ซ้ำเสี่ยงต่อการพิมพ์ผิด
sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.envก่อนรันแต่ละบรรทัด bash จะแสดงบรรทัดที่ได้จากการขยายตัวแปรให้เห็น:
sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07การ echo นี้คือการตรวจสอบของคุณ ให้อ่านบรรทัดดังกล่าวก่อนอ่านผลลัพธ์ของคำสั่ง เพราะนั่นเป็นโอกาสเดียวที่คุณจะได้เห็นว่า bash ตีความ !$ ว่าอย่างไร
ห่วงโซ่นี้ทำงานได้เพราะ bash จัดเก็บบรรทัดที่ ขยายแล้ว ไว้ในประวัติ ไม่ใช่ !$ ที่คุณพิมพ์ ดังนั้นบรรทัดที่ 3 จึงดึงอาร์กิวเมนต์สุดท้ายมาจากบรรทัดที่ 2 ที่ขยายแล้ว และบรรทัดที่ 4 ก็ดึงมาจากบรรทัดที่ 3 เช่นกัน บรรทัดที่ 4 ยังแสดงให้เห็นว่าสามารถมีข้อความต่อท้ายตัวระบุได้: !$/config.env จะกลายเป็นเส้นทางที่มี /config.env ต่อท้าย เพราะตัวระบุคำจะหยุดทำงานที่ /
อีกหนึ่งตัวอย่างจาก session เดียวกัน หลังจากแก้ไขไฟล์แล้ว คุณต้องการแสดงรายการไดเรกทอรีที่เก็บไฟล์นั้นไว้:
ls -l !$:h:h คือตัวปรับแต่ง (modifier) ส่วนหัว มันจะตัดส่วนประกอบสุดท้ายของเส้นทางออก ซึ่งเป็นหน้าที่เดียวกับที่ dirname ทำ ส่วน :t จะเก็บไว้เฉพาะส่วนประกอบสุดท้าย, :r จะลบนามสกุลไฟล์ออก และ :e จะเก็บไว้เฉพาะนามสกุลไฟล์เท่านั้น
การทำซ้ำคำสั่งล่าสุดด้วย !! และ sudo !!
คุณลืมใส่ sudo และตัวจัดการบริการจะแจ้งเตือนดังนี้:
systemctl restart nginxFailed to restart nginx.service: Interactive authentication required.sudo !!Bash จะแทนที่ !! ด้วยข้อความของบรรทัดก่อนหน้า ดังนั้นเชลล์จะรัน sudo systemctl restart nginx
!! คือบรรทัดก่อนหน้าแบบเป๊ะๆ ไม่ว่าบรรทัดนั้นจะเป็นคำสั่งใดก็ตาม นี่คือจุดที่มักเกิดอุบัติเหตุบ่อยครั้ง คุณอาจเข้าใจว่าคำสั่งล่าสุดคือคำสั่งที่ล้มเหลว แต่ในระหว่างนั้นคุณอาจรัน cd หรือ history ไปแล้ว หรือคำสั่งที่ล้มเหลวอาจอยู่ห่างไปสองบรรทัด sudo !! จึงไปรันคำสั่งที่ผิดด้วยสิทธิ์ root ให้พิมพ์คำสั่งออกมาก่อนหากคุณไม่แน่ใจ:
sudo !!:p:p จะพิมพ์บรรทัดที่ขยายผลแล้วออกมาโดยไม่รันคำสั่งนั้น บรรทัดที่พิมพ์ออกมาจะถูกเพิ่มลงในประวัติการใช้งานของคุณ ดังนั้นหากตรวจสอบแล้วว่าถูกต้อง คุณสามารถพิมพ์ !! ในบรรทัดถัดไปเพื่อรันคำสั่งนั้นได้เลย
การใช้ argument ล่าสุดซ้ำด้วย !$
!$ คือคำสุดท้ายของบรรทัดก่อนหน้า นี่คือรูปแบบที่คุณจะได้ใช้บ่อยที่สุด เนื่องจากคำสุดท้ายมักจะเป็นสิ่งที่คุณกำลังทำงานด้วย เช่น path หรือชื่อ service
sudo systemctl status nginx
sudo systemctl reload !$มีสองสิ่งที่ทำให้ผู้ใช้ประหลาดใจในส่วนนี้
ประการแรก !$ คือ คำ สุดท้าย ไม่ใช่ argument สุดท้ายที่คุณตั้งใจไว้ หากบรรทัดก่อนหน้าลงท้ายด้วยการ redirection คำสุดท้ายจะเป็นเป้าหมายของการ redirect นั้น:
sudo nginx -T > /tmp/nginx-dump.conf
less !$กรณีนั้นทำงานได้ตามปกติ แต่หลังจาก journalctl -u nginx > /tmp/log.txt แล้ว !$ จะกลายเป็น /tmp/log.txt ไม่ใช่ nginx ให้ตรวจสอบบรรทัดที่ถูกแสดงผลออกมา
ประการที่สอง การขยายความ (expansion) เป็นแบบข้อความ ดังนั้นตัวแปรจะถูกดึงกลับมาในสถานะที่ยังไม่ได้ขยาย หลังจาก ls $HOME/backups แล้ว !$ จะให้ผลลัพธ์เป็นตัวอักษร $HOME/backups ซึ่ง bash จะขยายความอีกครั้งในฐานะ parameter ปกติ โปรดจำลำดับนี้ไว้: history expansion จะทำงานก่อน parameter expansion และก่อน command substitution ด้วย $( ) ดังนั้นมันจึงไม่เห็นค่าของตัวแปร เห็นเพียงแค่ข้อความเท่านั้น
รูปแบบใกล้เคียงที่ควรทราบคือ !^ ซึ่งเป็น argument แรก, !:2 คือ argument ที่สอง และ !:2-4 คือช่วงของ argument ส่วน !!:$ เป็นวิธีเขียนแบบยาวของ !$
หากคุณต้องการเห็นข้อความก่อนที่จะยืนยันการใช้งาน ให้กด Alt-. (หรือ Esc แล้วตามด้วย .) Readline จะแทรก argument ล่าสุดของคำสั่งก่อนหน้าลงใน prompt ของคุณโดยตรง ซึ่งคุณสามารถแก้ไขได้ ให้กดซ้ำอีกครั้งเพื่อย้อนกลับไปยัง argument ล่าสุดของคำสั่งก่อนหน้านั้นไปอีกขั้น จะไม่มีการรันคำสั่งใดๆ จนกว่าคุณจะกด Enter
ส่งผ่านอาร์กิวเมนต์ทั้งหมดด้วย !*
!* คือทุกคำของบรรทัดก่อนหน้ายกเว้นคำแรก
stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*วลีที่ว่า "ยกเว้นคำแรก" มีความสำคัญมาก !* จะตัดคำที่ 0 ออกเพียงคำเดียว ดังนั้นตัวเลือก (options) จึงถูกส่งต่อไปพร้อมกับพาธ หลังจาก ls -l file1 file2 แล้ว !* จะกลายเป็น -l file1 file2 ส่งผลให้ sudo chmod 600 !* ทำงานล้มเหลว เพราะ chmod ได้รับ -l เข้าไปเป็นอาร์กิวเมนต์ ให้ใช้วิธีนับคำโดยเริ่มจากศูนย์แล้วเลือกช่วงแทน: !!:2* หมายถึงคำที่ 2 จนถึงคำสุดท้าย
กับดักแบบเดียวกันนี้เกิดขึ้นกับ sudo เช่นกัน หลังจาก sudo chown deploy:deploy /srv/www/app คำที่ 0 คือ sudo และคำที่ 1 คือ chown ดังนั้น !* จึงส่ง chown deploy:deploy /srv/www/app ไปให้คำสั่งถัดไป ซึ่งมักไม่ใช่สิ่งที่คุณต้องการ
การเลือกคำสั่งตามตำแหน่งด้วย !n และ !-n
history จะแสดงรายการคำสั่งพร้อมตัวเลขกำกับไว้หน้าแต่ละรายการ
history 5 512 sudo nginx -t
513 sudo systemctl reload nginx
514 ss -tulpn
515 sudo tail -f /var/log/nginx/error.log
516 history 5!513 จะเรียกใช้รายการที่ 513 อีกครั้ง !-2 จะเรียกใช้รายการที่ย้อนหลังไป 2 บรรทัดโดยนับจากบรรทัดที่คุณกำลังพิมพ์อยู่ ดังนั้น !-1 และ !! จึงมีความหมายเดียวกัน
โปรดตรวจสอบตัวเลขก่อนใช้งานทุกครั้ง !-2 จะชี้ไปยังตำแหน่งใหม่ทุกครั้งที่คุณรันคำสั่งใดๆ รวมถึง ls ที่คุณอาจเผลอกดไปโดยไม่ได้ตั้งใจ ตัวเลขแบบสัมบูรณ์จะคงที่ภายในเซสชันเดียว แต่จะไม่ใช่ตัวเลขเดิมหากคุณเริ่มเซสชันใหม่บนเครื่องเดิม และจะไม่ใช่ตัวเลขเดิมหลังจากที่คุณล็อกอินใหม่แล้วระบบอ่านไฟล์ประวัติคำสั่งกลับเข้ามา ตัวเลขที่คุณจำได้เมื่อวานนี้จะชี้ไปยังคำสั่งที่ต่างออกไปในวันนี้
เรียกใช้คำสั่งเดิมซ้ำด้วยการระบุคำนำหน้าด้วย !string
!string จะเรียกใช้คำสั่งล่าสุดที่ขึ้นต้นด้วย string
!ssคำสั่งดังกล่าวจะเรียกใช้ ss -tulpn จากรายการด้านบนซ้ำอีกครั้ง ซึ่งเป็นการตรวจสอบ socket ที่กำลังฟังอยู่ตามที่อธิบายไว้ใน พอร์ตใดบ้างที่เปิดใช้งานอยู่บนเซิร์ฟเวอร์ Linux ส่วน !?string? จะจับคู่กับข้อความที่ปรากฏที่ใดก็ได้ในบรรทัดแทนที่จะเป็นแค่ส่วนต้น ซึ่งมีประโยชน์เมื่อคุณจำอาร์กิวเมนต์ได้แต่จำชื่อคำสั่งไม่ได้
ควรใช้คำนำหน้าที่ยาวพอสมควร !s อาจจับคู่กับ ss, sudo, systemctl หรือ shutdown ขึ้นอยู่กับว่าคำสั่งใดถูกเรียกใช้ล่าสุด ซึ่งคุณจะไม่ทราบจนกว่าคำสั่งนั้นจะทำงานจริง ส่วน !string:p จะแสดงคำสั่งที่จับคู่ได้ออกมาโดยไม่เรียกใช้งาน หากไม่มีคำสั่งใดตรงกัน bash จะแสดง bash: !ss: event not found และไม่เรียกใช้คำสั่งใดๆ ซึ่งเป็นผลลัพธ์ที่ปลอดภัย
แก้ไขคำผิดหนึ่งจุดด้วย ^old^new
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxShell จะรันคำสั่งบรรทัดก่อนหน้าซ้ำอีกครั้งโดยแทนที่ ngnix ตัวแรกด้วย nginx การเปลี่ยนแปลงนี้จะมีผลเฉพาะกับตำแหน่งที่พบเป็นจุดแรกเท่านั้น หากต้องการเปลี่ยนทุกจุดที่พบ ให้ใช้รูปแบบยาวคือ !!:gs/ngnix/nginx/ โดยที่ s หมายถึงการแทนที่ และ g หมายถึงการดำเนินการให้ครอบคลุมทั้งบรรทัด
แสดงผลการขยายคำสั่งก่อนเรียกใช้งานจริง
มีสองนิสัยที่ช่วยป้องกันอุบัติเหตุจากการที่การขยายคำสั่ง (expansion) ทำงานในสิ่งที่คุณไม่ได้ตั้งใจ
นิสัยแรกคือ :p ซึ่งคุณได้เห็นไปแล้ว ให้เติมต่อท้ายการขยายคำสั่งแล้ว bash จะแสดงผลลัพธ์แทนการเรียกใช้งานจริง เช่น !!:p หรือ !systemctl:p บรรทัดที่แสดงออกมาจะถูกบันทึกลงในประวัติการใช้งาน ดังนั้นการใช้ !! ในภายหลังจะเป็นการเรียกใช้งานสิ่งที่คุณเพิ่งอ่านไป
นิสัยที่สองมีประสิทธิภาพมากกว่า เพราะใช้ได้กับการขยายคำสั่งทุกรูปแบบโดยที่คุณไม่ต้องคอยจดจำอะไรเพิ่มเติม:
shopt -s histverifyให้ใส่บรรทัดดังกล่าวลงใน ~/.bashrc เมื่อตั้งค่า histverify ไว้ การขยายคำสั่งจะไม่ทำงานทันทีที่คุณกด Enter แต่ bash จะเขียนบรรทัดที่ขยายแล้วกลับลงใน prompt ของคุณเพื่อให้คุณอ่านและแก้ไข จากนั้นคุณจึงกด Enter อีกครั้งเพื่อเรียกใช้งานจริง วิธีนี้เพิ่มการกดปุ่มเพียงหนึ่งครั้งแต่ช่วยขจัดอุบัติเหตุประเภทนี้ได้ทั้งหมด เนื่องจากวิธีนี้ต้องอาศัย readline จึงใช้งานได้เฉพาะที่ prompt แบบโต้ตอบเท่านั้น
Ctrl-R เป็นอีกวิธีหนึ่งในการทำงานและไม่มีการขยายคำสั่งใดๆ ให้กด Ctrl-R แล้วพิมพ์บางส่วนของคำสั่ง prompt จะเปลี่ยนเป็น reverse-i-search และแสดงรายการที่ตรงกันล่าสุดในขณะที่คุณพิมพ์ ให้กด Ctrl-R อีกครั้งเพื่อย้อนกลับไปยังรายการที่เก่ากว่า การกด Enter จะเป็นการเรียกใช้งานบรรทัดที่แสดงอยู่ ส่วน Ctrl-G จะเป็นการยกเลิกการค้นหาและคืนค่า prompt เดิมของคุณ และปุ่มลูกศรซ้ายจะเป็นการสิ้นสุดการค้นหาโดยทิ้งบรรทัดที่ตรงกันไว้ใน prompt เพื่อให้คุณแก้ไข
ให้ใช้ Ctrl-R เมื่อคุณต้องการตรวจสอบคำสั่งก่อน และใช้ !! กับ !$ เมื่อคำสั่งนั้นเพิ่งผ่านไปเพียงหนึ่งบรรทัดและคุณยังสามารถอ่านได้บนหน้าจอ
เหตุใดคำสั่ง echo "done!" จึงแจ้งว่าไม่พบเหตุการณ์ (event not found)
echo "deploy done!"bash: !": event not foundการขยายประวัติ (history expansion) จะทำงานก่อนการประมวลผลเครื่องหมายคำพูด และเครื่องหมายคำพูดคู่ (double quotes) ไม่สามารถป้องกันอักขระ ! ได้ มีเพียงเครื่องหมายคำพูดเดี่ยว (single quotes) และแบ็กสแลช (backslash) เท่านั้นที่ทำได้ ดังนั้น echo 'deploy done!' จึงแสดงผลลัพธ์ตามที่คุณคาดหวัง แบ็กสแลชสามารถหยุดการขยายได้เช่นกัน แต่เมื่ออยู่ภายในเครื่องหมายคำพูดคู่ bash จะคงแบ็กสแลชไว้ในผลลัพธ์ ดังนั้นการใช้เครื่องหมายคำพูดเดี่ยวจึงเป็นวิธีแก้ไขที่สะอาดที่สุด
ปัญหานี้ส่งผลกระทบมากที่สุดกับรหัสผ่าน เนื่องจากรหัสผ่านที่ปลอดภัยมักมีอักขระ ! อยู่ด้วย คำสั่งอย่าง mysql -u app -p"S3cret!pass" จะล้มเหลวด้วยข้อผิดพลาด event-not-found หรือหากมีรายการในประวัติที่ตรงกันพอดี คำสั่งจะส่งสตริงอื่นไปโดยที่คุณไม่รู้ตัว ควรใช้เครื่องหมายคำพูดเดี่ยว หรือวิธีที่ดีกว่าคือให้เครื่องมือถามรหัสผ่านจากคุณโดยตรง เพื่อไม่ให้ข้อมูลลับหลุดไปถึงบรรทัดคำสั่ง นิสัยนี้ควรเป็นส่วนหนึ่งของสุขอนามัยในการจัดการบัญชีของคุณ: ดูที่ สิบนาทีแรกบน VPS ใหม่ เพื่อศึกษาวิธีจัดการคีย์และรหัสผ่านบนเซิร์ฟเวอร์ที่เพิ่งติดตั้ง
หากคุณกำลังจะวางข้อความจำนวนมากที่มีอักขระ ! ให้ปิดการขยายประวัติสำหรับเซสชันนั้นด้วย set +H และเปิดกลับด้วย set -H
HISTSIZE, HISTFILESIZE และตำแหน่งที่เก็บประวัติคำสั่งของคุณ
ตัวแปรสองตัวนี้มักถูกสับสนเนื่องจากชื่อที่คล้ายกัน
HISTSIZEคือจำนวนคำสั่งที่ shell ที่กำลังทำงานอยู่เก็บไว้ในหน่วยความจำHISTFILESIZEคือจำนวนบรรทัดที่เก็บไว้ในไฟล์บนดิสก์~/.bash_historyเว้นแต่HISTFILEจะกำหนดไว้เป็นอย่างอื่น
ไฟล์จะถูกเขียนเมื่อ shell ปิดตัวลง และจะถูกตัดทอนให้เหลือ HISTFILESIZE บรรทัดในขณะนั้น ค่าเริ่มต้นของ ~/.bashrc ใน Ubuntu กำหนดไว้ที่ 1000 และ 2000 คุณสามารถตรวจสอบค่าปัจจุบันของคุณได้ดังนี้:
grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"ให้เพิ่มค่าทั้งสองหากการค้นหาด้วย !string ของคุณไม่พบคำสั่งที่คุณมั่นใจว่าเคยเรียกใช้ ค่า 10000 และ 20000 เป็นค่าที่นิยมใช้และแทบไม่กินทรัพยากรเนื่องจากไฟล์เป็นข้อความธรรมดา ค่าติดลบหมายถึงไม่มีการจำกัดจำนวน
การประทับเวลา (Timestamp) จะช่วยได้มากเมื่อคุณกลับมาใช้งานเซิร์ฟเวอร์หลังจากผ่านไปหนึ่งสัปดาห์:
export HISTTIMEFORMAT='%F %T 'history จะแสดงวันที่และเวลานำหน้าแต่ละรายการ เนื่องจาก bash จะเขียนบรรทัดคอมเมนต์ที่เก็บค่า epoch seconds ไว้ก่อนหน้าแต่ละคำสั่งในไฟล์
ปัญหาที่พบบ่อยคือประวัติคำสั่งหายไปเมื่อคุณเปิดใช้งานมากกว่าหนึ่ง terminal เนื่องจากแต่ละ shell จะเก็บรายการของตนเองไว้ในหน่วยความจำและเขียนทับไฟล์เมื่อปิดตัวลง ดังนั้นหากไม่มี histappend shell สุดท้ายที่ปิดตัวลงจะเขียนทับสิ่งที่ shell อื่นบันทึกไว้ คุณสามารถแก้ไขได้ด้วยการตั้งค่าสองอย่างนี้:
shopt -s histappend
export PROMPT_COMMAND='history -a'histappend ทำให้ shell เพิ่มรายการต่อท้ายไฟล์แทนที่จะเขียนทับไฟล์เดิม ส่วน history -a จะเพิ่มบรรทัดใหม่หลังทุกครั้งที่มีการแสดง prompt ดังนั้นหาก session ถูกสั่ง kill แทนที่จะปิดอย่างถูกต้อง คำสั่งเหล่านั้นก็จะยังคงถูกบันทึกไว้ ค่าเริ่มต้นของ ~/.bashrc ใน Ubuntu ได้กำหนด histappend ไว้ให้แล้ว เรื่องนี้มีความสำคัญมากขึ้นตามจำนวนเครื่องที่คุณดูแล เพราะประวัติจะถูกเก็บแยกตามผู้ใช้และแยกตามเครื่อง ดังนั้นเมื่อคุณ จัดการเซิร์ฟเวอร์หลายเครื่องจาก workstation เครื่องเดียว !$ ที่คุณต้องการอาจจะอยู่ในโฮสต์อื่นแทน
การเก็บข้อมูลลับให้พ้นจากประวัติการใช้งาน bash
HISTCONTROL เป็นตัวกำหนดว่าบรรทัดใดบ้างที่จะถูกบันทึกไว้
ignorespaceจะละทิ้งบรรทัดใดก็ตามที่ขึ้นต้นด้วยเว้นวรรคignoredupsจะละทิ้งบรรทัดที่ซ้ำกับบรรทัดก่อนหน้าignorebothจะทำงานทั้งสองอย่างข้างต้นerasedupsจะลบสำเนาเก่าทั้งหมดของบรรทัดที่กำลังจะถูกบันทึกออก
export HISTCONTROL=ignorebothเมื่อเปิดใช้งาน ignorespace การเว้นวรรคไว้ที่หน้าคำสั่งหนึ่งครั้งจะช่วยป้องกันไม่ให้คำสั่งนั้นถูกเก็บไว้ในรายการบนหน่วยความจำ ดังนั้นคำสั่งดังกล่าวจึงไม่ถูกบันทึกลงในไฟล์ด้วย ควรตรวจสอบค่านี้ก่อนใช้งานจริงด้วยคำสั่ง echo "$HISTCONTROL" โดยปกติแล้วผู้ใช้ทั่วไปใน Ubuntu จะมีการตั้งค่า .bashrc ให้เป็น ignoreboth แต่ใน shell ของ root หรือ image ขนาดเล็ก ค่านี้อาจไม่ได้ถูกตั้งไว้ ซึ่งจะทำให้การเว้นวรรคหน้าคำสั่งไม่มีผลและบรรทัดนั้นจะถูกบันทึกตามปกติ
ควรตระหนักถึงขอบเขตการป้องกันนี้ให้ดี บรรทัดดังกล่าวจะไม่อยู่ใน ~/.bash_history แต่ยังคงมองเห็นได้ในผลลัพธ์ของ ps ในขณะที่กระบวนการทำงานอยู่ ดังนั้นผู้ใช้อื่นบนเครื่องเดียวกันจึงสามารถอ่านข้อมูลได้ นอกจากนี้ sudo ยังบันทึกคำสั่งที่เรียกใช้งานลงใน log ของระบบด้วย ไฟล์ประวัติเป็นเพียงข้อความธรรมดา (plain text) จึงควรตั้งค่าสิทธิ์เป็น 600 และพึงระลึกว่าใครก็ตามที่สามารถอ่าน home directory ของคุณได้ ก็สามารถอ่านคำสั่งย้อนหลังหลายพันรายการของคุณได้เช่นกัน
หากมีข้อมูลสำคัญหลุดเข้าไปในรายการแล้ว ให้ลบรายการนั้นออกและเขียนไฟล์ใหม่:
history
history -d 517
history -whistory -d จะลบรายการนั้นออกจากหน่วยความจำ และ history -w จะเขียนรายการปัจจุบันทับลงในไฟล์ ส่วน history -c จะล้างรายการทั้งหมดทิ้ง สำหรับ HISTIGNORE เป็นตัวเลือกที่เกี่ยวข้องกับการจัดการข้อมูลที่ไม่สำคัญมากกว่าข้อมูลลับ โดย HISTIGNORE='ls:pwd:history:clear' จะช่วยกันบรรทัดเหล่านั้นออกจากรายการ เพื่อให้การค้นหาของคุณพบเฉพาะผลลัพธ์ที่มีประโยชน์เท่านั้น
เหตุใด history expansion จึงไม่ทำงานใน shell script
History expansion เป็นคุณสมบัติของ interactive shell เท่านั้น สคริปต์จะทำงานใน non-interactive shell ซึ่งรายการประวัติ (history list) จะไม่ถูกเปิดใช้งานและระบบ expansion จะปิดอยู่ ดังนั้น !! และ !$ จึงคงอยู่ในบรรทัดในฐานะข้อความปกติ การใช้ sudo !! ภายในสคริปต์จะเป็นการสั่งให้ sudo รันคำสั่งที่มีชื่อว่า !! ตรงตัว ซึ่งจะทำให้เกิดข้อผิดพลาด
ตรวจสอบ shell ที่คุณกำลังใช้งานอยู่ด้วยคำสั่งนี้:
echo $-ผลลัพธ์ที่ได้คือชุดของ option flags ปัจจุบัน ซึ่งจะมีลักษณะคล้ายกับ himBHs โดย i หมายถึง shell อยู่ในโหมด interactive และ H หมายถึงเปิดใช้งาน history expansion หากคุณรันบรรทัดเดียวกันนี้ภายในสคริปต์ ตัวอักษรทั้งสองจะไม่ปรากฏอยู่
นี่คือเส้นแบ่งระหว่างการทำงานสองรูปแบบของ shell ที่คุณใช้ ที่ prompt คำสั่ง !$ และ Ctrl-R ช่วยประหยัดเวลาในการพิมพ์คำสั่งที่คุณยังมองเห็นได้ แต่ในไฟล์สคริปต์ คุณต้องระบุชื่อสิ่งต่างๆ แทน เช่น การเก็บ path ไว้ในตัวแปร หรือการรับค่า output ด้วย command substitution สคริปต์ที่ขึ้นอยู่กับประวัติการใช้งานส่วนตัวของคุณจะทำงานแตกต่างออกไปเมื่อผู้อื่นนำไปรัน ซึ่งขัดกับจุดประสงค์หลักของการเขียนสคริปต์
ด้วยเหตุนี้ ตัวอย่างทั้งหมดในหน้านี้จึงมีไว้สำหรับพิมพ์ที่ prompt โดยตรง ไม่มีตัวอย่างใดที่ทำงานเหมือนเดิมหากนำไปวางในไฟล์ .sh
FAQ
!! ใน bash ทำหน้าที่อะไร?
!! จะขยายผลเป็นข้อความทั้งหมดของคำสั่งก่อนหน้า ดังนั้น sudo !! จึงเป็นการรันคำสั่งล่าสุดของคุณอีกครั้งในฐานะ root การขยายผลนี้เป็นเพียงการแทนที่ข้อความและเกิดขึ้นก่อนที่ bash จะประมวลผลบรรทัดนั้น โดย bash จะแสดงบรรทัดที่สมบูรณ์ให้เห็นก่อนเริ่มรันจริง หากคุณไม่แน่ใจว่าบรรทัดก่อนหน้าคืออะไร ให้พิมพ์ sudo !!:p ก่อน :p จะแสดงผลการขยายโดยไม่รันคำสั่ง และจะเพิ่มบรรทัดนั้นลงในประวัติการใช้งาน ทำให้การใช้ !! ในลำดับถัดไปสามารถรันคำสั่งนั้นได้
ฉันจะนำอาร์กิวเมนต์สุดท้ายของคำสั่งก่อนหน้ามาใช้ซ้ำได้อย่างไร?
ให้ใช้ !$ หลังจากรัน sudo mkdir -p /srv/www/app/releases แล้ว บรรทัด ls -ld !$ จะกลายเป็น ls -ld /srv/www/app/releases มันจะดึงเอา คำ สุดท้ายของบรรทัดมาใช้ ดังนั้นเป้าหมายการ redirect ที่อยู่ท้ายบรรทัดจะถูกนับเป็นคำสุดท้าย คุณสามารถเพิ่มข้อความต่อท้ายได้ เช่น !$/config.env จะเป็นการเพิ่ม path ต่อท้าย เพราะตัวระบุคำจะหยุดทำงานที่ / ทางเลือกสำหรับการใช้งานแบบโต้ตอบคือ Alt-. ซึ่งจะแทรกข้อความเดียวกันลงใน prompt ของคุณเพื่อให้คุณตรวจสอบได้ก่อนกด Enter
ทำไม bash ถึงแจ้งว่า "event not found" เมื่อข้อความของฉันมีเครื่องหมายอัศเจรีย์?
เครื่องหมายคำพูดคู่ (double quotes) ไม่สามารถป้องกัน ! จากการขยายผลประวัติ (history expansion) ได้ ดังนั้น echo "done!" จะทำให้ bash ค้นหาเหตุการณ์ในประวัติและแสดงผล bash: !": event not found ออกมา เครื่องหมายคำพูดเดี่ยว (single quotes) สามารถป้องกันได้ ดังนั้นให้เขียน echo 'done!' แทน การใช้ backslash ก็สามารถหยุดการขยายผลได้เช่นกัน แต่หากอยู่ในเครื่องหมายคำพูดคู่ bash จะคง backslash ไว้ในผลลัพธ์ หากต้องการวางข้อความยาวๆ ที่มี ! ให้ปิดการขยายผลสำหรับ session นั้นด้วย set +H
ทำไม !! และ !$ ถึงไม่ทำงานใน shell script ของฉัน?
การขยายผลประวัติจะเปิดใช้งานเฉพาะใน interactive shell เท่านั้น สคริปต์จะรันในรูปแบบ non-interactive ซึ่ง shell จะไม่สร้างรายการประวัติ และ !! จะถูกทิ้งไว้ในบรรทัดเป็นเพียงข้อความธรรมดา ให้รัน echo $- เพื่อตรวจสอบว่าคุณอยู่ในโหมดใด หากเป็น interactive shell จะแสดง flag รวมถึง i และ H แต่หากเป็นสคริปต์จะไม่แสดงทั้งสองอย่าง ในสคริปต์ให้ใช้ตัวแปรหรือ command substitution แทน
ฉันจะป้องกันไม่ให้รหัสผ่านถูกบันทึกลงใน bash history ได้อย่างไร?
ตั้งค่า HISTCONTROL=ignorespace หรือ HISTCONTROL=ignoreboth ใน ~/.bashrc จากนั้นให้เริ่มคำสั่งด้วยการเว้นวรรคหนึ่งครั้ง คำสั่งนั้นจะไม่ถูกบันทึก ตรวจสอบค่าด้วย echo "$HISTCONTROL" ก่อนเสมอ เพราะหากไม่ได้ตั้งค่าไว้ การเว้นวรรคข้างหน้าจะไม่มีผลใดๆ วิธีนี้ช่วยป้องกันไม่ให้บรรทัดนั้นถูกบันทึกลงใน ~/.bash_history เท่านั้น แต่คำสั่งยังคงปรากฏใน ps ขณะที่กำลังทำงาน และ sudo จะบันทึกสิ่งที่ถูกรันไว้ หากรหัสผ่านถูกบันทึกไปแล้ว ให้ค้นหาหมายเลขบรรทัดด้วย history จากนั้นรัน history -d <number> ตามด้วย history -w เพื่อเขียนไฟล์ประวัติใหม่