Εισαγωγή στο 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 scripts) .
Αφαιρετικά λοιπόν το shell πρόκειται για ένα interpreter για μια γλώσσα προγραμματισμού. Μπορούμε λοιπόν να σκεφτούμε το prompt ως μια γραμμή εντολής η οποία θα δωθεί στο interpreter το οποίο θα την εκτελέσει.
Εφοδιασμένοι λοιπόν με τις βασικές έννοιες γύρω απο το shell θα προχωρήσουμε εστιάζοντας σε ένα συγκεκριμένο shell το οποίο λέγεται bash
.
Ο λόγος που εστιάζουμε στο bash
είναι οτι πρόκειται για το πιο ευρέως διαδεδομένο interactive shell άρα είναι πολύ πιθανό να το συναντήσεις στην καθημερινότητα σου.
Πέραν αυτού υπάρχουν πάρα πολλά προγράμματα γραμμένα για αυτό τα οποία θα είναι πολύ χρήσιμο να είσαι σε θέση να καταλάβεις και να επεκτείνεις.
Υπάρχουν διάφορα άλλα ενδιαφέροντα shell στα οποία μπορεί να θέλεις να ρίξεις μια ματιά όπως:
- fish - η δικιά μου προσωπική επιλογή για interactive shell
- zsh - default user shell στο macOS απο το Catalina και μετά
- nushell - μια φρέσκια οπτική για το πως να υλοποιήσεις ένα shell
- oil - ένα ενδιαφέρον πειραματικό shell το οποίο "αγκαλιάζει" το
bash
-
Το άρθρο είναι γραμμένο χρησιμοποιώντας
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
το οποίο τις εκτελεί.
Αυτές χωρίζονται επίσης σε κατηγορίες ανάλογα το είδος τους:
- Builtin commands. Είναι commands τα οποία έχουν υλοποιηθεί και ενσωματωθεί στο ίδιο το
bash
. Συνήθως είναι εντολές που είναι πιο εύκολο να υλοποιηθούν εντός του shell (πχcd
,pwd
,echo
). - Keywords. Είναι commands τα οποία συνήθως επηρρεάζουν το control flow του προγράμματος (πχ
if
,for
,function
) - Functions. Είναι commands τα οποία έχουν δηλωθεί μέσα στο shell και ουσιαστικά είναι λίστες απο άλλα commands τα οποία εκτελούνται όταν χρησιμοποιηθεί το όνομα του function σαν command.
- Executable files. Αν το command είναι το όνομα ενός εκτελέσιμου αρχείο το οποίο βρίσκεται σε κάποιο path το οποίο περιέχεται στην μεταβλητή
PATH
τότε τοbash
θα το τρέξει. - Shell scripts. Αν ισχύουν τα ίδια με τα executable files αλλά το αρχείο δεν είναι εκτελέσιμο τότε το
bash
θα θεωρήσει οτι πρόκειται για shell script και θα προσπαθήσει να εκτελέσει μια μια τις γραμμές του αρχείου σαν commands.
Για να μάθεις το είδος ενός 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 μεγάλες κατηγορίες:
- Τα global variables τα οποία είναι προσβάσιμα απο οποιοδήποτε σημείο ενώς
bash
προγράμματος και μπορούν να αρχικοποιηθούν πριν την εκτέλεση του προγράμματος. - Τα local variables τα οποία μπορούν να δημιουργηθούν και είναι προσβάσιμα μόνο μέσα στα όρια ενός function αλλά και των function που θα καλέσει αυτό το function.
Υπάρχουν πολλοί τρόποι να δηλώσουμε μεταβλητές στο 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 της επόμενης φάσης.
Οι φάσεις αυτές, καθώς και μια ιδέα για το τι γίνεται περίπου σε κάθε φάση, συνοπτικά είναι οι εξής:
- 1η φάση
- brace expansion 3.5.1 - Κάνει expand patterns όπως
mkdir a/{b,c}
σεmkdir a/b a/c
.
- brace expansion 3.5.1 - Κάνει expand patterns όπως
- 2η φάση
- tilde expansion 3.5.2 - Κάνει expand το
~
στο home directory του χρήστη. - parameter and variable expansion 3.5.3 - Κάνει expand το
$VAR_NAME
στο περιεχόμενο της μεταβλητήςVAR_NAME
. - command substitution 3.5.4 - Κάνει expand το
$(command)
στο output τουcommand
. - arithmetic expansion 3.5.5 - Κάνει expand αριθμητικά expression όπως
$(( 4 + 3 ))
στο αποτέλεσμα τους, εδώ7
. - process substitution 3.5.6 - Κάνει expand το
<(command)
στο path για ένα file το οποίο περιέχει το output τουcommand
.
- tilde expansion 3.5.2 - Κάνει expand το
- 3η φάση
- word splitting 3.5.7 - "Σπάει" τα αποτελέσματα των προηγούμενων expansion που δεν βρίσκονταν μέσα σε
""
σε πολλαπλές λέξεις.
- word splitting 3.5.7 - "Σπάει" τα αποτελέσματα των προηγούμενων expansion που δεν βρίσκονταν μέσα σε
- 4η φάση
- filename expansion 3.5.8 - Κάνει expand κάποια ειδικά pattern στα αρχεία τα οποία ταιριάζουν με το pattern.
⚠️ Ξανατονίζω οτι τα αποτελέσματα ενώς προηγούμενου 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 το οποίο αυτοματοποιεί κάτι.
Εις το επανιδείν...