ステートフル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'

可愛い。しかし、上記の制限のために機能しません。

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


ファイルスタッシュを使用したWRTの衝突を参照してくださいman 1 mktemp
goldilocks 14

あなたは私の編集を見るはずです-あなたはそれを好きになると思います。
mikeserv 14

回答:


14

ここに画像の説明を入力してください

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

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あり、そのための最も信頼できる方法は$IFSsplit onを使用すること/です。後でそれを気にする必要はありません-これ以降のすべての分割$@は、次のようなコマンドのシェルの位置パラメータ配列によって定義されます:

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

この1つは少しトリッキーですが、主なものは、我々している分裂ということであるので、$PWD上の/シンボル。また、パラメーター拡張を使用し$lastて、左端と右端の/スラッシュの間にある値の後にすべてを割り当てます。このようにして、私がちょうど/1人だけいて、1人しかいない/場合$lastでも、全体$PWDと等しく$1なり、空になることを知っています。これは重要です。また$last$PWDに割り当てる前にの末尾から削除し$@ます。

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

だからここに-限り${1+is set}、私たちprintf最初の%c各たちのシェルの引数のharacter -私達はちょうど私たちの現在の各ディレクトリに設定しました$PWD-レストップディレクトリ-に分割/。したがって、基本的に$PWDは、最上位以外のすべてのディレクトリの最初の文字を印刷するだけです。場合にのみ起こる実現するためにかかわらず、それは重要だ$1ルートに起こらないであろう、まったく設定されます/または1でから取り出し/などのように/etc

printf "$last > "

$last最上位ディレクトリに割り当てたばかりの変数です。これが今、私たちのトップディレクトリです。最後のステートメントが実行したかどうかを出力します。そして、それは>適切な測定のために少しきれいになります。

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

そして、$PS2条件の問題があります。以前にこれをどのように行うことができるかを示しましたが、それは以下で見つけることができます-これは基本的にスコープの問題です。しかし、多くのprintf \backspaces を始めてから、キャラクター数のバランスをとろうとしない限り、それはもう少しあります...うーん。だから私はこれをします:

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

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

そして、${parameter} $((expansion))これらの定義を評価する場所に関係なく、別のサブシェルに設定することなく、現在のシェルにこれらの定義を保持するPOSIX指定のおかげです。で、それが動作する理由、これがある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 >

1
@mikeserv私たちは輪になっています。私はこれをすべて知っています。しかし、私の定義でこれをどのように使用しPS2ますか?これは難しい部分です。ここであなたのソリューションを適用できるとは思いません。そうでないと思われる場合は、その方法を教えてください。
コンラッドルドルフ14

1
@mikeservいいえ、それは無関係です、ごめんなさい。詳細については私の質問をご覧ください。PS1PS2は、コマンドプロンプトとして表示されるシェル内の特別な変数であり(PS1新しいシェルウィンドウで別の値に設定して試してください)、コードとは非常に異なって使用されます。使用法に関する詳細情報を次に示します。linuxconfig.org
Konrad Rudolph 14

1
@KonradRudolphあなたがそれらを二度定義するのを止めることは何ですか?これが私の元々のことです...私はあなたの答えを見なければなりません...これは常に行われています。
mikeserv 14

1
@mikeserv echo 'thisプロンプトで入力し、次にPS2一重引用符を入力する前に値を更新する方法を説明します。
chepner 14

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

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

+1 I / Oはおそらくそれほど重要ではありません。ファイルが小さく、頻繁にアクセスされる場合、キャッシュされる、つまり、本質的に共有メモリとして機能するからです。
goldilocks 14

1

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

他の回答によると、最も簡単なことは、そのデータをファイルに格納することです。

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

等。


もちろん、この方法で変数を設定できます。一時ファイルは必要ありません。サブシェルの変数に設定し、その値を吸収する親シェルにその値を出力します。サブシェルで値を計算するために必要なすべての状態を取得し、それを実行します。
mikeserv 14

1
@mikeservそれは同じものではないので、OPはそのような解決策はうまくいかないと言っています(これは質問でより明確にすべきでしたが)。あなたが言及しているのは、IPCを介して別のプロセスに値を渡し、その値を何にでも割り当てることができるようにすることです。OPが望む/必要なことは、多くのプロセスで共有されるグローバル変数の値に影響することであり、環境を介してそれを行うことはできません。IPCにはあまり役立ちません。
goldilocks 14

男、私はここで必要なものを完全に誤解しているか、他の誰もが持っている。私には本当に簡単に思えます。私の編集が表示されますか?どうしたの?
mikeserv 14

@mikeserv私は何を持っていることは、あなたが誤解をしたとは思わないし、公平にある IPCの形と可能性が働きます。Konradが気に入らない理由はわかりませんが、柔軟性が十分でない場合、ファイルスタッシュは非常に簡単です(衝突を回避する方法などmktemp)。
goldilocks 14

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"
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.