How to open (read-write) or create a file with truncation allowed?
Question:
I want to:
- open a file in read-write mode if it exists;
- create it if it doesn’t exist;
- be able to truncate it anytime-anywhere.
EDIT: with truncate I mean write until a position and discard the remaining part of the file, if present
All this atomically (with a single open()
call or simulating a single open()
call)
No single open modality seems to apply:
- r : obviously doesn’t work;
- r+ : fails if the file doesn’t exist;
- w: recreate the file if it exists;
- w+: recreate the file if it exists;
- a: can’t read;
- a+: can’t truncate.
Some combinations I tried (rw, rw+, r+w, etc.) seems to not work either. Is it possible?
Some doc from Ruby (applies to python too):
r
Read-only mode. The file pointer is placed at the beginning of the file.
This is the default mode.
r+
Read-write mode. The file pointer will be at the beginning of the file.
w
Write-only mode. Overwrites the file if the file exists. If the file
does not exist, creates a new file for writing.
w+
Read-write mode. Overwrites the existing file if the file exists. If the
file does not exist, creates a new file for reading and writing.
a
Write-only mode. The file pointer is at the end of the file if the file
exists. That is, the file is in the append mode. If the file does not exist,
it creates a new file for writing.
a+
Read and write mode. The file pointer is at the end of the file if the file
exists. The file opens in the append mode. If the file does not exist, it
creates a new file for reading and writing.
Answers:
I don’t know of any elegant way to do exactly this in Ruby. My solution would probably be to create a temporary file, write contents to it, and then rename it to the filename I really wanted. This would overwrite the previous file if it exists, or create the file if it did not. Something like this:
orig_filename = './whatever_file.log'
temp_filename = './.tempfile'
temp_file = File.new(temp_filename, 'w')
// Write contents to file
temp_file.close
File.rename(temp_filename, orig_filename)
The rename will raise SystemCallError
if it fails for whatever reason.
Well, there are only these modes, and all of them have the “defects” you listed.
Your only option is to wrap open()
.
Why not something like this? (Python)
def touchopen(filename, *args, **kwargs):
open(filename, "a").close() # "touch" file
return open(filename, *args, **kwargs)
it behaves just like open, you could even rebind it to open() if you really wish.
all of open’s features are preserved, you can even do:
with touchopen("testfile", "r+") as testfile:
do_stuff()
You could of course create a contextmanager which opens the file in a+ mode, reads it into memory, and intercepts writes so you handle truncation by magically creating a temporary file in w mode, and renames that tempfile to your original file when you close it, but that would be overkill I guess.
According to OpenGroup:
O_TRUNC
If the file exists and is a regular file, and the file is successfully
opened O_RDWR or O_WRONLY, its length is truncated to 0 and the mode
and owner are unchanged. It will have no effect on FIFO special files
or terminal device files. Its effect on other file types is
implementation-dependent. The result of using O_TRUNC with O_RDONLY is
undefined.
So, O_TRUNC is probably passed when opening a file with “w” or “w+”. This gives “truncation” a different meaning, not what I want.
With python the solution seems to open file at low-level I/O with os.open()
function.
The following python function:
def touchopen(filename, *args, **kwargs):
# Open the file in R/W and create if it doesn't exist. *Don't* pass O_TRUNC
fd = os.open(filename, os.O_RDWR | os.O_CREAT)
# Encapsulate the low-level file descriptor in a python file object
return os.fdopen(fd, *args, **kwargs)
has the behavior I wanted. You can use it like this (it’s in fact my use case):
# Open an existing file or create if it doesn't exist
with touchopen("./tool.run", "r+") as doing_fd:
# Acquire a non-blocking exclusive lock
fcntl.lockf(doing_fd, fcntl.LOCK_EX)
# Read a previous value if present
previous_value = doing_fd.read()
print previous_value
# Write the new value and truncate
doing_fd.seek(0)
doing_fd.write("new value")
doing_fd.truncate()
You can read, write and truncate with “a+” (Ruby):
File.open("test.txt", "a+") do |f|
f.print "abcndefgh"
f.rewind
p f.read
f.truncate(5)
end
puts File.size("test.txt") #=> 5
I want to:
- open a file in read-write mode if it exists;
- create it if it doesn’t exist;
- be able to truncate it anytime-anywhere.
EDIT: with truncate I mean write until a position and discard the remaining part of the file, if present
All this atomically (with a single open()
call or simulating a single open()
call)
No single open modality seems to apply:
- r : obviously doesn’t work;
- r+ : fails if the file doesn’t exist;
- w: recreate the file if it exists;
- w+: recreate the file if it exists;
- a: can’t read;
- a+: can’t truncate.
Some combinations I tried (rw, rw+, r+w, etc.) seems to not work either. Is it possible?
Some doc from Ruby (applies to python too):
r
Read-only mode. The file pointer is placed at the beginning of the file.
This is the default mode.
r+
Read-write mode. The file pointer will be at the beginning of the file.
w
Write-only mode. Overwrites the file if the file exists. If the file
does not exist, creates a new file for writing.
w+
Read-write mode. Overwrites the existing file if the file exists. If the
file does not exist, creates a new file for reading and writing.
a
Write-only mode. The file pointer is at the end of the file if the file
exists. That is, the file is in the append mode. If the file does not exist,
it creates a new file for writing.
a+
Read and write mode. The file pointer is at the end of the file if the file
exists. The file opens in the append mode. If the file does not exist, it
creates a new file for reading and writing.
I don’t know of any elegant way to do exactly this in Ruby. My solution would probably be to create a temporary file, write contents to it, and then rename it to the filename I really wanted. This would overwrite the previous file if it exists, or create the file if it did not. Something like this:
orig_filename = './whatever_file.log'
temp_filename = './.tempfile'
temp_file = File.new(temp_filename, 'w')
// Write contents to file
temp_file.close
File.rename(temp_filename, orig_filename)
The rename will raise SystemCallError
if it fails for whatever reason.
Well, there are only these modes, and all of them have the “defects” you listed.
Your only option is to wrap open()
.
Why not something like this? (Python)
def touchopen(filename, *args, **kwargs):
open(filename, "a").close() # "touch" file
return open(filename, *args, **kwargs)
it behaves just like open, you could even rebind it to open() if you really wish.
all of open’s features are preserved, you can even do:
with touchopen("testfile", "r+") as testfile:
do_stuff()
You could of course create a contextmanager which opens the file in a+ mode, reads it into memory, and intercepts writes so you handle truncation by magically creating a temporary file in w mode, and renames that tempfile to your original file when you close it, but that would be overkill I guess.
According to OpenGroup:
O_TRUNC
If the file exists and is a regular file, and the file is successfully
opened O_RDWR or O_WRONLY, its length is truncated to 0 and the mode
and owner are unchanged. It will have no effect on FIFO special files
or terminal device files. Its effect on other file types is
implementation-dependent. The result of using O_TRUNC with O_RDONLY is
undefined.
So, O_TRUNC is probably passed when opening a file with “w” or “w+”. This gives “truncation” a different meaning, not what I want.
With python the solution seems to open file at low-level I/O with os.open()
function.
The following python function:
def touchopen(filename, *args, **kwargs):
# Open the file in R/W and create if it doesn't exist. *Don't* pass O_TRUNC
fd = os.open(filename, os.O_RDWR | os.O_CREAT)
# Encapsulate the low-level file descriptor in a python file object
return os.fdopen(fd, *args, **kwargs)
has the behavior I wanted. You can use it like this (it’s in fact my use case):
# Open an existing file or create if it doesn't exist
with touchopen("./tool.run", "r+") as doing_fd:
# Acquire a non-blocking exclusive lock
fcntl.lockf(doing_fd, fcntl.LOCK_EX)
# Read a previous value if present
previous_value = doing_fd.read()
print previous_value
# Write the new value and truncate
doing_fd.seek(0)
doing_fd.write("new value")
doing_fd.truncate()
You can read, write and truncate with “a+” (Ruby):
File.open("test.txt", "a+") do |f|
f.print "abcndefgh"
f.rewind
p f.read
f.truncate(5)
end
puts File.size("test.txt") #=> 5