【问题标题】:Create new file but add number if filename already exists in bash如果 bash 中已经存在文件名,则创建新文件但添加数字
【发布时间】:2012-08-24 15:34:49
【问题描述】:

我发现了类似的问题,但在 Linux/Bash 中没有

我希望我的脚本创建一个具有给定名称的文件(通过用户输入),但如果文件名已经存在,则在末尾添加数字。

例子:

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

【问题讨论】:

    标签: linux bash filenames


    【解决方案1】:

    没有循环并且不使用正则表达式或shell expr。

    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
    

    【讨论】:

      【解决方案2】:

      以下脚本可以帮助您。您不应该同时运行脚本的多个副本以避免竞争条件。

      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
      

      【讨论】:

      • 它有效,但我将初始 i 更改为 2,以便成为文件的“第二个”副本。谢谢!
      • 我将它与 else 条件一起用于循环中的第一段
      • 如果列表可能会增长到很多文件,则不会使用此选项,因为它会越来越慢
      • 这段代码有一个竞争条件:你决定的文件名可能在[[ -e … ]]断定它不存在之后被另一个程序创建和使用,但在touch实际创建它之前。这是一个很大的陷阱,因为它违背了目的,而且问题通常可能从您开始并行使用脚本的那一刻起出现。请参阅@StephaneChazelas 的回答,了解避免(大多数)竞争条件的解决方案。
      • @Maëlan,请注意,竞争条件已经在答案中注明。
      【解决方案3】:

      为了避免竞争条件

      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 不会失败)并且在大多数 shell 中本身都有竞争条件。

      shell 首先对文件执行stat(2) 以检查它是否是常规文件(fifo、目录、设备...)。只有当文件不存在(尚)或者是常规文件时,3> "$file" 才会使用 O_EXCL 标志来保证不会破坏文件。

      因此,如果存在同名的 fifo 或设备文件,则将使用它(前提是它可以以只写方式打开),并且如果创建常规文件作为 fifo/设备的替代品,则它可能会被破坏/directory... 在 stat(2)open(2) 之间,没有 O_EXCL!

      改变

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

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

      将避免使用已经存在的非常规文件,但不解决竞争条件。

      现在,这只是面对想要让您覆盖文件系统上的任意文件的恶意对手时真正需要考虑的问题。在同一脚本的两个实例同时运行的正常情况下,它确实消除了竞争条件。因此,在这方面,它比仅使用 [ -e "$file" ] 预先检查文件是否存在的方法要好。

      对于完全没有竞争条件的工作版本,您可以使用zsh shell 而不是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
      

      【讨论】:

      • 这是最好的也是唯一可以接受的答案,因为它是唯一一个处理竞争条件的答案。
      • 如果我理解正确,有两个问题。除了stat(2)open(2) 之间的竞争条件(如您所说的不太可能)之外,还有碰巧是可写的非常规文件的问题(fifo、设备……或到其中一种的符号链接) )。如果"$file" 已经存在并且属于这种类型,则循环将(?)停止而不是尝试另一个文件名。这种情况不应该通过将试探性重定向与测试&& [ -f "$file" ](在重定向成功之后)结合起来来处理吗?还是对open(2) 的调用会失败?
      【解决方案4】:

      使用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
      • 在管道和增量中组合所有内容(上面的完整表达式)

      【讨论】:

        【解决方案5】:

        更简单:

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

        你会得到:

        $ ls file*
        file0.ext  file1.ext  file2.ext  file3.ext  file4.ext  file5.ext  file6.ext
        

        【讨论】:

        • 如果我希望第一个输出是 file1.ext,它将如何工作?
        • 很好,如果(大如果)你确定你永远不会有不连续的数字,即如果我在你的例子中删除了file2.txt,我现在会破坏file6.txt
        • 一般情况下你应该avoid using ls in scripts 但这似乎是无害的。对于风格点,将ls 替换为printf '%s\n'(或者甚至printf '.\n',这对于包含换行符的文件名来说是健壮的)。
        【解决方案6】:

        choroba's answer 简单重新打包为一个泛化函数:

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

        【讨论】:

        • 正如@Choroba 的答案[[ -e .... ]] 已经评论过stat(2),您需要[[ -e ... || -L ... ]] 来搜索文件是否存在,但即使这样也存在竞争条件,最好用O_EXCL 让系统保证您没有使用已经存在的文件(或者更糟糕的是指向敏感文件的符号链接)。
        【解决方案7】:

        试试这样的

        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
        

        【讨论】:

        • 这应该是公认的答案,因为它确实处理了所有事情,包括在扩展之前添加后缀!
        • @sorin,对于以- 开头的$name 的值,它仍然失败。对于 name 的值,它可能会在 //file 特殊的系统上失败,例如 /file。它不能防止符号链接损坏的$names。并且有一个 TOCTOU 竞争条件(注意它在 zsh 中中断,其中 $path 是绑定到 $PATH 的特殊数组)
        • 当上面的内容被复制到backup.sh:$ ./backup.sh blah.txt时,下面的命令什么也不做。我希望有一个名为“blah2.txt”或类似名称的新文件。
        【解决方案8】:

        这是我用来增量创建目录的更好方法。

        文件名也可以调整。

        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
        

        【讨论】:

        • 这有许多不幸的反模式,包括useless use of echo 和使用大写的私有变量,这些是保留给系统使用的。
        【解决方案9】:

        尝试这样的事情(未经测试,但你明白了):

        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 的调用替换为创建您正在使用的文件所需的任何代码。

        【讨论】:

          猜你喜欢
          • 2012-11-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-08-31
          • 2015-03-31
          相关资源
          最近更新 更多