使用非 GNU awk 保存修改

Rav*_*h13 10 linux bash shell awk inplace-editing

我遇到了一个问题(关于 SO 本身),其中 OP 必须对 Input_file(s) 本身进行编辑和保存操作。

我知道对于单个 Input_file 我们可以执行以下操作:

awk '{print "test here..new line for saving.."}' Input_file > temp && mv temp Input_file
Run Code Online (Sandbox Code Playgroud)

现在假设我们需要以相同类型的文件格式进行更改(假设这里是 .txt)。

我对这个问题的尝试/想法:它的方法是通过 .txt 文件的 for 循环,调用 singleawk是一个痛苦且不推荐的过程,因为它会浪费不必要的 cpu 周期,并且对于更多数量的文件,它会更多减缓。

那么在这里可以做什么来使用awk不支持就地选项的非 GNU 对多个文件执行就地编辑。我也经历过这个线程使用 awk 保存修改,但对于非 GNU awk 的恶习和在awk其内部更改多个文件没有什么意义,因为非 GNU awk 将无法inplace选择它。

注意:为什么我要添加bash标签,因为在我的回答部分中,我使用 bash 命令将临时文件重命名为它们的实际 Input_file 名称,因此添加了它。



编辑:根据 Ed sir 的评论,在此处添加示例示例,尽管此线程代码的用途也可用于通用就地编辑。

示例 Input_file(s):

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest
Run Code Online (Sandbox Code Playgroud)

预期输出示例:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2
Run Code Online (Sandbox Code Playgroud)

Rav*_*h13 7

由于这个线程的主要目的是如何在非 GNU 中进行就地保存,awk所以我首先发布它的模板,这将帮助任何有任何需求的人,他们需要在他们的代码中添加/附加BEGINEND部分,保持他们的主块按照他们的要求,它应该进行就地编辑,然后:

注意:以下会将其所有输出写入 output_file,因此,如果您想将任何内容打印到标准输出,请仅添加print...没有> (out)以下内容的语句。

通用模板:

awk -v out_file="out" '
FNR==1{
close(out)
out=out_file count++
rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
    .....your main block code.....
}
END{
 if(rename){
   system(rename)
 }
}
' *.txt
Run Code Online (Sandbox Code Playgroud)

具体提供样品的解决方案:

我自己提出了以下方法awk(对于添加的示例,以下是我解决此问题并将输出保存到 Input_file 本身的方法)

awk -v out_file="out" '
FNR==1{
  close(out)
  out=out_file count++
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
  print FNR > (out)
}
END{
  if(rename){
    system(rename)
  }
}
' *.txt
Run Code Online (Sandbox Code Playgroud)

注意:这只是将编辑后的输出保存到 Input_file(s) 本身的测试,可以在他们的程序中使用它的 BEGIN 部​​分,以及它的 END 部分,主要部分应该按照特定问题本身的要求。

公平警告:此外,由于这种方法在路径中创建了一个新的临时输出文件,因此最好确保我们在系统上有足够的空间,尽管在最终结果中这将只保留主 Input_file(s) 但在操作期间它需要系统/目录上的空间



以下是对上述代码的测试。

以示例执行程序:假设以下是.txtInput_file(s):

cat << EOF > test1.txt
onetwo three
tets testtest
EOF

cat << EOF > test2.txt
onetwo three
tets testtest
EOF

cat << EOF > test3.txt
onetwo three
tets testtest
EOF
Run Code Online (Sandbox Code Playgroud)

现在,当我们运行以下代码时:

awk -v out_file="out" '
FNR==1{
  close(out)
  out=out_file count++
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
  print "new_lines_here...." > (out)
}
END{
  if(rename){
    system("ls -lhtr;" rename)
  }
}
' *.txt
Run Code Online (Sandbox Code Playgroud)

注意:我有意放置ls -lhtrsystem部分中以查看它正在创建哪些输出文件(临时基础),因为稍后它会将它们重命名为它们的实际名称。

-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test2.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test1.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test3.txt
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out2
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out1
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out0
Run Code Online (Sandbox Code Playgroud)

当我们ls -lhtrawk 运行后执行脚本时,我们只能看到.txt那里的文件。

-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test2.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test1.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test3.txt
Run Code Online (Sandbox Code Playgroud)

说明:在此处添加上述命令的详细说明:

awk -v out_file="out" '                                    ##Starting awk program from here, creating a variable named out_file whose value SHOULD BE a name of files which are NOT present in our current directory. Basically by this name temporary files will be created which will be later renamed to actual files.
FNR==1{                                                    ##Checking condition if this is very first line of current Input_file then do following.
  close(out)                                               ##Using close function of awk here, because we are putting output to temp files and then renaming them so making sure that we shouldn't get too many files opened error by CLOSING it.
  out=out_file count++                                     ##Creating out variable here, whose value is value of variable out_file(defined in awk -v section) then variable count whose value will be keep increment with 1 whenever cursor comes here.
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"     ##Creating a variable named rename, whose work is to execute commands(rename ones) once we are done with processing all the Input_file(s), this will be executed in END section.
}                                                          ##Closing BLOCK for FNR==1  condition here.
{                                                          ##Starting main BLOCK from here.
  print "new_lines_here...." > (out)                       ##Doing printing in this example to out file.
}                                                          ##Closing main BLOCK here.
END{                                                       ##Starting END block for this specific program here.
  if(rename){                                              ##Checking condition if rename variable is NOT NULL then do following.
    system(rename)                                         ##Using system command and placing renme variable inside which will actually execute mv commands to rename files from out01 etc to Input_file etc.
  }
}                                                          ##Closing END block of this program here.
' *.txt                                                    ##Mentioning Input_file(s) with their extensions here.
Run Code Online (Sandbox Code Playgroud)


Ed *_*ton 5

如果我尝试这样做,我可能会选择这样的方法:

$ cat ../tst.awk
FNR==1 { saveChanges() }
{ print FNR > new }
END { saveChanges() }

function saveChanges(   bak, result, mkBackup, overwriteOrig, rmBackup) {
    if ( new != "" ) {
        bak = old ".bak"
        mkBackup = "cp \047" old "\047 \047" bak "\047; echo \"$?\""
        if ( (mkBackup | getline result) > 0 ) {
            if (result == 0) {
                overwriteOrig = "mv \047" new "\047 \047" old "\047; echo \"$?\""
                if ( (overwriteOrig | getline result) > 0 ) {
                    if (result == 0) {
                        rmBackup = "rm -f \047" bak "\047"
                        system(rmBackup)
                    }
                }
            }
        }
        close(rmBackup)
        close(overwriteOrig)
        close(mkBackup)
    }
    old = FILENAME
    new = FILENAME ".new"
}

$ awk -f ../tst.awk test1.txt test2.txt test3.txt
Run Code Online (Sandbox Code Playgroud)

我更愿意先将原始文件复制到备份,然后对保存对原始文件的更改进行操作,但这样做会更改每个输入文件的 FILENAME 变量的值,这是不希望的。

请注意,如果您的目录中有名为whatever.bak或 的原始文件whatever.new,那么您将用临时文件覆盖它们,因此您也需要为此添加测试。调用来mktemp获取临时文件名会更加可靠。

在这种情况下,更有用的东西是执行任何其他命令并执行“就地”编辑部分的工具,因为它可用于为 POSIX sed、awk、grep、tr 等提供“就地”编辑不需要您print > out每次想要打印值时都将脚本的语法更改为等。一个简单、脆弱的例子:

$ cat inedit
#!/bin/env bash

for (( pos=$#; pos>1; pos-- )); do
    if [[ -f "${!pos}" ]]; then
        filesStartPos="$pos"
    else
        break
    fi
done

files=()
cmd=()
for (( pos=1; pos<=$#; pos++)); do
    arg="${!pos}"
    if (( pos < filesStartPos )); then
        cmd+=( "$arg" )
    else
        files+=( "$arg" )
    fi
done

tmp=$(mktemp)
trap 'rm -f "$tmp"; exit' 0

for file in "${files[@]}"; do
    "${cmd[@]}" "$file" > "$tmp" && mv -- "$tmp" "$file"
done
Run Code Online (Sandbox Code Playgroud)

您可以按如下方式使用:

$ awk '{print FNR}' test1.txt test2.txt test3.txt
1
2
1
2
1
2

$ ./inedit awk '{print FNR}' test1.txt test2.txt test3.txt

$ tail test1.txt test2.txt test3.txt
==> test1.txt <==
1
2

==> test2.txt <==
1
2

==> test3.txt <==
1
2
Run Code Online (Sandbox Code Playgroud)

该脚本的一个明显问题inedit是,当您有多个输入文件时,很难从命令中单独识别输入/输出文件。上面的脚本假设所有输入文件在命令末尾显示为一个列表,并且该命令一次对它们运行一个,但这当然意味着您不能将它用于需要 2 个或更多文件的脚本一个时间,例如:

awk 'NR==FNR{a[$1];next} $1 in a' file1 file2
Run Code Online (Sandbox Code Playgroud)

或在 arg 列表中的文件之间设置变量的脚本,例如:

awk '{print $7}' FS=',' file1 FS=':' file2
Run Code Online (Sandbox Code Playgroud)

使其更加健壮,作为读者的练习,但将概要作为健壮性需要xargs如何工作的起点:-)。inedit