Παρουσίαση bash/sed/awk

Από Κοινότητα Ελεύθερου Λογισμικού ΕΜΠ
Μετάβαση σε: πλοήγηση, αναζήτηση

Το scripting σε bash, σε συνδυασμό με το sed και το awk, αποτελεί ένα πολύ δυνατό εργαλείο για κάθε διαχειριστή Unix. Επιπλέον μπορεί να αυτοματοποιήσει εύκολα και γρήγορα πολλές καθημερινές εργασίες ενός απλού χρήστη.


Παρουσίαση 06/12/07

Εισαγωγικά

BASH

Το bash (Bourne Again SHell) αποτελεί το defacto interactive shell (κέλυφος) για την πλειοψηφία των διανομών GNU/Linux. Εκτός από interactive shell, αποτελεί και διερμηνέα για αντίστοιχα shell scripts, που δεν είναι παρά αλληλουχίες εντολών, ανακατεμένες με τα κατάλληλα builtin constructs, που θυμίζουν πολύ γλώσσες προγραμματισμού σαν τη C. Έτσι υπάρχουν builtins για loops (for, while), conditional execution (if, case), ανάθεση και αποτίμηση μεταβλητών, ορισμό συναρτήσεων, και άλλα.

SED

O sed (Stream EDitor) είναι ένας non-interactive επεξεργαστής κειμένου. Μπορεί να λειτουργήσει με αρχεία ή είσοδο από κάποιο pipe. Έχει τη δική του γλώσσα εντολών, με κυριότερη την s/<regex_pattern>/<replacement>/ που αντικαθιστά το κείμενο που ταιριάζει στο regular expression <regex_pattern> με το <replacement>. Η εντολές μπορούν να δοθούν από την γραμμή εντολών σαν παράμετρος, ή να διαβαστούν από κατάλληλο αρχείο (sed script).

AWK

Με τον όρο awk αναφερόμαστε στη γλώσσα προγραμματισμού που δημιούργησαν οι Alfred Aho, Peter Weinberger, και Brian Kernighan (απ' όπου πήρε και το όνομα της) και την αντίστοιχη εντολή που καλεί τον διερμηνέα της. Είναι σχεδιασμένη κυρίως για εύκολη επεξεργασία κειμένου, αλλά μπορεί να χρησιμοποιηθεί σαν γλώσσα γενικής χρήσης. Όπως το sed, οι εντολές μπορούν να δοθούν σαν παράμετρος στην γραμμή εντολών ή να διαβαστούν από κατάλληλο αρχείο (awk script).

Regular Expressions

Κοινός παρανομοστής ανάμεσα στο sed και awk, είναι οι regular expressions (κανονικές εκφράσεις), μια domain specific language που χρησιμοποιείται για την περιγραφή συμβολοσειρών, και έχει ευρεία εφαρμογή σε εφαρμογές αναζήτησης και επεξεργασίας κειμένου.


Προσοχή στο πέρασμα προγραμμάτων sed/awk από την γραμμή εντολών. Χαρακτήρες απαραίτητοι στην σύνταξη τους, έχουν συνήθως ειδική σημασία και στο shell, οπότε πρέπει είτε να γίνουν escaped (συνήθως με το backslash \) είτε να μπει το πρόγραμμα σε μονά (single) quotes.

Παράδειγμα

Ακολουθεί ένα απλό παράδειγμα που συνδυάζει και τα τρία εργαλεία για να μετονομάσει μαζικά κάποια αρχεία, με βάση προκαθορισμένους κανόνες.

bash part

rename.sh:

#!/bin/bash
# File: rename.sh

for file in "$@"; do
    name=`basename "$file" | ./dots.awk | ./replace.sed`
    dir=`dirname "$file"`
    if [ "." != "$dir" ]; then # Όταν δεν είναι το current directory
        name="$dir/$name"
    fi
    if [[ "$file" != "$name" ]]; then
        echo "Renaming $file to $name"
        mv "$file" "$name"
    fi
done

Αρχικά αξίζει να αναφερθούμε στη μορφή της 1ης γραμμής, που είναι κοινή και για τα τρία αρχεία. Κατ'αρχάς αποτελεί σχόλιο (αυτό εξασφαλίζεται με το #), αλλά το ! που ακολουθεί, σε συνδυασμό με το γεγονός ότι είναι η πρώτη γραμμή, λέει στο λειτουργικό σύστημα ποιο πρόγραμμα να καλέσει για να ερμηνεύσει όσα ακολουθούν. Έτσι κάνοντας τα αρχεία εκτελέσιμα, αρκεί να γράψουμε ./rename.sh (ή rename.sh αν το βάλουμε κάπου στο $PATH) και θα εκτελεστεί από το bash. Ισοδύναμα δηλαδή με το να γράφαμε:

$ bash rename.sh

με τη διαφορά ότι μέσα στο script πρέπει να γράψουμε ακριβώς που βρίσκεται το αρχείο που θα εκτελεστεί, δηλαδή /bin/bash. Αυτό μπορούμε να το βρούμε με την εντολή which:

$ which bash
/bin/bash

Αντίστοιχα για τα άλλα scripts, γράφουμε την αντίστοιχη εντολή, που είναι παρόμοια με την διαφορά ότι χρειάζεται η παράμετρος -f, αφού από την γραμμή εντολών θα γράφαμε

$ awk -f dots.awk

ή

$ sed -f replace.sed

Ακολουθεί ένα απλό for loop, που διαβάζεται ως εξής: Για κάθε όρισμα, αποθήκευσε το στην μεταβλητή file και κάνε ότι περικλείεται μέσα στο do και done. Η μεταβλητή @ είναι μια ειδική μεταβλητή του bash, ένας πίνακας που περιέχει όλα τα ορίσματα που πέρασε ο χρήστης στη γραμμή εντολών. Με το $ παίρνουμε την τιμή μιας μεταβλητής. Τα διπλά εισαγωγικά είναι απαραίτητα αν θέλουμε το πρόγραμμα να δουλέψει σωστά για ονόματα αρχείων με κενά.

Η πρώτη γραμμή στο loop αναθέτει σε μια νέα μεταβλητή name το αποτέλεσμα μιας σειράς εντολών. Η ανάθεση γίνεται με το σύμβολο =, χωρίς κενά πριν ή μετά. Προσοχή, κατά την ανάθεση δεν βάζουμε το $ (όπως θα περίμενε ίσως κάποιος χρήστης php/perl). Για να συμπεριλάβουμε κενά στην τιμή της μεταβλητής, χρειαζόμαστε μονά ή διπλά quotes.

name=my name   # Λάθος, η name περιέχει το αλφαριθμητικό 'my'
name="my name" # Σωστό, η name περιέχει το αλφαριθμητικό 'my name'
name='my name' # Επίσης σωστό

Ότι περικλείεται σε backtics (`) θα εκτελεστεί σαν εντολή και θα αντικατασταθεί από το αποτέλεσμα της εκτέλεσης. Έτσι η name περιέχει τώρα το αποτέλεσμα της εντολής:

$ basename "$file" | ./dots.awk | ./replace.sed

H basename επιστρέφει το τελευταίο μέρος ενός file path, αφού θέλουμε να επέμβουμε μόνο στο αρχείο, όχι στα ονόματα των directories που το περιέχουν. Το αποτέλεσμα περνάει σαν είσοδος στο dots.awk, και η έξοδος αυτού γίνεται τέλος είσοδος του replace.sed. Έτσι η name περιέχει τώρα το μετονομασμένο αρχείο. Τα διπλά εισαγωγικά στην αποτίμηση της file χρειάζονται πάλι για να μην 'σπάσει' το περιεχόμενο της αν περιέχει κενά.

Στην επόμενη γραμμή αποθηκεύουμε το directory part του ονόματος στην μεταβλητή dir, με την dirname που είναι το δυαδικό της basename. Αν αυτό δεν υπάρχει, επιστρέφει '.' (που συμβολίζει το current directory).

Στις επόμενες τρεις γραμμές χρησιμοποιούμε ένα if construct που διαβάζεται: Αν η dir δεν είναι ίση με '.', τότε ανάθεσε στη name την τιμή "$dir/$name". Εισάγει δηλαδή το directory path πριν το νέο όνομα (κάτι απαραίτητο για τη μετονομασία μετά, αλλιώς το αρχείο θα μεταφερόταν στο current working directory κατά το rename). Ο λόγος που δεν το εισάγουμε αν είναι '.' (σωστό όσον αφορά το rename), είναι για την σύγκριση που ακολουθεί. Προσέξτε ότι η δομή if τερματίζεται με το ανάστροφο της, fi (αντίστοιχα η case με esac).

Στην τελευταία σύγκριση, ελέγχουμε αν το παλιό και το νέο όνομα είναι ίδια. Αν δεν είναι, κάνουμε την μετονομασία. Αν είχαμε εισάγει το './' πριν το νέο όνομα, τα ονόματα θα διέφεραν (στη σύγκριση), αλλά θα ήταν ίδια όσον αφορά την mv, και θα έδινε μήνυμα λάθους. Με την εντολή echo απλά τυπώνουμε ένα ενημερωτικό μήνυμα για το χρήστη.

Ο προσεκτικός αναγνώστης θα παρατηρήσει μια μικρή αλλά σημαντική διαφορά στις δύο εκφράσεις if. Στην πρώτη ελέγχουμε με τη δομή [ .. ] (single brackets) ενώ στη δεύτερη με την [[ ... ]] (double brackets). Η πρώτη είναι ισοδύναμη με την εντολή test(1), θα μπορούσαμε να είχαμε γράψει δηλαδή (και μερικοί το θεωρούν καθαρότερο):

if test "." != "dir ; then ...

Σημαντική επίσης είναι η σειρά ελέγχου, με τη σταθερά "." να προηγείται. Η δεύτερη είναι builtin εντολή του bash, που επεκτείνει την πρώτη. Το γιατί το γράψαμε έτσι, εξηγείται αναλυτικά στο άρθρο Bash Pitfalls, και η απλή εξήγηση είναι, για να μη μπερδέψουμε την test με περίεργους χαρακτήρες στη μεταβλητή (π.χ. -). Όπου είναι δυνατόν προτιμάται η πρώτη μορφή, για λόγους συμβατότητας με άλλα shells (όπως το sh). Σε αυτή την περίπτωση, το δεύτερο if θα γινόταν:

if [ x"$file" != x"$name" ]; then ...

Ακόμα σημαντικότερο όμως, είναι να διατηρούμε ένα ομοιόμορφο και συνεπές coding style. Οπότε, επιλέγουμε τι θέλουμε -συμβατότητα ή διευκόλυνση ή/και αναγνωσιμότητα- και χρησιμοποιούμε τις ίδιες δομές με συνέπεια (το παράδειγμα μας είναι επιτηδευμένα ασυνεπές, ώστε να αναδείξει τη διαφορά).

awk part

dots.awk:

#!/usr/bin/awk -f
# File: dots.awk

BEGIN {
    # Οι τελείες είναι το νέο διαχωριστικό για fields.
    FS="."
    # Το underscore θα αντικαταστήσει όλες τις τελείες.
    replacement="_"
    # Εκτός από 'number' τελείες στο τέλος.
    number=1
}

{
    if (NF > number + 1) {
        upto = NF - number
        for (i = 1; i <= upto; i++) {
            if (i != upto)
                printf("%s%s", $i, replacement);
            else
                printf($i);
        }
        for (; i <= NF; i++)
            printf("%s%s", FS, $i);
        printf("\n");
    } else {
        print $0
    }
}

END { }

Ένα awk script αποτελείται από τρία μέρη. Στο πρώτο, που περικλείεται από τη δήλωση BEGIN { ... }, βάζουμε τυχόν αρχικοποιήσεις απαραίτητες για το υπόλοιπο script. Στο δεύτερο { ... } τοποθετούμε τις δηλώσεις που θα εκτελεστούν για κάθε record εισόδου (συνήθως κάθε γραμμή, αλλά όχι απαραίτητα), και στο τρίτο END { ... } δηλώσεις που θα εκτελεστούν αφού επεξεργαστεί όλη είσοδος. Το πρώτο και τρίτο μέρος μπορούν να παραλειφθούν τελείως, ή να παραμείνουν κενά (όπως το END { }) στο παράδειγμα μας. Όπως και στο bash, οτιδήποτε ακολουθεί τον χαρακτήρα # αποτελεί σχόλιο.

Η είσοδος ενός προγράμματος awk χωρίζεται εσωτερικά σε δύο επίπεδα επεξεργασίας. Το πρώτο ονομάζεται records, και by default, ένα record αντιστοιχεί σε μία γραμμή εισόδου. Το δεύτερο ονομάζεται fields, και by default, ένα field αντιστοιχεί σε μια συμβολοσειρά που δεν περιέχει whitespace (δηλαδή spaces ή tabs). Ο διαχωρισμός σε records και fields ελέγχεται από δύο ειδικές μεταβλητές, RS και FS αντίστοιχα (Record Separator, Field Separator). Αυτές μπορεί να είναι μια ακολουθία χαρακτήρων, ή μία regular expression (οι προεπιλεγμένες τιμές όπως ίσως μαντέψατε είναι RS='\n' και FS='[ \t]'). Σχηματικά η διαδικασία είναι η ακόλουθη (η ειδικές μεταβλητές NR και NF περιέχουν τον αντίστοιχο αριθμό records και fields):

input --> record1  --> field1, ... fieldNF
          ...
          recordNR --> field1, ... fieldNF

Η σύνταξη των δηλώσεων είναι πολύ κοντά σε αυτή της C, και δεν θα ξενίσει χρήστες που έχουν μια εξοικείωση με τη γλώσσα. Στο πρώτο μέρος λοιπόν, αναθέτουμε τιμές σε τρεις μεταβλητές. Η πρώτη όπως περιγράψαμε είναι η ειδική μεταβλητή που χωρίζει τα fields, και τη θέτουμε στην '.', οι άλλες δύο ορίζονται από εμάς (replacement, number), και απλά τις αρχικοποιούμε. Αξίζει να σημειωθεί ότι η awk αναγνωρίζει τρεις τύπους μεταβλητών, συμβολοσειρές, αριθμούς κινητής υποδιαστολής, και πίνακες.

Στη συνέχεια πραγματοποιούμε την κυρίως επεξεργασία: Αν ο αριθμός των fields είναι μεγαλύτερος από τον επιθυμητό, συνενώνουμε τον επιθυμητό αριθμό από fields με τον χαρακτήρα αντικατάστασης, και τα υπόλοιπα με τον αρχικό (δηλαδή την τελεία). Σε άλλη περίπτωση τυπώνουμε το record όπως είναι. Πρόσβαση σε κάθε field έχουμε με την ειδική σύνταξη $n όπου n ο αριθμός του field, ξεκινώντας τη μέτρηση από το 1. Μπορούμε να χρησιμοποιήσουμε και μεταβλητές στη θέση του n, όπως κάνουμε στο παράδειγμα μας με την μεταβλητή i. Το field 0 μας δίνει όλο το record αυτούσιο. Στο παράδειγμα μας κρατάμε μόνο την τελευταία τελεία, αλλά αλλάζοντας το number θα μπορούσαμε να κρατήσουμε όσες τελείες θέλουμε από το τέλος, π.χ. για να δουλέψει σωστά το script σε αρχεία όπως file.tar.gz, θα ήταν προτιμότερο να θέσουμε number=2.

Σημειώστε τη χρήση δύο διαφορετικών συναρτήσεων για έξοδο, printf και print. Η πρώτη είναι ίδια με την αντίστοιχη της C, ενώ η δεύτερη είναι για διευκόλυνση και έχει απλούστερη σύνταξη.

Αξίζει να σημειωθεί ότι μπορούμε να δώσουμε τιμές σε μεταβλητές και έξω από το script, κατά την κλήση του. Έτσι, για να αλλάξουμε την number ή την replacement, δεν είναι ανάγκη να πειράξουμε το script, αρκεί να το καλέσουμε ως εξής:

$ echo file.with.dots.tar.gz | ./dots.awk number=2 replacement=' '
file with dots.tar.gz

sed part

replace.sed:

#!/bin/sed -f
# File: replace.sed

# Αντικαθιστούμε συνεχόμενα underscores και whitespace με ένα κενό:
s/[_ \t]\+/ /g

# Μετατρέπουμε κεφαλαία σε πεζά γράμματα:
y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/
y/ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΆΈΏΌΉΎΫ/αβγδεζηθικλμνξοπρστυφχψωάέώόήύϋ/

Δεν υπάρχουν εκπλήξεις εδώ. Όπως και στα προηγούμενα προγράμματα, ο χαρακτήρας # ξεκινάει ένα σχόλιο. Γράφουμε μια εντολή ανά γραμμή, ή τις διαχωρίζουμε με το σύμβολο ; (το ίδιο ισχύει και στο bash και το awk).

Η εντολή s/REGEXP/REPLACEMENT/FLAGS (substitute) αντικαθιστά το κείμενο που ταιριάζει στην regular expression REGEXP με αυτό του REPLACEMENT. Τα FLAGS είναι προαιρετικές παράμετροι της εντολής. Στο παράδειγμα μας η έκφραση [_ \t]\+ ταιριάζει τον χαρακτήρα '_' ή τον ' ' ή τον '\t' (tab), μία ή περισσότερες φορές (\+), και τους αντικαθιστά με ένα κενό. Το flag g (global;) σημαίνει να μην σταματήσει μετά την πρώτη αντικατάσταση, αλλά να κάνει όσες μπορεί. Για παράδειγμα:

$ echo "hello    _ \t __world__." | sed 's/[_ \t]\+/ /' 
hello world__.
$ echo "hello    _ \t __world__." | sed 's/[_ \t]\+/ /g'
hello world .

Η εντολή y/SOURCE-CHARS/DEST-CHARS/ αντικαθιστά κάθε χαρακτήρα από τους SOURCE-CHARS που συναντά με τον αντίστοιχο από τους DEST-CHARS (προφανώς πρέπει να παρέχουμε τον ίδιο αριθμό χαρακτήρων για τα SOURCE-CHARS και DEST-CHARS).

Σημείωση

Ομολογουμένως οι τελευταίες δύο εντολές δεν είναι ο απλούστερος, ούτε ο καλύτερος τρόπος για να μετατρέψουμε κεφαλαία σε πεζά (ή το αντίστροφο). Κατ'αρχάς πρέπει να γράψουμε όλους τους χαρακτήρες. Επιπλέον, λειτουργεί μεν για ελληνικούς και αγγλικούς χαρακτήρες, αλλά όχι για άλλους, και τέλος δεν θα δουλέψει σωστά αν έχουμε σώσει π.χ. το script μας σε UTF8 και το χρησιμοποιήσουμε σε ένα σύστημα με ISO-8859-7 locale. Μια απλή υλοποίηση θα ήταν με το πρόγραμμα tr(1), ως εξής:

echo "$name" | tr [:upper:] [:lower:]

Σε κάποια shell (π.χ. zsh) ίσως είναι απαραίτητα μονά quotes, δηλαδή '[:upper:]' κλπ. Η εντολή αυτή, ενώ είναι locale specific (θα δουλέψει δηλαδή για οποιαδήποτε γλώσσα), δυστυχώς δεν θα δουλέψει με UTF8 locale. Μια άλλη λύση είναι κάποιο builtin του shell, το bash δεν διαθέτει κατάλληλη σύνταξη, αλλά στο zsh για παράδειγμα, θα αρκούσε ${name:l}. Μια ακόμα λύση, που είναι αρκετά συμβατή είναι οι συναρτήσεις toupper() και tolower() που παρέχει το awk. Δυστυχώς δεν χειρίζεται διαφορετικά locales σε όλες τις εκδόσεις (στο gawk όμως δουλεύει σωστά). Έτσι για παράδειγμα θα μπορούσαμε να γράψουμε:

 echo "$name" | awk '{ print tolower($0) }'

ή να το ενσωματώσουμε στο script που παραθέσαμε παραπάνω.

Pitfalls

Γενικά

Συνίσταται η προσεκτική μελέτη του Bash Pitfalls, προς αποφυγή των συνηθέστερων λαθών κατά τον προγραμματισμό σε bash.

Μην γράφετε #!/bin/sh (αντί για #!/bin/bash) στην αρχή ενός bash script, εκτός αν είστε σίγουροι ότι το script σας είναι συμβατό με το Bourne shell. Στις περισσότερες διανομές GNU/Linux το sh είναι symbolic link στο bash, αλλά σε ειδικές περιπτώσεις μπορεί να χρησιμοποιείται κάποιο άλλο, όπως το dash. Επιπλέον, άλλα Unix συστήματα (όπως τα διάφορα BSD variants, ή το Solaris), έχουν δικές τους υλοποιήσεις του sh.

Απόδοση

Αν και τα περισσότερα shell scripts δεν είναι performance critical εφαρμογές, και πολύ συχνά δεν είναι κάτι παραπάνω από μια quick & dirty λύση σε ένα εφήμερο πρόβλημα, πολλές φορές αξίζει μια προσεκτικότερη μελέτη της απόδοσης τους.

Μια συνηθισμένη εγκατάσταση GNU/Linux είναι πιθανό να περιέχει εκατοντάδες shell scripts, και πολλά εκτελούνται κατά την εκκίνηση του συστήματος, ή σε τακτά χρονικά διαστήματα (με cronjobs κλπ). Η επίδραση τους στην απόδοση ενός συστήματος σε τέτοιες περιπτώσεις μπορεί να είναι σημαντική.

Επιπλέον, πολλές φορές κατά τον έλεγχο καλής λειτουργίας ενός script (και ενός προγράμματος γενικότερα), παραμελούνται για ευκολία σημαντικές περιπτώσεις, όπως π.χ. πολύ μεγάλα αρχεία εισόδου. Μπορεί ένα script να τρέχει ικανοποιητικά με είσοδο ένα αρχείο μερικά kilobytes (ή ακόμα και megabytes), αλλά τι γίνεται όταν του δοθεί ένα αρχείο αρκετά gigabytes (συνηθισμένο σενάριο για log files αρκετών εφαρμογών). Πολλές φορές, μικρές και φαινομενικά ασήμαντες αλλαγές μπορεί να επιφέρουν βελτίωση πολλών τάξεων μεγέθους.

Σε πολλά scripts καθοριστικό παράγοντα στην απόδοση παίζει ο αριθμός των προγραμμάτων που καλούνται. Μία μόνο γραμμή μπορεί να δημιουργεί αρκετές νέες διεργασίες, με κόστος δυσανάλογο προς την επερξεργασία που πραγματοποιεί η κάθε μία, όπως αναδυκνύει (σε διαφορετικό context ομολογουμένως) η εργασία Recursive Make Considered Harmful. Επιπλέον πολλές φορές χρησιμοποιούνται περιττές εντολές, φαινόμενο που παρατηρείται ιδιαιτέρως συχνά με την εντολή cat(1). Αναλυτικά παράδειγματα μπορούν να βρεθούν στο Useless Use of Cat Award. Μια απλή περίπτωση είναι οι ακόλουθες (ισοδύναμες) γραμμές:

$ cat file | grep something | awk '{ print $2 }'
$ grep something file | awk '{ print $2 }'
$ awk '/something/ { print $2 }' file

Ενδιαφέρουσες παρατηρήσεις στο θέμα παρουσιάζονται στο άρθρο The Relativity of Performance Improvements.

Παραδόξως, η τρίτη εντολή, αν και πιο σύντομη και κομψή, μπορεί να είναι πιο αργή από την δεύτερη, αναλόγως με την υλοποίηση του awk, και το είδος της regular expression (το gawk για παράδειγμα φαίνεται πιο αργό σε μερικά πρόχειρα benchmarks, σε αντίθεση με το mawk που είναι στην ίδια τάξη μεγέθους).

Όσο μεγαλώνει το μέγεθος της εισόδου, ή γίνονται πιο εξεζητημένες οι λειτουργίες που θέλουμε (και πολυπλοκότερες οι regular expressions που χρησιμοποιούμε), ίσως συμφέρει να κινηθούμε αντίθετα με την παραπάνω λογική, και να σπάσουμε μια εντολή σε μια αλληλουχία απλούστερων εντολών. Ένα χαρακτηριστικό παράδειγμα παρουσιάζεται στο άρθρο The Treacherous Power of Extended Regular Expressions.

Newlines

Έστω ότι θέλετε να αντικαταστήσετε τους χαρακτήρες αλλαγής γραμμής (\n στο Unix), με κάποιους άλλους χαρακτήρες. Μια πρώτη ιδέα θα ήταν να χρησιμοποιήσετε το sed, π.χ.

sed 's/\n/-/g' file

Αυτό όμως δεν θα έχει κανένα αποτέλεσμα, αφού το sed λειτουργεί σε μία γραμμή εισόδου κάθε φορά, οπότε δεν βλέπει ποτέ το χαρακτήρα αλλαγής γραμμής. Σωστότερη και απλούστερη λύση είναι να χρησιμοποιήσετε το tr:

tr '\n' '-' < file

για να αντικαταστήσετε κάθε χαρακτήρα αλλαγής γραμμής με μια παύλα, ή

tr -d '\n'

αν απλά θέλετε να σβήσετε όλους τους χαρακτήρες αλλαγής γραμμής. Τι γίνεται όμως αν θέλουμε περισσότερους του ενός χαρακτήρες αντικατάστασης, έστω δύο παύλες; Το tr δεν θα δουλέψει σε αυτή την περίπτωση. Το sed θα μπορούσε να χρησιμοποιηθεί, με πιο εξωτικές εντολές, π.χ.

sed ':j N; s/\n/--/; bj' file

Με το :j ορίζουμε μια ετικέτα j, με το N διαβάζουμε στο buffer εισόδου την επόμενη γραμμή, έπειτα κάνουμε την αντικατάσταση, και με το bj κάνουμε branch στην ετικέτα j (κάτι σαν goto), και πάλι από την αρχή. Δεν φαίνεται όμως ιδιαίτερα αποδοτικό, και δεν είναι, μάλιστα για αρχεία μερικών megabyte, αυτή η επεξεργασία μπορεί να πάρει ώρες (αφού, πέρα από τις απαιτήσεις σε μνήμη, σε κάθε επανάληψη η εντολή s διαβάζει από την αρχή τον buffer εισόδου που είχε επεξεργαστεί και πριν). Επιπλέον, κάποιες υλοποιήσεις του sed (όχι το GNU sed) έχουν όριο στο συνολικό μέγεθος των buffer. Παρ' όλα αυτά, το sed αποδεικνύεται αρκετά πιο ευέλικτο, απ' ότι σε πρώτη ματιά. Με λίγο περισσότερη προσπάθεια, βρίσκεται αποδοτικότερη λύση (με το πρόβλημα ότι δεν αντικαθιστά το τελευταίο '\n', καθώς και αυτό που αναφέρθηκε για τα buffers)[1]:

sed -n 'H; ${g; s/\n//; s/\n/--/g; p}' file

Με την παράμετρο -n λέμε στο sed να μην τυπώνει την (επεξεργασμένη ή μη) είσοδο του, εκτός αν του το πούμε ρητά με την εντολή p. Η εντολή H προσθέτει τα περιεχόμενα του pattern buffer σε έναν βοηθητικό (hold buffer). Τέλος, η παράμετρος $ δηλώνει ότι η εντολή που ακολουθεί (ό,τι περικλείεται σε { }) θα εφαρμοστεί μόνο στην τελευταία γραμμή, όπου με την εντολή g, επαναφέρουμε τα περιεχόμενα του hold buffer (που τώρα περιέχει όλη την είσοδο, και έναν επιπλέον χαρακτήρα αλλαγής γραμμής στην αρχή) στον pattern buffer, για να κάνουμε την τελική επεξεργασία (τα δύο s) και να τυπώσουμε το αποτέλεσμα (p).

Σε κάθε περίπτωση όμως δεν συγκρίνεται σε απλότητα ή ταχύτητα με την ακόλουθη λύση σε awk:

awk '{ printf("%s--", $0) }' file

Ασφάλεια

Ιδιαίτερη προσοχή πρέπει να δίνεται όταν εκτελούμε ένα script χωρίς να γνωρίζουμε και να κατανοούμε πως λειτουργεί. Σε καμία περίπτωση δεν πρέπει να εκτελούμε script (και γενικότερα οποιοδήποτε πρόγραμμα) αγνώστων ως root, πριν σιγουρευτούμε ότι δεν είναι επιβλαβές για το σύστημα μας, και ότι είναι όντως απαραίτητο να το τρέξουμε ως root, και όχι ως απλός χρήστης.

Μην χρησιμοποιείται setuid/setgid scripts, στο GNU/Linux δεν θα δουλέψουν έτσι κι αλλιώς, καθώς αποτελούν security hole [2].

Εξετάζετε τα glob patterns προσεκτικά, και αποφεύγετε να χρησιμοποιείτε πολύ γενικά patterns, ιδιαίτερα σε εντολές που μπορεί να έχουν μη αντιστρέψιμες συνέπειες. Για παράδειγμα, έστω ότι θέλουμε να σβήσουμε όλα τα αρχεία που αρχίζουν με foo και έχουν κατάληξη txt ή log:

rm foo*           # Λάθος, σβήνουμε όλα τα αρχεία που ξεκινούν με foo
rm foo*.???       # Λάθος, σβήνουμε όλα τα αρχεία που ξεκινούν με foo και έχουν κατάληξη με τρεις χαρακτήρες
rm foo*{txt,log}  # Λάθος, σβήνουμε και αρχεία σαν το footxt
rm foo*.{txt,log} # Σωστό

Δείτε επίσης

  • zsh Ένα ακόμα shell, που δανείζεται τα καλύτερα στοιχεία από τα bash, ksh και tcsh.
  • fish Ένα shell που στοχεύει στην απλή και καθαρή σύνταξη, και την εύκολη χρήση.
  • Perl Μια γλώσσα προγραμματισμού που μεταξύ άλλων συνδυάζει τη δύναμη των bash, sed, awk.
  • Unix Shell Ιστορική αναδρομή του Unix shell και εκτενής λίστα με διαφορετικά shells.

Αναφορές

  1. Κανάλι #sed στο irc.freenode.net
  2. Secure-Programs-HOWTO