コマンド置換:スペースではなく改行で分割


30

私はこの問題をいくつかの方法で解決できることを知っていますが、bashビルトインのみを使用してそれを実行する方法があるかどうか、そしてそうでない場合、最も効率的な方法は何ですか?

次のような内容のファイルがあります

AAA
B C DDD
FOO BAR

つまり、複数の行があり、各行にスペースがある場合とない場合があります。次のようなコマンドを実行したい

cmd AAA "B C DDD" "FOO BAR"

使用するcmd $(< file)

cmd AAA B C DDD FOO BAR

そして、私が使うなら、私はcmd "$(< file)"得る

cmd "AAA B C DDD FOO BAR"

各行を1つのパラメーターだけで処理するにはどうすればよいですか?


回答:


26

移植性:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

または、サブシェルを使用してIFS、オプションの変更をローカルにします。

( set -f; IFS='
'; exec cmd $(cat <file) )

シェルは、二重引用符で囲まれていない変数またはコマンド置換の結果に対して、フィールド分割とファイル名生成を実行します。したがってset -f、でファイル名の生成をオフにし、IFS改行のみでフィールドを区切るようにフィールド分割を構成する必要があります。

bashまたはkshコンストラクトで得られるものはあまりありません。IFS関数に対してローカルにすることはできますが、できませんset -f

bashまたはksh93では、複数のコマンドにフィールドを渡す必要がある場合、フィールドを配列に格納できます。アレイを構築するときに拡張を制御する必要があります。次に"${a[@]}"、ワードごとに1つの配列の要素に展開します。

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

これは、一時配列を使用して実行できます。

セットアップ:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

配列を埋めます:

$ IFS=$'\n'; set -f; foo=($(<input))

配列を使用します。

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

その一時変数なしでそれを行う方法を理解することはできません- IFS変更がにとって重要でない場合を除き、cmdその場合:

$ IFS=$'\n'; set -f; cmd $(<input) 

それを行う必要があります。


IFSいつも混乱します。 IFS=$'\n' cmd $(<input)動作しません。 IFS=$'\n'; cmd $(<input); unset IFS動作します。どうして?私が使用しますね(IFS=$'\n'; cmd $(<input))
オールド・プロ

6
@OldProはの環境でIFS=$'\n' cmd $(<input)のみ設定さIFSれるため機能しませんcmd$(<input)への割り当てIFSが実行される前に、コマンドを形成するために展開されます。
ジル 'SO-悪であるのをやめる

8

これを行う標準的な方法bashは次のようなものです

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

または、bashのバージョンが次の場合mapfile

mapfile -t args < filename
cmd "${args[@]}"

mapfileとwhile-readループとone-linerの間で見つけられる唯一の違い

(set -f; IFS=$'\n'; cmd $(<file))

前者は空行を空の引数に変換しますが、1行は空行を無視します。この場合、ワンライナーの動作はとにかく私が好むものなので、それがコンパクトであることの二重のボーナスです。

使用しますIFS=$'\n' cmd $(<file)が、機能しません。$(<file)コマンドラインを形成する前に解釈されるため、IFS=$'\n'有効になります。

それは私の場合は仕事をしませんが、私は今、ツールの多くが持つ終端ラインをサポートすることを学んだnull (\000)の代わりにnewline (\n)、これらの状況の一般的な供給源であるこれを簡単に対処する、たとえば、ファイル名、の多くを作るんいるが:

find / -name '*.config' -print0 | xargs -0 md5

完全修飾ファイル名のリストを、グロビングや補間などを行わずにmd5の引数としてフィードします。それは非組み込みソリューションにつながります

tr "\n" "\000" <file | xargs -0 cmd

ただし、これも空白行のみを無視しますが、空白のみの行をキャプチャします。


使い方cmd $(<file)(スプリットワードへのbashの機能を使用して)引用することなく、値は常に危険な賭けです。行がある場合*、シェルによってファイルのリストに展開されます。

3

bashビルトインを使用してmapfile、ファイルを配列に読み込むことができます

mapfile -t foo < filename
cmd "${foo[@]}"

または、テストされていないxargs可能性があります

xargs cmd < filename

mapfileのドキュメントから:「mapfileは一般的なまたはポータブルなシェル機能ではありません」。実際、私のシステムではサポートされていません。 xargs助けにもなりません
オールドプロ

あなたは、必要があるだろうxargs -dxargs -L
ジェームス・ヤングマン

@James、いいえ、-dオプションがなくxargs -L 1、コマンドを1行に1回実行しますが、引数を空白で分割します。
オールドプロ

1
@OldPro、「一般的なまたはポータブルなシェル機能」ではなく、「bashビルトインのみを使用して実行する方法」を求めました。bashのバージョンが古すぎる場合、更新できますか?
グレンジャックマン

mapfile配列項目として空白行を取得するため、このIFS方法では実行されないため、私にとって非常に便利です。IFS連続する改行を単一の区切り文字として扱います...コマンドを認識していなかったため、表示してくれてありがとう(ただし、OPの入力データと予想されるコマンドラインに基づいて、彼は実際に空白行を無視したいようです)。
-Peter.O

0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

私が見つけることができる最善の方法。ただ動作します。


そして、IFSスペースに設定するとなぜ機能するのでしょうか?しかし、問題はスペースで分割しないことです?
RalfFriedl

0

ファイル

改行でファイルを分割するための最も基本的なループ(ポータブル)は次のとおりです。

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

どちらが印刷されます:

$ ./script
<AAA><A B C><DE F>

はい、デフォルトのIFS =でspacetabnewline

なぜ機能するのか

  • IFSは、シェルが入力をいくつかの変数に分割するために使用されます。変数は1つしかないため、シェルによる分割は実行されません。したがって、変更はIFS必要ありません。
  • はい、先頭と末尾のスペース/タブは削除されますが、この場合は問題ないようです。
  • いいえ、展開が引用符で囲まれていないため、グロビングは行われません。だから、必要ありません。set -f
  • 使用される(または必要な)唯一の配列は、配列のような位置パラメータです。
  • -r(生)オプションは、ほとんどのバックスラッシュを除去することを避けるためです。

分割やグロビングが必要な場合、それは機能しません。そのような場合、より複雑な構造が必要です。

必要な場合(まだポータブル):

  • 先頭および末尾のスペース/タブの削除を避け、以下を使用します: IFS= read -r line
  • ある文字で行を変数に分割します:を使用しますIFS=':' read -r a b c

ファイルを他の文字(ポータブルではなく、ksh、bash、zshで動作)で分割します

IFS=':' read -d '+' -r a b c

拡張

もちろん、質問のタイトルは、スペースでの分割を避けて、改行でコマンド実行を分割することに関するものです。

シェルから分割する唯一の方法は、引用符なしで展開を残すことです:

echo $(< file)

これはIFSの値によって制御され、引用符で囲まれていない展開では、グロビングも適用されます。動作するようにするには、次のものが必要です。

  • 新しい行に設定IFS のみ、のみ改行で分割を取得します。
  • グロビングシェルオプションの設定を解除しますset +f

    set + f IFS = '' cmd $(<file)

もちろん、それによってIFSの価値が変わり、スクリプトの残りの部分のグロビングの価値が変わります。

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