【问题标题】: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 来执行此操作,但我不知道如何操作。

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

【问题讨论】:

标签: linux sed terminal


【解决方案1】:

保持简单:)

grep + echo 就足够了:

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

编辑: 合并了@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;next}f==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;next}{print}END{if(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=s}7;END{if(!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;next}f==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} END{if(!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 的更多信息和命令概述在我的博客上:

            【讨论】:

            • 对于像“选项值”这样的文件会怎样?当我将 = 替换为空格时,该函数修复了该选项,但文件的其余部分丢失了:(
            • @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 中表达式评估算法的副作用。所以你可以随心所欲地称呼它,除了直截了当——这是我最初的观点。
            【解决方案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" 'BEGIN{FS=OFS="="}match($1, "^\\s*" var "\\s*") {$2=" " new_val}1' "$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
                

                【讨论】:

                • 欢迎来到stackoverflow!请在发布答案之前仔细阅读问题 - 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)
                

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2017-03-20
                  • 2013-07-18
                  • 2012-10-05
                  • 1970-01-01
                  • 2014-05-12
                  • 2016-09-05
                  • 2016-12-19
                  相关资源
                  最近更新 更多