ΕΛ

Pyrsos.dev

"Passing on the flame"

Εισαγωγή στο bash

Σπύρος Τρυφωνίδης - 20 Σεπτεμβρίου 2024

Εισαγωγή

Μόλις εγκατέστησες κάποιο Linux distribution και οι οδηγίες που βρήκες στο ίντερνετ σε καθοδήγησαν να ανοίξεις ένα πρόγραμμα που λέγεται terminal και να γράψεις κάποια commands σε αυτό.

Μόλις κατάφερες να συνδεθείς με SSH στα Linux μηχανήματα του τμήματος της σχολής σου.

Μόλις ξεκίνησες το WSL σου στο Windows workstation σου.

Αμέσως έρχεσαι απέναντι σε μια μαύρη γραμμή εντολών την οποία απο εδώ και πέρα θα ξαναδείς πολλές φορές.

username@hostname:~$ █

Αυτό που βλέπεις μπροστά σου είναι το prompt ενώς shell. Πρόκειται δηλαδή για έναν απο τους τρόπους με τους οποίους μπορούμε να αλληλεπιδράσουμε με το shell. Τι είναι αυτο το shell για το οποίο διαβάζεις συνέχεια όποτε ψάξεις οτιδήποτε σχετικό με Linux όμως;

Αν θες να ακολουθήσεις αυτό το άρθρο εκτελώντας αυτά που θα δείς παρακάτω σιγούρεψε οτι το shell στο οποίο βρίσκεσαι είναι το bash. Αυτό μπορείς να το δείς εύκολα εκτελώντας echo $BASH. Αν αυτή η εντολή δεν εκτυπώσει τίποτα τότε το shell που χρησιμοποιείς δεν είναι το bash. Ψάξε στο ίντερνετ οδηγίες για το πως να το εγκαταστήσεις στο distribution σου και ξεκίνα το εκτελώντας bash.

Τι είναι;

Το shell (στα ελληνικά κέλυφος αλλά ας πετάξουμε απο τώρα τα προσωπεία και ας χρησιμοποιούμε μόνο τους αγγλικούς ορισμούς απο εδω και πέρα) πρόκειται για ένα πρόγραμμα όπως όλα τα υπόλοιπα. Οι χρήσεις του είναι ποικίλες αλλά χρησιμοποιείται κυρίως:

Αφαιρετικά λοιπόν το shell πρόκειται για ένα interpreter για μια γλώσσα προγραμματισμού. Μπορούμε λοιπόν να σκεφτούμε το prompt ως μια γραμμή εντολής η οποία θα δωθεί στο interpreter το οποίο θα την εκτελέσει.

Εφοδιασμένοι λοιπόν με τις βασικές έννοιες γύρω απο το shell θα προχωρήσουμε εστιάζοντας σε ένα συγκεκριμένο shell το οποίο λέγεται bash. Ο λόγος που εστιάζουμε στο bash είναι οτι πρόκειται για το πιο ευρέως διαδεδομένο interactive shell άρα είναι πολύ πιθανό να το συναντήσεις στην καθημερινότητα σου. Πέραν αυτού υπάρχουν πάρα πολλά προγράμματα γραμμένα για αυτό τα οποία θα είναι πολύ χρήσιμο να είσαι σε θέση να καταλάβεις και να επεκτείνεις.

Υπάρχουν διάφορα άλλα ενδιαφέροντα shell στα οποία μπορεί να θέλεις να ρίξεις μια ματιά όπως:

  • Το άρθρο είναι γραμμένο χρησιμοποιώντας bash 5.2.26 σε Linux

  • Όλες οι πληροφορίες σε αυτό το άρθρο προέρχονται απο το Bash Reference Manual (BRM απο εδώ και εξής)

    • μπορείς να το βρεις online
    • ή να το διαβάσεις διαδραστικά στο μηχάνημα σου εκτελώντας info bash στο shell σου.
  • Το άρθρο είναι γραμμένο θεωρώντας οτι οι αναγνώστες του έχουν κάποια βασική γνώση συστατικών Unix-like συστημάτων όπως processes, file descriptors, filesystems κοκ.

Πως λειτουργεί;

Τα προγράμματα που γράφουμε σε bash είτε αυτά είναι ολοκληρωμένα shell script είτε είναι απλα one-liners που δίνουμε σε ένα interactive shell βασίζονται σε κάποια βασικά δομικά στοιχεία. Σε αυτο το κεφάλαιο θα μάθουμε για αυτά τα βασικά στοιχεία.

Commands

Το βασικότερο εργαλείο για να κάνεις οτιδήποτε με το bash είναι τα commands. Το bash εκτελεί ανά γραμμές και κάθε γραμμή αποτελείται απο ανάθεση μεταβλητών, εκτέλεση commands ή και τα 2. Τα commands είναι εντολές προς το bash το οποίο τις εκτελεί. Αυτές χωρίζονται επίσης σε κατηγορίες ανάλογα το είδος τους:

Για να μάθεις το είδος ενός command πριν το εκτελέσεις (αλλά και για να ξέρεις που να ψάξεις πληροφορίες για αυτό) μπορείς να χρησιμοποιήσεις το type builtin:

username@hostname:~$ type declare
declare is a shell builtin

username@hostname:~$ type if
if is a shell keyword

username@hostname:~$ type cat
cat is /usr/bin/cat

Για να μάθεις πληροφορίες για ένα keyword ή builtin (πέραν του BRM) μπορείς να χρησιμοποιήσεις το help builtin:

username@hostname:~$ help declare

Variables

Όπως κάθε άλλη γλώσσα προγραμματισμού το bash υποστηρίζει την χρήση μεταβλητών. Μάλιστα κάποιες μεταβλητές έχουν σημασία για το bash και ελέγχουν διάφορα κομμάτια της συμπεριφοράς του. Αυτές χωρίζονται σε 2 μεγάλες κατηγορίες:

Υπάρχουν πολλοί τρόποι να δηλώσουμε μεταβλητές στο bash αλλά οι πιο απλοί είναι οι εξής:


# Δηλώνουμε μια global μεταβλητή με όνομα FOO και της δίνουμε την τιμή "bar"
FOO="bar"

function foo {
    # Δηλώνουμε μια local μεταβλητή με όνομα bar και της δίνουμε την τιμή "baz"
    local bar="baz"
}

Για να μάθεις πληροφορίες για ένα variable μπορεις να χρησιμοποιήσεις το declare builtin:

username@hostname:~$ MY_VARIABLE="foo"
# Δείχνει το πως δηλώθηκε αυτή η μεταβλητή καθώς και την τιμή της.
username@hostname:~$ declare -p MY_VARIABLE
declare -- MY_VARIABLE="foo"

# Δείχνει τις τιμές όλων των μεταβλητών στην συγκεκριμένη εκτέλεση του shell
username@hostname:~$ declare

Expansions

Προτού εκτελέσει το command που περιέχει η κάθε γραμμή, το bash προσπαθεί να βρει κάποια patterns εντός αυτής τα οποία μπορεί να αντικαταστήσει βάσει ενός συνόλου από προκαθορισμένους κανόνες. Αυτή η διαδικασία λέγεται expansion και επιτρέπει στον χρήστη να σχηματίσει commands με περίπλοκα ή πολλά arguments.

Υπάρχουν πολλά είδη απο expansion τα οποία καλύπτονται στο κεφάλαιο 3.5 του BRM το οποίο προτείνω να διαβάσεις αν θες να γράψεις κάποιο μεγάλο πρόγραμμα σε bash. Το bash κάνει expand τα expansions που θα βρει σε μια γραμμή σε 4 φάσεις. Τα αποτελέσματα του expansion μιας προηγούμενης φάσης μπορούν να επηρεάσουν τα expansion της επόμενης φάσης. Οι φάσεις αυτές, καθώς και μια ιδέα για το τι γίνεται περίπου σε κάθε φάση, συνοπτικά είναι οι εξής:

⚠️ Ξανατονίζω οτι τα αποτελέσματα ενώς προηγούμενου expansion μπορούν να επηρρεάσουν ένα επόμενο expansion, fun! ⚠️

Στην συνέχεια θα δούμε κάποιες παραπάνω λεπτομέρειες για τα πιο γνωστά απο αυτά τα expansion. Για να δούμε πως λειτουργούν θα χρησιμοποιήσουμε το echo builtin το οποίο απλά γράφει τα arguments που του δίνονται.

Tilde expansion

Ίσως το πιο γνωστό expansion, το tilde expansion έχει πολλές δυνατότητες αρα το κεφάλαιο 3.5.2 του BRM θα είναι φίλος σου εδώ. Η πιο γνωστή απο αυτές είναι η αντικατάσταση του χαρακτήρα ~ με το home directory του χρήστη. Έστω οτι έχουμε κάνει login ως ο χρήστης user:

user@hostname:~$ echo ~
/home/user

user@hostname:~$ echo ~/Desktop/assignments
/home/user/Desktop/assignments

Parameter and variable expansion

Επίσης ένα απο τα πιο γνωστά expansion, το parameter expansion μας επιτρέπει να αντικαταστήσουμε μια μεταβλητή με την τιμή της. Προφανώς, όπως τα περισσότερα expansion, το parameter expansion έχει πολλές δυνατότητες, το σύνολο των οποίων περιγράφονται αναλυτικά στο κεφάλαιο 3.5.3 του BRM. Η βασικότερη απο αυτές τις δυνατότητες είναι η αντικατάσταση των $VAR και ${VAR} με την τιμή της μεταβλητής VAR, για παράδειγμα:

username@hostname:~$ MY_NAME=spyros MY_SURNAME=trifonidis
username@hostname:~$ echo $MY_NAME ${MY_SURNAME}
spyros trifonidis

Command substitution

Το command substitution (κεφάλαιο 3.5.4 του BRM) μας επιτρέπει να αντικαταστήσουμε την χρήση ενός command με το output του. Αυτό είναι χρήσιμο για να δώσουμε input σε commands αλλά και για να δώσουμε τιμές σε μεταβλητές. Η σύνταξη του είναι $(COMMAND), για παράδειγμα:

# Έστω οτι το HTTP API στο my.private.api προστατεύεται με κάποιο access 
# token το οποίο έχουμε αποθηκεύσει στο αρχείο /tmp/access_token και το
# οποίο πρέπει να στείλουμε στον HTTP header "x-access-token"
username@hostname:~$ curl -H "x-access-token: $(cat /tmp/access_token)" https://my.private.api/data

# Ακόμα καλύτερα μπορούμε να βάλουμε το access token σε μια μεταβλητή 
# έτσι ώστε να μπορούμε να το χρησιμοποιήσουμε και σε επόμενα request
username@hostname:~$ access_token="$(< /tmp/access_token)" # Το $(< file) είναι ισοδύναμο με το $(cat file)
username@hostname:~$ curl -H "x-access-token: $access_token" https://my.private.api/data

Redirections & Pipelines

Το τελευταίο δομικό συστατικό του bash είναι τα redirections. Τα redirection μας επιτρέπουν (στην απλούστερη μορφή τους) να ανακατευθύνουμε οποιοδήποτε file θέλουμε προς κάποιο file descriptor που έχει ανοίξει το command που χρησιμοποιούμε. Τα file descriptor που κάνουμε redirect πιο συχνά είναι τα stdin/stdout/stderr. Αυτό μας επιτρέπει να συνθέσουμε commands χρησιμοποιώντας τα λεγόμενα pipelines. Τα redirection μπορούν να εμφανιστούν σε οποιοδήποτε σημείο μέσα σε ενα command, αλλά συνήθως εμφανίζονται στο τέλος γιατι είναι πιο ξεκάθαρο για τον αναγνώστη.

Όπως όλα τα άλλα δομικά συστατικά του bash, τα redirection έχουν πολλές δυνατότητες για τις οποίες μπορείτε να διαβάσετε στο κεφάλαιο 3.6 του BRM. Εδώ θα δούμε τα input redirection, output redirection και τα pipelines.

Input redirection

Για να ανακατευθύνουμε το input χρησιμοποιούμε το <. Για παράδειγμα το πρόγραμμα tr διαβάζει απο το stdin του και μετατρέπει τους χαρακτήρες απο το 1ο του argument στους χαρακτήρες του 2ου argument του. Έχοντας λοιπόν δημιουργήσει ένα αρχείο /tmp/a με περιεχόμενο την γραμμή a a a μπορούμε να κάνουμε το εξής:

username@hostname:~$ tr a b < /tmp/a
b b b

Αυτο που πετύχαμε εδώ είναι να κάνουμε το tr να διαβάζει το /tmp/a όταν διαβάζει το stdin του ή με άλλα λόγια κάναμε redirect το /tmp/a στο stdin

Output redirection

Για να ανακατευθύνουμε το output >. Χρησιμοποιώντας το cat θα ενώσουμε 3 αρχεία και θα γράψουμε το αποτέλεσμα σε ενα 4ο. Έχοντας τα αρχεία /tmp/foo, /tmp/bar και /tmp/baz τα οποία περιέχει το καθένα μια γραμμή με 1, 2 και 3 αντίστοιχα μπορούμε:

username@hostname:~$ cat /tmp/foo /tmp/bar /tmp/baz > /tmp/foobar
username@hostname:~$ cat /tmp/foobar
1
2
3

Εδώ κάναμε redirect το stdout στο /tmp/foobar

Pipelines

Τα pipelines μας επιτρέπουν να συνδυάσουμε το input & output redirection έτσι ώστε να συνθέσουμε commands. Φτιάχνουμε ενα pipeline με το |. Το stdout του command στα αριστερά του | "ενώνεται" με το stdin του command στα δεξιά.

Ας δούμε ένα παράδειγμα. Το bash καταγράφει στο αρχείο $HOME/.bash_history το ιστορικό όλων των γραμμών που του έχουμε δώσει για εκτέλεση. Ας ψάξουμε να βρούμε πόσες φορές έχουμε χρησιμοποιήσει το cat για να ξεκινήσουμε μια γραμμή. Για αυτόν τον σκοπό θα χρησιμοποιήσουμε το grep, το οποίο γράφει στο stdout του τις γραμμές που ταιριάζουν σε ένα pattern, και το wc το οποίο γράφει τον αριθμό απο τις γραμμές που διαβάζει.

# Εδώ το ^ συμβολίζει την αρχή της γραμμής. Αν θες να μάθεις περισσότερα για την γλώσσα του grep (και όχι μόνο) ψάξε για "regular expressions"
username@hostname:~$ grep '^cat' ~/.bash_history | wc --lines
2 # Αυτό το αποτέλεσμα μάλλον θα είναι διαφορετικό για εσένα αν το δοκιμάσεις.

Εδώ "ενώσαμε" το stdin του wc με το stdout του grep συνθέτοντας έτσι τα προγράμματα για να πετύχουμε τον σκοπό μας.

Σε περίπτωση που θες όντως να κάνεις κάτι τέτοιο μην κάνεις pipe το stdout του grep στο stdin του wc. Το grep μπορεί ήδη να μετρήσει και να γράψει τις γραμμές που ταιριάζουν σε ένα pattern με το -c/--count flag.

Στην πράξη

Σε αυτό το κεφάλαιο θα χρησιμοποιήσουμε κάποια απο τα δομικα συστατικά που αναφέρθηκαν πιο πάνω για να γράψουμε ένα χρήσιμο script το οποίο θα μας βοηθήσει στο γράψιμο άλλων scripts.

args

Αυτό είναι ένα πάρα πολύ χρήσιμο script το οποίο μας επιτρέπει να δούμε σε πόσα arguments καταλήγει κάτι αφού το bash πραγματοποιήσει όλα τα expanions που μπορεί. Είναι καλή πρακτική να χρησιμοποιήσεις αυτό το script για να επιβεβαιώσεις οτι ένα expansion που έχεις γράψει κάνει αυτό που πιστεύεις. Το script αυτο προέρχεται απο το Greg's Wiki το οποίο έχει πάρα πολλές χρήσιμες πληροφορίες γύρω απο το bash και όχι μόνο.

Το script είναι το εξής:

#!/usr/bin/env bash
printf "%d args:" "$#"
test "$#" -eq 0 || printf " <%s>" "$@"
echo

Ας το εξετάσουμε γραμμή-γραμμή.

#!/usr/bin/env bash 

Η πρώτη γραμμή (και συγκεκριμένα το κομμάτι #!) λέγεται shebang. Όταν προσπαθήσουμε να τρέξουμε ένα εκτελέσιμο αρχείο το οποίο ξεκινάει με το shebang τότε το λειτουργικό σύστημα θα τρέξει το πρόγραμμα που το ακολουθεί με το path για το αρχείο σαν 1ο argument. Αυτό ουσιαστικά μας επιτρέπει να τρέξουμε το script (αφού το βάλουμε κάπου στο $PATH) χωρίς να τρέξουμε ρητά το bash. Με άλλα λόγια μπορουμε να τρέξουμε args αντί για bash args

printf "%d args:" "$#"

Σε αυτή την γραμμή χρησιμοποιούμε το printf builtin για να γράψουμε τον αριθμό απο arguments που έχουν δοθεί στο script. Το bash κάνει expand το $# σε αυτόν τον αριθμό.

test "$#" -eq 0 || printf " <%s>" "$@"

Αυτή η γραμμή αν και πιο περίπλοκη εισάγει μια πολύ χρήσιμη κατηγορία απο commands, τα λεγόμενα lists (BRM 3.2.4). Όταν το bash συναντήσει την μορφή COMMAND1 || COMMAND2 τότε θα εκτελέσει το COMMAND1 και αν η εκτέλεση του αποτύχει (δηλαδή αν έχει status code διαφορετικό του 0) τότε θα εκτελέσει το COMMAND2. Σε αυτή την γραμμή λοιπόν ελέγχουμε με το test builtin για το αν έχουμε 0 arguments και αν δεν τότε χρησιμοποιούμε το printf για να γράψουμε ΚΑΘΕ word στο οποίο κάνει expand το $@. Το $@ είναι ένα ειδικό expansion το οποίο κάνει expand σε όλα τα arguments σαν ξεχωριστά words.

echo

Αυτή η γραμμή απλά εκτυπώνει ένα newline για να είναι πιο καθαρό το output.

Μερικά παραδείγματα εκτέλεσης:

username@hostname:~$ MY_VAR="a lot of words"
username@hostname:~$ args $MY_VAR
4 args: <a> <lot> <of> <words>
username@hostname:~$ args "$MY_VAR"
1 args: <a lot of words>

# Χρησιμοποιόντας αυτά που έχουμε μάθει ως εδώ και το BRM μπορείς να 
# εξηγήσεις γιατί είναι αυτό το αποτέλεσμα;
# (hint η απάντηση βρίσκεται στο source αυτού του script)

Αυτά για τώρα

Σε αυτό το άρθρο μάθαμε τι είναι το shell και γιατί το χρησιμοποιούμε. Στην συνέχεια είδαμε τα πολύ βασικά στοιχεία του bash shell πάνω στα οποία βασίζονται τα περισσότερα scripts. Τέλος χρησιμοποιήσαμε αυτά που μάθαμε για να γράψουμε ένα μικρό script.

Τα περισσότερα απο τα στοιχεία του bash τα οποία είδαμε έχουν πολλές περισσότερες δυνατότητες τις οποίες θα εξερευνήσουμε σε μελλοντικά άρθρα. Μέχρι τότε σου προτείνω να πειραματιστείς με αυτά που έμαθες, να διαβάσεις παραπάνω για δυνατότητες που σε ενδιαφέρουν στο BRM και να γράψεις κάποιο script το οποίο αυτοματοποιεί κάτι.

Εις το επανιδείν...