web-dev-qa-db-ja.com

ステートフルbash関数

呼び出しごとにカウントを増やす(そして返す)関数を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'

可愛い。ただし、上記の制限により、機能しません。

動作しない解決策は、変数ではなくファイルにカウントを書き込むことです。ただし、これにより、同時に実行されている複数のセッション間で競合が発生します。もちろん、シェルのプロセスIDをファイル名に追加することもできます。しかし、システムが大量のファイルで乱雑にならない、より良いソリューションがあることを望んでいます。

16
Konrad Rudolph

enter image description here

質問でメモしたのと同じ出力を取得するには、必要なのはこれだけです。

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

ゆがめる必要はありません。これらの2行は、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_

それがすべてです。そこで起こっている主なことが2つあります。そして、これはそれがどのように見えるかです:

_/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_スラッシュの間に値が発生した後のすべてを_/_に割り当てます。このようにして、私が_/_にいて、_/_が1つしかない場合、_$last_は_$PWD_全体と等しくなり、_$1_は空の。これは重要です。また、_$last_に割り当てる前に、_$PWD_の末尾から_$@_を削除します。

_printf "${1+%c/}" "$@"_

したがって、ここで-_${1+is set}_である限り、printf各シェルの引数の最初の_%c_ haracter-現在の_$PWD_の各ディレクトリに設定したばかりです-以下トップディレクトリ-_/_で分割。したがって、基本的には_$PWD_のすべてのディレクトリの最初の文字を出力しますが、一番上の文字を出力します。ただし、これは_$1_がまったく設定されていない場合にのみ発生し、ルート_/_または_/_から削除された場合(_/etc_など)には発生しないことを理解することが重要です。

_printf "$last > "_

_$last_は、トップディレクトリに割り当てたばかりの変数です。これがトップディレクトリです。最後のステートメントが実行したかどうかを出力します。そして、それは適切な測定のためにきちんとした小さな_>_を取ります。

しかし、インクリメントについてはどうですか?

そして、_$PS2_条件付きの問題があります。これをどのように行うことができるかを前に示しましたが、これはまだ以下にあります。これは基本的にスコープの問題です。しかし、_printf \b_ ackspacesをたくさん実行してから、それらの文字数のバランスをとろうとしない限り、もう少し多くのことがあります...うーん。だから私はこれを行います:

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

繰り返しますが、_${parameter##expansion}_はその日を節約します。ただし、ここでは少し奇妙です。実際には、変数を削除しながら変数を設定します。ストリップするグロブとして、その新しい値(ストリップの中間に設定)を使用します。分かりますか?インクリメント変数の先頭から最後の文字まですべてを_##*_ストリップします。最後の文字は[$((PS2c=0))-9]から何でもかまいません。このようにして値を出力しないことが保証されていますが、それでも値を割り当てます。それはかなりクールです-私は前にそれをしたことがありません。しかし、POSIXは、これがこれを実行できる最も移植性の高い方法であることも保証します。

そして、POSIXで指定された${parameter} $((expansion))のおかげで、評価する場所に関係なく、これらの定義を別のサブシェルに設定しなくても、現在のシェルに保持されます。これが、dashshで機能するのと同じように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 >
_

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
_

上で述べたように、主な問題は、どこで計算を行うかを考慮する必要があるということです。親シェルで状態を取得しないため、そこで計算しません。サブシェルで状態を取得します-そこで計算します。ただし、親シェルで定義を行います。

_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 >
_
14
mikeserv

このアプローチ(サブシェルで実行される関数)では、ゆがみを経ずにマスターシェルプロセスの状態を更新することはできません。代わりに、関数がマスタープロセスで実行されるように調整してください。

Prompt_COMMAND 変数の値は、PS1プロンプトを出力する前に実行されるコマンドとして解釈されます。

PS2の場合、これに匹敵するものはありません。ただし、代わりにトリックを使用できます。実行したいのは算術演算だけなので、サブシェルを含まない算術展開を使用できます。

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

算術計算の結果は、プロンプトに表示されます。非表示にする場合は、存在しない配列添え字として渡すことができます。

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

少しI/Oが集中しますが、カウントの値を保持するために一時ファイルを使用する必要があります。

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

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

シェルセッションごとに個別のファイルが必要になることを懸念している場合(これは小さな懸念のようです。実際には、2つの異なるシェルで同時に複数行のコマンドを入力するのでしょうか?)、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"
}
4
chepner

あなたできませんこのようにシェル変数を使用すると、その理由はすでに理解できます。サブシェルは、プロセスがその環境を継承するのとまったく同じ方法で変数を継承します。加えられた変更は、サブシェルとその子にのみ適用されのみ、祖先プロセスには適用されません。

他の回答によると、最も簡単な方法は、そのデータをファイルに隠しておくことです。

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

等。

1
goldilocks

参考までに、シェルプロセスごとに一意であり、できるだけ早く削除される一時ファイルを使用する私のソリューションを次に示します(質問で言及されているように、混乱を避けるため)。

# 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"
}
0
Konrad Rudolph