Shellkonfiguration – Übersicht

Aliase und Funktionen (bash)

Aliase sind alternative Aufrufformen für ein Kommando oder eine Kommandozeile (bzw. Teile davon). Typischerweise sind es Abkürzungen für sehr häufig benötigte Kommandos (z.B. ls mit Optionen), selten benutzte Kommandos, deren Details man leicht vergisst, oder sehr umfangreiche Kommandozeilen. Ein Alias ist eine rein textuelle Ersetzung, vergleichbar mit einem Readline-macro. Insbesondere kann ein Alias nicht mit Argumenten arbeiten (man kann allerdings weitere Worte in der Kommandozeile haben, die dann von dem aus dem Alias generierten Kommando ausgewertet werden).

Eine Shellfunktion ist ein mächtigeres Konstrukt, das ein Alias in fast jeder Hinsicht ersetzen kann. Insbesondere können Funktionen mit Argumenten arbeiten.

Theoretisch können auch Aliase komplexen Inhalt haben, aber typischerweise verbergen sich hinter einem Alias kurze Kommandozeilen und hinter einer Funktion mehrere Kommandos.

Aliase und Funktionen sind eine verbreitete Möglichkeit für Distributionen, das Benutzererlebnis (von Leuten ohne ausgeprägte Shell-Kenntnisse) zu verbessern, indem sinnvolle Optionen voreingestellt werden (z.B. --color=auto für grep).

Wenn ein externes Kommando verwendet werden soll, auch wenn ein Alias oder eine Funktion (oder ein shell builtin) dafür definiert ist, kann man entweder das Kommandowort (teilweise) mit quoting versehen (\grep statt grep) oder das shell builtin command voranstellen: command grep. Dies ist insbesondere relevant, wenn man ein Kommando mit einer Funktion überlagert und innerhalb der Funktion das externe Kommando aufruft.

Aliase

Eine Alias-Definition hat die Form alias ..='cd ..'. Die Definition eines konkreten Alias kann man sich mit type cmd oder alias cmd anzeigen lassen. Löschen kann man den Alias mit unalias cmd. Eine Liste aller Alias-Definitionen kann man mit alias ausgeben. Die Auswertung von Aliasen erfolgt standardmäßig nur in interaktiven Shells (lässt sich mit shopt expand_aliases konfigurieren). In Shellscripten ist die Nutzung von Funktionen und Variablen sinnvoller.

Normalerweise wird immer nur das erste Wort eines simple command darauf überprüft, ob ein Alias dafür definiert ist. Wenn aber das letzte Zeichen einer Alias-Definition ein Leerzeichen ist und auf das Alias-Wort ein weiteres Wort folgt, wird auch das darauf überprüft, ob dafür ein Alias definiert ist. In dieser Weise kann man also auch für Argumente ein Alias anlegen. Ich habe von dieser Möglichkeit bisher nie Gebrauch gemacht, aber das könnte sinnvoll sein, wenn man häufig dieselben langen Argumente für ein Kommando hat. Ein einzelnes langes Argument könnte man genauso in eine Variable packen (was den Vorteil hätte, dass man sie überall verwenden kann). Aber etwas wie arg1 tippt sich angenehmer als ${arg1[@]}:

ec:0   01:08:07  hl@notebook:~
start cmd:> alias ald='ls -ld '

ec:0   01:08:23  hl@notebook:~
start cmd:> alias arg1='/etc/pki/trust/anchors /usr/lib/os-probes/mounted/efi /usr/share/vim/vim91/autoload'

ec:0   01:09:09  hl@notebook:~
start cmd:> set -x

ec:0   01:09:11  hl@notebook:~
start cmd:> ald arg1
+ ls -ld /etc/pki/trust/anchors /usr/lib/os-probes/mounted/efi /usr/share/vim/vim91/autoload
drwxr-xr-x 1 root root    0 26. Jul 17:15 /etc/pki/trust/anchors
drwxr-xr-x 1 root root   36 28. Apr 2023  /usr/lib/os-probes/mounted/efi
drwxr-xr-x 1 root root 1190 16. Aug 01:43 /usr/share/vim/vim91/autoload

ec:0   01:13:44  hl@notebook:~
start cmd:> arg1=(/etc/pki/trust/anchors /usr/lib/os-probes/mounted/efi /usr/share/vim/vim91/autoload)

ec:0   01:13:46  hl@notebook:~
start cmd:> ald ${arg1[@]}
drwxr-xr-x 1 root root    0 26. Jul 17:15 /etc/pki/trust/anchors
drwxr-xr-x 1 root root   36 28. Apr 2023  /usr/lib/os-probes/mounted/efi
drwxr-xr-x 1 root root 1190 16. Aug 01:43 /usr/share/vim/vim91/autoload

Abkürzungen (neue Kommandos)

verbreitete Aliase
Alias Definition Anmerkungen
.. cd ..
... cd ../..
l ls -lhL o.ä.
ll ls -l o.ä.
la ls -la o.ä.
meine Aliase
Alias Definition Anmerkungen
r realpath Häufig brauche ich den kompletten Pfad einer Datei im aktuellen Verzeichnis. realpath erspart einem das Zusammenkopieren des Verzeichnispfads und des Dateinamens.
ssrv systemctl --type=service list-units Funktioniert auch mit filtern pattern: ssrv 'sy*'

Voreinstellungen

verbreitete Aliase
Alias Definition Anmerkungen
grep grep --color=auto
egrep egrep --color=auto
fgrep fgrep --color=auto
meine Aliase
Alias Definition Anmerkungen
less less -WiNS -W markiert die erste ungelesene Zeile; -i sorgt bei Suchen für das Ignorieren von Groß- und Kleinschreibung; -N zeigt Zeilennummern an (kann zur Laufzeit durch wiederholte Eingabe von -n umgeschaltet werden); -S verhindert das Umbrechen von Zeilen, die nicht auf den Bildschirm passen
od od -t c -t x1 Anzeige der einzelnen Bytes sowohl als ASCII / Escape-Sequenz als auch in hex.
ps ps -eo pid,ppid,user,stat,time,etime,args

Leichteres Auffinden "unbekannter" Kommandos

Man kann dem Vergessen von Kommandonamen oder dem Pfad von eigenen Scripten (gerade bei Usern, die sie nur selten verwenden) entgegenwirken, indem man ein einheitliches Präfix definiert (etwa eine Abkürzung des Firmennamens, org-) und für jedes Kommando oder jede Kommandozeile einen Alias definiert. Auf diese Weise kann man sich mittels tab completion trivial die Liste der relevanten Kommandos ausgeben lassen, indem man o r g - Tab Tab eingibt.

programmable completion (tab completion)

Für die einfache Eingabe des Kommandoworts gibt es (ab bash 5.x) eine Alternative zu Aliasen: programmable completion.

Wenn man keine Lust hat, ständig systemctl oder journalctl einzugeben, kann man sich entweder Aliase definieren (alias s=systemctl oder alias j=journalctl) oder programmable completion nutzen, so dass die Eingabe von s Tab das s zu systemctl und die Eingabe von j Tab das j zu journalctl expandiert:

complete -W 'systemctl journalctl' -o bashdefault -I

Die Kombination eines Alias mit programmable completion ermöglicht eine engere Vervollständigungs-Auswahl für den Alias:

ec:0   16:46:50  hl@notebook:~
start cmd:> complete -W '/etc/pki/trust/anchors /usr/lib/os-probes/mounted/efi /usr/share/vim/vim91/autoload' -- -l -c

ec:0   16:47:49  hl@notebook:~
start cmd:> alias -l='ls -lhL'

ec:0   16:47:57  hl@notebook:~
start cmd:> alias -c='cd'

ec:0   16:48:04  hl@notebook:~
start cmd:> -l /
/etc/pki/trust/anchors          /usr/lib/os-probes/mounted/efi  /usr/share/vim/vim91/autoload

Mit etwas mehr Konfigurationsaufwand (einer eigenen Funktion für die tab completion des Alias) kann man den Tippaufwand weiter reduzieren, so dass man die Argumente mit einem einzelnen Buchstaben auswählen kann. Die Shellfunktion:

_PROG_COMPL_ABBR_ARGS () {
        local keys=( e o a )
        local DEFAULT_COMPLETIONS_EVAL_STRING='local -A PROG_COMPL_ABBR_ARGS=(
                [e]=/etc/pki/trust/anchors
                [o]=/usr/lib/os-probes/mounted/efi
                [a]=/usr/share/vim/vim91/autoload
                ['?']=help
        )'
        local word="${COMP_WORDS[$COMP_CWORD]}"
        if ! declare -p PROG_COMPL_ABBR_ARGS >/dev/null 2>&1; then
                eval "$DEFAULT_COMPLETIONS_EVAL_STRING"
        fi
        if [ -z "$word" ]; then
                COMPREPLY=( "${!PROG_COMPL_ABBR_ARGS[@]}" )
                return 0
        fi
        if [ "$word" = '?' ]; then
                COMPREPLY=( )
                echo >&2
                for key in "${keys[@]}"; do
                        printf '%2s : %s\n' "${key}" "${PROG_COMPL_ABBR_ARGS["${key}"]}" >&2
                done
                return 0
        fi
        if [ "${PROG_COMPL_ABBR_ARGS["$word"]:+set}" = 'set' ]; then
                COMPREPLY=( "${PROG_COMPL_ABBR_ARGS["$word"]}" )
        else
                COMPREPLY=( "$word" )
        fi
}

Da die Anzeige der verfügbaren Abkürzungen allein einem nicht wirklich weiterhilft, kann man sich mit ? Tab die Bedeutungen anzeigen lassen.

ec:0   17:45:54  hl@notebook:~
start cmd:> . completion.PROG_COMPL_ABBR_ARGS.sh

ec:0   18:05:48  hl@notebook:~
start cmd:> complete -F _PROG_COMPL_ABBR_ARGS -o filenames -A file -- -l -c

ec:0   18:05:55  hl@notebook:~
start cmd:>

Die Eingabe von - l Space e Tab ändert dann -l e in -l /etc/pki/trust/anchors.

Indem man z.B. mit declare -A PROG_COMPL_ABBR_ARGS=([y]="/etc/systemd/system" ) die assoziative Array-Variable in der aufrufenden Shell setzt, kann man die Standardkonfiguration der Funktion überschreiben.

Shellfunktionen

Shellfunktionen sind, besser als Aliase, dafür geeignet, Funktionalität zu implementieren, die man in der Shell häufig benötigt.

Eine Funktion wird folgendermaßen angelegt (oder überschrieben):

func_name () { : some shell code; }

Die Definition einer Shellfunktion kann man mit type func_name und declare -fp func_name anzeigen lassen.

Shellfunktionen sind typischerweise in großem Umfang im Hintergrund aktiv, ohne dass der Benutzer dies mitbekommt. Dies betrifft vor allem die Nutzung von programmable completion.

Eigene Shellfunktionen

Hier nun einige Beispiele für Shellfunktionen, die ich mir gebaut habe:

screen-Wrapper

Wenn bereits eine screen-Session läuft, dann sollte man durch das Kommando screen mit dieser Session verbunden werden. Diese Funktion prüft, ob bereits eine Session mit dem jeweiligen Namen existiert. Wenn ja, wird mit ihr verbunden; wenn nicht, wird eine gestartet.

Wenn screen in einer root-Shell aufgerufen wird, sollten nicht unterschiedliche User dieselbe Sitzung nutzen. Diese Funktion setzt deshalb den Session-Namen (je nach Konfiguration) auf den Wert in /proc/self/loginuid oder auf den zugehörigen Usernamen.

screen () {
    local session_name loginuid login_name count
    local use_value='name' # 'name' | 'uid'
    if [ "$EUID" -eq 0 ]; then
        if [ -f '/proc/self/loginuid' ]; then
            loginuid="$(< /proc/self/loginuid)"
            if [ "$use_value" = 'name' ]; then
                login_name="$( id -un "$loginuid" 2>/dev/null )"
                if [ -n "$login_name" ]; then
                    session_name="$login_name"
                else
                    session_name="$loginuid"
                fi
            else
                session_name="$loginuid"
            fi
        else
            session_name='root'
        fi
    else
        session_name="$USER"
    fi
    if [ $# -eq 0 ]; then
        count="$(command screen -ls | grep -cE $'^\t''[0-9]+[.]'"${session_name}"$'\t')"
        if   [ "$count" -eq 1 ]; then
            command screen -S "$session_name" -x
        elif [ "$count" -eq 0 ]; then
            command screen -S "$session_name"
        else
            echo "ERROR: Es laufen mehrere screen-Sitzungen mit dem Namen '${session_name}'; Abbruch"
        fi
    else
        command screen "$@"
    fi
}

backup-file

Es kommt häufig vor, dass man ein Backup einer Datei anlegen will, bevor man sie bearbeitet. Diese Funktion braucht nur den Dateinamen (oder Dateipfad) als Argument, fügt das Datum in den Dateinamen ein, vermeidet das Überschreiben früherer Backups und erhält die Metadaten (Zeitstempel, Besitzer, Gruppe, Zugriffsrechte).

backup-file () {
    local path date_string suffix='.bak' new_path
    if [ $# -ne 1 ]; then
        echo "ERROR: argument number is not 1"
        echo "${FUNCNAME[0]} <file path>"
        return 1
    fi
    path="$1"
    if [ ! -f "$path" ]; then
        echo "ERROR: the argument path does not exist or is nor a regular file"
        echo "${FUNCNAME[0]} <file path>"
        return 1
    fi
    date_string="$(date +%Y-%m-%d)"
    new_path="${path}.${date_string}${suffix}"
    if [ -e "$new_path" ]; then
        echo "ERROR: there is already a file at the backup path"
        command ls -lh "$new_path"
        echo "${FUNCNAME[0]} <file path>"
        return 1
    fi
    if ( set -x; cp -p --no-clobber "$path" "$new_path" ); then
        echo "Backup file created successfully"
        command ls -lh "$path" "$new_path"
        return 0
    else
        echo "ERROR: Backup file creation failed"
        return 1
    fi
}

lcat: Anzeige von Datei- und Verzeichnis-Inhalten

Wenn ich mich im Dateisystem umsehe, wechsele ich vielfach zwischen der Anzeige eines Verzeichnisinhalts und der Anzeige des Inhalts von Textdateien. Um diesen Prozess zu beschleunigen, habe ich eine Funktion geschrieben, die das Argument entsprechend behandelt, so dass ich das Kommando nicht austauschen muss (alternativ könnte man wohl auch einfach einen Dateimanager wie mc verwenden...); eine Mischung aus ls und cat, daher der Name lcat:

lcat () {
    test $# -eq 0 && return 1
    local file
    for file; do
        if [ ! -e "$file" ]; then
            echo "File '${file}' does not exist; skipping"
            continue
        fi
        if [ -f "$file" ]; then
            echo "cat regular file '${file}':"
            cat "$file"
            continue
        fi
        # else
        echo "ls -ld '${file}':"
        command ls -ld "$file"
        echo
        echo -----------------------------------
        echo
        echo "ls -l '${file}':"
        command ls -l "$file"
    done
}

psp: detaillierte ps-Ausgabe für die übergebene PID

psp () {
    test $# -ne 1 && return 1
    if [[ $1 =~ ^[1-9][0-9]*$ ]]; then
        pid="$1"
    else
        echo "ERROR: Das Argument ('${1}') ist ungültig; Abbruch"
        return 1
    fi
    if [ ! -d "/proc/${pid}" ]; then
        echo "ERROR: Es existiert kein Prozess mit der PID '${1}'; Abbruch"
        return 1
    fi
    /bin/ps -o 'pid,ppid,pgid,sess,nlwp,euser,nice,class,rtprio,stat,tty,time,etime,args' -p "$pid" | cat
}

pspv: noch detailliertere ps-Ausgabe für die übergebene PID

pspv () {
    test $# -ne 1 && return 1
    if [[ $1 =~ ^[1-9][0-9]*$ ]]; then
        pid="$1"
    else
        echo "ERROR: Das Argument ('${1}') ist ungültig; Abbruch"
        return 1
    fi
    if [ ! -d "/proc/${pid}" ]; then
        echo "ERROR: Es existiert kein Prozess mit der PID '${1}'; Abbruch"
        return 1
    fi
    /bin/ps -o 'pid,ppid,sess,nlwp,ruser,euser,userns,mntns,netns,nice,class,rtprio,stat,tty,rss,maj_flt,time,etime,unit,args' -p "$pid" | cat
}