Marcel Logen <
33320000...@ybtra.de>:
> Helmut Waitzmann in de.comp.os.unix.shell:
>
>>Helmut Waitzmann <
nn.th...@xoxy.net>:
>
> [...]
>
>> my_robuste_pfade()
>> {
>> # Aufruf:
>> #
>> # my_robuste_pfade ergebnisvar Pfade...
>> #
>> # Der erste Parameter ist der Name der Feldvariablen, in
>> # der das Ergebnis zurueckgegeben werden wird.
>> #
>> # Weitere Parameter sind die Pfade, die robust gemacht
>> # werden sollen.
>>
>> # Verwendungsbeispiel:
>> #
>> # Das Shell-Kommando
>> #
>> # my_robuste_pfade ergebnis …
>> #
>> # liefert in der Feldvariablen 'ergebnis' die robusten
>> # Varianten der Aufrufparameter.
>>
>> local -- - &&
>
> Hier werden die "shell options" funktions-lokal gemacht, ...
>
Ja, genau, denn ich will sie innerhalb der Funktion geändert
haben, aber nach dem Ende sollen sie wieder so sein wie zuvor.
>> set +x -e -u &&
>
> ... z. B. wird das Shell-Attribut "x" (command expansion) abgeschal-
> tet, "e" (stop on error) und "u" (expansion of unset variables) ein-
> geschaltet. (Die Bezeichnugen in den Klammern stammen von mir.)
Genau. Wenn man die Funktion in einem Shell‐Skript verwendet und
im Shell‐Skript «set -x» einstellt, um ihm zuschauen zu können,
wie es läuft, will man ja im Allgemeinen nicht in diese Funktion
hineinschauen (von der man ja bereits weiß – oder zu wissen
glaubt – dass sie funktioniert).
>
>> local -- ergebnisvarname pfad &&
>> ergebnisvarname="${1?"${FUNCNAME[0]}"': Der Name der Ergebnisvariablen fehlt.'}" &&
>>
>> # Der Name der Ergebnisvariablen stoert im Weiteren in
>> # der Parameterliste nur, deshalb wird er entfernt:
>> #
>> shift &&
>> if ! LC_ALL=C expr " $ergebnisvarname" : \
>> ' [[:alpha:]_][[:alnum:]_]*$' > /dev/null
>
> Hier wird offenbar geprüft, ob der Variablenname aus den erlaubten
> Zeichen besteht. Wenn nicht, erscheint eine Fehlermeldung:
Genau. Man muss ja verhindern, dass der Aufrufer als
Variablenname irgend etwas Unpassendes übergibt.
>
>> then
>> printf >&2 '%s: %q ist als Variablenname nicht erlaubt.\n' \
>> "${FUNCNAME[0]}" "$ergebnisvarname"
>
> Der Text wird auf stderr ausgegeben, wenn ich das richtig sehe.
>
Ja. Fehlermeldungen gehören nach stderr. Der Umlenkoperator
muss dabei bei einem simple command nicht ganz hinten stehen.
Damit ich sofort sehe, wohin die Ausgabe von «printf» geht, habe
ich ihn gleich vorne hinter «printf» gesetzt.
>> return 1
>> fi &&
>> for pfad
>> do
>> case "${pfad%%/*}" in
>> (.|'')
>
> Hier, dachte ich zunächst, könnte man auch "(.|)" schreiben, aber
> das klappt so nicht:
>
> | user14@n14:~$ case '' in (.|'') echo eins ;; (*) echo zwei ;; esac
> | eins
>
> | user14@n14:~$ case '' in (.|) echo eins ;; (*) echo zwei ;; esac
> | bash: Syntaxfehler beim unerwarteten Symbol »)«
>
> | user14@n14:~$ case '.' in (.|'') echo eins ;; (*) echo zwei ;; esac
> | eins
>
> | user14@n14:~$ case '.bar' in (.|'') echo eins ;; (*) echo zwei ;; esac
> | zwei
Das ist mir ganz genau so gegangen.
>
>> # Alles, was mit der Komponente '.' oder '' beginnt,
>> # ist schon robust und kann bleiben, wie es ist:
>> #
>> # Meines Wissens muss in jedem "case"-Zweig
>> # wenigstens ein Kommando stehen, also nehme ich
>> # hier das Kommando ":", das nichts tut:
>> # (Falls ich mich irre, kann man es auch weglassen.)
>> :
>> ;;
>> (*)
>> # Alles andere wird robust gemacht durch Voranstellen
>> # von './':
>> #
>> pfad=./"$pfad"
>> ;;
>> esac &&
>> shift &&
>> set -- "$@" "$pfad"
>> done &&
>> eval 'declare -g -a -- '"$ergebnisvarname"'=("$@")'
>> }
>
> Zu "eval" siehe meine Frage oben.
>
[…]
> | user14@n14:~$ my_robuste_pfade ev '/' '' '.' './foo' 'bar' ' ' '.baz' './.qux' '/.quux'
>
> | user14@n14:~$ declare -p ev
> | declare -a ev=([0]="/" [1]="" [2]="." [3]="./foo" [4]="./bar" [5]="./ " [6]="./.baz" [7]="./.qux" [8]="/.quux")
>
> Scheint also so zu funktionieren wie gedacht. Danke!
>
Ich hatte ja geschrieben, dass sich die faule Sau in mir noch
dagegen wehrt, die Funktion so umzuarbeiten, dass sie den
Optionen‐Ende‐Parameter «--» und vielleicht Optionen, etwa die
Option «-h» für Hilfe, akzeptiert. Und die faule Sau hat
gewonnen, aber nicht deswegen, weil sie ihre Faulheit
durchgesetzt hat, sondern weil sie mir bewiesen hat, dass das
Gewünschte nicht machbar ist:
Zwei Probleme hat es noch, die man meines Wissens nicht beheben
kann (es sei denn, man geht ganz anders vor, s.u.):
Wenn man vor dem Aufruf der Funktion beispielsweise die Variable
«pfad», die ja innen drin als lokale Variable verwendet wird,
beispielsweise mit der «readonly»‐Eigenschaft versieht, hat diese
Eigenschaft auch die lokale Variable «pfad», und man hat
keine Möglichkeit, diese Eigenschaft innerhalb der Funktion
loszuwerden. Dann scheitert die Funktion:
readonly -- pfad
my_robuste_pfade ev / '' '.'
Für die Hilfeausgabe würde ich eine Funktion, etwa «syntax»
genannt, schreiben wollen. So weit ich weiß, kann man aber keine
lokalen Funktionen, die also nur innerhalb der Funktion
«my_robuste_pfade» existieren, schreiben. Oder anders
ausgedrückt: Nach dem erstmaligen Aufrufen der Funktion
«my_robuste_pfade» gäbe es dann die Funktion «syntax», die
außerhalb der Funktion «my_robuste_pfade» keinen Sinn hätte und
eine möglicherweise bereits vorher vorhandene Funktion «syntax»
ersetzen würde. Also scheitert das auch daran.
Von daher gehe ich dann doch anders vor: Ich schreibe keine
Funktion «my_robuste_pfade» sondern ein Shell‐Skript
«my_robuste_pfade»: Das Shell‐Skript wird von einem neuen Shell,
der von den Shell‐Variablen in seiner Aufrufumgebung (außer den
Umgebungsvariablen) nichts mitbekommt, abgearbeitet. Auch kann
man innerhalb des Shell‐Skripts Funktionen definieren, ohne dass
die nach außen wirken. => Beide Probleme sind verschwunden.
Allerdings hat man dann ein anderes, das damit zusammenhängt: Man
kann innerhalb eines Shell‐Skripts keinen Einfluss auf Variable
außerhalb des Shell‐Skripts nehmen. Also gibt es keine
Möglichkeit, im Shell‐Skript ein «eval»‐Kommando so aufzurufen,
dass es eine Feldvariable außerhalb des Shell‐Skripts mit dem
Ergebnis füllt.
Aber es gibt eine andere Möglichkeit, die man in solchen Fällen
gerne nutzt: Man kann das Shell‐Skript «my_robuste_pfade» so
schreiben, dass es eine Kommandozeile, die eine
Feldvariablen‐Zuweisung darstellt, als Ausgabe ausspuckt. Die
nimmt man dann als Aufrufer entgegen und legt sie «eval» zum
Ausführen vor:
kommandozeile="$(
my_robuste_pfade -- ev die Pfade als Parameter
)" &&
eval "$kommandozeile"
Dabei ist es sogar noch besser, wenn man es noch anders macht:
«my_robuste_pfade» soll nicht eine vollständige Kommandozeile
(für die Zuweisung des Ergebnisses an eine Feldvariable)
ausgeben, sondern nur einen Teil einer Kommandozeile, nämlich die
robusten Entsprechungen der übergebenen Pfade.
Der Vorteil daran ist, dass das Shell‐Skript «my_robuste_pfade»
nicht mehr mit einem Variablennamen für das Ergebnis aufgerufen
wird. Und deshalb muss es sich auch nicht mehr darum kümmern, ob
der Variablenname gültig ist.
Der Variablenname wird ihm nicht übergeben, und es liefert auch
keine Variablenzuweisung sondern nur den Teil einer
Kommandozeile, der die Liste der robusten Pfade darstellt.
Ob der Aufrufer die Liste dann mit einer Zuweisung an eine
Feldvariable oder mit einem «set»‐Kommando zum Setzen der
positional parameters kombiniert, bleibt dann ihm überlassen.
Man kann das Shell‐Skript beispielsweise so aufrufen:
commandozeilenteil="$(
my_robuste_pfade -- die Pfade als Parameter
)" &&
# und dann die positional parameters mit dem Ergebnis
# belegen:
eval 'set -- '"$kommandozeilenteil"
# oder im Bash eine Feldvariable füllen:
eval 'ergebnis=( '"$kommandozeilenteil"' )'
Ein Shell‐Skript statt einer Funktion zu verwenden, hat dann auch
den Vorteil, dass man das Konzept der funktionslokalen
Shell‐Variablen nicht mehr braucht, und, dass keine störenden
«readonly»‐Attribute von Shell‐Variablen von außen mehr
hereindrücken können. Auch ist es ohne weiteres möglich,
innerhalb des Shell‐Skripts die benötigte Funktion «syntax» für
die Ausgabe des Hilfetexts zu schreiben, ohne dass die nach außen
entweichen kann. => Man braucht für das Shell‐Skript nur noch
Fähigkeiten, die jeder zum POSIX‐Standard kompatible Shell
mitbringt. Und auch die Verwendung kommt damit aus, wenn man mit
der Ausgabe des Shell‐Skripts keine Zuweisung an eine
Feldvariable sondern an die positional parameters zusammensetzt.
Allerdings hat man jetzt noch eine Aufgabe zu lösen: Wenn man
innerhalb der Funktion die robusten Pfade berechnet hat, muss man
sie so ausgeben, dass sie Teil einer Kommandozeile (die in der
Aufruferumgebung dem «eval»‐Kommando übergeben wird) werden
können.
Dazu kann man entweder nicht zum POSIX‐Standard kompatibel die
Formatierungsanweisung «%q» des in den Bash eingebauten
«printf»‐Kommandos oder auch des selbständigen «printf»‐Programms
nutzen; oder man schreibt (zu POSIX kompatibel) eine dem
entsprechende Funktion (oder ein Shell‐Skript) selbst.
Um zu erklären, worum es dabei geht, stell dir vor, einer der
(nicht‐robusten) Pfade sei der folgende Text:
«"Hallo, wie geht's?", fragte er.»
Dateinamen können in Unix ja alle möglichen Zeichen außer dem
ASCII null enthalten, also könnte das ein Pfad sein.
Robust gemacht, käme dabei folgendes heraus:
«./"Hallo, wie geht's?", fragte er.»
Nur kann man das so nicht in eine Kommandozeile setzen, denn die
Anführungszeichen werden vom Kommandozeilenparser gefressen, und
die nicht durch Anführungszeichen, Apostrophe oder umgekehrte
Schrägstriche geschützten Leerstellen führen zum Zerbrechen des
Pfads, und es entstehen drei Teile:
«./Hallo, wie geht's?,» «fragte» und «er.».
So darf das das Shell‐Skript «my_robuste_pfade» also nicht
ausgeben.
Wenn es einfach noch einen Apostroph vorne und hinten dranklebt,
wird's auch nicht besser:
«'./"Hallo, wie geht's?", fragte er.'»
=> Der dritte Apostroph führt zum Syntaxfehler in der
Kommandozeile.
Und Anführungszeichen statt Apostrophe vorne und hinten
dranzukleben, funktioniert auch nicht, weil dem die bereits
darin enthaltenen Anführungszeichen in die Quere kommen:
«"./"Hallo, wie geht's?", fragte er."»
=> Der Apostroph führt zum Syntaxfehler in der Kommandozeile.
Funktionieren würde jedoch beispielsweise, wenn das Shell‐Skript
folgenden Text ausgäbe,
«'./"Hallo, wie geht'\''s?", fragte er.'»
bei dem der ganze Text in Apostrophe eingefasst ist. Dadurch
sind alle Zeichen außer den Apostrophen vor dem Parser
geschützt. Apostrophe kann man mit Apostrophen nicht einfassen,
deshalb wendet man da einen Kniff an: Kommt ein Apostroph in der
Zeichenfolge vor, setzt man zunächst einen Apostroph hin, um die
Einfassung mit Apostrophen zu beenden, schützt dann den
vorhandenen Apostroph mit einem «\» und setzt danach noch einen
Apostroph hin, um die Einfassung mit Apostrophen wieder
aufzunehmen: Aus «'» wird «'\''».
Also braucht das Shell‐Skript innen drin eine Funktion, die genau
das macht.
Wenn man will, kann man mehrere unmittelbar hintereinander
auftretende Apostrophe, statt jeden einzeln zu schützen
«mit drei ''' Apostrophen» => «'mit drei '\'\'\'' Apostrophen'»,
gemeinsam in Anführungszeichen stellen:
«mit drei ''' Apostrophen» => «'mit drei '"'''"' Apostrophen'»
Das ist dann etwas kürzer und, falls man es lesen will, auch
leichter zu verstehen.
Die Funktion, die das macht, nenne ich
«my_quote_words_for_shells». Sie kann beispielsweise so
aussehen:
my_quote_words_for_shells()
(
# gibt eine Wortliste so aus, dass sie als (Teil einer)
# Parameterliste in die Kommandozeile eines POSIX-Shells
# eingefuegt werden kann.
#
# Exit-Code:
# 0, falls die Umsetzung ohne Fehler erfolgt ist,
# !=0, falls ein Fehler aufgetreten ist.
#
# Beispiele:
#
# eine Wortliste als Kommandoaufruf in eine Kommandozeile
# stellen:
#
# if kommandozeile="$(
# my_quote_words_for_shells Programm mit Parametern
# )"
# then
# # verwende sie mittels "eval":
# eval " $kommandozeile"
#
# # oder gib sie einem neuen Shell:
# sh -c -- "$kommandozeile" sh
#
# # oder reiche sie an "su" weiter:
# su -- - ein_Benutzer -c -- "$kommandozeile" -su
#
# # oder verwende sie auf einem anderen Rechner:
# ssh benu...@remote.host.example " $kommandozeile"
#
# else
# # Ein Fehler ist bei der Errechnung der Kommandozeile
# # aufgetreten.
# fi
#
#
# Die positional parameters in einer Variablen speichern und
# wieder herstellen:
#
# if args="$(my_quote_words_for_shells "$@")"
# then
# # irgend etwas mit den positional parameters anstellen.
# # ...
#
# # die vorherigen positional parameters wieder herstellen:
#
# eval "set -- $args"
# else
# # Ein Fehler ist beim Sichern der positional parameters
# # aufgetreten.
# fi
# "wordsep" enthaelt ein Trennzeichen fuer die Liste der
# maskierten Woerter. Vor dem ersten Wort braucht keines
# ausgegeben zu werden:
wordsep=
for word
do
# "$wordsep" trennt jedes (auszer das erste) auszugebende Wort
# von seinem Vorgaenger:
printf '%s' "$wordsep"
if test -z "$word"
then
# Es liegt das leere Wort vor. Dann ist das Ergebnis "''":
printf '%s' "''"
else
# Das Wort ist nicht leer.
while
{
prefix="${word%%\'*}"
word="${word#"$prefix"}"
# "$prefix" ist das laengste Anfangsstueck von "$word",
# das keinen Apostroph enthaelt; "$word" erhaelt den Rest.
# Folgerung: "$word" ist entweder leer oder beginnt mit
# einem Apostroph.
if test -n "$prefix"
then
# "$prefix" besteht aus mehr als null Stueck Zeichen.
# Setze sie zwischen zwei Apostrophe:
printf \''%s'\' "${prefix}"
fi
test -n "$word" &&
{
# "$word" ist nicht leer. Folgerung: "$word" beginnt mit
# mindestens einem Apostroph.
apostr="${word%%[!\']*}"
# "$apostr" ist das laengste Anfangsstueck von "$word",
# das nur aus Apostrophen besteht.
if test -n "${apostr#"'"}"
then
# Es hat mindestens 2 Apostrophe: Setze es zwischen
# zwei Anfuehrungszeichen:
printf '"%s"' "${apostr}"
else
# Es besteht aus 1 Apostroph: Stelle ihm einen
# umgekehrten Schraegstrich voran:
printf '\\%s' "${apostr}"
fi
# Schneide das Apostrophe-Anfangsstueck von "$word" ab:
word="${word#"$apostr"}"
# Bleibt nichts mehr uebrig, ist das Wort fertig
# verarbeitet:
${word:+:} false
}
}
do
:
done
fi
# Alle weiteren Woerter (auszer dem ersten) muessen von ihrem
# Vorgaenger mit einem Leerzeichen abgetrennt werden:
wordsep=' '
done
printf '\n'
)
Die stellt man dann in das Shell‐Skript «my_robuste_pfade» hinein
und erhält, wenn jetzt keine Fehler mehr drin sind, eine wirklich
robuste Lösung, die robuste Pfade liefert. Sie hat die Option
«-h» für einen kurzen Hilfetext und muss deswegen natürlich auch
den speziellen Optionen‐Ende‐Parameter «--» verwenden:
#!/bin/sh -
# Fassung vom 2022-08-30T18:00:44+02:00
set -ue
readonly -- prgnam="${0}" exit_failure=1 exit_syntax=2 \
exit_program_error=3
syntax()
{
{
if ${1+:} false
then
cut -d . -f2- <<-EOF
.${prgnam}: $@
.
EOF
fi
cut -d . -f2- <<-EOF
.$prgnam
.
.Syntax:
.
.$prgnam -h
.
.$prgnam -W help
.
.Optionen:
.
. -h, -W help
.
. Zeige diesen Hilfetext
.
.Exit statuses:
.
.0 Erfolg
.
.1 Fehler
.
.2 Aufrufsyntaxfehler
.
.3 Programmfehler
EOF
} |
output_formatted
} # syntax()
columns="${COLUMNS:-}"
${columns:+:} false ||
{
columns="$( tput cols )" &&
LC_ALL=C expr " $columns" : ' [[:digit:]]\{1,\}$' \
> /dev/null && test "$columns" -gt 0
} || columns=80
for foldcmdline in \
'fmt ${columns:+-w} ${columns:+"$columns"} --' \
'fold -s ${columns:+-w} ${columns:+"$columns"} --' \
'cat --'
do
(
eval "set '' $foldcmdline" && shift &&
command -v -- "$1" > /dev/null 2>&1
) && break
done
eval 'output_formatted() { '"$foldcmdline"'; }'
option_help=false &&
while getopts hW: option
do
case "$option"
in
(h)
option_help=:
;;
(W)
case "${OPTARG-?}"
in
(help)
option_help=:
;;
(*)
{
printf 'Unbekannter Parameter der Option -W: %s\n' \
"$OPTARG"
syntax ${1+"$@"}
} >&2
exit "$exit_syntax"
;;
esac
;;
(\?)
syntax >&2 ${1+"$@"}
exit "$exit_syntax"
;;
(*)
printf >&2 \
'%s:\nProgrammfehler: Optionsbehandlung fuer %s fehlt.\n' \
"$prgnam" "$option"
exit "$exit_program_error"
;;
esac
done &&
shift $((OPTIND - 1)) &&
if "$option_help"
then
syntax
exit
fi &&
my_quote_words_for_shells()
(
# gibt eine Wortliste so aus, dass sie als (Teil einer)
# Parameterliste in die Kommandozeile eines POSIX-Shells
# eingefuegt werden kann.
#
# Exit-Code:
# 0, falls die Umsetzung ohne Fehler erfolgt ist,
# !=0, falls ein Fehler aufgetreten ist.
#
# Beispiele:
#
# eine Wortliste als Kommandoaufruf in eine Kommandozeile
# stellen:
#
# if kommandozeile="$(
# my_quote_words_for_shells Programm mit Parametern
# )"
# then
# # verwende sie mittels "eval":
# eval " $kommandozeile"
#
# # oder gib sie einem neuen Shell:
# sh -c -- "$kommandozeile" sh
#
# # oder reiche sie an "su" weiter:
# su -- - ein_Benutzer -c -- "$kommandozeile" -su
#
# # oder verwende sie auf einem anderen Rechner:
# ssh benu...@remote.host.example " $kommandozeile"
#
# else
# # Ein Fehler ist bei der Errechnung der Kommandozeile
# # aufgetreten.
# fi
#
#
# Die positional parameters in einer Variablen speichern und
# wieder herstellen:
#
# if args="$(my_quote_words_for_shells "$@")"
# then
# # irgend etwas mit den positional parameters anstellen.
# # ...
#
# # die vorherigen positional parameters wieder herstellen:
#
# eval "set -- $args"
# else
# # Ein Fehler ist beim Sichern der positional parameters
# # aufgetreten.
# fi
# "wordsep" enthaelt ein Trennzeichen fuer die Liste der
# maskierten Woerter. Vor dem ersten Wort braucht keines
# ausgegeben zu werden:
wordsep=
for word
do
# "$wordsep" trennt jedes (auszer das erste) auszugebende Wort
# von seinem Vorgaenger:
printf '%s' "$wordsep"
if test -z "$word"
then
# Es liegt das leere Wort vor. Dann ist das Ergebnis "''":
printf '%s' "''"
else
# Das Wort ist nicht leer.
while
{
prefix="${word%%\'*}"
word="${word#"$prefix"}"
# "$prefix" ist das laengste Anfangsstueck von "$word",
# das keinen Apostroph enthaelt; "$word" erhaelt den Rest.
# Folgerung: "$word" ist entweder leer oder beginnt mit
# einem Apostroph.
if test -n "$prefix"
then
# "$prefix" besteht aus mehr als null Stueck Zeichen.
# Setze sie zwischen zwei Apostrophe:
printf \''%s'\' "${prefix}"
fi
test -n "$word" &&
{
# "$word" ist nicht leer. Folgerung: "$word" beginnt mit
# mindestens einem Apostroph.
apostr="${word%%[!\']*}"
# "$apostr" ist das laengste Anfangsstueck von "$word",
# das nur aus Apostrophen besteht.
if test -n "${apostr#"'"}"
then
# Es hat mindestens 2 Apostrophe: Setze es zwischen
# zwei Anfuehrungszeichen:
printf '"%s"' "${apostr}"
else
# Es besteht aus 1 Apostroph: Stelle ihm einen
# umgekehrten Schraegstrich voran:
printf '\\%s' "${apostr}"
fi
# Schneide das Apostrophe-Anfangsstueck von "$word" ab:
word="${word#"$apostr"}"
# Bleibt nichts mehr uebrig, ist das Wort fertig
# verarbeitet:
${word:+:} false
}
}
do
:
done
fi
# Alle weiteren Woerter (auszer dem ersten) muessen von ihrem
# Vorgaenger mit einem Leerzeichen abgetrennt werden:
wordsep=' '
done
printf '\n'
) &&
for pfad
do
case "${pfad%%/*}" in
(.|'')
# Alles, was mit der Komponente '.' oder '' beginnt,
# ist schon robust und kann bleiben, wie es ist:
#
# Meines Wissens muss in jedem "case"-Zweig
# wenigstens ein Kommando stehen, also nehme ich
# hier das Kommando ":", das nichts tut:
# (Falls ich mich irre, kann man es auch weglassen.)
:
;;
(*)
# Alles andere wird robust gemacht durch Voranstellen
# von './':
#
pfad=./"$pfad"
;;
esac &&
shift &&
set -- "$@" "$pfad"
done &&
my_quote_words_for_shells "$@"