bash を使用して、上書きせずにログ ファイルのバッチの名前を段階的に変更するにはどうすればよいですか?

bash を使用して、上書きせずにログ ファイルのバッチの名前を段階的に変更するにはどうすればよいですか?

問題が与えられ、私の解決策は最初のテストケースに合格しましたが、送信時に 50% 失敗しました。

問題: ディレクトリには多数のファイルとフォルダが含まれており、これらのファイルの一部は異なるタイプのログです。error.log、error.log.1、error.log.2、access.log.1、access.log.2 など。これらのファイルの内容は翌日にマッピングされるため、「cat error.log.1」には「Day 2 logs」などがあります。

タスクは、ログの末尾の番号のみを増分し、ディレクトリの残りの内容は変更しないことです。また、ログの種類ごとに空のファイルを作成します。

例えば:

./
example_dir
example2_dir
error.log
error.log.1
info.log.20
access.log.1
readme.txt

スクリプトはディレクトリを次のように変更します:

./
example_dir (unchanged)
example2_dir (unchanged)
error.log (empty)
error.log.1 (originally error.log)
error.log.2 (originally error.log.1)
info.log (empty)
info.log.21 (originally info.log.20)
access.log (empty)
access.log.2 (originally access.log.1)
readme.txt (unchanged)

条件: ディレクトリ内のファイル数 < 1000、タイプごとの最大ファイル数 < 21

私の解決策:

#!/bin/bash

declare -a filenames

# Renaming in ascending order will lead to overwrite; so start rename from the bottom

files=$(find . -maxdepth 1 -name "*.log.*" -exec basename {} \; | sort -rn)


for i in $files; do

    currentFileNumber=$(echo -e "$i" | sed -e 's/[^0-9]*//g') # Extract the current number from the filename
    fileName=$(echo -e "$i" | sed -e 's/\.[0-9]*$//g') # Extract the name without the trailing number

    newFileNumber=$(("$currentFileNumber" + 1)) # Increment the current number

    mv "$i" "$fileName.$newFileNumber" # Rename and append the incremented value

    if [[ ! ${filenames[*]} =~ ${fileName} ]] # Store names of existing types to create empty files
    then
        filenames=("${filenames[@]}" "${fileName}")
    fi
    # Could make use of [[ -e "$fileName.log" ]] instead of an array, but won't pass the test for some reason
done

for j in "${filenames[@]}"; do touch "$j"; done # Create the empty files
unset filenames

失敗したテストケースが表示されなかったので、どうすればもっとうまく解決できるのかよくわかりません。

答え1

これは楽しい練習だったので、私の解決策は次のとおりです。

#/bin/bash
log_names=$(for logfile in $(find . -type f -name '*.log*'); do echo ${logfile%.[0-9]*}; done | sort -u)

for name in $log_names; do
    echo "Processing $name"
    i=20
    until [[ "$i" -eq 0 ]]; do
        if [[ -f "$name.$i" ]]; then
            next_num=$((i+1))
            mv -v "$name.$i" "$name.$next_num"
        fi
        i=$((i-1))
    done
    if [[ -f "$name" ]]; then
        mv -v "$name" "$name.1"
    fi
    touch "$name"
done

log_names 変数は、findコマンドを使用してログ ファイルのリストを取得します。次に、文字列置換を適用して数値サフィックスを削除します。その後、並べ替えて重複を削除します。

この時点で、ディレクトリ内の一意のログ ファイル名のリストが取得されます./access.log ./error.log ./info.log

次に、ループを使用してそれぞれの名前を順番に処理しますfor

現在、各ファイルについて、可能な最大数は 20 であると通知されています。そこから開始し、untilループを使用してカウントダウンします。

ロジックmvは単純です。「filname.number」が存在する場合は、「filename.(number+1)」に移動します。

ループが終了するとuntil(i = 0)、回転されていないファイルが 1 つ (数値サフィックスのないファイル) 残ります。その場合は、それを filename.1 に移動します。

最後のステップは、を使用して空のファイルを作成することですtouch


サンプル実行:

$ ls
access.log.1  error.log  error.log.1  example_dir  example2_dir  info.log.20  readme.txt  rotate.bash
    
$ bash rotate.bash
Processing ./access.log
'./access.log.1' -> './access.log.2'
Processing ./error.log
'./error.log.1' -> './error.log.2'
'./error.log' -> './error.log.1'
Processing ./info.log
'./info.log.20' -> './info.log.21'

$ ls -1
access.log
access.log.2
error.log
error.log.1
error.log.2
example_dir
example2_dir
info.log
info.log.21
readme.txt
rotate.bash

答え2

@Haxiel が解決策を投稿しました。これは私が考えていたものと似ており、私が「最も簡単」と説明したものです。ループforではなくループを使用したほうがよかったと思いますuntil

mvこれは、既存のファイルごとに 1つ、最後に新しいファイルを作成するために 1 つという、ほぼ最小限の数の外部プロセスを使用するものですtouch。(タッチは、リダイレクトを使用してファイルを作成するループに置き換えることができ、外部プロセスの数を 1 つ減らすことができます)。

#!/bin/bash
shopt -s nullglob # Reduce the number of things we have to work with

# get a list of the files we want to work with. 
files=( *.log *.log.[1-9] *.log.[1-9][0-9] )

# reverse the list into rfiles, getting rid of non-file things
rfiles=()
for ((i=${#files[@]}-1;i>=0;i--)) ; do
        if [ -f "${files[i]}" ] ; then
                rfiles+=("${files[i]}")
        fi
done

# exit early if there is nothing to do
if [ ${#rfiles[@]} -eq 0 ] ; then
        exit 0
fi

# an array of the files we need to create
typeset -A newfiles

# Loop over the reversed file list
for f in "${rfiles[@]}"; do
    # Get everything up to the last "log"
    baseName=${f%log*}log
    # Remove up to the last "log" and then the optional "."
    currentFileNum=${f#"$baseName"}
    currentFileNum=${currentFileNum#.}
    mv -v "$f" "$baseName.$((currentFileNum+1))"
    # record the name to make the new files
    newfiles[$baseName]=1
done

# Create all the needed new files, using the names stored in the array
touch "${!newfiles[@]}"

この処理の順序は、@Haxiel のソリューションで生成される順序とは異なります。これは、最初の部分が同じすべてのファイルをまとめて処理するのではなく、最初に 2 桁の番号を持つすべてのファイルを移動し、次に 1 桁の番号を持つすべてのファイルを移動し、最後に「.log」で終わるファイルを移動します。

元の質問では、ファイルの数が 1,000 未満で、ファイルあたりのバージョンが 21 未満であると述べられていました。この数を超える場合の対処法については説明されていませんでした。このソリューションは、ファイルあたり最大 100 バージョンに対応しており、パターンを拡張するだけで 1000 以上に拡張できます。

ファイルの数は、bash で使用可能なメモリの量によって制限されます。

これは、すべての名前に対して N 個のファイルを試すのではなく、存在するファイルのみを処理しようとするため、より優れたソリューションであると考えています。N が小さい場合 (21 など)、これは問題になりません。

関連情報