仅当文件尚不存在时才将行附加到文件中

Posted

技术标签:

【中文标题】仅当文件尚不存在时才将行附加到文件中【英文标题】:Appending a line to a file only if it does not already exist 【发布时间】:2011-04-03 04:34:57 【问题描述】:

我需要将以下行添加到配置文件的末尾:

include "/configs/projectname.conf"

到一个名为lighttpd.conf的文件

我正在考虑使用 sed 来执行此操作,但我不知道如何操作。

如果该行不存在,我将如何只插入它?

【问题讨论】:

如果您正在尝试编辑 ini 文件,工具 crudini 可能是一个不错的选择(但不适用于 lighthttpd) 或许也可以看看sed,awk.grep - add a line to the end of a configuration section if the line doesn't already exist 【参考方案1】:

保持简单:)

grep + echo 就足够了:

grep -qxF 'include "/configs/projectname.conf"' foo.bar || echo 'include "/configs/projectname.conf"' >> foo.bar
-q安静 -x匹配整行 -F 模式是纯字符串 https://linux.die.net/man/1/grep

编辑: 合并了@cerin 和@thijs-wouters 的建议

【讨论】:

我会将 -q 开关添加到 grep 以抑制输出:grep -vq ... 仅供参考,使用 -v 和 && 看起来并不奏效,而 ||没有 -v 的作品。 这仅适用于非常简单的行。否则,grep 会将行中的大多数非字母字符解释为模式,导致它无法检测文件中的行。 您需要添加 -x 选项以确保仅匹配整行,而不是仅匹配行的一部分。 要附加的文本在表达式中出现两次。在命令前面加上ADDTEXT='include "/configs/projectname.conf"' 然后使用$ADDTEXT 怎么样?文件名foo.bar也是如此。【参考方案2】:

使用 awk

awk 'FNR==NR && /configs.*projectname\.conf/f=1;nextf==0;END if(!f)  print "your line" ' file file

【讨论】:

是“文件文件”还是“文件”? 它是file file,因为它对同一个文件进行了两次传递。 NR==FNR 在第一遍是真的,但不是第二遍。这是一个常见的 Awk 习惯用法。【参考方案3】:

这是sed 版本:

sed -e '\|include "/configs/projectname.conf"|h; $x;s/incl//;g;t;a\' -e 'include "/configs/projectname.conf"' -e '' file

如果您的字符串在变量中:

string='include "/configs/projectname.conf"'
sed -e "\|$string|h; \$x;s|$string||;g;t;a\\" -e "$string" -e "" file

【讨论】:

对于用完整/更新的配置文件条目替换部分字符串或根据需要附加行的命令:sed -i -e '\|session.*pam_mkhomedir.so|h; $x;s/mkhomedir//;g;tF;a\' -e 'session\trequired\tpam_mkhomedir.so umask=0022' -e ';:F;s/.*mkhomedir.*/session\trequired\tpam_mkhomedir.so umask=0022/g;' /etc/pam.d/common-session 很好,它对我帮助很大 +1。你能解释一下你的 sed 表达吗? 我很想看到这个解决方案的注释解释。我已经使用sed 多年,但主要是s 命令。 holdpattern 空间交换超出了我的范围。【参考方案4】:

这是一个awk 实现

/^option *=/  
  print "option=value"; # print this instead of the original line
  done=1;               # set a flag, that the line was found
  next                  # all done for this line

print                 # all other lines -> print them
END                    # end of file
  if(done != 1)         # haven't found /option=/ -> add it at the end of output
    print "option=value"

运行它使用

awk -f update.awk < /etc/fdm_monitor.conf > /etc/fdm_monitor.conf.tmp && \
   mv /etc/fdm_monitor.conf.tmp /etc/fdm_monitor.conf

awk -f update.awk < /etc/fdm_monitor.conf | sponge /etc/fdm_monitor.conf

编辑: 作为一个单行:

awk '/^option *=/ print "option=value";d=1;nextprintENDif(d!=1)print "option=value"' /etc/fdm_monitor.conf | sponge /etc/fdm_monitor.conf

【讨论】:

我需要一个单行,因为我将它作为来自 c 程序的系统调用发送。【参考方案5】:

这是一个 awk 单行代码:

 awk -v s="option=value" '/^option/f=1;$0=s7;ENDif(!f)print s' file

这不会对文件进行就地更改,但是您可以:

awk '...' file > tmpfile && mv tmpfile file

【讨论】:

【参考方案6】:

试试这个:

grep -q '^option' file && sed -i 's/^option.*/option=value/' file || echo 'option=value' >> file

【讨论】:

解释见:explainshell Marc Wäckerlin 的回答更全面,可以在一个 sed 中完成所有要求。此外,使用 && 和 ||模棱两可(|| 可以应用于 grep 和 sed,如果 sed 返回 false 会怎样?【参考方案7】:

使用sed,你可以说:

sed -e '/option=/s/.*/option=value/;:a;n;:ba;q' -e 'aoption=value' filename

如果参数存在,这将替换参数,否则会将其添加到文件底部。


如果您想就地编辑文件,请使用-i 选项:

sed -i -e '/option=/s/.*/option=value/;:a;n;:ba;q' -e 'aoption=value' filename

【讨论】:

这似乎根本不起作用。它在每一行之后添加“option=value”。不知道它是如何得到两个赞成票的。你可以做 '$aoption=value' 但我似乎无法以上一场比赛为条件,所以无论如何它都会附加到末尾。【参考方案8】:
sed -i '1 h
1 !H
$ 
   x
   s/^option.*/option=value/g
   t
   s/$/\
option=value/
   ' /etc/fdm_monitor.conf

加载缓冲区中的所有文件,最后,改变所有出现的地方,如果没有改变,则添加到最后

【讨论】:

【参考方案9】:
 sed -i 's/^option.*/option=value/g' /etc/fdm_monitor.conf
 grep -q "option=value" /etc/fdm_monitor.conf || echo "option=value" >> /etc/fdm_monitor.conf

【讨论】:

【参考方案10】:

我需要编辑具有受限写入权限的文件,因此需要sudo。从 ghostdog74 的回答工作并使用临时文件:

awk 'FNR==NR && /configs.*projectname\.conf/f=1;nextf==0;END if(!f)  print "your line" ' file > /tmp/file
sudo mv /tmp/file file

【讨论】:

这看起来不正确,当缺少配置行时,它将丢失文件中的其他行。【参考方案11】:

另一种 sed 解决方案是始终将其附加到最后一行并删除预先存在的一个。

sed -e '$a\' -e '<your-entry>' -e "/<your-entry-properly-escaped>/d"

“正确转义”意味着放置一个与您的条目匹配的正则表达式,即从您的实际条目中转义所有正则表达式控件,即在 ^$/*?+() 前面放置一个反斜杠。

这可能会在文件的最后一行失败,或者如果没有悬空的换行符,我不确定,但这可以通过一些漂亮的分支来处理...

【讨论】:

漂亮的单行字,简短易读。非常适合更新 etc/hosts:sed -i.bak -e '$a\' -e "$NEW_IP\t\t$HOST.domain\t$HOST" -e "/.*$HOST/d" /etc/hosts 小版本:sed -i -e '/&lt;entry&gt;/d; $a &lt;entry&gt;' @Jezz 我认为这将失败,如果条目已经在文件末尾,因为d 命令将重新启动 sed 程序,从而跳过 append,如果删除恰好在最后一行。 @Robin479 该死的,append 必须在delete 之前执行:sed -i -e '$a&lt;entry&gt;' -e '/&lt;entry&gt;/d'。这与您的原始答案几乎相同。【参考方案12】:

这将是一个干净、可读和可重用的解决方案,使用 grepecho 仅在文件不存在时向文件添加一行:

LINE='include "/configs/projectname.conf"'
FILE='lighttpd.conf'
grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"

如果你需要匹配整行使用grep -xqF

添加-s 以在文件不存在时忽略错误,只使用该行创建一个新文件。

【讨论】:

如果是需要root权限的文件:sudo grep -qF "$LINE" "$FILE" || echo "$LINE" | sudo tee -a "$FILE" 当行以-开头时,这实际上不起作用。 @zsero:好点!我在 grep 命令中添加了--,因此它不会再将以破折号开头的 $LINE 解释为选项。【参考方案13】:

作为一个 awk-only 单行:

awk -v s=option=value '/^option=/$0=s;f=1 a[++n]=$0 ENDif(!f)a[++n]=s;for(i=1;i<=n;i++)print a[i]>ARGV[1]' file

ARGV[1] 是您的输入 file。它在END 块的for 循环中打开和写入。在END 块中打开file 以进行输出取代了对sponge 等实用程序或写入临时文件然后mv 将临时文件写入file 的需要。

数组a[] 的两个赋值将所有输出行累积到a。如果主 awk 循环在 file 中找不到 optionif(!f)a[++n]=s 将附加新的 option=value

为了便于阅读,我添加了一些空格(不是很多),但在整个 awk 程序中确实只需要一个空格,即print 之后的空格。 如果file 包含# comments,它们将被保留。

【讨论】:

【参考方案14】:

为了减少重复,我通过设置变量详细阐述了kev的grep/sed解决方案。

在第一行设置变量(提示:$_option 应匹配行中的所有内容,直到值 [包括任何分隔符,如 = 或 :])。

_file="/etc/ssmtp/ssmtp.conf" _option="mailhub=" _value="my.domain.tld" \ sh -c '\ grep -q "^$_option" "$_file" \ && sed -i "s/^$_option.*/$_option$_value/" "$_file" \ ||回声“$_option$_value”>>“$_file”\ '

请注意,sh -c '...' 只是具有扩大变量范围的效果,而不需要 export。 (见Setting an environment variable before a command in bash not working for second command in a pipe)

【讨论】:

【参考方案15】:

如果写入受保护的文件,@drAlberT 和 @rubo77 的答案可能对您不起作用,因为无法 sudo &gt;&gt;。那么similarly simple solution 将使用tee --append(或者,在MacOS 上,tee -a):

LINE='include "/configs/projectname.conf"'
FILE=lighttpd.conf
grep -qF "$LINE" "$FILE"  || echo "$LINE" | sudo tee --append "$FILE"

【讨论】:

你也可以使用 sed,这样你就可以在你想要的地方插入这一行。 grep -qF "$LINE" "$FILE" || sudo sed -i "/line_where_you_want_to_insert_before/i $LINE" "$FILE"【参考方案16】:

这是一个单行 sed,它内联完成工作。请注意,当变量存在时,它会在文件中保留变量的位置及其缩进。这对于上下文通常很重要,例如当周围有 cmets 或变量位于缩进块中时。任何基于“delete-then-append”范式的解决方案在这方面都失败了。

   sed -i '/^[ \t]*option=/h;s/=.*/=value/;$x;/^$/s//option=value/;H;x' test.conf

使用一对通用的变量/值,您可以这样写:

   var=c
   val='12 34' # it handles spaces nicely btw
   sed -i '/^[ \t]*'"$var"'=/h;s/=.*/='"$val"'/;$x;/^$/s//c='"$val"'/;H;x' test.conf

最后,如果您还想保留内联 cmets,您可以使用 catch 组来实现。例如。如果test.conf 包含以下内容:

a=123
# Here is "c":
  c=999 # with its own comment and indent
b=234
d=567

然后运行这个

var='c'
val='"yay"'
sed -i '/^[ \t]*'"$var"'=/h;s/=[^#]*\(.*\)/='"$val"'\1/;s/'"$val"'#/'"$val"' #/;$x;/^$/s//'"$var"'='"$val"'/;H;x' test.conf

产生:

a=123
# Here is "c":
  c="yay" # with its own comment and indent
b=234
d=567

【讨论】:

不错的解决方案,但对代码的解释将不胜感激【参考方案17】:

使用sed,最简单的语法:

sed \
    -e '/^\(option=\).*/s//\1value/;:a;n;ba;q' \
    -e '$aoption=value' filename

如果参数存在,这将替换参数,否则会将其添加到文件底部。

如果您想就地编辑文件,请使用-i 选项。


如果您想接受并保留空格,并且除了删除注释之外,如果该行已经存在但被注释掉,请写:

sed -i \
    -e '/^#\?\(\s*option\s*=\s*\).*/s//\1value/;:a;n;ba;q' \
    -e '$aoption=value' filename

请注意,选项和值都不能包含斜杠/,否则您必须将其转义为\/


要使用 bash 变量 $option$value,您可以这样写:

sed -i \
    -e '/^#\?\(\s*'$option//\//\\/'\s*=\s*\).*/s//\1'$value//\//\\/'/;:a;n;ba;q' \
    -e '$a'$option//\//\\/'='$value//\//\\/ filename

bash 表达式$option//\//\\/ 引用斜线,它将所有/ 替换为\/

注意: 只是陷入了一个问题。在 bash 中你可以引用 "$option//\//\\/",但是在busybox 的 sh 中,这不起作用,所以你应该避免使用引号,至少在非 bourne-shell 中是这样。


全部组合在一个 bash 函数中:

# call option with parameters: $1=name $2=value $3=file
function option() 
    name=$1//\//\\/
    value=$2//\//\\/
    sed -i \
        -e '/^#\?\(\s*'"$name"'\s*=\s*\).*/s//\1'"$value"'/;:a;n;ba;q' \
        -e '$a'"$name"'='"$value" $3


说明:

/^\(option=\).*/:匹配以option= 和(.*)开头的行忽略= 之后的所有内容。 \(\) 包含我们将作为\1later 重用的部分。 /^#?(\s*'"$option/////"'\s*=\s*).*/:忽略行首带有#的注释代码. \? 表示«可选»。注释将被删除,因为它在 \(...\) 中的复制部分之外。 \s* 表示«任意数量的空格»(空格,制表符)。空格会被复制,因为它们在 \(...\) 内,所以不会丢失格式。 /^\(option=\).*/…:如果匹配一行/…/,则执行下一条命令。要执行的命令不是单个命令,而是一个块s//…/:搜索替换。由于搜索词为空 //,因此它适用于最后一个匹配项,即 /^\(option=\).*/s//\1value/: Replace the last match with everything in (…), referenced by \1and the textvalue` :a;n;ba;q:设置标签a,然后读取下一行n,然后分支b(或转到)回到标签a,这意味着:读取所有行直到文件末尾,所以在第一场比赛,只需获取以下所有行,无需进一步处理。然后q 退出,因此忽略其他所有内容。 $aoption=value:在文件$的末尾,追加a文本option=value

有关sed 的更多信息和命令概述在我的博客上:

https://marc.wäckerlin.ch/computer/stream-editor-sed-overview-and-reference

【讨论】:

对于像“选项值”这样的文件会怎样?当我将 = 替换为空格时,该函数修复了该选项,但文件的其余部分丢失了:( @jlanza,非常抱歉,我的代码中有错字。命令是«ba»,而不是«:ba»。我编辑了文本并修复了它。请重试。 您可以通过在替换命令中使用另一个字符来转义/ 字符(如!):sed -i -e '/^\(option=\).*/s!!\1value!;:a;n;ba;q' -e '$aoption=value' filename 是的,您可以为 sed 命令使用任何字符。事实上,我经常使用逗号:sed 's,from,to,g',但允许任意名称值对,例如来自函数调用的参数,您仍然需要转义它们。 值得一提的是文件filename$3不能为空【参考方案18】:

如果有一天,其他人将此代码作为“遗留代码”处理,那么如果您编写一个不那么公开的代码,那么该人将不胜感激,例如

grep -q -F 'include "/configs/projectname.conf"' lighttpd.conf
if [ $? -ne 0 ]; then
  echo 'include "/configs/projectname.conf"' >> lighttpd.conf
fi

【讨论】:

感谢您的评论格雷厄姆!是的,这是正常的 shell 语法。是的,任何称职的管理员都应该理解它。但是,它的工作原理是基于 shell 中表达式评估算法的副作用。所以你可以随心所欲地称呼它,除了直截了当——这是我最初的观点。 另见Why is testing "$?" to see if a command succeeded or not, an anti-pattern?【参考方案19】:

您可以使用此功能查找和搜索配置更改:

#!/bin/bash

#Find and Replace config values
find_and_replace_config () 
   file=$1
   var=$2
   new_value=$3
   awk -v var="$var" -v new_val="$new_value" 'BEGINFS=OFS="="match($1, "^\\s*" var "\\s*") $2=" " new_val1' "$file" > output.tmp && sudo mv output.tmp $file


find_and_replace_config /etc/php5/apache2/php.ini max_execution_time 60

【讨论】:

【参考方案20】:

使用 grep 的答案是错误的。您需要添加一个 -x 选项来匹配整行,否则像 #text to add 这样的行在寻找精确添加 text to add 时仍然会匹配。

所以正确的解决方案是这样的:

grep -qxF 'include "/configs/projectname.conf"' foo.bar || echo 'include "/configs/projectname.conf"' >> foo.bar

【讨论】:

【参考方案21】:

使用 sed: 它将插入到行尾。当然你也可以像往常一样传入变量。

grep -qxF "port=9033" $light.conf
if [ $? -ne 0 ]; then
  sed -i "$ a port=9033" $light.conf
else
    echo "port=9033 already added"
fi

使用 oneliner sed

grep -qxF "port=9033" $lightconf || sed -i "$ a port=9033" $lightconf

使用 echo 可能无法在 root 下工作,但会像这样工作。但是,如果您希望这样做,它不会让您自动化操作,因为它可能会要求输入密码。

我在尝试从根目录为特定用户进行编辑时遇到问题。只需在之前添加 $username 对我来说就是一个解决方法。

grep -qxF "port=9033" light.conf
if [ $? -ne 0 ]; then
  sudo -u $user_name echo "port=9033" >> light.conf
else
    echo "already there"    
fi

【讨论】:

欢迎来到***!请在发布答案之前仔细阅读问题 - OP 询问如何使用 sed 插入【参考方案22】:

如果您想在 Linux 终端中使用 python 脚本 运行此命令...

import os,sys
LINE = 'include '+ <insert_line_STRING>
FILE = <insert_file_path_STRING>                                
os.system('grep -qxF $"'+LINE+'" '+FILE+' || echo $"'+LINE+'" >> '+FILE)

$ 和双引号让我陷入了困境,但这有效。 谢谢大家

【讨论】:

以上是关于仅当文件尚不存在时才将行附加到文件中的主要内容,如果未能解决你的问题,请参考以下文章

仅当表中不存在两个 id 的组合时才将值插入表中

仅当记录不存在时才将 SQL 插入表中[重复]

PostgreSQL:仅当元素唯一时才将元素附加到 jsonb 数组

Swiftt3:如果不重复则附加到数组的扩展

仅当行不存在时才附加到 csv

仅当表中不存在该值时才更新 SQL 列