问题描述
我在 Mac 终端上,想从电子邮件标头中“grep”一个字符串(这是一个 UNIX 时间戳),将其转换为操作系统可以使用的格式,并将其作为文件的创建日期。我想对文件夹中的所有邮件(具有多个可能的子文件夹)递归执行此操作。
结构大概是这样的:
#!/bin/bash
for i in `ls`
do
# Find the date field (X-Delivery-Time) inside an email header and grep the UNIX timestamp
# convert timestamp to a format the OS can work with
# overwrite the existing creation date with the new one
done
X-Envelope-From: <[email protected]>
X-Envelope-To: <[email protected]>
X-Delivery-Time: 1535436541
...
一些背景:Apple Mail 使用文件创建日期作为 Apple Mail 中显示的日期。这就是为什么将邮件从一台服务器移动到另一台服务器后,所有邮件现在都显示相同的日期,这使得无法进行排序。
由于我是 Terminal/Bash 的新手,所以不胜感激。谢谢
解决方法
在 Mac 上这应该可以工作,但由于我没有 Mac,我无法自己测试。我假设您的电子邮件文件具有 .emlx
扩展名。
对于单个目录:
for i in ./*.emlx; do
unixTime=$(grep -m1 '^X-Delivery-Time:' "$i" | grep -Eo '[0-9]+') &&
humanTime=$(date -r "$unixTime" +%Y%m%d%H%M.%S) &&
touch -t "$humanTime" "$i"
done
对于整个目录树:
fixdate() {
unixTime=$(grep -m1 '^X-Delivery-Time:' "$1" | grep -Eo '[0-9]+') &&
humanTime=$(date -r "$unixTime" +%Y%m%d%H%M.%S) &&
touch -t "$humanTime" "$1"
}
export -f fixdate
find . -name '*.emlx' -exec bash -c 'fixdate "$@"' . {} \;
或者,如果您安装了 bash 4 或更高版本(macOS 仍然默认使用 3)
shopt -s globstar
for i in ./**/*.emlx; do
unixTime=$(grep -m1 '^X-Delivery-Time:' "$i" | grep -Eo '[0-9]+') &&
humanTime=$(date -r "$unixTime" +%Y%m%d%H%M.%S) &&
touch -t "$humanTime" "$i"
done
,
以下内容假设您使用的是默认的 macOS 实用程序(touch
、date
...),因为它们已经完全过时了,如果您使用更新的版本(例如 {{1 }} 或 macports
)。它还假定您使用的是 brew
。
如果您有子文件夹,bash
不是正确的工具。无论如何,ls
的输出不是针对计算机的,而是针对人类的。因此,首先要做的是找到所有电子邮件文件。你猜怎么着?执行此操作的实用程序名为 ls
:
find
从当前目录 ($ find . -type f -name '*.emlx'
foo/bar.emlx
baz.emlx
...
) 开始搜索名称为 -type f
(.
) 的真实文件 (anything.emlx
)。适应你的情况。如果所有文件都是电子邮件文件,您可以跳过 -name '*.emlx'
部分。
接下来我们需要遍历所有这些文件并处理每个文件。这比 -name ...
稍微复杂一些,原因有很多(文件数量多、文件名带有空格……)一个可靠的方法是将 for f in ...
命令的输出重定向到while循环:
find
while IFS= read -r -d '' f; do
<process file "$f">
done < <(find . -type f -name '*.emlx' -print0)
的 -print0
选项用于使用空字符而不是默认换行符分隔文件名。 find
部分是一种将 < <(find...)
的输出重定向到 while 循环输入的方法。 find
读取 while IFS= read -r -d '' f; do
生成的每个文件名,将其存储在 shell 变量 find
中,保留前导和尾随空格(f
),反斜杠({{ 1}}) 并使用空字符作为分隔符 (IFS=
)。
现在我们必须对每个文件的处理进行编码。我们首先检索交货时间,假设它总是以-r
开头的最后一行的第二个单词:
-d ''
这样做。如果您还不了解 awk,那么现在是学习它的时候了。它是非常有用的文本处理瑞士刀之一(sed 是另一个)。但是让我们稍微改进一下,使其返回第一个遇到的交付时间而不是最后一个,在遇到它时立即停止,并检查时间戳是否是真实的时间戳(数字):
X-Delivery-Time:
正则表达式的 awk '/^X-Delivery-Time:/ {t = $2} END {print t}' "$f"
部分匹配 1 个或多个空格、制表符等,而 awk '/^X-Delivery-Time:[[:space:]]+[[:digit:]]+$/ {print $2; exit}' "$f"
匹配 1 个或多个数字。 [[:space:]]+
和 [[:digit:]]+
分别匹配行的开头和结尾。结果可以分配给一个shell变量:
^
请注意,如果没有匹配项,$
变量将存储空字符串。稍后我们将使用它来跳过此类文件。
一旦我们有了这个交付时间,在您的示例中它看起来像一个 UNIX 时间戳(自 1970/01/01 以来的秒数),我们必须使用它来更改电子邮件文件的最后修改时间。执行此操作的命令是 t="$(awk '/^X-Delivery-Time:[[:space:]]+[[:digit:]]+$/ {print $2; exit}' "$f")"
:
t
不幸的是,touch
需要 $ man touch
...
touch [-A [-][[hh]mm]SS] [-acfhm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...
...
格式的时间。不用担心,touch
实用程序可用于将 UNIX 时间戳转换为我们喜欢的任何格式。例如,使用您的示例时间戳 (CCYYMMDDhhmm.SS
):
date
我们快完成了:
1535436541
注意我们如何测试 $ date -r 1535436541 +%Y%m%d%H%M.%S
201808280809.01
是否为空字符串 (while IFS= read -r -d '' f; do
# uncomment for debugging
# echo "processing $f"
t="$(awk '/^X-Delivery-Time:[[:space:]]+[[:digit:]]+$/ {print $2; exit}' "$f")"
if [ -z "$t" ]; then
echo "no delivery time found in $f"
continue
fi
# uncomment for debugging
# echo touch -t "$(date -r "$t" +%Y%m%d%H%M.%S)" "$f"
touch -t "$(date -r "$t" +%Y%m%d%H%M.%S)" "$f"
done < <(find . -type f -name '*.emlx' -print0)
)。如果是,我们打印一条消息并跳转到下一个文件 (t
)。只需将所有这些放在一个带有 shebang 行的文件中并运行...
如果您必须使用具有更复杂和可变格式(例如 if [ -z "$t" ]
)的 continue
字段而不是 X-Delivery-Time
字段,那么最好安装一个最近的带有 Mac Ports 或 Homebrew 的 coreutils 包的 Date
版本。然后:
Date: Mon,11 Jun 2018 10:36:14 +0200
awk 命令稍微复杂一些。它打印没有 touch
前缀的匹配行。以下 sed 命令将以更紧凑的形式执行相同的操作,但实际上并不会更具可读性:
while IFS= read -r -d '' f; do
t="$(awk '/^Date:/ {print gensub(/^Date:[[:space:]+](.*)$/,"\\1","1"); exit}' "$f")"
if [ -z "$t" ]; then
echo "no delivery time found in $f"
continue
fi
touch -d "$t" "$f"
done < <(find . -type f -name '*.emlx' -print0)