変数値にスペースが含まれている場合、区切り文書の変数をパラメータに拡張するにはどうすればよいですか?

変数値にスペースが含まれている場合、区切り文書の変数をパラメータに拡張するにはどうすればよいですか?

lftp経由でアップロードするためのスクリプトを作成しました。

#! /usr/bin/bash
set -xe

if [ -n "$1" ]; then
  # ...
else
  source="."
  # target=name of current local folder
  target="${PWD##*/}"/
  cmd="mirror --reverse --continue --parallel=5 "$source" "$target""
fi


lftp -u $user,$pass $host << EOF
set log:enabled true

eval "$cmd"

quit
EOF

現在のディレクトリにスペースがない場合は正常に動作します。この問題は、現在のディレクトリにスペースがある場合に発生します。デバッグ情報は次のように表示されます。

➜  upload.sh
+ '[' -n '' ']'
+ source=.
+ target='Two Words/'
+ cmd='mirror --reverse --continue --parallel=5 . Two Words/'

Twoこの問題により、スクリプトは代わりに名前付きディレクトリにアップロードされますTwo Words

特に、どの行が私が間違っているのかわからないので、この問題をどのように解決するのかわかりませんcmd=...eval "$cmd"?どれでもない?両方とも?

とにかくデバッグ出力は次のようになると予想しましたが(二重引用符を+ cmd='mirror --reverse --continue --parallel=5 "." "Two Words/"'参照)、なぜそうでないのかわかりません。

この問題は、これまで私が調査した他の多くの問題に似ています。 Googleでこれが異なる点/難しいのは、拡張が「ここの文字列」内で発生することです。私が知っている限り、それは重要ではありませんが、私が知っている限り、それは非常にユニークです。


コメントでは、すべてを配列に保存するよう提案しました。私の試みは次のとおりです。

#! /usr/bin/bash
set -xe

if [ -n "$1" ]; then
  cmd=(mput -c -P 5 "$1")
  if [ -n "$2" ]; then
    cmd=(mkdir -p "$2"
        mput -c -P 5 -O "$2" "$1") #*
  fi
else
  source="."
  # target=name of current local folder
  target="${PWD##*/}"/
  cmd=(mirror --reverse --continue --parallel=5 "$source" "$target")
fi


lftp -u $user,$pass $host << EOF

"${cmd[@]}"

quit
EOF

(*サブシェルに複数行を含める方法がわからないので、実際の改行を入れましたが、それが正しいのかどうかわかりません。Googleで検索した結果はすべてコマンドの置き換えに関連しています。サブシェルではありません。)

これを実行すると、次のエラーが発生します。Unknown command 'mirror --reverse --continue --parallel=5 . Two Words/'.これをコピーしてシェルに貼り付けると、他のエラーが発生するため、何が起こっているのか理解できません。

電話でもlftp -u $user,$pass $host -e "${cmd[@]}"接続できません。このエラーが発生します。

open: unrecognized option '--reverse'
Usage: lftp [-e cmd] [-p port] [-u user[,pass]] <host|url>

lftp ... -e "$("${cmd[@]}")"このエラーは、次の実行時に発生します。

open: option requires an argument -- 'e'
Usage: lftp [-e cmd] [-p port] [-u user[,pass]] <host|url>

ご覧のとおり、現在私はランダムに試しています。これは良い戦略ではありません。

答え1

sコマンドの言語構文からこれらのパラメーターを引用する必要があります(または少なくともlftps言語の対応するスペースまたはその他の特殊文字をエスケープする必要があります)。lftpmirrorlftp

lftp簡単な引用フォームをサポートします。ほとんどのシェルと同様に、単一引用符、二重引用符、およびバックスラッシュを使用できますが、構文は異なります。lftpのコマンドを使って実験してみることができますecho

lftp :~> echo \'
'
lftp :~> echo \a
\a
lftp :~> echo \
> a
a

\エスケープ演算子ですが、特定の文字に対してのみ機能します。改行文字が後に続く場合は行連続文字で、改行文字をエスケープしません。

lftp :~> echo "a\'"
a\'
lftp :~> echo "a\"b"
a"b
lftp :~> echo "a\$b"
a\$b

二重引用符内でエスケープする文字のリストはさらに減ります。"そこにゴミの欠片があるかもしれません"..."\"

lftp :~> echo 'a\'b'
a'b
lftp :~> echo 'a\\b'
a\b

一重引用符内でも同様です。'...'そこにある引用符は、shなどのシェルの強い引用符とは異なります。

lftp :~> echo $'a\nb'
$a\nb

$'...'ksh93スタイルの引用フォームはサポートされていません。

lftp :~> echo 'foo
foo
lftp :~> echo 'a\
b'
ab

引用符はペアで表示する必要はありません。引用符内にあるかどうかにかかわらず、コマンドパラメータに改行文字を含めることはできません。

'...'コードを見てエスケープする必要がある文字は、"..."バックスラッシュとその引用符文字だけです。 lftpバイトレベルで作業すると、バイトシーケンスは文字としてデコードされません。

したがって、構文内の文字列を適切に引用する最善の方法lftpは、すべて'および\その中(実際には0x5cバイトまたは他のマルチバイト文字エンコーディングのバイト)\とPackaged inをエスケープすることです'...'。または"..."

ここで使用するevalことは不要で、2つのレベルの参照を管理する必要があるため、より複雑になります。

#! /usr/bin/bash -
set -xe

lftp_quote() {
  local LC_ALL=C
  local -n var
  for var do
    if [[ $var = *$'\n'* ]]; then
      printf >&2 '%s\n' "\"$var\" contains newline characters. That's not supported by lftp"
      exit 1
    fi
    var=${var//'\'/'\\'}
    var=${var//"'"/"\'"}
    var="'$var'"
  done
}
  
if (( $# > 0 )); then
  ...
else
  source="."
  # target=name of current local folder
  target="${PWD##*/}"/
  lftp_quote source target
  cmd="mirror --reverse --continue --parallel=5 -- $source $target"
fi

lftp_quote user pass host
lftp << EOF
set log:enabled true
open --user $user --password $pass -- $host && $cmd
EOF

(パスワードは公開なので、ここからコマンドラインにパスワードを渡さないでください。)

これらの文字列をそのままコマンドに渡すことができますが、FTPプロトコルのコマンドでこれらのファイル名を適切にエスケープするかどうかは別の問題lftpですmirrorlftpFTPはこの点で信頼できないものとして悪名高いもので、サーバーごとに異なる動作をします。lftpFTPに加えて、他の多くのファイル転送プロトコルもサポートされており、それらのいくつかはより安定しています。

答え2

特に、どの行が私が間違っているのかわからないので、この問題をどのように解決するのかわかりませんcmd=...eval "$cmd"?どれでもない?両方とも?

とにかくデバッグ出力は次のようになると予想しましたが(二重引用符を+ cmd='mirror --reverse --continue --parallel=5 "." "Two Words/"'参照)、なぜそうでないのかわかりません。

あなたが持っているものは次のとおりです。

cmd="mirror --reverse --continue --parallel=5 "$source" "$target""

最初の"(at cmd=")は引用符付き文字列を開始し、2番目の(at "$source)はそれを終了し、次の文字列にも同じように適用されます。結果文字列には引用符はありません。使用する必要があります

cmd="mirror ... \"$source\" \"$target\""

文字列に二重引用符を追加するか、lftpに一重引用符を使用できる場合は、次のようにします。

cmd="mirror ... '$source' '$target'"

cmd=(mirror --reverse --continue --parallel=5 "$source" "$target")
lftp -u $user,$pass $host << EOF

"${cmd[@]}"

このコマンドを実行すると、Unknown command 'mirror --reverse --continue --parallel=5 エラーが発生します。 2つの単語/ '。

素晴らしい。拡張機能は、"${array[@]}"配列要素を複数の異なるシェルワード(文字列)に拡張し、ファイル名にスペースが残っているため、シェルで役立ちます。しかし、ここではhere-docにあるので、複数の単語/文字列を生成することは不可能です。完全なhere-docはただ1つの文字列です。したがって、配列要素をスペースで連結し、その値に拡張します。区切り文書と同様に、引用符が保存されます。

lftpで取得したコマンドは次のとおりです。

"mirror --reverse --continue ..."

引用符は単一コマンドとして扱われます。

lftp :~> "echo foo"
Unknown command `echo foo'.
lftp :~> echo foo
foo

質問タイトルでは、「区切られた文書の変数をパラメータとして[拡張]」について尋ねますが、そうすることは不可能なので質問ではありません。とにかくここに文書があります)。いいえ、問題はlftpが正しく引用することです。

これをコピーしてシェルに貼り付けると、他のエラーが発生するため、何が起こっているのか理解できません。

おそらくこのようなものはありませんかbash: mirror: command not found? lftpのコマンドがあるので、mirrorそれをシェルのコマンドラインに貼り付けるのが役に立つかどうかはわかりません。

シェルコマンドを保存するために配列を使用するように提案を理解します。いくつかありますとても良い理由。しかし、ここにはシェルコマンドがないので不要です。

電話でもlftp -u $user,$pass $host -e "${cmd[@]}"接続できません。このエラーが発生します。

open: unrecognized option '--reverse'
Usage: lftp [-e cmd] [-p port] [-u user[,pass]] <host|url>

ここでは、コマンドラインの配列の内容を lftp に渡します。[@]配列要素は以下を使用したので、別の引数に渡されます。

lftp -u USER,PASSWORD HOSTNAME -e mirror --reverse --continue --parallel=5 SOURCE TARGET

(もちろん、、$userも拡張されました。$host$source$target

この-eオプションはコマンドを引数として使用します。この場合、コマンドはですmirror。次のオプションは--reverse他のコマンドラインオプションとして扱われ、lftpでは認識されません。ここであなたが"${cmd[*]}"使いたい避ける-elftpのオプションはとにかく文字列のみを使用できるため、配列要素を一意の引数として使用します。

一方、単一の文字列としてのみ機能できるため、配列を使用することは最初は役に立ちません。

lftp ... -e "$("${cmd[@]}")"このエラーは、次の実行時に発生します。

$(...)シェルから内部的にコマンドとして実行されるコマンド置換があります。どこで入手したのかわかりませんが、たとえば試してみてください。

$ cmd=(date)
$ lftp ...  -e "$("${cmd[@]}")"

(おそらくそこにないという事実に感謝しなければなりませんrm something。)


Stéphaneの答えは、ほとんどの特殊文字の引用を処理する解決策を提供しますが、もっと簡単なことをしたい場合あなたは空白文字についてだけ心配すればよいと思います。、私の考えにはこれが効果があるようです:

#! /usr/bin/bash
set -xe

source="."
target="Two Words/"

cmd="mirror --reverse --continue --parallel=5 '$source' '$target'"

user=foo
pass=secret

lftp -u "$user,$pass" http://localhost << EOF
set log:enabled true
$cmd
quit
EOF

とにかく配列は最終的に文字列になるため、配列は必要ありません。結果文字列では、ファイル名の周りに引用符が必要です。なぜなら、その中に一重引用符を使用した$cmdからです。これは怠惰な解決策です、ファイル名に引用符が含まれているかどうかは関係ありません。代わりに二重引用符を使用できます"... \"$source\"..."。ここにある文書は次のように表示されます。

set log:enabled true
mirror --reverse --continue --parallel=5 '.' 'Two Words/'
quit

これはlftpに渡す合理的な命令セットのようです。

もちろん、lftpガラガラをチェックする前に引用符なしでファイル名をテストするのは比較的簡単です。

case $target in
    *[\'\"]*) echo "ugh, can't have quotes in filename!"; exit 1;;
esac

または、より厳密に受け入れられる文字をリストします。

case $target in
    *[![:alnum:]._" "/]*) echo "ugh, disallowed characters in filename!"; exit 1;;
esac

関連情報