staging.inyokaproject.org

bash: Argumentstring bearbeiten

Status: Gelöst | Ubuntu-Version: Ubuntu 20.04 (Focal Fossa)
Antworten |

kB Team-Icon

Supporter, Wikiteam
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 9837

Ich möchte in einem bash-Skript aus dem Argumentstring $* das letzte Wort abschneiden. Um das zu testen, benutze ich dieses Skript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#! /bin/bash

X=$*
echo X=$X.
echo '${X%e}='${X%e}.
echo '"${X%e}"='"${X%e}".
echo -----
echo '*='$*.
echo '@='$@.
echo '"*"='"$*".
echo '"@="'"$@".

echo '${*%e}='${*%e}.
echo '"${*%e}"='"${*%e}".
echo '"${@%e}"='"${@%e}".
echo '"${@%e}"='"${@%e}".

Beispielsweise mit diesem Ergebnis:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ ~/Skripte/TEST 1 e2 ea ae 3e '4 5' e
X=1 e2 ea ae 3e 4 5 e.
${X%e}=1 e2 ea ae 3e 4 5 .
"${X%e}"=1 e2 ea ae 3e 4 5 .
-----
*=1 e2 ea ae 3e 4 5 e.
@=1 e2 ea ae 3e 4 5 e.
"*"=1 e2 ea ae 3e 4 5 e.
"@="1 e2 ea ae 3e 4 5 e.
${*%e}=1 e2 ea a 3 4 5 .
"${*%e}"=1 e2 ea a 3 4 5 .
"${@%e}"=1 e2 ea a 3 4 5 .
"${@%e}"=1 e2 ea a 3 4 5 .

Wenn ich den Argumentstring erst einer Variablen X zuweise und diese bearbeite, passiert genau das, was ich haben will. Nun will ich aber auf die Variable X verzichten und mit $* direkt arbeiten. Das verhält sich anders: Es wird aus jedem Wort das Ende weggeschnitten, und zwar unabhängig von der Verwendung von $@ oder $* und auch unabhängig vom Quoting. Das Verhalten würde ich beim Array $@ erwarten, aber nicht bei $*!

Warum verhält sich hier $* genau wie $@, entgegen der Dokumentation? Ist das ein Bug in bash?

Bearbeitet von rklm:

Syntaxhighlighting

shiro Team-Icon

Supporter

Anmeldungsdatum:
20. Juli 2020

Beiträge: 1303

Die Parameter sind ein normales Array. Wenn du die Array-Elemente einer Variablen zuweist, klappt das Löschen des letzten "e", wie du auch festgestellt hast, sonst wird jedes einzelne Element behandelt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ arr=( 1 e2 ea ae 3e '4 5' e )
$ echo ${#arr[@]}: ${arr[@]%e}
7: 1 e ea ae 3e 4 5 e
$ echo ${#arr[*]}: ${arr[*]%e}
7: 1 e ea ae 3e 4 5 e
$ echo ${arr[*]%e}
1 e2 ea a 3 4 5
$ echo ${arr[@]%e}
1 e2 ea a 3 4 5
$ x=${arr[@]};echo ${x%e}
1 e2 ea ae 3e 4 5

Die Parameter "*" und "@" werden in diesem Kontext durchaus gleich behandelt. Sie unterscheiden sich in wenigen Eigenschaften (z.B. "word splitting" und "pathname expansion"). Da im "${}" Ausdruck eine doppelte Substitution nicht auftreten darf, muss man leider den Weg über eine Hilfsvariable machen.

> Warum verhält sich hier $* genau wie $@, entgegen der Dokumentation? Ist das ein Bug in bash?

Die Anmerkung habe ich leider nicht genau genug verstanden. Wo hättest du bei den gewählten Parametern eine unterschiedliche Behandlung erwartet (gemäß man page)?

Oder beziehst du dich auf die (1st char) IFS Eigenschaft von "$*"? Diese wird aber erst nach der Bearbeitung des Arrays durchgeführt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ savIFS="$IFS";export IFS=","
$ cat x.sh
echo ">$*<"
echo ">$@<"
echo "${*%e}"
echo "${@%e}"
$ . x.sh 1 e2 ea ae 3e '4 5' e
>1,e2,ea,ae,3e,4 5,e<
>1 e2 ea ae 3e 4 5 e<
1,e2,ea,a,3,4 5,
1 e2 ea a 3 4 5 

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Am einfachsten erscheint mir, das in ein Array zu packen und per unset das letzte zu löschen:

1
2
3
4
tmp=("$@")
echo "vorher : ${#tmp[@]}: ${tmp[*]}"
unset tmp[$(( ${#tmp[@]} - 1 ))]
echo "nachher: ${#tmp[@]}: ${tmp[*]}"

Wenn Du das Argument-Array verändern willst, kannst Du am Ende wieder zurück kopieren:

1
2
set -- "${tmp[@]}"
unset tmp

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

Man kann auch den Array mit Ausnahme des letzten Elements ausgeben:

1
echo "${@:1:$((${#@} - 1))}"

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 9837

shiro schrieb:

[…] Wo hättest du bei den gewählten Parametern eine unterschiedliche Behandlung erwartet (gemäß man page)?

Es geht um diesen Passus aus der man page von bash:

Special Parameters The shell treats several parameters specially. These parameters may only be referenced; assignment to them is not al‐ lowed.

  • * Expands to the positional parameters, starting from one. When the expansion is not within double quotes, each po‐

sitional parameter expands to a separate word. In contexts where it is performed, those words are subject to fur‐ ther word splitting and pathname expansion. When the expansion occurs within double quotes, it expands to a sin‐ gle word with the value of each parameter separated by the first character of the IFS special variable. That is, "$*" is equivalent to "$1c$2c...", where c is the first character of the value of the IFS variable. If IFS is un‐ set, the parameters are separated by spaces. If IFS is null, the parameters are joined without intervening sepa‐ rators.

  • @ Expands to the positional parameters, starting from one. In contexts where word splitting is performed, this ex‐

pands each positional parameter to a separate word; if not within double quotes, these words are subject to word splitting. In contexts where word splitting is not performed, this expands to a single word with each positional parameter separated by a space. When the expansion occurs within double quotes, each parameter expands to a sepa‐ rate word. That is, "$@" is equivalent to "$1" "$2" ... If the double-quoted expansion occurs within a word, the expansion of the first parameter is joined with the beginning part of the original word, and the expansion of the last parameter is joined with the last part of the original word. When there are no positional parameters, "$@" and $@ expand to nothing (i.e., they are removed).

Insbesondere die markierten Stellen. Daraus verstehe ich „"$*" ist ein string (also kein array wie $@)“ und daher sollten sich diese beiden Programme gleich verhalten:

# P1
X=$*
echo "${X%e}" 

und

# P2
echo "${*%e} 

Tun sie aber nicht.

Es geht mir nicht um die Lösung der Aufgabe, hinten etwas abzuschneiden. Das löst bereits das Programm P1. Es geht mir nur darum, den Fehler in meinem Verständnis zu fassen. Oder einen Bug in bash.

shiro ist wohl schon auf der richtigen Fährte. Er schrieb:

Die Parameter sind ein normales Array. Wenn du die Array-Elemente einer Variablen zuweist, klappt das Löschen des letzten "e", wie du auch festgestellt hast, sonst wird jedes einzelne Element behandelt.

Seine Worte liegen mir aber noch unverstanden im Hirn.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

kB schrieb:

Insbesondere die markierten Stellen. Daraus verstehe ich „"$*" ist ein string (also kein array wie $@)“ und daher sollten sich diese beiden Programme gleich verhalten

Innerhalb der ${} Umgebung für die Parameter-Expansion findet so wie ich das verstehe noch keine Expansion des Wertes des Arrays für die Shell statt, bei der ein String unter Berücksichtigung des IFS aus dem Array erzeugt werden würde - das passiert erst, nachdem die Parameter Expansion stattgefunden hat - so sieht man das recht gut:

1
2
3
4
5
6
7
8
9
#!/usr/bin/bash
echo 'Argumente aus ${*%e}'
printf "@%s@\n" ${*%e}
echo 'Argumente aus "${*%e}"'
printf "@%s@\n" "${*%e}"
echo 'Argumente aus ${#%e}'
printf "@%s@\n" ${@%e}
echo 'Argumente aus "${#%e}"'
printf "@%s@\n" "${@%e}"
$ bash test.sh  1 e2 ea ae 3e '4e 5e' eeee
Argumente aus ${*%e}
@1@
@e2@
@ea@
@a@
@3@
@4e@
@5@
@eee@
Argumente aus "${*%e}"
@1 e2 ea a 3 4e 5 eee@
Argumente aus ${#%e}
@1@
@e2@
@ea@
@a@
@3@
@4e@
@5@
@eee@
Argumente aus "${#%e}"
@1@
@e2@
@ea@
@a@
@3@
@4e 5@
@eee@ 

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

seahawk1986 schrieb:

Man kann auch den Array mit Ausnahme des letzten Elements ausgeben:

1
echo "${@:1:$((${#@} - 1))}"

Oh cool! Man kann es noch ein klitzekleines Bisschen vereinfachen

1
echo "${@:1:$(($# - 1))}"

😉

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 9837

rklm schrieb:

seahawk1986 schrieb:

Man kann auch den Array mit Ausnahme des letzten Elements ausgeben:

1
echo "${@:1:$((${#@} - 1))}"

Oh cool! Man kann es noch ein klitzekleines Bisschen vereinfachen

1
echo "${@:1:$(($# - 1))}"

… und sogar das kann man wesentlich kürzer und verständlicher schreiben:

echo ${*:1:$#-1} 

bzw.:

echo ${*::$#-1} 

Es löst aber leider alles nicht meine Aufgabenstellung, denn ich will kein Array bearbeiten, sondern eine Zeichenkette (string) manipulieren.

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 9837

seahawk1986 schrieb:

[…] Innerhalb der ${} Umgebung für die Parameter-Expansion findet so wie ich das verstehe noch keine Expansion des Wertes des Arrays für die Shell statt, bei der ein String unter Berücksichtigung des IFS aus dem Array erzeugt werden würde - das passiert erst, nachdem die Parameter Expansion stattgefunden hat - so sieht man das recht gut: […]

Danke für Deine Ausarbeitung! Das hilft mir weiter. Ich hake das Thema mal ab mit dem Ergebnis: „Die man page von bash ist in dieser Beziehung missverständlich und ich bin darauf hereingefallen.“

Richtig ist wohl: Sowohl $@ wie auch $* stehen unabhängig vom Quoting immer in einem array-Kontext und Bearbeitungen erfolgen deshalb auch immer im array-Kontext. Wenn man – wie ich – in einem skalaren Kontext bearbeiten möchte, muss man diesen erst über Zuweisung an eine Hilfsvariable herstellen und dann bearbeiten:

X=$* ; echo ${X%e} 

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

kB schrieb:

Es löst aber leider alles nicht meine Aufgabenstellung, denn ich will kein Array bearbeiten, sondern eine Zeichenkette (string) manipulieren.

Das hast Du doch aber geschrieben:

Ich möchte in einem bash-Skript aus dem Argumentstring $* das letzte Wort abschneiden.

Das habe ich so verstanden, dass Du ein Skript mit mehreren Wörtern (und also mehreren Parametern) aufrufst und das letzte Wort (damit den letzten Parameter) loswerden willst. Vielleicht wäre ein konkretes Beispiel hilfreich gewesen, was Eingabe und gewünschte Ausgabe sind.

kB schrieb:

Richtig ist wohl: Sowohl $@ wie auch $* stehen unabhängig vom Quoting immer in einem array-Kontext und Bearbeitungen erfolgen deshalb auch immer im array-Kontext.

Beide bezeichnen ein Array. Unterschied ist nur die Parameter Expansion innerhalb doppelter Anführungsstriche.

Wenn man – wie ich – in einem skalaren Kontext bearbeiten möchte, muss man diesen erst über Zuweisung an eine Hilfsvariable herstellen und dann bearbeiten:

X=$* ; echo ${X%e} 

Irgendwie auch logisch, dass Du eine Zeichenkette brauchst, wenn Du es als eine Zeichenkette bearbeiten willst. 😉

shiro Team-Icon

Supporter

Anmeldungsdatum:
20. Juli 2020

Beiträge: 1303

Ich denke der Satz

When the expansion occurs within double quotes, it expands to a sin‐ gle word

will exakt gelesen werden. Im "${name}" Konstrukt ist der "name" Eintrag nicht als Expansion erlaubt, da dies zu einer doppelten Substitution führen würde, die (zwar aus meiner Sicht wünschenswert aber) hier nicht unterstützt wird. Es müsste daher etwas wie ${"$*"%e} erlaubt sein, damit die oben beschriebene Expansion zum Einsatz kommen könnte. Da dies bei "${}" nicht implementiert ist, greift die "expansion with double quotes" erst nachdem ${*%e} die Einzelelemente bearbeitet hat mit dem bereits gezeigten Ergebnis.

In der normalen indirekten Variablen-Zuweisung ist ein Konstrukt wie "var=wert;var1=var;var2=${$var1}" ja nicht erlaubt, um var2=wert zu erhalten. Hier wird somit die indirekte Referenz eingesetzt (z.B. "eval var2=\$$var1" oder einfacher "var2=${!var1}"). Leider hilft dieser Ansatz (eval) für das geschilderte Problem nicht weiter, da der "name" Eintrag eine Variable und kein gequoteter String sein darf (ein ${"abcde"%e} wird ja leider nicht erlaubt).

In meinem Verständnis liegt daher keine Fehlimplementierung (bug) oder falsche Dokumentation der bestehenden bash Implementierung vor. Wohl könnte ich mir aber vorstellen, dass dies in die whishlist aufgenommen werden könnte (ein "tr [:upper:] [:lower:]" hat man ja auch mittlerweile als ${x,,[A-Z]} implementiert).

Antworten |