-
Notifications
You must be signed in to change notification settings - Fork 0
/
automtime
executable file
·1314 lines (1154 loc) · 42.5 KB
/
automtime
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
# automtime - automatically get modification time from internal file metadata
#
# Copyright (c) 2023-2024 Dan Fandrich <[email protected]>
# Licensed under the MIT license (see LICENSE).
###########################
# Print a shell-quoted version of the first argument
shquote () {
printf '%s' "$1" | awk -v q="'" '{gsub(q, q "\\" q q, $0); printf "%s", q $0 q;}'
}
# Use the time program to normalize an input time without time zone into the
# canonical form. This implies that the time is relative to local time and is
# not absolute. The input form is anything date can handle and the output is
# like:
# 2012-01-23 10:09:08.901234
# Note that not all date programs support all dates that might be passed in.
# GNU date supports them all but Busybox date, for example, only supports a
# small number of numeric-only date formats. OS X & BSD date force the caller
# to specify the date format being given, and use different arguments.
# Solaris date doesn't parse arbitrary dates at all.
# The Python fallback code is tried if the regular date returns an error, and,
# while it isn't as powerful as GNU date, it does a decent job (but only if
# the external dateutil and pytz modules are available).
normalize_time () {
test -z "$1" -o "$1" = "@" -o "$1" = "@0" -o "$1" = "@0.0" && return
date --date="$1" '+%04Y-%m-%d %H:%M:%S' 2>/dev/null || { python3 -c 'import datetime, dateutil.parser, sys; print((datetime.datetime.fromtimestamp(float(sys.argv[1][1:])) if sys.argv[1][0] == "@" else dateutil.parser.parse(sys.argv[1])).astimezone().strftime("%Y-%m-%d %H:%M:%S"))' "$1"; }
}
# Use the time program to normalize an input time with time zone (or at least
# an absolute time) into the canonical form. The input form is anything date
# can handle and the output is like:
# 2012-01-23 10:09:08.901234 +0800
# The output can't preserve the original time zone because "date" always
# returns the time offset for the current time zone. The Python fallback code
# actually does the right thing here.
normalize_time_tz () {
test -z "$1" -o "$1" = "@" -o "$1" = "@0" -o "$1" = "@0.0" && return
date --date="$1" '+%04Y-%m-%d %H:%M:%S %z' 2>/dev/null || {
python3 -c '
import datetime, dateutil.parser, pytz, sys
if sys.argv[1][0] == "@":
localtz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
d = datetime.datetime.fromtimestamp(float(sys.argv[1][1:]), tz=localtz)
else:
pyz = {x:pytz.timezone(x) for x in pytz.all_timezones}
rfc822z = {abbr:pyz[f"Etc/GMT{-off:+}"] for abbr,off in {
"UT":0,"Z":0,"AST":-4,"ADT":-3,"EST":-5,"EDT":-4,"CST":-6,"CDT":-5,
"MST":-7,"MDT":-6,"PST":-8,"PDT":-7}.items()} # tz abbr. from RFC 822
d = dateutil.parser.parse(sys.argv[1], tzinfos={**pyz, **rfc822z})
print(d.strftime("%Y-%m-%d %H:%M:%S %z").rstrip())
' "$1"
}
}
# Works like normalize_time but returns the given time as seconds past 1970.
epoch_time () {
test -z "$1" -o "$1" = "@" -o "$1" = "@0" -o "$1" = "@0.0" && return
date --date="$1" '+%s' 2>/dev/null || { python3 -c 'import dateutil.parser, sys; print(sys.argv[1][1:] if sys.argv[1][0] == "@" else float(dateutil.parser.parse(sys.argv[1]).timestamp()))' "$1"; }
}
# Use the time program to normalize a ISO 8601 input time into the
# canonical form, preserving time zones if possible.
# The input form is like:
# 2012-01-23
# 2009-10-11T12:13:14
# 2012-01-23T13:14:15Z
# 2012-01-23T13:14:15-0100
# 2012-01-23T13:14:15+08:00
normalize_iso_time () {
# Delete any CR characters so end-of-line matches work
printf '%s' "$1" | tr -d '\015' | sed -Ee 's/([0-9])T([0-2])/\1 \2/' -e 's/Z$/+0000/' -e 's/([-+][0-9][0-9])(:)?([0-9][0-9])$/ \1\3/'
}
# Make a filename starting with a dash - safe to provide a program that
# would interpret it as an option.
safefn () {
case "$1" in
-*) echo "./$1" ;;
*) echo "$1" ;;
esac
}
###########################
#
# mtime extraction functions
#
# Each function takes an argument of the file name and results in the TIME
# variable holding a time in the format:
# 2012-01-23 10:09:08.901234 -0700
# if the time contains a known time zone, or:
# 2012-01-23 10:09:08.901234
# if it does not (local time). The decimal portion of the seconds is optional.
#
# Sometimes the time is shown in the local time zone instead of in the original
# time zone, but the actual point in time will be accurate (e.g. a time in UTC
# is sometimes shown as the equivalent time in the local time zone).
# File type: 7zip (7zip archive)
# requires: p7zip
mtime_7zip () {
# The mtime is considered to be the latest file time in the archive.
RAWTIME="$(7za l -slt -- "$1" | sed -E -n -e 's/^Modified = //p' | sort -d | tail -1)"
# $RAWTIME is like 2021-10-14 12:48:03
TIME="$RAWTIME"
}
# File type: abw (AbiWord document)
# requires: xmlstarlet
mtime_abw () {
RAWTIME=$(xmlstarlet sel -N a=http://www.abisource.com/awml.dtd -t -v "/a:abiword/a:history/@last-saved" < "$1" 2>/dev/null)
# $RAWTIME is like 1234567890
if [ -n "$RAWTIME" ] ; then
TIME=$(normalize_time_tz "@$RAWTIME")
else
# This one does not include a time zone so is less desirable
RAWTIME=$(xmlstarlet sel -N a=http://www.abisource.com/awml.dtd -t -v "/a:abiword/a:metadata/a:m[@key='dc.date']" < "$1" 2>/dev/null)
# $RAWTIME is like Mon Apr 4 17:31:07 2022
TIME=$(normalize_time "$RAWTIME")
fi
}
# File type: dat (Allegro4 not packed datafile)
# See https://liballeg.org/
# requires: allegro4
mtime_allegro4 () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
# There is also a packed Allegro4 datafile type with the same
# extension, but there seems to be no metadata within the file to
# display without uncompressing the file.
RAWTIME="$(dat -l -v "$sf" | sed -E -n -e "s/'//g" -e 's/ . DATE //p')"
# $RAWTIME is like 10-13-2021, 7:53
# The second sed here is to zero-prefix the time, when necessary
TIME="$(echo "$RAWTIME" | sed -E -e 's/^([0-9][0-9])-([0-9][0-9])-([0-9][0-9][0-9][0-9]), *([0-9]+:[0-9][0-9]).*$/\3-\1-\2 \4/' | sed -E -e 's/^(.{10}) ([0-9]):/\1 0\2:/')"
}
# File type: amf (Additive Manufacturing File)
# requires: file, unzip (Info-ZIP version), findutils
mtime_amf () {
# Not all amf files are zipped but those that aren't have no other mtime
# and must return nothing
if file - < "$1" | grep " Zip " >/dev/null; then
mtime_zip "$1"
fi
}
# File type: ar (ar archive)
# requires binutils
mtime_ar () {
# The mtime is considered to be the latest file time in the archive.
# The date format can not, unfortunately, be easily sorted as-is, so it
# is converted to epoch time, sorted and only the most recent is used.
# The real raw time from ar is like: Jan 12 11:56 2023
RAWTIME=$(ar tv -- "$1" | awk '{print $4 " " $5 " " $6 " " $7}' | uniq | tr '\n' '\0' | xargs -0 -n1 "$AUTOMTIME" --epoch_time | sort -n | tail -1)
# $RAWTIME is like 1234567890
# The time is stored absolute, but is displayed in the local time zone
TIME=$(normalize_time_tz "@$RAWTIME")
}
# File type: arj (arj archive)
# requires: unarj || arj
mtime_arj () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME="$( (unarj l "$sf" 2>/dev/null || arj l -- "$1") | sed -E -n -e 's/^Archive created:.*modified: //p')"
# $RAWTIME is like 2002-03-04 05:06:07
# Note that unarj ver. 2.65 has a bug with date handling and dates around
# 2022 are wrong by almost a decade.
TIME="$RAWTIME"
}
# File type: avif (AVIF image)
# requires: exiftool
mtime_avif () {
# Uses exiftool that works just as well for this format
mtime_heif "$@"
}
# File type: cab (Microsoft Cabinet archive)
# requires: cabextract
mtime_cab () {
# The mtime is considered to be the latest file time in the archive.
# The date format can not, unfortunately, be easily sorted as-is, so it
# is converted, sorted and only the most recent is used.
# The real raw time from cabextract is like: 18.02.2021 17:10:92
RAWTIME="$(cabextract -l -- "$1" | sed -e '1,/^-----------/d' -e '/^$/,$d' | awk '{print $3 " " $4}' | sed -E -e 's/^([0-3][0-9])\.([01][0-9])\.([12][0-9][0-9][0-9])/\3-\2-\1/' | sort -d | tail -1)"
# $RAWTIME is like 2002-03-04 05:06:07
TIME="$RAWTIME"
}
# File type: cpio (CPIO Archive)
# requires: busybox
# Busybox is the only program I found that always returned both the date and
# time of each file in the archive, and did so in a consistent format that is
# easy to parse.
mtime_cpio () {
# The mtime is considered to be the latest file time in the archive.
RAWTIME=$(busybox cpio -itv <"$1" | awk '{print $4 " " $5}' | sort -d | tail -1)
# $RAWTIME is like 21-10-13 01:34:12
# The time is stored absolute, but is displayed in the local time zone
TIME="$RAWTIME"
}
# File type: cpiogz (gzip-compressed CPIO Archive)
# requires: busybox, gzip
# Busybox is the only program I found that always returned both the date and
# time of each file in the archive, and did so in a consistent format that is
# easy to parse.
mtime_cpiogz () {
# The mtime is considered to be the latest file time in the archive.
RAWTIME=$(gzip -dc -- "$1" | busybox cpio -itv | awk '{print $4 " " $5}' | sort -d | tail -1)
# $RAWTIME is like 21-10-13 01:34:12
# The time is stored absolute, but is displayed in the local time zone
TIME="$RAWTIME"
}
# File type: dar (Disk Archiver archive)
# See http://dar.linux.free.fr/
# requires: dar >= 2.7.0, xmlstarlet
mtime_dar () {
RAWTIME=$(LC_ALL=C dar -Q -T xml -l "$1" 2>/dev/null | xmlstarlet sel -t -v '/Catalog/Directory//Attributes/@mtime' -nl -v '/Catalog/File//Attributes/@mtime' -nl 2>/dev/null | uniq | sort -n | tail -1)
# $RAWTIME is like 1234567890
TIME=$(normalize_time_tz "@$RAWTIME")
}
# File type: docbook (DocBook document)
# requires: xmlstarlet
mtime_docbook () {
# Docbook document
RAWTIME=$(xmlstarlet sel -t -v /book/bookinfo/date < "$1" 2>/dev/null)
if [ -z "$RAWTIME" ] ; then
# Docbook man page
RAWTIME=$(xmlstarlet sel -t -v /refentry/refentryinfo/date < "$1" 2>/dev/null)
fi
# $RAWTIME is usually human generated so is fairly free form
TIME="$(normalize_time "$RAWTIME")"
}
# File type: doc (Microsoft composite document)
# requires: file
mtime_doc () {
RAWTIME=$(file - < "$f" | sed -n -e 's@^.*Last Saved Time/Date: \([^,]\+\)\>.*$@\1@p')
# First revision sometimes doesn't included Last Saved, so use Create then
if [ -z "$RAWTIME" ] ; then
RAWTIME=$(file - < "$f" | sed -n -e 's@^.*Create Time/Date: \([^,]\+\)\>.*$@\1@p')
fi
# $RAWTIME is like Tue Mar 23 12:34:56 2010
TIME="$(normalize_time "$RAWTIME")"
}
# File type: docx (Microsoft Office Open XML)
# requires: unzip, xmlstarlet
mtime_docx () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(unzip -pq "$sf" docProps/core.xml | xmlstarlet sel -t -v /cp:coreProperties/dcterms:modified)
# $RAWTIME is like 2012-01-23T13:14:15Z
# Some programs incorrectly designate a time as UTC instead of local time,
# but there's nothing we can do to know that (in the general case).
TIME="$(normalize_iso_time "$RAWTIME")"
}
# File type: exe (Microsoft Windows PE executable)
# File type: dll (Microsoft Windows PE dynamic link library)
# requires: python >= 3, pefile (see https://github.com/erocarrera/pefile/)
mtime_exe () {
RAWTIME=$(python3 -c '
import pefile,sys
try:
pe=pefile.PE(data=sys.stdin.buffer.read())
if hasattr(pe, "FILE_HEADER") and hasattr(pe.FILE_HEADER, "TimeDateStamp"):
print(pe.FILE_HEADER.TimeDateStamp)
except pefile.PEFormatError:
pass # probably an old-style file
' < "$1")
# The time is stored absolute
TIME=$(normalize_time_tz "@$RAWTIME")
}
# File type: email (E-mail or similar message)
mtime_email () {
RAWTIME=$(sed -n -e '1,/^$/s/^[Dd][Aa][Tt][Ee]: *//p' < "$1")
# $RAWTIME is like Tue, 12 Oct 2021 12:34:56 +0000 or another RFC-2822
# style date.
# The time is stored absolute
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: fodf (Open Document Format flat file)
# requires: xmlstarlet
mtime_fodf () {
RAWTIME=$(xmlstarlet sel -t -v /office:document/office:meta/dc:date < "$1" 2>/dev/null)
# $RAWTIME is like 2009-10-11T12:13:14
TIME="$(normalize_iso_time "$RAWTIME")"
}
# File type: gcode (G-code machine control file)
# gcode doesn't include a code to embed a date, but some creators include
# a date in a comment. Many seem to use an ambiguous locale-dependent date
# format that might be different depending on where the file was written, so
# those are not parsed here to avoid errors.
mtime_gcode () {
# PrusaSlicer & Slic3r, PyCAM
RAWTIME=$(sed -n -E -e '1s/^; generated.* on ([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) +(at)?( [0-9][0-9]:[0-9][0-9]:[0-9][0-9])( +[A-Z0-9]+)?$/\1\3\4/p' -e 's/;PYCAM-META-DATA: Timestamp: //p' < "$1")
# $RAWTIME is like 2023-01-15 21:57:53 UTC or 2021-09-11 13:29:21 or 2012-07-06 03:55:01.829000
# The time is stored absolute in only some formats. Don't show the time
# zone here since it might actually not be known and that could be
# confusing.
TIME="$(normalize_time "$RAWTIME")"
}
# File type: gnumeric (Gnumeric spreadsheet)
# requires: xmlstarlet
mtime_gnumeric () {
RAWTIME=$(xmlstarlet sel -N gnm=http://www.gnumeric.org/v10.dtd -N office=urn:oasis:names:tc:opendocument:xmlns:office:1.0 -N dc=http://purl.org/dc/elements/1.1/ -t -v '/gnm:Workbook/office:document-meta/office:meta/dc:date' < "$1")
# $RAWTIME is like 2022-04-04T17:07:51Z
TIME="$(normalize_iso_time "$RAWTIME")"
}
# File type: gzip (gzip-compressed file)
# requires: python >= 3
mtime_gz () {
RAWTIME=$(python3 - "$1" <<EOF
import struct, sys
with open(sys.argv[1], 'rb') as f:
magic, _, epoch = struct.unpack('<HHI', f.read(8))
if magic!=0x8b1f: print('Not a gzip archive', file=sys.stderr); sys.exit(1);
if epoch != 0:
print(epoch)
EOF
)
# $RAWTIME is like 1234567890 or empty if the time was 0
TIME=$(normalize_time_tz "@$RAWTIME")
}
# File type: heif (High Efficiency Image Format)
# requires: exiftool
mtime_heif () {
# There are many possible fields that could be used as mtime. If a "modify"
# one exists, use that in preference to others by sorting. The odd sed
# replacement with AA and sort keys ensures that.
RAWTIME=$(LC_ALL=C exiftool -- "$1" | grep -E '^((Date/Time Original)|((Create|Modify) Date))' | sed 's@/@AA@' | sort -k1.6 | tail -1 | sed 's/^.*: //' )
# $RAWTIME is like 2022:03:14 23:52:52 or 2023:04:05 06:07:08.0900000333786011Z (the
# Z in the latter may be a bug)
TIME="$(echo $RAWTIME | sed -E -e 's/^((20|19|00)[0-9][0-9]):([01][0-9]):/\1-\3-/' -e 's/Z$//' )"
if [ -z "$(echo "$TIME" | sed 's/[-:0 ]//g' )" ]; then
# Don't return an empty time like "0000-00-00 00:00:00"
TIME=""
fi
}
# File type: ics (iCalendar file)
mtime_ics () {
# The most recent date stamp or last-modified entry in the file is used
RAWTIME=$(sed -E -n -e '/^(LAST-MODIFIED|DTSTAMP):/s/^[^:]+: *//p' < "$1" | sort -d | tail -1)
# $RAWTIME is like 20180213T070722Z
# Make it look like ISO-8601, then convert
RAWTIME=$(echo "$RAWTIME" | sed -E -e 's/([12][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-2][0-9])([0-9][0-9])([0-9][0-9])/\1-\2-\3T\4:\5:\6/')
TIME=$(normalize_iso_time "$RAWTIME")
}
# File type: iso (ISO-9660 CD-ROM image)
# requires: python >= 3
mtime_iso () {
# Obtaining the "Volume Creation Date and Time" the hard way, since isoinfo
# doesn't seem to extract it for us.
# The raw value is like: 2022021110514800o where o is a signed 8-bit offset
# from GMT in 15 minute increments.
TIME=$(python3 - "$1" <<EOF
import os, struct, sys
with open(sys.argv[1], 'rb') as f:
f.seek(33581)
y,mo,d,h,m,s,s100, offset15 = struct.unpack('4s2s2s2s2s2s2sb', f.read(17))
if not y.isdigit() or not mo.isdigit() or not d.isdigit() or not h.isdigit() or not m.isdigit() or not s.isdigit() or not s100.isdigit():
os.exit(1)
offset_h = offset15/4
offset_m = int(60*(offset_h - int(offset_h)))
offset_hm = 100*int(offset_h) + offset_m
print(f'{y.decode()}-{mo.decode()}-{d.decode()} {h.decode()}:{m.decode()}:{s.decode()}.{s100.decode()} {offset_hm:+05d}')
EOF
)
}
# File type: jpeg (JPEG JFIF image)
# requires: exif
# TODO: also look at XMP and IPTC metadata
mtime_jpeg () {
# Look through several possible tags in order of most likely to hold the
# most recent modification time.
RAWTIME=$(exif --ifd=0 --tag=DateTime -m -- "$1" 2>/dev/null)
if [ -n "$RAWTIME" ] ; then
RAWSUBSEC=$(exif --ifd=EXIF --tag=SubsecTime -m -- "$1" 2>/dev/null)
if [ -n "$RAWSUBSEC" ] ; then
RAWTIME="${RAWTIME}.$RAWSUBSEC"
fi
# TimeZoneOffset is part of TIFF/EP, not EXIF, but you still find
# it occasionally in EXIF images
TZOFFSET=$(exif --ifd=0 --tag=TimeZoneOffset -m -- "$1" 2>/dev/null | sed -E -n -e 's/^[^,]+, *([0-9]+).*$/\1/p')
if [ -n "$TZOFFSET" ] ; then
RAWTIME="$(printf '%s %+03d00' "$RAWTIME" "$TZOFFSET")"
fi
fi
if [ -z "$RAWTIME" ] ; then
RAWTIME=$(exif --ifd=EXIF --tag=DateTimeDigitized -m -- "$1" 2>/dev/null)
if [ -n "$RAWTIME" ] ; then
RAWSUBSEC=$(exif --ifd=EXIF --tag=SubSecTimeDigitized -m -- "$1" 2>/dev/null)
if [ -n "$RAWSUBSEC" ] ; then
RAWTIME="${RAWTIME}.$RAWSUBSEC"
fi
fi
fi
if [ -z "$RAWTIME" ] ; then
RAWTIME=$(exif --ifd=EXIF --tag=DateTimeOriginal -m -- "$1" 2>/dev/null)
if [ -n "$RAWTIME" ] ; then
RAWSUBSEC=$(exif --ifd=EXIF --tag=SubSecTimeOriginal -m -- "$1" 2>/dev/null)
if [ -n "$RAWSUBSEC" ] ; then
RAWTIME="${RAWTIME}.$RAWSUBSEC"
fi
# TimeZoneOffset is part of TIFF/EP, not EXIF, but you still find
# it occasionally in EXIF images
TZOFFSET=$(exif --ifd=0 --tag=TimeZoneOffset -m -- "$1" 2>/dev/null | sed -E -n -e 's/^([0-9]+).*$/\1/p')
if [ -n "$TZOFFSET" ] ; then
RAWTIME="$(printf '%s %+03d00' "$RAWTIME" "$TZOFFSET")"
fi
fi
fi
if [ -z "$RAWTIME" ] ; then
RAWTIME=$(exif --ifd=GPS --tag=GPSDateStamp -m -- "$1" 2>/dev/null)
if [ -n "$RAWTIME" ] ; then
RAWDATE="$(echo "$RAWTIME" | sed 's/:/-/g')"
RAWTIME=$(exif --ifd=GPS --tag=GPSTimeStamp -m -- "$1" 2>/dev/null)
if [ -n "$RAWTIME" ] ; then
RAWTIME="$RAWDATE $RAWTIME +0000"
else
RAWTIME="$RAWDATE +0000"
fi
fi
fi
# $RAWTIME is like 2021:10:14 13:02:32 or 2021:10:14 13:02:32.45 +0000
TIME=$(echo "$RAWTIME" | sed -E -e 's/^([0-9][0-9][0-9][0-9]):([0-9][0-9]):/\1-\2-/')
}
# File type: journal (journald log file)
# requires: systemd
mtime_journal () {
RAWTIME=$(TZ=UTC journalctl --header --file "$f" | sed -E -n 's/Tail realtime timestamp: (.*) \(.*\)$/\1/p')
# $RAWTIME is like Sat 2024-08-24 14:33:29 UTC
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: jxl (JPEG XL)
# requires: exiftool
mtime_jxl () {
# Uses exiftool that works just as well for this format
mtime_heif "$@"
}
# File type: kicad (Kicad schematic)
mtime_kicad () {
RAWTIME=$(sed -n -e '1,/(title_block/d' -e 's/^[[:space:]]*(date "\(.*\)")$/\1/p' -e '/^[[:space:]]*)$/,$d' < "$1")
# $RAWTIME is like 2021-01-03
TIME=$(normalize_time "$RAWTIME")
}
# File type: kra (Krita image)
# requires: unzip, xmlstarlet
mtime_kra () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(unzip -pq "$sf" documentinfo.xml | xmlstarlet sel -N d=http://www.calligra.org/DTD/document-info -t -v /d:document-info/d:about/d:date 2>/dev/null )
# $RAWTIME is like 2022-01-01T13:57:46
TIME="$(normalize_iso_time "$RAWTIME")"
}
# File type: lzh (lzh archive)
# requires: lha
mtime_lzh () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
# The mtime is considered to be the latest file time in the archive.
# File name lines are interleaved with metadata lines, so the complicated
# sed expression is needed to find the metadata lines and weed out the file
# names.
RAWTIME="$(lha -vv "$sf" | sed -E -n -e 's/^[-rwxs]{10} *[0-9/]+ *[0-9]+ *[0-9]+ *[0-9.%]* *..... *[0-9a-f]{4} ([12][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-2][0-9]:[0-9][0-9]:[0-9][0-9]).*$/\1/p' | sort -d | tail -1)"
# $RAWTIME is like 2021-10-14 23:09:47
TIME="$RAWTIME"
}
# File type: lzo (lzo archive)
# requires lzop
mtime_lzo () {
# The mtime is considered to be the latest file time in the archive.
RAWTIME=$(lzop -lvv -- "$1" | awk '{print $2 " " $3}' | sort -d | tail -1)
# $RAWTIME is like 2021-10-11 20:50:26
# The time is stored absolute, but is displayed in the local time zone
TIME="$RAWTIME"
}
# File type: appdata (Appdata metainfo file)
# requires: xmlstarlet
mtime_appdata () {
RAWTIME=$(xmlstarlet sel -t -v '/component/releases/release/@date' < "$1" | sort -d | tail -1)
# $RAWTIME is like 2021-10-11
TIME="$RAWTIME"
}
# File type: mkv (Matroska video)
# requires: exiftool
mtime_mkv () {
# Uses exiftool that works just as well for this format
mtime_mov "$@"
}
# File type: mov (QuickTime video)
# requires: exiftool
mtime_mov () {
# There are many possible fields that could be used as mtime. If a "modify"
# one exists, use that in preference to others by sorting. The odd sed
# replacement with AA and sort keys ensures that.
RAWTIME=$(LC_ALL=C exiftool -- "$1" | grep -E '^((Date/Time Original)|(Creation Date)|(Track (Create|Modify) Date)|(Media (Create|Modify) Date))' | sed 's@/@AA@' | sort -k1.6 | tail -1 | sed 's/^.*: //' )
# $RAWTIME is like 2022:03:14 23:52:52 or 2023:04:05 06:07:08.0900000333786011Z (the
# Z in the latter may be a bug)
TIME="$(echo $RAWTIME | sed -E -e 's/^((20|19|00)[0-9][0-9]):([01][0-9]):/\1-\3-/' -e 's/Z$//' )"
if [ -z "$(echo "$TIME" | sed 's/[-:0 ]//g' )" ]; then
# Don't return an empty time like "0000-00-00 00:00:00"
TIME=""
fi
}
# File type: odf (Open Document Format)
# requires: unzip, xmlstarlet
mtime_odf () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(unzip -pq "$sf" meta.xml | xmlstarlet sel -t -v /office:document-meta/office:meta/dc:date 2>/dev/null)
# $RAWTIME is like 2009-10-11T12:13:14
TIME="$(normalize_iso_time "$RAWTIME")"
if [ -n "$RAWTIME" -a "$TIME" = "$RAWTIME" ]; then
# Time was not in ISO format. Old Star Office files may do this, so
# try a fallback normalization just in case.
TIME=$(normalize_time "$RAWTIME")
fi
}
# File type: patch (unified diff style patch)
mtime_patch () {
# git-style format-patch
RAWTIME=$(sed -n -e '1,/^$/s/^[Dd][Aa][Tt][Ee]: *//p' < "$1")
# $RAWTIME is usually like Wed, 18 Jun 2014 17:06:32 -0400
if [ -z "$RAWTIME" ]; then
# The standard diff -u output includes the mtime of each file.
# Use the most recent file date included in the diff.
# The date format can not, unfortunately, be easily sorted as-is, so it
# is converted to epoch time, sorted and only the most recent is used.
# The real raw time from diff is usually like:
# 2023-04-01 13:18:48.718247759 -0700
# but is sometimes not supplied at all.
RAWTIME=$(sed -nE -e "s/^\+\+\+ .*$(printf '\t')([^(])/\1/p" < "$1" | tr '\n' '\0' | xargs -0 -n1 "$AUTOMTIME" --epoch_time | sort -n | tail -1)
# $RAWTIME is like 1234567890
TIME=$(normalize_time_tz "@$RAWTIME")
else
TIME=$(normalize_time_tz "$RAWTIME")
fi
}
# File type: pcap (Pcap network capture file)
# requires: wireshark-cli || wireshark-tools
# Wireshark handles a lot more packet capture file formats that are all handled
# here.
mtime_pcap () {
RAWTIME=$(capinfos -- "$1" | sed -n -e 's/^Last packet time: *//p')
if [ -z "$RAWTIME" ]; then
# In case capinfos isn't available, use the slower tshark instead
RAWTIME=$(tshark -t ad -r "$1" | awk '{print $2 " " $3}' | uniq | sort -d | tail -1)
fi
# $RAWTIME is like 2022-07-05 13:41:26.333333
# The time is stored absolute, but is displayed in the local time zone
TIME="$RAWTIME"
}
# File type: palm (Palm Pilot file)
# requires: pilot-tools
mtime_palm () {
RAWTIME=$(pilot-file -H -- "$1" | sed -n -e 's/^modified_time\.: *//p')
# $RAWTIME is like 2008-07-07 08:40:50
TIME=$(normalize_time "$RAWTIME")
}
# File type: pdf (Portable Document Format)
# requires: poppler
mtime_pdf () {
RAWTIME=$(pdfinfo -- "$1" | sed -E -n -e 's/^CreationDate:[[:space:]]*//p' | head -1)
# $RAWTIME is like Tue Jan 24 21:22:23 2012 PST
# The time is stored absolute, but is displayed in the local time zone
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: png (PNG image)
# requires: pngtools
# TODO: some PNG files (e.g. written by GIMP) have a tIME chunk that could be used
mtime_png () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(pnginfo "$sf" | sed -n -e 's/^ *Creation Time[^:]*: *//p')
# $RAWTIME is like Mon, 11 Oct 2021 12:34:56 +0000
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: ps (PostScript)
mtime_ps () {
RAWTIME=$(sed -n -e 's/^%%CreationDate: *//p' < "$1" | head -1)
# $RAWTIME is like Tue Dec 27 07:23:25 2022
TIME=$(normalize_time "$RAWTIME")
}
# File type: rar (rar archive)
# requires: unrar
mtime_rar () {
# The mtime is considered to be the latest file time in the archive.
# The time is displayed like 08:45:32,000000000 but it's not clear if the
# portion after the comma is sub-second time or something completely
# different, so it's just ignored
RAWTIME="$(unrar lta -idc -- "$1" | sed -E -n -e 's/^ *mtime: *//' -e 's/,.*$//p' | sort -d | tail -1)"
# $RAWTIME is like 2021-10-13 08:45:32
TIME="$RAWTIME"
}
# File type: rpm (rpm package)
# requires: rpm
mtime_rpm () {
RAWTIME=$(rpm -q --queryformat '%{BUILDTIME}' -p -- "$1")
# $RAWTIME is like 1234567890
TIME=$(normalize_time_tz "@$RAWTIME")
}
# File type: rtf (Rich Text Format file)
mtime_rtf () {
RAWTIME="$(sed -E -n -e 's/^.*\{\\revtim\\yr([0-9]+)\\mo([0-9]+)\\dy([0-9]+)\\hr([0-9]+)\\min([0-9]+)\\sec([0-9]+)[^}]*\}.*/\1-\2-\3 \4:\5:\6/p' < "$1")"
# $RAWTIME is like 2022-3-17 0:18:5
if [ -z "$RAWTIME" ]; then
# All files I've actually seen are missing the seconds
RAWTIME="$(sed -E -n -e 's/^.*\{\\revtim\\yr([0-9]+)\\mo([0-9]+)\\dy([0-9]+)\\hr([0-9]+)\\min([0-9]+)[^}]*\}.*/\1-\2-\3 \4:\5/p' < "$1")"
fi
# $RAWTIME is like 2022-3-17 0:18
if [ -z "$RAWTIME" ]; then
# Some files don't have the time, only the date
RAWTIME="$(sed -E -n -e 's/^.*\{\\revtim\\yr([0-9]+)\\mo([0-9]+)\\dy([0-9]+)[^}]*\}.*/\1-\2-\3/p' < "$1")"
# $RAWTIME is like 2022-3-17
fi
# Add leading zeros to month, day, hour, minute, seconds and append :00
# seconds if seconds aren't given
TIME=$(echo "$RAWTIME" | sed -E -e 's/([0-9]+-)([0-9]-)/\10\2/' -e 's/([0-9]+-[0-9][0-9]-)([0-9])\>/\10\2/' -e 's/([^0-9])([0-9]:)/\10\2/' -e 's/([^0-9][0-9][0-9]:)([0-9]\>)/\10\2/' -e 's/:([0-9])$/:0\1/' -e 's/( [0-9][0-9]:[0-9][0-9])$/\1:00/' )
}
# File type: shar (shell archive)
mtime_shar () {
RAWTIME="$(sed -E -n -e '1,/^$/s/^# Made on ([012][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-9][0-9](:[0-9][0-9])? ?[^ ]*).*/\1/p' < "$1")"
# $RAWTIME is like 2021-10-13 01:02 PDT
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: sla (Scribus document)
# requires: xmlstarlet
mtime_sla () {
RAWTIME=$(xmlstarlet sel -t -v '/SCRIBUSUTF8NEW/DOCUMENT/@DOCDATE' < "$1")
# $RAWTIME is like 23 October 2010
TIME=$(normalize_time "$RAWTIME")
}
# File type: slob (Sorted List of Blobs dictionary)
# See https://github.com/itkach/slob/
# requires: slob
mtime_slob () {
RAWTIME=$(slob tag -n created.at -- "$1")
# $RAWTIME is like 2020-12-28T10:06:20.108760+00:00
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: squashfs (Squashfs filesystem image)
# requires: squashfs-tools
mtime_squashfs () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(unsquashfs -s "$sf" | sed -n -e 's/^Creation or last append time //p')
# $RAWTIME is like Sun May 23 06:33:44 2021
# The time is stored absolute, but is displayed in the local time zone
TIME=$(normalize_time_tz "$RAWTIME")
}
# File type: svg (Scalable Vector Graphics image)
# requires: xmlstarlet
mtime_svg () {
RAWTIME=$(xmlstarlet sel -t -v "/*[local-name()='svg']/*[local-name()='metadata']/*[local-name()='RDF']/*[local-name()='Work']/*[local-name()='date']" < "$1")
# $RAWTIME can be just about anything, but DublinCore recommends ISO 8601-1.
# Anything else is too ambiguous, so ignore them.
if echo "$RAWTIME" | grep '^[012][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]' >/dev/null; then
# $RAWTIME is like 2012-01-23 or 2012-01-23T13:14:15Z or 2012-01-23T13:14:15-0100
TIME="$(normalize_iso_time "$RAWTIME")"
else
# Not ISO 8601, so ignore it since it's too dangerous guessing what it is
TIME=''
fi
}
# This takes a tar file on stdin and returns a raw time on stdout
# It is intended to be used by any tar file variant to centralize tar
# time extraction.
handle_tar () {
# The mtime is considered to be the latest file time in the archive.
# GNU tar doesn't include seconds in the time
# GNU tar returns lines like:
# -rw-rw-r-- root/root 4 2021-10-13 01:34 foo
# BSD tar returns lines like:
# -rw-r--r-- 1 root wheel 4 Sep 18 2021 foo
# -rw-r--r-- 1 root root 7 Jul 27 00:22 bar
# -rw-r--r-- 1 reallylonguser alsoalonggroup 212 Apr 19 2021 baz
RAWTIME=$(tar tvf - | awk '{if (index($2, "/")) print $4 " " $5; else print $6 " " $7 " " $8;}' | sort -d | tail -1)
# $RAWTIME is like 2021-10-13 01:34 or 2021-10-13 01:34:12 (Busybox tar)
# or Sep 18 2021 (BSD tar) or Jul 27 00:22 (BSD tar)
# If we know it's GNU tar we could skip the normalization, but it's
# safe to do even in that case.
# The time is stored absolute, but is displayed in the local time zone
normalize_time_tz "$RAWTIME"
}
# File type: tar (Tape Archive)
# requires: tar
mtime_tar () {
TIME=$(handle_tar <"$1")
}
# File type: pbi (PC-BSD package)
# requires: bzip2, tar
mtime_pbi () {
TIME=$(sed '1,/^__PBI_ARCHIVE__$/d' < "$f" | handle_tar)
}
# File type: tbz (Bzip2-compressed Tape Archive)
# requires: bzip2, tar
mtime_tbz () {
TIME=$(bzip2 -dc <"$1" | handle_tar)
}
# File type: tgz (Gzip-compressed Tape Archive)
# requires: gzip, tar
mtime_tgz () {
TIME=$(gzip -dc <"$1" | handle_tar)
}
# File type: tlz (Lzma-compressed Tape Archive)
# requires: lzma, tar
mtime_tlz () {
TIME=$(lzma -dc -- "$1" | handle_tar)
}
# File type: tlzip (Lzip-compressed Tape Archive)
# requires: lzip, tar
mtime_tlzip () {
TIME=$(lzip -dc -- "$1" | handle_tar)
}
# File type: txz (Xzip-compressed Tape Archive)
# requires: xz, tar
mtime_txz () {
TIME=$(xz -dc <"$1" | handle_tar)
}
# File type: tzst (Zstd-compressed Tape Archive)
# requires: zstd, tar
mtime_tzst () {
TIME=$(zstd -dc <"$1" | handle_tar)
}
# File type: tiff (TIFF image)
# requires: libtiff-progs
mtime_tiff () {
RAWTIME=$(tiffinfo -- "$1" | sed -n -e 's/^ *DateTime: //p')
# $RAWTIME is like 2009:01:22 14:56:39
# TimeZoneOffset is part of TIFF/EP
# As of ver. 4.5.0 tiffinfo does not yet support TimeZoneOffset so this
# is commented out as the output format is unknown.
#TZOFFSET=$(tiffinfo -- "$1" | sed -n -e 's/^ *TimeZoneOffset: //p' | sed -E -n -e 's/^[^,]+, *([0-9]+).*$/\1/p')
#if [ -n "$TZOFFSET" ] ; then
# RAWTIME="$(printf '%s %+03d00' "$RAWTIME" "$TZOFFSET")"
#fi
TIME=$(echo "$RAWTIME" | sed -E -e 's/^([0-9][0-9][0-9][0-9]):([0-9][0-9]):/\1-\2-/')
}
# File type: otf (OpenType font/TrueType font)
# requires: freetype2-demos
mtime_otf () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
RAWTIME=$(ftdump "$sf" | sed -n -e 's/^ *modified: *//p' | sort -d | tail -1)
# $RAWTIME is like 2021-10-17
TIME="$RAWTIME"
}
# File type: gpx (GPX GPS track)
# requires: xmlstarlet
mtime_gpx () {
# The most recent date stamp or last-modified entry in the file is used
RAWTIME=$(xmlstarlet sel -N g=http://www.topografix.com/GPX/1/1 -N g0=http://www.topografix.com/GPX/1/0 -N m=http://www.topografix.com/GPX/gpx_modified/0/1 -t -v '/g:gpx/g:trk/g:trkseg/g:trkpt/g:time' -n -v '/g0:gpx/g0:trk/g0:trkseg/g0:trkpt/g0:time' -n -v '/g0:gpx/g0:time' -n -v '/g:gpx/g:metadata/g:time' -n -v '/g:gpx/g:metadata/g:extensions/m:time' -n -v '/g:gpx/g:wpt/g:time' -n -v '/g0:gpx/g0:wpt/g0:time' -n < "$1" | sort -d | tail -1)
# $RAWTIME is like 2022-10-01T23:43:05Z
TIME=$(normalize_iso_time "$RAWTIME")
}
# File type: ipk (Itsy package)
# See http://www.handhelds.org/ (defunct as of 2021)
# requires: binutils, file, grep, gzip, tar
mtime_ipk () {
if file "$1" | egrep -iq 'Debian|ar archive' ; then
mtime_ar "$1"
else
mtime_tgz "$1"
fi
}
# File type: vbox (VirtualBox machine file)
# requires: xmlstarlet
mtime_vbox () {
RAWTIME=$(xmlstarlet sel -N vb=http://www.virtualbox.org/ -t -v '/vb:VirtualBox/vb:Machine/@lastStateChange' <"$1" 2>/dev/null)
# $RAWTIME is like 2016-10-28T18:39:42Z
TIME=$(normalize_iso_time "$RAWTIME")
}
# File type: vcf (vCard contact)
mtime_vcf () {
RAWTIME=$(sed -E -n -e 's/^REV:([^()]*)(\(.*\))?$/\1/p' <"$1" | sort | tail -1)
# $RAWTIME is like 2017-05-10T14:34:50Z or 20180213T070722Z
# Make sure it look like ISO-8601, then convert
RAWTIME=$(echo "$RAWTIME" | sed -E -e 's/([12][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-2][0-9])([0-9][0-9])([0-9][0-9])/\1-\2-\3T\4:\5:\6/')
if [ -n "$RAWTIME" -a "$RAWTIME" != "0" ]; then
TIME=$(normalize_iso_time "$RAWTIME")
fi
}
# File type: warcgz (Compressed Web Archive Collection file)
mtime_warcgz () {
# Look for the WARC-Date: lines only within the WARC header sections
RAWTIME=$(gzip -dc < "$1" | tr -d '\015' | sed -E -n -e '/^WARC\/1\.[0-9]+$/,/^$/s/^WARC-Date: *//p' | sort -d | tail -1)
# $RAWTIME is like 2022-05-27T13:75:25.812Z
TIME=$(normalize_iso_time "$RAWTIME")
}
# File type: webp (WEBP image)
# requires: exiftool
mtime_webp () {
# Uses exiftool that works just as well for this format
mtime_heif "$@"
}
# File type: wml (Wireless Markup Language)
# requires: xmlstarlet
mtime_wml () {
RAWTIME=$(xmlstarlet sel -t -v "/wml/head/meta[@name='date']/@content" < "$1" 2>/dev/null)
# $RAWTIME is like Thu Jan 12 10:33:38 2023
TIME=$(normalize_time "$RAWTIME")
}
# File type: xcf (Gimp image)
# requires: gimp
mtime_xcf () {
# "safe" filename with quoted double quotes
sf="$(echo "$1" | sed 's/"/\\"/g')"
# The mtime is considered to be the most recent stored event, which is
# generally a "save" event (which makes sense).
# gimp's Scheme interpreter displays some logging info before and after the
# desired output, so use sed to delete it to leave only XML.
METADATA=$(echo '((display "\nMETADATA-START\n") (let ((img (car (gimp-file-load RUN-NONINTERACTIVE "'"$sf"'" "file")))) (display (car (gimp-image-get-metadata img))) (gimp-image-delete img)) (gimp-quit TRUE))' | gimp -n -i -d -f -s -g /dev/null --stack-trace-mode=never -b - 2>/dev/null | sed -e '1,/^METADATA-START/d' -e '/<\/metadata>/q')
if [ -n "$METADATA" ]; then
RAWTIME=$(echo "$METADATA" | xmlstarlet sel -t -v "/metadata/tag[starts-with(@name, 'Xmp.xmpMM.History[') and contains(@name, '/stEvt:when')]" | sed 's/lang="x-default" *//' | sort -d | tail -1)
# $RAWTIME is like 2022-02-23T22:50:48-08:00
TIME="$(normalize_iso_time "$RAWTIME")"
fi
if [ -z "$TIME" ]; then
# Gimp probably isn't installed; maybe exiftool is
mtime_xcf_exiftool "$1"
fi
}
# File type: xcf (Gimp image)
# requires: exiftool, xmlstarlet
# This version uses exiftool instead of Gimp itself.
# This version does not support xcf compressed with gzip, bzip2, etc.
mtime_xcf_exiftool () {
# The mtime is considered to be the most recent stored event, which is
# generally a "save" event (which makes sense).
RAWTIME=$(exiftool -X -XML:* -a -- "$1" | xmlstarlet sel -N rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' -N XML=http://ns.exiftool.ca/XML/XML/1.0/ -t -v "/rdf:RDF/rdf:Description/XML:MetadataTagName[starts-with(text(), 'Xmp.xmpMM.History[') and contains(text(), ']/stEvt:when')]//following::XML:MetadataTag[1]" | sort -d | tail -1)
# $RAWTIME is like 2022:02:23 22:50:48-08:00
TIME=$(echo "$RAWTIME" | sed -E -e 's/^([0-9][0-9][0-9][0-9]):([0-9][0-9]):/\1-\2-/' -e 's/([-+][0-9][0-9])(:)?([0-9][0-9])$/ \1\3/')
}
# File type: zip (zip archive)
# requires: unzip (Info-ZIP version), findutils
mtime_zip () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
# The mtime is considered to be the latest file time in the archive.
# The date format can not, unfortunately, be easily sorted as-is, so it
# is converted to epoch time, sorted and only the most recent is used.
# The real raw time from zipinfo is like: 2007 Dec 9 18:26:28 or:
# 2021 Oct 15 22:12:13 local
# A third raw time like: 2021 Oct 16 05:12:13 UTC is filtered out since the
# equivalent local time is also shown and that can be treated like all the
# others, which are also local time.
# An "empty" time of '1980 000 0 00:00:00' is also filtered out.
RAWTIME=$(zipinfo -v "$sf" | sed -n -e '/^ *file last modified on.*[^U]..$/s/^[^:]*: *//p' | awk '$2 != "000" {print $2 " " $3 ", " $1 " " $4}' | uniq | tr '\n' '\0' | xargs -0 -n1 "$AUTOMTIME" --epoch_time | sort -n | tail -1)
# $RAWTIME is like 1234567890
# For ZIP files with time extensions, the time is stored absolute, but is
# displayed in the local time zone. Don't show the time zone here since
# it might actually not be known and that could be confusing.
TIME=$(normalize_time "@$RAWTIME")
}
# File type: zpaq (ZPAQ compressed archive)
# requires: zpaq
mtime_zpaq () {
# "safe" filename guaranteed not to start with a dash
sf="$(safefn "$1")"
# The mtime is considered to be the latest file time in the archive.
RAWTIME=$(zpaq l "$sf" 2>/dev/null | sed -E -n -e 's/^- ([12][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-2][0-9]:[0-9][0-9]:[0-9][0-9]).*$/\1/p' | sort -d | tail -1)
# $RAWTIME is like 2021-10-14 18:40:56
TIME="$RAWTIME"
}
###########################
if [ $# -eq 0 -o "$1" = "-h" -o "$1" = "-?" ] ; then
echo 'automtime ver. 8-dev'
echo 'Usage: automtime [-?] [-h] [-l] [-m] [-e program] [ -q ] [ -t type ] file1 [ file2 ... ]'
echo 'Extracts modification from internal file metadata'
echo ' -e cmd command to run once for each file with args: mtime file'
echo ' e.g. "touch -d"'
echo ' -h, -? show this help'
echo ' -l list supported file types'
echo ' -m set mtime of file to internal file mtime metadata'
echo ' -q quiet output'
echo ' -t type where type is one of the names shown with -l'
exit 1
fi
if [ "$1" = "-l" ]; then
sed -n -e 's/^# File type: //p' < "$0" | sort -u
exit 0
fi
if [ "$1" = "--epoch_time" ]; then
# This is an internal option to gain access to the epoch_time function
# so it can be called by xargs within this script.
epoch_time "$2"
exit $?
fi
# Store this script's location so it can be called recursively later.
# This is needed by zsh since it changes $0 within shell functions.
readonly AUTOMTIME="$0"
PROG=
if [ "$1" = "-m" ] ; then
PROG="touch -d"
shift
fi
if [ "$1" = "-e" ] ; then
PROG="$2"
shift
shift
fi
if [ "$1" = "-q" ] ; then
VERBOSE=0
shift
else
VERBOSE=1
fi
SETTYPE=
if [ "$1" = "-t" ] ; then
SETTYPE="$2"
shift
shift
fi
# Loop through files, extracting one at a time
for f in "$@" ; do
TIME=""
RAWTIME=""
if ! [ -r "$f" ] ; then
echo "$f": Not found 1>&2
continue
fi
if [ -n "$SETTYPE" ] ; then
TYPE="$SETTYPE"
else