【发布时间】:2012-08-24 15:34:49
【问题描述】:
我发现了类似的问题,但在 Linux/Bash 中没有
我希望我的脚本创建一个具有给定名称的文件(通过用户输入),但如果文件名已经存在,则在末尾添加数字。
例子:
$ create somefile
Created "somefile.ext"
$ create somefile
Created "somefile-2.ext"
【问题讨论】:
我发现了类似的问题,但在 Linux/Bash 中没有
我希望我的脚本创建一个具有给定名称的文件(通过用户输入),但如果文件名已经存在,则在末尾添加数字。
例子:
$ create somefile
Created "somefile.ext"
$ create somefile
Created "somefile-2.ext"
【问题讨论】:
没有循环并且不使用正则表达式或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
【讨论】:
以下脚本可以帮助您。您不应该同时运行脚本的多个副本以避免竞争条件。
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,以便成为文件的“第二个”副本。谢谢!
[[ -e … ]]断定它不存在之后被另一个程序创建和使用,但在touch实际创建它之前。这是一个很大的陷阱,因为它违背了目的,而且问题通常可能从您开始并行使用脚本的那一刻起出现。请参阅@StephaneChazelas 的回答,了解避免(大多数)竞争条件的解决方案。
为了避免竞争条件:
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) 的调用会失败?
使用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
【讨论】:
更简单:
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。
ls in scripts 但这似乎是无害的。对于风格点,将ls 替换为printf '%s\n'(或者甚至printf '.\n',这对于包含换行符的文件名来说是健壮的)。
将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")"
【讨论】:
[[ -e .... ]] 已经评论过stat(2),您需要[[ -e ... || -L ... ]] 来搜索文件是否存在,但即使这样也存在竞争条件,最好用O_EXCL 让系统保证您没有使用已经存在的文件(或者更糟糕的是指向敏感文件的符号链接)。
试试这样的
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 的值,它仍然失败。对于 name 的值,它可能会在 //file 特殊的系统上失败,例如 /file。它不能防止符号链接损坏的$names。并且有一个 TOCTOU 竞争条件(注意它在 zsh 中中断,其中 $path 是绑定到 $PATH 的特殊数组)
backup.sh:$ ./backup.sh blah.txt时,下面的命令什么也不做。我希望有一个名为“blah2.txt”或类似名称的新文件。
这是我用来增量创建目录的更好方法。
文件名也可以调整。
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
【讨论】:
echo 和使用大写的私有变量,这些是保留给系统使用的。
尝试这样的事情(未经测试,但你明白了):
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 的调用替换为创建您正在使用的文件所需的任何代码。
【讨论】: