add unix and extend
diff --git a/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md b/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md new file mode 100644 index 0000000..6356677 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-cli-fs-unix-standard-extensions.md
@@ -0,0 +1,55 @@ +# CLI Filesystem Unix Standard Extensions Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extend CLI filesystem mode with a first slice of standard Unix-style database operations. + +**Architecture:** Keep command parsing in `FilesystemCommandParser`, command dispatch in `FilesystemShell`, and database mutations behind `FilesystemMutationProvider`. Table-mode mutations map to SQL through `TableFilesystemMutationProvider`; tree-mode writes remain unsupported through `UnsupportedFilesystemMutationProvider`. + +**Tech Stack:** Java, JUnit 4, Mockito, Maven module `iotdb-client/cli`. + +--- + +### Task 1: Add Standard Command Parsing + +**Files:** +- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java` + +- [ ] Write failing tests for `rmdir <path>`, `rm -r <path>`, `cp <source> <target>`, and `ls -R [path]`. +- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest test` and verify the new tests fail because commands are not implemented. +- [ ] Implement the minimal parser changes. +- [ ] Re-run the parser test and verify it passes. + +### Task 2: Add Shell Dispatch + +**Files:** +- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java` + +- [ ] Write failing shell tests proving `rmdir`, `rm -r`, and `cp` call the mutation provider only when writes are enabled, and `ls -R` recursively lists children. +- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemShellTest test` and verify the tests fail for missing behavior. +- [ ] Implement minimal shell dispatch and provider interface methods. +- [ ] Re-run the shell test and verify it passes. + +### Task 3: Add Table Mutation SQL + +**Files:** +- Modify: `iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java` +- Modify: `iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java` + +- [ ] Write failing provider tests for dropping a database through `rmdir`/recursive remove and copying `/db/t1.schema` to `/db/t2.schema`. +- [ ] Run `mvn -pl iotdb-client/cli -Dtest=TableFilesystemMutationProviderTest test` and verify failures. +- [ ] Implement minimal table mutation SQL: `DROP DATABASE <db>` and `CREATE TABLE <target> LIKE <source>`. +- [ ] Re-run provider tests and verify they pass. + +### Task 4: Regression Verification + +**Files:** +- Existing fs-mode tests. + +- [ ] Run `mvn -pl iotdb-client/cli -Dtest=FilesystemCommandParserTest,FilesystemShellTest,TableFilesystemMutationProviderTest,CliFilesystemModeTest test`. +- [ ] Fix only failures caused by this change.
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java index 407744f..508a084 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/FilesystemShell.java
@@ -54,8 +54,8 @@ private static final List<String> COMMANDS = Arrays.asList( "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", "grep", "find", "less", - "more", "file", "du", "mkdir", "rm", "mv", "cut", "paste", "join", "tree", "help", "exit", - "quit", "tee"); + "more", "file", "du", "mkdir", "rmdir", "rm", "mv", "cp", "cut", "paste", "join", "tree", + "help", "exit", "quit", "tee"); private final CliContext ctx; private final FilesystemSchemaProvider provider; @@ -127,12 +127,18 @@ case MKDIR: mkdir(command.getPath()); return true; + case RMDIR: + rmdir(command.getPath()); + return true; case RM: - remove(command.getPath()); + remove(command.getPath(), command.getOption()); return true; case MV: move(command.getPaths()); return true; + case CP: + copy(command.getPaths()); + return true; case CUT: printCut(command.getPath(), command.getOption(), command.getPattern()); return true; @@ -432,11 +438,23 @@ mutationProvider.mkdir(resolvedPath); } - private void remove(String path) throws SQLException { + private void rmdir(String path) throws SQLException { + FsPath resolvedPath = resolve(path); + if (!ensureWritable("rmdir", resolvedPath)) { + return; + } + mutationProvider.rmdir(resolvedPath); + } + + private void remove(String path, String option) throws SQLException { FsPath resolvedPath = resolve(path); if (!ensureWritable("rm", resolvedPath)) { return; } + if ("-r".equals(option)) { + mutationProvider.removeRecursive(resolvedPath); + return; + } mutationProvider.remove(resolvedPath); } @@ -449,6 +467,15 @@ mutationProvider.move(source, target); } + private void copy(List<String> paths) throws SQLException { + FsPath source = resolve(paths.get(0)); + FsPath target = resolve(paths.get(1)); + if (!ensureWritable("cp", source)) { + return; + } + mutationProvider.copy(source, target); + } + private void append(String path, boolean nonInteractive) throws SQLException { FsPath resolvedPath = resolve(path); if (!ensureWritable("tee", resolvedPath)) { @@ -549,8 +576,10 @@ ctx.getPrinter().println("file <path>"); ctx.getPrinter().println("du <path>"); ctx.getPrinter().println("mkdir <path>"); + ctx.getPrinter().println("rmdir <path>"); ctx.getPrinter().println("rm <path>"); ctx.getPrinter().println("mv <source> <target>"); + ctx.getPrinter().println("cp <source> <target>"); ctx.getPrinter().println("cut -d<delimiter> -f<fields> <path>"); ctx.getPrinter().println("paste <path>..."); ctx.getPrinter().println("join [-t delimiter] [-1 field] [-2 field] <path1> <path2>");
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java index 7d37923..14a6f19 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommand.java
@@ -41,8 +41,10 @@ FILE, DU, MKDIR, + RMDIR, RM, MV, + CP, CUT, PASTE, JOIN,
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java index 565b9ba..8b1aea2 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParser.java
@@ -100,12 +100,18 @@ if ("mkdir".equals(command)) { return FilesystemCommand.path(FilesystemCommand.Type.MKDIR, pathArgument(tokens)); } + if ("rmdir".equals(command)) { + return FilesystemCommand.path(FilesystemCommand.Type.RMDIR, pathArgument(tokens)); + } if ("rm".equals(command)) { return parseRm(tokens); } if ("mv".equals(command)) { return parseMv(tokens); } + if ("cp".equals(command)) { + return parseCp(tokens); + } if ("cut".equals(command)) { return parseCut(tokens); } @@ -255,6 +261,9 @@ return FilesystemCommand.invalid("Missing rm path"); } if (tokens[1].startsWith("-")) { + if ("-r".equals(tokens[1]) && tokens.length >= 3) { + return FilesystemCommand.option(FilesystemCommand.Type.RM, "-r", tokens[2]); + } return FilesystemCommand.invalid("Unsupported rm option: " + tokens[1]); } return FilesystemCommand.path(FilesystemCommand.Type.RM, tokens[1]); @@ -270,10 +279,21 @@ return FilesystemCommand.paths(FilesystemCommand.Type.MV, paths); } + private static FilesystemCommand parseCp(String[] tokens) { + if (tokens.length < 3) { + return FilesystemCommand.invalid("Usage: cp <source> <target>"); + } + List<String> paths = new ArrayList<>(); + paths.add(tokens[1]); + paths.add(tokens[2]); + return FilesystemCommand.paths(FilesystemCommand.Type.CP, paths); + } + private static FilesystemCommand parseList(String[] tokens, boolean longMode) { FilesystemCommand.Type type = longMode ? FilesystemCommand.Type.LL : FilesystemCommand.Type.LS; String path = DEFAULT_PATH; boolean all = false; + boolean recursive = false; for (int i = 1; i < tokens.length; i++) { String token = tokens[i]; @@ -284,6 +304,8 @@ type = FilesystemCommand.Type.LL; } else if (option == 'a') { all = true; + } else if (option == 'R') { + recursive = true; } else { return FilesystemCommand.invalid("Unsupported ls option: -" + option); } @@ -292,6 +314,9 @@ path = token; } } + if (recursive) { + return FilesystemCommand.tree(path, DEFAULT_TREE_DEPTH); + } return FilesystemCommand.option(type, all ? "-a" : "", path); }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java index 3d1d3af..81c7df6 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
@@ -28,9 +28,15 @@ void mkdir(FsPath path) throws SQLException; + void rmdir(FsPath path) throws SQLException; + void remove(FsPath path) throws SQLException; + void removeRecursive(FsPath path) throws SQLException; + void move(FsPath source, FsPath target) throws SQLException; + void copy(FsPath source, FsPath target) throws SQLException; + void append(FsPath path, List<String> lines) throws SQLException; }
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java index 710ff02..e4642be 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -30,6 +30,7 @@ private static final String INVALID_WRITE_OPERATION = "Invalid filesystem write operation for this path"; private static final String CSV_SUFFIX = ".csv"; + private static final String SCHEMA_SUFFIX = ".schema"; private final SqlExecutor executor; @@ -46,6 +47,11 @@ } @Override + public void rmdir(FsPath path) throws SQLException { + dropDatabase(path); + } + + @Override public void remove(FsPath path) throws SQLException { if (!isDataFile(path)) { throw invalidOperation(); @@ -54,6 +60,11 @@ } @Override + public void removeRecursive(FsPath path) throws SQLException { + dropDatabase(path); + } + + @Override public void move(FsPath source, FsPath target) throws SQLException { if (!isDataFile(source) || !isDataFile(target)) { throw invalidOperation(); @@ -69,6 +80,18 @@ } @Override + public void copy(FsPath source, FsPath target) throws SQLException { + if (!isSchemaFile(source) || !isSchemaFile(target)) { + throw invalidOperation(); + } + executor.execute( + "CREATE TABLE " + + toTablePath(target, SCHEMA_SUFFIX) + + " LIKE " + + toTablePath(source, SCHEMA_SUFFIX)); + } + + @Override public void append(FsPath path, List<String> lines) throws SQLException { if (!isDataFile(path)) { throw invalidOperation(); @@ -91,10 +114,21 @@ return new SQLException(INVALID_WRITE_OPERATION); } + private void dropDatabase(FsPath path) throws SQLException { + if (path.getSegments().size() != 1) { + throw invalidOperation(); + } + executor.execute("DROP DATABASE " + TableFilesystemSql.identifier(path.getFileName())); + } + private static String toTablePath(FsPath path) { return TableFilesystemSql.tablePath(databaseName(path), tableName(path)); } + private static String toTablePath(FsPath path, String suffix) { + return TableFilesystemSql.tablePath(databaseName(path), tableName(path, suffix)); + } + private static String databaseName(FsPath path) { return path.getSegments().get(0); } @@ -103,11 +137,20 @@ return path.getSegments().size() == 2 && path.getFileName().endsWith(CSV_SUFFIX); } + private static boolean isSchemaFile(FsPath path) { + return path.getSegments().size() == 2 && path.getFileName().endsWith(SCHEMA_SUFFIX); + } + private static String tableName(FsPath path) { String fileName = path.getFileName(); return fileName.substring(0, fileName.length() - CSV_SUFFIX.length()); } + private static String tableName(FsPath path, String suffix) { + String fileName = path.getFileName(); + return fileName.substring(0, fileName.length() - suffix.length()); + } + private static FsPath parent(FsPath path) { List<String> segments = path.getSegments(); StringBuilder builder = new StringBuilder("/");
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java index 2d061c3..9b3cf6b 100644 --- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java +++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
@@ -34,16 +34,31 @@ } @Override + public void rmdir(FsPath path) throws SQLException { + throw unsupported(); + } + + @Override public void remove(FsPath path) throws SQLException { throw unsupported(); } @Override + public void removeRecursive(FsPath path) throws SQLException { + throw unsupported(); + } + + @Override public void move(FsPath source, FsPath target) throws SQLException { throw unsupported(); } @Override + public void copy(FsPath source, FsPath target) throws SQLException { + throw unsupported(); + } + + @Override public void append(FsPath path, List<String> lines) throws SQLException { throw unsupported(); }
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java index 6cb435e..78e9e88 100644 --- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java +++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/FilesystemShellTest.java
@@ -248,6 +248,28 @@ } @Test + public void executeStandardWriteCommandsWhenEnabled() throws SQLException { + shell = new FilesystemShell(shellContext(), provider, mutationProvider, true); + + assertTrue(shell.execute("rmdir /db1")); + assertTrue(shell.execute("rm -r /db2")); + assertTrue(shell.execute("cp /db1/table1.schema /db1/table2.schema")); + + verify(mutationProvider).rmdir(FsPath.absolute("/db1")); + verify(mutationProvider).removeRecursive(FsPath.absolute("/db2")); + verify(mutationProvider) + .copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1/table2.schema")); + } + + @Test + public void executeRecursiveRemoveRejectsReadOnlyMode() throws SQLException { + assertTrue(shell.execute("rm -r /db1")); + + assertTrue(out.toString().contains("rm: /db1: Read-only file system")); + verifyZeroInteractions(mutationProvider); + } + + @Test public void executeTeeRejectsReadOnlyMode() throws SQLException { assertTrue(shell.execute("tee -a /db1/table1.csv")); @@ -322,6 +344,28 @@ } @Test + public void executeLsRecursivePrintsChildren() throws SQLException { + when(provider.describe(FsPath.absolute("/"))) + .thenReturn(new FsNode("/", FsPath.absolute("/"), FsNodeType.VIRTUAL_ROOT)); + when(provider.list(FsPath.absolute("/"))) + .thenReturn( + Arrays.asList(new FsNode("db1", FsPath.absolute("/db1"), FsNodeType.TABLE_DATABASE))); + when(provider.list(FsPath.absolute("/db1"))) + .thenReturn( + Arrays.asList( + new FsNode( + "table1.csv", FsPath.absolute("/db1/table1.csv"), FsNodeType.TABLE_DATA_FILE))); + + assertTrue(shell.execute("ls -R /")); + + assertTrue(out.toString().contains("db1")); + assertTrue(out.toString().contains("table1.csv")); + verify(provider).describe(FsPath.absolute("/")); + verify(provider).list(FsPath.absolute("/")); + verify(provider).list(FsPath.absolute("/db1")); + } + + @Test public void executeTreeUnknownPathPrintsNoSuchFile() throws SQLException { when(provider.describe(FsPath.absolute("/db1/table1"))) .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), FsNodeType.UNKNOWN));
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java index df0bf2f..edf9f4b 100644 --- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java +++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/command/FilesystemCommandParserTest.java
@@ -293,6 +293,42 @@ } @Test + public void parseRmdirCommand() { + FilesystemCommand command = FilesystemCommandParser.parse("rmdir /db1"); + + assertEquals(FilesystemCommand.Type.RMDIR, command.getType()); + assertEquals("/db1", command.getPath()); + } + + @Test + public void parseRmRecursiveCommand() { + FilesystemCommand command = FilesystemCommandParser.parse("rm -r /db1"); + + assertEquals(FilesystemCommand.Type.RM, command.getType()); + assertEquals("-r", command.getOption()); + assertEquals("/db1", command.getPath()); + } + + @Test + public void parseCpCommand() { + FilesystemCommand command = + FilesystemCommandParser.parse("cp /db1/table1.schema /db1/table2.schema"); + + assertEquals(FilesystemCommand.Type.CP, command.getType()); + assertEquals(2, command.getPaths().size()); + assertEquals("/db1/table1.schema", command.getPaths().get(0)); + assertEquals("/db1/table2.schema", command.getPaths().get(1)); + } + + @Test + public void parseLsRecursiveAsTreeCommand() { + FilesystemCommand command = FilesystemCommandParser.parse("ls -R /db1"); + + assertEquals(FilesystemCommand.Type.TREE, command.getType()); + assertEquals("/db1", command.getPath()); + } + + @Test public void parseTreeDepthBeforePath() { FilesystemCommand command = FilesystemCommandParser.parse("tree -L 2 /root/sg");
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java index 17efc72..0a944b2 100644 --- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java +++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -78,6 +78,28 @@ } @Test + public void rmdirDatabaseDropsDatabase() throws SQLException { + provider.rmdir(FsPath.absolute("/db1")); + + verify(executor).execute("DROP DATABASE db1"); + } + + @Test + public void removeRecursiveDatabaseDropsDatabase() throws SQLException { + provider.removeRecursive(FsPath.absolute("/db1")); + + verify(executor).execute("DROP DATABASE db1"); + } + + @Test + public void rmdirAndRemoveRecursiveRejectUnsafeLevels() throws SQLException { + assertInvalidOperation(() -> provider.rmdir(FsPath.absolute("/"))); + assertInvalidOperation(() -> provider.rmdir(FsPath.absolute("/db1/table1.csv"))); + assertInvalidOperation(() -> provider.removeRecursive(FsPath.absolute("/"))); + assertInvalidOperation(() -> provider.removeRecursive(FsPath.absolute("/db1/table1.csv"))); + } + + @Test public void moveTableCsvRenamesTableInSameDatabase() throws SQLException { provider.move(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv")); @@ -101,6 +123,22 @@ } @Test + public void copySchemaCreatesTableLikeSource() throws SQLException { + provider.copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1/table2.schema")); + + verify(executor).execute("CREATE TABLE db1.table2 LIKE db1.table1"); + } + + @Test + public void copyRejectsNonSchemaPaths() throws SQLException { + assertInvalidOperation( + () -> + provider.copy(FsPath.absolute("/db1/table1.csv"), FsPath.absolute("/db1/table2.csv"))); + assertInvalidOperation( + () -> provider.copy(FsPath.absolute("/db1/table1.schema"), FsPath.absolute("/db1"))); + } + + @Test public void appendCsvWithHeaderBuildsMultiRowInsert() throws SQLException { mockTableSchema();