This is an automated email from the ASF dual-hosted git repository.

SpriCoder pushed a commit to branch fs/inner-view
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 08ad58029e76a817b82a75ef2445a4206381a6d0
Author: spricoder <[email protected]>
AuthorDate: Wed Apr 29 20:50:12 2026 +0800

    add write
---
 .../java/org/apache/iotdb/cli/AbstractCli.java     |  35 +++++
 .../src/main/java/org/apache/iotdb/cli/Cli.java    |  15 ++-
 .../org/apache/iotdb/cli/fs/FilesystemShell.java   | 142 ++++++++++++++++++++-
 .../iotdb/cli/fs/command/FilesystemCommand.java    |  57 ++++++++-
 .../cli/fs/command/FilesystemCommandParser.java    | 127 ++++++++++++++++++
 .../FilesystemMutationProvider.java}               |  13 +-
 .../cli/fs/provider/FilesystemSchemaProvider.java  |   8 ++
 .../provider/TableFilesystemMutationProvider.java  |  86 +++++++++++++
 .../fs/provider/TableFilesystemSchemaProvider.java |  48 +++++++
 .../fs/provider/TreeFilesystemSchemaProvider.java  |  29 +++++
 ... => UnsupportedFilesystemMutationProvider.java} |  29 +++--
 .../apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java   |   7 +
 .../org/apache/iotdb/cli/fs/sql/SqlExecutor.java   |   2 +
 .../org/apache/iotdb/cli/utils/JlineUtils.java     |   3 +-
 .../java/org/apache/iotdb/cli/AbstractCliTest.java |  29 +++++
 .../apache/iotdb/cli/CliFilesystemModeTest.java    |  26 ++++
 .../apache/iotdb/cli/fs/FilesystemShellTest.java   | 132 ++++++++++++++++++-
 .../fs/command/FilesystemCommandParserTest.java    |  65 ++++++++++
 .../TableFilesystemMutationProviderTest.java       | 103 +++++++++++++++
 .../TableFilesystemSchemaProviderTest.java         |  26 ++++
 .../iotdb/cli/fs/sql/JdbcSqlExecutorTest.java      |  10 ++
 21 files changed, 963 insertions(+), 29 deletions(-)

diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
index 5e7680d2cc5..1fce4ae8e27 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/AbstractCli.java
@@ -74,6 +74,10 @@ public abstract class AbstractCli {
   static final String ACCESS_MODE_NAME = "access mode";
   static final String ACCESS_MODE_SQL = "sql";
   static final String ACCESS_MODE_FILESYSTEM = "filesystem";
+  static final String FS_WRITE_MODE_ARGS = "fs_write_mode";
+  static final String FS_WRITE_MODE_NAME = "fs write mode";
+  static final String FS_WRITE_MODE_DISABLED = "disabled";
+  static final String FS_WRITE_MODE_ENABLED = "enabled";
 
   private static final String EXECUTE_ARGS = "e";
 
@@ -141,6 +145,7 @@ public abstract class AbstractCli {
   static String execute;
   static boolean hasExecuteSQL = false;
   static String accessMode = ACCESS_MODE_SQL;
+  static String fsWriteMode = FS_WRITE_MODE_DISABLED;
 
   static Set<String> keywordSet = new HashSet<>();
 
@@ -166,6 +171,7 @@ public abstract class AbstractCli {
     keywordSet.add("-" + ISO8601_ARGS);
     keywordSet.add("-" + RPC_COMPRESS_ARGS);
     keywordSet.add("--" + ACCESS_MODE_ARGS);
+    keywordSet.add("--" + FS_WRITE_MODE_ARGS);
   }
 
   static Options createOptions() {
@@ -238,6 +244,17 @@ public abstract class AbstractCli {
             .build();
     options.addOption(accessMode);
 
+    Option fsWriteMode =
+        Option.builder()
+            .longOpt(FS_WRITE_MODE_ARGS)
+            .argName(FS_WRITE_MODE_NAME)
+            .hasArg()
+            .desc(
+                "Filesystem write mode, supports disabled and enabled. Default 
is disabled."
+                    + " (optional)")
+            .build();
+    options.addOption(fsWriteMode);
+
     Option isRpcCompressed =
         Option.builder(RPC_COMPRESS_ARGS)
             .argName(RPC_COMPRESS_NAME)
@@ -281,6 +298,24 @@ public abstract class AbstractCli {
     AbstractCli.accessMode = accessMode;
   }
 
+  static String getFsWriteMode(CliContext ctx, CommandLine commandLine) throws 
ArgsErrorException {
+    String value = commandLine.getOptionValue(FS_WRITE_MODE_ARGS, 
FS_WRITE_MODE_DISABLED);
+    String normalized = value.toLowerCase(Locale.ROOT);
+    if (FS_WRITE_MODE_DISABLED.equals(normalized) || 
FS_WRITE_MODE_ENABLED.equals(normalized)) {
+      return normalized;
+    }
+    String msg =
+        String.format(
+            "%s: Unsupported fs write mode '%s'. Supported values are disabled 
and enabled.",
+            IOTDB, value);
+    ctx.getPrinter().println(msg);
+    throw new ArgsErrorException(msg);
+  }
+
+  static void setFsWriteMode(String fsWriteMode) {
+    AbstractCli.fsWriteMode = fsWriteMode;
+  }
+
   static String checkRequiredArg(
       CliContext ctx,
       String arg,
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
index 4ded47379b9..ad5571efb41 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/Cli.java
@@ -20,9 +20,12 @@
 package org.apache.iotdb.cli;
 
 import org.apache.iotdb.cli.fs.FilesystemShell;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.TableFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.TableFilesystemSchemaProvider;
 import org.apache.iotdb.cli.fs.provider.TreeFilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.UnsupportedFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.sql.JdbcSqlExecutor;
 import org.apache.iotdb.cli.type.ExitType;
 import org.apache.iotdb.cli.utils.CliContext;
@@ -145,6 +148,7 @@ public class Cli extends AbstractCli {
         setSqlDialect(commandLine.getOptionValue(Config.SQL_DIALECT));
       }
       setAccessMode(getAccessMode(ctx, commandLine));
+      setFsWriteMode(getFsWriteMode(ctx, commandLine));
     } catch (ArgsErrorException e) {
       ctx.getPrinter()
           .println(IOTDB_ERROR_PREFIX + ": Input params error because " + 
e.getMessage());
@@ -224,6 +228,11 @@ public class Cli extends AbstractCli {
           .println(
               IOTDB_ERROR_PREFIX + ": Can't execute filesystem command because 
" + e.getMessage());
       ctx.exit(CODE_ERROR);
+    } catch (TException e) {
+      ctx.getPrinter()
+          .println(
+              IOTDB_ERROR_PREFIX + ": Can't execute filesystem command because 
" + e.getMessage());
+      ctx.exit(CODE_ERROR);
     }
   }
 
@@ -264,12 +273,16 @@ public class Cli extends AbstractCli {
   static FilesystemShell createFilesystemShell(CliContext ctx, IoTDBConnection 
connection) {
     JdbcSqlExecutor executor = new JdbcSqlExecutor(connection);
     FilesystemSchemaProvider provider;
+    FilesystemMutationProvider mutationProvider;
     if (Constant.TABLE_DIALECT.equalsIgnoreCase(connection.getSqlDialect())) {
       provider = new TableFilesystemSchemaProvider(executor);
+      mutationProvider = new TableFilesystemMutationProvider(executor);
     } else {
       provider = new TreeFilesystemSchemaProvider(executor);
+      mutationProvider = new UnsupportedFilesystemMutationProvider();
     }
-    return new FilesystemShell(ctx, provider);
+    return new FilesystemShell(
+        ctx, provider, mutationProvider, 
FS_WRITE_MODE_ENABLED.equals(fsWriteMode));
   }
 
   private static boolean readerReadLine(CliContext ctx, IoTDBConnection 
connection) {
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 6a3cf9e976d..8929c964524 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
@@ -24,7 +24,9 @@ import 
org.apache.iotdb.cli.fs.command.FilesystemCommandParser;
 import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.node.FsNodeType;
 import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
+import org.apache.iotdb.cli.fs.provider.UnsupportedFilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.sql.SqlRow;
 import org.apache.iotdb.cli.utils.CliContext;
 
@@ -44,15 +46,28 @@ public class FilesystemShell {
   private static final int DEFAULT_READ_LIMIT = 20;
   private static final List<String> COMMANDS =
       Arrays.asList(
-          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "paste", "tree", 
"help", "exit", "quit");
+          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", 
"grep", "find", "less",
+          "more", "file", "du", "mkdir", "rm", "mv", "paste", "tree", "help", 
"exit", "quit");
 
   private final CliContext ctx;
   private final FilesystemSchemaProvider provider;
+  private final FilesystemMutationProvider mutationProvider;
+  private final boolean writeEnabled;
   private FsPath currentPath = FsPath.absolute("/");
 
   public FilesystemShell(CliContext ctx, FilesystemSchemaProvider provider) {
+    this(ctx, provider, new UnsupportedFilesystemMutationProvider(), false);
+  }
+
+  public FilesystemShell(
+      CliContext ctx,
+      FilesystemSchemaProvider provider,
+      FilesystemMutationProvider mutationProvider,
+      boolean writeEnabled) {
     this.ctx = ctx;
     this.provider = provider;
+    this.mutationProvider = mutationProvider;
+    this.writeEnabled = writeEnabled;
   }
 
   public boolean execute(String input) throws SQLException {
@@ -79,6 +94,37 @@ public class FilesystemShell {
       case HEAD:
         printRows(provider.read(resolve(command.getPath()), 
command.getLimit()));
         return true;
+      case TAIL:
+        printRows(provider.tail(resolve(command.getPath()), 
command.getLimit()));
+        return true;
+      case WC:
+        printLineCount(command.getPath());
+        return true;
+      case GREP:
+        printMatchingRows(command.getPath(), command.getPattern());
+        return true;
+      case FIND:
+        printFind(resolve(command.getPath()), command.getPattern());
+        return true;
+      case LESS:
+      case MORE:
+        printRows(provider.read(resolve(command.getPath()), 
DEFAULT_READ_LIMIT));
+        return true;
+      case FILE:
+        printFile(command.getPath());
+        return true;
+      case DU:
+        printDiskUsage(command.getPath());
+        return true;
+      case MKDIR:
+        mkdir(command.getPath());
+        return true;
+      case RM:
+        remove(command.getPath());
+        return true;
+      case MV:
+        move(command.getPaths());
+        return true;
       case PASTE:
         printRows(provider.read(resolve(command.getPaths()), 
DEFAULT_READ_LIMIT));
         return true;
@@ -151,8 +197,15 @@ public class FilesystemShell {
   }
 
   private void printNodes(List<FsNode> nodes) {
+    StringBuilder builder = new StringBuilder();
     for (FsNode node : nodes) {
-      ctx.getPrinter().println(node.getName());
+      if (builder.length() > 0) {
+        builder.append(',');
+      }
+      builder.append(node.getName());
+    }
+    if (builder.length() > 0) {
+      ctx.getPrinter().println(builder.toString());
     }
   }
 
@@ -181,6 +234,80 @@ public class FilesystemShell {
     }
   }
 
+  private void printLineCount(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(provider.count(resolvedPath) + " " + 
resolvedPath);
+  }
+
+  private void printMatchingRows(String path, String pattern) throws 
SQLException {
+    for (SqlRow row : provider.read(resolve(path), DEFAULT_READ_LIMIT)) {
+      String line = joinValues(row);
+      if (line.contains(pattern)) {
+        ctx.getPrinter().println(line);
+      }
+    }
+  }
+
+  private void printFind(FsPath path, String pattern) throws SQLException {
+    FsNode node = provider.describe(path);
+    if (matchesFind(node, pattern)) {
+      ctx.getPrinter().println(path.toString());
+    }
+    if (!isDirectory(node.getType())) {
+      return;
+    }
+    for (FsNode child : provider.list(path)) {
+      printFind(child.getPath(), pattern);
+    }
+  }
+
+  private static boolean matchesFind(FsNode node, String pattern) {
+    return pattern == null || pattern.isEmpty() || 
node.getName().equals(pattern);
+  }
+
+  private void printFile(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(resolvedPath + ": " + 
provider.describe(resolvedPath).getType());
+  }
+
+  private void printDiskUsage(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    ctx.getPrinter().println(provider.count(resolvedPath) + "\t" + 
resolvedPath);
+  }
+
+  private void mkdir(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("mkdir", resolvedPath)) {
+      return;
+    }
+    mutationProvider.mkdir(resolvedPath);
+  }
+
+  private void remove(String path) throws SQLException {
+    FsPath resolvedPath = resolve(path);
+    if (!ensureWritable("rm", resolvedPath)) {
+      return;
+    }
+    mutationProvider.remove(resolvedPath);
+  }
+
+  private void move(List<String> paths) throws SQLException {
+    FsPath source = resolve(paths.get(0));
+    FsPath target = resolve(paths.get(1));
+    if (!ensureWritable("mv", source)) {
+      return;
+    }
+    mutationProvider.move(source, target);
+  }
+
+  private boolean ensureWritable(String command, FsPath path) {
+    if (writeEnabled) {
+      return true;
+    }
+    ctx.getPrinter().println(command + ": " + path + ": Read-only file 
system");
+    return false;
+  }
+
   private static String joinValues(SqlRow row) {
     StringBuilder builder = new StringBuilder();
     for (String value : row.asMap().values()) {
@@ -202,6 +329,17 @@ public class FilesystemShell {
     ctx.getPrinter().println("stat [path]");
     ctx.getPrinter().println("cat <path>...");
     ctx.getPrinter().println("head [-n lines] <path>");
+    ctx.getPrinter().println("tail [-n lines] <path>");
+    ctx.getPrinter().println("wc -l <path>");
+    ctx.getPrinter().println("grep <pattern> <path>");
+    ctx.getPrinter().println("find [path] [-name pattern]");
+    ctx.getPrinter().println("less <path>");
+    ctx.getPrinter().println("more <path>");
+    ctx.getPrinter().println("file <path>");
+    ctx.getPrinter().println("du <path>");
+    ctx.getPrinter().println("mkdir <path>");
+    ctx.getPrinter().println("rm <path>");
+    ctx.getPrinter().println("mv <source> <target>");
     ctx.getPrinter().println("paste <path>...");
     ctx.getPrinter().println("tree [-L depth] [path]");
     ctx.getPrinter().println("exit");
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 2cdd492fdc5..67aa6561d41 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
@@ -32,6 +32,17 @@ public class FilesystemCommand {
     STAT,
     CAT,
     HEAD,
+    TAIL,
+    WC,
+    GREP,
+    FIND,
+    LESS,
+    MORE,
+    FILE,
+    DU,
+    MKDIR,
+    RM,
+    MV,
     PASTE,
     TREE,
     SQL,
@@ -45,6 +56,8 @@ public class FilesystemCommand {
   private final List<String> paths;
   private final int depth;
   private final int limit;
+  private final String option;
+  private final String pattern;
   private final String statement;
   private final String errorMessage;
 
@@ -54,6 +67,8 @@ public class FilesystemCommand {
       List<String> paths,
       int depth,
       int limit,
+      String option,
+      String pattern,
       String statement,
       String errorMessage) {
     this.type = type;
@@ -61,40 +76,60 @@ public class FilesystemCommand {
     this.paths = paths;
     this.depth = depth;
     this.limit = limit;
+    this.option = option;
+    this.pattern = pattern;
     this.statement = statement;
     this.errorMessage = errorMessage;
   }
 
   public static FilesystemCommand simple(Type type) {
-    return new FilesystemCommand(type, "", Collections.emptyList(), -1, -1, 
"", "");
+    return new FilesystemCommand(type, "", Collections.emptyList(), -1, -1, 
"", "", "", "");
   }
 
   public static FilesystemCommand path(Type type, String path) {
-    return new FilesystemCommand(type, path, Collections.singletonList(path), 
-1, -1, "", "");
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, "", "", "", "");
   }
 
   public static FilesystemCommand paths(Type type, List<String> paths) {
     String path = paths.isEmpty() ? "" : paths.get(0);
-    return new FilesystemCommand(type, path, 
Collections.unmodifiableList(paths), -1, -1, "", "");
+    return new FilesystemCommand(
+        type, path, Collections.unmodifiableList(paths), -1, -1, "", "", "", 
"");
   }
 
   public static FilesystemCommand head(String path, int limit) {
     return new FilesystemCommand(
-        Type.HEAD, path, Collections.singletonList(path), -1, limit, "", "");
+        Type.HEAD, path, Collections.singletonList(path), -1, limit, "", "", 
"", "");
+  }
+
+  public static FilesystemCommand tail(String path, int limit) {
+    return new FilesystemCommand(
+        Type.TAIL, path, Collections.singletonList(path), -1, limit, "", "", 
"", "");
+  }
+
+  public static FilesystemCommand option(Type type, String option, String 
path) {
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, option, "", "", 
"");
+  }
+
+  public static FilesystemCommand pattern(Type type, String pattern, String 
path) {
+    return new FilesystemCommand(
+        type, path, Collections.singletonList(path), -1, -1, "", pattern, "", 
"");
   }
 
   public static FilesystemCommand tree(String path, int depth) {
     return new FilesystemCommand(
-        Type.TREE, path, Collections.singletonList(path), depth, -1, "", "");
+        Type.TREE, path, Collections.singletonList(path), depth, -1, "", "", 
"", "");
   }
 
   public static FilesystemCommand sql(String statement) {
-    return new FilesystemCommand(Type.SQL, "", Collections.emptyList(), -1, 
-1, statement, "");
+    return new FilesystemCommand(
+        Type.SQL, "", Collections.emptyList(), -1, -1, "", "", statement, "");
   }
 
   public static FilesystemCommand invalid(String errorMessage) {
     return new FilesystemCommand(
-        Type.INVALID, "", Collections.emptyList(), -1, -1, "", errorMessage);
+        Type.INVALID, "", Collections.emptyList(), -1, -1, "", "", "", 
errorMessage);
   }
 
   public Type getType() {
@@ -117,6 +152,14 @@ public class FilesystemCommand {
     return limit;
   }
 
+  public String getOption() {
+    return option;
+  }
+
+  public String getPattern() {
+    return pattern;
+  }
+
   public String getStatement() {
     return statement;
   }
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 7bdab81d178..61000c17ee3 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
@@ -71,6 +71,39 @@ public class FilesystemCommandParser {
     if ("head".equals(command)) {
       return parseHead(tokens);
     }
+    if ("tail".equals(command)) {
+      return parseTail(tokens);
+    }
+    if ("wc".equals(command)) {
+      return parseWc(tokens);
+    }
+    if ("grep".equals(command)) {
+      return parseGrep(tokens);
+    }
+    if ("find".equals(command)) {
+      return parseFind(tokens);
+    }
+    if ("less".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.LESS, 
pathArgument(tokens));
+    }
+    if ("more".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.MORE, 
pathArgument(tokens));
+    }
+    if ("file".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.FILE, 
pathArgument(tokens));
+    }
+    if ("du".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.DU, 
pathArgument(tokens));
+    }
+    if ("mkdir".equals(command)) {
+      return FilesystemCommand.path(FilesystemCommand.Type.MKDIR, 
pathArgument(tokens));
+    }
+    if ("rm".equals(command)) {
+      return parseRm(tokens);
+    }
+    if ("mv".equals(command)) {
+      return parseMv(tokens);
+    }
     if ("paste".equals(command)) {
       return parsePaste(tokens);
     }
@@ -99,6 +132,26 @@ public class FilesystemCommandParser {
     return FilesystemCommand.paths(FilesystemCommand.Type.PASTE, paths);
   }
 
+  private static FilesystemCommand parseRm(String[] tokens) {
+    if (tokens.length < 2) {
+      return FilesystemCommand.invalid("Missing rm path");
+    }
+    if (tokens[1].startsWith("-")) {
+      return FilesystemCommand.invalid("Unsupported rm option: " + tokens[1]);
+    }
+    return FilesystemCommand.path(FilesystemCommand.Type.RM, tokens[1]);
+  }
+
+  private static FilesystemCommand parseMv(String[] tokens) {
+    if (tokens.length < 3) {
+      return FilesystemCommand.invalid("Usage: mv <source> <target>");
+    }
+    List<String> paths = new ArrayList<>();
+    paths.add(tokens[1]);
+    paths.add(tokens[2]);
+    return FilesystemCommand.paths(FilesystemCommand.Type.MV, paths);
+  }
+
   private static FilesystemCommand parseLs(String[] tokens) {
     FilesystemCommand.Type type = FilesystemCommand.Type.LS;
     String path = DEFAULT_PATH;
@@ -164,6 +217,80 @@ public class FilesystemCommandParser {
     return FilesystemCommand.head(path, limit);
   }
 
+  private static FilesystemCommand parseTail(String[] tokens) {
+    String path = DEFAULT_PATH;
+    int limit = DEFAULT_HEAD_LIMIT;
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if ("-n".equals(token)) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing tail line count");
+        }
+        try {
+          limit = Integer.parseInt(tokens[++i]);
+        } catch (NumberFormatException e) {
+          return FilesystemCommand.invalid("Invalid tail line count: " + 
tokens[i]);
+        }
+      } else if (token.startsWith("-") && token.length() > 1) {
+        try {
+          limit = Integer.parseInt(token.substring(1));
+        } catch (NumberFormatException e) {
+          return FilesystemCommand.invalid("Unsupported tail option: " + 
token);
+        }
+      } else {
+        path = token;
+      }
+    }
+
+    if (limit < 0) {
+      return FilesystemCommand.invalid("Invalid tail line count: " + limit);
+    }
+    return FilesystemCommand.tail(path, limit);
+  }
+
+  private static FilesystemCommand parseWc(String[] tokens) {
+    String option = "-l";
+    String path = DEFAULT_PATH;
+
+    for (int i = 1; i < tokens.length; i++) {
+      String token = tokens[i];
+      if (token.startsWith("-")) {
+        if (!"-l".equals(token)) {
+          return FilesystemCommand.invalid("Unsupported wc option: " + token);
+        }
+        option = token;
+      } else {
+        path = token;
+      }
+    }
+    return FilesystemCommand.option(FilesystemCommand.Type.WC, option, path);
+  }
+
+  private static FilesystemCommand parseGrep(String[] tokens) {
+    if (tokens.length < 3) {
+      return FilesystemCommand.invalid("Usage: grep <pattern> <path>");
+    }
+    return FilesystemCommand.pattern(FilesystemCommand.Type.GREP, tokens[1], 
tokens[2]);
+  }
+
+  private static FilesystemCommand parseFind(String[] tokens) {
+    String path = tokens.length > 1 ? tokens[1] : DEFAULT_PATH;
+    String pattern = "";
+
+    for (int i = 2; i < tokens.length; i++) {
+      if ("-name".equals(tokens[i])) {
+        if (i + 1 >= tokens.length) {
+          return FilesystemCommand.invalid("Missing find name pattern");
+        }
+        pattern = tokens[++i];
+      } else {
+        return FilesystemCommand.invalid("Unsupported find option: " + 
tokens[i]);
+      }
+    }
+    return FilesystemCommand.pattern(FilesystemCommand.Type.FIND, pattern, 
path);
+  }
+
   private static FilesystemCommand parseTree(String[] tokens) {
     String path = DEFAULT_PATH;
     int depth = DEFAULT_TREE_DEPTH;
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
similarity index 74%
copy from 
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
copy to 
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
index c372c2e3509..325025178ed 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemMutationProvider.java
@@ -17,12 +17,17 @@
  * under the License.
  */
 
-package org.apache.iotdb.cli.fs.sql;
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
 
 import java.sql.SQLException;
-import java.util.List;
 
-public interface SqlExecutor {
+public interface FilesystemMutationProvider {
+
+  void mkdir(FsPath path) throws SQLException;
+
+  void remove(FsPath path) throws SQLException;
 
-  List<SqlRow> query(String sql) throws SQLException;
+  void move(FsPath source, FsPath target) throws SQLException;
 }
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
index f96f617f363..de92c9bf983 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
@@ -34,6 +34,14 @@ public interface FilesystemSchemaProvider {
 
   List<SqlRow> read(FsPath path, int limit) throws SQLException;
 
+  default List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    throw new SQLException("Path does not support tail: " + path);
+  }
+
+  default long count(FsPath path) throws SQLException {
+    throw new SQLException("Path does not support count: " + path);
+  }
+
   default List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException 
{
     if (paths.size() == 1) {
       return read(paths.get(0), limit);
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
new file mode 100644
index 00000000000..153bd53d9ae
--- /dev/null
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public class TableFilesystemMutationProvider implements 
FilesystemMutationProvider {
+
+  private static final String INVALID_WRITE_OPERATION =
+      "Invalid filesystem write operation for this path";
+
+  private final SqlExecutor executor;
+
+  public TableFilesystemMutationProvider(SqlExecutor executor) {
+    this.executor = executor;
+  }
+
+  @Override
+  public void mkdir(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 1) {
+      throw invalidOperation();
+    }
+    executor.execute("CREATE DATABASE " + path.getFileName());
+  }
+
+  @Override
+  public void remove(FsPath path) throws SQLException {
+    if (path.getSegments().size() != 2) {
+      throw invalidOperation();
+    }
+    executor.execute("DROP TABLE " + toTablePath(path));
+  }
+
+  @Override
+  public void move(FsPath source, FsPath target) throws SQLException {
+    if (source.getSegments().size() != 2 || target.getSegments().size() != 2) {
+      throw invalidOperation();
+    }
+    if (!parent(source).equals(parent(target))) {
+      throw invalidOperation();
+    }
+    executor.execute("ALTER TABLE " + toTablePath(source) + " RENAME TO " + 
target.getFileName());
+  }
+
+  private static SQLException invalidOperation() {
+    return new SQLException(INVALID_WRITE_OPERATION);
+  }
+
+  private static String toTablePath(FsPath path) {
+    List<String> segments = path.getSegments();
+    return segments.get(0) + "." + segments.get(1);
+  }
+
+  private static FsPath parent(FsPath path) {
+    List<String> segments = path.getSegments();
+    StringBuilder builder = new StringBuilder("/");
+    for (int i = 0; i < segments.size() - 1; i++) {
+      if (i > 0) {
+        builder.append('/');
+      }
+      builder.append(segments.get(i));
+    }
+    return FsPath.absolute(builder.toString());
+  }
+}
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
index 031ca186ab8..d735fa99a8b 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProvider.java
@@ -27,6 +27,7 @@ import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public class TableFilesystemSchemaProvider implements FilesystemSchemaProvider 
{
@@ -87,6 +88,46 @@ public class TableFilesystemSchemaProvider implements 
FilesystemSchemaProvider {
     throw new SQLException("Path is not readable: " + path);
   }
 
+  @Override
+  public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    int depth = path.getSegments().size();
+    List<SqlRow> rows;
+    if (depth == 2) {
+      rows =
+          executor.query(
+              "SELECT * FROM " + toTablePath(path) + " ORDER BY time DESC 
LIMIT " + limit);
+    } else if (depth == 3) {
+      String tablePath = toTablePath(parent(path));
+      rows =
+          executor.query(
+              "SELECT "
+                  + path.getFileName()
+                  + " FROM "
+                  + tablePath
+                  + " ORDER BY time DESC LIMIT "
+                  + limit);
+    } else {
+      throw new SQLException("Path is not readable: " + path);
+    }
+    Collections.reverse(rows);
+    return rows;
+  }
+
+  @Override
+  public long count(FsPath path) throws SQLException {
+    int depth = path.getSegments().size();
+    List<SqlRow> rows;
+    if (depth == 2) {
+      rows = executor.query("SELECT COUNT(*) FROM " + toTablePath(path));
+    } else if (depth == 3) {
+      String tablePath = toTablePath(parent(path));
+      rows = executor.query("SELECT COUNT(" + path.getFileName() + ") FROM " + 
tablePath);
+    } else {
+      throw new SQLException("Path is not countable: " + path);
+    }
+    return countValue(rows);
+  }
+
   @Override
   public List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException {
     if (paths.size() == 1) {
@@ -182,6 +223,13 @@ public class TableFilesystemSchemaProvider implements 
FilesystemSchemaProvider {
     return builder.toString();
   }
 
+  private static long countValue(List<SqlRow> rows) {
+    if (rows.isEmpty() || rows.get(0).asMap().isEmpty()) {
+      return 0;
+    }
+    return Long.parseLong(rows.get(0).asMap().values().iterator().next());
+  }
+
   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/TreeFilesystemSchemaProvider.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
index 0766bb271d4..292cb5da540 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/TreeFilesystemSchemaProvider.java
@@ -27,6 +27,7 @@ import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -78,6 +79,34 @@ public class TreeFilesystemSchemaProvider implements 
FilesystemSchemaProvider {
         "SELECT " + measurement + " FROM " + toTreePath(devicePath) + " LIMIT 
" + limit);
   }
 
+  @Override
+  public List<SqlRow> tail(FsPath path, int limit) throws SQLException {
+    String measurement = path.getFileName();
+    FsPath devicePath = parent(path);
+    List<SqlRow> rows =
+        executor.query(
+            "SELECT "
+                + measurement
+                + " FROM "
+                + toTreePath(devicePath)
+                + " ORDER BY time DESC LIMIT "
+                + limit);
+    Collections.reverse(rows);
+    return rows;
+  }
+
+  @Override
+  public long count(FsPath path) throws SQLException {
+    String measurement = path.getFileName();
+    FsPath devicePath = parent(path);
+    List<SqlRow> rows =
+        executor.query("SELECT COUNT(" + measurement + ") FROM " + 
toTreePath(devicePath));
+    if (rows.isEmpty() || rows.get(0).asMap().isEmpty()) {
+      return 0;
+    }
+    return Long.parseLong(rows.get(0).asMap().values().iterator().next());
+  }
+
   private List<FsNode> listTreeRoots() throws SQLException {
     Set<String> roots = new LinkedHashSet<>();
     for (SqlRow row : executor.query("SHOW DATABASES")) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
similarity index 62%
copy from 
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
copy to 
iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
index f96f617f363..9f4a08ceb01 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/FilesystemSchemaProvider.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/provider/UnsupportedFilesystemMutationProvider.java
@@ -19,25 +19,30 @@
 
 package org.apache.iotdb.cli.fs.provider;
 
-import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.path.FsPath;
-import org.apache.iotdb.cli.fs.sql.SqlRow;
 
 import java.sql.SQLException;
-import java.util.List;
 
-public interface FilesystemSchemaProvider {
+public class UnsupportedFilesystemMutationProvider implements 
FilesystemMutationProvider {
 
-  List<FsNode> list(FsPath path) throws SQLException;
+  private static final String UNSUPPORTED = "Filesystem write operation is not 
supported";
 
-  FsNode describe(FsPath path) throws SQLException;
+  @Override
+  public void mkdir(FsPath path) throws SQLException {
+    throw unsupported();
+  }
+
+  @Override
+  public void remove(FsPath path) throws SQLException {
+    throw unsupported();
+  }
 
-  List<SqlRow> read(FsPath path, int limit) throws SQLException;
+  @Override
+  public void move(FsPath source, FsPath target) throws SQLException {
+    throw unsupported();
+  }
 
-  default List<SqlRow> read(List<FsPath> paths, int limit) throws SQLException 
{
-    if (paths.size() == 1) {
-      return read(paths.get(0), limit);
-    }
-    throw new SQLException("Multiple paths are not readable by this provider");
+  private static SQLException unsupported() {
+    return new SQLException(UNSUPPORTED);
   }
 }
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
index 31a5925a64b..36a7e9016ee 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutor.java
@@ -54,4 +54,11 @@ public class JdbcSqlExecutor implements SqlExecutor {
       return rows;
     }
   }
+
+  @Override
+  public void execute(String sql) throws SQLException {
+    try (Statement statement = connection.createStatement()) {
+      statement.execute(sql);
+    }
+  }
 }
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
index c372c2e3509..6a6cb7dd39a 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/fs/sql/SqlExecutor.java
@@ -25,4 +25,6 @@ import java.util.List;
 public interface SqlExecutor {
 
   List<SqlRow> query(String sql) throws SQLException;
+
+  void execute(String sql) throws SQLException;
 }
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
index 0a98529f6af..aa68d220e77 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/cli/utils/JlineUtils.java
@@ -152,7 +152,8 @@ public class JlineUtils {
   static Completer createCompleter(String accessMode) {
     if ("filesystem".equalsIgnoreCase(accessMode)) {
       return new StringsCompleter(
-          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "paste", "tree", 
"help", "exit", "quit");
+          "pwd", "ls", "ll", "cd", "stat", "cat", "head", "tail", "wc", 
"grep", "find", "less",
+          "more", "file", "du", "mkdir", "rm", "mv", "paste", "tree", "help", 
"exit", "quit");
     }
     return new StringsCompleter(SQL_KEYWORDS);
   }
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
index aa6ed5332fd..f2de555a8aa 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/AbstractCliTest.java
@@ -136,6 +136,35 @@ public class AbstractCliTest {
     assertEquals(AbstractCli.ACCESS_MODE_FILESYSTEM, 
AbstractCli.getAccessMode(ctx, commandLine));
   }
 
+  @Test
+  public void testFsWriteModeEnabled() throws ParseException, 
ArgsErrorException {
+    CliContext ctx = new CliContext(System.in, System.out, System.err, 
ExitType.EXCEPTION);
+    Options options = AbstractCli.createOptions();
+    CommandLineParser parser = new DefaultParser();
+    CommandLine commandLine =
+        parser.parse(options, new String[] {"-u", "root", "--fs_write_mode", 
"enabled"});
+
+    assertEquals(AbstractCli.FS_WRITE_MODE_ENABLED, 
AbstractCli.getFsWriteMode(ctx, commandLine));
+  }
+
+  @Test
+  public void testFsWriteModeRejectsInvalidValue() throws ParseException {
+    CliContext ctx = new CliContext(System.in, System.out, System.err, 
ExitType.EXCEPTION);
+    Options options = AbstractCli.createOptions();
+    CommandLineParser parser = new DefaultParser();
+    CommandLine commandLine =
+        parser.parse(options, new String[] {"-u", "root", "--fs_write_mode", 
"bad"});
+
+    try {
+      AbstractCli.getFsWriteMode(ctx, commandLine);
+      fail();
+    } catch (ArgsErrorException e) {
+      assertEquals(
+          "IoTDB: Unsupported fs write mode 'bad'. Supported values are 
disabled and enabled.",
+          e.getMessage());
+    }
+  }
+
   @Test
   public void testAccessModeRejectsInvalidValue() throws ParseException {
     CliContext ctx = new CliContext(System.in, System.out, System.err, 
ExitType.EXCEPTION);
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
index e50a51efd68..d3d44a00972 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/CliFilesystemModeTest.java
@@ -71,6 +71,32 @@ public class CliFilesystemModeTest {
     verify(statement).executeQuery("SHOW TABLES FROM db1");
   }
 
+  @Test
+  public void createFilesystemShellUsesWriteModeForTableDialect() throws 
Exception {
+    when(connection.getSqlDialect()).thenReturn("table");
+    when(connection.createStatement()).thenReturn(statement);
+    AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_ENABLED);
+    try {
+      FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
+      shell.execute("mkdir /db1");
+    } finally {
+      AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_DISABLED);
+    }
+
+    verify(statement).execute("CREATE DATABASE db1");
+  }
+
+  @Test
+  public void createFilesystemShellRejectsWriteWhenWriteModeDisabled() throws 
Exception {
+    when(connection.getSqlDialect()).thenReturn("table");
+    AbstractCli.setFsWriteMode(AbstractCli.FS_WRITE_MODE_DISABLED);
+
+    FilesystemShell shell = Cli.createFilesystemShell(ctx, connection);
+    shell.execute("mkdir /db1");
+
+    org.junit.Assert.assertTrue(out.toString().contains("Read-only file 
system"));
+  }
+
   @Test
   public void createFilesystemShellUsesTreeProviderForTreeDialect() throws 
Exception {
     when(connection.getSqlDialect()).thenReturn("tree");
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 e4ea9161a85..7461aad1367 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
@@ -22,6 +22,7 @@ package org.apache.iotdb.cli.fs;
 import org.apache.iotdb.cli.fs.node.FsNode;
 import org.apache.iotdb.cli.fs.node.FsNodeType;
 import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.provider.FilesystemMutationProvider;
 import org.apache.iotdb.cli.fs.provider.FilesystemSchemaProvider;
 import org.apache.iotdb.cli.fs.sql.SqlRow;
 import org.apache.iotdb.cli.type.ExitType;
@@ -47,12 +48,15 @@ import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 public class FilesystemShellTest {
 
   @Mock private FilesystemSchemaProvider provider;
+  @Mock private FilesystemMutationProvider mutationProvider;
 
   private ByteArrayOutputStream out;
   private FilesystemShell shell;
@@ -81,11 +85,13 @@ public class FilesystemShellTest {
   public void executeLsPrintsChildNodes() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
         .thenReturn(
-            Arrays.asList(new FsNode("root", FsPath.absolute("/root"), 
FsNodeType.TREE_ROOT)));
+            Arrays.asList(
+                new FsNode("root", FsPath.absolute("/root"), 
FsNodeType.TREE_ROOT),
+                new FsNode("test", FsPath.absolute("/test"), 
FsNodeType.TREE_ROOT)));
 
     assertTrue(shell.execute("ls /"));
 
-    assertTrue(out.toString().contains("root"));
+    assertTrue(out.toString().contains("root,test"));
     assertFalse(out.toString().contains("TREE_ROOT"));
     verify(provider).list(FsPath.absolute("/"));
   }
@@ -136,6 +142,27 @@ public class FilesystemShellTest {
     assertFalse(shell.execute("exit"));
   }
 
+  @Test
+  public void executeWriteCommandRejectsReadOnlyMode() throws SQLException {
+    assertTrue(shell.execute("mkdir /db1"));
+
+    assertTrue(out.toString().contains("mkdir: /db1: Read-only file system"));
+    verifyZeroInteractions(mutationProvider);
+  }
+
+  @Test
+  public void executeWriteCommandsWhenEnabled() throws SQLException {
+    shell = new FilesystemShell(shellContext(), provider, mutationProvider, 
true);
+
+    assertTrue(shell.execute("mkdir /db1"));
+    assertTrue(shell.execute("rm /db1/table1"));
+    assertTrue(shell.execute("mv /db1/table1 /db1/table2"));
+
+    verify(mutationProvider).mkdir(FsPath.absolute("/db1"));
+    verify(mutationProvider).remove(FsPath.absolute("/db1/table1"));
+    verify(mutationProvider).move(FsPath.absolute("/db1/table1"), 
FsPath.absolute("/db1/table2"));
+  }
+
   @Test
   public void executeTreePrintsChildrenUntilDepth() throws SQLException {
     when(provider.list(FsPath.absolute("/")))
@@ -193,6 +220,99 @@ public class FilesystemShellTest {
     verify(provider).read(FsPath.absolute("/db1/table1"), 5);
   }
 
+  @Test
+  public void executeTailReadsPathWithLimit() throws SQLException {
+    when(provider.tail(FsPath.absolute("/db1/table1"), 3))
+        .thenReturn(Arrays.asList(SqlRow.of("Time", "2", "tag1", "b", "s1", 
"43")));
+
+    assertTrue(shell.execute("tail -n 3 /db1/table1"));
+
+    assertTrue(out.toString().contains("2\tb\t43"));
+    verify(provider).tail(FsPath.absolute("/db1/table1"), 3);
+  }
+
+  @Test
+  public void executeWcLineCountPrintsCountAndPath() throws SQLException {
+    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+
+    assertTrue(shell.execute("wc -l /db1/table1"));
+
+    assertTrue(out.toString().contains("2 /db1/table1"));
+    verify(provider).count(FsPath.absolute("/db1/table1"));
+  }
+
+  @Test
+  public void executeGrepPrintsOnlyMatchingRows() throws SQLException {
+    when(provider.read(FsPath.absolute("/db1/table1"), 20))
+        .thenReturn(
+            Arrays.asList(
+                SqlRow.of("Time", "1", "tag1", "spricoder", "s1", "42"),
+                SqlRow.of("Time", "2", "tag1", "other", "s1", "43")));
+
+    assertTrue(shell.execute("grep spricoder /db1/table1"));
+
+    assertTrue(out.toString().contains("1\tspricoder\t42"));
+    assertFalse(out.toString().contains("2\tother\t43"));
+    verify(provider).read(FsPath.absolute("/db1/table1"), 20);
+  }
+
+  @Test
+  public void executeFindPrintsMatchingDescendants() throws SQLException {
+    when(provider.describe(FsPath.absolute("/")))
+        .thenReturn(new FsNode("/", FsPath.absolute("/"), 
FsNodeType.VIRTUAL_ROOT));
+    when(provider.describe(FsPath.absolute("/db1")))
+        .thenReturn(new FsNode("db1", FsPath.absolute("/db1"), 
FsNodeType.TABLE_DATABASE));
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), 
FsNodeType.TABLE_TABLE));
+    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", FsPath.absolute("/db1/table1"), 
FsNodeType.TABLE_TABLE)));
+    when(provider.list(FsPath.absolute("/db1/table1"))).thenReturn(new 
ArrayList<>());
+
+    assertTrue(shell.execute("find / -name table1"));
+
+    assertTrue(out.toString().contains("/db1/table1"));
+    verify(provider).list(FsPath.absolute("/"));
+    verify(provider).list(FsPath.absolute("/db1"));
+  }
+
+  @Test
+  public void executeLessAndMoreReadPath() throws SQLException {
+    when(provider.read(FsPath.absolute("/db1/table1"), 20))
+        .thenReturn(Arrays.asList(SqlRow.of("Time", "1", "tag1", "a", "s1", 
"42")));
+
+    assertTrue(shell.execute("less /db1/table1"));
+    assertTrue(shell.execute("more /db1/table1"));
+
+    assertTrue(out.toString().contains("1\ta\t42"));
+    verify(provider, times(2)).read(FsPath.absolute("/db1/table1"), 20);
+  }
+
+  @Test
+  public void executeFilePrintsNodeType() throws SQLException {
+    when(provider.describe(FsPath.absolute("/db1/table1")))
+        .thenReturn(new FsNode("table1", FsPath.absolute("/db1/table1"), 
FsNodeType.TABLE_TABLE));
+
+    assertTrue(shell.execute("file /db1/table1"));
+
+    assertTrue(out.toString().contains("/db1/table1: TABLE_TABLE"));
+    verify(provider).describe(FsPath.absolute("/db1/table1"));
+  }
+
+  @Test
+  public void executeDuPrintsLogicalSizeAndPath() throws SQLException {
+    when(provider.count(FsPath.absolute("/db1/table1"))).thenReturn(2L);
+
+    assertTrue(shell.execute("du /db1/table1"));
+
+    assertTrue(out.toString().contains("2\t/db1/table1"));
+    verify(provider).count(FsPath.absolute("/db1/table1"));
+  }
+
   @Test
   public void executePasteReadsMultiplePaths() throws SQLException {
     when(provider.read(
@@ -231,4 +351,12 @@ public class FilesystemShellTest {
     completer.complete(null, parsedLine, candidates);
     return 
candidates.stream().map(Candidate::value).collect(Collectors.toList());
   }
+
+  private CliContext shellContext() {
+    return new CliContext(
+        new ByteArrayInputStream(new byte[0]),
+        new PrintStream(out),
+        System.err,
+        ExitType.EXCEPTION);
+  }
 }
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 2816912ab0b..7224876d239 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
@@ -87,6 +87,54 @@ public class FilesystemCommandParserTest {
     assertEquals(5, command.getLimit());
   }
 
+  @Test
+  public void parseTailLimitAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("tail -n 3 
/db1/table1");
+
+    assertEquals(FilesystemCommand.Type.TAIL, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals(3, command.getLimit());
+  }
+
+  @Test
+  public void parseWcLineCountAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("wc -l 
/db1/table1");
+
+    assertEquals(FilesystemCommand.Type.WC, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals("-l", command.getOption());
+  }
+
+  @Test
+  public void parseGrepPatternAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("grep spricoder 
/db1/table1");
+
+    assertEquals(FilesystemCommand.Type.GREP, command.getType());
+    assertEquals("/db1/table1", command.getPath());
+    assertEquals("spricoder", command.getPattern());
+  }
+
+  @Test
+  public void parseFindNamePatternAndPath() {
+    FilesystemCommand command = FilesystemCommandParser.parse("find /db1 -name 
table1");
+
+    assertEquals(FilesystemCommand.Type.FIND, command.getType());
+    assertEquals("/db1", command.getPath());
+    assertEquals("table1", command.getPattern());
+  }
+
+  @Test
+  public void parseLessMoreFileAndDu() {
+    assertEquals(
+        FilesystemCommand.Type.LESS, FilesystemCommandParser.parse("less 
/db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.MORE, FilesystemCommandParser.parse("more 
/db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.FILE, FilesystemCommandParser.parse("file 
/db1/table1").getType());
+    assertEquals(
+        FilesystemCommand.Type.DU, FilesystemCommandParser.parse("du 
/db1/table1").getType());
+  }
+
   @Test
   public void parsePastePaths() {
     FilesystemCommand command =
@@ -98,6 +146,23 @@ public class FilesystemCommandParserTest {
     assertEquals("/db1/table1/s1", command.getPaths().get(1));
   }
 
+  @Test
+  public void parseWriteCommands() {
+    FilesystemCommand mkdir = FilesystemCommandParser.parse("mkdir /db1");
+    assertEquals(FilesystemCommand.Type.MKDIR, mkdir.getType());
+    assertEquals("/db1", mkdir.getPath());
+
+    FilesystemCommand rm = FilesystemCommandParser.parse("rm /db1/table1");
+    assertEquals(FilesystemCommand.Type.RM, rm.getType());
+    assertEquals("/db1/table1", rm.getPath());
+
+    FilesystemCommand mv = FilesystemCommandParser.parse("mv /db1/table1 
/db1/table2");
+    assertEquals(FilesystemCommand.Type.MV, mv.getType());
+    assertEquals(2, mv.getPaths().size());
+    assertEquals("/db1/table1", mv.getPaths().get(0));
+    assertEquals("/db1/table2", mv.getPaths().get(1));
+  }
+
   @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
new file mode 100644
index 00000000000..955046688c4
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemMutationProviderTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.cli.fs.provider;
+
+import org.apache.iotdb.cli.fs.path.FsPath;
+import org.apache.iotdb.cli.fs.sql.SqlExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.sql.SQLException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.verify;
+
+public class TableFilesystemMutationProviderTest {
+
+  @Mock private SqlExecutor executor;
+
+  private TableFilesystemMutationProvider provider;
+
+  @Before
+  public void setUp() {
+    MockitoAnnotations.initMocks(this);
+    provider = new TableFilesystemMutationProvider(executor);
+  }
+
+  @Test
+  public void mkdirDatabaseCreatesDatabase() throws SQLException {
+    provider.mkdir(FsPath.absolute("/db1"));
+
+    verify(executor).execute("CREATE DATABASE db1");
+  }
+
+  @Test
+  public void mkdirRejectsRootAndTableLevel() throws SQLException {
+    assertInvalidOperation(() -> provider.mkdir(FsPath.absolute("/")));
+    assertInvalidOperation(() -> 
provider.mkdir(FsPath.absolute("/db1/table1")));
+  }
+
+  @Test
+  public void removeTableDropsTable() throws SQLException {
+    provider.remove(FsPath.absolute("/db1/table1"));
+
+    verify(executor).execute("DROP TABLE db1.table1");
+  }
+
+  @Test
+  public void removeRejectsRootDatabaseAndColumnLevel() throws SQLException {
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/")));
+    assertInvalidOperation(() -> provider.remove(FsPath.absolute("/db1")));
+    assertInvalidOperation(() -> 
provider.remove(FsPath.absolute("/db1/table1/s1")));
+  }
+
+  @Test
+  public void moveTableRenamesTableInSameDatabase() throws SQLException {
+    provider.move(FsPath.absolute("/db1/table1"), 
FsPath.absolute("/db1/table2"));
+
+    verify(executor).execute("ALTER TABLE db1.table1 RENAME TO table2");
+  }
+
+  @Test
+  public void moveRejectsUnsafeLevelsAndCrossDatabaseRename() throws 
SQLException {
+    assertInvalidOperation(() -> provider.move(FsPath.absolute("/db1"), 
FsPath.absolute("/db2")));
+    assertInvalidOperation(
+        () -> provider.move(FsPath.absolute("/db1/table1/s1"), 
FsPath.absolute("/db1/table1/s2")));
+    assertInvalidOperation(
+        () -> provider.move(FsPath.absolute("/db1/table1"), 
FsPath.absolute("/db2/table1")));
+  }
+
+  private static void assertInvalidOperation(SqlOperation operation) throws 
SQLException {
+    try {
+      operation.run();
+      fail();
+    } catch (SQLException e) {
+      assertEquals("Invalid filesystem write operation for this path", 
e.getMessage());
+    }
+  }
+
+  private interface SqlOperation {
+    void run() throws SQLException;
+  }
+}
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
index d846c3b7be8..9d7c1bb6715 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/provider/TableFilesystemSchemaProviderTest.java
@@ -171,6 +171,32 @@ public class TableFilesystemSchemaProviderTest {
     verify(executor).query("SELECT * FROM db1.table1 LIMIT 5");
   }
 
+  @Test
+  public void tailTableSelectsNewestRowsAndReturnsOriginalOrder() throws 
SQLException {
+    when(executor.query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 2"))
+        .thenReturn(
+            SqlRow.list(
+                SqlRow.of("Time", "2", "tag1", "b", "s1", "43"),
+                SqlRow.of("Time", "1", "tag1", "a", "s1", "42")));
+
+    List<SqlRow> rows = provider.tail(FsPath.absolute("/db1/table1"), 2);
+
+    assertEquals("1", rows.get(0).get("Time"));
+    assertEquals("2", rows.get(1).get("Time"));
+    verify(executor).query("SELECT * FROM db1.table1 ORDER BY time DESC LIMIT 
2");
+  }
+
+  @Test
+  public void countTableSelectsRowCount() throws SQLException {
+    when(executor.query("SELECT COUNT(*) FROM db1.table1"))
+        .thenReturn(SqlRow.list(SqlRow.of("count", "2")));
+
+    long count = provider.count(FsPath.absolute("/db1/table1"));
+
+    assertEquals(2L, count);
+    verify(executor).query("SELECT COUNT(*) FROM db1.table1");
+  }
+
   @Test
   public void readColumnsSelectsMultipleColumnsFromSameTable() throws 
SQLException {
     when(executor.query("SELECT tag1, s1 FROM db1.table1 LIMIT 5"))
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
index 83f073e30ea..ae405f859a0 100644
--- 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/cli/fs/sql/JdbcSqlExecutorTest.java
@@ -68,4 +68,14 @@ public class JdbcSqlExecutorTest {
     verify(resultSet).close();
     verify(statement).close();
   }
+
+  @Test
+  public void executeRunsStatement() throws Exception {
+    when(connection.createStatement()).thenReturn(statement);
+
+    new JdbcSqlExecutor(connection).execute("CREATE DATABASE db1");
+
+    verify(statement).execute("CREATE DATABASE db1");
+    verify(statement).close();
+  }
 }

Reply via email to