ファイルリストから日付リストを抽出

awk command 2/N

公開日: 2022-08-08
更新日: 2023-09-28

  Table of Contents

Problem

カレントディレクトリにyyyy-mm-dd-<description>.mdというファイルが以下のように存在しているとします.

1
2
3
4
5
% find . -type f -printf "%f\n"
2021-02-01-order-pizza.md
2021-01-01-Linux-is-awesome.md
2021-07-28-Ubuntu-so-kind.md
2021-03-03-shougi.md

このときやりたいことは以下,

  1. このファイル一覧からyyyy-mm-ddの形式でsortされた日付リストを取得
1
2
3
4
5
% CMD ./
2021-01-01
2021-02-01
2021-03-01
2021-07-28
  1. 上記で得られたリストから任意の2つの日付の間のdateのみ取得
1
2
3
% CMD ./ 2021-01-02 2021-02-01
2021-01-01
2021-02-01

Solution 1: ファイル一覧からyyyy-mm-ddの形式でsortされた日付リストを取得

カレントディレクトリからファイル一覧のみを取得するためにはfindコマンドを以下のように利用すれば良い:

1
% find . -type f

ここから日付リストを取得する方法として,

  1. シンプルにgrepを用いる
  2. awk + matchを用いる

の2つが考えられます.

1
2
3
4
5
## grepを用いる場合
% find . -type f -printf "%f\n" | grep -Eo '^[0-9]{4}-[0-9]{2}-[0-9]{2}'

## awk & matchを用いる場合
% find $1 -type f -printf "%f\n" | awk 'match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2}/){print substr($0, RSTART, RLENGTH)}'

awk: RLENGTH and RSTART

awkコマンドで利用可能なmatch関数は, input文字列を対象に /pattern(regex also available)/ の検索機能を提供してくれます.

1
2
3
4
5
6
## 以下の2つは同じ結果を返す
% echo "unko\nkirby" | awk 'match($0, /un/)' 
unko

% echo "unko\nkirby" | grep 'un'
unko

match関数はmatchした場合, RSTART and RLENGTHという変数を設定します.

RSTART 検索に合致する文字列の開始場所
RLENGTH 検索に合致する文字列の長さ

この2つの変数とsubstr関数を組み合わせることでgrep -oと似た挙動を再現することができます.

1
2
3
% echo '2090-09-01-aaaabbbb\n2001-09-01-kkkkk' | awk 'match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2}/){print substr($0, RSTART, RLENGTH)}'
2090-09-01
2001-09-01

Solution 2: 上記で得られたリストから任意の2つの日付の間のdateのみ取得

上記のコマンドで日付リストは取得できています. そこからfilterをする方法として awkの変数定義とgensub関数を利用する方法があります

1
2
3
4
5
6
7
8
9
10
11
## filter date between 2000-08-01 and 2012-08-31
% echo '2090-09-01\n2010-09-01\n2011-09-01' | awk -v start='2000-08-01'\
 -v end='2012-08-31' -F= '{a=gensub("-", "","g", $1);\
  gsub("-", "", start); gsub("-", "", end)} (a >= start) && (a <= end)'|sort
2010-09-01
2011-09-01

% echo '2090-09-01\n2010-09-01\n2011-09-01' | awk -F= 'BEGIN{start="2000-08-01"; end="2012-08-31"}\
  {a=gensub("-", "","g", $1); gsub("-", "", start); gsub("-", "", end)} (a >= start) && (a <= end)'|sort
2010-09-01
2011-09-01

変数定義

awkコマンドでは-vオプションやBEGIN{}を組み合わせることでawkコマンド内部で利用可能となる変数 の定義ができます.

1
2
3
4
5
## -vによる変数定義
awk -v varname=value

## BEGIN{}での
awk 'BEGIN { varname=value }'

awkに与えられた変数を利用して新たな変数を作成する場合はgensub()関数を用います

1
2
3
4
5
6
7
8
9
10
## syntax
gensub("置換前", "置換後", "変更箇所(基本は数値, gならばすべて)", 対象カラム)

## yyyy-mm-ddをyyyy/mm/ddへ変更
% echo "2020-09-01" | awk '{date=gensub("-", "/", "g", $1)}{print date}' 
2020/09/01

## 1つ目の-のみ置換
% echo "2020-09-01" | awk '{date=gensub("-", "/", 1, $1)}{print date}' 
2020/09-01

日付の大小関係を評価する

awkでは条件式に基づいた評価が可能ですが, 日付は文字列として認識されるのでそのままでは大小関係を評価することはできません. 幸いにも今回は日付形式がyyyy-mm-ddなのでyyyymmddと数値変換しても大小関係は同じになるのでこの特徴を利用します.

方針としては

  • start, endyyyy-mm-ddからyyyymmddへ変換する
  • inputとして読み込まれるyyyy-mm-ddをベースにyyyymmdd形式の変数を新たに定義する
  • 条件式にあったyyyy-mm-ddを返す

その解答が以下:

1
2
% echo '2090-09-01\n2010-09-01\n2011-09-01' | awk -F= 'BEGIN{start="2000-08-01"; end="2012-08-31"}\
  {a=gensub("-", "","g", $1); gsub("-", "", start); gsub("-", "", end)} (a >= start) && (a <= end)'|sort

gsub("-", "", 変数)はglobalにsub(=置換)する関数です. 変換位置を”g”に指定したgensubの挙動と似ていますが, これは変数そのものを変化させる関数です. 今回はstart, end変数は出力させる予定はないので, 直接gsubで形式を変換しています.

変換後, (a >= start) && (a <= end)評価式に合致するレコードのみを出力するという流れになっています

Column: mm/dd/yyyy 形式の場合

mm/dd/yyyyの場合はそのままmmddyyyyと変換して大小関係を比較すると厄介なことが起きます. このような場合は,

  • yyyy要素
  • mm要素
  • dd要素

を個別に取得して, 順番を入れ替えた上でyyyymmddと変換することが必要です. これはawkの配列機能を利用することで実現できます. 詳しくは説明しませんが, 解答例は以下

1
2
3
% find . -type f | awk 'match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/){print substr($0, RSTART, RLENGTH)}'\
    | awk -v start='2022-08-02' -v end='2022-08-31' -F= '{split($1, a, /-/); split(start, b, /-/);\
          split(end, c, /-/)} (a[1]a[2]a[3] >= b[1]b[2]b[3]) && (a[0]a[1]a[2] <= c[0]c[1]c[2])'| sort

Summary

これをシェルスクリプトに落とし込むと

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
## get date list from start to end
## Author: Ryo Nakagami
## Revised: 2023-08-09

set -e

# var
SEARCH_PATH=$1
START=$2
END=$3

# Functions
function args_error {
    echo 'fatal: start or end missing, date format is yyyymmdd'
    exit 1
}

# Main
if [[ -z $3  ]]; then
    args_error
fi

find $SEARCH_PATH -type f -printf "%f\n" \
| grep -Eo '[0-9]{4}-[0-9]{2}-[0-9]{2}'\
| awk -v start=$START -v end=$END\
      -F= '{date=gensub("-", "", "g", $0); gsub("-", "", start); gsub("-", "", end)}\
           (date >= start) && (date <= end)'\
| sort

References



Share Buttons
Share on:

Feature Tags
Leave a Comment
(注意:GitHub Accountが必要となります)