使用 NON GNU awk 保存修改

Save modifications in place with NON GNU awk

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

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

awk '{print "test here..new line for saving.."}' Input_file > temp && mv temp Input_file

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

对于这个问题我有什么tried/thought:它的方法是通过.txt文件的for循环和调用单个 awk 是一个痛苦且不推荐的过程,因为它会浪费不必要的 cpu 周期并且对于更多数量的文件它会更慢。

那么在这里可以做些什么来使用不支持就地选项的非 GNU awk 对多个文件执行就地编辑。我也经历了这个线程 Save modifications in place with awk 但是对于 NON GNU awk 副和在 awk 本身内更改多个文件没有什么太多的,因为非 GNU awk 没有 inplace 选项它。

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



编辑: 根据 Ed 先生的评论,在此处添加示例示例,尽管此线程代码的目的可以被通用使用目的就地编辑。

样本Input_file(s):

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest

预期输出示例:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2

因为这个帖子的主要目的是如何在非 GNU 中进行就地保存 awk 所以我首先发布它的模板,它将帮助任何有任何需求的人,他们需要 add/append他们代码中的 BEGINEND 部分按照他们的要求保留了他们的主要 BLOCK,然后应该进行就地编辑:

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

通用模板:

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


具体提供示例的解决方案:

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

awk -v out_file="out" '
FNR==1{
  close(out)
  out=out_file count++
  rename=(rename?rename ORS:"") "mv 7" out "7 7" FILENAME "7"
}
{
  print FNR > (out)
}
END{
  if(rename){
    system(rename)
  }
}
' *.txt

注意:这只是将编辑后的输出保存到 Input_file(s) 本身的测试,可以使用其 BEGIN 部​​分及其 END 部分他们的程序,主要部分应该根据具体问题本身的要求。

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



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

程序的执行示例: 让我们假设以下是 .txt Input_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

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

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

注意: 我在 system 部分有意放置 ls -lhtr 以查看它是哪些输出文件creating(temporary basis) 因为稍后它会将它们重命名为它们的实际名称。

-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

当我们在使用 运行ning 完成 awk 脚本后执行 ls -lhtr 时,我们只能在其中看到 .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


解释: 在此处添加对上述命令的详细解释:

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 7" out "7 7" FILENAME "7"     ##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.

shell 解决方案很简单而且可能足够快:

for f in *.txt
do  awk '...' "$f" > "$f.tmp"
    mv "$f.tmp" "$f"
done

仅当您最终证明这太慢时才搜索不同的解决方案。请记住:过早的优化是万恶之源。

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

$ 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 7" old "7 7" bak "7; echo \"$?\""
        if ( (mkBackup | getline result) > 0 ) {
            if (result == 0) {
                overwriteOrig = "mv 7" new "7 7" old "7; echo \"$?\""
                if ( (overwriteOrig | getline result) > 0 ) {
                    if (result == 0) {
                        rmBackup = "rm -f 7" bak "7"
                        system(rmBackup)
                    }
                }
            }
        }
        close(rmBackup)
        close(overwriteOrig)
        close(mkBackup)
    }
    old = FILENAME
    new = FILENAME ".new"
}

$ awk -f ../tst.awk test1.txt test2.txt test3.txt

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

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

在这种情况下,FAR 更有用的东西是可以执行任何其他命令并执行 "inplace" 编辑部分的工具,因为它可以用于为 [= 提供 "inplace" 编辑41=] 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

您将按如下方式使用:

$ 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

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

awk 'NR==FNR{a[];next}  in a' file1 file2

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

awk '{print }' FS=',' file1 FS=':' file2

让它更健壮作为 reader 的练习,但将 xargs 概要作为一个健壮的 inedit 需要如何工作的起点:-) .