5 คะแนน โดย GN⁺ 2023-11-25 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ShellCheck เป็นเครื่องมือที่ช่วยตรวจสอบข้อผิดพลาดที่พบบ่อยในเชลล์สคริปต์ได้ทันทีบนเว็บ โดยสามารถดูปัญหาของโค้ดที่วางลงไปได้จากผลลัพธ์ใต้ตัวแก้ไข
  • ในเครื่องโลคัลสามารถติดตั้งได้ด้วย cabal, apt, dnf, pkg, brew install เป็นต้น จึงใช้งานผ่าน แพ็กเกจเมเนเจอร์ ได้
  • ตัวอย่างแสดงให้เห็นจุดที่มักพลาดในสคริปต์จริง เช่น คำเตือนด้าน portability ของ shebang sh, ปัญหาเชิงความหมาย และข้อผิดพลาดเกี่ยวกับการจัดการเครื่องหมายคำพูด
  • เป็นซอฟต์แวร์เสรีภายใต้สัญญาอนุญาต GPLv3 และมีให้ใช้งานทั้งบน GitHub, Wiki และเป็นลินเตอร์แบบผสานรวมในเอดิเตอร์หลัก ๆ
  • ใช้ร่วมกับ CodeClimate, Codacy และ CodeFactor เพื่อตรวจสอบ GitHub repository อัตโนมัติได้ และตัว ShellCheck เองเขียนด้วย Haskell

ความสามารถในการวิเคราะห์เชลล์สคริปต์

  • ShellCheck เป็น เครื่องมือวิเคราะห์สำหรับค้นหาบั๊ก ในเชลล์สคริปต์
  • หากวางสคริปต์ลงในเว็บไซต์ จะสามารถดู ผลการตรวจสอบ ได้ทันทีจากหน้าต่างผลลัพธ์ใต้ตัวแก้ไข
  • ตัวอย่างที่มีมาให้แสดงว่าเครื่องมือนี้สามารถตรวจจับปัญหาได้หลายประเภท
    • ปัญหาหลายอย่างที่ซ่อนอยู่ในสคริปต์ทั่วไป
    • คำเตือนด้าน portability ที่เกิดขึ้นเมื่อ shebang เป็น sh
    • ปัญหาเชิงความหมาย ในระดับที่สูงขึ้น
    • ปัญหาการจัดการเครื่องหมายคำพูด หลายรูปแบบ

การติดตั้ง, สัญญาอนุญาต และการผสานรวม

  • ติดตั้งในเครื่องได้ด้วย cabal, apt, dnf, pkg, brew install
  • เป็นซอฟต์แวร์เสรีภายใต้สัญญาอนุญาต GPLv3
  • ดูเอกสารได้ที่ ShellCheck Wiki
  • โค้ดเปิดเผยบน GitHub และมี โค้ดของเว็บไซต์ เปิดเผยด้วย
  • มีแพ็กเกจสำหรับดิสทริบิวชันหรือแพ็กเกจเมเนเจอร์พร้อมให้ใช้งานแล้ว
  • ใช้เป็น ลินเตอร์แบบผสานรวม ในเอดิเตอร์หลัก ๆ ได้
  • ใช้กับ CodeClimate, Codacy, CodeFactor เพื่อตรวจสอบ GitHub repository อัตโนมัติได้
  • ShellCheck เขียนด้วย Haskell

1 ความคิดเห็น

 
GN⁺ 2023-11-25
ความคิดเห็นจาก Hacker News
  • เคล็ดลับที่ผมใช้เป็นแบบนี้: ใน shebang แทบทุกครั้งควรใส่ -u(nounset) เพื่อให้ ตัวแปรที่ไม่ได้ประกาศ กลายเป็นข้อผิดพลาด และข้อยกเว้นที่เจอบ่อยคือกรณีที่การขยายอาร์เรย์ว่างด้วยไวยากรณ์ "${arr[@]}" ถูกมองว่าเป็น unbound
    -n(noexec) จะป้องกันไม่ให้รันคำสั่ง จึงใช้เหมือน dry-run แบบคนงบน้อยได้ ส่วน -e(errexit) ก็มีประโยชน์ แต่ต้องระวังว่าจริง ๆ แล้วมันทำให้จบการทำงานเฉพาะคำสั่งที่ล้มเหลว “ด้วยตัวมันเอง” เท่านั้น ดังนั้นโดยส่วนตัวผมจึงหลีกเลี่ยงและมักใส่ || fail "..." ต่อท้ายคำสั่งมากกว่า

    • ปัญหา "${arr[@]}" มีเฉพาะใน bash 3 หรือต่ำกว่า เท่านั้น ตั้งแต่ bash 4 เป็นต้นมา แม้ตัวแปรจะไม่ได้ถูกนิยามจริง ๆ [@] ก็จะไม่โยน unbound variable
      ถึงอย่างนั้น macOS ก็ยังติดตั้ง bash v3 เป็นค่าเริ่มต้นและไม่อัปเดตอัตโนมัติ จึงยังเป็นปัญหาอยู่ การที่รีลีสสุดท้ายของ bash 3 ออกมาเมื่อ 20 ปีก่อนเป็นเรื่องบ้ามาก ๆ ส่วน unbound ที่เกิดจากการขยายอาร์เรย์ว่างสามารถเลี่ยงได้ด้วยการขยาย ${var+alter}: echo "${arr+${arr[@]}}"
    • ยังมีคำแนะนำที่พบบ่อยว่าไม่ควรใส่ออปชันของ Bash/shell ไว้ใน shebang เพราะถ้ามีคน ระบุ interpreter เพื่อรัน สคริปต์อย่าง bash my_script.sh แทนที่จะเป็น ./my_script.sh ออปชันจะไม่ถูกใช้
      มีคนจำนวนไม่น้อยที่ทำแบบนี้เพราะไม่อยากตั้งค่า execute bit และบางครั้งก็เกิดจากความไม่เข้าใจด้วย ดังนั้นจึงมักแนะนำให้ใช้ set เช่น set -euo pipefail ในบรรทัดแรกถัดจาก shebang และยังช่วยในกรณีที่ shebang จัดการอาร์กิวเมนต์เพิ่มเติมได้ยาก เช่น #!/usr/bin/env bash
    • สงสัยว่ามีเหตุผลพิเศษอะไรในการใส่ -u ไว้ใน shebang ทำไมต้อง shebang แทนที่จะเป็น set -u?
      ใน Bash ดูเหมือนว่า "${arr[@]}" จะทำงานได้ดี ตามที่คอมเมนต์อื่นก็พูดถึง Bash รุ่นใหม่ดีขึ้นกว่านั้นอีก และดูเหมือนจะมีปัญหาเฉพาะใน <= 4.3: https://news.ycombinator.com/item?id=38397241
      ตัวอย่างเช่น bash -uc 'unset x; echo "=> ${x[@]}"' และ bash -uc 'x=(); echo "=> ${x[@]}"' ผ่านด้วยค่าว่าง แต่ bash -uc 'x=(); echo "=> ${x[0]}"' จะเหลือข้อความ bash: x[0]: unbound variable ส่วน Zsh ไม่ชอบตัวอย่างแรก แต่ทั้งคู่ควรรองรับการขยายค่าเริ่มต้น เช่น bash -uc 'unset x; echo "=> ${x[@]:-null}"' สำหรับ -e พอเกี่ยวพันกับฟังก์ชันแล้วชวนสับสนมาก และยิ่งเวลาผ่านไปผมก็ยิ่งชอบมันน้อยลง
    • ปัญหา -e จัดการได้ดีด้วย -o pipefail และสิ่งนี้ถูกรวมอยู่ใน POSIX ตั้งแต่ปีที่แล้ว
    • เมื่อใช้ออปชันที่ทำให้สคริปต์จบการทำงานก่อนเวลา ถ้ามีไฟล์ที่ต้องเก็บกวาด โดยปกติต้องใช้ trap ควบคู่ไปด้วย
      trap เป็นฟีเจอร์ scripting ที่ยอดเยี่ยม แต่จากความรู้สึกแล้วไม่ค่อยถูกพูดถึงมากพอ
  • เมื่อไม่นานมานี้ผมพบ ช่องโหว่ยกระดับสิทธิ์ ใน shell script เนื่องจาก arithmetic expansion เป็นประเภทคล้ายกับที่อยู่ใน https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex...
    เช่น $((1 + ENV_VAR)) หากควบคุม $ENV_VAR ได้ ก็สามารถทำ code injection ได้ น่าเสียดายที่ ShellCheck จับสิ่งนี้ไม่ได้ อย่างน้อยก็ในค่าเริ่มต้น แต่ถ้าจะทำอะไรที่สำคัญด้านความปลอดภัยแม้เพียงเล็กน้อย ก็ไม่ควรใช้ shell ตั้งแต่แรก

    • ถ้าอยากได้ความปลอดภัยมากกว่านี้ ควรใช้อะไร?
  • ShellCheck คือผู้กอบกู้จริง ๆ ก่อนหน้านี้ผมทำ wrapper ตัวเล็ก ๆ ไว้ที่ https://github.com/jamespwilliams/strictbash ซึ่งใช้เป็น shebang ของสคริปต์ได้
    มันจะรัน ShellCheck ก่อนเริ่มสคริปต์ และถ้ามีข้อผิดพลาดก็ไม่ให้รันเลย รวมถึงตั้งค่าแฟล็ก “strict mode” ของ bash ทั้งหมดด้วย อ้างอิง: http://redsymbol.net/articles/unofficial-bash-strict-mode/

    • ก็ดีนะ แต่ดูเหมือนจะใช้ได้จริงเฉพาะกับสคริปต์ของตัวเอง ไม่อย่างนั้นคงต้องดีบักและแก้สคริปต์ทุกตัวที่ต้องรันทุกครั้ง
  • หัวข้อนี้เคยถูกพูดถึงหลายครั้งแล้ว: https://news.ycombinator.com/from?site=shellcheck.net
    การถกเถียงใหญ่ครั้งล่าสุดอยู่ในปี 2021 ได้ 301 คะแนนและมี 54 ความคิดเห็น: https://news.ycombinator.com/item?id=27030504

    • โดยพื้นฐานแล้วเป็น โพสต์ซ้ำต่อเนื่อง ของบทความนี้ https://news.ycombinator.com/item?id=38387464 และที่นั่นก็ถูกนำมาพูดถึงซ้ำ ๆ เช่นกัน
  • เมื่อไม่นานมานี้ ผมได้เปลี่ยนสคริปต์ bash หลายตัวสำหรับบิลด์และดีพลอย รวมถึงสำหรับเซิร์ฟเวอร์โปรดักชันเดี่ยว ไปใช้ Turtle ของ Haskell
    ดีตรงที่ลดความซ้ำซ้อนได้มาก และโค้ดที่ได้ก็สั้นลงมากด้วย https://hackage.haskell.org/package/turtle

    • ช่วงหลังได้ลองใช้ Turtle แต่สุดท้ายก็เลิกใช้แล้วเลือก typed-process แทน
      เท่าที่ผมรู้ โปรแกรม Turtle มี current directory ได้เพียงที่เดียว จึงลำบากเวลารันงานพร้อมกันที่ต้องทำงานในไดเรกทอรีเฉพาะ แก้ได้บางส่วนด้วย lock, queue และ worker แต่พอ current directory ของ Turtle ถูกลบจนเริ่มล้มเหลว ก็รับมือยากเกินไป
      ตรงกันข้าม typed-process จะสร้างโปรเซสแยก และรันใน working directory ได้โดยไม่ต้อง cd จึงเหมาะกับ workflow ขนาดใหญ่และซับซ้อน การรองรับ OverloadedStrings ก็ดี ทำให้โดยปกติคัดลอกสิ่งที่จะพิมพ์ใน bash แล้ววางลงไป ก็ทำงานได้เลย
      ผมยังใช้แพ็กเกจ interpolate กับ QuasiQuotes เพื่อให้ raw string ดูอ่านง่ายขึ้นในซอร์สโค้ด แต่เพราะเข้ากันไม่ได้กับ hlint เลยคิดว่าจะลองหาแพ็กเกจอื่นสำหรับจัดการสตริง
  • เป็นการโปรโมตตัวเองแบบตรง ๆ แต่ผมใส่ ShellCheck และ linter หลายตัวไว้ใน pre-commit config โดยยึดหลักว่า แก้ warning ทั้งหมดก่อน commit หรืออย่างน้อยก่อน merge
    แต่ shell ส่วนใหญ่ในโปรเจกต์ของผมอยู่ในไฟล์ .gitlab-ci.yml เลยตรวจสอบยาก ผมจึงทำ wrapper สำหรับจัดการให้อัตโนมัติ: https://pypi.org/project/glscpc/
    โดยใช้โปรเจกต์ ShellCheck กับเวทมนตร์เล็กน้อย มันจะแสดงคำเตือนจาก ShellCheck พร้อมเลขบรรทัดที่แทบจะตรง

    • ถ้ามีโปรเจกต์ที่ทำสิ่งนี้ได้แบบทั่วไปกว่านี้ก็คงดี
      ผมไม่ได้ใช้ GitLab CI แต่ใช้ไฟล์หลายรูปแบบที่โดยแก่นแล้วฝัง shell script แบบ inline อยู่พอสมควร เช่น Dockerfile, GitHub Actions, Justfile
      โดยปกติ แม้เพื่อ ShellCheck อย่างเดียว ผมก็มักแยกอะไรที่ซับซ้อนกว่าคำสั่งไม่กี่คำสั่งออกเป็น shell script ต่างหาก แล้วให้ inline script ใน Dockerfile เรียกใช้ แพตเทิร์นนี้ยังช่วยไม่ให้ CI ผูกติดกับ GitHub Actions มากเกินไปด้วย
    • ไฮไฟว์ ผมก็เพิ่งทำของคล้าย ๆ กันเมื่อไม่นานมานี้: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      ตัวอย่างการใช้งานคือ pre-commit hook สำหรับ .gitlab-ci.yml และตัวอย่างการตั้งค่าอยู่ที่นี่: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...
    • เจ๋งมาก เมื่อก่อนผมเคยลองแก้ปัญหานี้จากอีกมุมหนึ่ง อยากใส่ preprocessing ที่รับ shell script “ทั่วไป” แล้ว render เป็นส่วน script ของ job นั้นตอน build
      ข้อดีคือทุกอย่างยัง self-contained อยู่ใน job ของ gitlab-ci แต่การจัดการสารพัดความประหลาดของ shell ในสภาพแวดล้อม GitLab CI runner นั้นเจ็บปวดเกินไป เลยหยุดไป และตอนนี้กำลังย้ายทุกงานไปเป็น Python script
  • ยังมี bash language server ด้วย: https://github.com/bash-lsp/bash-language-server/

  • ดีเลย สคริปต์โปรดักชัน /bin/sh ตัวแรกที่ลองรันก็ทำให้ได้เรียนรู้อะไรหลายอย่างทันที ทั้งที่ผมแตะสคริปต์พวกนี้มาตั้งแต่ยุค 80 แล้ว

  • ถ้ามันยาวเกินไปสำหรับการเขียนด้วย Bash จนจริง ๆ แล้วไม่ควรทำแบบนั้น ก็ขอแนะนำ https://github.com/bach-sh/bach ด้วย

  • ShellCheck ยอดเยี่ยม แต่การจัดการ source/import นั้นเจ็บปวดจริง ๆ ไม่ใช่ความผิดของ ShellCheck หรอก แต่เป็นเพราะ sh นี่แหละที่เป็นฝันร้าย

    • ทำแบบนี้ได้: # shellcheck source=./deployment/deployment-example.env แล้วตามด้วย . "${1}"
      แต่ถ้ามี shell script ย่อยหลายตัวและมีไฟล์ที่จะ source จำนวนมาก ก็จะเข้าใจว่าทำไมมันถึงเจ็บปวด