Державна функція bash


16

Я хотів би реалізувати функцію в Bash, яка збільшує (і повертає) кількість при кожному виклику. На жаль, це здається нетривіальним, оскільки я викликаю функцію всередині підклітини, і, отже, не вдається змінити змінні його батьківської оболонки.

Ось моя спроба:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

Це буде використано наступним чином (а отже, і моя потреба викликати функції з підшару):

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

Таким чином, я мав би нумерований багаторядковий рядок:

> echo 'this
1   is
2   a
3   test'

Симпатичний. Але через вищезазначене обмеження не працює.

Неробочим рішенням буде записувати підрахунок у файл замість змінної. Однак це створить конфлікт між декількома одночасно запущеними сесіями. Звичайно, я міг би додати ідентифікатор процесу оболонки до імені файлу. Але я сподіваюся, що є краще рішення, яке не буде захаращувати мою систему безліччю файлів.


WRT зіткнення з використанням сховища файлів див man 1 mktemp.
goldilocks

Ви повинні побачити мою редакцію - я думаю, вам сподобається.
mikeserv

Відповіді:


14

введіть тут опис зображення

Щоб отримати той самий результат, який ви зазначаєте у своєму запитанні, все, що потрібно, це:

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

Вам не потрібно контрувати. Ці два рядки будуть робити це у будь-якій оболонці, яка претендує на що-небудь, близьке до сумісності POSIX.

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

Але мені це сподобалось. І я хотів продемонструвати основи того, що робить цю роботу трохи кращою. Тому я це трохи відредагував. Я до цього часу застряг, /tmpале я думаю, що я збираюся це зберегти і для себе. Це тут:

cat /tmp/prompt

СКРИПТ ПРОМПИ:

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

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

Примітка: нещодавно дізнавшись про яш , я побудував його вчора. З будь-якої причини він не друкує перший байт кожного аргументу %cрядком - хоча документи були специфічними щодо розширень із широкою діаграмою для цього формату, і, можливо, це пов'язано, - але це добре справляється з%.1s

Ось і вся справа. Там відбуваються дві основні речі. І ось як це виглядає:

/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 >

ЧАСТИНА $PWD

Кожен раз, коли $PS1оцінюється, він аналізує та друкує, $PWDщоб додати до підказки. Але мені не подобається весь $PWDпереповнюючий екран, тому я хочу лише першу букву кожної сухарки в поточному контурі до поточного каталогу, яку я хотів би побачити повністю. Подобається це:

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

Тут є кілька кроків:

IFS=/

нам доведеться розділити поточний $PWDі найнадійніший спосіб зробити це з $IFSрозділенням /. Після цього взагалі не потрібно зациклюватися на цьому - все розщеплення звідси і далі буде визначено $@масивом позиційних параметрів оболонки в наступній команді, наприклад:

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

Отже, це трохи хитро, але головне, що ми розбиваємося $PWDна /символи. Я також використовую розширення параметрів, щоб призначити $lastвсім після будь-якого значення, що виникає між лівою лівою частиною та правою найбільшою /косою рисою. У такий спосіб я знаю, що якщо я просто у себе /і маю лише один, /то $lastвсе одно дорівнюватиме цілому $PWDі $1буде порожнім. Це має значення. Я також знімаю $lastз хвостового кінця $PWDперед тим, як призначити його $@.

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

Тож тут - доки ${1+is set}ми printfперший %cхарактер аргументів кожної своєї оболонки - який ми тільки що встановили до кожного каталогу в нашому поточному $PWD- за винятком верхнього каталогу - розділилися /. Таким чином, ми по суті просто друкуємо перший символ у кожному каталозі, $PWDокрім верхнього. Однак важливо усвідомити, що це відбувається лише тоді, коли $1він взагалі встановиться, що не відбудеться під коренем /або коли буде видалено /таке, як у /etc.

printf "$last > "

$last- це змінна, яку я щойно призначив нашому верхньому каталогу. Отже, це наш головний каталог. Він друкує, чи зробив останній вислів чи ні. І це потрібно для акуратної >мірки.

АЛЕ ЩО ПРО ВПРОВАДЖЕННЯ?

А тут справа $PS2умовного. Я раніше показав, як це можна зробити, що ви все ще можете знайти нижче - це принципово питання сфери застосування. Але є ще трохи до цього, якщо ви не хочете почати робити купу printf \bпросторів, а потім намагатися врівноважити їх кількість персонажів ... тьфу. Тому я роблю це:

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

Знову ж, ${parameter##expansion}економить день. Тут трохи дивно - ми фактично встановлюємо змінну, поки знімаємо її. Ми використовуємо його нове значення - встановлену середину смуги - як глобус, з якого ми знімаємо. Розумієш? Ми ##*знімаємо все від голови нашої змінної приросту до останнього символу, який може бути чим завгодно [$((PS2c=0))-9]. Ми гарантуємо таким чином, що не виводимо значення, і все-таки ми його призначаємо. Це досить круто - я ніколи цього не робив. Але POSIX також гарантує нам, що це самий портативний спосіб зробити це.

І саме завдяки POSIX вказано, ${parameter} $((expansion))що ці визначення зберігаються в поточній оболонці, не вимагаючи, щоб ми встановлювали їх в окрему підпакет, незалежно від того, де ми їх оцінюємо. І саме тому він працює в dashі shтак само добре, як і в bashі zsh. Ми не використовуємо оболонки / термінали, що залежать від оболонок / терміналів, і ми дозволяємо самим змінним перевірити. Ось що робить портативний код швидким.

Решта досить проста - просто приріст нашого лічильника щоразу $PS2оцінюється, поки $PS1знову не скине його. Подобається це:

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

Тож тепер я можу:

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

Це працює так само в bashабо 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

Як я вже говорив вище, головна проблема полягає в тому, що вам потрібно врахувати, де ви робите свої обчислення. Ви не отримуєте стан у батьківській оболонці - значить, ви не обчислюєте там. Ви отримуєте стан в нижній частині - значить, там і обчислюєте. Але ви робите визначення у батьківській оболонці.

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 Ми обертаємося по колах. Я все це знаю. Але як я можу це використовувати у своєму визначенні PS2? Це складна частина. Я не думаю, що ваше рішення можна застосувати тут. Якщо ви думаєте про інше, покажіть мені, як.
Конрад Рудольф

1
@mikeserv Ні, це не пов'язано, вибачте. Дивіться моє запитання для деталей. PS1і PS2це спеціальні змінні в оболонці, які друкуються як командний рядок (спробуйте, встановивши PS1інше значення у новому вікні оболонки), вони, таким чином, використовуються дуже по-різному від вашого коду. Ось додаткова інформація про їх використання: linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KonradRudolph що заважає тобі їх визначати двічі? Це те, що зробила моя оригінальна річ ... Я повинен подивитися на вашу відповідь ... Це робиться весь час.
mikeserv

1
@mikeserv Введіть echo 'thisпідказку, а потім поясніть, як оновити значення, PS2перш ніж набрати закриту цитату.
чепнер

1
Гаразд, ця відповідь зараз офіційно дивовижна. Мені також подобаються сухарі, хоча я не прийму її, оскільки я все-таки надрукую повний шлях окремим рядком: i.imgur.com/xmqrVxL.png
Konrad Rudolph

8

При такому підході (функція, запущена в підшлубку), ви не зможете оновлювати стан основного процесу оболонки, не проходячи перекручення. Натомість домовтеся, щоб функція запускалася в головному процесі.

Значення PROMPT_COMMANDзмінної інтерпретується як команда, яка виконується перед друкомPS1 запиту.

Бо PS2немає нічого порівняного. Але ви можете скористатися хитрістю замість цього: оскільки все, що ви хочете зробити, - це арифметична операція, ви можете використовувати арифметичне розширення, яке не передбачає підшару.

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

Результат арифметичних обчислень закінчується в підказці. Якщо ви хочете приховати його, ви можете передати його як індекс, що не існує.

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

4

Це трохи інтенсивне введення-виведення, але вам потрібно використовувати тимчасовий файл, щоб утримувати значення підрахунку.

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

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

Якщо ви стурбовані необхідністю окремого файлу за сеанс оболонки (що здається незначною проблемою; чи справді ви будете вводити багаторядкові команди в дві різні оболонки одночасно?), Вам слід використовувати mktempдля створення нового файлу для кожного використання.

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 Введення / виведення, ймовірно, не дуже суттєве, оскільки якщо файл невеликий і часто доступ до нього, він буде кешований, тобто він по суті функціонує як спільна пам'ять.
goldilocks

1

Ви не можете використовувати змінну оболонки таким чином, і ви вже розумієте, чому. Піддіаграма успадковує змінні точно так само, як процес успадковує своє оточення: будь-які внесені зміни застосовуються лише його та його дітей, а не будь-якого процесу предків.

Згідно з іншими відповідями, найпростіше зробити це зберігання даних у файлі.

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

І т.д.


Звичайно, ви можете встановити змінну таким чином. Вам не потрібен тимчасовий файл. Ви встановлюєте змінну в підклітині і друкуєте її значення в батьківській оболонці, де ви поглинаєте це значення. Ви отримуєте все необхідне стан, щоб обчислити його значення в нижній частині, так що це ви робите.
mikeserv

1
@mikeserv Це не те саме, саме тому ОП заявило, що таке рішення не буде працювати (хоча це повинно було бути зрозумілішим у питанні). Те, про що ви маєте на увазі, передає значення іншому процесу через IPC, щоб він міг призначити це значення будь-якому. Те, що ОП хотіло / потрібно було зробити, впливало на значення глобальної змінної, поділеної багатьма процесами, і ви не можете цього зробити через середовище; це не дуже корисно для IPC.
goldilocks

Людина, або я повністю зрозумів, що тут потрібно, або всі інші. Мені це здається дуже простим. Ви бачите мою редакцію? Що з цим не так?
mikeserv

@mikeserv Я не вважаю, що ви зрозуміли неправильно і щоб бути чесним, те, що у вас є, є формою МПК і може працювати. Мені незрозуміло, чому Конраду це не подобається, але якщо він недостатньо гнучкий, сховище файлів є досить простим (і так є способи уникнути зіткнень, наприклад mktemp).
goldilocks

2
@mikeserv Зазначена функція викликається, коли значення PS2розширюється оболонкою. У вас немає можливості оновити значення змінної в батьківській оболонці на той час.
чепнер

0

Для довідки, ось моє рішення з використанням тимчасових файлів, які є унікальними для кожного процесу оболонки, і видаляються якнайшвидше (щоб уникнути безладу, про що йдеться у питанні):

# 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"
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.