bash:findのselectへのホワイトスペースセーフな手続き的使用


12

これらのファイル名を考えると:

$ ls -1
file
file name
otherfile

bash 埋め込まれた空白でそれ自体は完全にうまくいきます:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

しかし、時々私も厳密内のすべてのファイル、またはと仕事したくない場合があります$PWDところで、find入ってくるも、ハンドル空白名目上。:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

私はのwhispaceセーフバージョンでっち上げるしようとしているこのの出力を取るスクリプトレットfindとにそれを提示しますselect

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

ただし、これはファイル名に空白が含まれていると爆発します。

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

通常、私はをいじってこれを回避しIFSます。しかしながら:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

これに対する解決策は何ですか?



1
特定のファイル名に一致させる機能のみを使用findしている場合は、4 select file in **/file*以降で(を設定した後shopt -s globstar)を使用できますbash
-chepner

回答:


14

スペースとタブのみを処理する必要がある場合(埋め込まれた改行は不要)、mapfile(またはその同義語readarray)を使用して配列に読み込むことができます。

$ ls -1
file
other file
somefile

それから

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

あなたは場合に行うハンドル改行する必要があり、あなたのbashバージョンはヌル区切り提供mapfile1、そしてあなたがそれを変更することができますIFS= mapfile -t -d '' files < <(find . -type f -print0)。それ以外の場合はfindreadループを使用して、ヌルで区切られた出力から同等の配列を組み立てます。

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1-dオプションがに追加されたmapfile中でbash、バージョン4.4 IIRC


2
私が前に使用したことがない別の動詞の+1
roaima

確かに、mapfile私にとっても新しいものです。称賛。
-DopeGhoti

while IFS= readバージョンは(MacOSのを使用してこれらの私たちのために重要である)、bashのV3に戻って動作します。
ゴードン

3
find -print0バリアントの+1 。不平を言うそれを置くためにした後、既知の誤ったバージョン、および1つの場合にのみ使用するためにそれを記述する知っている彼らは改行を処理する必要があること。予想される場所でのみ予期しないことを処理する場合、予期しないことはまったく処理されません。
チャールズダフィー

8

この回答には、あらゆる種類のファイルに対する解決策があります。改行またはスペースを使用します。
古代のbash、さらには古いposixシェルだけでなく、最近のbashのソリューションもあります。

この回答[1]の下にリストされているツリーは、テストに使用されます。

選択する

select配列を使用して作業するのは簡単です:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

または、定位置パラメーターを使用する場合:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

したがって、唯一の実際の問題は、配列内または位置パラメータ内で「ファイルのリスト」(正しく区切られている)を取得することです。読み続けます。

バッシュ

bashで報告する問題は見当たりません。Bashは、指定されたディレクトリ内を検索できます。

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

または、ループが好きな場合:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

上記の構文は、(少なくともcshではなく)すべての(合理的な)シェルで正しく機能することに注意してください。

上記の構文が持つ唯一の制限は、他のディレクトリに降りることです。
しかし、bashはそれを行うことができます。

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

一部のファイル(ファイルで終わるファイルなど)のみを選択するには、*を置き換えます。

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

壮健

タイトルに「スペースセーフ」を配置するとき、私はあなたが意図したものが「堅牢」であると仮定します。

スペース(または改行)について堅牢にする最も簡単な方法は、スペース(または改行)を持つ入力の処理を拒否することです。シェルでこれを行う非常に簡単な方法は、ファイル名がスペースで拡張された場合にエラーで終了することです。これを行うにはいくつかの方法がありますが、最もコンパクトな(およびposix)(ただし、suddirectories名とドットファイルの回避を含む1つのディレクトリコンテンツに制限されます):

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

使用したソリューションがこれらの項目のいずれかで堅牢な場合、テストを削除します。

bashでは、上記で説明した**を使用して、サブディレクトリを一度にテストできます。

ドットファイルを含める方法はいくつかありますが、Posixソリューションは次のとおりです。

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

見つける

何らかの理由でfindを使用する必要がある場合は、区切り文字をNUL(0x00)に置き換えます。

bash 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

findにNUL区切り文字がなく、読み取り用の-d(nor -a)がない有効なPOSIXソリューションを作成するには、まったく異なるアプローチが必要です。

-execシェルの呼び出しでfindからの複合体を使用する必要があります。

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

または、選択が必要な場合(selectはshではなくbashの一部です):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1]このツリー(\ 012は改行です):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

次の2つのコマンドで構築できます。

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

ループ構造の前に変数を設定することはできませんが、条件の前に変数を設定することはできます。マニュアルページのセグメントは次のとおりです。

上記の「パラメーター」で説明したように、単純なコマンドまたは機能の環境は、パラメーターの割り当てをプレフィックスとして付けることで一時的に拡張できます。

(ループは単純なコマンドではありません。)

失敗と成功のシナリオを示す一般的に使用される構成は次のとおりです。

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

残念ながら、それが関連しているの処理に影響を与えている間、変化IFSselect構造に埋め込む方法を見ることができません$(...)。ただし、IFSループの外側に設定されるのを防ぐものは何もありません。

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

そして、私が見ることができるのはこの構造ですselect

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

守備のコードを書くとき、私は句はいずれかのサブシェルで実行するか、またはすることをお勧めしたいIFSSHELLOPTS保存され、ブロックの周りを復元します:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
それIFS=$'\n'が安全であると仮定することは根拠がありません。ファイル名には改行リテラルを完全に含めることができます。
チャールズダフィー

4
私は、たとえ存在する場合であっても、額面価格で可能なデータセットに関するそのような主張を受け入れることを率直にためらっています。私が経験した最悪のデータ損失イベントは、古いバックアップのクリーンアップを担当するメンテナンススクリプトが、ランダムガベージをダンプする不正なポインター逆参照を持つCモジュールを使用してPythonスクリプトによって作成されたファイルを削除しようとした場合でした-空白で区切られたワイルドカードを含む-名前に。
チャールズダフィー

2
これらのファイルのクリーンアップを行うシェルスクリプトを作成している人は、名前が「一致しない」可能性があるため、引用することに煩わされませんでした[0-9a-f]{24}。顧客の請求をサポートするために使用されたTBのデータのバックアップが失われました。
チャールズダフィー

4
@CharlesDuffyに完全に同意します。エッジケースを処理しないのは、インタラクティブに作業していて、自分が何をしているのかを確認できるときだけです。 selectその設計はスクリプト化されたソリューション向けであるため、常にエッジケースを処理するように設計する必要があります。
ワイルドカード

2
@ilkkachu、もちろん- select実行するコマンドを入力しているシェルから呼び出すことはありませんが、そのスクリプトによって提供さたプロンプトに応答するスクリプトでのみ、そのスクリプトはその入力に基づいて、事前に定義されたロジック(操作対象のファイル名の知識なしで構築)を実行します。
チャールズダフィー

4

私はここで私の管轄から外れているかもしれませんが、おそらくあなたはこのようなものから始めることができます、少なくともそれは空白に問題はありません:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

コメントに記載されているように、潜在的な誤った仮定を避けるために、上記のコードは次と同等であることに注意してください。

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -d賢い解決策です。これに感謝します。
-DopeGhoti

2
read -d $'\000'正確に同じでread -d ''はなく、(それは、文字列リテラル内NULsを表現することが可能だということを、誤っ、暗示)はbashの機能についての人々の誤解を招くため。を実行しs1=$'foo\000bar'; s2='foo'、2つの値を区別する方法を見つけてください。(将来のバージョンでは、格納された値をと同等にすることで、コマンド置換動作で正規化される可能性がfoobarありますが、今日はそうではありません)。
チャールズダフィー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.