Feature #14007
closedopen mode 'x' to raise error if file exists
Description
I propose (and attach a patch) to add a mode 'x' for Kernel#open, File.open, and similar methods. Mode 'wx' or 'ax' would create a new file, or raise an error if the file exists. Mode 'x' would be a shortcut for IO::EXCL. It would work like mode 'x' of fopen(3) in C.
# Create file.txt, or raise an error if it exists.
open('file.txt', 'wx') {|f| f.puts("Some text") }
Background¶
Mode 'x' appears in the fopen(3) manuals of Linux, Illumos, and multiple BSDs. In all these systems, mode 'x' to fopen(3) acts like flag O_EXCL to open(2). Linux fopen(3) describes 'x' as an extension in glibc, but 'x' now appears in other systems. FreeBSD fopen(3) and cppreference.com describe 'wx' as a C11 standard feature.
- Linux: http://man7.org/linux/man-pages/man3/fopen.3.html
- FreeBSD: https://www.freebsd.org/cgi/man.cgi?query=fopen&apropos=0&sektion=0&manpath=FreeBSD+11-current&arch=default&format=html
- http://en.cppreference.com/w/c/io/fopen
Mode 'x' is a shortcut in C programs. Without 'x', program would need to call open(2) then fdopen(3):
/* short way */
fp = fopen("file.txt", "wx");
if (!fp) ...
/* long way */
fd = open("file.txt", O_WRONLY|O_CREAT|O_EXCL, 0666);
if (fd == -1) ...
fp = fdopen(fd, "w");
if (!fp) { close(fd); ... }
Some C libraries also have a mode 'e' to set close-on-exec when opening the file. I don't propose to add mode 'e' to Ruby, because Ruby sets close-on-exec by default on most files, so I would never need to use 'e' in Ruby. NetBSD also has a mode 'f' to open only regular files, but my patch doesn't add 'f' to Ruby. (NetBSD fopen(3): http://netbsd.gw.com/cgi-bin/man-cgi?fopen)
Proposal¶
I propose to add mode 'x' to Ruby. If the mode is 'wx' or 'ax', then Ruby would pass O_EXCL to open(2). It would create the file, or raise an error if the file exists.
Mode 'x' would be a shortcut in Ruby, but the benefit is less than in C, because Ruby provides other ways to pass O_EXCL.
# with 'x'
open('file.txt', 'wx') { ... }
# short way without 'x'
open('file.txt', 'w', flags: IO::EXCL) { ... }
# long way without 'x'
open('file.txt', IO::WRONLY|IO::CREAT|IO::EXCL) { ... }
I wouldn't need 'x' to create a temporary file (because Ruby's Tempfile handles that), but I might use 'x' in other places, like IRB, if I didn't want to modify an existing file by mistake. One can also use 'x' when spawning external commands.
# Don't clobber /tmp/example if it exists.
system 'dmesg', out: ['/tmp/example', 'wx']
I also propose that 'rx' would raise ArgumentError in Ruby, because 'rx' would probably be a mistake of the Ruby programmer. Without the error, 'rx' would pass O_EXCL without O_CREAT and cause undefined behavior in open(2). I don't want easy undefined behavior, so my patch doesn't allow 'rx' in Ruby. It allows 'wx' and 'ax' because they pass both O_CREAT and O_EXCL.
irb(main):011:0> File.read('/tmp/example', mode: 'rx')
ArgumentError: can't use mode "x" with "r"
from (irb):11:in `read'
from (irb):11:in `<top (required)>'
from /home/kernigh/prefix/bin/irb:11:in `<main>'
One can bypass this 'rx' check by not using 'x' in the mode string. For example, open('file.txt', 'r', flags: IO::EXCL)
passes O_EXCL without O_CREAT, both before and after my patch.
Implementation¶
My patch
- defines FMODE_EXCL in the public header
ruby/io.h
. (I'm not sure how to pick a value; I picked 0x00002000.) - edits a few functions in io.c, so
- rb_io_modestr_fmode() accepts 'x' and rejects 'rx'. It translates 'x' to FMODE_EXCL.
- rb_io_oflags_fmode() translates O_EXCL to FMODE_EXCL. (I'm not sure if this part is needed.)
- rb_io_fmode_oflags() translates FMODE_EXCL to O_EXCL.
- adds 'x' to the document for IO.new.
- specifies 'x' in Ruby spec suite. The only tested method is File.open.
To run the tests,
$ make test-spec SPECOPTS=core/file/open_spec.rb
My patch doesn't change make test-all
. (Because I run OpenBSD, I have some difficulty running Ruby's tests. The FIFO tests can get stuck because of OpenBSD's bug. I have hacked those tests to fail instead of getting stuck, using the diff at https://gist.github.com/kernigh/5770f8b90427ce6ede535dae729cb960)
My patch assumes that O_EXCL works on every system. Ruby made this assumption before me. Ruby always defines File::Constants::EXCL
in file.c. Also, the Tempfile library doesn't work unless the system knows O_EXCL.
Odd behavior of 'x'¶
The document for 'x' in my patch says only,
"x" Exclusive open
Creates a new file, or raises an error if the file
exists. Mode "x" is available since Ruby 2.5.
This document might be too brief. It doesn't mention that "x" acts like File::EXCL. It also doesn't describe some oddities of 'x', like how 'rx' raises an ArgumentError, or when 'x' is ignored.
With my patch, Ruby ignores 'x' if it isn't calling open(2). For example, opening a process always ignores 'x'. That's not too bad, because Ruby creates a new process.
# ignores 'x'
open('|cat', 'wx') {|f| f.puts "Hi" }
Also, Ruby ignores 'x' when opening a file descriptor. That's odd, because 'x' should never open an existing file, but it can do so if we use fd.
fd = IO.sysopen('/tmp/example', 'a')
# ignores 'x', also doesn't truncate file
IO.open(f, 'wx') {|f| f.puts('the last line') }
It may seem strange that my patch raises ArgumentError for 'rx', but ignores other strange uses of 'x'.
Files