当变量的值包含空格时,如何将定界文档中的变量扩展为一个参数?

当变量的值包含空格时,如何将定界文档中的变量扩展为一个参数?

我创建了一个通过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/"'注意双引号),但我不确定为什么它没有。

这个问题与我到目前为止研究的许多其他问题类似。这对谷歌来说不同/困难的是,扩展发生在“这里字符串”内部。据我所知,这无关紧要,但据我所知,它也可能是非常独特的。


评论建议我将所有内容存储到数组中。这是我的尝试:

#! /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

(* 我不知道如何在子 shell 中包含多行。这就是为什么我在其中放置了实际的换行符,但我不确定这是否是正确的做法。我谷歌搜索的每个结果都与命令替换有关,不是子壳。)

当我运行这个时,它给了我这个错误:Unknown command 'mirror --reverse --continue --parallel=5 . Two Words/'.如果我将其复制并粘贴到我的 shell 中,它会给我一个不同的错误,所以我不明白发生了什么。

打电话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支持简单的引用形式。虽然与大多数 shell 一样,它可以使用单引号、双引号和反斜杠,但语法不同。您可以使用lftp's命令进行实验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 一样的 shell 中的强引号。

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

$'...'不支持ksh93 样式的引用形式。

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

引号不必成对出现。似乎不可能在命令参数中包含换行符,无论它是否在引号内。

'...'从代码看, or里面需要转义的字符"..."只是反斜杠和对应的引号字符。 lftp在字节级别工作,它不会将字节序列解码为字符。

因此,在这里,在 语法中正确引用字符串的最佳方法lftp是使用 转义其中的每个'and \(实际上是 0x5c 字节,甚至是其他多字节字符编码中存在的字节)并将\其包装在 中'...'。或者与 相同"..."

在这里,使用evalhere是不必要的,并且会增加更多的复杂性,因为您需要管理两个级别的引用。

#! /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

(这里还要避免在命令行上传递密码,因为密码是公开的)

现在,虽然我们可能已经设法将这些字符串按原样传递给lftpmirror命令,但lftp它本身是否会在 FTP 协议的命令中正确转义这些文件名是另一回事。 FTP 因在这方面不可靠而臭名昭著,不同的服务器表现不同。请注意,lftp除了 FTP 之外,它还支持许多其他文件传输协议,其中一些协议更为可靠。

答案2

我不知道如何解决这个问题,特别是因为我不知道哪一行是我出错的地方:cmd=...eval "$cmd"?两者都不?两个都?

无论如何,我期望调试输出看起来像这样:(+ cmd='mirror --reverse --continue --parallel=5 "." "Two Words/"'注意双引号),但我不确定为什么它没有。

你那里有的是:

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

第一个"(at cmd=") 开始一个带引号的字符串,第二个 (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 。两个字/'。

出色地。扩展"${array[@]}"在 shell 中很有用,因为它将数组元素扩展为多个不同的 shell 单词(字符串),保持带空格的文件名完整。但在这里,我们位于一个here-doc中,所以不可能生成多个单词/字符串:整个here-doc只是一个字符串。所以它只是将数组元素与空格连接起来并扩展到该值。那里的引号会保留下来,就像在定界文档中一样。

lftp 获取的命令是

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

由于引号,它将其视为单个命令,比较例如

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

问题标题还询问“[扩展]定界文档中的变量作为一个参数”,但这不是问题,因为不可能这样做(并且有人可能会说“参数”的概念不存在于无论如何,这里的文档)。不,您的问题是为 lftp 正确引用。

如果我将其复制并粘贴到我的 shell 中,它会给我一个不同的错误,所以我不明白发生了什么。

大概是这样的bash: mirror: command not found?有mirror一个用于 lftp 的命令,所以我不确定将其粘贴到 shell 的命令行是否有用。

我理解使用数组来存储 shell 命令的建议,有一些非常好其原因。但这里没有 shell 命令,所以这是不必要的。

打电话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[*]}"使用避免将数组元素作为不同的参数,因为 lftp 的-e选项无论如何只能采用一个字符串。

另一方面,由于它只能使用单个字符串,因此使用数组一开始就没什么用。

运行lftp ... -e "$("${cmd[@]}")"出现这个错误:

$(...)一个命令替换,它会将内部作为 shell 中的命令运行。我不确定你从哪里得到的,但尝试例如

$ 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

相关内容