В Bash, как найти наименее нумерованный неиспользуемый дескриптор файла?

В Bash - script можно ли открыть файл на "дескрипторе файла с наименьшим номером, который еще не используется"?

Я посмотрел, как это сделать, но кажется, что 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".

Ответ 1

Я знаю, что эта ветка устарела, но считаю, что лучший ответ отсутствует, и будет полезен для других, таких как я, которые приходят сюда искать решение.

Bash и Zsh построили способы поиска неиспользуемых дескрипторов файлов, без необходимости писать скрипты. (Я не нашел такой вещи для тире, поэтому приведенные выше ответы могут быть полезны.)

Примечание: это находит наименьший неиспользуемый дескриптор файлa > 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

Ответ 2

Если это в Linux, вы всегда можете прочитать каталог /proc/self/fd/, чтобы узнать используемые дескрипторы файлов.

Ответ 3

Я пересмотрел свой первоначальный ответ и теперь имею однострочное решение для оригинального сообщения.
Следующая функция могла бы работать в глобальном файле или источника script (например, ~/.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 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 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

Ответ 4

В Basile Starynkevitch ответ на этот вопрос, 29 ноября 2011 года, он пишет:

Если он находится в Linux, вы всегда можете прочитать каталог /proc/self/fd/, чтобы узнать используемые дескрипторы файлов.

Сделав несколько экспериментов, основанных на чтении каталога fd, я пришел к следующему коду, как к "ближайшему совпадению" с тем, что я искал. То, что я искал, на самом деле было bash с одним слоем, например

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

который найдет наименьший неиспользуемый дескриптор файла И откройте файл на нем И присвойте его переменной. Как видно из кода ниже, вводя функцию "lower_unused_fd", я, по крайней мере, получаю "двухстрочный" (FD = $(lower_unused_fd), за которым следует eval "exec $FD < $FILENAME" ) для задачи. Я НЕ смог написать функцию, которая работает как (мнимая) "open_r" выше. Если кто-то знает, как это сделать, сделайте шаг вперед! Вместо этого мне пришлось разделить задачу на два шага: один шаг, чтобы найти неиспользуемый дескриптор файла и один шаг, чтобы открыть файл на нем. Также обратите внимание, что, чтобы иметь возможность поместить шаг поиска в функцию ( "lower_unused_fd" ) и присвоить ее stdout для FD, мне пришлось использовать "/proc/$$/fd" вместо "/proc/self/fd" (как в предположении Базиля Старинкевича), поскольку bash порождает подоболочку для выполнения функции.

#!/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<&-"

Ответ 5

Мне нужно было поддерживать как bash v3 на Mac, так и bash v4 на Linux, а для других решений требуется либо bash v4, либо Linux, поэтому я придумал решение, которое работает для обоих, используя /dev/fd.

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
}

Например, для дублирования stdout вы можете:

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