我有一个目录,其中包含多个配置文件,我想将它们复制到我的主目录的不同部分。为了更好地控制文件的存放位置和复制的内容,我有一个索引文件 ( 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
命令将独立地为其生成许多参数。这样做有几个问题:
- 它仍然一命令,而不是每行一个命令
CONFIGSINDEX
。对你的问题的第一次修改尝试遍历行;这是一个繁琐的尝试(一遍又一遍地读取同一个文件很无力),但它可能cp
每行运行一次就成功了。在当前代码中,只有最后一个对象被视为目标。几乎所有你想作为目标的对象都被视为源。此外,由于指定了多个源,最后一个参数应该是(目标)目录,不是文件。 - 即使你设法每行运行一个,使用命令替换( )
cp
检索对象在这里仍然几乎没有用,因为:sed
$(…)
- 一般来说,您需要适当的引用来处理带有空格等的名称;
- shell 在参数和变量扩展之后执行命令替换;不再执行变量扩展,因此
$HOME
保持文字;该消息target '$HOME/.xinitrc' is not a directory
指的是文字路径$HOME/.xinitrc
(要清楚:用文字$
) 显然它不存在作为目录。
要处理$HOME
文件中的 ,您需要让 shell 解析它,因此实际上会发生变量扩展。我猜这可以用 和 来完成sed
。eval
的目的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
的实际内容替换。读取两个制表符分隔的字符串(如果有的话,会分配多余的字段,这些字段永远不会使用)。这非常安全,现在您根本无法注入代码,无法执行任何代码。$HOME
read
garbage
但也存在缺点:
- 如果
$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
不处理$HOME
,envsubst
处理 。该工具将处理$HOME
和${HOME}
。它还将处理其他环境变量,除非您限制它(例如envsubst '$HOME'
)。
据我所知,该代码非常安全且强大。