cp:从索引将多个文件复制到多个目标

cp:从索引将多个文件复制到多个目标

我有一个目录,其中包含多个配置文件,我想将它们复制到我的主目录的不同部分。为了更好地控制文件的存放位置和复制的内容,我有一个索引文件 ( CONFIGSINDEX),其中包含制表符分隔的文件和目标;左侧是文件名,右侧是目标,如下所示:

#files 目的地
zshrc $HOME/.zshrc
i3config $HOME/.config/i3/config
mu4e.el $HOME/.emacs.d/mu4e.el
xinitrc $HOME/.xinitrc
# 等等

为了将文件复制到目的地,我运行了以下命令:

cp -v $(sed "s/\s*#.*//g; /^$/ d" CONFIGSINDEX | awk {'print $1'}) $(sed "s/\s*#.*//g; /^$/ d" CONFIGSINDEX | awk {'print $2'})

并收到以下错误:

cp: target '$HOME/.xinitrc' is not a directory

无论最后一行是什么,都会发生这种情况,并且我之前已经复制过这样的单个文件(例如cp xinitrc ~/.xinitrc),没有任何问题。

答案1

您的cp命令只是一个。两个sed命令将独立地为其生成许多参数。这样做有几个问题:

  1. 它仍然命令,而不是每行一个命令CONFIGSINDEX对你的问题的第一次修改尝试遍历行;这是一个繁琐的尝试(一遍又一遍地读取同一个文件很无力),但它可能cp每行运行一次就成功了。在当前代码中,只有最后一个对象被视为目标。几乎所有你想作为目标的对象都被视为源。此外,由于指定了多个源,最后一个参数应该是(目标)目录,不是文件。
  2. 即使你设法每行运行一个,使用命令替换( )cp检索对象在这里仍然几乎没有用,因为: sed$(…)
    • 一般来说,您需要适当的引用来处理带有空格等的名称;
    • shell 在参数和变量扩展之后执行命令替换;不再执行变量扩展,因此$HOME保持文字;该消息target '$HOME/.xinitrc' is not a directory指的是文字路径$HOME/.xinitrc(要清楚:用文字 $) 显然它不存在作为目录。

要处理$HOME文件中的 ,您需要让 shell 解析它,因此实际上会发生变量扩展。我猜这可以用 和 来完成sedeval的目的eval是再次解析一些字符串。

但请注意,您的文件几乎是一个 shell 脚本。如果您cp -v在每一行开头添加,它将是一个可以使用sh或运行的脚本bash。所以让我们这样做。这只会打印动态创建的脚本:

<CONFIGSINDEX sed -e 's/^[[:space:]]*//' -e '/^$/d' -e '/^#/d' -e 's/^/cp -v -- /'

检查它是否看起来像您想要在 shell 中输入的命令。如果是,请将整个命令通过管道传输到sh,它将如下所示:

<CONFIGSINDEX sed -e 's/^[[:space:]]*//' -e '/^$/d' -e '/^#/d' -e 's/^/cp -v -- /' | sh

稍微解释一下。sed做四件事:

  • 它消除了空格字符从每行的开头;
  • 它删除了空行(并且由于上述原因,这包括过去仅由空格字符组成的行);
  • 它会删除以 开头的行#(包括以空格字符开头然后 的行#);
  • 它在行的最开始添加一个空格字符。cp -v --

最后,生成的脚本被传递到单独的 shell ( sh)。文件中的制表符将很好地分隔参数。

像这样(或使用 )解释文件有优点也有缺点eval。主要优点是你可以使用 shell 支持的所有语法。这包括参数(变量),就像你的情况一样。但也包括引用或转义特殊字符,例如此行将被正确解析:

'带空格的名称' "$HOME/带制表符的新名称"

主要缺点是您可以使用 shell 支持的所有语法。如果不注意,你可能会无意中使用它们,或者有人会故意使用该文件来注入代码。 例子:

foo 新名称;rm -rf 非常重要的目录

sh实际上,在将行传递给(或使用)之前不可能对其进行清理eval。我提出的解决方案在某种程度上相当简单和优雅,但只有当文件完全受您控制时才使用它。

如果另一个工具使用(或生成)该文件,则解决方案将不适用,因此您既不能引用也不能退出。我的意思是,如果您不能这样做:

‘带有空格的名称’ ‘foo/带有空格的名称’
# 或者
name\ 带有\ 空格 foo/name\ 带有\ 空格

只有这个:

带空格的名称 foo/带空格的名称

那么传递将sh不起作用,您需要另一种方法。


另一种方法可能是这样的:

<CONFIGSINDEX sed -e 's/^[[:space:]]*//' -e '/^$/d' -e '/^#/d' -e 's:$HOME:'"$HOME"':g' \
| while IFS=$'\t' read -r source target garbage; do
  cp -v -- "$source" "$target"
done

(如果您不熟悉\行末的命令:它使行继续,所以这里的命令就像sed … | while read …)。

现在sed不仅负责删除空行和注释;它还用shell 返回$HOME的实际内容替换。读取两个制表符分隔的字符串(如果有的话,会分配多余的字段,这些字段永远不会使用)。这非常安全,现在您根本无法注入代码,无法执行任何代码。$HOMEreadgarbage

但也存在缺点:

  • 如果$HOME文件中是,并且变量包含制表符(理论上可能),则会破坏逻辑。可以通过$HOME在 之后read分别“扩展” $source(如果需要)和 来解决这个问题$target
  • 如果$HOME文件中有 并且变量包含:,则会破坏sed命令。但由于 的语法/etc/passwd(其中:是分隔符),$HOME可能永远不会包含:。这正是我:sed这里使用 的原因。
  • $HOMERSIMPSON将被改变,但不会${HOME}
  • 要扩展其他变量,您需要更多sed代码。

因此您可能还需要另一种方法。


另一种方法是使用envsubst。它替换环境中的变量。请注意,这意味着它不会看到所有 shell 变量,只会看到导出的变量。HOME肯定在环境中,所以envsubst可以使用它。POSIX 不需要该工具,因此您可能没有它。如果您有它,这是要走的路:

<CONFIGSINDEX sed -e 's/^[[:space:]]*//' -e '/^$/d' -e '/^#/d' | envsubst \
| while IFS=$'\t' read -r source target garbage; do
  cp -v -- "$source" "$target"
done

现在sed不处理$HOMEenvsubst处理 。该工具将处理$HOME${HOME}。它还将处理其他环境变量,除非您限制它(例如envsubst '$HOME')。

据我所知,该代码非常安全且强大。

相关内容