如何使用 Bash 在所有子目录中执行命令

如何使用 Bash 在所有子目录中执行命令

我有一个由以下组成的目录结构:

iTunes/Music/${author}/${album}/${song.mp3}

我实现了一个脚本,使用 lame(每次对一个文件进行操作)将我的 mp3 比特率降低到 128 kbps。

我的“normalize_mp3.sh”如下所示:

#!/bin/bash
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in *.mp3
do
  lame --cbr $f __out.mp3
  mv __out.mp3 $f
done
IFS=$SAVEIFS

如果我逐个文件夹并执行此命令,这可以正常工作,但我想要一个“全局”命令,就像在 4DOS 中一样,这样我就可以运行:

$ cd iTunes/Music
$ global normalize_mp3.sh

global命令将遍历所有子目录并执行normalize_mp3.sh以删除所有子文件夹中的所有 mp3。

有人知道是否有与 4DOSglobal命令等效的 Unix 命令吗?我尝试过,find -exec但就是头疼。

答案1

这是所述global命令的一个非常基本的实现bash

global() {
  shopt -s globstar
  origdir="$PWD"
  for i in **/; do
    cd "$i"
    echo -n "${PWD}: "
    eval "$@"
    echo
    cd "$origdir"
  done
}
  • shopt -s globstar 启用递归通配符**我认为你需要 bash 版本 >= 4)
  • **/查找当前工作目录下的所有目录,因此$i循环遍历这些目录
  • 我添加了一个echo,以便您可以看到循环当前位于哪个目录中
  • eval "$@"计算函数的所有参数
  • cd $origdir返回到原始目录。(起初,我使用pushd/ 的方法更优雅popd,但我认为 cd back to$origdir更安全,以防命令"$@"本身更改目录(这当然是个坏主意!)

global命令接受带有参数的命令,例如global touch foo。如果要使用更高级的 shell 语法(例如重定向或变量),则必须将命令括在单引号中'...'。但是,如果您希望在每个目录中运行脚本,normalize_mp3.sh则用法很简单

global normalize_mp3.sh

请注意,如果子目录很多,则第一个命令的启动可能需要一些时间来收集所有子目录。

当然,请使用最新的文件备份进行测试!


它的工作原理如下(我只能在 Linux 上测试,但我假设 bash 在 OSX 上以相同的方式运行):

$ echo $BASH_VERSION 
4.1.5(1)-release
$ cd /var
$ tree -d | head
.
├── backups
├── cache
│   ├── apt
│   │   ├── apt-file
│   │   └── archives
│   │       └── partial
│   ├── cups
│   │   └── rss
│   ├── debconf
$ global 'echo -n This command is executed in $PWD at $(date); sleep 1'
/var/backups: This command is executed in /var/backups at Mo 1. Jul 12:11:00 CEST 2013
/var/cache: This command is executed in /var/cache at Mo 1. Jul 12:11:01 CEST 2013
/var/cache/apt: This command is executed in /var/cache/apt at Mo 1. Jul 12:11:02 CEST 2013
/var/cache/apt/apt-file: This command is executed in /var/cache/apt/apt-file at Mo 1. Jul 12:11:03 CEST 2013
/var/cache/apt/archives: This command is executed in /var/cache/apt/archives at Mo 1. Jul 12:11:04 CEST 2013
/var/cache/apt/archives/partial: This command is executed in /var/cache/apt/archives/partial at Mo 1. Jul 12:11:05 CEST 2013
/var/cache/cups: This command is executed in /var/cache/cups at Mo 1. Jul 12:11:07 CEST 2013
/var/cache/cups/rss: This command is executed in /var/cache/cups/rss at Mo 1. Jul 12:11:08 CEST 2013
/var/cache/debconf: This command is executed in /var/cache/debconf at Mo 1. Jul 12:11:09 CEST 2013
...

答案2

其他选择:

find ~/Music -name \*.mp3 | while IFS= read -r f; do
  lame --cbr "$f" "$f"_temp
  mv "$f"_temp "$f"
done
mdfind 'kMDItemAudioBitRate>128000' -onlyin ~/Music |\
parallel lame --cbr {} {}_temp \; mv {}_temp {}
f() { lame --cbr "$1" "$1"_temp; mv "$1"_temp "$1"; }
export -f f
mdfind 'kMDItemAudioBitRate>128000' -onlyin ~/Music | parallel f
shopt -s globstar # bash 4.0 or later
for f in ~/Music/**/*.mp3; do lame --cbr "$f" "$f"_temp; mv "$f"_temp "$f"; done

答案3

尝试以下方法:

#!/bin/bash
# first cd to iTunes/Music
for f in */*/*.mp3
do 
  lame --cbr "$f" __out.mp3 && mv __out.mp3 "$f"
done

无需设置IFS。但您需要加上双引号$f。我不知道如果出现问题,lame 是否有返回代码。也许先尝试使用printf "%s\n" "$f" first而不是 lame 命令。

答案4

首先,编写一个稍微精简一点的脚本,循环遍历提供给脚本的参数

#! /usr/bin/env bash
for f do
  lame --cbr "$f" "$f_out.mp3"
  mv "$f_out.mp3" "$f"
done

为了能够从任何地方访问它,请将脚本放在 $PATH 中的某个位置。在 Linux 上,我会使用$HOME/bin,默认情况下它会添加到 $PATH 中;我怀疑 OSX 也是如此。

在单个 mp3 目录中使用它:

downrate_mp3 ./*.mp3

我更喜欢在这些 glob 前面加上./,因为以 开头的文件名是合法的-,但这可能会导致问题,因为许多程序(包括mv)都希望以 开头的参数-是一个选项,而不是一个文件。对于递归性,最好的方法是使用 globstar(需要 bash 4+,但我认为最近的 OSX 有这个):

shopt -s globstar
downrate_mp3 ./**/*.mp3

或者,您可以使用 find -exec,正如您已经尝试过的那样:

find iTunes/Music -type f -name '*.mp3' -exec downrate_mp3 {} +

这对您现有的命令不起作用的原因是,它find本质上试图使用大文件列表作为脚本的参数——而由于您问题中的脚本没有对参数执行任何操作,所以这显然不起作用。

相关内容