Stateful функция 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.
Златовласка
Вы должны увидеть мое редактирование - думаю, вам понравится.
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}спасает день. Здесь это немного странно - мы на самом деле устанавливаем переменную, а сами отбрасываем ее. Мы используем его новое значение - set mid-strip - как шар, из которого мы раздеваемся. Вы видите? Мы ##*удаляем все от начала нашей переменной приращения до последнего символа, который может быть чем угодно [$((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 >
mikeserv
источник
1
@mikeserv Мы вращаемся кругами. Я все это знаю. Но как мне использовать это в моем определении PS2? Это сложная часть. Я не думаю, что ваше решение может быть применено здесь. Если вы думаете иначе, пожалуйста, покажите мне, как.
Конрад Рудольф
1
@mikeserv Нет, это не связано, извини. Смотрите мой вопрос для деталей. PS1и PS2являются специальными переменными в оболочке, которые печатаются как командная строка (попробуйте установить PS1другое значение в новом окне оболочки), поэтому они используются совершенно иначе, чем ваш код. Вот еще немного информации об их использовании: linuxconfig.org/bash-prompt-basics
Конрад Рудольф,
1
@KonradRudolph, что мешает вам определить их дважды? Что и сделал мой оригинальный поступок ... Я должен посмотреть на твой ответ ... Это делается постоянно.
mikeserv
1
@mikeserv Введите echo 'thisподсказку, затем объясните, как обновить значение, PS2прежде чем вводить заключительную одинарную кавычку.
chepner
1
Хорошо, этот ответ теперь официально удивителен. Мне также нравятся панировочные сухари, хотя я не приму их, так как в любом случае я печатаю полный путь отдельной строкой: i.imgur.com/xmqrVxL.png
Конрад Рудольф,
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"
}
chepner
источник
+1 Ввод / вывод, вероятно, не очень важен, поскольку, если файл небольшой и часто используется, он будет кэширован, т. Е. Он по существу функционирует как разделяемая память.
Златовласка
1

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

Как и в других ответах, самое простое, что нужно сделать - это спрятать эти данные в файл.

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

И т.п.

лютик золотистый
источник
Конечно, вы можете установить переменную таким образом. Вам не нужен временный файл. Вы устанавливаете переменную в подоболочке и выводите ее значение в родительскую оболочку, где вы принимаете это значение. Вы получаете все состояние, необходимое для вычисления его значения в подоболочке, и именно там вы делаете это.
mikeserv
1
@mikeserv Это не одно и то же, поэтому ОП сказал, что такое решение не будет работать (хотя это должно было быть более четко указано в вопросе). Вы имеете в виду передачу значения другому процессу через IPC, чтобы он мог присвоить это значение чему угодно. Оператор хотел / должен был сделать так, чтобы он влиял на значение глобальной переменной, разделяемой несколькими процессами, и вы не можете сделать это через среду; это не очень полезно для IPC.
Златовласка
Чувак, или я совершенно не понял, что здесь нужно, или все остальные. Это кажется мне очень простым. Вы видите мою правку? Что с этим не так?
mikeserv
@mikeserv Я не думаю , что вы неправильно поняли и быть справедливыми, что у вас есть это формой IPC и может сработать. Мне непонятно, почему Конраду это не нравится, но если он недостаточно гибок, то копить файл довольно просто (как, например, способы избежать коллизий mktemp).
Златовласка
2
@mikeserv Предполагаемая функция вызывается, когда значение PS2 расширяется оболочкой. В то время у вас нет возможности обновить значение переменной в родительской оболочке.
chepner
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"
}
Конрад Рудольф
источник