【问题标题】:In Bash, how to find the lowest-numbered unused file descriptor?在 Bash 中,如何找到编号最小的未使用文件描述符?
【发布时间】:2012-01-08 00:07:04
【问题描述】:

在 Bash 脚本中,是否可以在“尚未使用的最低编号文件描述符”上打开文件?

我已经四处寻找如何做到这一点,但 Bash 似乎总是要求您指定数字,例如像这样:

exec 3< /path/to/a/file    # Open file for reading on file descriptor 3.

相比之下,我希望能够做类似的事情

my_file_descriptor=$(open_r /path/to/a/file)

这将打开“文件”以读取尚未使用的最低编号文件描述符,并将该编号分配给变量“my_file_descriptor”。

【问题讨论】:

    标签: bash file-io


    【解决方案1】:

    Apple Mac OS X 不是 Linux。我在 OS X 上看不到任何“/proc”文件系统。

    我想一个答案是使用“zsh”,但我想要一个在“bash”中同时在 OS X(又名 BSD)和 Linux 上运行的脚本。所以,我在 2020 年,使用最新版本的 OS X,此时是 Catalina,我意识到苹果似乎早就放弃了对 Bash 的维护;显然赞成Zsh。

    这是我在 Apple Mac OS X 或 Linux 上查找最低未使用文件描述符的多操作系统解决方案。我创建了一个完整的 Perl 脚本,并将其内嵌到 Shell 脚本中。一定有更好的方法,但现在,这对我有用。

    lowest_unused_fd() {
      # For "bash" version 4.1 and higher, and for "zsh", this entire function  
      # is replaced by the more modern operator "{fd}", used like this:
      #    exec {FD}>myFile.txt; echo "hello" >&$FD;
      if [ $(uname) = 'Darwin' ] ; then
        lsof -p $$ -a -d 0-32 | perl -an \
          -e 'BEGIN { our @currentlyUsedFds; };' \
          -e '(my $digits = $F[3]) =~ s/\D//g;' \
          -e 'next if $digits eq "";' \
          -e '$currentlyUsedFds[$digits] = $digits;' \
          -e 'END { my $ix; 
                for( $ix=3; $ix <= $#currentlyUsedFds; $ix++) {  
                  my $slotContents = $currentlyUsedFds[$ix];
                  if( !defined($slotContents) ) { 
                    last; 
                  } 
                } 
                print $ix;
              }' ;
      else 
        local FD=3
        while [ -e /proc/$$/fd/$FD ]; do
          FD=$((FD+1))
        done
        echo $FD
      fi;
    }
    

    Perl 的 -an 选项告诉它 (-n) 运行一个隐含的 while() 循环,该循环逐行读取文件,并且 (-a) 自动将其拆分为一个单词数组,按照惯例, 被命名为@FBEGIN 表示在 while() 循环之前要做什么,END 表示之后要做什么。while() 循环选择每行的字段 [3],减少它到它的前导数字,这是一个端口号,并将其保存在当前正在使用的端口号数组中,因此不可用。END 块然后找到其插槽未被占用的最小整数。

    更新:在完成所有这些之后,我实际上并没有在自己的代码中使用它。我意识到 KingPong 和 Bruno Bronsky 的回答要优雅得多。但是,我将保留此答案;对某人来说可能很有趣。

    【讨论】:

      【解决方案2】:

      我需要同时支持 Mac 上的 bash v3 和 Linux 上的 bash v4,而其他解决方案需要 bash v4 或 Linux,因此我想出了一个适用于两者的解决方案,使用 @987654321 @。

      find_unused_fd() {
        local max_fd=$(ulimit -n)
        local used_fds=" $(/bin/ls -1 /dev/fd | sed 's/.*\///' | tr '\012\015' '  ') "
        local i=0
        while [[ $i -lt $max_fd ]]; do
          if [[ ! $used_fds =~ " $i " ]]; then
            echo "$i"
            break
          fi
          (( i = i + 1 ))
        done
      }
      

      例如复制标准输出,你可以这样做:

      newfd=$(find_unused_fd)
      eval "exec $newfd>&1"
      

      【讨论】:

      • 我喜欢这个主意。我把它打到 1 班轮。 /bin/ls -1 /dev/fd | sed 's/.*\///' | sort -n | awk 'n&lt;$1{exit}{n=$1+1}END{print n}'
      • 非常好。从中我了解到(并确认)“/dev/fd”对于每个不同的 shell 实例都是自定义的。 @"Bruno Bronsky" 为什么是 'sed'?
      【解决方案3】:

      我知道这个帖子已经过时了,但相信缺少最佳答案,并且对像我这样来这里寻找解决方案的其他人很有用。

      Bash 和 Zsh 内置了查找未使用文件描述符的方法,而无需编写脚本。 (我没有发现破折号这样的东西,所以上面的答案可能仍然有用。)

      注意:这会找到大于 10 的最低未使用文件描述符,而不是总体 最低

      $ man bash /^REDIRECTION (paragraph 2)
      $ man zshmisc /^OPENING FILE DESCRIPTORS
      

      示例适用于 bsh 和 zsh。

      打开一个未使用的文件描述符,并将编号分配给 $FD:

      $ exec {FD}>test.txt
      $ echo line 1 >&$FD
      $ echo line 2 >&$FD
      $ cat test.txt
      line 1
      line 2
      $ echo $FD
      10  # this number will vary
      

      完成后关闭文件描述符:

      $ exec {FD}>&-
      

      以下显示文件描述符现在已关闭:

      $ echo line 3 >&$FD
      bash: $FD: Bad file descriptor
      zsh: 10: bad file descriptor
      

      【讨论】:

      • 非常感谢你没有让这个线程的年龄阻止你发布这个!事实上,您的回答是(该主题的)第一个真正提供我在大约 18 个月前发布我的问题时所寻找的内容;其他答案确实是“解决方法”。但是,在您的解决方案中使用的 {FD}> 功能在 Bash 4.0 或更早版本中不受支持。它是在 Bash 4.1-alpha 中引入的,因此其他答案中提出的变通办法对于被 Bash 4.0 或更早版本卡住的人来说可能是有价值的。
      • 不错!也适用于典型的flock 场景:( flock $FD; echo got the lock; ) {FD}&gt; mylock
      • 我一直在使用 mkfifo。不是真正的解决方案,因为先进先出没有“存储”。所以非常感谢!
      • 不幸的是在 bash 3 中不起作用。bash: exec: {FD}: not found
      【解决方案4】:

      我修改了我原来的答案,现在有一个原始帖子的单行解决方案。
      以下函数可以存在于全局文件或源脚本中(例如 ~/.bashrc):

      # Some error code mappings from errno.h
      readonly EINVAL=22   # Invalid argument
      readonly EMFILE=24   # Too many open files
      
      # Finds the lowest available file descriptor, opens the specified file with the descriptor
      # and sets the specified variable's value to the file descriptor.  If no file descriptors
      # are available the variable will receive the value -1 and the function will return EMFILE.
      #
      # Arguments:
      #   The file to open (must exist for read operations)
      #   The mode to use for opening the file (i.e. 'read', 'overwrite', 'append', 'rw'; default: 'read')
      #   The global variable to set with the file descriptor (must be a valid variable name)
      function openNextFd {
          if [ $# -lt 1 ]; then
              echo "${FUNCNAME[0]} requires a path to the file you wish to open" >&2
              return $EINVAL
          fi
      
          local file="$1"
          local mode="$2"
          local var="$3"
      
          # Validate the file path and accessibility
          if [[ "${mode:='read'}" == 'read' ]]; then
              if ! [ -r "$file" ]; then
                  echo "\"$file\" does not exist; cannot open it for read access" >&2
                  return $EINVAL
              fi
          elif [[ !(-w "$file") && ((-e "$file") || !(-d $(dirname "$file"))) ]]; then
              echo "Either \"$file\" is not writable (and exists) or the path is invalid" >&2
              return $EINVAL
          fi
      
          # Translate mode into its redirector (this layer of indirection prevents executing arbitrary code in the eval below)
          case "$mode" in
              'read')
                  mode='<'
                  ;;
              'overwrite')
                  mode='>'
                  ;;
              'append')
                  mode='>>'
                  ;;
              'rw')
                  mode='<>'
                  ;;
              *)
                  echo "${FUNCNAME[0]} does not support the specified file access mode \"$mode\"" >&2
                  return $EINVAL
                  ;;
          esac
      
          # Validate the variable name
          if ! [[ "$var" =~ [a-zA-Z_][a-zA-Z0-9_]* ]]; then
              echo "Invalid variable name \"$var\" passed to ${FUNCNAME[0]}" >&2
              return $EINVAL
          fi
      
          # we'll start with 3 since 0..2 are mapped to standard in, out, and error respectively
          local fd=3
          # we'll get the upperbound from bash's ulimit
          local fd_MAX=$(ulimit -n)
          while [[ $fd -le $fd_MAX && -e /proc/$$/fd/$fd ]]; do
              ((++fd))
          done
      
          if [ $fd -gt $fd_MAX ]; then
              echo "Could not find available file descriptor" >&2
              $fd=-1
              success=$EMFILE
          else
              eval "exec ${fd}${mode} \"$file\""
              local success=$?
              if ! [ $success ]; then
                  echo "Could not open \"$file\" in \"$mode\" mode; error: $success" >&2
                  fd=-1
              fi
          fi
      
          eval "$var=$fd"
          return $success;
      }
      

      可以使用上述函数如下打开文件进行输入和输出:

      openNextFd "path/to/some/file" "read" "inputfile"
      # opens 'path/to/some/file' for read access and stores
      # the descriptor in 'inputfile'
      
      openNextFd "path/to/other/file" "overwrite" "log"
      # truncates 'path/to/other/file', opens it in write mode, and
      # stores the descriptor in 'log'
      

      然后像往常一样使用前面的描述符来读取和写入数据:

      read -u $inputFile data
      echo "input file contains data \"$data\"" >&$log
      

      【讨论】:

      • 感谢 Coren,感谢您花费时间和精力进行这项令人印象深刻的彻底调查!作为问题的所有者,我已将您的答案标记为“已接受”。然而,令人着迷的是,解决这个问题是如此困难!
      • Coren,只是在这里添加评论,所以也许你会收到通知并来到这里。以为您会对 Weldabar 的解决方案感兴趣,如果使用 Bash 4.1-alpha 或更高版本,这绝对是解决问题的方法。 (Bash 4.0 或更早版本不支持 Weldabar 的解决方案。)
      【解决方案5】:

      在 2011 年 11 月 29 日 Basile Starynkevitch 对这个问题的回答中,他写道:

      如果是在Linux上,你可以随时读取/proc/self/fd/目录来找出使用的文件描述符。

      在阅读 fd 目录的基础上做了几个实验,我得到了以下代码,作为我正在寻找的“最接近的匹配”。我正在寻找的实际上是一个 bash 单线,比如

      my_file_descriptor=$(open_r /path/to/a/file)
      

      它会找到最低的、未使用的文件描述符AND打开它上面的文件AND将它分配给变量。如下代码所示,通过引入函数“lowest_unused_fd”,我至少得到了一个“双线”(FD=$(lowest_unused_fd)后跟 eval "exec $FD找到未使用的文件描述符,一步打开上面的文件。另请注意,为了能够将 find 步骤放入函数(“lowest_unused_fd”)并将其标准输出分配给 FD,我必须使用“/proc/$$/fd”而不是“/proc/self/fd”(在 Basile Starynkevitch 的建议中),因为 bash 为函数的执行生成了一个子 shell。

      #!/bin/bash
      
      lowest_unused_fd () {
          local FD=0
          while [ -e /proc/$$/fd/$FD ]; do
              FD=$((FD+1))
          done
          echo $FD
      }
      
      FILENAME="/path/to/file"
      
      #  Find the lowest, unused file descriptor
      #+ and assign it to FD.
      FD=$(lowest_unused_fd)
      
      # Open the file on file descriptor FD.
      if ! eval "exec $FD<$FILENAME"; then
          exit 1
      fi
      
      # Read all lines from FD.
      while read -u $FD a_line; do
          echo "Read \"$a_line\"."
      done
      
      # Close FD.
      eval "exec $FD<&-"
      

      【讨论】:

      • 很高兴这个答案仍然存在,仍然需要它。这一年是 2020 年,我有最新更新的 Max OS X (Catalina),但我的 Bash 版本仍然不是 4.1 或更高版本。它停留在 3.2.57。我认为 Apple 改用了“zsh”,但这不是借口。
      【解决方案6】:

      如果是在Linux上,你可以随时读取/proc/self/fd/目录来找出使用的文件描述符。

      【讨论】:

      • 感谢您为我指明方向,巴西尔!我随后使用它并为这个问题写了我自己的答案。
      • 是的,如果您不在 Linux 上,您可以使用/dev/fd。例如,请参阅我的答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-26
      • 1970-01-01
      • 2012-01-07
      • 1970-01-01
      • 1970-01-01
      • 2011-09-26
      相关资源
      最近更新 更多