在Bash中,如何找到编号最小的未使用文件描述符?

时间:2011-11-28 14:35:34

标签: bash file-io

在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)

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

6 个答案:

答案 0 :(得分:59)

我知道这个帖子已经过时了,但我相信最好的答案都会丢失,对于像我这样来寻找解决方案的人来说会很有用。

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

注意:这会找到最低的未使用文件描述符&gt; 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

答案 1 :(得分:7)

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

答案 2 :(得分:5)

我修改了我的原始答案,现在有一个原始帖子的单行解决方案 以下函数可以存在于全局文件或源脚本中(例如〜/ .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

答案 3 :(得分:2)

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

  

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

基于读取fd目录完成了几个实验后,我得到了以下代码,作为与我所寻找的“最接近的匹配”。我正在寻找的实际上是一个bash one-liner,比如

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

将找到最低的,未使用的文件描述符 AND 在其上打开文件 AND 将其分配给变量。如下面的代码所示,通过引入函数“lowest_unused_fd”,我至少得到一个“双线”(FD = $(lowest_unused_fd)后跟 eval“exec $ FD&lt; $ FILENAME” )为任务。我无法编写一个像上面的(虚构的)“open_r”一样的函数。如果有人知道该怎么做,请向前迈进!相反,我不得不将任务分成两个步骤:一步到找到未使用的文件描述符,一步到打开文件就可以了。还要注意,为了能够在函数中放置 find 步骤(“lowest_unused_fd”)并将其stdout分配给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<&-"

答案 4 :(得分:2)

我需要在Mac上支持两个 bash v3,在Linux上支持bash v4,其他解决方案需要bash v4或Linux,所以我想出了一个适用于两者的解决方案,使用{{ 1}}。

/dev/fd

例如对于dup stdout,你可以这样做:

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
}

答案 5 :(得分:0)

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

我猜一个答案是使用“ zsh”,但是我想在“ bash”中有一个适用于OS X(aka 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)自动将其拆分为一组约定为@F的单词,BEGIN说在while()循环之前要做什么,而END说在此之后要做什么。 while()循环会挑选出每行的字段[3],将其缩减为仅前导数字(即端口号),并将其保存在当前使用的端口号数组中,因此然后END块会找到其插槽未被占用的最小整数。

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