【问题标题】:Best way to choose a random file from a directory in a shell script从 shell 脚本中的目录中选择随机文件的最佳方法
【发布时间】:2009-03-31 15:12:36
【问题描述】:

在 shell 脚本中从目录中选择随机文件的最佳方法是什么?

这是我在 Bash 中的解决方案,但我非常有兴趣在 Unix 上使用更便携(非 GNU)的版本。

dir='some/directory'
file=`/bin/ls -1 "$dir" | sort --random-sort | head -1`
path=`readlink --canonicalize "$dir/$file"` # Converts to full path
echo "The randomly-selected file is: $path"

有人有其他想法吗?

编辑: lhunath 对解析 ls 提出了一个很好的观点。我想这取决于你是否想要便携。如果你有 GNU findutils 和 coreutils 那么你可以这样做:

find "$dir" -maxdepth 1 -mindepth 1 -type f -print0 \
  | sort --zero-terminated --random-sort \
  | sed 's/\d000.*//g/'

哇,太有趣了!由于我说的是“随机文件”,它也更符合我的问题。老实说,现在很难想象一个 Unix 系统部署在那里安装了 GNU 但没有安装 Perl 5。

【问题讨论】:

  • bash 方式将使用 $(...) 而不是 ...
  • 好点。我有点不清楚。在实践中,我在 Linux 上使用 Bash,但理论上如果它在 Unix 上的 sh 上运行会很酷,这意味着反引号并且没有 GNU coreutils。
  • @JasonSmith $(…) 在 POSIX 中。如果您仍然有一个不支持它的外壳,请将/usr/xpg4/bin 或类似的东西放在/usr/bin 前面,然后调用/usr/bin/env sh 而不是/bin/sh。 (否则你经营的是真正的古董。)

标签: bash file shell random


【解决方案1】:
files=(/my/dir/*)
printf "%s\n" "${files[RANDOM % ${#files[@]}]}"

并且不要解析 ls。阅读http://mywiki.wooledge.org/ParsingLs

编辑:祝你好运找到可靠的非bash 解决方案。大多数文件名会因某些类型的文件名而中断,例如带有空格或换行符或破折号的文件名(这在纯 sh 中几乎是不可能的)。要在没有 bash 的情况下正确执行此操作,您需要完全迁移到 awk/perl/python/... 而不使用管道输出以进行进一步处理等。

【讨论】:

  • RANDOM 和数组是 Bash 功能,并且 OP “对 [in] 一个更便携(非 GNU)的版本在 Unix 上使用感兴趣”。
  • 感谢@lhunath,关于 ls 的观点很好理解。我更新了问题。
  • 您的示例实际上不起作用,printf "%s\n" "${files[RANDOM % ${#files}]}" 应该是printf "%s\n" "${files[RANDOM % ${#files[@]}]}" -- ${#files} 表示files 数组中第一个值的长度(strlen)。 ${#files[@]} 表示files 数组中的元素个数,这就是我们想要的。
  • 在便携式 sh 中处理任意文件名并不比在 bash 中难多少。 bash 中唯一让它变得更容易的是数组,并且仅在您需要同时操作多个文件名列表时才有用。
  • 请注意,printf 不是解决方案的一部分,除非您希望将文件名放在标准输出上,而不是作为任意命令的 arg。
【解决方案2】:

“shuf”不是可移植的吗?

shuf -n1 -e /path/to/files/*

或查找文件是否比一个目录更深:

find /path/to/files/ -type f | shuf -n1

它是 coreutils 的一部分,但您需要 6.4 或更高版本才能获得它...所以 RH/CentOS 不包含它。

【讨论】:

  • 对于需要工作的人来说真的很有用。不管是谁,不管它是不是 如何 hacky。
  • 您可以在 Mac 上使用 gshuf (brew install gshuf)。肯定适用于 Mavericks,但未在任何其他版本上测试!
  • shuf 现在在 coreutils 公式中,并以 g 为前缀(安装 coreutils 公式后键入 gshuf
  • brew install gshuf 对我不起作用,但 brew install coreutils 对我有用。
【解决方案3】:
# ******************************************************************
# ******************************************************************
function randomFile {
  tmpFile=$(mktemp)

  files=$(find . -type f > $tmpFile)
  total=$(cat "$tmpFile"|wc -l)
  randomNumber=$(($RANDOM%$total))

  i=0
  while read line;  do
    if [ "$i" -eq "$randomNumber" ];then
      # Do stuff with file
      amarok $line
      break
    fi
    i=$[$i+1]
  done < $tmpFile
  rm $tmpFile
}

【讨论】:

  • 最好附上代码的一些解释。
【解决方案4】:

类似:

let x="$RANDOM % ${#file}"
echo "The randomly-selected file is ${path[$x]}"

$RANDOM在bash中是一个特殊的变量,它返回一个随机数,然后我用模除法得到一个有效的索引,然后在数组中引用那个索引。

【讨论】:

  • 海报想要的是一个没有 Bash-isms 的解决方案。
  • @MGoDave 感觉还不错。我总是对一个好的 Bash 解决方案和一个好的 GNU-free 解决方案感兴趣,适合不同的情况和作为一种心理锻炼。
  • 而#file到底是什么?
  • @harperville ${#file} 是 bash 数组中的元素数 file
【解决方案5】:

这归结为:如何以可移植的方式在 Unix 脚本中创建随机数?

因为如果你有一个介于 1 和 N 之间的随机数,你可以使用head -$N | tail 在中间的某个位置进行剪切。不幸的是,我不知道仅使用外壳就可以做到这一点的便携式方法。如果你有 Python 或 Perl,你可以很容易地使用它们的随机支持,但是 AFAIK,没有标准的 rand(1) 命令。

【讨论】:

  • 这是一个很好的观点。 ls -1 是 Unix 上的标准,还是只是 GNU?无论如何,是的,最大的问题是获得一个随机数。我认为 Perl 是相当普遍的,因为它自 IIRC Solaris 2.6 和 HP-UX 11i 起就已成为标准
  • -1 作为 ls 的参数在 SUS2 (opengroup.org/onlinepubs/007908799/xcu/ls.html) 中是标准的。我不知道它是什么时候添加的,但我相信它在 POSIX 时代也可以使用。
  • @Chas 感谢您的链接。不过,Aaron 有一个观点,即带有换行符的文件名可能会导致问题。因此,这可能是相关的,具体取决于您是否以及如何让“平民”直接在文件系统上创建文件。
【解决方案6】:

我认为 Awk 是一个很好的获取随机数的工具。根据Advanced Bash Guide,Awk 是$RANDOM 的一个很好的随机数替换。

这是您的脚本版本,它避免了 Bash 主义和 GNU 工具。

#! /bin/sh

dir='some/directory'
n_files=`/bin/ls -1 "$dir" | wc -l | cut -f1`
rand_num=`awk "BEGIN{srand();print int($n_files * rand()) + 1;}"`
file=`/bin/ls -1 "$dir" | sed -ne "${rand_num}p"`
path=`cd $dir && echo "$PWD/$file"` # Converts to full path.  
echo "The randomly-selected file is: $path"

如果文件包含换行符,它会继承其他答案提到的问题。

【讨论】:

  • 这是个好主意。您必须扫描目录两次,如果文件数量在两次扫描之间发生变化,则会出现竞争条件,但实际上这可能没什么大不了的。
  • 是的,我相信传统的 Bourne shell 编程在许多情况下都存在根本缺陷,无论你是否尽了最大努力。输入 Bash 和 GNU coreutils 来挽救这一天。
  • Awk 确实为您提供了一个随机数,这是 POSIX 提供的唯一方法,但它是一个非常糟糕的 RNG(可预测,并且输出每秒仅更改一次)。另外,don't parse the output of ls.
【解决方案7】:

可以通过在 Bash 中执行以下操作来避免文件名中的换行:

#!/bin/sh

OLDIFS=$IFS
IFS=$(echo -en "\n\b")

DIR="/home/user"

for file in $(ls -1 $DIR)
do
    echo $file
done

IFS=$OLDIFS

【讨论】:

    【解决方案8】:

    这是一个 shell sn-p,它仅依赖于 POSIX 功能并处理任意文件名(但从选择中省略了点文件)。随机选择使用 awk,因为这就是您在 POSIX 中获得的全部内容。这是一个非常糟糕的随机数生成器,因为 awk 的 RNG 是以秒为单位的当前时间播种的(所以它很容易预测,如果你每秒调用多次,它会返回相同的选择)。

    set -- *
    n=$(echo $# | awk '{srand(); print int(rand()*$0) + 1}')
    eval "file=\$$n"
    echo "Processing $file"
    

    如果您不想忽略点文件,则需要将文件名生成代码 (set -- *) 替换为更复杂的内容。

    set -- *; [ -e "$1" ] || shift
    set .[!.]* "$@"; [ -e "$1" ] || shift
    set ..?* "$@"; [ -e "$1" ] || shift
    if [ $# -eq 0]; then echo 1>&2 "empty directory"; exit 1; fi
    

    如果您有可用的 OpenSSL,则可以使用它来生成随机字节。如果您没有,但您的系统有/dev/urandom,请将对openssl 的调用替换为dd if=/dev/urandom bs=3 count=1 2&gt;/dev/null。这是一个将n 设置为1 到$# 之间的随机值的sn-p,注意不要引入偏差。这个 sn-p 假设 $# 最多为 2^23-1。

    while
      n=$(($(openssl rand 3 | od -An -t u4) + 1))
      [ $n -gt $((16777216 / $# * $#)) ]
    do :; done
    n=$((n % $#))
    

    【讨论】:

      【解决方案9】:

      BusyBox(用于嵌入式设备)通常配置为支持$RANDOM,但它没有 bash 样式的数组或sort --random-sortshuf。因此如下:

      #!/bin/sh
      FILES="/usr/bin/*"
      for f in $FILES; do  echo "$RANDOM $f" ; done | sort -n | head -n1 | cut -d' ' -f2-
      

      注意cut -f2- 后面的“-”;这是避免截断包含空格(或您要使用的任何分隔符)的文件所必需的。

      它不会正确处理带有嵌入换行符的文件名。

      【讨论】:

        【解决方案10】:

        将命令“ls”的每一行输出放入名为 line 的关联数组中,然后选择其中一个...

        ls | awk '{ line[NR]=$0 } END { print line[(int(rand()*NR+1))]}'
        

        【讨论】:

        • 第一组花括号 { line[NR]=$0 } ,创建一个任意命名为'line'的关联数组,存储来自ls的每一行输出,以NR为索引,这是一个特殊的awk表示记录数的变量。在所有输出行都存储在数组中之后,awk 移至 END 部分。此时的 NR 等于 ls 输出的总行数。因此,我们从 NR 中选择一个随机数并检索该索引处的行。为了更好地回答OP的问题 ls 可以替换为 'find 。 -maxdepth 1 -type f'
        • 在仅限于 Unix Shell 的环境中,这非常有效。谢谢!
        【解决方案11】:

        我的 2 美分,当存在带有特殊字符的文件名时不应该中断的版本:

        #!/bin/bash --
        dir='some/directory'
        
        let number_of_files=$(find "${dir}" -type f -print0 | grep -zc .)
        let rand_index=$((1+(RANDOM % number_of_files)))
        
        printf "the randomly-selected file is: "
        find "${dir}" -type f -print0 | head -z -n "${rand_index}" | tail -z -n 1
        printf "\n"
        

        【讨论】:

        • 目录周围不需要单引号。除此之外,这是一种享受!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-13
        • 2012-01-30
        • 2020-08-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多