有状态的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
}

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

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

这样,我将获得一个编号的多行提示:

> echo 'this
1   is
2   a
3   test'

可爱。但由于上述限制,因此无法使用。

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


使用文件存储的WRT冲突请参见man 1 mktemp
goldilocks 2014年

您应该会看到我的编辑-我想您会喜欢的。
mikeserv

Answers:


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)) > '

注意:我最近了解了yash,所以昨天就做了。不管出于什么原因,它都不会打印出带有%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}我们printf%c壳的每个参数的第一个讨厌对象-只需将其设置为当前目录中的每个目录$PWD-减去顶层目录,即可拆分到/。因此,我们实际上只是在打印$PWD除顶部目录之外的每个目录的第一个字符。重要的是要意识到这仅$1在完全设置时才会发生,而根本不会发生/或从中删除时不会发生/诸如in中/etc

printf "$last > "

$last是我刚分配给顶层目录的变量。现在这是我们的顶级目录。它打印最后一条语句是否执行。它需要一个整洁的一点>

但是增加什么呢?

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

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

再次,${parameter##expansion}节省了一天。不过,这有点奇怪-我们实际上在设置变量的同时设置了变量本身。我们使用它的新值-设置中间条-作为我们从中剥离的球体。你看?我们##*将所有从增量变量的开头剥离到最后一个字符,该字符可以是[$((PS2c=0))-9]。我们以这种方式保证不会输出该值,但是我们仍将其分配给它。非常酷-我以前从未做过。但是POSIX还向我们保证,这是最便携的方法。

这要归功于POSIX规范${parameter} $((expansion)),这些定义将这些定义保留在当前的shell中,而不需要我们在单独的子shell中设置它们,而无论我们在何处进行评估。这就是为什么它在dashsh一样好,因为它在bashzsh。我们不使用任何依赖于外壳/终端的转义符,而是让变量进行自我测试。这就是使可移植代码快速的原因。

其余的工作非常简单-每次$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演示

它在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中获得状态-这样便可以在其中进行计算。但是您可以在父外壳中进行定义。

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呢?是棘手的部分。我认为您的解决方案无法在此处应用。如果您有其他疑问,请告诉我如何操作。
Konrad Rudolph

1
@mikeserv不,没关系,对不起。有关详细信息,请参见我的问题。PS1并且PS2是外壳程序中的特殊变量,它们作为命令提示符打印(通过PS1在新的外壳程序窗口中将其设置为不同的值来尝试),因此它们的使用方式与您的代码有很大不同。以下是有关其用法的更多信息:linuxconfig.org/bash-prompt-basics
Konrad Rudolph

1
@KonradRudolph是什么阻止您两次定义它们?这是我最初的工作...我必须看一下您的答案...一直都在做。
mikeserv

1
@mikeserv echo 'this在提示符下键入,然后解释PS2在键入结束单引号之前如何更新的值。
chepner

1
好的,这个答案现在正式令人惊奇。我也喜欢面包屑,尽管我不会采用它,因为无论如何我都会在单独的行中打印完整路径:i.imgur.com/xmqrVxL.png
Konrad Rudolph

8

使用这种方法(功能在子shell中运行),您将无法不扭曲地更新主shell进程的状态。而是安排该函数在主进程中运行。

PROMPT_COMMAND变量的值与在打印PS1提示之前执行的命令有关。

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

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

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

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

4

这有点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来为每个文件创建一个新文件采用。

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可能不是很重要,因为如果文件很小并且被频繁访问,它将被缓存,即,它实际上起着共享内存的作用。
goldilocks 2014年

1

不能以这种方式使用shell变量,并且您已经了解了原因。子Shell继承变量的方式与进程继承其环境的方式完全相同:所做的任何更改适用于其及其子级,而不适用于任何祖先进程。

按照其他答案,最简单的方法是将该数据存储在文件中。

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

等等。


当然,您可以通过这种方式设置变量。您不需要临时文件。您可以在子外壳中设置变量,然后将其值打印到父外壳,然后在其中吸收该值。您获得了在子外壳中计算其值所需的所有状态,因此可以在其中进行操作。
mikeserv

1
@mikeserv并不是同一回事,这就是OP表示这样的解决方案不起作用的原因(尽管应该在问题中更清楚地说明这一点)。您指的是通过IPC将值传递给另一个进程,以便它可以将该值分配给任何进程。OP想要/需要做的事情是影响多个进程共享的全局变量的值,而您不能通过环境来做到这一点。对于IPC来说不是很有用。
goldilocks 2014年

伙计,或者我完全误解了这里的需求,或者其他所有人都有。对我来说似乎很简单。您看到我的编辑了吗?它出什么问题了?
mikeserv

@mikeserv我不认为您会误会并且说句公道话,您拥有的 IPC的一种形式并且可以工作。我尚不清楚为什么Konrad不喜欢它,但是如果它不够灵活,则文件存储非常简单(例如,避免冲突的方法也是如此mktemp)。
goldilocks 2014年

2
@mikeserv当PS2shell扩展的值时,将调用预期的函数。那时您没有机会更新父外壳中变量的值。
chepner

0

作为参考,这是我的使用临时文件的解决方案,该文件在每个shell进程中都是唯一的,并且应尽快删除(以避免出现混乱,如问题所暗示):

# 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"
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.