Project

General

Profile

Feature #14007

open mode 'x' to raise error if file exists

Added by kernigh (George Koehler) about 3 years ago. Updated about 2 years ago.

Status:
Closed
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:83223]

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.

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

  1. defines FMODE_EXCL in the public header ruby/io.h. (I'm not sure how to pick a value; I picked 0x00002000.)
  2. 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.
  3. adds 'x' to the document for IO.new.
  4. 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

ruby-mode-x.diff (3.52 KB) ruby-mode-x.diff add mode 'x' to Ruby, with specs kernigh (George Koehler), 10/12/2017 12:20 AM

Related issues

Is duplicate of Ruby master - Feature #11258: add 'x' mode character for O_EXCLClosedznz (Kazuhiro NISHIYAMA)Actions

Also available in: Atom PDF