diff --git a/components/modules/file.c b/components/modules/file.c index a68bedbf..1e0878d0 100644 --- a/components/modules/file.c +++ b/components/modules/file.c @@ -2,14 +2,29 @@ #include "module.h" #include "lauxlib.h" +#include "lmem.h" #include "platform.h" #include "vfs.h" #include +#define FILE_READ_CHUNK 1024 + +// use this time/date in absence of a timestamp +#define FILE_TIMEDEF_YEAR 1970 +#define FILE_TIMEDEF_MON 01 +#define FILE_TIMEDEF_DAY 01 +#define FILE_TIMEDEF_HOUR 00 +#define FILE_TIMEDEF_MIN 00 +#define FILE_TIMEDEF_SEC 00 + static int file_fd = 0; +static int file_fd_ref = LUA_NOREF; static int rtc_cb_ref = LUA_NOREF; +typedef struct _file_fd_ud { + int fd; +} file_fd_ud; static void table2tm( lua_State *L, vfs_time *tm ) { @@ -23,12 +38,12 @@ static void table2tm( lua_State *L, vfs_time *tm ) lua_getfield( L, idx, "min" ); lua_getfield( L, idx, "sec" ); - tm->year = luaL_optint( L, ++idx, 2016 ); - tm->mon = luaL_optint( L, ++idx, 6 ); - tm->day = luaL_optint( L, ++idx, 21 ); - tm->hour = luaL_optint( L, ++idx, 0 ); - tm->min = luaL_optint( L, ++idx, 0 ); - tm->sec = luaL_optint( L, ++idx, 0 ); + tm->year = luaL_optint( L, ++idx, FILE_TIMEDEF_YEAR ); + tm->mon = luaL_optint( L, ++idx, FILE_TIMEDEF_MON ); + tm->day = luaL_optint( L, ++idx, FILE_TIMEDEF_DAY ); + tm->hour = luaL_optint( L, ++idx, FILE_TIMEDEF_HOUR ); + tm->min = luaL_optint( L, ++idx, FILE_TIMEDEF_MIN ); + tm->sec = luaL_optint( L, ++idx, FILE_TIMEDEF_SEC ); // remove items from stack lua_pop( L, 6 ); @@ -90,13 +105,47 @@ static int file_on(lua_State *L) // Lua: close() static int file_close( lua_State* L ) { - if(file_fd){ - vfs_close(file_fd); - file_fd = 0; + file_fd_ud *ud; + + if (lua_type( L, 1 ) != LUA_TUSERDATA) { + // fall back to last opened file + if (file_fd_ref != LUA_NOREF) { + lua_rawgeti( L, LUA_REGISTRYINDEX, file_fd_ref ); + // top of stack is now default file descriptor + ud = (file_fd_ud *)luaL_checkudata(L, -1, "file.obj"); + lua_pop( L, 1 ); + } else { + // no default file currently opened + return 0; + } + } else { + ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj"); + } + + // unref default file descriptor + luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref ); + file_fd_ref = LUA_NOREF; + + if(ud->fd){ + vfs_close(ud->fd); + // mark as closed + ud->fd = 0; } return 0; } +static int file_obj_free( lua_State *L ) +{ + file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj"); + if (ud->fd) { + // close file if it's still open + vfs_close(ud->fd); + ud->fd = 0; + } + + return 0; +} + #ifdef CONFIG_BUILD_SPIFFS // Lua: format() static int file_format( lua_State* L ) @@ -130,10 +179,10 @@ static int file_fscfg (lua_State *L) static int file_open( lua_State* L ) { size_t len; - if(file_fd){ - vfs_close(file_fd); - file_fd = 0; - } + + // unref last file descriptor to allow gc'ing if not kept by user script + luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref ); + file_fd_ref = LUA_NOREF; const char *fname = luaL_checklstring( L, 1, &len ); const char *basename = vfs_basename( fname ); @@ -146,7 +195,14 @@ static int file_open( lua_State* L ) if(!file_fd){ lua_pushnil(L); } else { - lua_pushboolean(L, 1); + file_fd_ud *ud = (file_fd_ud *) lua_newuserdata( L, sizeof( file_fd_ud ) ); + ud->fd = file_fd; + luaL_getmetatable( L, "file.obj" ); + lua_setmetatable( L, -2 ); + + // store reference to opened file + lua_pushvalue( L, -1 ); + file_fd_ref = luaL_ref( L, LUA_REGISTRYINDEX ); } return 1; } @@ -170,19 +226,36 @@ static int file_list( lua_State* L ) return 0; } -static int file_seek (lua_State *L) +static int get_file_obj( lua_State *L, int *argpos ) { + if (lua_type( L, 1 ) == LUA_TUSERDATA) { + file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj"); + *argpos = 2; + return ud->fd; + } else { + *argpos = 1; + return file_fd; + } +} + +#define GET_FILE_OBJ int argpos; \ + int fd = get_file_obj( L, &argpos ); + +static int file_seek (lua_State *L) +{ + GET_FILE_OBJ; + static const int mode[] = {VFS_SEEK_SET, VFS_SEEK_CUR, VFS_SEEK_END}; static const char *const modenames[] = {"set", "cur", "end", NULL}; - if(!file_fd) + if(!fd) return luaL_error(L, "open a file first"); - int op = luaL_checkoption(L, 1, "cur", modenames); - long offset = luaL_optlong(L, 2, 0); - op = vfs_lseek(file_fd, offset, mode[op]); + int op = luaL_checkoption(L, argpos, "cur", modenames); + long offset = luaL_optlong(L, ++argpos, 0); + op = vfs_lseek(fd, offset, mode[op]); if (op < 0) lua_pushnil(L); /* error */ else - lua_pushinteger(L, vfs_tell(file_fd)); + lua_pushinteger(L, vfs_tell(fd)); return 1; } @@ -210,7 +283,6 @@ static int file_remove( lua_State* L ) const char *fname = luaL_checklstring( L, 1, &len ); const char *basename = vfs_basename( fname ); luaL_argcheck(L, strlen(basename) <= CONFIG_FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid"); - file_close(L); vfs_remove((char *)fname); return 0; } @@ -218,9 +290,11 @@ static int file_remove( lua_State* L ) // Lua: flush() static int file_flush( lua_State* L ) { - if(!file_fd) + GET_FILE_OBJ; + + if(!fd) return luaL_error(L, "open a file first"); - if(vfs_flush(file_fd) == 0) + if(vfs_flush(fd) == 0) lua_pushboolean(L, 1); else lua_pushnil(L); @@ -231,10 +305,6 @@ static int file_flush( lua_State* L ) static int file_rename( lua_State* L ) { size_t len; - if(file_fd){ - vfs_close(file_fd); - file_fd = 0; - } const char *oldname = luaL_checklstring( L, 1, &len ); const char *basename = vfs_basename( oldname ); @@ -253,38 +323,65 @@ static int file_rename( lua_State* L ) } // g_read() -static int file_g_read( lua_State* L, int n, int16_t end_char ) +static int file_g_read( lua_State* L, int n, int16_t end_char, int fd ) { - if(n <= 0 || n > LUAL_BUFFERSIZE) - n = LUAL_BUFFERSIZE; - if(end_char < 0 || end_char >255) - end_char = EOF; - - luaL_Buffer b; - if(!file_fd) - return luaL_error(L, "open a file first"); - - luaL_buffinit(L, &b); - char *p = luaL_prepbuffer(&b); - int i; - - n = vfs_read(file_fd, p, n); - for (i = 0; i < n; ++i) - if (p[i] == end_char) - { - ++i; - break; - } - - if(i==0){ - luaL_pushresult(&b); /* close buffer */ - return (lua_objlen(L, -1) > 0); /* check whether read something */ + static char *heap_mem = NULL; + // free leftover memory + if (heap_mem) { + luaM_free(L, heap_mem); + heap_mem = NULL; } - vfs_lseek(file_fd, -(n - i), VFS_SEEK_CUR); - luaL_addsize(&b, i); - luaL_pushresult(&b); /* close buffer */ - return 1; /* read at least an `eol' */ + if(n <= 0) + n = FILE_READ_CHUNK; + + if(end_char < 0 || end_char >255) + end_char = EOF; + + + if(!fd) + return luaL_error(L, "open a file first"); + + char *p; + int i; + + if (n > LUAL_BUFFERSIZE) { + // get buffer from heap + p = heap_mem = luaM_malloc(L, n); + } else { + // small chunks go onto the stack + p = alloca(n); + } + + n = vfs_read(fd, p, n); + // bypass search if no end character provided + if (n > 0 && end_char != EOF) { + for (i = 0; i < n; ++i) + if (p[i] == end_char) + { + ++i; + break; + } + } else { + i = n; + } + + if (i == 0 || n == VFS_RES_ERR) { + if (heap_mem) { + luaM_free(L, heap_mem); + heap_mem = NULL; + } + lua_pushnil(L); + return 1; + } + + vfs_lseek(fd, -(n - i), VFS_SEEK_CUR); + lua_pushlstring(L, p, i); + if (heap_mem) { + luaM_free(L, heap_mem); + heap_mem = NULL; + } + return 1; } // Lua: read() @@ -293,42 +390,46 @@ static int file_g_read( lua_State* L, int n, int16_t end_char ) // file.read('q') will read until 'q' or EOF is reached. static int file_read( lua_State* L ) { - unsigned need_len = LUAL_BUFFERSIZE; + unsigned need_len = FILE_READ_CHUNK; int16_t end_char = EOF; size_t el; - if( lua_type( L, 1 ) == LUA_TNUMBER ) + + GET_FILE_OBJ; + + if( lua_type( L, argpos ) == LUA_TNUMBER ) { - need_len = ( unsigned )luaL_checkinteger( L, 1 ); - if( need_len > LUAL_BUFFERSIZE ){ - need_len = LUAL_BUFFERSIZE; - } + need_len = ( unsigned )luaL_checkinteger( L, argpos ); } - else if(lua_isstring(L, 1)) + else if(lua_isstring(L, argpos)) { - const char *end = luaL_checklstring( L, 1, &el ); + const char *end = luaL_checklstring( L, argpos, &el ); if(el!=1){ return luaL_error( L, "wrong arg range" ); } end_char = (int16_t)end[0]; } - return file_g_read(L, need_len, end_char); + return file_g_read(L, need_len, end_char, fd); } // Lua: readline() static int file_readline( lua_State* L ) { - return file_g_read(L, LUAL_BUFFERSIZE, '\n'); + GET_FILE_OBJ; + + return file_g_read(L, FILE_READ_CHUNK, '\n', fd); } // Lua: write("string") static int file_write( lua_State* L ) { - if(!file_fd) + GET_FILE_OBJ; + + if(!fd) return luaL_error(L, "open a file first"); size_t l, rl; - const char *s = luaL_checklstring(L, 1, &l); - rl = vfs_write(file_fd, s, l); + const char *s = luaL_checklstring(L, argpos, &l); + rl = vfs_write(fd, s, l); if(rl==l) lua_pushboolean(L, 1); else @@ -339,13 +440,15 @@ static int file_write( lua_State* L ) // Lua: writeline("string") static int file_writeline( lua_State* L ) { - if(!file_fd) + GET_FILE_OBJ; + + if(!fd) return luaL_error(L, "open a file first"); size_t l, rl; - const char *s = luaL_checklstring(L, 1, &l); - rl = vfs_write(file_fd, s, l); + const char *s = luaL_checklstring(L, argpos, &l); + rl = vfs_write(fd, s, l); if(rl==l){ - rl = vfs_write(file_fd, "\n", 1); + rl = vfs_write(fd, "\n", 1); if(rl==1) lua_pushboolean(L, 1); else @@ -390,6 +493,20 @@ static int file_chdir( lua_State *L ) } #endif +static const LUA_REG_TYPE file_obj_map[] = +{ + { LSTRKEY( "close" ), LFUNCVAL( file_close ) }, + { LSTRKEY( "read" ), LFUNCVAL( file_read ) }, + { LSTRKEY( "readline" ), LFUNCVAL( file_readline ) }, + { LSTRKEY( "write" ), LFUNCVAL( file_write ) }, + { LSTRKEY( "writeline" ), LFUNCVAL( file_writeline ) }, + { LSTRKEY( "seek" ), LFUNCVAL( file_seek ) }, + { LSTRKEY( "flush" ), LFUNCVAL( file_flush ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( file_obj_free ) }, + { LSTRKEY( "__index" ), LROVAL( file_obj_map ) }, + { LNILKEY, LNILVAL } +}; + // Module function map static const LUA_REG_TYPE file_map[] = { { LSTRKEY( "list" ), LFUNCVAL( file_list ) }, @@ -416,4 +533,9 @@ static const LUA_REG_TYPE file_map[] = { { LNILKEY, LNILVAL } }; -NODEMCU_MODULE(FILE, "file", file_map, NULL); +int luaopen_file( lua_State *L ) { + luaL_rometatable( L, "file.obj", (void *)file_obj_map ); + return 0; +} + +NODEMCU_MODULE(FILE, "file", file_map, luaopen_file); diff --git a/docs/en/modules/file.md b/docs/en/modules/file.md index 1c31c960..a197d605 100644 --- a/docs/en/modules/file.md +++ b/docs/en/modules/file.md @@ -7,9 +7,7 @@ The file module provides access to the file system and its individual files. The file system is a flat file system, with no notion of subdirectories/folders. -Only one file can be open at any given time. - -Besides the SPIFFS file system on internal flash, this module can also access FAT partitions on an external SD card is [FatFS is enabled](../sdcard.md). +Besides the SPIFFS file system on internal flash, this module can also access FAT partitions on an external SD card if [FatFS is enabled](../sdcard.md). ```lua -- open file in flash: @@ -34,6 +32,10 @@ Change current directory (and drive). This will be used when no drive/directory Current directory defaults to the root of internal SPIFFS (`/FLASH`) after system start. +!!! note + + Function is only available when [FatFS support](../sdcard.md#enabling-fatfs) is compiled into the firmware. + #### Syntax `file.chdir(dir)` @@ -43,30 +45,6 @@ Current directory defaults to the root of internal SPIFFS (`/FLASH`) after syste #### Returns `true` on success, `false` otherwise -## file.close() - -Closes the open file, if any. - -#### Syntax -`file.close()` - -#### Parameters -none - -#### Returns -`nil` - -#### Example -```lua --- open 'init.lua', print the first line. -if file.open("init.lua", "r") then - print(file.readline()) - file.close() -end -``` -#### See also -[`file.open()`](#fileopen) - ## file.exists() Determines whether the specified file exists. @@ -95,39 +73,13 @@ end #### See also [`file.list()`](#filelist) -## file.flush() - -Flushes any pending writes to the file system, ensuring no data is lost on a restart. Closing the open file using [`file.close()`](#fileclose) performs an implicit flush as well. - -#### Syntax -`file.flush()` - -#### Parameters -none - -#### Returns -`nil` - -#### Example -```lua --- open 'init.lua' in 'a+' mode -if file.open("init.lua", "a+") then - -- write 'foo bar' to the end of the file - file.write('foo bar') - file.flush() - -- write 'baz' too - file.write('baz') - file.close() -end -``` -#### See also -[`file.close()`](#fileclose) - ## file.format() Format the file system. Completely erases any existing file system and writes a new one. Depending on the size of the flash chip in the ESP, this may take several seconds. -Not supported for SD cards. +!!! note + + Function is not supported for SD cards. #### Syntax `file.format()` @@ -145,7 +97,9 @@ none Returns the flash address and physical size of the file system area, in bytes. -Not supported for SD cards. +!!! note + + Function is not supported for SD cards. #### Syntax `file.fscfg()` @@ -211,6 +165,7 @@ end Registers callback functions. Trigger events are: + - `rtc` deliver current date & time to the file system. Function is expected to return a table containing the fields `year`, `mon`, `day`, `hour`, `min`, `sec` of current date and time. Not supported for internal flash. #### Syntax @@ -255,7 +210,7 @@ When done with the file, it must be closed using `file.close()`. - "a+": append update mode, previous data is preserved, writing is only allowed at the end of file #### Returns -`nil` if file not opened, or not exists (read modes). `true` if file opened ok. +file object if file opened ok. `nil` if file not opened, or not exists (read modes). #### Example ```lua @@ -265,70 +220,19 @@ if file.open("init.lua", "r") then file.close() end ``` -#### See also -- [`file.close()`](#fileclose) -- [`file.readline()`](#filereadline) - -## file.read() - -Read content from the open file. - -#### Syntax -`file.read([n_or_str])` - -#### Parameters -- `n_or_str`: - - if nothing passed in, read up to `LUAL_BUFFERSIZE` bytes (default 1024) or the entire file (whichever is smaller) - - if passed a number n, then read the file until the lesser of `n` bytes, `LUAL_BUFFERSIZE` bytes, or EOF is reached. Specifying a number larger than the buffer size will read the buffer size. - - if passed a string `str`, then read until `str` appears next in the file, `LUAL_BUFFERSIZE` bytes have been read, or EOF is reached - -#### Returns -File content as a string, or nil when EOF - -#### Example +#### Example (object model) ```lua --- print the first line of 'init.lua' -if file.open("init.lua", "r") then - print(file.read('\n')) - file.close() -end - --- print the first 5 bytes of 'init.lua' -if file.open("init.lua", "r") then - print(file.read(5)) - file.close() +-- open 'init.lua', print the first line. +fd = file.open("init.lua", "r") +if fd then + print(fd:readline()) + fd:close(); fd = nil end ``` #### See also -- [`file.open()`](#fileopen) -- [`file.readline()`](#filereadline) - -## file.readline() - -Read the next line from the open file. Lines are defined as zero or more bytes ending with a EOL ('\n') byte. If the next line is longer than `LUAL_BUFFERSIZE`, this function only returns the first `LUAL_BUFFERSIZE` bytes (this is 1024 bytes by default). - -#### Syntax -`file.readline()` - -#### Parameters -none - -#### Returns -File content in string, line by line, including EOL('\n'). Return `nil` when EOF. - -#### Example -```lua --- print the first line of 'init.lua' -if file.open("init.lua", "r") then - print(file.readline()) - file.close() -end -``` -#### See also -- [`file.open()`](#fileopen) -- [`file.close()`](#fileclose) -- [`file.read()`](#filereade) +- [`file.close()`](#fileclose-fileobjclose) +- [`file.readline()`](#filereadline-fileobjreadline) ## file.remove() @@ -373,12 +277,183 @@ Renames a file. If a file is currently open, it will be closed first. file.rename("temp.lua","init.lua") ``` -## file.seek() +# File access functions + +The `file` module provides several functions to access the content of a file after it has been opened with [`file.open()`](#fileopen). They can be used as part of a basic model or an object model: + +## Basic model +In the basic model there is max one file opened at a time. The file access functions operate on this file per default. If another file is opened, the previous default file needs to be closed beforehand. + +```lua +-- open 'init.lua', print the first line. +if file.open("init.lua", "r") then + print(file.readline()) + file.close() +end +``` + +## Object model +Files are represented by file objects which are created by `file.open()`. File access functions are available as methods of this object, and multiple file objects can coexist. + +```lua +src = file.open("init.lua", "r") +if src then + dest = file.open("copy.lua", "w") + if dest then + local line + repeat + line = src:read() + if line then + dest:write(line) + end + until line == nil + dest:close(); dest = nil + end + src:close(); dest = nil +end +``` + +!!! Attention + + It is recommended to use only one single model within the application. Concurrent use of both models can yield unpredictable behavior: Closing the default file from basic model will also close the correspoding file object. Closing a file from object model will also close the default file if they are the same file. + +!!! Note + + The maximum number of open files on SPIFFS is determined at compile time by `SPIFFS_MAX_OPEN_FILES` in `user_config.h`. + +## file.close(), file.obj:close() + +Closes the open file, if any. + +#### Syntax +`file.close()` + +`fd:close()` + +#### Parameters +none + +#### Returns +`nil` + +#### See also +[`file.open()`](#fileopen) + +## file.flush(), file.obj:flush() + +Flushes any pending writes to the file system, ensuring no data is lost on a restart. Closing the open file using [`file.close()` / `fd:close()`](#fileclose-fileobjclose) performs an implicit flush as well. + +#### Syntax +`file.flush()` + +`fd:flush()` + +#### Parameters +none + +#### Returns +`nil` + +#### Example (basic model) +```lua +-- open 'init.lua' in 'a+' mode +if file.open("init.lua", "a+") then + -- write 'foo bar' to the end of the file + file.write('foo bar') + file.flush() + -- write 'baz' too + file.write('baz') + file.close() +end +``` + +#### See also +[`file.close()` / `file.obj:close()`](#fileclose-fileobjclose) + +## file.read(), file.obj:read() + +Read content from the open file. + +!!! note + + The function temporarily allocates 2 * (number of requested bytes) on the heap for buffering and processing the read data. Default chunk size (`FILE_READ_CHUNK`) is 1024 bytes and is regarded to be safe. Pushing this by 4x or more can cause heap overflows depending on the application. Consider this when selecting a value for parameter `n_or_char`. + +#### Syntax +`file.read([n_or_char])` + +`fd:read([n_or_char])` + +#### Parameters +- `n_or_char`: + - if nothing passed in, then read up to `FILE_READ_CHUNK` bytes or the entire file (whichever is smaller). + - if passed a number `n`, then read up to `n` bytes or the entire file (whichever is smaller). + - if passed a string containing the single character `char`, then read until `char` appears next in the file, `FILE_READ_CHUNK` bytes have been read, or EOF is reached. + +#### Returns +File content as a string, or nil when EOF + +#### Example (basic model) +```lua +-- print the first line of 'init.lua' +if file.open("init.lua", "r") then + print(file.read('\n')) + file.close() +end +``` + +#### Example (object model) +```lua +-- print the first 5 bytes of 'init.lua' +fd = file.open("init.lua", "r") +if fd then + print(fd:read(5)) + fd:close(); fd = nil +end +``` + +#### See also +- [`file.open()`](#fileopen) +- [`file.readline()` / `file.obj:readline()`](#filereadline-fileobjreadline) + +## file.readline(), file.obj:readline() + +Read the next line from the open file. Lines are defined as zero or more bytes ending with a EOL ('\n') byte. If the next line is longer than 1024, this function only returns the first 1024 bytes. + +#### Syntax +`file.readline()` + +`fd:readline()` + +#### Parameters +none + +#### Returns +File content in string, line by line, including EOL('\n'). Return `nil` when EOF. + +#### Example (basic model) +```lua +-- print the first line of 'init.lua' +if file.open("init.lua", "r") then + print(file.readline()) + file.close() +end +``` + +#### See also +- [`file.open()`](#fileopen) +- [`file.close()` / `file.obj:close()`](#fileclose-fileobjclose) +- [`file.read()` / `file.obj:read()`](#fileread-fileobjread) + + +## file.seek(), file.obj:seek() + Sets and gets the file position, measured from the beginning of the file, to the position given by offset plus a base specified by the string whence. #### Syntax `file.seek([whence [, offset]])` +`fd:seek([whence [, offset]])` + #### Parameters - `whence` - "set": base is position 0 (beginning of the file) @@ -391,7 +466,7 @@ If no parameters are given, the function simply returns the current file offset. #### Returns the resulting file position, or `nil` on error -#### Example +#### Example (basic model) ```lua if file.open("init.lua", "r") then -- skip the first 5 bytes of the file @@ -403,20 +478,22 @@ end #### See also [`file.open()`](#fileopen) -## file.write() +## file.write(), file.obj:write() Write a string to the open file. #### Syntax `file.write(string)` +`fd:write(string)` + #### Parameters `string` content to be write to file #### Returns `true` if the write is ok, `nil` on error -#### Example +#### Example (basic model) ```lua -- open 'init.lua' in 'a+' mode if file.open("init.lua", "a+") then @@ -426,24 +503,37 @@ if file.open("init.lua", "a+") then end ``` +#### Example (object model) +```lua +-- open 'init.lua' in 'a+' mode +fd = file.open("init.lua", "a+") +if fd then + -- write 'foo bar' to the end of the file + fd:write('foo bar') + fd:close() +end +``` + #### See also - [`file.open()`](#fileopen) -- [`file.writeline()`](#filewriteline) +- [`file.writeline()` / `file.obj:writeline()`](#filewriteline-fileobjwriteline) -## file.writeline() +## file.writeline(), file.obj:writeline() Write a string to the open file and append '\n' at the end. #### Syntax `file.writeline(string)` +`fd:writeline(string)` + #### Parameters `string` content to be write to file #### Returns `true` if write ok, `nil` on error -#### Example +#### Example (basic model) ```lua -- open 'init.lua' in 'a+' mode if file.open("init.lua", "a+") then @@ -455,4 +545,4 @@ end #### See also - [`file.open()`](#fileopen) -- [`file.readline()`](#filereadline) +- [`file.readline()` / `file.obj:readline()`](#filereadline-fileobjreadline)