Bash – dlaczego shift nie działa tak, jak oczekiwałbym?

0

Prosty problem. Mam taki kawałek kodu w Bashu:

for ARG in "${@}" ; do
    case "${ARG}" in
        "x")
            echo "Processing x..."
            shift
        ;;
        "y")
            echo "Processing y..."
            shift
            if test "${#}" != "0" ; then
                shift "${#}"
                echo "Warning: Some arguments have been skipped"
            fi
        ;;
    esac
done

Załóżmy, że jest on w pliku run. Jeśli wywołuję go tak:

./run x y

to wynik jest zgodny z moimi oczekiwaniami:

Processing x...
Processing y...

Ale jeśli wywołuję go z odwróconą kolejnością argumentów:

./run y x

to powinien zgodnie z moimi oczekiwaniami nie przetwarzać argumentu x – powinien on zostać opuszczony, "przesunięty", "wyshiftowany" – i wynik powinien być taki:

Processing y...
Warning: Some arguments have been skipped

Natomiast rzeczywisty wynik mówi, że argument ten jest przetwarzany:

Processing y...
Warning: Some arguments have been skipped
Processing x...

Dlaczego Bash tak robi?


UPDATE:

Co ciekawe, jeśli wyświetlać listę argumentów przed przetworzeniem poszczególnych z nich w ten sposób:

for ARG in "${@}" ; do
    echo '${@}' == "${@}"
    # tu reszta kodu
done

to wynikowa jej zawartość w przypadkach obu wywołań jest taka, jak oczekiwana przeze mnie:

Wywołanie ./run x y:

${@} == x y
Processing x...
${@} == y
Processing y...

Wywołanie ./run y x (odwrócona kolejność argumentów):

${@} == y x
Processing y...
Warning: Some arguments have been skipped
${@} ==
Processing x... # Skąd? Przecież lista jest pusta, jak widać wyżej…

To powoduje uszczegółowienie mojego problemu z tym kodem: dlaczego Bash przetwarza pustą listę argumentów tak, jakby nie była pusta?

1

Wykorzystaj while zamiast for. Pętla for tak ma, ale tego raczej nie trzeba tłumaczyć (tak mi się wydaje).

while (( "$#" )) ; do
    case "$1" in
        "x")
            echo "Processing x..."
            shift
        ;;
        "y")
            echo "Processing y..."
            shift
            if test "${#}" != "0" ; then
                shift "${#}"
                echo "Warning: Some arguments have been skipped"
            fi
        ;;
    esac
done

0

TL; DR:

Masz tutaj kombinację dwóch czynników jednocześnie. Pierwszym jest pętla, która iteruje po argumentach, drugim jest przesuwanie się po liście argumentów za pomocą shift. Te listy są niezależne.

Wyjaśnienie:

Przeróbmy nieco kod:

#!/usr/bin/env bash

for ARG in "${@}" ; do
    echo -e "\nnew iteration"
    echo "(a)" "${#}": "${@}", "${ARG}"
    case "${ARG}" in
        "x")
            echo "Processing x..."
            shift
            echo "(b)" "${#}": "${@}", "${ARG}"
        ;;
        "y")
            echo "Processing y..."
            shift
            echo "(c)" "${#}": "${@}", "${ARG}"
            if test "${#}" != "0" ; then
                shift "${#}"
                echo "Warning: Some arguments have been skipped"
            fi
        ;;
    esac
done

Wywołując skrypt ./run.sh x y otrzymamy:

new iteration
(a) 2: x y, x
Processing x...
(b) 1: y, x

new iteration
(a) 1: y, y
Processing y...
(c) 0: , y

W pierwszej iteracji przed wejściem do switcha (a) mamy 2 argumenty: x i y. Bieżącym argument w tej iteracji jest x. Stąd wejdziemy do pierwszego warunku i wydrukujemy Processing x... jednocześnie z pomocą shift lista argumentów przesuwa się z x y do y (b). Problem w tym, że to przesuwanie nie dotyczy tej listy, która jest już załadowana do pamięci (${@} w linii 3.). To powoduje, że w drugiej iteracji mamy wciąż dostęp do y. Po wejściu do drugiego warunku wykonujemy shift (c) co powoduje, że if się nie wykona, bo ten shift opróżnia listę z pojedynczego argumentu, którym jest y. Wywołajmy teraz ./run.sh y x:

new iteration
(a) 2: y x, y
Processing y...
(c) 1: x, y
Warning: Some arguments have been skipped

new iteration
(a) 0: , x
Processing x...
(b) 0: , x

y jako pierwszy argument zapewnia, że wchodzimy w drugi warunek, drukujemy Processing y..., ściągamy z listy y, zostaje tylko x. Ponieważ jest jeden argument (${#} = 1) to wchodzimy do ifa i opróżniamy za pomocą shift 1 tego x, który się ostał. Przechodzimy do drugiej iteracji głównej pętli, czyli teraz argumentem jest x. Wchodzimy w pierwszy warunek. shift w tym momencie za wiele nie zrobi, bo lista jest już pusta.

0

@Riki: rzeczywiście kombinacja pętli while z ${1} wydaje się działać.

Napisałeś jednak, że "pętla for tak ma". Nie wiedziałem o tym; masz jakieś źródło?


@Pyxis: nie spotkałem się z czymś takim, żeby lista argumentów w Bashu była kopiowana do pętli for – ale w sumie nie znam Basha za dobrze. Masz jakieś źródło, gdzie to kopiowanie jest dokładnie opisane?

Jeżeli zaś jest dokładnie tak, jak mówisz, to w takim razie shitf-a nigdy nie należy używać z pętlą for, tak?

0

*W tamtym przykładzie, który napisałem brakuje jeszcze default'a "**)", aby pętla while się nie zawieszała, całość w mojej opinii też powinna mieć inną konstrukcję.

Wartości są kopiowane do pętli for. Pętla operuje na danych z chwili wywołania
Przykład:

a1=(1 2 3 4 5 6 7 8 9)

for item in ${a1[@]}; do
    echo "ARRAY: ${a1[@]}"
    a1=()
    echo "Item: $item"
done

po uruchomieniu będzie widać jak to dokładnie się dzieje.

1 użytkowników online, w tym zalogowanych: 0, gości: 1