Feature #1081

add File::write() convenience method

Added by Suraj Kurapati about 5 years ago. Updated over 2 years ago.

[ruby-core:21701]
Status:Closed
Priority:Normal
Assignee:Yusuke Endoh
Category:core
Target version:1.9.3

Description

=begin
Please add a File::write() convenience method to the core Ruby API.

Currently, it is easier to read whole files than to write them:

# reading a whole file --- less effort
text = File::read('foo.txt')

# writing a whole file --- more effort
File::open('foo.txt', 'wb') {|f| f.write 'ruby!' }

This imbalance can be corrected by adding a File::write method,
such as the following, to the core Ruby API:

class File
def self.write path, data, mode = 'wb'
open(path, mode) {|f| f.write data }
end
end

Thanks for your consideration.
=end

0001-io.c-io_s_write-io_s_binwrite-Re-add-IO.write-binwri.patch Magnifier (2.86 KB) Run Paint Run Run, 03/07/2010 02:32 AM

new.diff Magnifier (9.02 KB) Roger Pack, 04/29/2010 03:42 AM

latest.diff Magnifier (7.49 KB) Roger Pack, 04/30/2010 02:32 AM

add_file_write_prelude.diff Magnifier (4.66 KB) Roger Pack, 03/19/2011 04:41 AM

latest_also_accomodate_lack_of_mode_param.diff Magnifier (5.59 KB) Roger Pack, 05/18/2011 03:37 AM

sorah_implementation.diff Magnifier (7.46 KB) Shota Fukumori, 05/30/2011 10:57 AM


Related issues

Related to ruby-trunk - Bug #4846: Permission denied - /tmp/nonexisting Closed 06/07/2011

Associated revisions

Revision 31902
Added by Shota Fukumori almost 3 years ago

  • io.c: Add File.write, File.binwrite. [Feature #1081]

  • test/ruby/test_io.rb: Test for File.write, File.binwrite.

  • NEWS: News for above.

History

#1 Updated by Koichi Sasada about 5 years ago

  • Assignee set to Yukihiro Matsumoto

=begin

=end

#2 Updated by Yukihiro Matsumoto about 5 years ago

=begin
Hi,

In message "Re: [Feature #1081] add File::write() convenience method"
on Sun, 1 Feb 2009 05:46:57 +0900, Suraj Kurapati redmine@ruby-lang.org writes:

|Please add a File::write() convenience method to the core Ruby API.
|
|Currently, it is easier to read whole files than to write them:
|
| # reading a whole file --- less effort
| text = File::read('foo.txt')
|
| # writing a whole file --- more effort
| File::open('foo.txt', 'wb') {|f| f.write 'ruby!' }

open() has following API

open(path, mode="r", mode=0, opthash=nil)

write() has

IO#write(string)

What do you think proper API for combined File.open?

File::write(path, string, mode, opt)

or

File::write(string, path, mode, opt)

or whatever? The latter would conflict with some in-house definition
of File::write.

                        matz.

=end

#3 Updated by Kornelius Kalnbach about 5 years ago

=begin
Yukihiro Matsumoto wrote:

What do you think proper API for combined File.open?

File::write(path, string, mode, opt)

or

File::write(string, path, mode, opt)
we badly need keyword arguments.

[murphy]

=end

#4 Updated by Suraj Kurapati about 5 years ago

=begin
Hi,

Sorry for my late response.

Matz wrote:

What do you think proper API for combined File.open?

File::write(path, string, mode, opt)

I prefer this order of parameters because it feels natural:

File::open('hello.txt', 'r').read()
File::read('hello.txt')

File::open('hello.txt', 'w').write('hello')
File::write('hello.txt', 'hello')

In addition, the Facets library also perfers this order:

http://facets.rubyforge.org/doc/api/core/classes/File.html#M000241

or

File::write(string, path, mode, opt)

The latter would conflict with some in-house definition of File::write.

Then, to minimize disruption, let us avoid this latter order of parameters.

Thanks for your consideration.
=end

#5 Updated by Roger Pack about 5 years ago

=begin

What do you think proper API for combined File.open?

File::write(path, string, mode, opt)

I also prefer this first way.
File.write('hello.txt', 'some stuff')

and default to mode 'wb' for all platforms
Thanks for looking into this. It would be quite useful.

=end

#6 Updated by Suraj Kurapati over 4 years ago

=begin
To summarize Roger's comment, we prefer this API:

File::write(path, string, mode='wb', opt={})

Thanks for your consideration.
=end

#7 Updated by xiong ai over 4 years ago

=begin
File::write(path, string, mode='wb', opt={})
+1
=end

#8 Updated by Roger Pack over 4 years ago

=begin
Hmm.
On second thought perhaps on windows it should be

File::write(path, string, mode='w', opt={})

And there should be

File::binwrite(path, string, mode='wb', opt={})

for consistency with 1.9's existing File::read and File::binread

Thoughts?
-r

=end

#9 Updated by Suraj Kurapati over 4 years ago

=begin
That seems reasonable Roger. I agree:

File::write(path, string, mode='w', opt={})
File::binwrite(path, string, mode='wb', opt={})
=end

#10 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

Please add a File::write() convenience method to the core Ruby API.

I have written a patch. The API is similar to File.read except string:

File.write(path, string[, offset]) #=> length written
File.write(path, string[, offset], open_args) #=> length written

If offset is given, the file is not truncated. Otherwise, truncated.

$ ./ruby -e 'File.write("foo", "foo\nbar\nbaz\n")' && cat foo
foo
bar
baz

$ ./ruby -e 'File.write("foo", "BAR", 4)' && cat foo
foo
BAR
baz

$ ./ruby -e 'File.write("foo", "FOO\n")' && cat foo
FOO

$ ./ruby -e 'File.write("foo", "あいうえお", mode: "w", encoding: "EUC-JP")' && nkf -Ew foo
あいうえお

I'll commit the patch if anyone says objection.

diff --git a/io.c b/io.c
index 8026ea3..08d4988 100644
--- a/io.c
+++ b/io.c
@@ -7701,7 +7701,7 @@ struct foreach_arg {
};

static void
-openkeyargs(int argc, VALUE *argv, struct foreacharg *arg)
+open
keyargswithopt(int argc, VALUE *argv, struct foreacharg *arg, int mandatoryargc, int defaultmode, int default_perm)
{
VALUE opt, v;

@@ -7709,9 +7709,9 @@ openkeyargs(int argc, VALUE argv, struct foreacharg *arg)
arg->io = 0;
arg->argc = argc - 1;
arg->argv = argv + 1;
- if (argc == 1) {
+ if (argc == mandatory
argc) {
nokey:
- arg->io = rb
ioopen(argv[0], INT2NUM(ORDONLY), INT2FIX(0666), Qnil);
+ arg->io = rbioopen(argv[0], INT2NUM(defaultmode), INT2FIX(defaultperm), Qnil);
return;
}
opt = poplasthash(&arg->argc, arg->argv);
@@ -7736,9 +7736,23 @@ openkeyargs(int argc, VALUE *argv, struct foreacharg *arg)
rb
ary_clear(args); /
prevent from GC */
return;
}
+ if (defaultmode != ORDONLY && NILP(rbhasharef(opt, symmode))) {
+ opt = rbhashdup(opt);
+ rbhashaset(opt, symmode, INT2NUM(defaultmode));
+ }
+ if (defaultperm != 0666 && NILP(rbhasharef(opt, symperm))) {
+ opt = rb
hashdup(opt);
+ rb
hashaset(opt, symperm, INT2FIX(defaultperm));
+ }
arg->io = rb
io_open(argv[0], Qnil, Qnil, opt);
}

+static void
+openkeyargs(int argc, VALUE *argv, struct foreacharg *arg)
+{
+ open
keyargswithopt(argc, argv, arg, 1, ORDONLY, 0666);
+}
+
static VALUE
iosforeach(struct foreacharg *arg)
{
@@ -7826,6 +7840,16 @@ io
sread(struct foreacharg *arg)
return io_read(arg->argc, arg->argv, arg->io);
}

+struct writearg {
+ VALUE io, str;
+};
+
+static VALUE
+io
swrite(struct writearg *arg)
+{
+ return iowrite(arg->io, arg->str, 0);
+}
+
struct seek
arg {
VALUE io;
VALUE offset;
@@ -7833,7 +7857,7 @@ struct seek_arg {
};

static VALUE
-seekbeforeread(struct seekarg *arg)
+seek
beforeaccess(struct seekarg arg)
{
rbiobinmode(arg->io);
return rbioseek(arg->io, arg->offset, arg->mode);
@@ -7844,7 +7868,7 @@ seekbeforeread(struct seekarg *arg)
* IO.read(name, [length [, offset]] ) => string
* IO.read(name, [length [, offset]], open
args) => string
*
- * Opens the file, optionally seeks to the given offset, then returns
+ * Opens the file, optionally seeks to the given offset, then returns
* length bytes (defaulting to the rest of the file).
* read ensures the file is closed before returning.
*
@@ -7886,7 +7910,7 @@ rbiosread(int argc, VALUE *argv, VALUE io)
sarg.io = arg.io;
sarg.offset = offset;
sarg.mode = SEEK
SET;
- rb_protect((VALUE (
)(VALUE))seekbeforeread, (VALUE)&sarg, &state);
+ rbprotect((VALUE (*)(VALUE))seekbeforeaccess, (VALUE)&sarg, &state);
if (state) {
rb
ioclose(arg.io);
rb
jumptag(state);
@@ -7900,9 +7924,9 @@ rb
iosread(int argc, VALUE *argv, VALUE io)
* call-seq:
* IO.binread(name, [length [, offset]] ) => string
*
- * Opens the file, optionally seeks to the given offset, then returns
+ * Opens the file, optionally seeks to the given offset, then returns
* length bytes (defaulting to the rest of the file).
- * read ensures the file is closed before returning.
+ * binread ensures the file is closed before returning.
* The open mode would be "rb:ASCII-8BIT".
*
* IO.binread("testfile") #=> "This is line one\nThis is line two\nThis is line three\nAnd so on...\n"
@@ -7928,6 +7952,117 @@ rbiosbinread(int argc, VALUE *argv, VALUE io)
return rb
ensure(iosread, (VALUE)&arg, rbioclose, arg.io);
}

+/*
+ * call-seq:
+ * IO.write(name, string, [offset] ) => fixnum
+ * IO.write(name, string, [offset], openargs ) => fixnum
+ *
+ * Opens the file, optionally seeks to the given offset, writes
+ * string, then returns the length written.
+ * write ensures the file is closed before returning.
+ * If offset is not given, the file is truncated. Otherwise,
+ * it is not truncated.
+ *
+ * If the last argument is a hash, it specifies option for internal
+ * open(). The key would be the following. open
args: is exclusive
+ * to others.
+ *
+ * encoding: string or encoding
+ *
+ * specifies encoding of the read string. encoding will be ignored
+ * if length is specified.
+ *
+ * mode: string
+ *
+ * specifies mode argument for open(). it should start with "w" or "a" or "r+"
+ * otherwise it would cause error.
+ *
+ * perm: fixnum
+ *
+ * specifies perm argument for open().
+ *
+ * openargs: array of strings
+ *
+ * specifies arguments for open() as an array.
+ *
+ * IO.write("testfile", "0123456789") #=> "0123456789"
+ * IO.write("testfile", "0123456789", 20) #=> "This is line one\nThi0123456789two\nThis is line three\nAnd so on...\n"
+ */
+
+static VALUE
+rb
ioswrite(int argc, VALUE argv, VALUE io)
+{
+ VALUE offset;
+ struct foreacharg arg;
+ struct write
arg warg;
+ int mode = OWRONLY | OCREAT, mandatoryargc;
+
+ rb
scanargs(argc, argv, "22", NULL, &warg.str, &offset, NULL);
+ if (!NIL
P(offset) && FIXNUMP(offset)) {
+ mandatory
argc = 3;
+ }
+ else {
+ mode |= OTRUNC;
+ mandatory
argc = 2;
+ }
+ openkeyargswithopt(argc, argv, &arg, mandatoryargc, mode, 0666);
+ if (NIL
P(arg.io)) return Qnil;
+ if (!NILP(offset) && FIXNUMP(offset)) {
+ struct seekarg sarg;
+ int state = 0;
+ sarg.io = arg.io;
+ sarg.offset = offset;
+ sarg.mode = SEEK
SET;
+ rb_protect((VALUE (
)(VALUE))seekbeforeaccess, (VALUE)&sarg, &state);
+ if (state) {
+ rbioclose(arg.io);
+ rbjumptag(state);
+ }
+ if (arg.argc == 2) arg.argc = 1;
+ }
+ warg.io = arg.io;
+ return rbensure(ioswrite, (VALUE)&warg, rbioclose, arg.io);
+}
+
+/*
+ * call-seq:
+ * IO.binwrite(name, string, [offset] ) => fixnum
+ *
+ * Opens the file, optionally seeks to the given offset, write
+ * string then returns the length written.
+ * binwrite ensures the file is closed before returning.
+ * The open mode would be "wb:ASCII-8BIT".
+ * If offset is not given, the file is truncated. Otherwise,
+ * it is not truncated.
+ *
+ * IO.binwrite("testfile", "0123456789") #=> "0123456789"
+ * IO.binwrite("testfile", "0123456789", 20) #=> "This is line one\nThi0123456789two\nThis is line three\nAnd so on...\n"
+ */
+
+static VALUE
+rb
iosbinwrite(int argc, VALUE *argv, VALUE io)
+{
+ VALUE offset;
+ const char *mode;
+ struct writearg warg;
+
+ rb
scanargs(argc, argv, "21", NULL, &warg.str, &offset);
+ if (!NIL
P(offset)) {
+ NUM2OFFT(offset);
+ mode = "ab:ASCII-8BIT";
+ }
+ else {
+ mode = "wb:ASCII-8BIT";
+ }
+ FilePathValue(argv[0]);
+ warg.io = rbioopen(argv[0], rbstrnewcstr(mode), Qnil, Qnil);
+ if (NIL
P(warg.io)) return Qnil;
+ if (!NILP(offset)) {
+ rb
ioseek(warg.io, offset, SEEKSET);
+ }
+ return rbensure(ioswrite, (VALUE)&warg, rbioclose, warg.io);
+}
+
struct copy
streamstruct {
VALUE src;
VALUE dst;
@@ -9731,6 +9866,8 @@ Init
IO(void)
rbdefinesingletonmethod(rbcIO, "readlines", rbiosreadlines, -1);
rb
definesingletonmethod(rbcIO, "read", rbiosread, -1);
rbdefinesingletonmethod(rbcIO, "binread", rbiosbinread, -1);
+ rb
definesingletonmethod(rbcIO, "write", rbioswrite, -1);
+ rbdefinesingletonmethod(rbcIO, "binwrite", rbiosbinwrite, -1);
rb
definesingletonmethod(rbcIO, "select", rbfselect, -1);
rb
definesingletonmethod(rbcIO, "pipe", rbiospipe, -1);
rbdefinesingletonmethod(rbcIO, "tryconvert", rbiostryconvert, 1);
diff --git a/test/ruby/test
io.rb b/test/ruby/testio.rb
index 5fc3b13..6ea128a 100644
--- a/test/ruby/test
io.rb
+++ b/test/ruby/testio.rb
@@ -1512,4 +1512,34 @@ End
t.close
assert
raise(IOError) {t.binmode}
end
+
+ def testswrite
+ t = Tempfile.new("foo")
+ path = t.path
+ t.close(false)
+ File.write(path, "foo\nbar\nbaz")
+ assert("foo\nbar\nbaz", File.read(path))
+ File.write(path, "FOO", 0)
+ assert("FOO\nbar\nbaz", File.read(path))
+ File.write(path, "BAR")
+ assert("BAR", File.read(path))
+ File.write(path, "\u{3042}", mode: "w", encoding: "EUC-JP")
+ assert("\u{3042}".encode("EUC-JP"), File.read(path, encoding: "EUC-JP"))
+ t.unlink
+ end
+
+ def testsbinwrite
+ t = Tempfile.new("foo")
+ path = t.path
+ t.close(false)
+ File.binwrite(path, "foo\nbar\nbaz")
+ assert("foo\nbar\nbaz", File.read(path))
+ File.binwrite(path, "FOO", 0)
+ assert("FOO\nbar\nbaz", File.read(path))
+ File.binwrite(path, "BAR")
+ assert("BAR", File.read(path))
+ File.binwrite(path, "\u{3042}")
+ assert("\u{3042}", File.read(path, encoding: "EUC-JP"))
+ t.unlink
+ end
end

--
Yusuke Endoh mame@tsg.ne.jp
=end

#11 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

Yusuke Endoh wrote:

I have written a patch. The API is similar to File.read except string:

File.write(path, string[, offset]) #=> length written
File.write(path, string[, offset], open_args) #=> length written

snip

I'll commit the patch if anyone says objection.

Sorry, I have misunderstood matz had already approved the feature in
. But I realized he just discussed API design.

I have not commit it yet. Matz, may I commit my patch?

--
Yusuke Endoh mame@tsg.ne.jp
=end

#12 Updated by Yukihiro Matsumoto about 4 years ago

=begin
Hi,

In message "Re: [Feature #1081] add File::write() convenience method"
on Thu, 4 Mar 2010 02:48:14 +0900, Yusuke Endoh redmine@ruby-lang.org writes:

|I have not commit it yet. Matz, may I commit my patch?

Go ahead.

                        matz.

=end

#13 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/4 Yukihiro Matsumoto matz@ruby-lang.org:

In message "Re: [Feature #1081] add File::write() convenience method"
   on Thu, 4 Mar 2010 02:48:14 +0900, Yusuke Endoh redmine@ruby-lang.org writes:

|I have not commit it yet.  Matz, may I commit my patch?

Go ahead.

Thank you! Done.

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#14 Updated by Yusuke Endoh about 4 years ago

  • Status changed from Open to Closed
  • % Done changed from 0 to 100

=begin
This issue was solved with changeset r26816.
Suraj, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.

=end

#15 Updated by Run Paint Run Run about 4 years ago

=begin
Is it intended that the length of the leading nulls are not included
in the return value? I assumed the second line in the example below
would return 3.

File.delete('/tmp/glark') #=> 1
File.write('/tmp/glark','s',2) #=> 1
File.read('/tmp/glark') #=> "\u0000\u0000s"

=end

#16 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/5 Run Paint Run Run runrun@runpaint.org:

Is it intended that the length of the leading nulls are not included
in the return value? I assumed the second line in the example below
would return 3.

File.delete('/tmp/glark') #=> 1
File.write('/tmp/glark','s',2) #=> 1
File.read('/tmp/glark') #=> "\u0000\u0000s"

Intended. It currently returns the length actually written. The
\u0000s are padded by seek, not write.

Consider the situation where there is /tmp/glark whose size is more
than 2:

File.read('/tmp/glark') #=> "foo"
File.write('/tmp/glark', "s", 2) #=> 1
File.read('/tmp/glark') #=> "fos"

In this case, it is natural (for me) to return 1.

But I'm happy to change the spec if you guys want.

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#17 Updated by Run Paint Run Run about 4 years ago

=begin

Is it intended that the length of the leading nulls are not included
in the return value? I assumed the second line in the example below
would return 3.

 >> File.delete('/tmp/glark') #=> 1
 >> File.write('/tmp/glark','s',2) #=> 1
 >> File.read('/tmp/glark') #=> "\u0000\u0000s"

Intended.  It currently returns the length actually written.  The
\u0000s are padded by seek, not write.

OK, just wanted to check. As for #binwrite, I'm also assuming that, at
least on Linux, it's intentionally ignoring the offset, treating it
instead as merely a cue to append.

File.delete('/tmp/offset') #=> 1
File.write('/tmp/offset','ruby') #=> 4
File.write('/tmp/offset','ruby',2) #=> 4
File.size('/tmp/offset') #=> 6

File.delete('/tmp/offset') #=> 1
File.binwrite('/tmp/offset','ruby') #=> 4
File.binwrite('/tmp/offset','ruby',2) #=> 4
File.size('/tmp/offset') #=> 8

=end

#18 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/6 Run Paint Run Run runrun@runpaint.org:

As for #binwrite, I'm also assuming that, at
least on Linux, it's intentionally ignoring the offset, treating it
instead as merely a cue to append.

File.delete('/tmp/offset') #=> 1
File.write('/tmp/offset','ruby') #=> 4
File.write('/tmp/offset','ruby',2) #=> 4
File.size('/tmp/offset') #=> 6

File.delete('/tmp/offset') #=> 1
File.binwrite('/tmp/offset','ruby') #=> 4
File.binwrite('/tmp/offset','ruby',2) #=> 4
File.size('/tmp/offset') #=> 8

No, it is never intended :-(
Fixed. Thanks.

# I had used assert instead of assert_equal in the test...

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#19 Updated by Run Paint Run Run about 4 years ago

=begin
Teamwork. :-)

The only other inconsistency I found is how offsets are handled for
non-existent paths.

 >> File.delete('/tmp/offset') #=> 1
 >> File.binwrite('/tmp/offset','string',2)
 Errno::ENOENT: No such file or directory - /tmp/offset
from (irb):6:in `binwrite'
from (irb):6
from /usr/local/bin/irb:12:in `<main>'
 >> File.write('/tmp/offset','string',2) #=> 6
 >> File.read('/tmp/offset') #=> "\u0000\u0000string"

  >> File.delete('/tmp/offset') #=> 1
  >> File.binwrite('/tmp/offset','string',-2)
  Errno::ENOENT: No such file or directory - /tmp/offset
from (irb):14:in `binwrite'
from (irb):14
from /usr/local/bin/irb:12:in `<main>'
  >> File.write('/tmp/offset','string',-2)
  Errno::EINVAL: Invalid argument - /tmp/offset
from (irb):16:in `write'
from (irb):16
from /usr/local/bin/irb:12:in `<main>'

=end

#20 Updated by Yusuke Endoh about 4 years ago

=begin
2010/3/6 Run Paint Run Run runrun@runpaint.org:

The only other inconsistency I found is how offsets are handled for
non-existent paths.

Grr, I hate IO.

In addition, I noticed we cannot specify permission with the current
File#binwrite.

I'll consider for some time.

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#21 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/6 Yusuke ENDOH mame@tsg.ne.jp:

2010/3/6 Run Paint Run Run runrun@runpaint.org:

The only other inconsistency I found is how offsets are handled for
non-existent paths.

Grr, I hate IO.

In addition, I noticed we cannot specify permission with the current
File#binwrite.

I'll consider for some time.

I give up. I have already reverted and deleted the two methods.

I wrote the patch because I thought closing the ticket encourages 1.9.2
release.
But it seems to take some time to make the feature stable, and I became
afraid that it may delay the release. It is never the effect I expected.

I think refactoring io.c is needed to implement the feature.
open argument is easy to use, but difficult for me to implement :-(

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#22 Updated by Yusuke Endoh about 4 years ago

  • Status changed from Closed to Open
  • Target version changed from 1.9.2 to 2.0.0

=begin

=end

#23 Updated by Run Paint Run Run about 4 years ago

=begin

I give up.  I have already reverted and deleted the two methods.

I wrote the patch because I thought closing the ticket encourages 1.9.2
release.
But it seems to take some time to make the feature stable, and I became
afraid that it may delay the release.  It is never the effect I expected.

I think refactoring io.c is needed to implement the feature.
open argument is easy to use, but difficult for me to implement :-(

Why don't we just start simple? Add IO.write and IO.binwrite that take
exactly two arguments--a path and a string--and truncate the file and
write to it the string. That covers the common use case which inspired
this ticket, and allows the optional arguments to be added later
without breaking compatibility.

=end

#24 Updated by Run Paint Run Run about 4 years ago

=begin
For example, with the usual caveat that I don't speak C, the attached patch passes the following RubySpec: http://goo.gl/RcAW (the two methods are treated as aliases because I haven't looked at testing binmode under Linux). I suspect that if it was rewritten by somebody who knew what they were doing it would satisfy the original reporter and simplify the common idiom of "File.open('/tmp/glark','w'){|f| f << 'string'}". If more functionality is desired, IO.open can be used or another ticket can be opened.
=end

#25 Updated by Nobuyoshi Nakada about 4 years ago

=begin
Hi,

At Sun, 7 Mar 2010 02:32:56 +0900,
Run Paint Run Run wrote in :

File 0001-io.c-ioswrite-iosbinwrite-Re-add-IO.write-binwri.patch added

It's broken, and will cause segfault or similar.

+static VALUE
+rbios_write(int argc, VALUE path, VALUE str, VALUE io)

+static VALUE
+rbios_binwrite(int argc, VALUE path, VALUE str, VALUE io)

  • rbdefinesingletonmethod(rbcIO, "write", rbios_write, 2);
  • rbdefinesingletonmethod(rbcIO, "binwrite", rbios_binwrite, 2);

    Nobu Nakada

=end

#26 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/6 Run Paint Run Run runrun@runpaint.org:

I give up. I have already reverted and deleted the two methods.

I wrote the patch because I thought closing the ticket encourages 1.9.2
release.
But it seems to take some time to make the feature stable, and I became
afraid that it may delay the release. ?It is never the effect I expected.

I think refactoring io.c is needed to implement the feature.
open argument is easy to use, but difficult for me to implement :-(

Why don't we just start simple?

Because there are already complex File.read/binread.
It is confusing to make File.write/binwrite inconsistent with them.

That covers the common use case which inspired
this ticket, and allows the optional arguments to be added later
without breaking compatibility.

There is no reason to rush into 1.9.2.

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#27 Updated by Suraj Kurapati about 4 years ago

=begin
Hi,

Is this issue solved? Or was the patch1 removed from Ruby?

Sorry for asking these (perhaps self-evident) questions;
I really don't understand the discussion succeeding 1.

Thanks for your consideration.

=end

#28 Updated by Yusuke Endoh about 4 years ago

=begin
Hi,

2010/3/15 Suraj Kurapati redmine@ruby-lang.org:

Is this issue solved? ?Or was the patch[1] removed from Ruby?

Removed.

I thought it is difficult (for me) to make the feature stable by the
deadline.
I will not disagree with a patch that anyone writes, if it passes Run
Paint's corner cases.

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#29 Updated by Roger Pack almost 4 years ago

=begin
I was unable to reproduce the odd behavior observed previously by Run Paint Run.

Here is the original diff plus some test cases that cover the corner cases (they don't appear to fail--maybe something else has been fixed since then, allowing things to work right now?)
Maybe somebody can point out a failing test case to me?

Thanks.

./ruby test/ruby/testio.rb
Loaded suite test/ruby/test
io
Started
...........................................................................................
Finished in 0.687023 seconds.

91 tests, 358 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 26858
=end

#30 Updated by Nobuyoshi Nakada almost 4 years ago

=begin
Hi,

At Thu, 29 Apr 2010 03:42:57 +0900,
Roger Pack wrote in :

Here is the original diff plus some test cases that cover the
corner cases (they don't appear to fail--maybe something else
has been fixed since then, allowing things to work right
now?)
Maybe somebody can point out a failing test case to me?

It's sorry that your tests have no meanings at all, except for
assertraise. You should use assertequal instead of mere
assert.

--
Nobu Nakada

=end

#31 Updated by Roger Pack almost 4 years ago

=begin
Sorry for the poor tests.
Fixing the tests revealed that there were some bugs in binwrite.

The attached (new) patch fixes the tests and code. The tests pass on windows and linux (and I think are accurate).
Feel free to refactor it as desired.

(as a note, windows currently fails some other tests in test_io.rb http://gist.github.com/383867).

Thanks.
-rp
=end

#32 Updated by Shyouhei Urabe over 3 years ago

  • Status changed from Open to Assigned

=begin

=end

#33 Updated by Roger Pack over 3 years ago

=begin
Any chance of getting this committed at all? (File#write)?
Thanks!
=end

#34 Updated by Benoit Daloze over 3 years ago

=begin
On 1 November 2010 15:37, Roger Pack redmine@ruby-lang.org wrote:

Issue #1081 has been updated by Roger Pack.

Any chance of getting this committed at all? (File#write)?
Thanks!

I want to show my wish to back up this, it is definitely worth it.

Is there any issue left ?

=end

#35 Updated by Roger Pack about 3 years ago

=begin
Ok here is a new patch (the old one no longer merged cleanly, plus had minor bugs). Feel free to reformat it or convert to C.

The format is:

File::write(path, string, offset, opt={}) or
File::write(path, string, opt={})

Which I think is reasonable, and matches the previous commits (the ones that were rolled out because of bugs). Apparently offset is common in other api's so I guess is good to have in there.
Thanks for consideration of this.
-r
=end

#36 Updated by Motohiro KOSAKI almost 3 years ago

  • Assignee changed from Yukihiro Matsumoto to Motohiro KOSAKI

#37 Updated by Motohiro KOSAKI almost 3 years ago

  • Assignee changed from Motohiro KOSAKI to Yusuke Endoh

#38 Updated by Roger Pack almost 3 years ago

Uploading new tests and a fix that accomodate for the previously failing example.

#39 Updated by Yusuke Endoh almost 3 years ago

  • Target version changed from 2.0.0 to 1.9.3

Hello,

The status of this feature request is considered as "accepted".
If anyone writes a patch (in C) for this feature by the implementation deadline (the end of June) and if there is no objection to the patch, it will be imported in 1.9.3.
If not, this feature will be postponed to 1.9.4 or later.

Sora seems to be interested in this feature. Good luck.

Yusuke Endoh mame@tsg.ne.jp

#40 Updated by Shota Fukumori almost 3 years ago

Hi, I'm implementing this feature in C.

I've been completed File.write, I'll complete implementation of File.binwrite in C.

--sora_h

#41 Updated by Shota Fukumori almost 3 years ago

Hi,

My Implementation has been completed File.write, binwrite.

Patch is attached.
RDocs in patch and test is copied from previous patches.

#42 Updated by Yusuke Endoh almost 3 years ago

Hello,

2011/5/30 Shota Fukumori sorah@tubusu.net:

My Implementation has been completed File.write, binwrite.

Good work.

But File.binwrite does not accept an optional hash while write does.

$ rm -f /tmp/x && ./ruby -e 'File.write("/tmp/x", "foo", perm: 0700)'
"foo"

$ rm -f /tmp/x && ./ruby -e 'File.binwrite("/tmp/x", "foo", perm: 0700)'
-e:1:in binwrite': can't convert Hash into Integer (TypeError)
from -e:1:in
'

--
Yusuke Endoh mame@tsg.ne.jp

#43 Updated by Shota Fukumori almost 3 years ago

Patch has updated, Now File.binwrite accepts Hash for specifying options:

https://gist.github.com/69c544ec245f3a07aabd

#44 Updated by Yusuke Endoh almost 3 years ago

Hello,

2011/5/31 Shota Fukumori sorah@tubusu.net:

Patch has updated, Now File.binwrite accepts Hash for specifying options:

https://gist.github.com/69c544ec245f3a07aabd

Great! I tested your patch and noticed no problem.
Roger and rubyspec folks, could you also check it?

In terms of maintainability, I think that it would be better to
define a common function for rbioswrite and rbiosbinwrite:

http://www.atdot.net/sp/view/pw92ml

diff --git a/io.c b/io.c
index 4e1945c..25e0974 100644
--- a/io.c
+++ b/io.c
@@ -805,6 +805,12 @@ struct binwrite_arg {http://www.atdot.net/sp/view/pw92ml
long length;
};

+struct writearg {
+ VALUE io;
+ VALUE str;
+ int nosync;
+};
+
static VALUE
io
binwritestring(VALUE arg)
{
@@ -8366,6 +8372,124 @@ rb
iosbinread(int argc, VALUE *argv, VALUE io)
return rbensure(iosread, (VALUE)&arg, rbio_close, arg.io);
}

+static VALUE
+ioswrite0(struct writearg *arg)
+{
+ return io
write(arg->io,arg->str,arg->nosync);
+}
+
+static VALUE
+ioswrite(int argc, VALUE argv, int binary)
+{
+ VALUE string, offset, opt;
+ struct foreacharg arg;
+ struct write
arg warg;
+
+ rbscanargs(argc, argv, "21:", NULL, &string, &offset, &opt);
+
+ if (NILP(opt)) opt = rbhashnew();
+ else opt = rb
hashdup(opt);
+
+
+ if (NIL
P(rbhasharef(opt,symmode))) {
+ int mode = O
WRONLY|OCREAT;
+#ifdef O
BINARY
+ if (binary) mode |= OBINARY;
+#endif
+ if (NIL
P(offset)) mode |= OTRUNC;
+ rb
hashaset(opt,symmode,INT2NUM(mode));
+ }
+ openkeyargs(argc,argv,opt,&arg);
+
+#ifndef OBINARY
+ if (binary) rb
iobinmodem(arg.io);
+#endif
+
+ if (NILP(arg.io)) return Qnil;
+ if (!NIL
P(offset)) {
+ struct seekarg sarg;
+ int state = 0;
+ sarg.io = arg.io;
+ sarg.offset = offset;
+ sarg.mode = SEEK
SET;
+ rbprotect(seekbeforeaccess, (VALUE)&sarg, &state);
+ if (state) {
+ rb
ioclose(arg.io);
+ rb
jumptag(state);
+ }
+ }
+
+ warg.io = arg.io;
+ warg.str = string;
+ warg.nosync = 0;
+
+ return rb
ensure(ioswrite0, (VALUE)&warg, rbioclose, arg.io);
+}
+
+/

+ * call-seq:
+ * IO.write(name, string, [offset] ) => fixnum
+ * IO.write(name, string, [offset], openargs ) => fixnum
+ *
+ * Opens the file, optionally seeks to the given offset, writes
+ * string, then returns the length written.
+ * write ensures the file is closed before returning.
+ * If offset is not given, the file is truncated. Otherwise,
+ * it is not truncated.
+ *
+ * If the last argument is a hash, it specifies option for internal
+ * open(). The key would be the following. open
args: is exclusive
+ * to others.
+ *
+ * encoding: string or encoding
+ *
+ * specifies encoding of the read string. encoding will be ignored
+ * if length is specified.
+ *
+ * mode: string
+ *
+ * specifies mode argument for open(). it should start with "w" or "a" or "r+"
+ * otherwise it would cause error.
+ *
+ * perm: fixnum
+ *
+ * specifies perm argument for open().
+ *
+ * openargs: array of strings
+ *
+ * specifies arguments for open() as an array.
+ *
+ * IO.write("testfile", "0123456789") #=> "0123456789"
+ * IO.write("testfile", "0123456789", 20) #=> "This is line one\nThi0123456789two\nThis is line three\nAnd so on...\n"
+ */
+
+static VALUE
+rb
ioswrite(int argc, VALUE argv, VALUE io)
+{
+ ioswrite(argc, argv, 0);
+}
+
+/

+ * call-seq:
+ * IO.binwrite(name, string, [offset] ) => fixnum
+ *
+ * Opens the file, optionally seeks to the given offset, write
+ * string then returns the length written.
+ * binwrite ensures the file is closed before returning.
+ * The open mode would be "wb:ASCII-8BIT".
+ * If offset is not given, the file is truncated. Otherwise,
+ * it is not truncated.
+ *
+ * IO.binwrite("testfile", "0123456789") #=> "0123456789"
+ * IO.binwrite("testfile", "0123456789", 20) #=> "This is line one\nThi0123456789two\nThis is line three\nAnd so on...\n"
+ */
+
+static VALUE
+rbiosbinwrite(int argc, VALUE *argv, VALUE io)
+{
+ io
swrite(argc, argv, 1);
+}
+
struct copy
streamstruct {
VALUE src;
VALUE dst;
@@ -10315,6 +10439,8 @@ Init
IO(void)
rbdefinesingletonmethod(rbcIO, "readlines", rbiosreadlines, -1);
rb
definesingletonmethod(rbcIO, "read", rbiosread, -1);
rbdefinesingletonmethod(rbcIO, "binread", rbiosbinread, -1);
+ rb
definesingletonmethod(rbcIO, "write", rbioswrite, -1);
+ rbdefinesingletonmethod(rbcIO, "binwrite", rbiosbinwrite, -1);
rb
definesingletonmethod(rbcIO, "select", rbfselect, -1);
rb
definesingletonmethod(rbcIO, "pipe", rbiospipe, -1);
rbdefinesingletonmethod(rbcIO, "tryconvert", rbiostryconvert, 1);
diff --git a/test/ruby/test
io.rb b/test/ruby/testio.rb
index f919227..8aaecd1 100644
--- a/test/ruby/test
io.rb
+++ b/test/ruby/test_io.rb
@@ -1861,4 +1861,61 @@ End
end
end

  • def testswrite
  • t = Tempfile.new("foo")
  • path = t.path
  • t.close(false)
  • File.write(path, "foo\nbar\nbaz")
  • assert_equal("foo\nbar\nbaz", File.read(path))
  • File.write(path, "FOO", 0)
  • assert_equal("FOO\nbar\nbaz", File.read(path))
  • File.write(path, "BAR")
  • assert_equal("BAR", File.read(path))
  • File.write(path, "\u{3042}", mode: "w", encoding: "EUC-JP")
  • assert_equal("\u{3042}".encode("EUC-JP"), File.read(path, encoding: "EUC-JP"))
  • File.delete t
  • assert_equal(6, File.write(path,'string',2))
  • File.delete t
  • assert_raise(Errno::EINVAL) { File.write('/tmp/nonexisting','string',-2) }
  • assert_equal(6, File.write(path, 'string'))
  • assert_equal(3, File.write(path, 'sub', 1))
  • assert_equal("ssubng", File.read(path))
  • File.delete t
  • assert_equal(3, File.write(path, "foo", encoding: "UTF-8"))
  • File.delete t
  • assert_equal(3, File.write(path, "foo", 0, encoding: "UTF-8"))
  • assert_equal("foo", File.read(path))
  • assert_equal(1, File.write(path, "f", 1, encoding: "UTF-8"))
  • assert_equal("ffo", File.read(path))
  • File.delete t
  • assert_equal(1, File.write(path, "f", 1, encoding: "UTF-8"))
  • assert_equal("\00f", File.read(path))
  • assert_equal(1, File.write(path, "f", 0, encoding: "UTF-8"))
  • assert_equal("ff", File.read(path))
  • t.unlink
  • end +
  • def testsbinwrite
  • t = Tempfile.new("foo")
  • path = t.path
  • t.close(false)
  • File.binwrite(path, "foo\nbar\nbaz")
  • assert_equal("foo\nbar\nbaz", File.read(path))
  • File.binwrite(path, "FOO", 0)
  • assert_equal("FOO\nbar\nbaz", File.read(path))
  • File.binwrite(path, "BAR")
  • assert_equal("BAR", File.read(path))
  • File.binwrite(path, "\u{3042}")
  • assertequal("\u{3042}".forceencoding("ASCII-8BIT"), File.binread(path))
  • File.delete t
  • assert_equal(6, File.binwrite(path,'string',2))
  • File.delete t
  • assert_equal(6, File.binwrite(path, 'string'))
  • assert_equal(3, File.binwrite(path, 'sub', 1))
  • assert_equal("ssubng", File.binread(path))
  • assert_equal(6, File.size(path))
  • assert_raise(Errno::EINVAL) { File.binwrite('/tmp/nonexisting','string',-2) }
  • assertnothingraised(TypeError) { File.binwrite(path, "string", mode: "w", encoding: "EUC-JP") }
  • t.unlink
  • end end

Yusuke Endoh mame@tsg.ne.jp

#45 Updated by Shota Fukumori almost 3 years ago

hi,

On Tue, May 31, 2011 at 10:20 PM, Yusuke Endoh mame@tsg.ne.jp wrote:

Great!  I tested your patch and noticed no problem.
Roger and rubyspec folks, could you also check it?

In terms of maintainability, I think that it would be better to
define a common function for rbioswrite and rbiosbinwrite:

ok. thanks refactoring.

--
Shota Fukumori a.k.a. @sora_h - http://codnote.net/

#46 Updated by Shota Fukumori almost 3 years ago

I want to commit ASAP because we're going to freeze specification,
so if there aren't any problems, I'll commit patch at http://redmine.ruby-lang.org/issues/1081#note-44 .

I remembered that matz approved this new method with this specification, if not please notify me.

#47 Updated by Shota Fukumori almost 3 years ago

  • Status changed from Assigned to Closed
  • % Done changed from 0 to 100

This issue was solved with changeset r31902.
Suraj, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.


  • io.c: Add File.write, File.binwrite. [Feature #1081]

  • test/ruby/test_io.rb: Test for File.write, File.binwrite.

  • NEWS: News for above.

#48 Updated by Roman Gaufman over 2 years ago

It seems the docs here are wrong http://www.ruby-doc.org/core-1.9.3/IO.html#method-c-write -- they mention 4 arguments when only 3 are supported and the :mode isn't explained - please update the docs.

Also available in: Atom PDF