我在 Stack Overflow 上找到了类似的问题,但不是关于 Linux/Bash 的。
我想让我的脚本创建一个具有给定名称的文件(通过用户输入),但是如果文件名已经存在,则在结尾添加数字。
例如:
$ create somefile
Created "somefile.ext"
$ create somefile
Created "somefile-2.ext"
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
更容易:
touch file`ls file* | wc -l`.ext
您将获得:
$ ls file*
file0.ext file1.ext file2.ext file3.ext file4.ext file5.ext file6.ext
file2.txt
,那么现在会覆盖 file6.txt
。 - Ulrich Schwarzls
,但这似乎相当无害。为了风格上的考虑,可以将ls
替换为printf '%s\n'
(甚至是printf '.\n'
,这对包含换行符的文件名具有鲁棒性)。 - tripleee为了避免竞态条件:
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或设备文件,并且可以以只写模式打开,那么它将被使用,并且如果在stat(2)
和open(2)
之间替换了一个FIFO/设备/目录...,则可能会覆盖常规文件而创建新的文件!
更改
{ command exec 3> "$file"; } 2> /dev/null
to
[ ! -e "$file" ] && { command exec 3> "$file"; } 2> /dev/null
建议避免使用已存在的非正则文件,但不解决竞争条件问题。
现在,只有在面对恶意攻击者想让您在文件系统上覆盖任意文件时才会真正担心这个问题。它确实消除了同一脚本的两个实例同时运行的正常情况下的竞争条件。因此,在这方面,它比仅事先检查文件是否存在的方法更好,如 [ -e "$file" ]
。
要完全消除竞争条件的可工作版本,可以使用 zsh
shell 而不是 bash
,它具有 sysopen
内置于 zsh/system
模块中的 open()
原始接口。
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)
是否仍会失败? - Maëlan试试这个方法
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
$name
值仍然会失败。在//file
是特殊的系统上,像/file
这样的name
值也可能失败。它不能保护那些已经损坏的符号链接的$name
值。并且存在TOCTOU竞争条件(请注意,在zsh中,$path
是绑定到$PATH
的特殊数组,因此它会中断)。 - Stephane Chazelasbackup.sh
中时,以下命令不起作用:$ ./backup.sh blah.txt
。我期望会生成一个名为“blah2.txt”或类似的新文件。 - Tom Russell使用 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
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
调用,以创建你正在使用的文件。
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
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")"
[[ -e .... ]]
执行了stat(2)
,你需要使用[[ -e ... || -L ... ]]
来搜索文件是否存在,但即使这样仍存在竞态条件,更好的方法是使用O_EXCL打开文件,以确保系统不会使用已经存在的文件(或更糟糕的是敏感文件的符号链接)。 - Stephane Chazelas不使用循环,也不使用正则表达式或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
i
改为了 2,以便成为文件的“第二”份副本。谢谢! - heltonbiker$name
用双引号括起来。引用.ext
是可能的,但不会改变任何内容。 - choroba