【问题标题】:Strip characters in bash output去除 bash 输出中的字符
【发布时间】:2017-04-06 20:37:53
【问题描述】:

我有这个对我有用的命令,它将在当前目录中找到零个或一个与模式匹配的目录:

find . -maxdepth 1 -type d -name 'suman-*'| head -n1

在 MacOS 上,这将导致如下结果:

./suman-1479860474833

<!EDIT>

我的目标是找到最近的目录(带有最近的时间戳)。目录内容如下:

foo
bar 
baz
suman-1479860475524
suman-1479860471431
suman-1479860474233
...
etc.

</EDIT>

我有三个问题,

  1. 使用 bash,如果 ./ 字符存在于结果中,我如何去除它们?他们应该总是在那里,但我认为总是删除 前两个字符可能太笨拙了。

  2. 使用 bash,而不是使用 head 查找第一个结果,查找最后一个结果的最佳方法是什么?我猜是tail 但也许在那里 是更好的方法。

  3. 有没有办法只匹配一个数字而不是只使用'suman-*'?或许我应该使用-regex 而不是-name?

现在,我可以按目录名称中的时间戳对目录进行排序,或者我可以按元数据对它们进行排序(如果元数据准确并通过版本控制更新等保持不变)。我个人不确定目录元数据是否足够持久,所以我想更透明并使用目录名称中的时间戳。 而且看起来在 Unix 中没有为目录创建时间 =>“在 Unix 中不存储创建时间(仅:访问、修改和更改)。”

【问题讨论】:

  • find 没有保证订单。您不应该使用head 或tail 来尝试查找特定目录。
  • 实际目标是什么,是“第一”还是“最后”?
  • 在某种程度上,sort 在head 之前可以解决第一个/最后一个问题...
  • @AlexanderMills:感谢您的编辑;在 macOS 上,默认情况下您无权访问 GNU find,如果只对直接子目录感兴趣,那么 glob 和可能的 ls 的组合是最简单的。
  • @AlexanderMills 我在 OS X 和 FreeBSD 上玩过这个,因为这实际上对我的应用程序很有用。请参阅更新的答案。无论您是否采用这种方法,我都很想知道它是如何工作的。如果有什么我忽略了。它适用于开箱即用的 Mac 系统、我的开发 Mac 和 FreeBSD 10 系统。

标签: bash macos unix cross-platform sh


【解决方案1】:

认为“越简单越好”:

# 1: Using cut (The option "-c 3-" means "from 3rd character"
find... | cut -c 3-

# 2: You're right, tail is the command (-n1 == -1)
find... | tail -1

# 3: -name should do it:
TIME=1479860474833
find... -name "suman-$TIME"

编辑:

如你所说:

目录内容如下: foo bar baz suman-1479860475524 suman-1479860471431 suman-1479860474233 ... etc.

最近的

要根据名称获取最新的目录,您可以:

$ find . -maxdepth 1 -type d -name suman-\* | cut -c3- | sort -rnk1.7 | head -1
# Result is:
suman-1479860475524

如果内容只有名称为“suman*”的目录(绝不是具有该模式的文件),那么我认为这更容易:

$ ls -1d suman-* | sort -rnk1.7 | head -1
# Result is:
suman-1479860475524

在这两种情况下,排序选项-r(反向)会将最近的排在最前面,-k1.7 将从第 7 个字符(名称的毫秒部分)开始按数字排列 -n。

最古老的

为了获取最旧的目录,根据名称,不要使用-r 选项:

$ find . -maxdepth 1 -type d -name suman-\* | cut -c3- | sort -nk1.7 | head -1
# Result is:
suman-1479860471431

$ ls -1d suman-* | sort -nk1.7 | head -1
# Result is:
suman-1479860471431

【讨论】:

  • 如果find返回的文件路径不是以./开头或者包含完整路径怎么办?
  • OP 明确表示他的命令是find . -maxdepth 1 -type d -name 'suman-*'| head -n1,这意味着输出always 将以./ 开头
  • 我在找最近的时间戳,我不知道我在找哪个时间戳,之前我只是在找最近的一个
  • @AlexanderMills, ...既然如此,您可能会发现 BashFAQ #3 很有趣。
  • @AlexanderMills,我编辑了我的答案,也许这就是你要找的。​​span>
【解决方案2】:

使用 GNU find,如果您不想要 ./,您可以简单地避免告诉 find 打印它。

# with GNU find
find . -maxdepth 1 -type d -name 'suman-*' -printf '%P\n' | head -n1

%P 格式字符串不包括从find 的参数派生的文件名部分——在本例中为./。

使用 BSD find,您没有该选项,但是一旦您的结果在 shell 变量中,您就可以进行后处理:

# strip "./" prefix from filename variable, if and only if it exists
filename=${filename#./}

对于head 或tail,无法保证排序,因此您不能依赖此类选项来查找特定文件。如果您想要最新的、最旧的、第一个、最后一个等,那么您需要做额外的工作才能以可靠的方式实现这一点。例如:

IFS= read -r -d '' filename \
  < <(find . -maxdepth 1 -type d -name 'suman-*' -printf '%P\0' | sort -z)

...将在流排序时读取第一项,并且...

IFS= read -r -d '' filename \
  < <(find . -maxdepth 1 -type d -name 'suman-*' -printf '%P\0' | sort -rz)

...将以相反的方向排序,从而读取最后一个。


可移植性说明

请注意,find -print0 或 sort -z 都不是 POSIX 指定的,但它们在 GNU 工具链和当前 MacOS 上都可用。相比之下,find -printf 需要 GNU 查找;这可以通过 macports findutils 包安装在 MacOS 上(安装为 gfind)

【讨论】:

    【解决方案3】:

    获取最近修改文件夹(OS X兼容):

    stat -f "%HT %Sm %i" -t %Y%m%d%H%M%S * | grep "^目录" |剪切 -f2- -d ' ' |排序-rn |头-1 |剪切 -f2- -d ' ' |同时读取 inode ;找到。 -inum "$inode" |基本名称 "$(cat -)" ;完成

    结果:suman-1479860475524


    获取最近命名的suman- 时间戳(OS X 兼容):

    find ./ -mindepth 1 -maxdepth 1 -type d -name "suman-*" -print0 |排序-zn |而 IFS= 读取 -d '' 文件;做 basename "$file" ;完成 |尾 -1

    结果:

    • suman-1479860475524



    最近的文件夹(任何名称):

    • stat -f "%HT %Sm %i" -t %Y%m%d%H%M%S *:以秒为单位列出以机器可读时间戳开头的文件夹(因此cut可以安全使用),并显示类型、阳极和时间戳(参见https://www.freebsd.org/cgi/man.cgi?stat(1)) :

      • 目录 20161124051357 17658795 目录 20161124051358 17658796 目录 20161124051356 17658793 目录 20161124051359 17658798 目录 20161124051400 17658800 目录 20161124051401 17658802
    • | grep "^Directory" | cut -f2- -d ' ':选择文件夹,并剪掉“目录”

    • | sort -rn:数字排序,最新到最旧

    • | head -1:仅最近的

    • | cut -f2- -d ' ': 仅显示 inode 组件

    • | while read inode ; do find . -inum "$inode" -print0:根据 inode 查找文件(有些人可能认为从这一步开始是不必要的,但如果出现带有嵌入特殊字符的奇数命名文件夹,它会返回完整的文件夹名称)

    此时(如果我们在此处添加; done)我们将拥有:

    • ./suman-1479860475524

    终于

    • | basename "$(cat -)" ; done :仅返回文件夹的名称:

      • suman-1479860475524




    最近的suman- 文件夹时间戳:

    • find ./ -mindepth 1 -maxdepth 1 -type d -name "suman-*" -print0:获取当前文件夹中的文件夹名称

    • | sort -zn:根据目录name中的时间戳,而不是实际的文件系统修改时间戳,对它们进行数字排序。

    • while IFS= read -d '' file ; do basename "$file" ; done :从文件中去除目录位置字符和斜杠,并以行分隔输出文件列表

    • | tail -1: 只列出最近的。

    在这个例子中,结果恰好是一样的:

    • suman-1479860475524

    ...while read inode ;... 变得有用的奇怪文件夹名称示例:


    1.

    • mkdir $'some \r\t 奇怪的\n 文件夹\n\n 名称\n 完全疯了'

    2.

    • stat -f "%HT %Sm %i" -t %Y%m%d%H%M%S * | grep "^目录" |剪切 -f2- -d ' ' |排序-rn |头-1 |剪切 -f2- -d ' ' |同时读取 inode ;找到。 -inum "$inode" -print0 |基本名称 "$(cat -)" ;完毕

    3.(输出)

    • 有些奇怪 文件夹 姓名 完全疯了

    具体问题

    使用 bash,如果 ./ 字符存在于 结果? (希望他们应该永远在那里,但我认为永远 删除前两个字符可能太笨拙了)。

    • 在结果文件上使用basename
    • 使用find "$PWD" 而不是find .(这将产生完整路径)
    • 使用-printf "%P"(这只会显示没有./的名称部分)(注意:-printf需要GNU查找)

    使用 bash,而不是使用 head 查找第一个结果 找到最后一个结果的最佳方法(我猜它是尾巴,但也许 有更好的方法)

    一种方法是使用:

    • 第一个结果:find ... | sort -n | tail -n1
    • 最后结果:find ... | sort -rn | tail -n1

    (显然,“First”实际上可能是“Last”,这取决于它对您的含义。考虑到这一事实,您基本上可以替换 head 命令,只要您保持一致,sort 中的 -r将颠倒顺序,因此两个管道命令集将分别为您提供“第一个”和“最后一个”结果)

    有没有办法只匹配一个数字(它是一个时间戳 毫)而不是仅仅使用'suman-*'?也许我应该使用 -regex 而不是-name?

    您可以只使用-name '*1479860474833' 而不是-name 'suman-*'


    GNU Find 的替代(更简单)方法:

    本例的三个测试文件夹:

    • suman-1479860474833
    • suman-1489860474833
    • suman-1499860474833

    示例 1:

    这是一个严格的示例,可以减轻带有嵌入特殊字符的疯狂文件夹名称的一些风险

    • 找到 "$PWD" -mindepth 1 -maxdepth 1 -type d -print0 |排序-zn |尾-zn1

    给予:

    • /my/dir/suman-1499860474833

    示例 2:

    这会去除前导的“./”,同时使用printf 和\0 保持NULL 分隔:

    • 找到 "$PWD" -mindepth 1 -maxdepth 1 -type d -printf "%P \0" |排序-zn |尾-zn1

    给予:

    • 苏曼-1499860474833

    注意:-mindepth 1 避免返回父文件夹。

    【讨论】:

    • 顺便说一句,考虑find ... -print0 | sort -z 以避免使用文字换行符破坏文件名。当然,这意味着还需要在另一端使用 NUL 感知工具,但 GNU 工具链非常适合这一点; IFS= read -r -d '' 将作为 bash 内置。
    • 点头。 ksh 中的循环性能没有 bash 中那么差,但这都是事实。
    • "POSIX find 中没有 -printf 选项。它是 GNU find 的一个特性,例如在 Linux 上。"
    • @AlexanderMills:如果您确实需要使用 stock macOS 实用程序的解决方案,请标记您的问题osx。除非您专门寻找 portable 解决方案,否则应始终添加特定于平台的标记。 (相反,在问题正文中说明您正在寻找跨平台兼容性永远不会有什么坏处。)
    • @mklement0 谢谢,我试图表明我在 MacOS 上,但肯定需要跨平台 (*nix) 解决方案
    【解决方案4】:

    tl;dr

    printf '%s\n' suman-*/ | tail -n 1 | sed 's|/$||' # ... | cut -d/ -f1 works too
    

    请注意,此答案假定文件名没有嵌入换行符,幸运的是,这在现实世界中很少引起关注。
    除非另有说明,否则此答案中的所有命令均符合 POSIX。

    如果您只是在目标目录中寻找 直接子目录,则不需要 find - 一个简单的 glob 就可以了:

    printf '%s\n' suman-*/ | head -n 1
    

    但请注意:

    • 这会输出带有结尾/ 的子目录名称。
    • 符号链接到目录包括。
    • 隐藏子目录不包含(与suman-*/无关,根据定义它永远不会匹配隐藏目录。) - 要包含一般隐藏项目,请先运行shopt -s dotglob (这是一个 Bash 扩展)。
    • 输出排序是大小写敏感的,即使 macOS 默认文件系统是大小写的不敏感 - 要改变这一点,请将printf 输出传送到sort -f 或sort -rf 在进一步处理之前。

    关于查找与模式匹配的最近修改目录的请求,将ls -dt 与 glob 结合是最简单的选择:

    ls -dt suman-*/ | head -n 1 # print most recently modified suman-* subdir.
    

    相比之下,如果嵌入在目录名称中的时间戳应该驱动排序(例如,1479860475524),反向词法排序会这样做:

    ls -dr suman-*/ | head -n 1 
    

    没有尾随/:

    ls -dr suman-*/ | head -n 1 | sed 's|/$||' # with no path prefix, | cut -d/ -f1 works too
    

    一个稍微麻烦一点,但更强大的替代方案,它避免使用ls,而是避免使用最大值。调用 external 实用程序时的命令行长度,如 getconf ARG_MAX 报告的那样,如果大量文件与 glob 匹配,这可能是一个问题:向Charles Duffy 致敬。

    printf '%s\n' suman-*/ | tail -n 1 | sed 's|/$||'
    

    注意:这里假定 printf 是作为 shell 内置 实现的(而不是必须依赖 printf 实用程序),但是,所有主要的类似 POSIX 的 shell 都是如此(bash、zsh、ksh、dash)。

    sort 不区分大小写(在这种情况下没有区别):

    printf '%s\n' suman-*/ | sort -rf | head -n 1 | sed 's|/$||'
    

    关于您的 3 个原始问题:

    Re 1):使用sed 修剪尾随/:printf '%s\n' suman-*/ | head -n 1 | sed 's|/$||'(如果剥离路径前缀也是一个问题,最简单的方法是先将cd 设置为路径前缀,然后使用仅文件名全局)。

    Re 2):使用printf ... | tail -1 | ... 获取词法上的最后一个条目(或printf ... | sort -rf | head -n 1 | ... 获取词法上的最后一个条目,不考虑大小写)。

    Re 3):(通配符)模式允许每个字符位置的数字与 [0-9] 等字符集匹配,但您不能将 ? 和 + 等正则表达式样式量词(重复符号)应用于他们。


    一般来说,使用 find 和 globbing / ls 之间存在许多细微差别 - 请注意购买者。
    一般来说,parsing ls output should be avoided,但是 - 假设人们知道边缘情况和限制 - 有时它是最方便的解决方案。

    【讨论】:

    • 不查找最近修改的,实际上只是查找最新的目录,它始终是具有最新时间戳的目录
    • @AlexanderMills: 明白了,所以 ls -dr 解决方案应该执行反向 lexical 排序,因为文件名中嵌入了固定宽度的时间戳,前面是固定-宽度前缀suman-,对吧?
    • 是的,时间戳应该是固定宽度的(我猜是几千年)。这对我有用: printf '%s\n' suman-*/ |排序-rf | head -1 ,但我不确定那里的排序是如何工作的
    • @AlexanderMills: -f 不区分大小写(这在您的场景中不重要),-r 按降序顺序排序;使用嵌入的时间戳,这意味着最近的时间戳首先。
    • 谢谢,我很喜欢“ls”版本 - “ls -dr suman-*/ | head -1”,为什么不只是“ls -dr suman-* | head -1”(没有 /) ?
    猜你喜欢
    • 1970-01-01
    • 2012-09-13
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    • 2010-11-30
    • 1970-01-01
    • 2015-11-16
    • 2021-09-15
    相关资源
    最近更新 更多