using System; using System.Collections; using System.IO; using System.Text; using Kalk.Core.Helpers; using Scriban.Runtime; namespace Kalk.Core.Modules { /// /// Modules providing file related functions. /// [KalkExportModule(ModuleName)] public partial class FileModule : KalkModuleWithFunctions { private const string ModuleName = "Files"; public const string CategoryMiscFile = "Misc File Functions"; public FileModule() : base(ModuleName) { RegisterFunctionsAuto(); } /// /// Gets the current directory. /// /// The current directory. /// /// ```kalk /// >>> pwd /// # pwd /// out = "/code/kalk/tests" /// ``` /// [KalkExport("pwd", CategoryMiscFile)] public string CurrentDirectory() { return Environment.CurrentDirectory; } /// /// Changes the current directory to the specified path. /// /// Path to the directory to change. /// The current directory or throws an exception if the directory does not exists /// /// ```kalk /// >>> cd /// # cd /// out = "/code/kalk/tests" /// >>> mkdir "testdir" /// >>> cd "testdir" /// # cd("testdir") /// out = "/code/kalk/tests/testdir" /// >>> cd ".." /// # cd("..") /// out = "/code/kalk/tests" /// >>> rmdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = false /// ``` /// [KalkExport("cd", CategoryMiscFile)] public string ChangeDirectory(string path = null) { if (path != null) { if (!DirectoryExists(path)) throw new ArgumentException($"The folder `{path}` does not exists", nameof(path)); Environment.CurrentDirectory = Path.Combine(Environment.CurrentDirectory, path); } return CurrentDirectory(); } /// /// Checks if the specified file path exists on the disk. /// /// Path to a file. /// `true` if the specified file path exists on the disk. /// /// ```kalk /// >>> rm "test.txt" /// >>> file_exists "test.txt" /// # file_exists("test.txt") /// out = false /// >>> save_text("content", "test.txt") /// >>> file_exists "test.txt" /// # file_exists("test.txt") /// out = true /// ``` /// [KalkExport("file_exists", CategoryMiscFile)] public KalkBool FileExists(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); return Engine.FileService.FileExists(Path.Combine(Environment.CurrentDirectory, path)); } /// /// Checks if the specified directory path exists on the disk. /// /// Path to a directory. /// `true` if the specified directory path exists on the disk. /// /// ```kalk /// >>> mkdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = true /// >>> rmdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = false /// ``` /// [KalkExport("dir_exists", CategoryMiscFile)] public KalkBool DirectoryExists(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); return Engine.FileService.DirectoryExists(Path.Combine(Environment.CurrentDirectory, path)); } /// /// List files and directories from the specified path or the current directory. /// /// The specified directory or the current directory if not specified. /// A boolean to perform a recursive list. Default is `false`. /// An enumeration of the files and directories. /// /// ```kalk /// >>> mkdir "testdir" /// >>> cd "testdir" /// # cd("testdir") /// out = "/code/kalk/tests/testdir" /// >>> mkdir "subdir" /// >>> save_text("content", "file.txt") /// >>> dir "." /// # dir(".") /// out = ["./file.txt", "./subdir"] /// >>> save_text("content", "subdir/file2.txt") /// >>> dir(".", true) /// # dir(".", true) /// out = ["./file.txt", "./subdir", "./subdir/file2.txt"] /// >>> cd ".." /// # cd("..") /// out = "/code/kalk/tests" /// >>> rmdir("testdir", true) /// ``` /// [KalkExport("dir", CategoryMiscFile)] public IEnumerable DirectoryListing(string path = null, bool recursive = false) { string search = "*"; if (!string.IsNullOrEmpty(path)) { var wildcardIndex = path.IndexOf('*', StringComparison.OrdinalIgnoreCase); if (wildcardIndex >= 0) { search = path.Substring(wildcardIndex); path = path.Substring(0, wildcardIndex); } } var fullDir = string.IsNullOrEmpty(path) ? Environment.CurrentDirectory : Path.Combine(Environment.CurrentDirectory, path); if (!Engine.FileService.DirectoryExists(fullDir)) throw new ArgumentException($"Directory `{fullDir}` not found."); if (string.IsNullOrEmpty(path)) { path = "."; } return new ScriptRange(Engine.FileService.EnumerateFileSystemEntries(path, search, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)); } /// /// Deletes a file from the specified path. /// /// Path to the file to delete. /// /// ```kalk /// >>> rm "test.txt" /// >>> file_exists "test.txt" /// # file_exists("test.txt") /// out = false /// >>> save_text("content", "test.txt") /// >>> file_exists "test.txt" /// # file_exists("test.txt") /// out = true /// ``` /// [KalkExport("rm", CategoryMiscFile)] public void RemoveFile(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); if (FileExists(path)) { Engine.FileService.FileDelete(Path.Combine(Environment.CurrentDirectory, path)); } } /// /// Creates a directory at the specified path. /// /// Path of the directory to create. /// /// ```kalk /// >>> mkdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = true /// >>> rmdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = false /// ``` /// [KalkExport("mkdir", CategoryMiscFile)] public void CreateDirectory(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); if (DirectoryExists(path)) return; Engine.FileService.DirectoryCreate(Path.Combine(Environment.CurrentDirectory, path)); } /// /// Deletes the directory at the specified path. /// /// Path to the directory to delete. /// /// ```kalk /// >>> mkdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = true /// >>> rmdir "testdir" /// >>> dir_exists "testdir" /// # dir_exists("testdir") /// out = false /// ``` /// [KalkExport("rmdir", CategoryMiscFile)] public void RemoveDirectory(string path, bool recursive = true) { if (path == null) throw new ArgumentNullException(nameof(path)); if (DirectoryExists(path)) { Engine.FileService.DirectoryDelete(Path.Combine(Environment.CurrentDirectory, path), recursive); } } /// /// Loads the specified file as text. /// /// Path to a file to load as text. /// The encoding of the file. Default is "utf-8" /// The file loaded as a string. /// /// ```kalk /// >>> load_text "test.csv" /// # load_text("test.csv") /// out = "a,b,c\n1,2,3\n4,5,6" /// ``` /// [KalkExport("load_text", CategoryMiscFile)] public string LoadText(string path, string encoding = KalkConfig.DefaultEncoding) { var fullPath = AssertReadFile(path); var encoder = GetEncoding(encoding); using var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return new StreamReader(stream, encoder).ReadToEnd(); } /// /// Loads the specified file as binary. /// /// Path to a file to load as binary. /// The file loaded as a a byte buffer. /// /// ```kalk /// >>> load_bytes "test.csv" /// # load_bytes("test.csv") /// out = bytebuffer([97, 44, 98, 44, 99, 10, 49, 44, 50, 44, 51, 10, 52, 44, 53, 44, 54]) /// >>> ascii out /// # ascii(out) /// out = "a,b,c\n1,2,3\n4,5,6" /// ``` /// [KalkExport("load_bytes", CategoryMiscFile)] public KalkNativeBuffer LoadBytes(string path) { var fullPath = AssertReadFile(path); using var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var buffer = new KalkNativeBuffer((int)stream.Length); stream.Read(buffer.AsSpan()); return buffer; } /// /// Load each lines from the specified file path. /// /// Path to a file to load lines from. /// The encoding of the file. Default is "utf-8" /// An enumeration on the lines. /// /// ```kalk /// >>> load_lines "test.csv" /// # load_lines("test.csv") /// out = ["a,b,c", "1,2,3", "4,5,6"] /// ``` /// [KalkExport("load_lines", CategoryMiscFile)] public ScriptRange LoadLines(string path, string encoding = KalkConfig.DefaultEncoding) { var fullPath = AssertReadFile(path); var encoder = GetEncoding(encoding); return new ScriptRange(new LineReader(() => { var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return new StreamReader(stream, encoder); })); } /// /// Saves an array of data as string to the specified files. /// /// An array of data. /// Path to the file to save the lines to. /// The encoding of the file. Default is "utf-8" /// /// ```kalk /// >>> save_lines(1..10, "lines.txt") /// >>> load_lines("lines.txt") /// # load_lines("lines.txt") /// out = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] /// ``` /// [KalkExport("save_lines", CategoryMiscFile)] public object SaveLines(IEnumerable lines, string path, string encoding = KalkConfig.DefaultEncoding) { if (lines == null) throw new ArgumentNullException(nameof(lines)); var fullPath = AssertWriteFile(path); var encoder = GetEncoding(encoding); using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write); using var writer = new StreamWriter(stream, encoder); foreach (var lineObj in lines) { var line = Engine.ObjectToString(lineObj); writer.WriteLine(line); } // return object to allow this function to be used in a pipe return null; } /// /// Saves a text to the specified file path. /// /// The text to save. /// Path to the file to save the text to. /// The encoding of the file. Default is "utf-8" /// /// ```kalk /// >>> save_text("Hello World!", "test.txt") /// >>> load_text("test.txt") /// # load_text("test.txt") /// out = "Hello World!" /// ``` /// [KalkExport("save_text", CategoryMiscFile)] public object SaveText(string text, string path, string encoding = KalkConfig.DefaultEncoding) { var fullPath = AssertWriteFile(path); var encoder = GetEncoding(encoding); using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write); using var writer = new StreamWriter(stream, encoder); text ??= string.Empty; writer.Write(text); // return object to allow this function to be used in a pipe return null; } /// /// Saves a byte buffer to the specified file path. /// /// The data to save. /// Path to the file to save the data to. /// /// ```kalk /// >>> utf8("Hello World!") /// # utf8("Hello World!") /// out = bytebuffer([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]) /// >>> save_bytes(out, "test.bin") /// >>> load_bytes("test.bin") /// # load_bytes("test.bin") /// out = bytebuffer([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]) /// >>> utf8(out) /// # utf8(out) /// out = "Hello World!" /// ``` /// [KalkExport("save_bytes", CategoryMiscFile)] public object SaveBytes(IEnumerable data, string path) { var fullPath = AssertWriteFile(path); using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write); switch (data) { case ScriptRange range when range.Values is byte[] byteBuffer: stream.Write(byteBuffer, 0, byteBuffer.Length); break; case KalkNativeBuffer byteBuffer: { stream.Write(byteBuffer.AsSpan()); break; } case ScriptArray scriptByteArray: for (int i = 0; i < scriptByteArray.Count; i++) { stream.WriteByte(scriptByteArray[i]); } break; default: { if (data != null) { foreach (var item in data) { var b = Engine.ToObject(0, item); stream.WriteByte(b); } } break; } } // return object to allow this function to be used in a pipe return null; } public string AssertWriteFile(string path) { return AssertFile(path, true); } public string AssertReadFile(string path) { return AssertFile(path, false); } private string AssertFile(string path, bool toWrite) { if (path == null) throw new ArgumentNullException(nameof(path)); var fullPath = Path.Combine(Environment.CurrentDirectory, path); if (toWrite) { var directory = Path.GetDirectoryName(fullPath); if (!Engine.FileService.DirectoryExists(directory)) { try { Engine.FileService.DirectoryCreate(directory); } catch (Exception ex) { throw new ArgumentException($"Cannot write to file {path}. Cannot create directory {directory}. Reason: {ex.Message}"); } } } else { if (!Engine.FileService.FileExists(fullPath)) throw new ArgumentException($"File {path} was not found."); } return fullPath; } private static Encoding GetEncoding(string encoding) { if (encoding == null) throw new ArgumentNullException(nameof(encoding)); try { switch (encoding) { case "utf-8": return Encoding.UTF8; case "utf-32": return Encoding.UTF32; case "ascii": return Encoding.ASCII; default: return Encoding.GetEncoding(encoding); } } catch (Exception ex) { throw new ArgumentException($"Invalid encoding name `{encoding}`. Reason: {ex.Message}"); } } } }