使用带有 SCP 的 bash 脚本来选择远程服务器上的目录

Using a bash script with SCP to choose directory on remote server

我的问题是,我想写一个bash脚本来上传一个文件到远程服务器,并使用文件名来确定使用远程服务器上的哪个目录。下面的脚本捕获文件名的第一部分以确定远程服务器上的目录。

#!/bin/bash

filename=
regex="(.*) S([0-9]{2})E([0-9]{2}) (.*)\.mp4"
for filename; do
    if [[ "$filename" =~ $regex ]]; then
        a=`echo scp \"$filename\" mal@serenity.local:\'\"/Volumes/Vault/${BASH_REMATCH[1]}\"\'`
        eval $a
fi
done

我的难处在于,对于文件名中确定目录的部分是单个单词的文件,此脚本可以正常工作。以Herdmasters S01E14 Three Men on the Same Horse.mp4为例,将文件定向到远程服务器上的"Herdmasters"目录。它也适用于第一部分有空格的情况 - Bonehead the Clown S03E15 My Stupid Dog.mp4 也适用。

但是,当文件名包含撇号或引号时,轮子会脱落 - I'd Hit That S02E09 Well, Maybe Not That.mp4 会抛出不匹配的引号错误。

如果我事先不知道文件名,我需要采取哪些步骤才能正确转义引号、括号等?

TL;DR。跳转到 "Solution to the problem".

教育material

你的剧本中的两个教训:

  1. 除非有正当理由,否则请始终引用参数扩展。
  2. 不惜一切代价避免 eval(或至少非常谨慎地使用它),并且 从不 将它与任意字符串一起使用。

第一点应该很明显,第二点需要一些解释。就这样吧。

您的 ${BASH_REMATCH[1]} 实际上可以是任何东西,eval 任意字符串可能会导致任意损坏 。您可以轻松地用恶意文件名摧毁您的计算机。例如,假设一个恶意文件名为 "'; ls $HOME; echo ' S01E01 whatever.mp4。当你通过你的脚本 运行 它时,你将执行 ls $HOME。如果您想亲自查看,只需尝试以下代码片段,将 ${BASH_REMATCH[1]} 作为上面的文件名放在 $malicious_string 中(set -x 让您看到正在执行的内容):

set -x
malicious_string="\"'; ls $HOME; echo '"
a=`echo scp \"$filename\" mal@serenity.local:\'\"/Volumes/Vault/$malicious_string\"\'`
eval $a

你最后执行的命令,根据set -x,是

scp '' 'mal@serenity.local:"/Volumes/Vault/"'; ls /Users/johndoe; echo '"'

想象一下,如果我将 ls $HOME 替换为 rm -rf $HOME


问题的解决

在这种情况下,您根本不需要 eval,尽管为 scp 引用文件名仍然很烦人。 scp 期望您传递远程 shell 可读的引用路径。因此,您可以做的是在路径周围添加单引号,并用 '\'' 转义每次出现的单引号 ''\'' 的解释:第一个 ' 关闭前一个单引号,\'是字面单引号,最后一个'开始新一轮的引号)。例如,try

stupid_name=$'word \n $`\"'\'  # This is the most stupid filename ever, with space, $, `, ", \, and '
quoted_stupid_name=\'${stupid_name//\'/\'\\'\'}\'  # This command wraps $stupid_name in single quotes and replace each occurrenceof ' with '\''
touch file && scp file user@domain:"$quoted_stupid_name"

放在一起,你的最终产品是

#!/bin/bash
regex="(.*) S([0-9]{2})E([0-9]{2}) (.*)\.mp4"
for filename; do
    if [[ "$filename" =~ $regex ]]; then
        remote_filename="/Volumes/Vault/${BASH_REMATCH[1]}"
        quoted_remote_filename=\'${remote_filename//\'/\'\\'\'}\'
        scp "$filename" mal@serenity.local:"$quoted_remote_filename"
done