bashループリストで空白をエスケープするにはどうすればよいですか?


121

特定のディレクトリのすべての子ディレクトリ(ファイルではない)をループするbashシェルスクリプトがあります。問題は、一部のディレクトリ名にスペースが含まれていることです。

これが私のテストディレクトリの内容です。

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

そして、ディレクトリをループするコード:

for f in `find test/* -type d`; do
  echo $f
done

出力は次のとおりです。

テスト/ボルチモア
テスト/チェリー
丘
テスト/エジソン 
テスト/新規
ヨーク
市
テスト/フィラデルフィア

Cherry HillとNew York Cityは、2つまたは3つの別々のエントリとして扱われます。

私は次のようにファイル名を引用しようとしました:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

しかし、役に立たない。

これを行う簡単な方法がなければなりません。


以下の答えは素晴らしいです。しかし、これをより複雑にするために、テストディレクトリにリストされているディレクトリを常に使用する必要はありません。代わりに、コマンドラインパラメータとしてディレクトリ名を渡したい場合があります。

私はIFSを設定するというチャールズの提案を取り入れ、次のことを思いつきました。

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

コマンドライン引数にスペースが含まれていない限り(これらの引数が引用符で囲まれている場合でも)、これは問題なく機能します。たとえばtest.sh "Cherry Hill" "New York City"、次のようなスクリプトを呼び出すと、次の出力が生成されます。

チェリー
丘
新着
ヨーク
市

re:編集、list="$@"元の値のリストネスを完全に破棄し、文字列に折りたたみます。与えられたとおりに私の回答の実践に従ってください-そのような割り当てはその中のどこにも奨励されていません。コマンドライン引数のリストをプログラムに渡したい場合は、それらを配列に収集し、その配列を直接展開する必要があります。
Charles Duffy

回答:


105

まず、そのようにしないでください。最善の方法は、find -exec適切に使用することです。

# this is safe
find test -type d -exec echo '{}' +

もう1つの安全なアプローチは、NULで終了するリストを使用することですが、これには検索サポートが必要です-print0

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

また、findから配列を生成し、後でその配列を渡すこともできます。

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

検索-print0結果がをサポートしていない場合、結果は安全ではありません。名前に改行が含まれているファイルが存在する場合、以下は期待どおりに動作しません(これは正当です)。

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

(それは単語分割を行う前に、サブプロセスの全体の出力を読み取るように、時間とメモリ使用量の両方の点で効率が低い)一つは、上記のいずれかを使用する第三のアプローチを行っていない場合に使用することであるIFSどのdoesnの変数をスペース文字を含まない。(globをオフにするset -fようなグロブ文字含む文字列を防止するために)[]*または?拡張されてからの:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

最後に、コマンドラインパラメーターの場合、シェルが配列をサポートしている場合は配列を使用する必要があります(つまり、ksh、bash、またはzsh)。

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

分離を維持します。引用(およびで$@はなくの使用$*)が重要であることに注意してください。配列は、グロブ式などの他の方法でも入力できます。

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1
-execの「+」フレーバーについては知らなかった。甘い
ヨハネスシャウブ-litb '19 / 11/08

1
xargsのように、それもできるように見えますが、指定されたコマンドの最後にのみ引数を置くことができます:/それは時々私を
悩ませました

-exec [name] {} +はGNUおよび4.4-BSDの拡張機能だと思います。(少なくとも、それはSolaris 8では表示されません。また、AIX 4.3でもそうだったとは思いません。)残りの人はxargsへのパイピングで立ち往生しているかもしれません...
Michael Ratanapintha

2
$ '\ n'の構文を見たことがない。それはどのように機能しますか?(IFS = '\ n'またはIFS = "\ n"のどちらでも機能すると考えていましたが、どちらも機能しません。)
MCS

1
@crosstalkそれは間違いなくSolaris 10にあります、私はそれを使用しました。
Nick

26
find . -type d | while read file; do echo $file; done

ただし、ファイル名に改行が含まれている場合は機能しません。上記は、変数にディレクトリ名を実際に入れたいときに知っている唯一の解決策です。何らかのコマンドを実行するだけの場合は、xargsを使用します。

find . -type d -print0 | xargs -0 echo 'The directory is: '

xargsは必要ありません。find-execを参照してください... {} +
Charles Duffy

4
@Charles:多数のファイルの場合、xargsの方がはるかに効率的です。1つのプロセスしか生成しません。-execオプションは、ファイルごとに新しいプロセスをforkします。これにより、桁違いに遅くなる場合があります。
Adam Rosenfield、

1
私はxargsがもっと好きです。これら2つは基本的に同じように動作するようですが、xargsには並列実行などのオプションがあります
Johannes Schaub-litb

2
Adamさん、 '+'はできるだけ多くのファイル名を集約して実行します。しかし、並列実行するようなきちんとした機能はありません:)
Johannes Schaub-litb '19 / 11/08

2
ファイル名で何かをしたい場合は、それらを引用符で囲まなければならないことに注意してください。例:find . -type d | while read file; do ls "$file"; done
デビッドモレス

23

ファイル名のタブや空白を処理する簡単なソリューションを次に示します。改行のようなファイル名の他の奇妙な文字を処理する必要がある場合は、別の答えを選んでください。

テストディレクトリ

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

ディレクトリに移動するコード

find test -type d | while read f ; do
  echo "$f"
done

"$f"引数として使用する場合は、ファイル名を引用符()で囲む必要があります。引用符がない場合、スペースは引数の区切り文字として機能し、呼び出されたコマンドに複数の引数が与えられます。

そして出力:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

おかげで、これは、現在のフォルダの各ディレクトリが使用しているスペースの量をリストするために作成していたエイリアスで機能しました。以前のインカネーションでスペースのあるディレクトリで窒息していました。これはzshで機能しますが、他のいくつかの回答は機能しませんでした:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid

2
:あなたはzshを使用している場合は、あなたもこれを行うことができますalias duc='du -sh *(/)'
cbliard

@cbliardこれはまだバグがあります。たとえば、タブシーケンスまたは複数のスペースを含むファイル名で実行してみてください。エコーで引用しているわけではないので、これらは単一のスペースに変更されることに注意してください。そして、改行を含むファイル名のケースがあります...
Charles Duffy

@CharlesDuffyタブシーケンスと複数のスペースを試してみました。それは引用符で動作します。改行も試してみましたが、まったく機能しません。私はそれに応じて答えを更新しました。これを指摘していただきありがとうございます。
cbliard 2013

1
@cbliard正解-エコーコマンドに引用符を追加するのが目的でした。改行については、find -print0とを使用して機能させることができますIFS='' read -r -d '' f
Charles Duffy

7

これは、標準のUnixでは非常にトリッキーであり、ほとんどのソリューションは改行やその他の文字を使用します。ただし、GNUツールセットを使用しているfind場合は、オプション-print0を利用xargsして、対応するオプション-0(マイナスゼロ)で使用できます。単純なファイル名では使用できない2つの文字があります。それらはスラッシュとNUL '\ 0'です。明らかに、パス名にスラッシュが含まれるため、NUL '\ 0'を使用して名前の終わりをマークするGNUソリューションは、独創的で間違いのないものです。


4

どうして

IFS='\n'

forコマンドの前?これにより、フィールドセパレータが<Space> <Tab> <Newline>から<Newline>に変更されます。


4
find . -print0|while read -d $'\0' file; do echo "$file"; done

1
-d $'\0'-d ''bashはNULで終了する文字列を使用するため、正確に同じです。空の文字列の最初の文字はNULであり、同じ理由で、NULはC文字列の内部ではまったく表現できません。
Charles Duffy

4

私が使う

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

それで十分ではないでしょうか?http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html
からのアイデア


素晴らしいヒント:コマンドラインのosascript(OS X AppleScript)のオプションでは、スペースが引数を複数のパラメーターに分割し、1つだけを意図している場合に非常に役立ちます。
ティム・

いいえ、それは十分ではありません。これは非効率的です(不必要にを使用しているため$(echo ...))。グロブ式でファイル名を正しく処理せず、$'\b'または$ '\ n'文字を含むファイル名を正しく処理しません。さらに、複数の空白スペースを単一の空白文字に変換します誤った引用による出力側。
Charles Duffy

4

リストを文字列として保存しないでください。これらのすべての区切り文字の混乱を避けるために、それらを配列として保管してください。testのすべてのサブディレクトリ、またはコマンドラインで指定されたリストを操作するスクリプトの例を次に示します。

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

ここで、1つまたは2つのカーブが投入されたテストディレクトリでこれを試してみましょう。

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1
これを振り返ると、実際に POSIX shで解決策があり"$@"ましたset -- "$@" "$f"。配列を再利用して、で追加できます。
Charles Duffy

4

あなたは一時的にIFS(内部フィールドセパレータ)を使用することができます:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS


説明してください。
スティーブK

IFSは区切り記号を指定したため、空白を含むファイル名は切り捨てられませんでした。
驚くべき

最後の$ IFS = $ OLD_IFSは次のようになります:IFS = $ OLD_IFS
Michel Donais

3

PSそれが入力のスペースについてのみである場合、いくつかの二重引用符は私にとってスムーズに機能しました...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;

2

ジョナサンの発言に追加するには、次の-print0オプションとfind組み合わせて使用しますxargs

find test/* -type d -print0 | xargs -0 command

これcommandにより、適切な引数でコマンドが実行されます。スペースを含むディレクトリは適切に引用されます(つまり、1つの引数として渡されます)。


1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

上記のコードは、.movファイルを.aviに変換します。.movファイルは別のフォルダーにあり、フォルダー名にも空白があります。上記のスクリプトは、.movファイルを同じフォルダー自体の.aviファイルに変換します。それが人々の役に立つかどうかはわかりません。

場合:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

乾杯!


echo "$name" | ...nameisの場合は機能せず-n、バックスラッシュエスケープシーケンスを含む名前での動作は実装によって異なります-POSIXはechoその場合の動作を明示的に未定義にします(XSI拡張POSIXはバックスラッシュエスケープシーケンスの展開を標準定義の動作にします) 、およびbashを含むGNUシステムはPOSIXLY_CORRECT=1、実装することでPOSIX標準を破ることなく-e(仕様では出力echo -eに印刷する必要があるため-e)、printf '%s\n' "$name" | ...より安全です
Charles Duffy

1

パス名の空白も処理する必要がありました。私が最後にやったことは、再帰を使用して、for item in /path/*

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}

1
functionキーワードを使用しないでください。これにより、コードがPOSIX shと互換性がなくなりますが、その他の有用な目的はありません。で関数を定義しrecursedir() {、2つの括弧を追加してfunctionキーワードを削除するだけでよく、これはすべてのPOSIX準拠のシェルと互換性があります。
Charles Duffy

1

ファイルリストをBash配列に変換します。これは、Bash関数から配列を返すためのMatt McClureのアプローチを使用しています。http//notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html 結果は方法です複数行の入力をBash配列に変換します。

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

このアプローチは、不正な文字が存在する場合でも機能するように見え、入力をBash配列に変換する一般的な方法です。欠点は、入力が長い場合、Bashのコマンドラインサイズの制限を超えたり、大量のメモリを使い果たしたりする可能性があることです。

リストで最終的に機能するループがパイプでリストを処理するアプローチには、stdinの読み取りが簡単ではない(ユーザーに入力を求めるなど)欠点があり、ループは新しいプロセスなので、なぜ変数なのか疑問に思われるかもしれません。ループ内で設定したものは、ループが終了すると使用できなくなります。

IFSの設定も嫌いです。他のコードを台無しにする可能性があります。


IFS='' read同じ行でを使用する場合、IFS設定は読み取りコマンドに対してのみ存在し、それをエスケープしません。このようにIFSを設定することを嫌う理由はありません。
Charles Duffy

1

さて、私はあまりにも多くの複雑な答えを見ています。findには「exec」オプションがあるため、findユーティリティの出力を渡したり、ループを作成したりしたくありません。

私の問題は、dbf拡張子が付いたすべてのファイルを現在のフォルダーに移動したいことであり、それらの一部には空白が含まれていました。

私はそれに取り組みました:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

私にはとてもシンプルに見える


0

私の質問とあなたの質問にはいくつかの類似点があることがわかりました。引数をコマンドに渡したい場合は別途

test.sh "Cherry Hill" "New York City"

順番に印刷する

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

$ @が二重引用符で囲まれていることに注意してください。


0

特定のフォルダから複数のディレクトリまたはファイルを順番に圧縮するには、同じ概念が必要でした。私はawkを使用してlsからリストを解析し、名前に空白が含まれる問題を回避することで解決しました。

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

どう思いますか?


ファイル名に改行がある場合、これは正しく機能しないと思います。多分あなたはそれを試すべきです。
user000001 2013


-3

私にとってこれは機能し、ほとんど「クリーン」です。

for f in "$(find ./test -type d)" ; do
  echo "$f"
done

4
しかし、これはさらに悪いことです。検索を二重引用符で囲むと、すべてのパス名が単一の文字列として連結されます。エコーlsに変更して、問題を確認します。
NVRAM

-4

単純なバリアントの問題が発生しました...タイプされた.flvのファイルを.mp3(あくび)に変換します。

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

すべてのMacintoshユーザーフラッシュファイルを再帰的に検索し、それらをオーディオに変換します(コピー、トランスコードなし)...これは上記のようなもので、単に「for file in 」の代わりに読み取るとエスケープします。


2
read後は、inあなたが反復処理しているリストの1つの以上の単語です。あなたが投稿したのは、質問者が持っていたものの少し壊れたバージョンであり、動作しません。あなたは何か違うものを投稿するつもりだったかもしれませんが、とにかくここではおそらく他の回答でカバーされています。
Gilles 'SO-悪をやめなさい'
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.