如果文件中不存在该行,则将一行内容追加到文件中。

253

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

include "/configs/projectname.conf"

将内容保存到名为lighttpd.conf的文件中。

我正在研究如何使用sed来做到这一点,但是我无法弄清楚如何操作。

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


如果您想要编辑一个ini文件,工具crudini可能是一个不错的选择(但还不能用于lighthttpd)。 - rubo77
25个回答

472

保持简单 :)

grep + echo 应该足够了:

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

7
这仅适用于非常简单的行。否则,grep会将行中大多数非字母字符解释为模式,导致它无法检测到文件中的该行。 - Cerin
2
注意,使用“-F”似乎解决了我的问题。 - Cerin
2
优美的解决方案。当然,这也适用于触发更复杂的表达式。我的解决方案使用 echo 触发一个多行 heredoc 的 cat 到一个配置文件中。 - Eric L.
3
你需要添加一个“-x”选项,以确保仅完整匹配行,而不是部分匹配。 - Thijs Wouters
5
要追加的文本在表达式中出现了两次。 将命令前缀改为ADDTEXT='include "/configs/projectname.conf"',然后使用$ADDTEXT如何? 对于文件名foo.bar也是一样。 - Jean Spector
显示剩余15条评论

109

使用 grepecho 实现干净、易读和可重用的解决方案,只有在文件中不存在该行时才添加一行:

LINE='include "/configs/projectname.conf"'
FILE='lighttpd.conf'
grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"
如果您需要匹配整行,请使用grep -xqF
如果需要忽略文件不存在的错误并创建仅包含该行的新文件,请添加-s

9
如果需要对一个需要 root 权限的文件进行操作:sudo grep -qF "$LINE" "$FILE" || echo "$LINE" | sudo tee -a "$FILE"。该命令会在检查文件中是否存在指定行 $LINE,如果不存在,则将该行写入文件 $FILE 中。注意,该操作需要管理员权限才能执行。 - nebffa
1
当行以“-”开头时,这实际上不起作用。 - hyperknot
4
@zsero说得很好!我在grep命令中添加了“--”,这样它就不会再将以破折号开头的$LINE解释为选项了。 - rubo77

104

试试这个:

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

8
请看解释:explainshell对应翻译如下:为了实现对文件进行设置,可以使用以下命令:首先使用grep命令在文件中查找以“option”开头的内容,如果查找成功,则执行sed命令来替换以“option”开头的行为“option=value”,并将更改保存到原始文件中。如果没有查找到匹配项,echo命令会将“option=value”追加写入文件末尾。 - Darwyn
Marc Wäckerlin的答案更全面,只使用一个sed命令完成了所有请求。此外,“&&”和“||”的用法是含糊的(“||”是否可以应用于grep和sed,如果sed返回false会发生什么?)。 - Dev Null
我在工作中使用了这个工具,效果非常好。感谢分享。 - gjjhhh_

46

使用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= 开头的行,并且 (.*) 忽略等号后面的所有内容。\(\) 括起来的部分将被重复使用,并在后面用 \1 引用。
  • /^#?(\s*'"${option//////}"'\s*=\s*).*/: 忽略以 # 开头的注释代码。\? 表示«可选项»。注释将被移除,因为它位于拷贝部分外的 \(\) 中。 \s* 表示«任意数量的空格»(空格、制表符)。空格被复制,因为它们在 \(\) 内,所以不会失去格式。
  • /^\(option=\).*/{…}: 如果匹配到一行 /…/,则执行下一条命令。要执行的命令不是单个命令,而是一个块 {…}
  • s//…/: 查找并替换。由于搜索词是空的 //,它应用于最后一次匹配,即 /^\(option=\).*/
  • s//\1value/: 用括号中的所有内容替换最后一次匹配,由 \1 引用,并添加文本值。
  • :a;n;ba;q: 设置标签 a,然后读取下一行 n,然后分支 b(或转到)返回标签 a,也就是说:读取文件结尾之前的所有行,因此在第一次匹配后,只获取所有后续行而不进行进一步处理。然后 q 退出,从而忽略其他所有内容。
  • $aoption=value: 在文件结尾 $ 处附加文本 option=value

有关 sed 的更多信息和命令概述,请访问我的博客:


这对于像“option value”这样的文件怎么处理呢?当我将=替换为空格时,函数会修复选项,但是文件的其余部分会丢失:( - jlanza
你可以在替换命令中使用另一个字符(如 !)来转义 / 字符:sed -i -e '/^\(option=\).*/{s!!\1value!;:a;n;ba;q}' -e '$aoption=value' filename - jmlemetayer
是的,您可以在sed命令中使用任何字符。实际上,我经常使用逗号:sed 's,from,to,g',但为了允许任意名称值对,例如从函数调用中的参数,您仍然需要对它们进行转义。 - Marc Wäckerlin
3
值得一提的是,文件'filename'或者'$3'不能为空。 - antonbormotov
嗨,这对我有帮助,但我有一个问题,如果文件的第一行是#option=value,并且文件的最后一行是option=value,那么我会得到两行相同信息。有什么方法可以处理这个问题吗? - Juan
显示剩余4条评论

21
如果要写入受保护的文件,则 @drAlberT 和 @rubo77 的答案可能对您无效,因为您不能使用sudo >>。 因此,一个类似简单的解决方案是使用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" - Juan

15

这是一个基于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 - bgStack15
很好,它帮了我很多 +1。你能解释一下你的sed表达式吗? - Rahul
我很想看到这个解决方案的注释说明。我已经使用 sed 多年了,但大多数情况下只用 s 命令。holdpattern 空间交换对我来说太复杂了。 - nortally

12

如果有一天,其他人需要将这段代码视为“遗留代码”来处理,那么如果你写一个不那么深奥的代码,那个人会很感激。

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

4
谢谢你的评论,Graham!是的,这是正常的shell语法。而且,任何称职的管理员都应该理解它。但是,它的工作原理基于shell内部表达式求值算法的副作用。所以,你可以随意调用它,除了直接调用——这也是我最初的观点。 - Marcelo Ventura

10

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

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

"properly-escaped" 的意思是使用正则表达式匹配输入,即从实际输入中转义所有正则表达式控制字符,例如在 ^$/*?+() 前面加上反斜杠。

如果文件的最后一行或没有悬挂的换行符可能会导致此方法失败,但可以通过巧妙的分支处理来解决这个问题...


3
дёҖеҸҘз®Җзҹӯжҳ“иҜ»зҡ„еҘҪд»Јз ҒпјҢйҖӮз”ЁдәҺжӣҙж–°/etc/hostsж–Ү件пјҡsed -i.bak -e '$a\' -e "$NEW_IP\t\t$HOST.domain\t$HOST" -e "/.*$HOST/d" /etc/hostsгҖӮ - Noam Manos
一个更简短的版本:sed -i -e '/<entry>/d; $a <entry>' - Jérôme Pouiller
@Jezz 我认为这个方法会失败,如果输入已经在文件末尾,因为d命令将重新启动sed程序,从而跳过append,如果删除恰好发生在最后一行。 - Robin479
2
@Robin479 起码得先执行append再执行delete:sed -i -e '$a<entry>' -e '/<entry>/d'。这和你最初的回答几乎一致。 - Jérôme Pouiller

8

这里有一个一行的sed命令可以在内部完成工作。请注意,它会在变量存在时保留其位置和缩进。这通常对于上下文很重要,例如当周围有注释或变量在缩进块中时。任何基于“删除-然后追加”范例的解决方案都会严重失败。

   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

最后,如果您希望保留行内注释,可以使用捕获组来实现。例如,如果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

不错的解决方案,但希望能够解释一下代码。 - Enissay

6
作为一个仅使用awk的一行命令:
awk -v s=option=value '/^option=/{$0=s;f=1} {a[++n]=$0} END{if(!f)a[++n]=s;for(i=1;i<=n;i++)print a[i]>ARGV[1]}' file

ARGV[1]是您的输入文件。它在END块的for循环中被打开并写入。在END块中为输出打开file替代了像sponge这样的实用工具或者先写入临时文件,然后再将临时文件mvfile

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

我已经为可读性添加了一些空格(不多),但整个awk程序中您只需要一个空格,即在print之后的空格。如果file包含# comments,它们将被保留。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接