有状态的 bash 函数

有状态的 bash 函数

我想在 Bash 中实现一个函数,每次调用都会增加(并返回)计数。不幸的是,这似乎并不简单,因为我在子 shell 内调用该函数,因此它无法修改其父 shell 的变量。

这是我的尝试:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

这将按如下方式使用(因此我需要从子 shell 调用函数):

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

这样,我就会得到一个编号的多行提示:

> echo 'this
1   is
2   a
3   test'

可爱的。但由于上述限制不起作用。

一个无效的解决方案是将计数写入文件而不是变量。但是,这会在多个同时运行的会话之间产生冲突。当然,我可以将 shell 的进程 ID 附加到文件名中。但我希望有一个更好的解决方案,不会因大量文件而使我的系统混乱。

答案1

在此输入图像描述

要获得与您在问题中注意到的相同的输出,所需要做的就是:

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

你不需要扭曲。这两行代码将在任何伪装成接近 POSIX 兼容性的 shell 中完成这一切。

- > 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,最可靠的方法是使用$IFSsplit on /。之后根本不需要费心——从这里开始的所有分割都将由$@下一个命令中 shell 的位置参数数组定义,例如:

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

所以这个有点棘手,但最主要的是我们在符号$PWD上进行了分裂。/我还使用参数扩展来分配$last最左边和最右边/斜杠之间出现的任何值之后的所有内容。这样我就知道,如果我只是在/并且只有一个,/那么$last仍然等于整体$PWD并且$1是空的。这很重要。在将其分配给 之前,我还$last从 的尾部剥离。$PWD$@

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

所以在这里 - 只要${1+is set}我们每个 shell 参数的printf第一个%c字符 - 我们刚刚将其设置为当前目录中的每个目录$PWD- 减去顶级目录 - 就可以分割/。因此,我们本质上只是打印$PWD除顶部目录之外的每个目录的第一个字符。但重要的是要认识到这种情况只有在$1被设置时才会发生,而这不会发生在 root/或从/诸如/etc.

printf "$last > "

$last是我刚刚分配给我们的顶级目录的变量。现在这是我们的顶级目录。它打印最后一条语句是否执行。>要想达到良好的效果,需要一点点整洁。

但增量又如何呢?

然后是条件问题$PS2。我之前展示了如何做到这一点,您仍然可以在下面找到它 - 这从根本上来说是一个范围问题。但还有更多的内容,除非你想开始做一堆printf \backspace,然后尝试平衡它们的字符数......呃。所以我这样做:

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

再次${parameter##expansion}拯救了这一天。不过这里有点奇怪 - 我们实际上是在删除变量本身的同时设置了变量。我们使用它的新值 - set mid-strip - 作为我们从中剥离的 glob。你看?我们##*删除从增量变量的头部到最后一个字符的所有字符,可以是 中的任何字符[$((PS2c=0))-9]。通过这种方式,我们保证不输出该值,但我们仍然分配它。这很酷——我以前从未这样做过。但 POSIX 也向我们保证这是最可移植的方式。

这要归功于 POSIX 规定${parameter} $((expansion)),将这些定义保留在当前 shell 中,而不需要我们将它们设置在单独的子 shell 中,无论我们在哪里评估它们。这就是为什么它在dash和中的效果与在和 中的sh效果一样好。我们不使用 shell/终端相关的转义符,并且让变量自行测试。这就是可移植代码的原因bashzsh快的。

其余的相当简单 - 只需在每次$PS2评估时增加我们的计数器,直到$PS1再次重置它。像这样:

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

所以现在我可以:

达世币演示

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 >

上海演示

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

正如我上面所说,主要问题是您需要考虑在哪里进行计算。您无法在父 shell 中获取状态 - 因此您不会在那里进行计算。您可以在子 shell 中获取状态 - 这就是您进行计算的地方。但是您在父 shell 中进行定义。

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 >

答案2

使用这种方法(在子 shell 中运行的函数),您将无法在不进行扭曲的情况下更新主 shell 进程的状态。相反,安排该函数在主进程中运行。

的价值PROMPT_COMMAND变量被解释为在打印提示之前执行的命令PS1

对于PS2,没有什么可比的。但您可以使用一个技巧:由于您想要做的只是算术运算,因此您可以使用算术扩展,它不涉及子 shell。

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

算术计算的结果最终出现在提示符中。如果你想隐藏它,你可以将它作为不存在的数组下标传递。

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

答案3

这有点 I/O 密集型,但您需要使用临时文件来保存计数值。

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

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

如果您担心每个 shell 会话需要一个单独的文件(这似乎是一个小问题;您真的会同时在两个不同的 shell 中键入多行命令吗?),您应该mktemp为每个 shell 创建一个新文件使用。

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"
}

答案4

不能以这种方式使用 shell 变量,您已经明白为什么了。子 shell 继承变量的方式与进程继承其环境的方式完全相同:所做的任何更改都适用仅有的对于它及其子进程,而不是任何祖先进程。

根据其他答案,最简单的事情是将数据存储在文件中。

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

ETC。

相关内容