לדלג לתוכן

Bash Scripting

יסודות במילון

Combining && and ||

&& and || can be combined to make an alternative to if-then-else. Consider:

if some_command
then
    echo yes
else
    echo no
fi

The above is similar to:

some_command && echo yes || echo no

​- nohup (command) - כל ההודעות שקשורות לפקודה הזו יירשמו בקובץ nohup.out - הקובץ ישמר בתיקיה שממנה ניתנה הפקודה, או ב-home במקרה שאין הרשאות כתיבה לתיקיה הזו - א I said "similar" rather than identical because the two are different in the case that the echo yes command fails. If the echo yes command were to fail (exit with a non-zero return code) then the echo no command is executed. This would not happen with the if-then-else-fi version. The echo command will only fail, though, if there is a write-error on the terminal. Consequently, this difference rarely matters and the && and || form is often used as a short-cut.

נראה ש-'קדימות' של && היא בעיקר עניין של איך שהסוגריים יכתבו. command && command2 || command3 נקרא כמו (command && command2) || command3 ולא כמו command && (command2 || command3) אבל אם אני רוצה ש-command3 תתעלם מההצלחה של command2, אני צריך להשתמש ב-if, else

script locations

  • הרבה פעמים יש דפוס כזה: סקריפטים לשימוש אישי של היוזר הולכים ל- ~/bin סקריפטים לשימוש של כל היוזרים הולכים ל-/usr/local/sbin
    • לא שמים קבצים משלנו בהיררכיה של /usr/bin או /bin, יש הרבה פאת'ים שמיועדים רק לקבצי מערכת - קרנל, דיסטרו וכו'

readability

  • קריאות של סקריפט חשובה לא רק בשביל להעביר לאחרים - היא חשובה כדי שנוכל לשנות אותו בעצמנו.
  • כשאנחנו כותבים סקריפט עדיף להשתמש ב-long form commands: ls --all במקום ls -a

  • אנחנו משתמשים ב- Backslash-linefeed sequences (רצף של: רווח, באקסלש, ירידת שורה) כדי להפריד שורות בסקריפט בלי באמת להפריד שורות, כלומר שכדי שיקראו ברצף ללא קטיעה, אבל יהיו קריאות:

    command \
    command \
    

  • בסקריפטים, אפשר להשתמש ב-tab characters כדי ליצור אינדנטציה. ב-command line הרגיל אי אפשר כי זה יוצר הרצה (completion)

    מה זה אומר טאבים במובן הזה? אני צריך להתחדד על טאבים, carriage return וכו'

  • כדי להריץ את VIM עם syntax highlighting, נשתמש ב- :syntax on - לשים לב שחייבים את ה-VIM המלא בשביל זה ולא את המיני שבא עם חלק מהדיסטרוז

  • לשים לב ש-VIM עושה היילייטינג רק אחרי שסיפקנו shebang לקובץ
  • אם יש בעיות לנסות :set syntax=sh ![[Pasted image 20250202142656.png|500]]

תהליך הכתיבה של סקריפט

  • נתחיל מלהניח יסודות, נניח קבצים ותיקיות שנצטרך להשתמש בהם; בהמשך נמלא אותם בדאטה וכו'

טיפים

להעביר מס' שורות לאאוטפוט

  • נניח שאנחנו רוצים שהסקריפט יכתוב תבנית HTML מסוימת לתוך קובץ. אנחנו רוצים את התבנית הזו כטקסט ב-STDOUT כדי שנוכל לנתב אותה לקובץ. יש מספר שורות ולא רק string אחד. אפשרי אבל לא מומלץ לעשות ככה:

    echo "<html>"
    echo "<head>"
    echo "<title>Page Title</title>"
    echo "</head>"
    echo "<body>"
    echo "Page body."
    echo "</body>"
    echo "</html>"
    

  • אני מסיק מזה שכל "שורה" במובן של פקודה חדשה, יוצרת שורה חדשה ב-STDOUT (הרי אין הבדל בין הפרדת השורות בסקריפט לבין לעשות רצף ארוך של echo output echo output echo output), בשני המקרים ה-shell יריץ כל פקודה עם הארגומנט שלה בנפרד ואז ימשיך להבאה.

  • יותר מומלץ להשתמש בעובדה ש-string כולל בתוכו newlines, ולהשתמש בסטרינג אחד: ![[Pasted image 20250202143515.png|500]]

משתנים לכל דבר

  • כל אלמנט בקוד שלנו שעשוי לחזור על עצמו, נגיד כותרות וכו', צריך להיות ניתן לשינוי בקלות, בלי לערוך מיליון שורות. לכן, כדאי מאוד להגדיר אותו כמשתנה בתחילת הקוד!!! (נניח ה-pathים בסקריפט הגיבוי שלי...)

top down design

  • גישה לפיתוח תוכנות שמתחילה "מלמעלה" - כלומר מהתוצר שאנחנו רוצים, ומתקדמת "למטה" בדרך של פריטת הפונקציה הסופית לתת-פונקציות הדרושות כדי לבצע אותה.
  • כמו לעשות רשימת "אילו פעולות דרושות לי כדי להתכונן בבוקר", ואז עבור כל פעולה, לרשימה של "אילו פעולות מרכיבות את הפעולה", וכן הלאה
  • ל-shell scripting זו גישה מתאימה במיוחד

shell functions

  • כדי להוסיף "רכיבים" של קוד לתוכנה, אנחנו יכולים לכתוב אותם כסקריפטים נפרדים, לשים ב-PATH ולהריץ באמצעות התוכנה (כמו שאני עושה את הגיבויים שלי כרגע), או שאנחנו יכולים לכלול אותה בתוך הקוד של התוכנה, כמה שנקרא shell function
  • פונקציות של הן בעצם "מיני סקריפטים" שכתובים בתוך סקריפטים אחרים, ויכולים לרוץ אוטונומית
  • יש שתי צורות סינטקטיות לפונקציה:
  • הרשמית יותר:

    function name {
    commands 
    return
    }
    

  • הפשוטה והמועדפת יותר:

    name () {
    commands 
    return
    }
    

  • ניים הוא השם שנפעיל איתו את הפונקציה, קומנדס הן הפקודות, return מסמל לטרמינל לשחרר מהפונקציה בסיום שלה, ולחזור לשורה שאחרי ה-function call (איפה שהשתמשנו בה בפועל)

  • הטרמינל לא מריץ את הפונקציה כשהיא מוגדרת, רק כשעושים לה call ע"י שימוש ב-name כפקודה
  • פונקציה חייבת לפחות פקודה אחת, return נחשב
  • אותם חוקים לגבי השם של פונקציה כמו לגבי השם של משתנים

  • חשוב להקפיד לכתוב פונקציות של באמצעות [[מילון BASH#local variables, local|משתנים מקומיים]] ולא באמצעות משתנים גלובליים. הסיבה היא שאנחנו רוצים שהפונקציה תהיה ניידת, ואם שמות המשתנים שלנו לא ייחודיים עבור הפונקציה, עלולות להיווצר כל מיני התנגשויות

  • כדאי גם לשים לב שכל הפקודות של הפונקציה עטופות בסוגריים - המשמעות היא שהן Group Command
  • כלומר - האאוטפוט של כל הפקודות של הפונקציה משולב יחד וניתן ל-Redirection, אם לקובץ, לפקודה שלוקחת STDIN, או כל אפשרות אחרת שעושים עם STDOUT
  • אם יש לנו פקודות בתוך הפונקציה שלוקחות STDIN, אפשר גם לעשות Redirection לתוך הפונקציה - כל הפקודות יחלקו את אותו STDIN
  • נהוג להשתמש ב-STUBים: כלומר, בפונקציות שסתם עושות echo או משהו טריביאלי, כדי לראות שהמבנה הלוגי של הקוד שלנו נכון ושכל פונקציה רצה בצורה הנכונה (בתנאים הנכונים, מצליחה לרוץ וכו') -- אם התוכנה עובדת עם Stubים, היא תעבוד גם עם פונקציות שבאמת עושות דברים (אלא אם בפונקציות עצמן יש בעיות)
  • בגדול, יותר נכון להשתמש בפונקציות shell מאשר ב-aliases כדי לקסטם פקודות - אנחנו רוצים תמיכה בסינטקס כמה שיותר נורמלי, אז עדיף להגדיר את ה-Function בקובץ ה-bashrc שלנו...
  • אפשר להשתמש בפונקציה באמצעות "call" רגיל (לשים אותה בשורה בקוד), או שאפשר להשתמש בה בכל אופן שניתן להשתמש בפקודה: my_var=$(function) function | piped-into-command function > redirect

Don't cat into grep

cat file | grep query זה טיפשי יש grep query file.path...

troubleshooting & debugging

syntax errors, missing/unexpected tokens

  • הרבה פעמים, השורה שנקבל בשגיאה היא לא השורה שכוללת את השגיאה הסינטקטית. הסיבה היא ששגיאה סינטקטית היא לא תמיד בלתי-קריאה ברגע שהיא קורית: אם שכחנו לסגור סוגריים, למשל, סביר שבאש ממשיך לקרוא לאחר המיקום בו היו צריכות להיסגר, ומתייחס לכל הערכים כאילו הם חלק מהסוגריים. במקרה כזה, רק ברגע שסוגריים אחרות יסגרו, באש יבין שהערכים שהוא קרא בתוך הסוגריים לא הגיוניים, ויחזיר שגיאה עבור השורה של ה-"סגירה" המוטעית. סינטקס היילייטינג עוזר עם זה קצת.

  • אותה תופעה מתקיימת בהקשר של tokens: אם שכחנו לתת ; להתניה של if, ייתכן שכל הפקודות שנכניס בכוונה לבצע אותן כשההתניה מתקיימת, יקראו כחלק מההתניה. במקרה כזה, השגיאה עלולה להגיע רק כשבאש יגיע ל-else שלא הגיונית לפני שסגרנו את ההתניה, או כשהוא יגיע ל-fi מבלי שסגרנו אותה.

expansion mistakes

  • לעתים נשתמש במשתנה ריק (blank): במצב כזה, לא נוכל להשתמש באופרטור '=' כחלק מפקודת 'טסט' (test [ $blank = 1]). הסיבה היא ש-$blank לא מחזיר שום ערך, בעוד שהאופרטור הוא בינארי וחייב לעבוד עם שני ערכים קיימים. אם נעשה "$blank" בגרשיים נקבל משתנה ריק "חיובי", ששווה ל-"" (ולא לכלום מוחלט) [ "$blank" = x ] ולכן ניתן לעשות את זה כחלק מטסט

    כידוע, מרכאות דרושות גם במצב שבו המשתנה מתרחב לכמה מילים עם רווחים ביניהן, כדי שבאש יבין אותן כסטרינג אחת. באופן כללי, כדאי תמיד לשים ערכי-משתנים ו-command substitutions בגרשיים, אלא אם אנחנו צריכים שהמילים יופרדו באאוטפוט.

logical errors

  • לעומת שגיאות סינטקטיות, שגיאות לוגיות לאו דווקא ימנעו מהתוכנה לרוץ, אבל הן ימנעו ממנה לפעול כמתוכנן: לחשוב טוב על ה-flow של התוכנה: איך הלופים וההתניות שלנו רצים, איך הם מגדירים משתנים, ואיפה עשויה להיות טעות. מצב נוסף הוא סיטואציות לא צפויות שהלוגיקה שלנו לא כתובה באופן שיכול להתמודד איתן.

Defensive Programming

  • לפעמים יש סקריפטים שעשויים לגרום לנזק אם חלק בהם לא יעבוד. למשל: cd $dirname & rm * במקרה שלא נמצאה התיקיה dirname, אנחנו הולכים למחוק את כל הקבצים ב-home, או גרוע מזה אם ה-cwd היא ה-root... לכן אנחנו מוודאים ש-rm תפעל רק אם הפקודה שקודמת לה תצליח (exit 0): cd $dirname && rm *

  • ולא רק שנעשה && לפני פקודה מסוכנת, נשתמש בעוד בדיקות: בדוגמה הקודמת, נעשה: if [ -d $file ]; then cd $file && rm *

שמות קבצים:

  • הרבה יגידו שלינוקס פרמיסיבי מדי עם שמות קבצים השם יכול לכלול כל תו חוץ מ-blank או / (שמרכיב פאת'ים) לכן יכול להתקיים בשם -rf ~

  • זה יוצר הרבה פוטנציאל לבאגים ואסונות... קובץ שמתחיל בדאש (-) יכול להתפרש כדגל לפקודה במקום כמטרה עבורה אז מה עושים?

    1. נקפיד להשתמש ב-./ לפני כל גלוב - ./* ולא *
    2. נקפיד להשתמש בשמות קבצים שלא יכולים להתפרש כפקודה (a-zA-Z1-9 ואולי - בין לבין מילים, או קו תחתון...)

פוסיקס סטנדרט (להעתיק יפה אח"כ) - כמעט מה שאמרתי, a-zA-Z1--9 וגם נקודה (.), דאש (-) או קו תחתון (_)

בדיקת אינפוטים

  • הספר מציע כלל טוב: אם התוכנה מקבלת אינפוט, היא צריכה לדעת להתמודד עם כל מה שנזרוק עליה. לכן נשתמש בבדיקות, לרוב test עם regex operator כך: [[ $reply =~ 'regular-expression' ]]

שימוש ב-stubs

  • כמו שכבר למדנו, מדובר בשימוש ב-echoים או שאר דברים שקל לראות אם הם עובדים או לא, כדי לבדוק את הזרימה של הקוד שלנו. אם לחזור לדוגמה שכבר הכרנו, במקום לעשות cd $variable & rm * נעשה: `cd $variable & echo rm * ונשלב איזושהי התניה שתתן את ה-echo רק אם הצלחנו להחליף תיקיה...

  • ה-stubs צריכים לשרת test cases - אנחנו רוצים לבדוק מה הסקריפט שלנו עושים במקרי קצה/מקרים חריגים שנגדיר לצורך העניין: מה יעשה כשהמשתנה הוא תיקיה קיימת? מה יעשה כשהיא כן? מה יעשה כשהמשתנה ריק? אם עשינו טסטים למרבית האפשרויות, אפשר להגיד שיש לנו "test coverage" טוב. מושגי מפתח: edge and corner cases, test case and coverage

  • בכל אופן, זו תמיד שאלה של זמן - לכתוב קוד בלי באגים שנבדק היטב דורש זמן. אין סיבה להקדיש שעות על בדיקה של סקריפט קטן שנריץ על המחשב שלנו - יש טעם להשקיע גם שבועות על בדיקה של סקריפט שנפיץ לשימוש רחב ותכוף.

debugging

  • אם משהו לא עובד, נצטרך לדבג המטרה של תכנות מתגונן, פידבק משמעותי למשתמש ושימוש בסטאבים היא לדאוג שלכל הפחות, יהיה לנו כיוון לגבי המקור של הבאג

  • ובכל זאת, לעתים נידרש לעבודת בלשות, לכן חשוב להכיר כמה כלים חשובים לדיבוג:

  • "commenting out" נשתמש ב-# כדי להשמיט חלקים מהקוד ולראות איך הוא רץ בלעדיהם

  • "Tracing" הרבה באגים הם תוצאה של זרימה לוגית לא נכונה: או שחלקים מסוימים מהקוד לא רצים, או שהם רצים בסדר הלא נכון. נשתמש ב-stubים שמצביעים לנו בכל מהלך של הקוד על המקום שהמחשב קורא באותו רגע ("מריץ את העברת תיקיה" <פונקציית העברת תיקיה> "עברנו תיקיה" "מתחיל את פונקציית מחיקה" <פונקציית מחיקה> "פונקציית מחיקה בוצאה)

    אפשר לעשות >&2 כדי להעביר את ההודעות ל-ERROR ולא לערבב אותן עם האינפוט

    יש גם את האפשרויות -x (מיד אחרי השיבנג) או set -x שיגרמו ל-bash לתת לנו את $PS4 בכל הרצה של פקודה. ה-PS4 הוא משתנה שאומר לבאש איך לדווח לנו על הפקודה האחרונה שהריץ. אנחנו נראה אותה עם + בהתחלה כחלק מהאאוטפוט - עבור כל שורה/פקודה שרצה!

    אפשר לערוך את PS4 כדי לקבל את המידע בצורות שונות. למשל עם $LINENO כדי להציג את מס' השורה לצד התוכן שלה. ההבדל בין -x אחרי השיבנג ל-set -x היא שאת סט-x אפשר לתחום לחלק מסוים של הסקריפט, באמצעות הטוקן הסוגר set +x, זאת בעוד ש-x אחרי השיבנג חל על כל הסקריפט.

  • "echoing values"

  • בנוסף לטרייסינג, לעתים נרצה לדעת מה הערך של משתנים מסוימים בסקריפט או בחלק ממנו: זה די straight-forward, נשתמש בעקרונות שכבר למדנו: echo של המשתנה שאנחנו רוצים לבדוק, בנקודות שבא לנו לבדוק אותו. אפשר גם לסדר שידפיס את עצמו עם כל שינוי.