web-dev-qa-db-ja.com

ワイルドカード展開から最初の一致を取得するにはどうすればよいですか?

BashやZshなどのシェルは、ワイルドカードを引数に展開します。引数と同じ数の引数がパターンに一致します。

$ echo *.txt
1.txt 2.txt 3.txt

しかし、すべての一致ではなく、最初の一致のみが返されるようにするにはどうすればよいですか?

$ echo *.txt
1.txt

シェル固有のソリューションは気にしませんが、ファイル名の空白で機能するソリューションが欲しいのですが。

44
Flimm

Bashの強力な方法の1つは、配列に展開して、最初の要素のみを出力することです。

_pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!
_

(_echo $files_でもかまいません。欠落しているインデックスは[0]として扱われます。)

これは、ファイル名を展開するときに、スペース/タブ/改行およびその他のメタ文字を安全に処理します。実際のロケール設定は、「最初の」とは何かを変更する可能性があることに注意してください。

Bash completion functionを使用してインタラクティブにこれを行うこともできます:

__echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo
_

これは__echo_関数をバインドして、echoコマンドへの引数を完了します(通常の完了を上書きします)。上記のコードには余分な「*」が追加されています。ファイル名の一部をTabキーで押すだけで、適切な結果が得られます。

コードはわずかに複雑であり、設定または仮定するのではなく、nullglob(_shopt -s nullglob_)をチェックします_compgen -G_は、グロブをいくつかの一致に展開してから、安全に展開します配列に入れ、最後にCOMPREPLYを設定して、引用が堅牢になるようにします。

部分的に bashの_compgen -G_でこれを(プログラムでグロブを拡張する)できますが、引用符で囲まれていない標準出力に出力するため、堅牢ではありません。

いつものように、補完はかなりうまくいきません。これは、環境変数を含む他のものの補完を壊します(デフォルトの動作のエミュレーションの詳細については、_bash_def_completion() function here を参照してください)。

補完関数の外でcompgenを使用することもできます。

_files=( $(compgen -W "$pattern") )
_

"〜"はグロブではなく、$ variablesやその他の展開と同様に、展開の別の段階でbashによって処理されることに注意してください。 _compgen -G_はファイル名の展開を行うだけですが、_compgen -W_はbashのデフォルトの展開をすべて提供しますが、展開が多すぎる可能性があります(_``_および$()を含む)。 _-G_とは異なり、_-W_ isは安全に引用されています(格差については説明できません)。 _-W_の目的はトークンを展開することなので、そのようなファイルが存在しなくても "a"を "a"に展開することを意味するため、おそらく理想的ではありません。

これは理解しやすいですが、望ましくない副作用があるかもしれません:

__echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}
_

次に:

_touch $'curious \n filename'_

_echo curious*_tab

値を安全に引用するために_printf %q_を使用していることに注意してください。

最後のオプションの1つは、GNUユーティリティ( bash FAQ を参照)で0区切りの出力を使用することです。

_pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )
_

このオプションを使用すると、並べ替え順序を少し制御できます(グロブを展開するときの順序は、locale/_LC_COLLATE_の影響を受け、大文字と小文字を区別する場合としない場合があります)が、それ以外の場合は、このような小さな問題;-)

31
mr.spuratic

Zshでは、[1]glob qualifier 。この特殊なケースでは最大で1つの一致が返されますが、それでもリストであり、割り当て(配列の割り当ては別)などの単一のWordを期待するコンテキストではグロブは展開されません。

echo *.txt([1])

Kshまたはbashでは、一致のリスト全体を配列に入れ、最初の要素を使用できます。

tmp=(*.txt)
echo "${tmp[0]}"

どのシェルでも、位置パラメータを設定して最初のパラメータを使用できます。

set -- *.txt
echo "$1"

これは、位置パラメータを破棄します。それが必要ない場合は、サブシェルを使用できます。

echo "$(set -- *.txt; echo "$1")"

独自の位置パラメータのセットを持つ関数を使用することもできます。

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"

試してください:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

ファイル名の展開は、現在のロケールで有効な照合順序に従ってソートされることに注意してください。

6
cuonglm

簡単な解決策:

sh -c 'echo "$1"' sh *.txt

または、必要に応じてprintfを使用します。

同じことを思いながら、私はこの古い質問に出くわしました。私はこれで終わりました:

echo $(ls *.txt | head -n1)

もちろん、headtailに、_-n1_を他の数値に置き換えることができます。


名前に改行が含まれているファイルを操作している場合、上記は機能しません。改行を処理するには、次のいずれかを使用できます。

  • _ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g'_ (BSDでは機能しません)
  • _ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'_
  • _ls -b *.txt | head -n1 | Perl -pe 's/\\n/\n/g'_
  • echo -e "$(ls -b *.txt | head -n1)"(任意の特殊文字で機能)
1
user149485