Создайте новый файл, но добавьте номер, если имя файла уже существует в bash

Я нашел похожие вопросы, но не в Linux/Bash

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

Пример:

$ create somefile
Created "somefile.ext"
$ create somefile
Created "somefile-2.ext"

person heltonbiker    schedule 29.08.2012    source источник


Ответы (9)


Следующий скрипт может вам помочь. Вы не должны запускать несколько копий скрипта одновременно, чтобы избежать состояния гонки.

name=somefile
if [[ -e $name.ext || -L $name.ext ]] ; then
    i=0
    while [[ -e $name-$i.ext || -L $name-$i.ext ]] ; do
        let i++
    done
    name=$name-$i
fi
touch -- "$name".ext
person choroba    schedule 29.08.2012
comment
Это сработало, но я изменил начальную i на 2, чтобы это была вторая копия файла. Спасибо! - person heltonbiker; 30.08.2012
comment
Я использовал его с условием else для первого прохода в цикле. - person PatriceG; 24.11.2015
comment
Не использовал бы это, если бы список мог вырасти до большого количества файлов, так как он будет работать медленнее. - person clearlight; 30.08.2018
comment
Этот код имеет состояние гонки: выбранное вами имя файла могло быть создано и использовано другой программой после того, как [[ -e … ]] сделает вывод, что он не существует, но до того, как touch фактически его создаст. Это большая ловушка, так как она противоречит самой цели, и проблема обычно может возникнуть с того момента, как вы начнете использовать свой скрипт параллельно. См. ответ @StephaneChazelas для решения, которое позволяет избежать (большинства) условий гонки. - person Maëlan; 08.02.2020
comment
@ Maëlan, обратите внимание, что состояние гонки уже было указано в ответе. - person Stephane Chazelas; 08.02.2020

Полегче:

touch file`ls file* | wc -l`.ext

Ты получишь:

$ ls file*
file0.ext  file1.ext  file2.ext  file3.ext  file4.ext  file5.ext  file6.ext
person Hugoren Martinako    schedule 05.04.2018
comment
Как это будет работать, если я хочу, чтобы первым выводом был file1.ext? - person Uzi; 13.11.2018
comment
Хорошо, если (большое если) вы уверены, что у вас никогда не будет там непоследовательных чисел, то есть, если я когда-нибудь удалю file2.txt в вашем примере, я теперь удалю file6.txt. - person Ulrich Schwarz; 03.12.2018
comment
Как правило, вам следует избегать использования ls в сценариях, но это кажется довольно безобидным. Для стилистических моментов замените ls на printf '%s\n' (или даже printf '.\n', который устойчив к именам файлов с символами новой строки). - person tripleee; 11.01.2019

Чтобы избежать условий гонки:

name=some-file

n=
set -o noclobber
until
  file=$name${n:+-$n}.ext
  { command exec 3> "$file"; } 2> /dev/null
do
  ((n++))
done
printf 'File is "%s"\n' "$file"
echo some text in it >&3

И вдобавок у вас файл открыт для записи на fd 3.

С помощью bash-4.4+ вы можете сделать это функцией, например:

create() { # fd base [suffix [max]]]
  local fd="$1" base="$2" suffix="${3-}" max="${4-}"
  local n= file
  local - # ash-style local scoping of options in 4.4+
  set -o noclobber
  REPLY=
  until
    file=$base${n:+-$n}$suffix
    eval 'command exec '"$fd"'> "$file"' 2> /dev/null
  do
    ((n++))
    ((max > 0 && n > max)) && return 1
  done
  REPLY=$file
}

Для использования, например, как:

create 3 somefile .ext || exit
printf 'File: "%s"\n' "$REPLY"
echo something >&3
exec 3>&- # close the file

Значение max можно использовать для защиты от бесконечных циклов, когда файлы не могут быть созданы по другой причине, кроме noclobber.

Обратите внимание, что noclobber применяется только к оператору >, а не к >> или <>.

Оставшееся состояние гонки

На самом деле noclobber не во всех случаях устраняет состояние гонки. Он только предотвращает затирание обычных файлов (но не других типов файлов, так что, например, cmd > /dev/null не дает сбоев) и сам имеет состояние гонки в большинстве оболочек.

Оболочка сначала делает stat(2) в файле, чтобы проверить, является ли он обычным файлом или нет (fifo, каталог, устройство...). Только если файл не существует (пока) или является обычным файлом, 3> "$file" использует флаг O_EXCL, чтобы гарантировать отсутствие затирания файла.

Таким образом, если есть fifo или файл устройства с таким именем, он будет использоваться (при условии, что он может быть открыт только для записи), и обычный файл может быть стерт, если он будет создан в качестве замены для fifo/device/directory. .. между этими stat(2) и open(2) без O_EXCL!

Изменение

  { command exec 3> "$file"; } 2> /dev/null

to

  [ ! -e "$file" ] && { command exec 3> "$file"; } 2> /dev/null

Избегает использования уже существующего нестандартного файла, но не устраняет состояние гонки.

Теперь это действительно проблема только перед лицом злонамеренного противника, который захочет заставить вас перезаписать произвольный файл в файловой системе. Это устраняет состояние гонки в обычном случае, когда два экземпляра одного и того же скрипта выполняются одновременно. Таким образом, это лучше, чем подходы, которые предварительно проверяют наличие файла только с помощью [ -e "$file" ].

Для рабочей версии вообще без состояния гонки вы можете использовать оболочку zsh вместо bash, которая имеет необработанный интерфейс к open() как встроенная sysopen в модуле zsh/system:

zmodload zsh/system

name=some-file

n=
until
  file=$name${n:+-$n}.ext
  sysopen -w -o excl -u 3 -- "$file" 2> /dev/null
do
  ((n++))
done
printf 'File is "%s"\n' "$file"
echo some text in it >&3
person Stephane Chazelas    schedule 30.08.2012
comment
Это лучший и единственный приемлемый ответ, поскольку только он учитывает условия гонки. - person Maëlan; 08.02.2020
comment
Если я вас правильно понял, есть два вопроса. Помимо состояния гонки между stat(2) и open(2), что, как вы сказали, маловероятно, существует также проблема с нестандартными файлами, доступными для записи (fifo, устройство… или символическая ссылка на один из этих типов). Если "$file" уже существует и относится к такому типу, то цикл (?) остановится вместо того, чтобы пытаться использовать другое имя файла. Разве этот случай не должен быть обработан путем объединения предварительного перенаправления с проверкой && [ -f "$file" ] (выходящей после успешного перенаправления)? Или вызов open(2) все равно не удастся? - person Maëlan; 08.02.2020

Попробуйте что-нибудь вроде этого

name=somefile
path=$(dirname "$name")
filename=$(basename "$name")
extension="${filename##*.}"
filename="${filename%.*}"
if [[ -e $path/$filename.$extension ]] ; then
    i=2
    while [[ -e $path/$filename-$i.$extension ]] ; do
        let i++
    done
    filename=$filename-$i
fi
target=$path/$filename.$extension
person Gyuha Shin    schedule 15.02.2016
comment
Это должен быть принятый ответ, потому что он заботится обо всем, включая добавление суффикса перед расширением! - person sorin; 10.05.2017
comment
@sorin, он по-прежнему не работает для значений $name, начинающихся с -. Это может привести к сбою для значений name, таких как /file, в системах, где //file является особым. Он не защищает от $name, которые являются неработающими символическими ссылками. И имеет состояние гонки TOCTOU (обратите внимание, что оно ломается в zsh, где $path — это специальный массив, привязанный к $PATH) - person Stephane Chazelas; 03.05.2018
comment
Следующая команда ничего не делает, когда вышеуказанное содержимое копируется в backup.sh: $ ./backup.sh blah.txt. Я бы ожидал, что новый файл будет иметь имя blah2.txt или что-то в этом роде. - person Tom Russell; 23.07.2018

Используйте touch или что хотите вместо echo:

echo file$((`ls file* | sed -n 's/file\([0-9]*\)/\1/p' | sort -rh | head -n 1`+1))

Объяснение частей выражения:

  • список файлов по шаблону: ls file*
  • брать только числовую часть в каждой строке: sed -n 's/file\([0-9]*\)/\1/p'
  • применить обратную человеческую сортировку: sort -rh
  • взять только первую строку (т.е. максимальное значение): head -n 1
  • объединить все в канал и увеличить (полное выражение выше)
person Serg Stetsuk    schedule 10.05.2018

Попробуйте что-то вроде этого (не проверено, но вы поняли идею):

filename=$1

# If file doesn't exist, create it
if [[ ! -f $filename ]]; then
    touch $filename
    echo "Created \"$filename\""
    exit 0
fi

# If file already exists, find a similar filename that is not yet taken
digit=1
while true; do
    temp_name=$filename-$digit
    if [[ ! -f $temp_name ]]; then
        touch $temp_name
        echo "Created \"$temp_name\""
        exit 0
    fi
    digit=$(($digit + 1))
done

В зависимости от того, что вы делаете, замените вызовы touch любым кодом, необходимым для создания файлов, с которыми вы работаете.

person bta    schedule 29.08.2012

Это гораздо лучший метод, который я использовал для постепенного создания каталогов.

Это также может быть скорректировано для имени файла.

LAST_SOLUTION=$(echo $(ls -d SOLUTION_[[:digit:]][[:digit:]][[:digit:]][[:digit:]] 2> /dev/null) | awk '{ print $(NF) }')
if [ -n "$LAST_SOLUTION" ] ; then
    mkdir SOLUTION_$(printf "%04d\n" $(expr ${LAST_SOLUTION: -4} + 1))
else
    mkdir SOLUTION_0001
fi
person Oliver    schedule 27.02.2014
comment
Это имеет ряд неудачных антипаттернов, в том числе бесполезное использование echo и использование прописных букв для частных переменных, которые зарезервированы для системного использования. - person tripleee; 11.01.2019

Простая переупаковка ответа choroba в виде обобщенной функции:

autoincr() {
    f="$1"
    ext=""

    # Extract the file extension (if any), with preceeding '.'
    [[ "$f" == *.* ]] && ext=".${f##*.}"

    if [[ -e "$f" ]] ; then
        i=1
        f="${f%.*}";

        while [[ -e "${f}_${i}${ext}" ]]; do
            let i++
        done

        f="${f}_${i}${ext}"
    fi
    echo "$f"
}

touch "$(autoincr "somefile.ext")"
person SamWN    schedule 14.10.2016
comment
Как уже отмечалось в ответе @Choroba, [[ -e .... ]] выполняет stat(2), вам нужно [[ -e ... || -L ... ]] для поиска существования файла, но даже в этом случае есть состояние гонки, лучше открыть файл с O_EXCL для системы, чтобы гарантировать, что вы не используете уже существующий файл (или, что еще хуже, символическая ссылка на конфиденциальный файл). - person Stephane Chazelas; 02.11.2016

без циклов и без использования регулярных выражений или выражений оболочки.

last=$(ls $1* | tail -n1)
last_wo_ext=$($last | basename $last .ext)
n=$(echo $last_wo_ext | rev | cut -d - -f 1 | rev)
if [ x$n = x ]; then
    n=2
else
    n=$((n + 1))
fi
echo $1-$n.ext

более простой без расширения и исключения -1.

n=$(ls $1* | tail -n1 | rev | cut -d - -f 1 | rev)
n=$((n + 1))
echo $1-$n.ext
person kuri65536    schedule 27.12.2020