Stanowa funkcja bash


16

Chciałbym zaimplementować funkcję w Bash, która zwiększa (i zwraca) liczbę przy każdym wywołaniu. Niestety wydaje się to nietrywialne, ponieważ wywołuję funkcję wewnątrz podpowłoki, w związku z czym nie może ona modyfikować zmiennych powłoki nadrzędnej.

Oto moja próba:

PS_COUNT=0

ps_count_inc() {
    let PS_COUNT=PS_COUNT+1
    echo $PS_COUNT
}

ps_count_reset() {
    let PS_COUNT=0
}

Byłoby to użyte w następujący sposób (a zatem moja potrzeba wywoływania funkcji z podpowłoki):

PS1='$(ps_count_reset)> '
PS2='$(ps_count_inc)   '

W ten sposób otrzymałem numerowany monit wieloliniowy:

> echo 'this
1   is
2   a
3   test'

Uroczy. Ale ze względu na wyżej wymienione ograniczenie nie działa.

Niedziałającym rozwiązaniem byłoby zapisanie liczby do pliku zamiast do zmiennej. Spowodowałoby to jednak konflikt między wieloma jednocześnie uruchomionymi sesjami. Oczywiście mogę dołączyć identyfikator procesu powłoki do nazwy pliku. Mam jednak nadzieję, że istnieje lepsze rozwiązanie, które nie zaśmieci mojego systemu dużą ilością plików.


Kolizje WRT przy użyciu skrytki pliku patrz man 1 mktemp.
złotowłosa

Powinieneś zobaczyć moją edycję - myślę, że ci się spodoba.
mikeserv

Odpowiedzi:


14

wprowadź opis zdjęcia tutaj

Aby uzyskać taki sam wynik, jaki zanotowano w pytaniu, wystarczy:

PS1='${PS2c##*[$((PS2c=0))-9]}- > '
PS2='$((PS2c=PS2c+1)) > '

Nie musisz się wykręcać. Te dwie linie zrobią to wszystko w dowolnej powłoce, która udaje, że jest bliska kompatybilności z POSIX.

- > cat <<HD
1 >     line 1
2 >     line $((PS2c-1))
3 > HD
    line 1
    line 2
- > echo $PS2c
0

Ale mi się podobało. Chciałem też zademonstrować podstawy tego, co czyni tę pracę nieco lepszą. Więc trochę to zredagowałem. Wcisnąłem to /tmpna razie, ale myślę, że też zatrzymam to dla siebie. To tu:

cat /tmp/prompt

PISMO PISMO:

ps1() { IFS=/
    set -- ${PWD%"${last=${PWD##/*/}}"}
    printf "${1+%c/}" "$@" 
    printf "$last > "
}

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
PS2='$((PS2c=PS2c+1)) > '

Uwaga: niedawno dowiedziałem się o yash , zbudowałem go wczoraj. Z jakiegokolwiek powodu nie wypisuje pierwszego bajtu każdego argumentu z %cciągiem znaków - chociaż dokumentacja była specyficzna dla rozszerzeń szerokich znaków dla tego formatu i dlatego może być spokrewniona - ale działa dobrze z%.1s

To wszystko. Działają tam dwie główne rzeczy. I tak to wygląda:

/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 >

ROZBIÓR GRAMATYCZNY ZDANIA $PWD

Za każdym razem, gdy $PS1jest oceniany, analizuje i drukuje, $PWDaby dodać do monitu. Ale nie podoba mi się to, że cały $PWDekran jest zatłoczony, więc chcę tylko pierwszą literę każdej bułki tartej w bieżącej ścieżce do bieżącego katalogu, który chciałbym zobaczyć w całości. Lubię to:

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cd /
/ > cd ~
/h/mikeserv > 

Tutaj jest kilka kroków:

IFS=/

będziemy musieli podzielić obecny $PWDi najbardziej niezawodny sposób, aby to zrobić, używając $IFSpodziału /. Po tym nie musisz się już tym przejmować - całe dzielenie od teraz będzie definiowane przez $@tablicę parametrów pozycyjnych powłoki w następnej komendzie, np .:

set -- ${PWD%"${last=${PWD##/*/}}"}

To jest trochę trudne, ale najważniejsze jest to, że dzielimy się $PWDna /symbole. Korzystam również z rozszerzania parametrów, aby przypisywać $lastwszystko po dowolnej wartości występującej między /ukośnikiem od lewej i prawej . W ten sposób wiem, że jeśli jestem tylko /i mam tylko jedną, /to $lastnadal będzie równa całości $PWDi $1będzie pusta. To ma znaczenie Usuwam również $lastz końca ogona $PWDprzed przypisaniem go do $@.

printf "${1+%c/}" "$@"

Więc tutaj - o ile ${1+is set}jesteśmy printfpierwszymi %ccechami argumentów każdej powłoki - które właśnie ustawiliśmy dla każdego katalogu w naszym bieżącym $PWDkatalogu - bez katalogu głównego - podzieliliśmy się /. Tak więc zasadniczo drukujemy tylko pierwszy znak każdego katalogu w, $PWDoprócz pierwszego. Ważne jest jednak, aby zdawać sobie sprawę, że dzieje się tak tylko wtedy, gdy $1zostanie w ogóle ustawiony, co nie nastąpi w katalogu głównym /lub w jednym usuniętym z /takiego jak /etc.

printf "$last > "

$lastto zmienna, którą właśnie przypisałem do naszego głównego katalogu. Więc teraz jest to nasz główny katalog. Drukuje, czy zrobiła to ostatnia instrukcja. I >na dobrą sprawę trzeba trochę schludnie .

ALE CO Z WYKORZYSTANIEM?

A potem jest kwestia $PS2warunkowa. Wcześniej pokazałem, jak można to zrobić, co wciąż można znaleźć poniżej - jest to zasadniczo kwestia zakresu. Ale jest jeszcze coś więcej, chyba że chcesz zacząć robić kilka miejsc printf \bpotwierdzających, a następnie próbować zrównoważyć liczbę ich postaci ... eee. Więc robię to:

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'

Znowu ${parameter##expansion}ratuje dzień. Jest to jednak trochę dziwne - ustawiamy zmienną, podczas gdy usuwamy ją z siebie. Używamy jej nowej wartości - zestawu środkowego paska - jako globu, z którego usuwamy. Zobaczysz? Mamy ##*rozebrać wszystko z głowy naszej zmiennej przyrostu do ostatniego znaku, który może być coś z [$((PS2c=0))-9]. W ten sposób gwarantujemy, że nie wyprowadzimy wartości, a jednak nadal ją przypisujemy. To całkiem fajne - nigdy wcześniej tego nie robiłem. Ale POSIX gwarantuje nam również, że jest to najbardziej przenośny sposób, w jaki można to zrobić.

I to dzięki specyfikacji POSIX, ${parameter} $((expansion))która przechowuje te definicje w bieżącej powłoce bez konieczności umieszczania ich w osobnej podpowłoce, niezależnie od tego, gdzie je oceniamy. I dlatego działa w dashi shtak samo dobrze jak w bashi zsh. Nie używamy żadnych ucieczek zależnych od powłoki / terminala i pozwalamy, aby zmienne same się testowały. To sprawia, że ​​przenośny kod jest szybki.

Reszta jest dość prosta - wystarczy zwiększyć nasz licznik za każdym razem, gdy $PS2jest oceniany, aż $PS1ponownie go zresetuje. Lubię to:

PS2='$((PS2c=PS2c+1)) > '

Teraz mogę:

DASH DEMO

ENV=/tmp/prompt dash -i

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 > printf '\t%s\n' "$PS1" "$PS2" "$PS2c"
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
    0
/u/s/m/man3 > cd ~
/h/mikeserv >

SH DEMO

To działa tak samo w bashlub sh:

ENV=/tmp/prompt sh -i

/h/mikeserv > cat <<HEREDOC
1 >     $( echo $PS2c )
2 >     $( echo $PS1 )
3 >     $( echo $PS2 )
4 > HEREDOC
    4
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
/h/mikeserv > echo $PS2c ; cd /
0
/ > cd /usr/share
/u/share > cd ~
/h/mikeserv > exit

Jak powiedziałem powyżej, głównym problemem jest to, że musisz rozważyć miejsce wykonywania obliczeń. Nie otrzymujesz stanu w powłoce nadrzędnej - więc tam nie obliczasz. Otrzymujesz stan w podpowłoce - więc tam obliczasz. Ale robisz definicję w powłoce nadrzędnej.

ENV=/dev/fd/3 sh -i  3<<\PROMPT
    ps1() { printf '$((PS2c=0)) > ' ; }
    ps2() { printf '$((PS2c=PS2c+1)) > ' ; }
    PS1=$(ps1)
    PS2=$(ps2)
PROMPT

0 > cat <<MULTI_LINE
1 > $(echo this will be line 1)
2 > $(echo and this line 2)
3 > $(echo here is line 3)
4 > MULTI_LINE
this will be line 1
and this line 2
here is line 3
0 >

1
@mikeserv Kręcimy się w kółko. Wiem to wszystko. Ale jak mam to wykorzystać w mojej definicji PS2? To trudna część. Nie sądzę, aby twoje rozwiązanie można było zastosować tutaj. Jeśli uważasz inaczej, pokaż mi, jak to zrobić.
Konrad Rudolph

1
@ Mikeserv Nie, to nie jest związane, przepraszam. Zobacz moje pytanie po szczegóły. PS1i PS2są specjalnymi zmiennymi w powłoce, które są drukowane jako wiersz polecenia (wypróbuj to, ustawiając PS1inną wartość w nowym oknie powłoki), dlatego są one używane zupełnie inaczej niż twój kod. Oto kilka informacji o ich użyciu: linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KonradRudolph co powstrzymuje cię przed zdefiniowaniem ich dwukrotnie? Tak właśnie zrobiła moja oryginalna rzecz ... Muszę spojrzeć na twoją odpowiedź ... Robi się to cały czas.
mikeserv

1
@mikeserv Wpisz echo 'thisw odpowiedzi na monit, a następnie wyjaśnij, jak zaktualizować wartość PS2przed wpisaniem zamykającego pojedynczego cudzysłowu.
chepner

1
Dobra, ta odpowiedź jest teraz oficjalnie niesamowita. Lubię też bułkę tartą, chociaż nie przyjmuję jej, ponieważ i tak drukuję
Konrad Rudolph

8

Dzięki takiemu podejściu (funkcja działająca w podpowłoce) nie będziesz w stanie zaktualizować stanu procesu powłoki głównej bez przechodzenia przez zniekształcenia. Zamiast tego należy ustawić, aby funkcja działała w procesie głównym.

Wartość PROMPT_COMMANDzmiennej jest interpretowana jako polecenie, które jest wykonywane przed wydrukowaniem PS1monitu.

Bo PS2nie ma nic porównywalnego. Możesz jednak użyć lewy: ponieważ wszystko, co chcesz zrobić, to operacja arytmetyczna, możesz użyć rozszerzenia arytmetycznego, które nie wymaga podpowłoki.

PROMPT_COMMAND='PS_COUNT=0'
PS2='$((++PS_COUNT))  '

Wynik obliczeń arytmetycznych kończy się na pytaniu. Jeśli chcesz go ukryć, możesz przekazać go jako indeks tablicy, który nie istnieje.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '

4

Jest to trochę intensywne we / wy, ale musisz użyć pliku tymczasowego, aby zachować wartość liczby.

ps_count_inc () {
   read ps_count < ~/.prompt_num
   echo $((++ps_count)) | tee ~/.prompt_num
}

ps_count_reset () {
   echo 0 > ~/.prompt_num
}

Jeśli obawiasz się, że potrzebujesz osobnego pliku na sesję powłoki (co wydaje się drobną obawą; czy naprawdę będziesz wpisywać polecenia wieloliniowe jednocześnie w dwóch różnych powłokach?), Powinieneś użyć, mktempaby utworzyć nowy plik dla każdej posługiwać się.

ps_count_reset () {
    rm -f "$prompt_count"
    prompt_count=$(mktemp)
    echo 0 > "$prompt_count"
}

ps_count_inc () {
    read ps_count < "$prompt_count"
    echo $((++ps_count)) | tee "$prompt_count"
}

+1 I / O prawdopodobnie nie ma większego znaczenia, ponieważ jeśli plik jest mały i jest często używany, zostanie zbuforowany, tzn. Zasadniczo działa jako pamięć współdzielona.
Złotowłosa

1

Nie możesz w ten sposób używać zmiennej powłoki i już rozumiesz dlaczego. Podkładka dziedziczy zmienne dokładnie tak samo, jak proces dziedziczy środowisko: wszelkie wprowadzone zmiany dotyczą tylko niego i jego elementów potomnych, a nie żadnego procesu przodka.

Zgodnie z innymi odpowiedziami najłatwiej jest ukryć te dane w pliku.

echo $count > file
count=$(<file)

Itp.


Oczywiście możesz w ten sposób ustawić zmienną. Nie potrzebujesz pliku tymczasowego. Ustawiasz zmienną w podpowłoce i wypisujesz jej wartość w powłoce nadrzędnej, gdzie absorbujesz tę wartość. Otrzymujesz cały stan, którego potrzebujesz, aby obliczyć jego wartość w podpowłoce, więc tam właśnie to robisz.
mikeserv

1
@mikeserv To nie to samo, dlatego OP powiedział, że takie rozwiązanie nie zadziała (chociaż powinno to być wyjaśnione w pytaniu). To, o czym mówisz, to przekazanie wartości do innego procesu za pośrednictwem IPC, aby mógł on przypisać tę wartość dowolnemu. To, czego OP chciał / musiał zrobić, miało wpływ na wartość zmiennej globalnej współdzielonej przez wiele procesów i nie można tego zrobić za pośrednictwem środowiska; nie jest to bardzo przydatne w przypadku IPC.
złotowłosa

Człowieku, albo zupełnie nie zrozumiałem, co jest tutaj potrzebne, albo wszyscy inni. Wydaje mi się to naprawdę proste. Widzisz moją edycję? Co jest z tym nie tak?
mikeserv

@ mikeserv Nie sądzę, że źle zrozumiałeś i uczciwie, to, co masz, jest formą IPC i może działać. Nie jest dla mnie jasne, dlaczego Konradowi się to nie podoba, ale jeśli nie jest wystarczająco elastyczny, skrytka pliku jest dość prosta (i tak są sposoby, na przykład, aby uniknąć kolizji mktemp).
złotowłosa

2
@mikeserv Planowana funkcja jest wywoływana, gdy wartość parametru PS2jest rozszerzana przez powłokę. W tej chwili nie masz możliwości zaktualizowania wartości zmiennej w powłoce nadrzędnej.
chepner

0

Dla porównania, oto moje rozwiązanie wykorzystujące pliki tymczasowe, które są unikalne dla każdego procesu powłoki i usunięte tak szybko, jak to możliwe (aby uniknąć bałaganu, jak wspomniano w pytaniu):

# Yes, I actually need this to work across my systems. :-/
_mktemp() {
    local tmpfile="${TMPDIR-/tmp}/psfile-$$.XXX"
    local bin="$(command -v mktemp || echo echo)"
    local file="$($bin "$tmpfile")"
    rm -f "$file"
    echo "$file"
}

PS_COUNT_FILE="$(_mktemp)"

ps_count_inc() {
    local PS_COUNT
    if [[ -f "$PS_COUNT_FILE" ]]; then
        let PS_COUNT=$(<"$PS_COUNT_FILE")+1
    else
        PS_COUNT=1
    fi

    echo $PS_COUNT | tee "$PS_COUNT_FILE"
}

ps_count_reset() {
    rm -f "$PS_COUNT_FILE"
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.