add project

This commit is contained in:
GuilhermeStrice
2025-07-09 19:31:34 +01:00
parent 8d2e88edf4
commit f37078157d
44 changed files with 7680 additions and 0 deletions

11
CommandTests/01_setup.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
echo "=== Redis Manager Test Setup ==="
echo "Verifying existing Redis instances..."
# List instances to verify
echo "Listing configured instances:"
dotnet run -- list-instances
echo "Setup complete!"
echo ""

View File

@ -0,0 +1,46 @@
#!/bin/bash
echo "=== Basic String Commands Test ==="
echo "1. Testing SET and GET..."
dotnet run -- set "test:string" "Hello World" -i default
dotnet run -- get "test:string" -i default
echo ""
echo "2. Testing APPEND..."
dotnet run -- append "test:string" " - Appended!" -i default
dotnet run -- get "test:string" -i default
echo ""
echo "3. Testing INCR/DECR..."
dotnet run -- set "test:counter" "10" -i default
dotnet run -- incr "test:counter" -i default
dotnet run -- incrby "test:counter" "5" -i default
dotnet run -- decr "test:counter" -i default
dotnet run -- get "test:counter" -i default
echo ""
echo "4. Testing INCRBYFLOAT..."
dotnet run -- set "test:float" "10.5" -i default
dotnet run -- incrbyfloat "test:float" "2.3" -i default
dotnet run -- get "test:float" -i default
echo ""
echo "5. Testing GETRANGE/SETRANGE..."
dotnet run -- set "test:range" "Hello World" -i default
dotnet run -- getrange "test:range" "0" "4" -i default
dotnet run -- setrange "test:range" "6" "Redis" -i default
dotnet run -- get "test:range" -i default
echo ""
echo "6. Testing STRLEN..."
dotnet run -- strlen "test:range" -i default
echo ""
echo "7. Testing MGET/MSET..."
dotnet run -- mset --pairs key1=value1,key2=value2,key3=value3 -i default
dotnet run -- mget key1 key2 key3 -i default
echo ""
echo "Basic String Commands Test Complete!"
echo ""

62
CommandTests/03_hash_tests.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/bash
echo "=== Testing Hash Commands ==="
# Test HSET and HGET
echo "Testing HSET and HGET..."
dotnet run -- hset user:1000 name "John Doe" -i localhost
dotnet run -- hset user:1000 email "john@example.com" -i localhost
dotnet run -- hset user:1000 age "30" -i localhost
dotnet run -- hget user:1000 name -i localhost
dotnet run -- hget user:1000 email -i localhost
dotnet run -- hget user:1000 age -i localhost
# Test HGETALL
echo "Testing HGETALL..."
dotnet run -- hgetall user:1000 -i localhost
# Test HKEYS and HVALS
echo "Testing HKEYS and HVALS..."
dotnet run -- hkeys user:1000 -i localhost
dotnet run -- hvals user:1000 -i localhost
# Test HEXISTS
echo "Testing HEXISTS..."
dotnet run -- hexists user:1000 name -i localhost
dotnet run -- hexists user:1000 phone -i localhost
# Test HLEN
echo "Testing HLEN..."
dotnet run -- hlen user:1000 -i localhost
# Test HINCRBY and HINCRBYFLOAT
echo "Testing HINCRBY and HINCRBYFLOAT..."
dotnet run -- hset user:1000 score "100" -i localhost
dotnet run -- hincrby user:1000 score "10" -i localhost
dotnet run -- hincrbyfloat user:1000 score "5.5" -i localhost
# Test HMSET and HMGET
echo "Testing HMSET and HMGET..."
dotnet run -- hmset user:1001 name "Jane Smith" email "jane@example.com" age "25" -i localhost
dotnet run -- hmget user:1001 name email age -i localhost
# Test HSETNX
echo "Testing HSETNX..."
dotnet run -- hsetnx user:1000 name "New Name" -i localhost
dotnet run -- hsetnx user:1000 phone "123-456-7890" -i localhost
# Test HSTRLEN
echo "Testing HSTRLEN..."
dotnet run -- hstrlen user:1000 name -i localhost
# Test HDEL
echo "Testing HDEL..."
dotnet run -- hdel user:1000 age -i localhost
dotnet run -- hgetall user:1000 -i localhost
# Test HSCAN
echo "Testing HSCAN..."
dotnet run -- hscan user:1000 0 -i localhost
echo "Hash tests completed!"

35
CommandTests/04_geo_tests.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
echo "=== Geo Commands Test ==="
echo "1. Testing GEOADD..."
dotnet run -- geoadd cities 2.3522 48.8566 London 2.2945 48.8584 Paris 13.4050 52.5200 Berlin 12.4964 41.9028 Rome -i localhost
echo ""
echo "2. Testing GEOHASH..."
dotnet run -- geohash cities London Paris -i localhost
echo ""
echo "3. Testing GEOPOS..."
dotnet run -- geopos cities London Berlin Rome -i localhost
echo ""
echo "4. Testing GEODIST..."
dotnet run -- geodist cities London Paris --unit km -i localhost
dotnet run -- geodist cities London Berlin --unit mi -i localhost
echo ""
echo "5. Testing GEORADIUS..."
dotnet run -- georadius cities 2.3522 48.8566 1000 --unit km -i localhost
echo ""
echo "6. Testing GEOHASH with multiple members..."
dotnet run -- geohash cities London Paris Berlin Rome -i localhost
echo ""
echo "7. Testing GEOPOS with multiple members..."
dotnet run -- geopos cities London Paris Berlin Rome -i localhost
echo ""
echo "Geo Commands Test Complete!"
echo ""

62
CommandTests/05_bit_tests.sh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/bash
echo "=== Bit Commands Test ==="
echo "1. Testing SETBIT and GETBIT..."
dotnet run -- setbit flags 0 1 -i localhost
dotnet run -- setbit flags 1 1 -i localhost
dotnet run -- setbit flags 2 0 -i localhost
dotnet run -- setbit flags 3 1 -i localhost
dotnet run -- getbit flags 0 -i localhost
dotnet run -- getbit flags 1 -i localhost
dotnet run -- getbit flags 2 -i localhost
dotnet run -- getbit flags 3 -i localhost
echo ""
echo "2. Testing BITCOUNT..."
dotnet run -- bitcount flags -i localhost
echo ""
echo "3. Testing BITPOS..."
dotnet run -- bitpos flags 1 -i localhost
dotnet run -- bitpos flags 0 -i localhost
echo ""
echo "4. Testing BITFIELD..."
dotnet run -- bitfield numbers SET u8 0 255 GET u8 0 -i localhost
dotnet run -- bitfield numbers SET i16 8 32767 GET i16 8 -i localhost
dotnet run -- bitfield numbers INCRBY u8 0 1 GET u8 0 -i localhost
echo ""
echo "5. Testing BITOP..."
dotnet run -- setbit flags1 0 1 -i localhost
dotnet run -- setbit flags1 1 0 -i localhost
dotnet run -- setbit flags2 0 0 -i localhost
dotnet run -- setbit flags2 1 1 -i localhost
dotnet run -- bitop AND result flags1 flags2 -i localhost
dotnet run -- getbit result 0 -i localhost
dotnet run -- getbit result 1 -i localhost
echo ""
echo "6. Testing BITOP OR..."
dotnet run -- bitop OR result_or flags1 flags2 -i localhost
dotnet run -- getbit result_or 0 -i localhost
dotnet run -- getbit result_or 1 -i localhost
echo ""
echo "7. Testing BITOP XOR..."
dotnet run -- bitop XOR result_xor flags1 flags2 -i localhost
dotnet run -- getbit result_xor 0 -i localhost
dotnet run -- getbit result_xor 1 -i localhost
echo ""
echo "8. Testing BITOP NOT..."
dotnet run -- bitop NOT result_not flags1 -i localhost
dotnet run -- getbit result_not 0 -i localhost
dotnet run -- getbit result_not 1 -i localhost
echo ""
echo "Bit Commands Test Complete!"
echo ""

25
CommandTests/06_module_tests.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
echo "=== Module Commands Test ==="
echo "1. Testing MODULE LIST..."
dotnet run -- module-list -i localhost
echo ""
echo "2. Testing MODULE LOAD (this may fail if no modules are available)..."
# Note: This will likely fail unless you have Redis modules available
dotnet run -- module-load "/path/to/module.so" "arg1" "arg2" -i localhost
echo ""
echo "3. Testing MODULE UNLOAD (this may fail if no modules are loaded)..."
# Note: This will likely fail unless you have modules loaded
dotnet run -- module-unload "testmodule" -i localhost
echo ""
echo "4. Testing MODULE LIST again..."
dotnet run -- module-list -i localhost
echo ""
echo "Module Commands Test Complete!"
echo "Note: Module commands may fail if no modules are available/loaded."
echo ""

70
CommandTests/07_list_tests.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/bash
echo "=== Testing List Commands ==="
# Test LPUSH and RPUSH
echo "Testing LPUSH and RPUSH..."
dotnet run -- lpush mylist "first" -i localhost
dotnet run -- lpush mylist "second" -i localhost
dotnet run -- rpush mylist "third" -i localhost
dotnet run -- rpush mylist "fourth" -i localhost
# Test LLEN
echo "Testing LLEN..."
dotnet run -- llen mylist -i localhost
# Test LRANGE
echo "Testing LRANGE..."
dotnet run -- lrange mylist 0 -1 -i localhost
dotnet run -- lrange mylist 0 1 -i localhost
dotnet run -- lrange mylist 1 2 -i localhost
# Test LINDEX
echo "Testing LINDEX..."
dotnet run -- lindex mylist 0 -i localhost
dotnet run -- lindex mylist 1 -i localhost
dotnet run -- lindex mylist -1 -i localhost
# Test LSET
echo "Testing LSET..."
dotnet run -- lset mylist 1 "updated_second" -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
# Test LINSERT
echo "Testing LINSERT..."
dotnet run -- linsert mylist before "updated_second" "before_second" -i localhost
dotnet run -- linsert mylist after "updated_second" "after_second" -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
# Test LREM
echo "Testing LREM..."
dotnet run -- lpush mylist "duplicate" -i localhost
dotnet run -- lpush mylist "duplicate" -i localhost
dotnet run -- lrem mylist 2 "duplicate" -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
# Test LTRIM
echo "Testing LTRIM..."
dotnet run -- ltrim mylist 1 3 -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
# Test LPOP and RPOP
echo "Testing LPOP and RPOP..."
dotnet run -- lpop mylist -i localhost
dotnet run -- rpop mylist -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
# Test BLPOP and BRPOP (with timeout)
echo "Testing BLPOP and BRPOP..."
dotnet run -- blpop mylist 1 -i localhost
dotnet run -- brpop mylist 1 -i localhost
# Test RPOPLPUSH
echo "Testing RPOPLPUSH..."
dotnet run -- rpush mylist "last_item" -i localhost
dotnet run -- rpush otherlist "existing_item" -i localhost
dotnet run -- rpoplpush mylist otherlist -i localhost
dotnet run -- lrange mylist 0 -1 -i localhost
dotnet run -- lrange otherlist 0 -1 -i localhost
echo "List tests completed!"

72
CommandTests/08_set_tests.sh Executable file
View File

@ -0,0 +1,72 @@
#!/bin/bash
echo "=== Testing Set Commands ==="
# Test SADD and SMEMBERS
echo "Testing SADD and SMEMBERS..."
dotnet run -- sadd set1 "apple" -i localhost
dotnet run -- sadd set1 "banana" -i localhost
dotnet run -- sadd set1 "cherry" -i localhost
dotnet run -- smembers set1 -i localhost
# Test SISMEMBER
echo "Testing SISMEMBER..."
dotnet run -- sismember set1 "apple" -i localhost
dotnet run -- sismember set1 "orange" -i localhost
# Test SCARD
echo "Testing SCARD..."
dotnet run -- scard set1 -i localhost
# Test SPOP and SRANDMEMBER
echo "Testing SPOP and SRANDMEMBER..."
dotnet run -- sadd set2 "one" "two" "three" "four" "five" -i localhost
dotnet run -- spop set2 -i localhost
dotnet run -- srandmember set2 -i localhost
dotnet run -- srandmember set2 2 -i localhost
# Test SREM
echo "Testing SREM..."
dotnet run -- srem set1 "banana" -i localhost
dotnet run -- smembers set1 -i localhost
# Test Set operations with multiple sets
echo "Testing Set operations..."
dotnet run -- sadd set3 "apple" "banana" "grape" -i localhost
dotnet run -- sadd set4 "banana" "cherry" "date" -i localhost
# Test SINTER
echo "Testing SINTER..."
dotnet run -- sinter set1 set3 set4 -i localhost
# Test SUNION
echo "Testing SUNION..."
dotnet run -- sunion set1 set3 set4 -i localhost
# Test SDIFF
echo "Testing SDIFF..."
dotnet run -- sdiff set3 set4 -i localhost
# Test SINTERSTORE, SUNIONSTORE, SDIFFSTORE
echo "Testing SINTERSTORE, SUNIONSTORE, SDIFFSTORE..."
dotnet run -- sinterstore result1 set1 set3 -i localhost
dotnet run -- sunionstore result2 set1 set3 -i localhost
dotnet run -- sdiffstore result3 set3 set4 -i localhost
dotnet run -- smembers result1 -i localhost
dotnet run -- smembers result2 -i localhost
dotnet run -- smembers result3 -i localhost
# Test SSCAN
echo "Testing SSCAN..."
dotnet run -- sscan set3 0 -i localhost
# Test SMOVE
echo "Testing SMOVE..."
dotnet run -- sadd source_set "item1" "item2" "item3" -i localhost
dotnet run -- sadd dest_set "existing_item" -i localhost
dotnet run -- smove source_set dest_set "item2" -i localhost
dotnet run -- smembers source_set -i localhost
dotnet run -- smembers dest_set -i localhost
echo "Set tests completed!"

View File

@ -0,0 +1,82 @@
#!/bin/bash
echo "=== Testing Sorted Set Commands ==="
# Test ZADD and ZRANGE
echo "Testing ZADD and ZRANGE..."
dotnet run -- zadd leaderboard 100 "player1" -i localhost
dotnet run -- zadd leaderboard 200 "player2" -i localhost
dotnet run -- zadd leaderboard 150 "player3" -i localhost
dotnet run -- zrange leaderboard 0 -1 -i localhost
dotnet run -- zrange leaderboard 0 -1 withscores -i localhost
# Test ZSCORE
echo "Testing ZSCORE..."
dotnet run -- zscore leaderboard "player1" -i localhost
dotnet run -- zscore leaderboard "player2" -i localhost
# Test ZRANK and ZREVRANK
echo "Testing ZRANK and ZREVRANK..."
dotnet run -- zrank leaderboard "player1" -i localhost
dotnet run -- zrevrank leaderboard "player1" -i localhost
# Test ZCARD
echo "Testing ZCARD..."
dotnet run -- zcard leaderboard -i localhost
# Test ZREM
echo "Testing ZREM..."
dotnet run -- zrem leaderboard "player2" -i localhost
dotnet run -- zrange leaderboard 0 -1 -i localhost
# Test ZRANGEBYSCORE and ZREVRANGEBYSCORE
echo "Testing ZRANGEBYSCORE and ZREVRANGEBYSCORE..."
dotnet run -- zadd scores 50 "user1" -i localhost
dotnet run -- zadd scores 75 "user2" -i localhost
dotnet run -- zadd scores 90 "user3" -i localhost
dotnet run -- zadd scores 120 "user4" -i localhost
dotnet run -- zrangebyscore scores 60 100 -i localhost
dotnet run -- zrangebyscore scores 60 100 withscores -i localhost
dotnet run -- zrevrangebyscore scores 100 60 -i localhost
# Test ZCOUNT
echo "Testing ZCOUNT..."
dotnet run -- zcount scores 60 100 -i localhost
# Test ZINCRBY
echo "Testing ZINCRBY..."
dotnet run -- zincrby scores 25 "user1" -i localhost
dotnet run -- zscore scores "user1" -i localhost
# Test ZUNIONSTORE and ZINTERSTORE
echo "Testing ZUNIONSTORE and ZINTERSTORE..."
dotnet run -- zadd set1 1 "a" 2 "b" 3 "c" -i localhost
dotnet run -- zadd set2 2 "b" 3 "c" 4 "d" -i localhost
dotnet run -- zunionstore union_result 2 set1 set2 -i localhost
dotnet run -- zinterstore inter_result 2 set1 set2 -i localhost
dotnet run -- zrange union_result 0 -1 withscores -i localhost
dotnet run -- zrange inter_result 0 -1 withscores -i localhost
# Test ZSCAN
echo "Testing ZSCAN..."
dotnet run -- zscan scores 0 -i localhost
# Test ZPOPMAX and ZPOPMIN
echo "Testing ZPOPMAX and ZPOPMIN..."
dotnet run -- zpopmax scores -i localhost
dotnet run -- zpopmin scores -i localhost
# Test ZREMRANGEBYRANK and ZREMRANGEBYSCORE
echo "Testing ZREMRANGEBYRANK and ZREMRANGEBYSCORE..."
dotnet run -- zadd test_set 10 "item1" 20 "item2" 30 "item3" 40 "item4" -i localhost
dotnet run -- zremrangebyrank test_set 1 2 -i localhost
dotnet run -- zrange test_set 0 -1 -i localhost
dotnet run -- zadd test_set2 10 "item1" 20 "item2" 30 "item3" 40 "item4" -i localhost
dotnet run -- zremrangebyscore test_set2 15 35 -i localhost
dotnet run -- zrange test_set2 0 -1 -i localhost
echo "Sorted Set tests completed!"

4
CommandTests/10_cleanup.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Flushing the current Redis database..."
dotnet run -- flushdb -i localhost --yes

0
CommandTests/README.md Normal file
View File

53
CommandTests/run_all_tests.sh Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
echo "=========================================="
echo "Redis Manager - Complete Test Suite"
echo "=========================================="
echo ""
# Make all scripts executable
chmod +x CommandTests/*.sh
echo "Running setup..."
./CommandTests/01_setup.sh
echo ""
echo "Running basic string tests..."
./CommandTests/02_basic_string_tests.sh
echo ""
echo "Running hash tests..."
./CommandTests/03_hash_tests.sh
echo ""
echo "Running geo tests..."
./CommandTests/04_geo_tests.sh
echo ""
echo "Running bit tests..."
./CommandTests/05_bit_tests.sh
echo ""
echo "Running module tests..."
./CommandTests/06_module_tests.sh
echo ""
echo "Running list tests..."
./CommandTests/07_list_tests.sh
echo ""
echo "Running set tests..."
./CommandTests/08_set_tests.sh
echo ""
echo "Running sorted set tests..."
./CommandTests/09_sorted_set_tests.sh
echo ""
echo "Running cleanup..."
./CommandTests/10_cleanup.sh
echo ""
echo "=========================================="
echo "All tests completed!"
echo "=========================================="

View File

@ -0,0 +1,302 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for advanced Redis string operations.
/// Provides functionality for APPEND, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT, GETRANGE, SETRANGE, STRLEN, MGET, and MSET commands.
/// </summary>
[Verb("append", HelpText = "Append a value to a key.")]
public class AppendOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "value", Required = true, HelpText = "Value to append.")]
public string Value { get; set; }
}
[Verb("incr", HelpText = "Increment the integer value of a key by one.")]
public class IncrOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
}
[Verb("decr", HelpText = "Decrement the integer value of a key by one.")]
public class DecrOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
}
[Verb("incrby", HelpText = "Increment the integer value of a key by a given amount.")]
public class IncrByOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to increment by.")]
public long Amount { get; set; }
}
[Verb("decrby", HelpText = "Decrement the integer value of a key by a given amount.")]
public class DecrByOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to decrement by.")]
public long Amount { get; set; }
}
[Verb("incrbyfloat", HelpText = "Increment the float value of a key by a given amount.")]
public class IncrByFloatOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to increment by.")]
public double Amount { get; set; }
}
[Verb("getrange", HelpText = "Get a substring of the string stored at a key.")]
public class GetRangeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start offset.")]
public long Start { get; set; }
[Value(2, MetaName = "end", Required = true, HelpText = "End offset.")]
public long End { get; set; }
}
[Verb("setrange", HelpText = "Overwrite part of a string at key starting at the specified offset.")]
public class SetRangeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
[Value(1, MetaName = "offset", Required = true, HelpText = "Offset.")]
public long Offset { get; set; }
[Value(2, MetaName = "value", Required = true, HelpText = "Value to set.")]
public string Value { get; set; }
}
[Verb("strlen", HelpText = "Get the length of the value stored in a key.")]
public class StrLenOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
public string Key { get; set; }
}
[Verb("mget", HelpText = "Get the values of all the given keys.")]
public class MGetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys.")]
public IEnumerable<string> Keys { get; set; }
}
[Verb("mset", HelpText = "Set multiple keys to multiple values.")]
public class MSetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("pairs", Required = true, HelpText = "Comma-separated key=value pairs.")]
public string Pairs { get; set; }
}
public static class AdvancedStringCommands
{
/// <summary>
/// Executes the APPEND command to append a value to a key.
/// </summary>
/// <param name="opts">The AppendOptions containing instance, key, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunAppend(AppendOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var len = db.StringAppend(opts.Key, opts.Value);
Console.WriteLine(Output.Green(len.ToString()));
return 0;
}
/// <summary>
/// Executes the INCR command to increment the integer value of a key by one.
/// </summary>
/// <param name="opts">The IncrOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunIncr(IncrOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringIncrement(opts.Key);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the DECR command to decrement the integer value of a key by one.
/// </summary>
/// <param name="opts">The DecrOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunDecr(DecrOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringDecrement(opts.Key);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the INCRBY command to increment the integer value of a key by a given amount.
/// </summary>
/// <param name="opts">The IncrByOptions containing instance, key, and amount.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunIncrBy(IncrByOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringIncrement(opts.Key, opts.Amount);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the DECRBY command to decrement the integer value of a key by a given amount.
/// </summary>
/// <param name="opts">The DecrByOptions containing instance, key, and amount.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunDecrBy(DecrByOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringDecrement(opts.Key, opts.Amount);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the INCRBYFLOAT command to increment the float value of a key by a given amount.
/// </summary>
/// <param name="opts">The IncrByFloatOptions containing instance, key, and amount.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunIncrByFloat(IncrByFloatOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringIncrement(opts.Key, opts.Amount);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the GETRANGE command to get a substring of the string stored at a key.
/// </summary>
/// <param name="opts">The GetRangeOptions containing instance, key, start, and end.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGetRange(GetRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var val = db.StringGetRange(opts.Key, opts.Start, opts.End);
Console.WriteLine(Output.Green(val.ToString()));
return 0;
}
/// <summary>
/// Executes the SETRANGE command to overwrite part of a string at key starting at the specified offset.
/// </summary>
/// <param name="opts">The SetRangeOptions containing instance, key, offset, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSetRange(SetRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var len = db.StringSetRange(opts.Key, opts.Offset, opts.Value);
Console.WriteLine(Output.Green(len.ToString()));
return 0;
}
/// <summary>
/// Executes the STRLEN command to get the length of the value stored in a key.
/// </summary>
/// <param name="opts">The StrLenOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunStrLen(StrLenOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var len = db.StringLength(opts.Key);
Console.WriteLine(Output.Green(len.ToString()));
return 0;
}
/// <summary>
/// Executes the MGET command to get the values of all the given keys.
/// </summary>
/// <param name="opts">The MGetOptions containing instance and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunMGet(MGetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var vals = db.StringGet(keys);
for (int i = 0; i < keys.Length; i++)
{
Console.WriteLine(Output.Green($"{keys[i]}: {vals[i]}"));
}
return 0;
}
/// <summary>
/// Executes the MSET command to set multiple keys to multiple values.
/// </summary>
/// <param name="opts">The MSetOptions containing instance and pairs.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunMSet(MSetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var pairs = opts.Pairs.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2).Select(p => new KeyValuePair<RedisKey, RedisValue>(p[0].Trim(), p[1].Trim())).ToArray();
db.StringSet(pairs);
Console.WriteLine(Output.Green("OK"));
return 0;
}
}
}

207
Commands/BitCommands.cs Normal file
View File

@ -0,0 +1,207 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
using RedisManager.Commands;
namespace RedisManager.Commands
{
[Verb("bitcount", HelpText = "Count set bits in a string.")]
public class BitCountOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Option("start", Default = 0L, HelpText = "Start byte.")]
public long Start { get; set; }
[Option("end", Default = -1L, HelpText = "End byte.")]
public long End { get; set; }
}
[Verb("bitfield", HelpText = "Perform arbitrary bitfield integer operations on strings.")]
public class BitFieldOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "operations", Min = 1, Required = true, HelpText = "Bitfield operations.")]
public IEnumerable<string> Operations { get; set; }
}
[Verb("bitop", HelpText = "Perform bitwise operations between strings.")]
public class BitOpOptions : InstanceOptions
{
[Value(0, MetaName = "operation", Required = true, HelpText = "Operation (AND, OR, XOR, NOT).")]
public string Operation { get; set; }
[Value(1, MetaName = "dest-key", Required = true, HelpText = "Destination key.")]
public string DestKey { get; set; }
[Value(2, MetaName = "source-keys", Min = 1, Required = true, HelpText = "Source keys.")]
public IEnumerable<string> SourceKeys { get; set; }
}
[Verb("bitpos", HelpText = "Find first bit set or clear in a string.")]
public class BitPosOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "bit", Required = true, HelpText = "Bit value (0 or 1).")]
public int Bit { get; set; }
[Option("start", Default = 0L, HelpText = "Start byte.")]
public long Start { get; set; }
[Option("end", Default = -1L, HelpText = "End byte.")]
public long End { get; set; }
}
[Verb("getbit", HelpText = "Returns the bit value at offset in the string value stored at key.")]
public class GetBitOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "offset", Required = true, HelpText = "Bit offset.")]
public long Offset { get; set; }
}
[Verb("setbit", HelpText = "Sets or clears the bit at offset in the string value stored at key.")]
public class SetBitOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "offset", Required = true, HelpText = "Bit offset.")]
public long Offset { get; set; }
[Value(2, MetaName = "bit", Required = true, HelpText = "Bit value (0 or 1).")]
public int Bit { get; set; }
}
public static class BitCommands
{
/// <summary>
/// Executes the BITCOUNT command to count set bits in a string.
/// </summary>
/// <param name="opts">The BitCountOptions containing instance, key, start, and end.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBitCount(BitCountOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key };
if (opts.Start != 0 || opts.End != -1) { args.Add(opts.Start); args.Add(opts.End); }
var result = db.Execute("BITCOUNT", args.ToArray());
Console.WriteLine(Output.Green($"Bit count: {result}"));
return 0;
}
/// <summary>
/// Executes the BITFIELD command to perform arbitrary bitfield integer operations on strings.
/// </summary>
/// <param name="opts">The BitFieldOptions containing instance, key, and operations.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBitField(BitFieldOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key };
args.AddRange(opts.Operations);
var result = db.Execute("BITFIELD", args.ToArray());
RedisResult[] results;
if (result is Array)
results = (RedisResult[])result;
else
results = new RedisResult[] { result };
Console.WriteLine(Output.Green("Bitfield results:"));
var operationsList = opts.Operations.ToList();
for (int i = 0; results != null && i < results.Length; i++)
{
var res = results[i];
if (res != null)
{
Console.WriteLine(Output.Cyan($"{operationsList[i]}: {res}"));
}
else
{
Console.WriteLine(Output.Red($"{operationsList[i]}: null"));
}
}
return 0;
}
/// <summary>
/// Executes the BITOP command to perform bitwise operations between strings.
/// </summary>
/// <param name="opts">The BitOpOptions containing instance, operation, destination key, and source keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBitOp(BitOpOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Operation, opts.DestKey };
args.AddRange(opts.SourceKeys);
var result = db.Execute("BITOP", args.ToArray());
Console.WriteLine(Output.Green($"BITOP result: {result}"));
return 0;
}
/// <summary>
/// Executes the BITPOS command to find the first bit set or clear in a string.
/// </summary>
/// <param name="opts">The BitPosOptions containing instance, key, bit, start, and end.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBitPos(BitPosOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key, opts.Bit };
if (opts.Start != 0 || opts.End != -1) { args.Add(opts.Start); args.Add(opts.End); }
var result = db.Execute("BITPOS", args.ToArray());
Console.WriteLine(Output.Green($"First {opts.Bit} bit at position: {result}"));
return 0;
}
/// <summary>
/// Executes the GETBIT command to return the bit value at offset in the string value stored at key.
/// </summary>
/// <param name="opts">The GetBitOptions containing instance, key, and offset.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGetBit(GetBitOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("GETBIT", new object[] { opts.Key, opts.Offset });
Console.WriteLine(Output.Green($"Bit at offset {opts.Offset}: {result}"));
return 0;
}
/// <summary>
/// Executes the SETBIT command to set or clear the bit at offset in the string value stored at key.
/// </summary>
/// <param name="opts">The SetBitOptions containing instance, key, offset, and bit value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSetBit(SetBitOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("SETBIT", new object[] { opts.Key, opts.Offset, opts.Bit });
Console.WriteLine(Output.Green($"Set bit at offset {opts.Offset} to {opts.Bit}: {result}"));
return 0;
}
}
}

View File

@ -0,0 +1,160 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis connection management operations.
/// Provides functionality for AUTH, QUIT, CLIENT LIST, CLIENT KILL, CLIENT GETNAME, and CLIENT SETNAME commands.
/// </summary>
[Verb("auth", HelpText = "Authenticate to the server.")]
public class AuthOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "password", Required = true, HelpText = "Password.")]
public string Password { get; set; }
}
[Verb("quit", HelpText = "Close the connection.")]
public class QuitOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("clientlist", HelpText = "Get the list of client connections.")]
public class ClientListOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("clientkill", HelpText = "Kill a client connection.")]
public class ClientKillOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "client_id", Required = true, HelpText = "Client ID.")]
public string ClientId { get; set; }
}
[Verb("clientgetname", HelpText = "Get the current connection name.")]
public class ClientGetNameOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("clientsetname", HelpText = "Set the current connection name.")]
public class ClientSetNameOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "name", Required = true, HelpText = "Connection name.")]
public string Name { get; set; }
}
public static class ConnectionCommands
{
/// <summary>
/// Executes the AUTH command to authenticate to the server.
/// </summary>
/// <param name="opts">The AuthOptions containing instance and password.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunAuth(AuthOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("AUTH", opts.Password);
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the QUIT command to close the connection.
/// </summary>
/// <param name="opts">The QuitOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunQuit(QuitOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("QUIT");
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the CLIENT LIST command to get the list of client connections.
/// </summary>
/// <param name="opts">The ClientListOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunClientList(ClientListOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("CLIENT", "LIST");
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the CLIENT KILL command to kill a client connection.
/// </summary>
/// <param name="opts">The ClientKillOptions containing instance and client ID.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunClientKill(ClientKillOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("CLIENT", "KILL", opts.ClientId);
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the CLIENT GETNAME command to get the current connection name.
/// </summary>
/// <param name="opts">The ClientGetNameOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunClientGetName(ClientGetNameOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("CLIENT", "GETNAME");
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the CLIENT SETNAME command to set the current connection name.
/// </summary>
/// <param name="opts">The ClientSetNameOptions containing instance and name.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunClientSetName(ClientSetNameOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("CLIENT", "SETNAME", opts.Name);
Console.WriteLine(Output.Green("OK"));
return 0;
}
}
}

View File

@ -0,0 +1,121 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
using RedisManager.Commands;
namespace RedisManager.Commands
{
[Verb("select", HelpText = "Change the selected database.")]
public class SelectOptions : InstanceOptions
{
[Value(0, MetaName = "database", Required = true, HelpText = "Database number.")]
public int Database { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("flushdb", HelpText = "Remove all keys from the current database.")]
public class FlushDbOptions : InstanceOptions
{
[Option("yes", Required = false, HelpText = "Skip confirmation prompt.")]
public bool Yes { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("flushall", HelpText = "Remove all keys from all databases.")]
public class FlushAllOptions : InstanceOptions
{
[Option("yes", Required = false, HelpText = "Skip confirmation prompt.")]
public bool Yes { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("dbsize", HelpText = "Return the number of keys in the current database.")]
public class DbSizeOptions : InstanceOptions
{
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class DatabaseCommands
{
public static int RunSelect(SelectOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase(opts.Database);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Database", "Status" }, new List<string[]> { new[] { opts.Database.ToString(), "Selected" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
public static int RunFlushDb(FlushDbOptions opts, Config config)
{
if (!opts.Yes)
{
Console.WriteLine(Output.Yellow("Are you sure you want to flush the current database? (y/N)"));
var response = Console.ReadLine()?.ToLower();
if (response != "y" && response != "yes")
{
Console.WriteLine(Output.Red("Operation cancelled."));
return 1;
}
}
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("FLUSHDB");
if (opts.Table)
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "FLUSHDB", "OK" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
public static int RunFlushAll(FlushAllOptions opts, Config config)
{
if (!opts.Yes)
{
Console.WriteLine(Output.Yellow("Are you sure you want to flush ALL databases? (y/N)"));
var response = Console.ReadLine()?.ToLower();
if (response != "y" && response != "yes")
{
Console.WriteLine(Output.Red("Operation cancelled."));
return 1;
}
}
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("FLUSHALL");
if (opts.Table)
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "FLUSHALL", "OK" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
public static int RunDbSize(DbSizeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var size = db.Execute("DBSIZE");
if (opts.Table)
RedisUtils.PrintTable(new[] { "Database", "Size" }, new List<string[]> { new[] { "Current", size.ToString() } });
else
Console.WriteLine(Output.Green(size.ToString()));
return 0;
}
}
}

242
Commands/GeoCommands.cs Normal file
View File

@ -0,0 +1,242 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis geospatial operations.
/// Provides functionality for GEOADD, GEORADIUS, GEODIST, GEOHASH, and GEOPOS commands.
/// </summary>
[Verb("geoadd", HelpText = "Add one or more geospatial items to a key.")]
public class GeoAddOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 3, Required = true, HelpText = "Members with longitude,latitude,name format.")]
public IEnumerable<string> Members { get; set; }
}
[Verb("georadius", HelpText = "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point.")]
public class GeoRadiusOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "longitude", Required = true, HelpText = "Longitude.")]
public double Longitude { get; set; }
[Value(2, MetaName = "latitude", Required = true, HelpText = "Latitude.")]
public double Latitude { get; set; }
[Value(3, MetaName = "radius", Required = true, HelpText = "Radius.")]
public double Radius { get; set; }
[Option("unit", Default = "m", HelpText = "Unit (m, km, mi, ft).")]
public string Unit { get; set; }
[Option("withcoord", HelpText = "Include coordinates in output.")]
public bool WithCoord { get; set; }
[Option("withdist", HelpText = "Include distance in output.")]
public bool WithDist { get; set; }
[Option("withhash", HelpText = "Include hash in output.")]
public bool WithHash { get; set; }
[Option("count", HelpText = "Limit results count.")]
public int Count { get; set; }
[Option("asc", HelpText = "Sort ascending by distance.")]
public bool Asc { get; set; }
[Option("desc", HelpText = "Sort descending by distance.")]
public bool Desc { get; set; }
}
[Verb("geodist", HelpText = "Return the distance between two members of a geospatial index.")]
public class GeoDistOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "member1", Required = true, HelpText = "First member.")]
public string Member1 { get; set; }
[Value(2, MetaName = "member2", Required = true, HelpText = "Second member.")]
public string Member2 { get; set; }
[Option("unit", Default = "m", HelpText = "Unit (m, km, mi, ft).")]
public string Unit { get; set; }
}
[Verb("geohash", HelpText = "Returns members of a geospatial index as standard geohash strings.")]
public class GeoHashOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members.")]
public IEnumerable<string> Members { get; set; }
}
[Verb("geopos", HelpText = "Returns longitude and latitude of members of a geospatial index.")]
public class GeoPosOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members.")]
public IEnumerable<string> Members { get; set; }
}
public static class GeoCommands
{
/// <summary>
/// Executes the GEOADD command to add one or more geospatial items to a key.
/// </summary>
/// <param name="opts">The GeoAddOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGeoAdd(GeoAddOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key };
var membersList = opts.Members.ToList();
for (int i = 0; i + 2 < membersList.Count; i += 3)
{
args.Add(membersList[i]); // longitude
args.Add(membersList[i + 1]); // latitude
args.Add(membersList[i + 2]); // name
}
var result = db.Execute("GEOADD", args.ToArray());
Console.WriteLine(Output.Green($"Added: {result}"));
return 0;
}
/// <summary>
/// Executes the GEORADIUS command to query a geospatial index for members within a radius.
/// </summary>
/// <param name="opts">The GeoRadiusOptions containing instance, key, longitude, latitude, radius, and options.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGeoRadius(GeoRadiusOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key, opts.Longitude, opts.Latitude, opts.Radius, opts.Unit };
if (opts.WithCoord) args.Add("WITHCOORD");
if (opts.WithDist) args.Add("WITHDIST");
if (opts.WithHash) args.Add("WITHHASH");
if (opts.Count > 0) { args.Add("COUNT"); args.Add(opts.Count); }
if (opts.Asc) args.Add("ASC");
if (opts.Desc) args.Add("DESC");
var result = db.Execute("GEORADIUS", args.ToArray());
Console.WriteLine(Output.Green($"Raw result: {result}"));
return 0;
}
/// <summary>
/// Executes the GEODIST command to return the distance between two members of a geospatial index.
/// </summary>
/// <param name="opts">The GeoDistOptions containing instance, key, member1, member2, and unit.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGeoDist(GeoDistOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("GEODIST", new object[] { opts.Key, opts.Member1, opts.Member2, opts.Unit });
Console.WriteLine(Output.Green($"Distance: {result} {opts.Unit}"));
return 0;
}
/// <summary>
/// Executes the GEOHASH command to return geohash strings for members of a geospatial index.
/// </summary>
/// <param name="opts">The GeoHashOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGeoHash(GeoHashOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key };
args.AddRange(opts.Members);
var result = db.Execute("GEOHASH", args.ToArray());
RedisResult[] hashes;
if (result is Array)
hashes = (RedisResult[])result;
else
hashes = new RedisResult[] { result };
var membersList = opts.Members.ToList();
for (int i = 0; i < membersList.Count; i++)
{
var hash = hashes != null && i < hashes.Length ? hashes[i]?.ToString() : null;
if (!string.IsNullOrEmpty(hash))
{
Console.WriteLine(Output.Green($"{membersList[i]}: {hash}"));
}
else
{
Console.WriteLine(Output.Red($"{membersList[i]}: not found"));
}
}
return 0;
}
/// <summary>
/// Executes the GEOPOS command to return longitude and latitude of members of a geospatial index.
/// </summary>
/// <param name="opts">The GeoPosOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunGeoPos(GeoPosOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Key };
args.AddRange(opts.Members);
var result = db.Execute("GEOPOS", args.ToArray());
RedisResult[] positions;
if (result is Array)
positions = (RedisResult[])result;
else
positions = new RedisResult[] { result };
var membersList = opts.Members.ToList();
for (int i = 0; i < membersList.Count; i++)
{
var pos = positions != null && i < positions.Length ? positions[i]?.ToString() : null;
if (!string.IsNullOrEmpty(pos))
{
Console.WriteLine(Output.Green($"{membersList[i]}: {pos}"));
}
else
{
Console.WriteLine(Output.Red($"{membersList[i]}: not found"));
}
}
return 0;
}
private static GeoUnit GetGeoUnit(string unit)
{
return unit.ToLower() switch
{
"km" => GeoUnit.Kilometers,
"mi" => GeoUnit.Miles,
"ft" => GeoUnit.Feet,
_ => GeoUnit.Meters
};
}
}
}

583
Commands/HashCommands.cs Normal file
View File

@ -0,0 +1,583 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis hash operations.
/// Provides functionality for HGET, HSET, HDEL, HGETALL, HKEYS, HVALS, HLEN, HEXISTS, HINCRBY, HINCRBYFLOAT, HMSET, HMGET, HSETNX, HSTRLEN, and HSCAN commands.
/// </summary>
[Verb("hget", HelpText = "Get value of a field in a hash.")]
public class HGetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hset", HelpText = "Set value of a field in a hash.")]
public class HSetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Value(2, MetaName = "value", Required = true, HelpText = "Value.")]
public string Value { get; set; }
[Option("ttl", Required = false, HelpText = "Time to live in seconds.")]
public int? TTL { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hdel", HelpText = "Delete a field in a hash.")]
public class HDelOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hgetall", HelpText = "Get all fields and values in a hash.")]
public class HGetAllOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hkeys", HelpText = "List all fields in a hash.")]
public class HKeysOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hvals", HelpText = "List all values in a hash.")]
public class HValsOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hlen", HelpText = "Get the number of fields in a hash.")]
public class HLenOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hexists", HelpText = "Check if a field exists in a hash.")]
public class HExistsOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hincrby", HelpText = "Increment field value in hash by integer.")]
public class HIncrByOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Value(2, MetaName = "increment", Required = true, HelpText = "Increment value.")]
public long Increment { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hincrbyfloat", HelpText = "Increment field value in hash by float.")]
public class HIncrByFloatOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Value(2, MetaName = "increment", Required = true, HelpText = "Increment value.")]
public double Increment { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hmset", HelpText = "Set multiple fields in a hash.")]
public class HMSetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "fields", Min = 1, Required = true, HelpText = "Field-value pairs (field1 value1 field2 value2...).")]
public IEnumerable<string> Fields { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hmget", HelpText = "Get multiple field values from a hash.")]
public class HMGetOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "fields", Min = 1, Required = true, HelpText = "Field names to get.")]
public IEnumerable<string> Fields { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hsetnx", HelpText = "Set field value in hash only if field does not exist.")]
public class HSetNxOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Value(2, MetaName = "value", Required = true, HelpText = "Value.")]
public string Value { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hstrlen", HelpText = "Get the string length of a hash field value.")]
public class HStrLenOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
public string Field { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("hscan", HelpText = "Incrementally iterate hash fields.")]
public class HScanOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
public string Hash { get; set; }
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
public long Cursor { get; set; }
[Option("match", Required = false, HelpText = "Pattern to match.")]
public string Match { get; set; }
[Option("count", Required = false, HelpText = "Count hint.")]
public long? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class HashCommands
{
/// <summary>
/// Executes the HGET command to get the value of a field in a hash.
/// </summary>
/// <param name="opts">The HGetOptions containing instance, hash, and field.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHGet(HGetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var value = db.HashGet(opts.Hash, opts.Field);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, new List<string[]> { new[] { opts.Hash, opts.Field, value.IsNull ? "[null]" : value.ToString() } });
else if (value.IsNull)
Console.WriteLine(Output.Yellow($"[null]"));
else
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the HSET command to set the value of a field in a hash.
/// </summary>
/// <param name="opts">The HSetOptions containing instance, hash, field, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHSet(HSetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var ok = db.HashSet(opts.Hash, opts.Field, opts.Value);
if (opts.TTL.HasValue)
db.KeyExpire(opts.Hash, TimeSpan.FromSeconds(opts.TTL.Value));
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Value, ok ? "OK" : "Failed" } });
else if (ok)
Console.WriteLine(Output.Green("OK"));
else
Console.WriteLine(Output.Red("Failed to set field."));
return 0;
}
/// <summary>
/// Executes the HDEL command to delete a field from a hash.
/// </summary>
/// <param name="opts">The HDelOptions containing instance, hash, and field.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHDel(HDelOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var ok = db.HashDelete(opts.Hash, opts.Field);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, ok ? "Deleted" : "Not found" } });
else if (ok)
Console.WriteLine(Output.Green("OK"));
else
Console.WriteLine(Output.Yellow("Field not found."));
return 0;
}
/// <summary>
/// Executes the HGETALL command to get all fields and values in a hash.
/// </summary>
/// <param name="opts">The HGetAllOptions containing instance and hash.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHGetAll(HGetAllOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var entries = db.HashGetAll(opts.Hash);
if (opts.Table)
{
var rows = entries.Select(e => new[] { e.Name.ToString(), e.Value.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Field", "Value" }, rows);
}
else
foreach (var entry in entries)
Console.WriteLine(Output.Green($"{entry.Name}: {entry.Value}"));
return 0;
}
/// <summary>
/// Executes the HKEYS command to list all fields in a hash.
/// </summary>
/// <param name="opts">The HKeysOptions containing instance and hash.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHKeys(HKeysOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = db.HashKeys(opts.Hash);
if (opts.Table)
{
var rows = keys.Select(k => new[] { k.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Field" }, rows);
}
else
foreach (var key in keys)
Console.WriteLine(Output.Green(key.ToString()));
return 0;
}
/// <summary>
/// Executes the HVALS command to list all values in a hash.
/// </summary>
/// <param name="opts">The HValsOptions containing instance and hash.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHVals(HValsOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var values = db.HashValues(opts.Hash);
if (opts.Table)
{
var rows = values.Select(v => new[] { v.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Value" }, rows);
}
else
foreach (var value in values)
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the HLEN command to get the number of fields in a hash.
/// </summary>
/// <param name="opts">The HLenOptions containing instance and hash.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHLen(HLenOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var len = db.HashLength(opts.Hash);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Length" }, new List<string[]> { new[] { opts.Hash, len.ToString() } });
else
Console.WriteLine(Output.Green(len.ToString()));
return 0;
}
/// <summary>
/// Executes the HEXISTS command to check if a field exists in a hash.
/// </summary>
/// <param name="opts">The HExistsOptions containing instance, hash, and field.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHExists(HExistsOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var exists = db.HashExists(opts.Hash, opts.Field);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Exists" }, new List<string[]> { new[] { opts.Hash, opts.Field, exists ? "Yes" : "No" } });
else
Console.WriteLine(Output.Green(exists ? "1" : "0"));
return 0;
}
/// <summary>
/// Executes the HINCRBY command to increment a field value in a hash by an integer.
/// </summary>
/// <param name="opts">The HIncrByOptions containing instance, hash, field, and increment.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHIncrBy(HIncrByOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.HashIncrement(opts.Hash, opts.Field, opts.Increment);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Increment", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } });
else
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the HINCRBYFLOAT command to increment a field value in a hash by a float.
/// </summary>
/// <param name="opts">The HIncrByFloatOptions containing instance, hash, field, and increment.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHIncrByFloat(HIncrByFloatOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.HashIncrement(opts.Hash, opts.Field, opts.Increment);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Increment", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } });
else
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the HMSET command to set multiple fields in a hash.
/// </summary>
/// <param name="opts">The HMSetOptions containing instance, hash, and field-value pairs.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHMSet(HMSetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var fieldsList = opts.Fields.ToList();
if (fieldsList.Count % 2 != 0)
{
Console.WriteLine(Output.Red("Error: Fields must be provided as field-value pairs."));
return 1;
}
var entries = new HashEntry[fieldsList.Count / 2];
for (int i = 0; i < fieldsList.Count; i += 2)
{
entries[i / 2] = new HashEntry(fieldsList[i], fieldsList[i + 1]);
}
db.HashSet(opts.Hash, entries);
if (opts.Table)
{
var rows = new List<string[]>();
for (int i = 0; i < entries.Length; i++)
{
rows.Add(new[] { opts.Hash, entries[i].Name.ToString(), entries[i].Value.ToString() });
}
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, rows);
}
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the HMGET command to get multiple field values from a hash.
/// </summary>
/// <param name="opts">The HMGetOptions containing instance, hash, and fields.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHMGet(HMGetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var fields = opts.Fields.Select(f => (RedisValue)f).ToArray();
var values = db.HashGet(opts.Hash, fields);
if (opts.Table)
{
var rows = new List<string[]>();
var fieldsList = opts.Fields.ToList();
for (int i = 0; i < fieldsList.Count; i++)
{
var value = values[i].IsNull ? "[null]" : values[i].ToString();
rows.Add(new[] { opts.Hash, fieldsList[i], value });
}
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, rows);
}
else
{
var fieldsList = opts.Fields.ToList();
for (int i = 0; i < fieldsList.Count; i++)
{
var value = values[i].IsNull ? "[null]" : values[i].ToString();
Console.WriteLine(Output.Green($"{fieldsList[i]}: {value}"));
}
}
return 0;
}
/// <summary>
/// Executes the HSETNX command to set a field value in a hash only if the field does not exist.
/// </summary>
/// <param name="opts">The HSetNxOptions containing instance, hash, field, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHSetNx(HSetNxOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.HashSet(opts.Hash, opts.Field, opts.Value, When.NotExists);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Value, result ? "Set" : "Not set (field exists)" } });
else
Console.WriteLine(Output.Green(result ? "1" : "0"));
return 0;
}
/// <summary>
/// Executes the HSTRLEN command to get the string length of a hash field value.
/// </summary>
/// <param name="opts">The HStrLenOptions containing instance, hash, and field.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHStrLen(HStrLenOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var length = db.HashStringLength(opts.Hash, opts.Field);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Hash", "Field", "Length" }, new List<string[]> { new[] { opts.Hash, opts.Field, length.ToString() } });
else
Console.WriteLine(Output.Green(length.ToString()));
return 0;
}
/// <summary>
/// Executes the HSCAN command to incrementally iterate hash fields.
/// </summary>
/// <param name="opts">The HScanOptions containing instance, hash, and scan options.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunHScan(HScanOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var pattern = opts.Match;
var pageSize = opts.Count.HasValue ? (int)opts.Count.Value : 10;
var scanResult = db.HashScan(opts.Hash, pattern, pageSize);
if (opts.Table)
{
var rows = scanResult.Select(e => new[] { e.Name.ToString(), e.Value.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Field", "Value" }, rows);
Console.WriteLine(Output.Yellow($"Scanned {rows.Count} entries"));
}
else
{
int count = 0;
foreach (var entry in scanResult)
{
Console.WriteLine(Output.Green($"{entry.Name}: {entry.Value}"));
count++;
}
Console.WriteLine(Output.Yellow($"Scanned {count} entries"));
}
return 0;
}
}
}

View File

@ -0,0 +1,117 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
[Verb("pfadd", HelpText = "Add elements to a HyperLogLog.")]
public class PFAddOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "HyperLogLog key.")]
public string Key { get; set; }
[Value(1, MetaName = "elements", Min = 1, Required = true, HelpText = "Elements to add.")]
public IEnumerable<string> Elements { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("pfcount", HelpText = "Get the approximated cardinality of HyperLogLog(s).")]
public class PFCountOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "HyperLogLog key(s).")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("pfmerge", HelpText = "Merge multiple HyperLogLogs into a single one.")]
public class PFMergeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "destkey", Required = true, HelpText = "Destination key.")]
public string DestKey { get; set; }
[Value(1, MetaName = "sourcekeys", Min = 1, Required = true, HelpText = "Source HyperLogLog keys.")]
public IEnumerable<string> SourceKeys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class HyperLogLogCommands
{
public static int RunPFAdd(PFAddOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var elements = opts.Elements.ToArray();
// Use PFADD command directly
var batch = db.CreateBatch();
var tasks = elements.Select(e => batch.ExecuteAsync("PFADD", opts.Key, e)).ToArray();
batch.Execute();
var count = tasks.Count(t => (bool)t.Result);
if (opts.Table)
{
var rows = elements.Select(e => new[] { e, "Added" }).ToList();
RedisUtils.PrintTable(new[] { "Element", "Result" }, rows);
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
}
else
Console.WriteLine(Output.Green($"Added {count} new element(s)"));
return 0;
}
public static int RunPFCount(PFCountOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.ToArray();
// Use PFCOUNT command directly
var count = db.Execute("PFCOUNT", keys);
if (opts.Table)
{
if (keys.Length == 1)
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { keys[0], count.ToString() } });
else
RedisUtils.PrintTable(new[] { "Keys", "Combined Cardinality" }, new List<string[]> { new[] { string.Join(", ", keys), count.ToString() } });
}
else
Console.WriteLine(Output.Green(count.ToString()));
return 0;
}
public static int RunPFMerge(PFMergeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var sourceKeys = opts.SourceKeys.ToArray();
// Use PFMERGE command directly
db.Execute("PFMERGE", opts.DestKey, sourceKeys);
if (opts.Table)
{
var rows = sourceKeys.Select(k => new[] { k, "Merged" }).ToList();
RedisUtils.PrintTable(new[] { "Source Key", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Destination Key", "Status" }, new List<string[]> { new[] { opts.DestKey, "OK" } });
}
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
}
}

View File

@ -0,0 +1,126 @@
using CommandLine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using RedisManager.Utils;
namespace RedisManager.Commands
{
[Verb("list-instances", HelpText = "List all configured Redis instances.")]
public class ListInstancesOptions { }
[Verb("add-instance", HelpText = "Add a new Redis instance.")]
public class AddInstanceOptions
{
[Option('n', "name", Required = true, HelpText = "Instance name.")]
public string Name { get; set; }
[Option('h', "host", Required = true, HelpText = "Redis host.")]
public string Host { get; set; }
[Option('p', "port", Required = false, HelpText = "Redis port.")]
public int Port { get; set; } = 6379;
[Option('w', "password", Required = false, HelpText = "Redis password.")]
public string Password { get; set; }
}
[Verb("update-instance", HelpText = "Update an existing Redis instance.")]
public class UpdateInstanceOptions
{
[Option('n', "name", Required = true, HelpText = "Instance name.")]
public string Name { get; set; }
[Option('h', "host", Required = false, HelpText = "Redis host.")]
public string Host { get; set; }
[Option('p', "port", Required = false, HelpText = "Redis port.")]
public int Port { get; set; } = 6379;
[Option('w', "password", Required = false, HelpText = "Redis password.")]
public string Password { get; set; }
}
[Verb("delete-instance", HelpText = "Delete a Redis instance.")]
public class DeleteInstanceOptions
{
[Option('n', "name", Required = true, HelpText = "Instance name.")]
public string Name { get; set; }
}
public static class InstanceCommands
{
public static int RunListInstances(Config config)
{
if (config.Instances.Count == 0)
{
Console.WriteLine(Output.Yellow("No instances configured."));
return 0;
}
var rows = config.Instances.Select(i => new[] { i.Name, i.Host, i.Port.ToString(), i.Password ?? "" }).ToList();
RedisUtils.PrintTable(new[] { "Name", "Host", "Port", "Password" }, rows);
return 0;
}
public static int RunAddInstance(AddInstanceOptions opts, Config config)
{
if (config.Instances.Any(i => i.Name == opts.Name))
{
Console.WriteLine(Output.Red($"Instance '{opts.Name}' already exists."));
return 1;
}
var instance = new InstanceConfig
{
Name = opts.Name,
Host = opts.Host,
Port = opts.Port,
Password = opts.Password
};
config.Instances.Add(instance);
SaveConfig(config, "redismanager.json");
Console.WriteLine(Output.Green($"Instance '{opts.Name}' added successfully."));
return 0;
}
public static int RunUpdateInstance(UpdateInstanceOptions opts, Config config)
{
var instance = config.Instances.Find(i => i.Name == opts.Name);
if (instance == null)
{
Console.WriteLine(Output.Red($"Instance '{opts.Name}' not found."));
return 1;
}
if (!string.IsNullOrEmpty(opts.Host))
instance.Host = opts.Host;
if (opts.Port != 6379)
instance.Port = opts.Port;
if (!string.IsNullOrEmpty(opts.Password))
instance.Password = opts.Password;
SaveConfig(config, "redismanager.json");
Console.WriteLine(Output.Green($"Instance '{opts.Name}' updated successfully."));
return 0;
}
public static int RunDeleteInstance(DeleteInstanceOptions opts, Config config)
{
var instance = config.Instances.Find(i => i.Name == opts.Name);
if (instance == null)
{
Console.WriteLine(Output.Red($"Instance '{opts.Name}' not found."));
return 1;
}
config.Instances.Remove(instance);
SaveConfig(config, "redismanager.json");
Console.WriteLine(Output.Green($"Instance '{opts.Name}' deleted successfully."));
return 0;
}
private static void SaveConfig(Config config, string path)
{
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
}
}

View File

@ -0,0 +1,13 @@
using CommandLine;
namespace RedisManager.Commands
{
/// <summary>
/// Base options for commands that require a Redis instance.
/// </summary>
public class InstanceOptions
{
[Option('i', "instance", Required = true, HelpText = "Name of the Redis instance to use.")]
public string Instance { get; set; }
}
}

227
Commands/KeyCommands.cs Normal file
View File

@ -0,0 +1,227 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
[Verb("exists", HelpText = "Check if key(s) exist.")]
public class ExistsOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys to check.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("expire", HelpText = "Set a timeout on a key.")]
public class ExpireOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key to expire.")]
public string Key { get; set; }
[Value(1, MetaName = "seconds", Required = true, HelpText = "Expire time in seconds.")]
public int Seconds { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("ttl", HelpText = "Get the time to live for a key.")]
public class TtlOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key to check.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("persist", HelpText = "Remove the expiration from a key.")]
public class PersistOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key to persist.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("rename", HelpText = "Rename a key.")]
public class RenameOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Current key name.")]
public string Key { get; set; }
[Value(1, MetaName = "newkey", Required = true, HelpText = "New key name.")]
public string NewKey { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("type", HelpText = "Get the type of a key.")]
public class TypeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Key to check.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("keys", HelpText = "Find all keys matching pattern.")]
public class KeysOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "pattern", Required = true, HelpText = "Pattern to match.")]
public string Pattern { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("scan", HelpText = "Incrementally iterate the keyspace.")]
public class ScanOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("cursor", Required = false, HelpText = "Cursor to start from.")]
public ulong Cursor { get; set; } = 0;
[Option("pattern", Required = false, HelpText = "Pattern to match.")]
public string Pattern { get; set; }
[Option("count", Required = false, HelpText = "Count of keys to return.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class KeyCommands
{
public static int RunExists(ExistsOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.KeyExists(keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Keys", "Exist Count" }, new List<string[]> { new[] { string.Join(", ", opts.Keys), count.ToString() } });
else
Console.WriteLine(Output.Green(count.ToString()));
return 0;
}
public static int RunExpire(ExpireOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var ok = db.KeyExpire(opts.Key, TimeSpan.FromSeconds(opts.Seconds));
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Expire Set" }, new List<string[]> { new[] { opts.Key, ok ? "Yes" : "No" } });
else
Console.WriteLine(Output.Green(ok ? "1" : "0"));
return 0;
}
public static int RunTtl(TtlOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var ttl = db.KeyTimeToLive(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "TTL (s)" }, new List<string[]> { new[] { opts.Key, ttl?.TotalSeconds.ToString() ?? "-1" } });
else
Console.WriteLine(Output.Green(ttl?.TotalSeconds.ToString() ?? "-1"));
return 0;
}
public static int RunPersist(PersistOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var ok = db.KeyPersist(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Persisted" }, new List<string[]> { new[] { opts.Key, ok ? "Yes" : "No" } });
else
Console.WriteLine(Output.Green(ok ? "1" : "0"));
return 0;
}
public static int RunRename(RenameOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.KeyRename(opts.Key, opts.NewKey);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Old Key", "New Key", "Status" }, new List<string[]> { new[] { opts.Key, opts.NewKey, "Renamed" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
public static int RunType(TypeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var type = db.KeyType(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Type" }, new List<string[]> { new[] { opts.Key, type.ToString() } });
else
Console.WriteLine(Output.Green(type.ToString()));
return 0;
}
public static int RunKeys(KeysOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = db.Execute("KEYS", opts.Pattern).ToString().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (opts.Table)
{
var rows = keys.Select(k => new[] { k }).ToList();
RedisUtils.PrintTable(new[] { "Key" }, rows);
}
else
{
foreach (var key in keys)
Console.WriteLine(Output.Green(key));
}
return 0;
}
public static int RunScan(ScanOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("SCAN", opts.Cursor.ToString(),
string.IsNullOrEmpty(opts.Pattern) ? null : "MATCH",
string.IsNullOrEmpty(opts.Pattern) ? null : opts.Pattern,
opts.Count.HasValue ? "COUNT" : null,
opts.Count.HasValue ? opts.Count.Value.ToString() : null);
var arr = (RedisResult[])result;
var nextCursor = arr[0].ToString();
var keys = ((RedisResult[])arr[1]).Select(x => x.ToString()).ToArray();
if (opts.Table)
{
var rows = keys.Select(k => new[] { k }).ToList();
RedisUtils.PrintTable(new[] { "Key" }, rows);
RedisUtils.PrintTable(new[] { "Next Cursor" }, new List<string[]> { new[] { nextCursor } });
}
else
{
foreach (var key in keys)
Console.WriteLine(Output.Green(key));
Console.WriteLine(Output.Yellow($"Next Cursor: {nextCursor}"));
}
return 0;
}
}
}

528
Commands/ListCommands.cs Normal file
View File

@ -0,0 +1,528 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
using RedisManager.Commands;
namespace RedisManager.Commands
{
[Verb("lpush", HelpText = "Push elements to the left of a list.")]
public class LPushOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "values", Min = 1, Required = true, HelpText = "Values to push.")]
public IEnumerable<string> Values { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("rpush", HelpText = "Push elements to the right of a list.")]
public class RPushOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "values", Min = 1, Required = true, HelpText = "Values to push.")]
public IEnumerable<string> Values { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lpop", HelpText = "Pop element from the left of a list.")]
public class LPopOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("rpop", HelpText = "Pop element from the right of a list.")]
public class RPopOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lrange", HelpText = "Get range of elements from a list.")]
public class LRangeOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start index.")]
public long Start { get; set; }
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop index.")]
public long Stop { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("llen", HelpText = "Get the length of a list.")]
public class LLenOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lindex", HelpText = "Get element at index in a list.")]
public class LIndexOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "index", Required = true, HelpText = "Element index.")]
public long Index { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lset", HelpText = "Set element at index in a list.")]
public class LSetOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "index", Required = true, HelpText = "Element index.")]
public long Index { get; set; }
[Value(2, MetaName = "value", Required = true, HelpText = "New value.")]
public string Value { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lrem", HelpText = "Remove elements from a list.")]
public class LRemOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "count", Required = true, HelpText = "Number of elements to remove (0=all, positive=from left, negative=from right).")]
public long Count { get; set; }
[Value(2, MetaName = "value", Required = true, HelpText = "Value to remove.")]
public string Value { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("linsert", HelpText = "Insert element before or after pivot in a list.")]
public class LInsertOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "position", Required = true, HelpText = "Position: BEFORE or AFTER.")]
public string Position { get; set; }
[Value(2, MetaName = "pivot", Required = true, HelpText = "Pivot element.")]
public string Pivot { get; set; }
[Value(3, MetaName = "value", Required = true, HelpText = "Value to insert.")]
public string Value { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("ltrim", HelpText = "Trim list to specified range.")]
public class LTrimOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start index.")]
public long Start { get; set; }
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop index.")]
public long Stop { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("blpop", HelpText = "Blocking pop from the left of a list.")]
public class BLPopOptions : InstanceOptions
{
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "List keys.")]
public IEnumerable<string> Keys { get; set; }
[Value(1, MetaName = "timeout", Required = true, HelpText = "Timeout in seconds.")]
public int Timeout { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("brpop", HelpText = "Blocking pop from the right of a list.")]
public class BRPopOptions : InstanceOptions
{
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "List keys.")]
public IEnumerable<string> Keys { get; set; }
[Value(1, MetaName = "timeout", Required = true, HelpText = "Timeout in seconds.")]
public int Timeout { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("rpoplpush", HelpText = "Pop from right of source, push to left of destination.")]
public class RPopLPushOptions : InstanceOptions
{
[Value(0, MetaName = "source", Required = true, HelpText = "Source list key.")]
public string Source { get; set; }
[Value(1, MetaName = "destination", Required = true, HelpText = "Destination list key.")]
public string Destination { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class ListCommands
{
/// <summary>
/// Executes the LPUSH command to push elements to the left of a list.
/// </summary>
/// <param name="opts">The LPushOptions containing instance, key, and values.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLPush(LPushOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var values = opts.Values.ToArray();
var length = db.ListLeftPush(opts.Key, values.Select(v => (RedisValue)v).ToArray());
if (opts.Table)
{
var rows = values.Select(v => new[] { v, "Pushed" }).ToList();
RedisUtils.PrintTable(new[] { "Value", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Key", "New Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
}
else
Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}"));
return 0;
}
/// <summary>
/// Executes the RPUSH command to push elements to the right of a list.
/// </summary>
/// <param name="opts">The RPushOptions containing instance, key, and values.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunRPush(RPushOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var values = opts.Values.ToArray();
var length = db.ListRightPush(opts.Key, values.Select(v => (RedisValue)v).ToArray());
if (opts.Table)
{
var rows = values.Select(v => new[] { v, "Pushed" }).ToList();
RedisUtils.PrintTable(new[] { "Value", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Key", "New Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
}
else
Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}"));
return 0;
}
/// <summary>
/// Executes the LPOP command to pop an element from the left of a list.
/// </summary>
/// <param name="opts">The LPopOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLPop(LPopOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var value = db.ListLeftPop(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { opts.Key, value.ToString() } });
else
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the RPOP command to pop an element from the right of a list.
/// </summary>
/// <param name="opts">The RPopOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunRPop(RPopOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var value = db.ListRightPop(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { opts.Key, value.ToString() } });
else
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the LRANGE command to get a range of elements from a list.
/// </summary>
/// <param name="opts">The LRangeOptions containing instance, key, start, and stop.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLRange(LRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var values = db.ListRange(opts.Key, opts.Start, opts.Stop);
if (opts.Table)
{
var rows = values.Select((v, i) => new[] { (opts.Start + i).ToString(), v.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Index", "Value" }, rows);
}
else
foreach (var value in values)
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the LLEN command to get the length of a list.
/// </summary>
/// <param name="opts">The LLenOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLLen(LLenOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var length = db.ListLength(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
else
Console.WriteLine(Output.Green(length.ToString()));
return 0;
}
/// <summary>
/// Executes the LINDEX command to get the element at a specific index in a list.
/// </summary>
/// <param name="opts">The LIndexOptions containing instance, key, and index.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLIndex(LIndexOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var value = db.ListGetByIndex(opts.Key, opts.Index);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Index", "Value" }, new List<string[]> { new[] { opts.Key, opts.Index.ToString(), value.ToString() } });
else
Console.WriteLine(Output.Green(value.ToString()));
return 0;
}
/// <summary>
/// Executes the LSET command to set the element at a specific index in a list.
/// </summary>
/// <param name="opts">The LSetOptions containing instance, key, index, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLSet(LSetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.ListSetByIndex(opts.Key, opts.Index, opts.Value);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Index", "New Value", "Status" }, new List<string[]> { new[] { opts.Key, opts.Index.ToString(), opts.Value, "OK" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the LREM command to remove elements from a list.
/// </summary>
/// <param name="opts">The LRemOptions containing instance, key, count, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLRem(LRemOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.ListRemove(opts.Key, opts.Value, opts.Count);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Value", "Removed Count" }, new List<string[]> { new[] { opts.Key, opts.Value, count.ToString() } });
else
Console.WriteLine(Output.Green($"Removed {count} element(s)"));
return 0;
}
/// <summary>
/// Executes the LINSERT command to insert an element before or after a pivot in a list.
/// </summary>
/// <param name="opts">The LInsertOptions containing instance, key, position, pivot, and value.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLInsert(LInsertOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
long result;
if (opts.Position.ToUpper() == "BEFORE")
result = db.ListInsertBefore(opts.Key, opts.Pivot, opts.Value);
else
result = db.ListInsertAfter(opts.Key, opts.Pivot, opts.Value);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Position", "Pivot", "Value", "New Length" }, new List<string[]> { new[] { opts.Key, opts.Position, opts.Pivot, opts.Value, result.ToString() } });
else
Console.WriteLine(Output.Green($"New length: {result}"));
return 0;
}
/// <summary>
/// Executes the LTRIM command to trim a list to a specified range.
/// </summary>
/// <param name="opts">The LTrimOptions containing instance, key, start, and stop.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLTrim(LTrimOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.ListTrim(opts.Key, opts.Start, opts.Stop);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Start", "Stop", "Status" }, new List<string[]> { new[] { opts.Key, opts.Start.ToString(), opts.Stop.ToString(), "OK" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the BLPOP command to perform a blocking pop from the left of a list.
/// </summary>
/// <param name="opts">The BLPopOptions containing instance, keys, and timeout.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBLPop(BLPopOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = opts.Keys.Select(k => (RedisKey)k).Cast<object>().ToList();
args.Add(opts.Timeout);
var result = db.Execute("BLPOP", args.ToArray());
if (result == null)
{
Console.WriteLine(Output.Yellow("Timeout - no elements available to pop."));
return 0;
}
var arr = (RedisResult[])result;
if (arr.Length == 2)
{
var key = arr[0]?.ToString();
var value = arr[1]?.ToString();
if (opts.Table)
{
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { key ?? "", value ?? "" } });
}
else
{
Console.WriteLine(Output.Green($"{key}: {value}"));
}
}
else
{
Console.WriteLine(Output.Yellow("Unexpected result format."));
}
return 0;
}
/// <summary>
/// Executes the BRPOP command to perform a blocking pop from the right of a list.
/// </summary>
/// <param name="opts">The BRPopOptions containing instance, keys, and timeout.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBRPop(BRPopOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = opts.Keys.Select(k => (RedisKey)k).Cast<object>().ToList();
args.Add(opts.Timeout);
var result = db.Execute("BRPOP", args.ToArray());
if (result == null)
{
Console.WriteLine(Output.Yellow("Timeout - no elements available to pop."));
return 0;
}
var arr = (RedisResult[])result;
if (arr.Length == 2)
{
var key = arr[0]?.ToString();
var value = arr[1]?.ToString();
if (opts.Table)
{
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { key ?? "", value ?? "" } });
}
else
{
Console.WriteLine(Output.Green($"{key}: {value}"));
}
}
else
{
Console.WriteLine(Output.Yellow("Unexpected result format."));
}
return 0;
}
/// <summary>
/// Executes the RPOPLPUSH command to pop from the right of the source and push to the left of the destination list.
/// </summary>
/// <param name="opts">The RPopLPushOptions containing instance, source, and destination.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunRPopLPush(RPopLPushOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.ListRightPopLeftPush(opts.Source, opts.Destination);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Source", "Destination", "Popped Value" }, new List<string[]> { new[] { opts.Source, opts.Destination, result.ToString() } });
else
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
}
}

View File

@ -0,0 +1,99 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
[Verb("module-load", HelpText = "Load a Redis module.")]
public class ModuleLoadOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "path", Required = true, HelpText = "Module path.")]
public string Path { get; set; }
[Value(1, MetaName = "args", HelpText = "Module arguments.")]
public IEnumerable<string> Args { get; set; }
}
[Verb("module-unload", HelpText = "Unload a Redis module.")]
public class ModuleUnloadOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "name", Required = true, HelpText = "Module name.")]
public string Name { get; set; }
}
[Verb("module-list", HelpText = "List loaded Redis modules.")]
public class ModuleListOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
public static class ModuleCommands
{
public static int RunModuleLoad(ModuleLoadOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
try
{
var args = new List<object> { "LOAD", opts.Path };
if (opts.Args != null)
args.AddRange(opts.Args);
var result = db.Execute("MODULE", args.ToArray());
Console.WriteLine(Output.Green($"Module loaded: {result}"));
return 0;
}
catch (Exception ex)
{
Console.WriteLine(Output.Red($"Failed to load module: {ex.Message}"));
return 1;
}
}
public static int RunModuleUnload(ModuleUnloadOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
try
{
var result = db.Execute("MODULE", new object[] { "UNLOAD", opts.Name });
Console.WriteLine(Output.Green($"Module unloaded: {result}"));
return 0;
}
catch (Exception ex)
{
Console.WriteLine(Output.Red($"Failed to unload module: {ex.Message}"));
return 1;
}
}
public static int RunModuleList(ModuleListOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
try
{
var result = db.Execute("MODULE", new object[] { "LIST" });
Console.WriteLine(Output.Green($"Module list: {result}"));
return 0;
}
catch (Exception ex)
{
Console.WriteLine(Output.Red($"Failed to list modules: {ex.Message}"));
return 1;
}
}
}
}

119
Commands/PubSubCommands.cs Normal file
View File

@ -0,0 +1,119 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis Pub/Sub operations.
/// Provides functionality for PUBLISH, SUBSCRIBE, and UNSUBSCRIBE commands.
/// </summary>
[Verb("publish", HelpText = "Publish a message to a channel.")]
public class PublishOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "channel", Required = true, HelpText = "Channel name.")]
public string Channel { get; set; }
[Value(1, MetaName = "message", Required = true, HelpText = "Message to publish.")]
public string Message { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("subscribe", HelpText = "Subscribe to one or more channels.")]
public class SubscribeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "channels", Min = 1, Required = true, HelpText = "Channels to subscribe to.")]
public IEnumerable<string> Channels { get; set; }
}
[Verb("unsubscribe", HelpText = "Unsubscribe from one or more channels.")]
public class UnsubscribeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "channels", Min = 1, Required = true, HelpText = "Channels to unsubscribe from.")]
public IEnumerable<string> Channels { get; set; }
}
public static class PubSubCommands
{
/// <summary>
/// Executes the PUBLISH command to publish a message to a channel.
/// </summary>
/// <param name="opts">The PublishOptions containing instance, channel, and message.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunPublish(PublishOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var sub = redis.GetSubscriber();
var count = sub.Publish(opts.Channel, opts.Message);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Channel", "Receivers" }, new List<string[]> { new[] { opts.Channel, count.ToString() } });
else
Console.WriteLine(Output.Green($"Published to {opts.Channel}, receivers: {count}"));
return 0;
}
/// <summary>
/// Executes the SUBSCRIBE command to subscribe to one or more channels.
/// </summary>
/// <param name="opts">The SubscribeOptions containing instance and channels.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSubscribe(SubscribeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var sub = redis.GetSubscriber();
var channels = opts.Channels;
Console.WriteLine(Output.Yellow($"Subscribed to: {string.Join(", ", channels)}. Press Ctrl+C to exit."));
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); };
foreach (var channel in channels)
{
sub.Subscribe(channel, (ch, msg) =>
{
Console.WriteLine(Output.Cyan($"[{ch}] {msg}"));
});
}
try
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
}
}
catch (OperationCanceledException) { }
Console.WriteLine(Output.Yellow("Unsubscribed and exiting."));
return 0;
}
/// <summary>
/// Executes the UNSUBSCRIBE command to unsubscribe from one or more channels.
/// </summary>
/// <param name="opts">The UnsubscribeOptions containing instance and channels.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunUnsubscribe(UnsubscribeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var sub = redis.GetSubscriber();
foreach (var channel in opts.Channels)
{
sub.Unsubscribe(channel);
Console.WriteLine(Output.Yellow($"Unsubscribed from {channel}"));
}
return 0;
}
}
}

View File

@ -0,0 +1,156 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis scripting operations.
/// Provides functionality for EVAL, EVALSHA, SCRIPT LOAD, SCRIPT EXISTS, and SCRIPT FLUSH commands.
/// </summary>
[Verb("eval", HelpText = "Evaluate a Lua script.")]
public class EvalOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "script", Required = true, HelpText = "Lua script.")]
public string Script { get; set; }
[Option("keys", Required = false, HelpText = "Comma-separated keys.")]
public string Keys { get; set; }
[Option("args", Required = false, HelpText = "Comma-separated arguments.")]
public string Args { get; set; }
}
[Verb("evalsha", HelpText = "Evaluate a Lua script by its SHA1 hash.")]
public class EvalShaOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "sha1", Required = true, HelpText = "SHA1 hash.")]
public string Sha1 { get; set; }
[Option("keys", Required = false, HelpText = "Comma-separated keys.")]
public string Keys { get; set; }
[Option("args", Required = false, HelpText = "Comma-separated arguments.")]
public string Args { get; set; }
}
[Verb("scriptload", HelpText = "Load a Lua script into the script cache.")]
public class ScriptLoadOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "script", Required = true, HelpText = "Lua script.")]
public string Script { get; set; }
}
[Verb("scriptexists", HelpText = "Check if scripts exist in the script cache.")]
public class ScriptExistsOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "sha1s", Min = 1, Required = true, HelpText = "SHA1 hashes.")]
public IEnumerable<string> Sha1s { get; set; }
}
[Verb("scriptflush", HelpText = "Remove all scripts from the script cache.")]
public class ScriptFlushOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
public static class ScriptingCommands
{
/// <summary>
/// Executes the EVAL command to evaluate a Lua script.
/// </summary>
/// <param name="opts">The EvalOptions containing instance, script, keys, and arguments.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunEval(EvalOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = string.IsNullOrEmpty(opts.Keys) ? Array.Empty<RedisKey>() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray();
var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty<RedisValue>() : opts.Args.Split(',').Select(a => (RedisValue)a.Trim()).ToArray();
var result = db.ScriptEvaluate(opts.Script, keys, args);
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the EVALSHA command to evaluate a Lua script by its SHA1 hash.
/// </summary>
/// <param name="opts">The EvalShaOptions containing instance, sha1, keys, and arguments.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunEvalSha(EvalShaOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = string.IsNullOrEmpty(opts.Keys) ? Array.Empty<RedisKey>() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray();
var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty<RedisValue>() : opts.Args.Split(',').Select(a => (RedisValue)a.Trim()).ToArray();
var result = db.ScriptEvaluate(opts.Sha1, keys, args);
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the SCRIPT LOAD command to load a Lua script into the script cache.
/// </summary>
/// <param name="opts">The ScriptLoadOptions containing instance and script.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunScriptLoad(ScriptLoadOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var server = redis.GetServer(redis.GetEndPoints().First());
var sha1Bytes = server.ScriptLoad(opts.Script);
// Convert byte[] to hex string
var sha1 = BitConverter.ToString(sha1Bytes).Replace("-", string.Empty).ToLower();
Console.WriteLine(Output.Green(sha1));
return 0;
}
/// <summary>
/// Executes the SCRIPT EXISTS command to check if scripts exist in the script cache.
/// </summary>
/// <param name="opts">The ScriptExistsOptions containing instance and SHA1 hashes.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunScriptExists(ScriptExistsOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var server = redis.GetServer(redis.GetEndPoints().First());
foreach (var sha in opts.Sha1s)
{
var exists = server.ScriptExists(sha);
Console.WriteLine(Output.Green($"{sha}: {(exists ? "YES" : "NO")}"));
}
return 0;
}
/// <summary>
/// Executes the SCRIPT FLUSH command to remove all scripts from the script cache.
/// </summary>
/// <param name="opts">The ScriptFlushOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunScriptFlush(ScriptFlushOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var server = redis.GetServer(redis.GetEndPoints().First());
server.ScriptFlush();
Console.WriteLine(Output.Green("Script cache flushed."));
return 0;
}
}
}

296
Commands/ServerCommands.cs Normal file
View File

@ -0,0 +1,296 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis server operations.
/// Provides functionality for INFO, CONFIG, SAVE, BGSAVE, LASTSAVE, TIME, and PING commands.
/// </summary>
[Verb("info", HelpText = "Get information and statistics about the server.")]
public class InfoOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("section", Required = false, HelpText = "Info section (server, clients, memory, etc.).")]
public string Section { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("config", HelpText = "Get/set Redis configuration parameters.")]
public class ConfigOptions
{
[Option("list-custom", Required = false, HelpText = "List all custom config parameters for this instance.")]
public bool ListCustom { get; set; }
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("get", Required = false, HelpText = "Get configuration parameter.")]
public string Get { get; set; }
[Option("set", Required = false, HelpText = "Set configuration parameter (format: parameter value).")]
public string Set { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("save", HelpText = "Synchronously save the dataset to disk.")]
public class SaveOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("bgsave", HelpText = "Asynchronously save the dataset to disk.")]
public class BgSaveOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("lastsave", HelpText = "Get the timestamp of the last successful save.")]
public class LastSaveOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("time", HelpText = "Return the current server time.")]
public class TimeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("ping", HelpText = "Ping the server.")]
public class PingOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class ServerCommands
{
/// <summary>
/// Executes the INFO command to get information and statistics about the server.
/// </summary>
/// <param name="opts">The InfoOptions containing instance and section.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunInfo(InfoOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var info = db.Execute("INFO", string.IsNullOrEmpty(opts.Section) ? null : opts.Section).ToString();
if (opts.Table)
{
var lines = info.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var rows = lines.Select(l => l.Split(':', 2)).Where(parts => parts.Length == 2)
.Select(parts => new[] { parts[0], parts[1] }).ToList();
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, rows);
}
else
{
Console.WriteLine(Output.Green(info));
}
return 0;
}
/// <summary>
/// Executes the CONFIG command to get or set Redis configuration parameters.
/// </summary>
/// <param name="opts">The ConfigOptions containing instance, get, set, and table options.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunConfig(ConfigOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.ListCustom)
{
if (instance.CustomConfig != null && instance.CustomConfig.Count > 0)
{
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, instance.CustomConfig.Select(kv => new[] { kv.Key, kv.Value }).ToList());
}
else
{
Console.WriteLine(Output.Yellow("No custom config parameters set for this instance."));
}
return 0;
}
if (!string.IsNullOrEmpty(opts.Get))
{
var result = db.Execute("CONFIG", "GET", opts.Get);
if (opts.Table)
{
var arr = (RedisResult[])result;
var rows = new List<string[]>();
for (int i = 0; i < arr.Length; i += 2)
{
if (i + 1 < arr.Length)
rows.Add(new[] { arr[i].ToString(), arr[i + 1].ToString() });
}
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, rows);
}
else
{
var arr = (RedisResult[])result;
for (int i = 0; i < arr.Length; i += 2)
{
if (i + 1 < arr.Length)
Console.WriteLine(Output.Green($"{arr[i]} {arr[i + 1]}"));
}
}
}
else if (!string.IsNullOrEmpty(opts.Set))
{
var parts = opts.Set.Split(' ', 2);
if (parts.Length == 2)
{
db.Execute("CONFIG", "SET", parts[0], parts[1]);
// Persist custom config in InstanceConfig and save config file
var instanceConfig = config.Instances.FirstOrDefault(i => i.Name == opts.Instance);
if (instanceConfig != null)
{
instanceConfig.CustomConfig[parts[0]] = parts[1];
RedisManager.ConfigManager.SaveConfig(config);
}
if (opts.Table)
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "CONFIG SET", "OK" } });
else
Console.WriteLine(Output.Green("OK"));
}
else
{
Console.WriteLine(Output.Red("Invalid format. Use: parameter value"));
return 1;
}
}
else
{
Console.WriteLine(Output.Red("Please specify --get or --set"));
return 1;
}
return 0;
}
/// <summary>
/// Executes the SAVE command to synchronously save the dataset to disk.
/// </summary>
/// <param name="opts">The SaveOptions containing instance and table option.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSave(SaveOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("SAVE");
if (opts.Table)
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "SAVE", "OK" } });
else
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the BGSAVE command to asynchronously save the dataset to disk.
/// </summary>
/// <param name="opts">The BgSaveOptions containing instance and table option.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunBgSave(BgSaveOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("BGSAVE");
if (opts.Table)
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "BGSAVE", "Background save started" } });
else
Console.WriteLine(Output.Green("Background save started"));
return 0;
}
/// <summary>
/// Executes the LASTSAVE command to get the timestamp of the last successful save.
/// </summary>
/// <param name="opts">The LastSaveOptions containing instance and table option.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunLastSave(LastSaveOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var timestamp = db.Execute("LASTSAVE").ToString();
var dateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp));
if (opts.Table)
RedisUtils.PrintTable(new[] { "Last Save", "Timestamp", "DateTime" }, new List<string[]> { new[] { "Last Save", timestamp, dateTime.ToString() } });
else
Console.WriteLine(Output.Green($"{timestamp} ({dateTime})"));
return 0;
}
/// <summary>
/// Executes the TIME command to return the current server time.
/// </summary>
/// <param name="opts">The TimeOptions containing instance and table option.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunTime(TimeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("TIME");
var arr = (RedisResult[])result;
var timestamp = arr[0].ToString();
var microseconds = arr[1].ToString();
var dateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp));
if (opts.Table)
RedisUtils.PrintTable(new[] { "Timestamp", "Microseconds", "DateTime" }, new List<string[]> { new[] { timestamp, microseconds, dateTime.ToString() } });
else
Console.WriteLine(Output.Green($"{timestamp} {microseconds} ({dateTime})"));
return 0;
}
/// <summary>
/// Executes the PING command to ping the server.
/// </summary>
/// <param name="opts">The PingOptions containing instance and table option.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunPing(PingOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var response = db.Ping();
if (opts.Table)
RedisUtils.PrintTable(new[] { "Ping", "Response" }, new List<string[]> { new[] { "PING", response.ToString() } });
else
Console.WriteLine(Output.Green(response.ToString()));
return 0;
}
}
}

573
Commands/SetCommands.cs Normal file
View File

@ -0,0 +1,573 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
using RedisManager.Commands;
namespace RedisManager.Commands
{
[Verb("sadd", HelpText = "Add members to a set.")]
public class SAddOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to add.")]
public IEnumerable<string> Members { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("srem", HelpText = "Remove members from a set.")]
public class SRemOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to remove.")]
public IEnumerable<string> Members { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("smembers", HelpText = "Get all members of a set.")]
public class SMembersOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("scard", HelpText = "Get the number of members in a set.")]
public class SCardOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sismember", HelpText = "Check if a member exists in a set.")]
public class SIsMemberOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Value(1, MetaName = "member", Required = true, HelpText = "Member to check.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("srandmember", HelpText = "Get random member(s) from a set.")]
public class SRandMemberOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Option("count", Required = false, HelpText = "Number of random members to get.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("spop", HelpText = "Remove and return random member(s) from a set.")]
public class SPopOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Option("count", Required = false, HelpText = "Number of members to pop.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sinter", HelpText = "Get intersection of multiple sets.")]
public class SInterOptions : InstanceOptions
{
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sunion", HelpText = "Get union of multiple sets.")]
public class SUnionOptions : InstanceOptions
{
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sdiff", HelpText = "Get difference of multiple sets.")]
public class SDiffOptions : InstanceOptions
{
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sinterstore", HelpText = "Store intersection of multiple sets.")]
public class SInterStoreOptions : InstanceOptions
{
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
public string Destination { get; set; }
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sunionstore", HelpText = "Store union of multiple sets.")]
public class SUnionStoreOptions : InstanceOptions
{
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
public string Destination { get; set; }
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sdiffstore", HelpText = "Store difference of multiple sets.")]
public class SDiffStoreOptions : InstanceOptions
{
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
public string Destination { get; set; }
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("sscan", HelpText = "Scan set members.")]
public class SScanOptions : InstanceOptions
{
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
public string Key { get; set; }
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
public int Cursor { get; set; }
[Option("match", Required = false, HelpText = "Pattern to match.")]
public string Match { get; set; }
[Option("count", Required = false, HelpText = "Count hint.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("smove", HelpText = "Move member from one set to another.")]
public class SMoveOptions : InstanceOptions
{
[Value(0, MetaName = "source", Required = true, HelpText = "Source set key.")]
public string Source { get; set; }
[Value(1, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
public string Destination { get; set; }
[Value(2, MetaName = "member", Required = true, HelpText = "Member to move.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class SetCommands
{
/// <summary>
/// Executes the SADD command to add members to a set.
/// </summary>
/// <param name="opts">The SAddOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSAdd(SAddOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var members = opts.Members.ToArray();
var count = db.SetAdd(opts.Key, members.Select(m => (RedisValue)m).ToArray());
if (opts.Table)
{
var rows = members.Select(m => new[] { m, "Added" }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
}
else
Console.WriteLine(Output.Green($"Added {count} new member(s)"));
return 0;
}
/// <summary>
/// Executes the SREM command to remove members from a set.
/// </summary>
/// <param name="opts">The SRemOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSRem(SRemOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var members = opts.Members.ToArray();
var count = db.SetRemove(opts.Key, members.Select(m => (RedisValue)m).ToArray());
if (opts.Table)
{
var rows = members.Select(m => new[] { m, "Removed" }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
}
else
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
return 0;
}
/// <summary>
/// Executes the SMEMBERS command to get all members of a set.
/// </summary>
/// <param name="opts">The SMembersOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSMembers(SMembersOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var members = db.SetMembers(opts.Key);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
return 0;
}
/// <summary>
/// Executes the SCARD command to get the number of members in a set.
/// </summary>
/// <param name="opts">The SCardOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSCard(SCardOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SetLength(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
else
Console.WriteLine(Output.Green(count.ToString()));
return 0;
}
/// <summary>
/// Executes the SISMEMBER command to check if a member exists in a set.
/// </summary>
/// <param name="opts">The SIsMemberOptions containing instance, key, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSIsMember(SIsMemberOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var exists = db.SetContains(opts.Key, opts.Member);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Member", "Exists" }, new List<string[]> { new[] { opts.Key, opts.Member, exists.ToString() } });
else
Console.WriteLine(Output.Green(exists.ToString()));
return 0;
}
/// <summary>
/// Executes the SRANDMEMBER command to get random member(s) from a set.
/// </summary>
/// <param name="opts">The SRandMemberOptions containing instance, key, and count.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSRandMember(SRandMemberOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.Count.HasValue)
{
var members = db.SetRandomMembers(opts.Key, opts.Count.Value);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Random Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
else
{
var member = db.SetRandomMember(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Random Member" }, new List<string[]> { new[] { opts.Key, member.ToString() } });
else
Console.WriteLine(Output.Green(member.ToString()));
}
return 0;
}
/// <summary>
/// Executes the SPOP command to remove and return random member(s) from a set.
/// </summary>
/// <param name="opts">The SPopOptions containing instance, key, and count.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSPop(SPopOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.Count.HasValue)
{
var members = db.SetPop(opts.Key, opts.Count.Value);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Popped Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
else
{
var member = db.SetPop(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Popped Member" }, new List<string[]> { new[] { opts.Key, member.ToString() } });
else
Console.WriteLine(Output.Green(member.ToString()));
}
return 0;
}
/// <summary>
/// Executes the SINTER command to get the intersection of multiple sets.
/// </summary>
/// <param name="opts">The SInterOptions containing instance and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSInter(SInterOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var members = db.SetCombine(SetOperation.Intersect, keys);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Intersection Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
return 0;
}
/// <summary>
/// Executes the SUNION command to get the union of multiple sets.
/// </summary>
/// <param name="opts">The SUnionOptions containing instance and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSUnion(SUnionOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var members = db.SetCombine(SetOperation.Union, keys);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Union Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
return 0;
}
/// <summary>
/// Executes the SDIFF command to get the difference of multiple sets.
/// </summary>
/// <param name="opts">The SDiffOptions containing instance and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSDiff(SDiffOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var members = db.SetCombine(SetOperation.Difference, keys);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Difference Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
return 0;
}
/// <summary>
/// Executes the SINTERSTORE command to store the intersection of multiple sets.
/// </summary>
/// <param name="opts">The SInterStoreOptions containing instance, destination, and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSInterStore(SInterStoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.SetCombineAndStore(SetOperation.Intersect, opts.Destination, keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
else
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
return 0;
}
/// <summary>
/// Executes the SUNIONSTORE command to store the union of multiple sets.
/// </summary>
/// <param name="opts">The SUnionStoreOptions containing instance, destination, and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSUnionStore(SUnionStoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.SetCombineAndStore(SetOperation.Union, opts.Destination, keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
else
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
return 0;
}
/// <summary>
/// Executes the SDIFFSTORE command to store the difference of multiple sets.
/// </summary>
/// <param name="opts">The SDiffStoreOptions containing instance, destination, and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSDiffStore(SDiffStoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.SetCombineAndStore(SetOperation.Difference, opts.Destination, keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
else
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
return 0;
}
/// <summary>
/// Executes the SSCAN command to scan set members.
/// </summary>
/// <param name="opts">The SScanOptions containing instance, key, cursor, and scan options.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSScan(SScanOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var args = new List<object> { opts.Cursor };
if (!string.IsNullOrEmpty(opts.Match))
{
args.Add("MATCH");
args.Add(opts.Match);
}
if (opts.Count.HasValue)
{
args.Add("COUNT");
args.Add(opts.Count.Value);
}
var result = db.Execute("SSCAN", opts.Key, args.ToArray());
var arr = (RedisResult[])result;
var newCursor = (int)arr[0];
var members = (RedisResult[])arr[1];
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
RedisUtils.PrintTable(new[] { "Next Cursor" }, new List<string[]> { new[] { newCursor.ToString() } });
}
else
{
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
Console.WriteLine(Output.Yellow($"Next cursor: {newCursor}"));
}
return 0;
}
/// <summary>
/// Executes the SMOVE command to move a member from one set to another.
/// </summary>
/// <param name="opts">The SMoveOptions containing instance, source, destination, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunSMove(SMoveOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var moved = db.SetMove(opts.Source, opts.Destination, opts.Member);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Source", "Destination", "Member", "Moved" }, new List<string[]> { new[] { opts.Source, opts.Destination, opts.Member, moved.ToString() } });
else
Console.WriteLine(Output.Green(moved.ToString()));
return 0;
}
}
}

View File

@ -0,0 +1,836 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis sorted set operations.
/// Provides functionality for ZADD, ZREM, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZCARD, ZSCORE, ZRANK, ZREVRANK, ZINCRBY, ZREVRANGEBYSCORE, ZCOUNT, ZUNIONSTORE, ZINTERSTORE, ZSCAN, ZPOPMAX, ZPOPMIN, ZREMRANGEBYRANK, and ZREMRANGEBYSCORE commands.
/// </summary>
[Verb("zadd", HelpText = "Add members to a sorted set.")]
public class ZAddOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "score", Required = true, HelpText = "Score for the member.")]
public double Score { get; set; }
[Value(2, MetaName = "member", Required = true, HelpText = "Member to add.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrem", HelpText = "Remove members from a sorted set.")]
public class ZRemOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to remove.")]
public IEnumerable<string> Members { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrange", HelpText = "Get range of members by rank.")]
public class ZRangeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
public long Start { get; set; }
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
public long Stop { get; set; }
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
public bool WithScores { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrevrange", HelpText = "Get range of members by rank (reverse order).")]
public class ZRevRangeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
public long Start { get; set; }
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
public long Stop { get; set; }
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
public bool WithScores { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrangebyscore", HelpText = "Get range of members by score.")]
public class ZRangeByScoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
public double Min { get; set; }
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
public double Max { get; set; }
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
public bool WithScores { get; set; }
[Option("limit", Required = false, HelpText = "Limit results (offset count).")]
public string Limit { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zcard", HelpText = "Get the number of members in a sorted set.")]
public class ZCardOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zscore", HelpText = "Get the score of a member.")]
public class ZScoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrank", HelpText = "Get the rank of a member.")]
public class ZRankOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrevrank", HelpText = "Get the reverse rank of a member.")]
public class ZRevRankOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zincrby", HelpText = "Increment the score of a member.")]
public class ZIncrByOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "increment", Required = true, HelpText = "Score increment.")]
public double Increment { get; set; }
[Value(2, MetaName = "member", Required = true, HelpText = "Member name.")]
public string Member { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zrevrangebyscore", HelpText = "Get range of members by score (reverse order).")]
public class ZRevRangeByScoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "max", Required = true, HelpText = "Maximum score.")]
public double Max { get; set; }
[Value(2, MetaName = "min", Required = true, HelpText = "Minimum score.")]
public double Min { get; set; }
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
public bool WithScores { get; set; }
[Option("limit", Required = false, HelpText = "Limit results (offset count).")]
public string Limit { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zcount", HelpText = "Count members within a score range.")]
public class ZCountOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
public double Min { get; set; }
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
public double Max { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zunionstore", HelpText = "Store union of multiple sorted sets.")]
public class ZUnionStoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination sorted set key.")]
public string Destination { get; set; }
[Value(1, MetaName = "numkeys", Required = true, HelpText = "Number of source keys.")]
public int NumKeys { get; set; }
[Value(2, MetaName = "keys", Min = 1, Required = true, HelpText = "Source sorted set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("weights", Required = false, HelpText = "Weights for each source set.")]
public IEnumerable<double> Weights { get; set; }
[Option("aggregate", Required = false, HelpText = "Aggregation method (sum, min, max).")]
public string Aggregate { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zinterstore", HelpText = "Store intersection of multiple sorted sets.")]
public class ZInterStoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination sorted set key.")]
public string Destination { get; set; }
[Value(1, MetaName = "numkeys", Required = true, HelpText = "Number of source keys.")]
public int NumKeys { get; set; }
[Value(2, MetaName = "keys", Min = 1, Required = true, HelpText = "Source sorted set keys.")]
public IEnumerable<string> Keys { get; set; }
[Option("weights", Required = false, HelpText = "Weights for each source set.")]
public IEnumerable<double> Weights { get; set; }
[Option("aggregate", Required = false, HelpText = "Aggregation method (sum, min, max).")]
public string Aggregate { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zscan", HelpText = "Scan sorted set members.")]
public class ZScanOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
public int Cursor { get; set; }
[Option("match", Required = false, HelpText = "Pattern to match.")]
public string Match { get; set; }
[Option("count", Required = false, HelpText = "Count hint.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zpopmax", HelpText = "Remove and return members with highest scores.")]
public class ZPopMaxOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Option("count", Required = false, HelpText = "Number of members to pop.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zpopmin", HelpText = "Remove and return members with lowest scores.")]
public class ZPopMinOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Option("count", Required = false, HelpText = "Number of members to pop.")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zremrangebyrank", HelpText = "Remove members by rank range.")]
public class ZRemRangeByRankOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
public long Start { get; set; }
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
public long Stop { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("zremrangebyscore", HelpText = "Remove members by score range.")]
public class ZRemRangeByScoreOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
public string Key { get; set; }
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
public double Min { get; set; }
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
public double Max { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class SortedSetCommands
{
/// <summary>
/// Executes the ZADD command to add members to a sorted set.
/// </summary>
/// <param name="opts">The ZAddOptions containing instance, key, score, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZAdd(ZAddOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SortedSetAdd(opts.Key, opts.Member, opts.Score);
if (opts.Table)
{
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
}
else
Console.WriteLine(Output.Green($"Added {count} new member(s)"));
return 0;
}
/// <summary>
/// Executes the ZREM command to remove members from a sorted set.
/// </summary>
/// <param name="opts">The ZRemOptions containing instance, key, and members.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRem(ZRemOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var members = opts.Members.ToArray();
var count = db.SortedSetRemove(opts.Key, members.Select(m => (RedisValue)m).ToArray());
if (opts.Table)
{
var rows = members.Select(m => new[] { m, "Removed" }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
}
else
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
return 0;
}
/// <summary>
/// Executes the ZRANGE command to get a range of members by rank.
/// </summary>
/// <param name="opts">The ZRangeOptions containing instance, key, start, and stop.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRange(ZRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.WithScores)
{
var entries = db.SortedSetRangeByRankWithScores(opts.Key, opts.Start, opts.Stop, Order.Ascending);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
}
else
{
foreach (var e in entries)
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
}
}
else
{
var members = db.SortedSetRangeByRank(opts.Key, opts.Start, opts.Stop, Order.Ascending);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
}
else
{
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
}
return 0;
}
/// <summary>
/// Executes the ZREVRANGE command to get a range of members by rank in reverse order.
/// </summary>
/// <param name="opts">The ZRevRangeOptions containing instance, key, start, and stop.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRevRange(ZRevRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.WithScores)
{
var entries = db.SortedSetRangeByRankWithScores(opts.Key, opts.Start, opts.Stop, Order.Descending);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
}
else
{
foreach (var e in entries)
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
}
}
else
{
var members = db.SortedSetRangeByRank(opts.Key, opts.Start, opts.Stop, Order.Descending);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
}
else
{
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
}
return 0;
}
/// <summary>
/// Executes the ZRANGEBYSCORE command to get a range of members by score.
/// </summary>
/// <param name="opts">The ZRangeByScoreOptions containing instance, key, min, and max.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRangeByScore(ZRangeByScoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.WithScores)
{
var entries = db.SortedSetRangeByScoreWithScores(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Ascending);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
}
else
{
foreach (var e in entries)
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
}
}
else
{
var members = db.SortedSetRangeByScore(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Ascending);
if (opts.Table)
{
var rows = members.Select(m => new[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
}
else
{
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
}
return 0;
}
/// <summary>
/// Executes the ZCARD command to get the number of members in a sorted set.
/// </summary>
/// <param name="opts">The ZCardOptions containing instance and key.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZCard(ZCardOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SortedSetLength(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
else
Console.WriteLine(Output.Green(count.ToString()));
return 0;
}
/// <summary>
/// Executes the ZSCORE command to get the score of a member in a sorted set.
/// </summary>
/// <param name="opts">The ZScoreOptions containing instance, key, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZScore(ZScoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var score = db.SortedSetScore(opts.Key, opts.Member);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Member", "Score" }, new List<string[]> { new[] { opts.Key, opts.Member, score?.ToString() ?? "[null]" } });
else if (score.HasValue)
Console.WriteLine(Output.Green(score.Value.ToString()));
else
Console.WriteLine(Output.Yellow("[null]"));
return 0;
}
/// <summary>
/// Executes the ZRANK command to get the rank of a member in a sorted set.
/// </summary>
/// <param name="opts">The ZRankOptions containing instance, key, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRank(ZRankOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var rank = db.SortedSetRank(opts.Key, opts.Member);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Member", "Rank" }, new List<string[]> { new[] { opts.Key, opts.Member, rank?.ToString() ?? "[null]" } });
else if (rank.HasValue)
Console.WriteLine(Output.Green(rank.Value.ToString()));
else
Console.WriteLine(Output.Yellow("[null]"));
return 0;
}
/// <summary>
/// Executes the ZREVRANK command to get the reverse rank of a member in a sorted set.
/// </summary>
/// <param name="opts">The ZRevRankOptions containing instance, key, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRevRank(ZRevRankOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var rank = db.SortedSetRank(opts.Key, opts.Member, Order.Descending);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Member", "Reverse Rank" }, new List<string[]> { new[] { opts.Key, opts.Member, rank?.ToString() ?? "[null]" } });
else if (rank.HasValue)
Console.WriteLine(Output.Green(rank.Value.ToString()));
else
Console.WriteLine(Output.Yellow("[null]"));
return 0;
}
/// <summary>
/// Executes the ZINCRBY command to increment the score of a member in a sorted set.
/// </summary>
/// <param name="opts">The ZIncrByOptions containing instance, key, increment, and member.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZIncrBy(ZIncrByOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var newScore = db.SortedSetIncrement(opts.Key, opts.Member, opts.Increment);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Member", "New Score" }, new List<string[]> { new[] { opts.Key, opts.Member, newScore.ToString() } });
else
Console.WriteLine(Output.Green(newScore.ToString()));
return 0;
}
/// <summary>
/// Executes the ZREVRANGEBYSCORE command to get a range of members by score in reverse order.
/// </summary>
/// <param name="opts">The ZRevRangeByScoreOptions containing instance, key, max, and min.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRevRangeByScore(ZRevRangeByScoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.WithScores)
{
var entries = db.SortedSetRangeByScoreWithScores(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Descending);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
}
else
foreach (var entry in entries)
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
}
else
{
var members = db.SortedSetRangeByScore(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Descending);
if (opts.Table)
{
var rows = members.Select(m => new string[] { m.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member" }, rows);
}
else
foreach (var member in members)
Console.WriteLine(Output.Green(member.ToString()));
}
return 0;
}
/// <summary>
/// Executes the ZCOUNT command to count members within a score range in a sorted set.
/// </summary>
/// <param name="opts">The ZCountOptions containing instance, key, min, and max.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZCount(ZCountOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SortedSetLengthByValue(opts.Key, opts.Min, opts.Max);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
else
Console.WriteLine(Output.Green(count.ToString()));
return 0;
}
/// <summary>
/// Executes the ZUNIONSTORE command to store the union of multiple sorted sets.
/// </summary>
/// <param name="opts">The ZUnionStoreOptions containing instance, destination, numkeys, and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZUnionStore(ZUnionStoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.SortedSetCombineAndStore(SetOperation.Union, opts.Destination, keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
else
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
return 0;
}
/// <summary>
/// Executes the ZINTERSTORE command to store the intersection of multiple sorted sets.
/// </summary>
/// <param name="opts">The ZInterStoreOptions containing instance, destination, numkeys, and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZInterStore(ZInterStoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
var count = db.SortedSetCombineAndStore(SetOperation.Intersect, opts.Destination, keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
else
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
return 0;
}
/// <summary>
/// Executes the ZSCAN command to scan sorted set members.
/// </summary>
/// <param name="opts">The ZScanOptions containing instance, key, cursor, and scan options.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZScan(ZScanOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.SortedSetScan(opts.Key, opts.Match ?? "*", pageSize: opts.Count ?? 10, cursor: opts.Cursor);
if (opts.Table)
{
var rows = result.Select(e => new[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
}
else
{
foreach (var entry in result)
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
}
return 0;
}
/// <summary>
/// Executes the ZPOPMAX command to remove and return members with the highest scores.
/// </summary>
/// <param name="opts">The ZPopMaxOptions containing instance, key, and count.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZPopMax(ZPopMaxOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.Count.HasValue)
{
var entries = db.SortedSetPop(opts.Key, opts.Count.Value);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, rows);
}
else
foreach (var entry in entries)
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
}
else
{
var entry = db.SortedSetPop(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, new List<string[]> { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } });
else
Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}"));
}
return 0;
}
/// <summary>
/// Executes the ZPOPMIN command to remove and return members with the lowest scores.
/// </summary>
/// <param name="opts">The ZPopMinOptions containing instance, key, and count.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZPopMin(ZPopMinOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
if (opts.Count.HasValue)
{
var entries = db.SortedSetPop(opts.Key, opts.Count.Value);
if (opts.Table)
{
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, rows);
}
else
foreach (var entry in entries)
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
}
else
{
var entry = db.SortedSetPop(opts.Key);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, new List<string[]> { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } });
else
Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}"));
}
return 0;
}
/// <summary>
/// Executes the ZREMRANGEBYRANK command to remove members by rank range.
/// </summary>
/// <param name="opts">The ZRemRangeByRankOptions containing instance, key, start, and stop.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRemRangeByRank(ZRemRangeByRankOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SortedSetRemoveRangeByRank(opts.Key, opts.Start, opts.Stop);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
else
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
return 0;
}
/// <summary>
/// Executes the ZREMRANGEBYSCORE command to remove members by score range.
/// </summary>
/// <param name="opts">The ZRemRangeByScoreOptions containing instance, key, min, and max.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunZRemRangeByScore(ZRemRangeByScoreOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var count = db.SortedSetRemoveRangeByScore(opts.Key, opts.Min, opts.Max);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
else
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
return 0;
}
}
}

46
Commands/StatusCommand.cs Normal file
View File

@ -0,0 +1,46 @@
using CommandLine;
using StackExchange.Redis;
using System;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Command line options for the status command.
/// Used to check the connectivity and response time of a Redis instance.
/// </summary>
[Verb("status", HelpText = "Check status of a Redis instance.")]
public class StatusOptions
{
/// <summary>
/// Gets or sets the name of the Redis instance to check.
/// This must match a configured instance name in the configuration file.
/// </summary>
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
/// <summary>
/// Static class containing the implementation for the status command.
/// Provides functionality to check Redis instance connectivity and response time.
/// </summary>
public static class StatusCommand
{
/// <summary>
/// Executes the status command to check Redis instance connectivity.
/// Connects to the specified Redis instance and measures the ping response time.
/// </summary>
/// <param name="opts">The status command options containing the instance name</param>
/// <param name="config">The RedisManager configuration containing instance details</param>
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
public static int RunStatus(StatusOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var pong = db.Ping();
Console.WriteLine(Output.Green($"PONG: {pong.TotalMilliseconds}ms"));
return 0;
}
}
}

154
Commands/StreamCommands.cs Normal file
View File

@ -0,0 +1,154 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
[Verb("xadd", HelpText = "Add an entry to a stream.")]
public class XAddOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
public string Stream { get; set; }
[Option("id", Required = false, HelpText = "Entry ID (default: *)")]
public string Id { get; set; } = "*";
[Option("fields", Required = true, HelpText = "Field-value pairs as field1:value1,field2:value2...")]
public IEnumerable<string> Fields { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("xrange", HelpText = "Get entries from a stream within a range.")]
public class XRangeOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
public string Stream { get; set; }
[Option("start", Required = false, HelpText = "Start ID (default: -)")]
public string Start { get; set; } = "-";
[Option("end", Required = false, HelpText = "End ID (default: +)")]
public string End { get; set; } = "+";
[Option("count", Required = false, HelpText = "Maximum number of entries")]
public int? Count { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("xlen", HelpText = "Get the length of a stream.")]
public class XLenOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
public string Stream { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
[Verb("xdel", HelpText = "Remove entries from a stream.")]
public class XDelOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
public string Stream { get; set; }
[Value(1, MetaName = "ids", Min = 1, Required = true, HelpText = "Entry IDs to delete.")]
public IEnumerable<string> Ids { get; set; }
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
public static class StreamCommands
{
public static int RunXAdd(XAddOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
// Use XADD command directly
var args = new List<RedisValue> { opts.Stream, opts.Id };
foreach (var field in opts.Fields)
{
var parts = field.Split(':', 2);
if (parts.Length == 2)
{
args.Add(parts[0]);
args.Add(parts[1]);
}
}
var id = db.Execute("XADD", args.ToArray());
if (opts.Table)
RedisUtils.PrintTable(new[] { "Stream", "ID", "Fields" }, new List<string[]> { new[] { opts.Stream, id.ToString(), string.Join(", ", opts.Fields) } });
else
Console.WriteLine(Output.Green(id.ToString()));
return 0;
}
public static int RunXRange(XRangeOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
// Use XRANGE command directly
var args = new List<RedisValue> { opts.Stream, opts.Start, opts.End };
if (opts.Count.HasValue)
{
args.Add("COUNT");
args.Add(opts.Count.Value);
}
var result = db.Execute("XRANGE", args.ToArray());
if (opts.Table)
{
Console.WriteLine(Output.Yellow("Table output not supported for XRANGE yet"));
}
else
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
public static int RunXLen(XLenOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
// Use XLEN command directly
var length = db.Execute("XLEN", opts.Stream);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Stream", "Length" }, new List<string[]> { new[] { opts.Stream, length.ToString() } });
else
Console.WriteLine(Output.Green(length.ToString()));
return 0;
}
public static int RunXDel(XDelOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
// Use XDEL command directly
var args = new List<RedisValue> { opts.Stream };
args.AddRange(opts.Ids.Select(id => (RedisValue)id));
var count = db.Execute("XDEL", args.ToArray());
if (opts.Table)
RedisUtils.PrintTable(new[] { "Stream", "Deleted Count" }, new List<string[]> { new[] { opts.Stream, count.ToString() } });
else
Console.WriteLine(Output.Green($"Deleted {count} entry(ies)"));
return 0;
}
}
}

201
Commands/StringCommands.cs Normal file
View File

@ -0,0 +1,201 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using RedisManager.Utils;
using RedisManager.Commands;
namespace RedisManager.Commands
{
/// <summary>
/// Command line options for the GET command.
/// Used to retrieve the value of a Redis key.
/// </summary>
[Verb("get", HelpText = "Get value of a key.")]
public class GetOptions : InstanceOptions
{
/// <summary>
/// Gets or sets the key name to retrieve the value for.
/// </summary>
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
/// <summary>
/// Gets or sets whether to output the result in table format.
/// </summary>
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
/// <summary>
/// Command line options for the SET command.
/// Used to set the value of a Redis key with optional TTL.
/// </summary>
[Verb("set", HelpText = "Set value of a key.")]
public class SetOptions : InstanceOptions
{
/// <summary>
/// Gets or sets the key name to set the value for.
/// </summary>
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
public string Key { get; set; }
/// <summary>
/// Gets or sets the value to store in the key.
/// </summary>
[Value(1, MetaName = "value", Required = true, HelpText = "Value.")]
public string Value { get; set; }
/// <summary>
/// Gets or sets the time to live in seconds for the key.
/// </summary>
[Option("ttl", Required = false, HelpText = "Time to live in seconds.")]
public int? TTL { get; set; }
}
/// <summary>
/// Command line options for the DEL command.
/// Used to delete one or more Redis keys with optional confirmation.
/// </summary>
[Verb("del", HelpText = "Delete one or more keys.")]
public class DelOptions : InstanceOptions
{
/// <summary>
/// Gets or sets whether to skip the confirmation prompt.
/// </summary>
[Option("yes", Required = false, HelpText = "Skip confirmation.")]
public bool Yes { get; set; }
/// <summary>
/// Gets or sets the collection of keys to delete.
/// </summary>
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Key(s) to delete.")]
public IEnumerable<string> Keys { get; set; }
/// <summary>
/// Gets or sets whether to output the result in table format.
/// </summary>
[Option("table", Required = false, HelpText = "Output as table.")]
public bool Table { get; set; }
}
/// <summary>
/// Static class containing implementations for basic Redis string operations.
/// Provides functionality for GET, SET, and DEL commands with various output formats.
/// </summary>
public static class StringCommands
{
/// <summary>
/// Executes the GET command to retrieve a value from Redis.
/// Supports table output format and JSON pretty-printing.
/// </summary>
/// <param name="opts">The GET command options</param>
/// <param name="config">The RedisManager configuration</param>
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
public static int RunGet(GetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var value = db.StringGet(opts.Key);
if (opts.Table)
{
RedisUtils.PrintTable(new[] { "Key", "Value" }, new List<string[]> { new[] { opts.Key, value.IsNull ? "[null]" : value.ToString() } });
}
else if (value.IsNull)
Console.WriteLine(Output.Yellow($"[null]"));
else
{
// Try pretty-print JSON
string str = value.ToString();
if (IsJson(str))
{
try
{
var doc = JsonDocument.Parse(str);
var pretty = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(Output.Green(pretty));
}
catch { Console.WriteLine(Output.Green(str)); }
}
else
{
Console.WriteLine(Output.Green(str));
}
}
return 0;
}
/// <summary>
/// Executes the SET command to store a value in Redis.
/// Supports optional TTL (time to live) for the key.
/// </summary>
/// <param name="opts">The SET command options</param>
/// <param name="config">The RedisManager configuration</param>
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
public static int RunSet(SetOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
bool ok;
if (opts.TTL.HasValue)
ok = db.StringSet(opts.Key, opts.Value, TimeSpan.FromSeconds(opts.TTL.Value));
else
ok = db.StringSet(opts.Key, opts.Value);
if (ok)
Console.WriteLine(Output.Green("OK"));
else
Console.WriteLine(Output.Red("Failed to set key."));
return 0;
}
/// <summary>
/// Executes the DEL command to delete one or more keys from Redis.
/// Supports confirmation prompts and table output format.
/// </summary>
/// <param name="opts">The DEL command options</param>
/// <param name="config">The RedisManager configuration</param>
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
public static int RunDel(DelOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
if (!opts.Yes)
{
Console.WriteLine(Output.Yellow($"Are you sure you want to delete {keys.Length} key(s)? (y/N)"));
var response = Console.ReadLine()?.ToLower();
if (response != "y" && response != "yes")
{
Console.WriteLine(Output.Red("Operation cancelled."));
return 1;
}
}
var count = db.KeyDelete(keys);
if (opts.Table)
RedisUtils.PrintTable(new[] { "Keys", "Deleted" }, new List<string[]> { new[] { string.Join(", ", opts.Keys), count.ToString() } });
else
Console.WriteLine(Output.Green($"Deleted {count} key(s)"));
return 0;
}
/// <summary>
/// Determines if a string appears to be JSON formatted.
/// Checks if the string starts and ends with curly braces or square brackets.
/// </summary>
/// <param name="str">The string to check for JSON formatting</param>
/// <returns>True if the string appears to be JSON, false otherwise</returns>
private static bool IsJson(string str)
{
str = str.Trim();
return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]"));
}
}
}

View File

@ -0,0 +1,134 @@
using CommandLine;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using RedisManager.Utils;
namespace RedisManager.Commands
{
/// <summary>
/// Contains command line options and implementations for Redis transaction operations.
/// Provides functionality for MULTI, EXEC, DISCARD, WATCH, and UNWATCH commands.
/// </summary>
[Verb("multi", HelpText = "Mark the start of a transaction block.")]
public class MultiOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("exec", HelpText = "Execute all commands issued after MULTI.")]
public class ExecOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("discard", HelpText = "Flush all commands issued after MULTI.")]
public class DiscardOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
[Verb("watch", HelpText = "Watch the given keys to determine execution of the MULTI/EXEC block.")]
public class WatchOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys to watch.")]
public IEnumerable<string> Keys { get; set; }
}
[Verb("unwatch", HelpText = "Forget about all watched keys.")]
public class UnwatchOptions
{
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
public string Instance { get; set; }
}
public static class TransactionCommands
{
/// <summary>
/// Executes the MULTI command to mark the start of a transaction block.
/// </summary>
/// <param name="opts">The MultiOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunMulti(MultiOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("MULTI");
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the EXEC command to execute all commands issued after MULTI.
/// </summary>
/// <param name="opts">The ExecOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunExec(ExecOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var result = db.Execute("EXEC");
Console.WriteLine(Output.Green(result.ToString()));
return 0;
}
/// <summary>
/// Executes the DISCARD command to flush all commands issued after MULTI.
/// </summary>
/// <param name="opts">The DiscardOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunDiscard(DiscardOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("DISCARD");
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the WATCH command to watch the given keys for a transaction block.
/// </summary>
/// <param name="opts">The WatchOptions containing instance and keys.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunWatch(WatchOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
db.Execute("WATCH", keys);
Console.WriteLine(Output.Green("OK"));
return 0;
}
/// <summary>
/// Executes the UNWATCH command to forget about all watched keys.
/// </summary>
/// <param name="opts">The UnwatchOptions containing instance.</param>
/// <param name="config">The RedisManager configuration.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
public static int RunUnwatch(UnwatchOptions opts, Config config)
{
var instance = RedisUtils.GetInstance(config, opts.Instance);
using var redis = RedisUtils.ConnectRedis(instance);
var db = redis.GetDatabase();
db.Execute("UNWATCH");
Console.WriteLine(Output.Green("OK"));
return 0;
}
}
}

108
Models/Config.cs Normal file
View File

@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace RedisManager
{
/// <summary>
/// Represents configuration for a single Redis instance, including connection details.
/// </summary>
public class InstanceConfig
{
/// <summary>
/// Custom Redis configuration parameters to be applied at startup.
/// </summary>
public Dictionary<string, string> CustomConfig { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Optional: Path to the redis/valkey server binary for this instance.
/// If not set, the global default will be used.
/// </summary>
public string ServerBinaryPath { get; set; }
/// <summary>
/// The name of the Redis instance.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The host address of the Redis instance.
/// </summary>
public string Host { get; set; }
/// <summary>
/// The port number for the Redis instance (default 6379).
/// </summary>
public int Port { get; set; } = 6379;
/// <summary>
/// The password for authenticating with the Redis instance.
/// </summary>
public string Password { get; set; }
}
/// <summary>
/// Represents the configuration containing all Redis instances.
/// </summary>
public class Config
{
/// <summary>
/// Optional: Default path to the redis/valkey server binary for all instances.
/// </summary>
public string ServerBinaryPath { get; set; }
/// <summary>
/// The list of Redis instance configurations.
/// </summary>
public List<InstanceConfig> Instances { get; set; } = new List<InstanceConfig>();
}
/// <summary>
/// Provides methods to load and save RedisManager configuration from disk.
/// </summary>
public static class ConfigManager
{
public static string ConfigPath = "redismanager.json";
/// <summary>
/// Loads the configuration from disk, or creates a default if not present.
/// </summary>
/// <returns>The loaded or default Config object.</returns>
public static Config LoadConfig(string path)
{
if (!File.Exists(path))
{
var defaultConfig = new Config
{
Instances = new List<InstanceConfig>
{
new InstanceConfig
{
Name = "default",
Host = "localhost",
Port = 6379
}
}
};
SaveConfig(defaultConfig, path);
return defaultConfig;
}
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Config>(json) ?? new Config();
}
public static Config LoadConfig()
{
return LoadConfig(ConfigPath);
}
/// <summary>
/// Saves the provided configuration to disk as JSON.
/// </summary>
/// <param name="config">The configuration to save.</param>
public static void SaveConfig(Config config, string path)
{
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(path, json);
}
public static void SaveConfig(Config config)
{
SaveConfig(config, ConfigPath);
}
}
}

87
Program.cs Normal file
View File

@ -0,0 +1,87 @@
using CommandLine;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using RedisManager.Commands;
using Crayon;
namespace RedisManager
{
/// <summary>
/// Main entry point for the RedisManager application.
/// Supports service (daemon) mode and client mode only.
/// </summary>
class Program
{
/// <summary>
/// Main entry point that determines the operational mode based on command line arguments.
/// </summary>
/// <param name="args">Command line arguments</param>
/// <returns>Exit code (0 for success, non-zero for errors)</returns>
static async Task<int> Main(string[] args)
{
// No arguments: run as a daemon (service mode)
// Service mode: allow optional --config <path>
if (args.Length == 0 || (args.Length >= 2 && args[0] == "--config"))
{
string configPath = null;
if (args.Length >= 2 && args[0] == "--config")
{
configPath = args[1];
// Remove --config and path from args for service mode
args = args.Skip(2).ToArray();
}
return await RunServiceMode();
} // Standalone mode removed: all commands require daemon/service.
// With arguments: try to connect to the daemon (client mode)
var client = new RedisManagerClient();
var clientResult = await client.ExecuteCommandAsync(args);
// A specific exit code (2) from the client indicates the service is not running
if (clientResult == 2)
{
// Allow help output even if no daemon and no --instance
bool wantsHelp = args.Any(arg => arg == "help" || arg == "--help" || arg == "-h");
if (!wantsHelp)
{
Console.WriteLine(Output.Red("Error: RedisManager daemon/service is not running. Please start the service (no arguments) and retry your command."));
}
}
// Return the result from the client (0 for success, 1 for other errors)
return clientResult;
}
/// <summary>
/// Runs RedisManager in service mode, starting a TCP server that accepts client connections.
/// The service runs indefinitely until interrupted by Ctrl+C or other termination signals.
/// </summary>
/// <returns>Exit code (0 for successful shutdown)</returns>
static async Task<int> RunServiceMode()
{
Console.WriteLine("Starting RedisManager Service...");
var service = new RedisManagerService();
await service.StartAsync();
// Block the main thread until Ctrl+C is pressed
var cts = new System.Threading.CancellationTokenSource();
Console.CancelKeyPress += (sender, e) => {
e.Cancel = true;
cts.Cancel();
};
Console.WriteLine("RedisManager daemon is running. Press Ctrl+C to exit.");
try
{
await Task.Delay(-1, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Shutting down RedisManager Service...");
}
await service.StopAsync();
return 0;
}
}
}

462
README.md
View File

@ -0,0 +1,462 @@
# RedisManager
A comprehensive Redis management tool with a modern daemon/client architecture. All Redis commands, configuration, and management operations are routed through a persistent background daemon (service), ensuring consistent state and robust multi-instance support.
---
## Daemon/Client Architecture
- **Daemon/Service:** A long-running background process (TCP server) that manages all Redis/Valkey instances, configuration persistence, and command execution.
- **Client CLI:** The command-line interface only sends commands to the daemon; it does not interact with Redis directly.
- **Standalone CLI mode is no longer supported.**
- All commands (except `--help`) require the daemon to be running.
---
## Quick Start
1. Install .NET 9.0 or later and Redis server(s) to connect to.
2. Build the project with `dotnet build` in the RedisManager directory.
3. Start the daemon with `dotnet run` in the RedisManager directory.
4. Run commands with `dotnet run -- <command>`.
---
## Features
### Core Functionality
- **Multi-Mode Operation**: Standalone CLI, Service mode, and Client mode
- **Instance Management**: Add, update, delete, and list Redis instances
- **Comprehensive Redis Commands**: Support for all major Redis data types and operations
- **Formatted Output**: Table formatting, JSON pretty-printing, and colored output
- **Persistent Redis Configuration**: Set, list, and persist custom Redis config parameters per instance; parameters are automatically applied on instance startup
- **Configuration Management**: JSON-based configuration with automatic defaults
### Supported Redis Operations
#### String Operations
- `get` - Retrieve key values with JSON formatting
- `set` - Set key values with optional TTL
- `del` - Delete keys with confirmation prompts
- Advanced operations: `append`, `incr`, `decr`, `getrange`, `setrange`, `strlen`, `mget`, `mset`
#### Hash Operations
- `hget`, `hset`, `hdel`, `hgetall`, `hkeys`, `hvals`, `hlen`, `hexists`
- `hincrby`, `hincrbyfloat`, `hmset`, `hmget`, `hsetnx`, `hstrlen`, `hscan`
#### List Operations
- `lpush`, `rpush`, `lpop`, `rpop`, `lrange`, `llen`, `lindex`, `lset`
- `linsert`, `lrem`, `ltrim`, `blpop`, `brpop`, `rpoplpush`
#### Set Operations
- `sadd`, `srem`, `smembers`, `scard`, `sismember`, `srandmember`, `spop`
- `sinter`, `sunion`, `sdiff`, `sinterstore`, `sunionstore`, `sdiffstore`, `sscan`, `smove`
#### Sorted Set Operations
- `zadd`, `zrem`, `zrange`, `zrevrange`, `zrangebyscore`, `zcard`, `zscore`
- `zrank`, `zrevrank`, `zincrby`, `zrevrangebyscore`, `zcount`
- `zunionstore`, `zinterstore`, `zscan`, `zpopmax`, `zpopmin`
- `zremrangebyrank`, `zremrangebyscore`
#### Key Management
- `exists`, `expire`, `ttl`, `persist`, `rename`, `type`, `keys`, `scan`
#### Database Operations
- `select`, `flushdb`, `flushall`, `dbsize`
#### Server Information
- `info`, `config`, `save`, `bgsave`, `lastsave`, `time`, `ping`
#### Persistent Config Management
- `config --set` - Persistently set a Redis config parameter for an instance (e.g., maxmemory, save, etc.)
- `config --list-custom` - List all custom config parameters persisted for an instance
- Custom config parameters are saved in `redismanager.json` and automatically applied to the Redis/Valkey server on instance startup
#### Connection Management
- `auth`, `quit`, `client list`, `client kill`, `client getname`, `client setname`
#### Pub/Sub
- `publish`, `subscribe`, `unsubscribe`
#### Scripting
- `eval`, `evalsha`, `script load`, `script exists`, `script flush`
#### Transactions
- `multi`, `exec`, `discard`, `watch`, `unwatch`
#### Geographic Operations
- `geoadd`, `geodist`, `geohash`, `geopos`, `georadius`
#### Bit Operations
- `bitcount`, `bitfield`, `bitop`, `bitpos`, `getbit`, `setbit`
#### HyperLogLog
- `pfadd`, `pfcount`, `pfmerge`
#### Streams
- `xadd`, `xrange`, `xlen`, `xdel`
#### Module Management
- `module list`, `module load`, `module unload`
## Installation
### Prerequisites
- .NET 9.0 or later
- Redis server(s) to connect to
### Build
```bash
cd RedisManager
dotnet build
```
## Usage
---
## Command Usage Examples
Below are examples for all major command types. All commands must be run with the daemon/service running.
### String Commands
```bash
# Set a value
dotnet run -- set -i default mykey "hello world"
# Get a value
dotnet run -- get -i default mykey
# Delete a key
dotnet run -- del -i default mykey
```
### Hash Commands
```bash
# Set multiple fields
dotnet run -- hset -i default user:1 name "Alice" age "30"
# Get all fields
dotnet run -- hgetall -i default user:1
# Get a field
dotnet run -- hget -i default user:1 name
```
### List Commands
```bash
# Push to a list
dotnet run -- lpush -i default mylist "one" "two"
# Get range
dotnet run -- lrange -i default mylist 0 -1
```
### Set Commands
```bash
# Add members
dotnet run -- sadd -i default myset "a" "b"
# List members
dotnet run -- smembers -i default myset
```
### Sorted Set Commands
```bash
# Add with scores
dotnet run -- zadd -i default myzset 1 "a" 2 "b"
# Range by score
dotnet run -- zrangebyscore -i default myzset 0 10
```
### Key Management
```bash
# List keys
dotnet run -- keys -i default "*"
# Set expiration
dotnet run -- expire -i default mykey 60
```
### Database Commands
```bash
# Select DB
dotnet run -- select -i default 0
# Get DB size
dotnet run -- dbsize -i default
```
### Server Information
```bash
# Get server info
dotnet run -- info -i default
# Ping server
dotnet run -- ping -i default
```
### Persistent Config Management
```bash
# Set and persist a config parameter
dotnet run -- config --instance default --set "maxmemory 128mb"
# List custom config parameters
dotnet run -- config --instance default --list-custom
# Get current value from Redis
dotnet run -- config --instance default --get maxmemory
```
### Pub/Sub
```bash
# Publish a message
dotnet run -- publish -i default mychannel "hello"
```
### Scripting
```bash
# Run a Lua script
dotnet run -- eval -i default "return redis.call('set', KEYS[1], ARGV[1])" 1 mykey myvalue
```
### Transactions
```bash
# Start a transaction
dotnet run -- multi -i default
# Execute transaction
dotnet run -- exec -i default
```
### Geo Commands
```bash
# Add geo data
dotnet run -- geoadd -i default places 13.361389 38.115556 "Palermo"
```
### Bit Operations
```bash
# Set and get bit
dotnet run -- setbit -i default mykey 7 1
dotnet run -- getbit -i default mykey 7
```
### Module Management
```bash
# List modules
dotnet run -- module list -i default
```
### Instance Management
```bash
# List instances
dotnet run -- list-instances
# Add a new instance
dotnet run -- add-instance -n newinst -h localhost -p 6381
# Delete an instance
dotnet run -- delete-instance -n newinst
```
---
### Service (Daemon) Mode
Run as a TCP service that accepts client connections:
```bash
# Start the daemon/service
cd RedisManager
dotnet run
# The service runs on port 6380 by default
```
### Client Mode
All commands must be executed via the daemon:
```bash
# Set a value
cd RedisManager
dotnet run -- set -i default mykey "hello world"
# Get a value
dotnet run -- get -i default mykey
# List all configured instances
dotnet run -- list-instances
```
> **Note:** The daemon/service must be running for any command to work (except --help).
### Service Mode
Run as a TCP service that accepts client connections:
```bash
# Start the service
dotnet run -- --service
# The service runs on port 6379 by default
# Clients can connect and send JSON-formatted requests
```
### Client Mode
Connect to a running service instance:
```bash
# Execute commands through the service
dotnet run -- --client get -i default mykey
dotnet run -- --client set -i default mykey "value"
```
## Configuration
The tool uses a `redismanager.json` configuration file that is automatically created with default settings:
```json
{
"Instances": [
{
"Name": "default",
"Host": "localhost",
"Port": 6379,
"Password": null,
"CustomConfig": {
"maxmemory": "128mb"
}
}
]
}
```
- The `CustomConfig` dictionary stores all persistent Redis config parameters for each instance.
- When the daemon or instance starts, all parameters in `CustomConfig` are automatically applied to the Redis/Valkey server.
- You can edit these parameters using the `config --set` and `config --list-custom` commands.
### Persistent Redis Config Commands
```bash
# Persistently set a Redis config parameter (e.g., maxmemory) for an instance
# This will save the parameter in redismanager.json and apply it on instance startup
dotnet run -- config --instance default --set "maxmemory 128mb"
# List all custom config parameters persisted for an instance
dotnet run -- config --instance default --list-custom
# Get the current value of a config parameter from the running Redis instance
dotnet run -- config --instance default --get maxmemory
```
### Instance Management Commands
```bash
# List all configured instances
dotnet run -- list-instances
# Add a new instance
dotnet run -- add-instance -n production -h redis.prod.com -p 6379 -w "password"
# Update an existing instance
dotnet run -- update-instance -n production -h new-redis.prod.com
# Delete an instance
dotnet run -- delete-instance -n production
```
## Output Formats
### Table Format
Many commands support `--table` output for structured data:
```bash
dotnet run -- get -i default mykey --table
```
### JSON Formatting
String values that appear to be JSON are automatically pretty-printed.
### Colored Output
- Green: Success messages and data
- Red: Error messages
- Yellow: Warnings and confirmations
- Blue: Headers and navigation
## Architecture
### Core Components
#### Program.cs
Main entry point that handles three operational modes:
- **Standalone Mode**: Direct command execution
- **Service Mode**: TCP server for client connections
- **Client Mode**: Client for communicating with service
#### RedisManagerService.cs
TCP server implementation that:
- Accepts client connections
- Processes JSON-formatted requests
- Executes Redis commands
- Returns formatted responses
#### RedisManagerClient.cs
Client implementation that:
- Connects to the service
- Sends command requests
- Displays responses
#### Command Classes
Organized by Redis data type and functionality:
- `StringCommands.cs` - Basic string operations
- `HashCommands.cs` - Hash operations
- `ListCommands.cs` - List operations
- `SetCommands.cs` - Set operations
- `SortedSetCommands.cs` - Sorted set operations
- `KeyCommands.cs` - Key management
- `DatabaseCommands.cs` - Database operations
- `ServerCommands.cs` - Server information
- `ConnectionCommands.cs` - Connection management
- `PubSubCommands.cs` - Pub/Sub operations
- `ScriptingCommands.cs` - Lua scripting
- `TransactionCommands.cs` - Transaction support
- `GeoCommands.cs` - Geographic operations
- `BitCommands.cs` - Bit operations
- `HyperLogLogCommands.cs` - HyperLogLog operations
- `StreamCommands.cs` - Stream operations
- `ModuleCommands.cs` - Module management
- `AdvancedStringCommands.cs` - Advanced string operations
- `InstanceCommands.cs` - Instance management
- `StatusCommand.cs` - Connection status
#### Utility Classes
##### RedisUtils.cs
Provides common Redis functionality:
- Instance configuration retrieval
- Redis connection establishment
- Data type conversions
- Table formatting
##### Output.cs
ANSI color formatting utilities for console output.
##### Config.cs
Configuration management:
- `InstanceConfig` - Individual Redis instance settings
- `Config` - Main configuration container
- `ConfigManager` - Configuration loading/saving utilities
## Error Handling
The tool provides comprehensive error handling:
- Connection failures with helpful messages
- Invalid command syntax with usage hints
- Missing instances with configuration guidance
- Redis operation failures with detailed error messages
## Security Considerations
- Passwords are stored in plain text in the configuration file
- No encryption for client-server communication
- Consider using Redis ACLs for production environments
- Use secure connections (TLS) for sensitive data
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add comprehensive documentation for new commands
4. Ensure all tests pass
5. Submit a pull request
## License
This project is part of a larger trading platform and follows the same licensing terms.

16
RedisManager.csproj Normal file
View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Crayon" Version="2.0.69" />
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
</ItemGroup>
</Project>

79
RedisManagerClient.cs Normal file
View File

@ -0,0 +1,79 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace RedisManager
{
/// <summary>
/// Client for communicating with the RedisManager service.
/// Connects to a running service instance and sends command requests over TCP.
/// </summary>
public class RedisManagerClient
{
private readonly string _host = "localhost";
private readonly int _port = 6380; // Same port as service
/// <summary>
/// Executes a Redis command by sending it to the RedisManager service.
/// Connects to the service, sends the command arguments, and displays the result.
/// </summary>
/// <param name="args">Command line arguments to execute on the service</param>
/// <returns>Exit code (0 for success, 1 for failure)</returns>
public async Task<int> ExecuteCommandAsync(string[] args)
{
try
{
using var client = new TcpClient();
await client.ConnectAsync(_host, _port);
var request = new ServiceRequest
{
Command = "execute",
Arguments = args
};
var requestJson = JsonSerializer.Serialize(request) + "\n";
using var stream = client.GetStream();
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
await writer.WriteAsync(requestJson);
using var reader = new StreamReader(stream, new UTF8Encoding(false));
var responseJson = await reader.ReadLineAsync();
if (responseJson != null)
{
var serviceResponse = JsonSerializer.Deserialize<ServiceResponse>(responseJson);
if (serviceResponse.Success)
{
Console.WriteLine(serviceResponse.Data);
return 0;
}
else
{
Console.WriteLine($"Error: {serviceResponse.Error}");
if (!string.IsNullOrEmpty(serviceResponse.ErrorCode))
Console.WriteLine($"Error Code: {serviceResponse.ErrorCode}");
if (serviceResponse.ErrorDetails != null)
Console.WriteLine($"Error Details: {JsonSerializer.Serialize(serviceResponse.ErrorDetails)}");
return 1;
}
}
Console.WriteLine("No response from service");
return 1;
}
catch (SocketException)
{
// Special code to indicate the service is not running
return 2;
}
catch (Exception ex)
{
Console.WriteLine($"Error communicating with service: {ex.Message}");
return 1;
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using StackExchange.Redis;
using System.Threading.Tasks;
using RedisManager;
using RedisManager.Utils;
namespace RedisManager
{
public static class CustomConfigApplier
{
public static async Task ApplyCustomConfigAsync(InstanceConfig instance)
{
if (instance?.CustomConfig == null || instance.CustomConfig.Count == 0)
return;
try
{
var mux = RedisUtils.ConnectRedis(instance);
var db = mux.GetDatabase();
foreach (var kv in instance.CustomConfig)
{
db.Execute("CONFIG", "SET", kv.Key, kv.Value);
}
mux.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Warning: Failed to apply custom config for instance '{instance.Name}': {ex.Message}");
}
}
}
}

931
RedisManagerService.cs Normal file
View File

@ -0,0 +1,931 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CommandLine;
using RedisManager.Commands;
namespace RedisManager
{
/// <summary>
/// TCP server that provides Redis command execution as a service.
/// Accepts client connections and executes Redis commands on their behalf.
/// Clients send JSON-formatted requests and receive JSON responses.
/// </summary>
public class RedisManagerService
{
private TcpListener _listener;
private readonly ConcurrentDictionary<string, DateTime> _clients = new();
private Config _config;
private readonly object _configLock = new();
// Track Valkey/Redis server processes started by the daemon
private readonly ConcurrentDictionary<string, System.Diagnostics.Process> _instanceProcesses = new();
private bool _isRunning = false;
private readonly int _port = 6380; // Port for service communication
/// <summary>
/// Initializes a new instance of the RedisManagerService.
/// Loads configuration and prepares the service for operation.
/// </summary>
public RedisManagerService()
{
_config = ConfigManager.LoadConfig();
}
/// <summary>
/// Starts the TCP server and begins accepting client connections.
/// The server runs indefinitely until StopAsync is called.
/// </summary>
/// <returns>A task that represents the asynchronous start operation</returns>
public Task StartAsync()
{
if (_isRunning) return Task.CompletedTask;
try
{
_listener = new TcpListener(IPAddress.Any, _port);
_listener.Start();
_isRunning = true;
}
catch (SocketException ex)
{
Console.WriteLine($"Error starting service on port {_port}: {ex.Message}");
Console.WriteLine("Another instance of the service may be running, or the port is in use.");
_isRunning = false;
return Task.CompletedTask; // Exit if we can't start the listener
}
Console.WriteLine($"RedisManager Service started on port {_port}");
// Start Valkey/Redis servers for all configured instances at startup
Task.Run(async () => {
bool anySuccess = false;
List<InstanceConfig> instancesSnapshot;
lock (_configLock)
{
instancesSnapshot = new List<InstanceConfig>(_config.Instances);
}
foreach (var instance in instancesSnapshot)
{
var result = await StartValkeyServerIfNeeded(instance);
if (result.Success)
{
Console.WriteLine($"[Daemon] Started Valkey/Redis for instance '{instance.Name}' on port {instance.Port}.");
anySuccess = true;
}
else
{
Console.WriteLine($"[Daemon] Failed to start Valkey/Redis for instance '{instance.Name}' on port {instance.Port}: {result.Output}");
}
}
if (!anySuccess)
{
Console.WriteLine("[Daemon] FATAL: Could not start any Valkey/Redis server. Check your ServerBinaryPath configuration and ensure the binary exists and is executable.");
Environment.Exit(1);
}
});
// Run the client accepting loop in the background
_ = AcceptClientsAsync();
return Task.CompletedTask;
}
private async Task AcceptClientsAsync()
{
while (_isRunning)
{
try
{
var client = await _listener.AcceptTcpClientAsync();
_ = Task.Run(() => HandleClientAsync(client));
}
catch (Exception ex) when (!_isRunning)
{
// Expected when stopping
break;
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error accepting client: {ex}");
// Continue loop; do not exit
}
}
}
/// <summary>
/// Stops the TCP server and closes all client connections.
/// </summary>
/// <returns>A task that represents the asynchronous stop operation</returns>
public async Task StopAsync()
{
if (!_isRunning) return;
_isRunning = false;
_listener?.Stop();
Console.WriteLine("RedisManager Service stopped");
}
/// <summary>
/// Handles a single client connection asynchronously.
/// Processes JSON-formatted command requests and sends back responses.
/// </summary>
/// <param name="client">The TCP client connection to handle</param>
/// <returns>A task that represents the asynchronous client handling operation</returns>
private async Task HandleClientAsync(TcpClient client)
{
var clientId = Guid.NewGuid().ToString();
_clients[clientId] = DateTime.UtcNow;
Console.WriteLine($"[Daemon] Client connected: {clientId}");
try
{
using var stream = client.GetStream();
using var reader = new StreamReader(stream, new UTF8Encoding(false));
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
while (client.Connected)
{
try
{
var line = await reader.ReadLineAsync();
if (line == null) break;
Console.WriteLine($"[Daemon] Received command: {line}");
var response = await ExecuteCommandAsync(line);
var responseJson = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = false });
await writer.WriteLineAsync(responseJson);
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error processing client command: {ex}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] Error handling client: {ex}");
}
finally
{
_clients.TryRemove(clientId, out _);
Console.WriteLine($"[Daemon] Client disconnected: {clientId}");
client.Close();
}
}
/// <summary>
/// Executes a Redis command from a JSON-formatted request.
/// </summary>
/// <param name="commandJson">JSON string containing the command request</param>
/// <returns>A ServiceResponse object with the command execution results</returns>
private async Task<ServiceResponse> ExecuteCommandAsync(string commandJson)
{
try
{
var request = JsonSerializer.Deserialize<ServiceRequest>(commandJson);
if (request == null)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = "Invalid request format",
ErrorCode = "INVALID_REQUEST",
ErrorDetails = new { RawInput = commandJson }
};
}
// Special handling for reload-config
if (request.Arguments != null && request.Arguments.Length > 0 && string.Equals(request.Arguments[0], "reload-config", StringComparison.OrdinalIgnoreCase))
{
if (request.Arguments.Length > 1)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = "'reload-config' takes no arguments",
ErrorCode = "ARGUMENT_ERROR",
ErrorDetails = new { Command = "reload-config", Args = request.Arguments }
};
}
try
{
var newConfig = ConfigManager.LoadConfig();
lock (_configLock)
{
_config = newConfig;
}
Console.WriteLine($"[Daemon] Config reloaded from disk at {DateTime.Now:O}.");
return new ServiceResponse
{
Success = true,
Data = "Configuration reloaded successfully.",
Error = null,
ErrorCode = null,
ErrorDetails = null
};
}
catch (Exception ex)
{
Console.WriteLine($"[Daemon] ERROR: Failed to reload config: {ex.Message}");
return new ServiceResponse
{
Success = false,
Data = null,
Error = $"Failed to reload configuration: {ex.Message}",
ErrorCode = "CONFIG_RELOAD_FAILED",
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
};
}
}
var (success, output) = await ExecuteRedisCommandAsync(request.Arguments);
if (!success)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = output,
ErrorCode = "COMMAND_EXECUTION_FAILED",
ErrorDetails = new { Arguments = request.Arguments }
};
}
return new ServiceResponse
{
Success = true,
Data = output,
Error = null,
ErrorCode = null,
ErrorDetails = null
};
}
catch (Exception ex)
{
return new ServiceResponse
{
Success = false,
Data = null,
Error = ex.Message,
ErrorCode = "EXCEPTION",
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
};
}
}
// Helper: Check if a TCP port is open
private bool IsPortOpen(string host, int port)
{
try
{
using var client = new TcpClient();
var result = client.BeginConnect(host, port, null, null);
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(300));
return success && client.Connected;
}
catch { return false; }
}
// Helper: Check if a Redis/Valkey server is running on host:port
private bool IsRedisServer(string host, int port)
{
try
{
using var client = new TcpClient(host, port);
using var stream = client.GetStream();
var ping = Encoding.ASCII.GetBytes("*1\r\n$4\r\nPING\r\n");
stream.Write(ping, 0, ping.Length);
stream.Flush();
var buffer = new byte[64];
int read = stream.Read(buffer, 0, buffer.Length);
var response = Encoding.ASCII.GetString(buffer, 0, read);
return response.Contains("PONG");
}
catch { return false; }
}
// Helper: Start Valkey/Redis server for an instance if not running
private async Task<(bool Success, string Output)> StartValkeyServerIfNeeded(InstanceConfig instance)
{
// Already started by us?
if (_instanceProcesses.TryGetValue(instance.Name, out var existingProc) && !existingProc.HasExited)
return (true, "");
// Is port open?
if (IsPortOpen(instance.Host, instance.Port))
{
if (IsRedisServer(instance.Host, instance.Port))
return (true, "");
else
return (false, $"Port {instance.Port} is in use but is not a Redis/Valkey server.");
}
// Determine binary path
string binaryPath = instance.ServerBinaryPath ?? _config.ServerBinaryPath;
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
return (false, $"Redis/Valkey server binary not found at '{binaryPath}'. Please set 'ServerBinaryPath' in redismanager.json to the correct path for your Valkey or Redis server binary.");
// Start the server
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = binaryPath,
Arguments = $"--port {instance.Port}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
return (false, $"Failed to start Valkey/Redis server for instance '{instance.Name}'.");
_instanceProcesses[instance.Name] = proc;
// Wait briefly for the server to start
await Task.Delay(500);
if (!IsPortOpen(instance.Host, instance.Port))
return (false, $"Valkey/Redis server did not start on port {instance.Port}.");
// Apply custom config options after server is confirmed up
await CustomConfigApplier.ApplyCustomConfigAsync(instance);
return (true, $"Started Valkey/Redis server for instance '{instance.Name}'.");
}
catch (Exception ex)
{
return (false, $"Exception starting Valkey/Redis server: {ex.Message}");
}
}
/// <summary>
/// Executes Redis commands by parsing arguments and routing to appropriate command handlers.
/// Captures console output and returns it as a string.
/// </summary>
/// <param name="args">Command line arguments to execute</param>
/// <returns>The captured console output from command execution</returns>
private async Task<(bool Success, string Output, string ErrorCode, object ErrorDetails)> ValidateArguments(string[] args)
{
// Special validation for reload-config
if (args.Length > 0 && string.Equals(args[0], "reload-config", StringComparison.OrdinalIgnoreCase))
{
if (args.Length == 1)
return (true, null, null, null);
else
return (false, "'reload-config' takes no arguments", "ARGUMENT_ERROR", new { Command = "reload-config", Args = args });
}
if (args == null || args.Length == 0)
{
return (false, "No command arguments provided.", "ARGUMENT_ERROR", new { Args = args });
}
// Instance flag validation
string instanceName = null;
var filteredArgs = new List<string>();
for (int i = 0; i < args.Length; i++)
{
if ((args[i] == "-i" || args[i] == "--instance") && i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
{
instanceName = args[i + 1];
i++; // Skip instance name
}
else
{
filteredArgs.Add(args[i]);
}
}
// Whitelist of commands that do NOT require instance
var noInstanceRequired = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"list-instances", "add-instance", "update-instance", "delete-instance", "reload-config", "help", "version"
};
string commandName = filteredArgs.Count > 0 ? filteredArgs[0].ToLowerInvariant() : null;
bool needsInstance = !noInstanceRequired.Contains(commandName);
if (needsInstance && string.IsNullOrEmpty(instanceName))
{
return (false, "Instance name must be specified with --instance or -i.", "INSTANCE_REQUIRED", new { Command = commandName, Args = args });
}
if (!string.IsNullOrEmpty(instanceName))
{
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
if (instanceConfig == null)
{
return (false, $"Instance '{instanceName}' not found in config.", "ARGUMENT_ERROR", new { Instance = instanceName });
}
}
// Command-specific validation
if (filteredArgs.Count > 0)
{
string cmd = filteredArgs[0].ToLowerInvariant();
switch (cmd)
{
case "get":
if (filteredArgs.Count != 2)
return (false, "'get' command requires exactly 1 argument: get <key>", "ARGUMENT_ERROR", new { Command = "get", Args = filteredArgs });
break;
case "set":
if (filteredArgs.Count < 3)
return (false, "'set' command requires at least 2 arguments: set <key> <value> [EX seconds] [PX milliseconds] [NX|XX]", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
// Validate optional flags for set
var setFlags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "EX", "PX", "NX", "XX" };
bool seenNX = false, seenXX = false;
int iSet = 3;
while (iSet < filteredArgs.Count)
{
string flag = filteredArgs[iSet];
if (flag.Equals("EX", StringComparison.OrdinalIgnoreCase))
{
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
return (false, "'set' EX flag must be followed by an integer (seconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
iSet += 2;
}
else if (flag.Equals("PX", StringComparison.OrdinalIgnoreCase))
{
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
return (false, "'set' PX flag must be followed by an integer (milliseconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
iSet += 2;
}
else if (flag.Equals("NX", StringComparison.OrdinalIgnoreCase))
{
if (seenXX)
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
seenNX = true;
iSet++;
}
else if (flag.Equals("XX", StringComparison.OrdinalIgnoreCase))
{
if (seenNX)
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
seenXX = true;
iSet++;
}
else
{
return (false, $"Unknown or misplaced flag '{flag}' in 'set' command", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
}
}
break;
case "del":
if (filteredArgs.Count < 2)
return (false, "'del' command requires at least 1 argument: del <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "del", Args = filteredArgs });
break;
case "hget":
if (filteredArgs.Count != 3)
return (false, "'hget' command requires exactly 2 arguments: hget <hash> <field>", "ARGUMENT_ERROR", new { Command = "hget", Args = filteredArgs });
break;
case "hset":
if (filteredArgs.Count != 4)
return (false, "'hset' command requires exactly 3 arguments: hset <hash> <field> <value>", "ARGUMENT_ERROR", new { Command = "hset", Args = filteredArgs });
break;
case "mget":
if (filteredArgs.Count < 2)
return (false, "'mget' command requires at least 1 argument: mget <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "mget", Args = filteredArgs });
break;
case "mset":
if (filteredArgs.Count < 3 || filteredArgs.Count % 2 != 1)
return (false, "'mset' command requires an even number of arguments ≥ 2: mset <key1> <value1> [key2 value2 ...]", "ARGUMENT_ERROR", new { Command = "mset", Args = filteredArgs });
break;
case "lpush":
case "rpush":
if (filteredArgs.Count < 3)
return (false, $"'{cmd}' command requires at least 2 arguments: {cmd} <list> <value1> [value2 ...]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "lrange":
if (filteredArgs.Count != 4)
return (false, "'lrange' command requires exactly 3 arguments: lrange <list> <start> <stop>", "ARGUMENT_ERROR", new { Command = "lrange", Args = filteredArgs });
break;
case "zadd":
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
return (false, "'zadd' command requires at least 3 arguments and pairs: zadd <zset> <score1> <member1> [score2 member2 ...]", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
// Validate that all scores are valid numbers
for (int i = 2; i < filteredArgs.Count; i += 2)
{
if (!double.TryParse(filteredArgs[i], out _))
return (false, $"'zadd' score argument at position {i} must be a valid number", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
}
break;
case "sadd":
if (filteredArgs.Count < 3)
return (false, "'sadd' command requires at least 2 arguments: sadd <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "sadd", Args = filteredArgs });
break;
case "exists":
if (filteredArgs.Count < 2)
return (false, "'exists' command requires at least 1 argument: exists <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "exists", Args = filteredArgs });
break;
case "expire":
if (filteredArgs.Count != 3)
return (false, "'expire' command requires exactly 2 arguments: expire <key> <seconds>", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
if (!int.TryParse(filteredArgs[2], out _))
return (false, "'expire' seconds argument must be a valid integer", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
break;
case "rename":
if (filteredArgs.Count != 3)
return (false, "'rename' command requires exactly 2 arguments: rename <key> <newkey>", "ARGUMENT_ERROR", new { Command = "rename", Args = filteredArgs });
break;
case "select":
if (filteredArgs.Count != 2)
return (false, "'select' command requires exactly 1 argument: select <dbindex>", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
if (!int.TryParse(filteredArgs[1], out int dbidx) || dbidx < 0)
return (false, "'select' dbindex must be a non-negative integer", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
break;
case "flushdb":
case "flushall":
case "dbsize":
case "info":
case "ping":
case "quit":
if (filteredArgs.Count != 1)
return (false, $"'{cmd}' command takes no arguments", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "auth":
if (filteredArgs.Count != 2)
return (false, "'auth' command requires exactly 1 argument: auth <password>", "ARGUMENT_ERROR", new { Command = "auth", Args = filteredArgs });
break;
case "keys":
if (filteredArgs.Count != 2)
return (false, "'keys' command requires exactly 1 argument: keys <pattern>", "ARGUMENT_ERROR", new { Command = "keys", Args = filteredArgs });
break;
case "ttl":
if (filteredArgs.Count != 2)
return (false, "'ttl' command requires exactly 1 argument: ttl <key>", "ARGUMENT_ERROR", new { Command = "ttl", Args = filteredArgs });
break;
case "type":
if (filteredArgs.Count != 2)
return (false, "'type' command requires exactly 1 argument: type <key>", "ARGUMENT_ERROR", new { Command = "type", Args = filteredArgs });
break;
case "persist":
if (filteredArgs.Count != 2)
return (false, "'persist' command requires exactly 1 argument: persist <key>", "ARGUMENT_ERROR", new { Command = "persist", Args = filteredArgs });
break;
case "scard":
if (filteredArgs.Count != 2)
return (false, "'scard' command requires exactly 1 argument: scard <set>", "ARGUMENT_ERROR", new { Command = "scard", Args = filteredArgs });
break;
case "smembers":
if (filteredArgs.Count != 2)
return (false, "'smembers' command requires exactly 1 argument: smembers <set>", "ARGUMENT_ERROR", new { Command = "smembers", Args = filteredArgs });
break;
case "sismember":
if (filteredArgs.Count != 3)
return (false, "'sismember' command requires exactly 2 arguments: sismember <set> <member>", "ARGUMENT_ERROR", new { Command = "sismember", Args = filteredArgs });
break;
case "srem":
if (filteredArgs.Count < 3)
return (false, "'srem' command requires at least 2 arguments: srem <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "srem", Args = filteredArgs });
break;
case "zcard":
if (filteredArgs.Count != 2)
return (false, "'zcard' command requires exactly 1 argument: zcard <zset>", "ARGUMENT_ERROR", new { Command = "zcard", Args = filteredArgs });
break;
case "zrange":
case "zrevrange":
if (filteredArgs.Count != 4 && filteredArgs.Count != 5)
return (false, $"'{cmd}' command requires 3 or 4 arguments: {cmd} <zset> <start> <stop> [WITHSCORES]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
if (filteredArgs.Count == 5 && !filteredArgs[4].Equals("WITHSCORES", StringComparison.OrdinalIgnoreCase))
return (false, $"'{cmd}' 5th argument must be 'WITHSCORES' if present.", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "zscore":
if (filteredArgs.Count != 3)
return (false, "'zscore' command requires exactly 2 arguments: zscore <zset> <member>", "ARGUMENT_ERROR", new { Command = "zscore", Args = filteredArgs });
break;
case "zrem":
if (filteredArgs.Count < 3)
return (false, "'zrem' command requires at least 2 arguments: zrem <zset> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "zrem", Args = filteredArgs });
break;
case "zcount":
if (filteredArgs.Count != 4)
return (false, "'zcount' command requires exactly 3 arguments: zcount <zset> <min> <max>", "ARGUMENT_ERROR", new { Command = "zcount", Args = filteredArgs });
break;
case "zrank":
case "zrevrank":
if (filteredArgs.Count != 3)
return (false, $"'{cmd}' command requires exactly 2 arguments: {cmd} <zset> <member>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "hincrby":
if (filteredArgs.Count != 4)
return (false, "'hincrby' command requires exactly 3 arguments: hincrby <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
if (!int.TryParse(filteredArgs[3], out _))
return (false, "'hincrby' increment must be an integer", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
break;
case "hincrbyfloat":
if (filteredArgs.Count != 4)
return (false, "'hincrbyfloat' command requires exactly 3 arguments: hincrbyfloat <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
if (!double.TryParse(filteredArgs[3], out _))
return (false, "'hincrbyfloat' increment must be a number", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
break;
case "hdel":
if (filteredArgs.Count < 3)
return (false, "'hdel' command requires at least 2 arguments: hdel <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hdel", Args = filteredArgs });
break;
case "hmget":
if (filteredArgs.Count < 3)
return (false, "'hmget' command requires at least 2 arguments: hmget <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hmget", Args = filteredArgs });
break;
case "hmset":
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
return (false, "'hmset' command requires at least 2 field-value pairs: hmset <hash> <field1> <value1> [field2 value2 ...]", "ARGUMENT_ERROR", new { Command = "hmset", Args = filteredArgs });
break;
case "hkeys":
case "hvals":
case "hlen":
if (filteredArgs.Count != 2)
return (false, $"'{cmd}' command requires exactly 1 argument: {cmd} <hash>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
break;
case "move":
if (filteredArgs.Count != 3)
return (false, "'move' command requires exactly 2 arguments: move <key> <dbindex>", "ARGUMENT_ERROR", new { Command = "move", Args = filteredArgs });
break;
case "randomkey":
if (filteredArgs.Count != 1)
return (false, "'randomkey' command takes no arguments", "ARGUMENT_ERROR", new { Command = "randomkey", Args = filteredArgs });
break;
case "echo":
if (filteredArgs.Count != 2)
return (false, "'echo' command requires exactly 1 argument: echo <message>", "ARGUMENT_ERROR", new { Command = "echo", Args = filteredArgs });
break;
}
}
return (true, null, null, null);
}
private async Task<(bool Success, string Output)> ExecuteRedisCommandAsync(string[] args)
{
// (reload-config now handled in ExecuteCommandAsync)
// Validate input arguments
var validation = await ValidateArguments(args);
if (!validation.Success)
{
return (false, validation.Output);
}
// Extract instance name if present in args (-i or --instance)
string instanceName = null;
for (int i = 0; i < args.Length - 1; i++)
{
if ((args[i] == "-i" || args[i] == "--instance") && !string.IsNullOrWhiteSpace(args[i + 1]))
{
instanceName = args[i + 1];
break;
}
}
if (!string.IsNullOrEmpty(instanceName))
{
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
if (instanceConfig == null)
return (false, $"Instance '{instanceName}' not found in config.");
// Check/start Valkey server if needed
var startResult = await StartValkeyServerIfNeeded(instanceConfig);
if (!startResult.Success)
return (false, startResult.Output);
}
try
{
// Capture console output
var originalOut = Console.Out;
var stringWriter = new StringWriter();
Console.SetOut(stringWriter);
// Always use the full in-memory config for all command handlers
Config config = _config;
bool commandRan = false;
bool parseError = false;
var parserResult = Parser.Default.ParseArguments(args, GetCommandTypes());
parserResult
.WithParsed<object>(o =>
{
commandRan = true;
switch (o)
{
case StatusOptions statusOpts: StatusCommand.RunStatus(statusOpts, config); break;
case ListInstancesOptions listInstancesOpts: InstanceCommands.RunListInstances(config); break;
case AddInstanceOptions addInstanceOpts: InstanceCommands.RunAddInstance(addInstanceOpts, config); break;
case UpdateInstanceOptions updateInstanceOpts: InstanceCommands.RunUpdateInstance(updateInstanceOpts, config); break;
case DeleteInstanceOptions deleteInstanceOpts: InstanceCommands.RunDeleteInstance(deleteInstanceOpts, config); break;
case GetOptions getOpts: StringCommands.RunGet(getOpts, config); break;
case SetOptions setOpts: StringCommands.RunSet(setOpts, config); break;
case DelOptions delOpts: StringCommands.RunDel(delOpts, config); break;
case HGetOptions hgetOpts: HashCommands.RunHGet(hgetOpts, config); break;
case HSetOptions hsetOpts: HashCommands.RunHSet(hsetOpts, config); break;
case HDelOptions hdelOpts: HashCommands.RunHDel(hdelOpts, config); break;
case HGetAllOptions hgetAllOpts: HashCommands.RunHGetAll(hgetAllOpts, config); break;
case HKeysOptions hkeysOpts: HashCommands.RunHKeys(hkeysOpts, config); break;
case HValsOptions hvalsOpts: HashCommands.RunHVals(hvalsOpts, config); break;
case HLenOptions hlenOpts: HashCommands.RunHLen(hlenOpts, config); break;
case HExistsOptions hexistsOpts: HashCommands.RunHExists(hexistsOpts, config); break;
case HIncrByOptions hincrByOpts: HashCommands.RunHIncrBy(hincrByOpts, config); break;
case HIncrByFloatOptions hincrByFloatOpts: HashCommands.RunHIncrByFloat(hincrByFloatOpts, config); break;
case HMSetOptions hmsetOpts: HashCommands.RunHMSet(hmsetOpts, config); break;
case HMGetOptions hmgetOpts: HashCommands.RunHMGet(hmgetOpts, config); break;
case HSetNxOptions hsetnxOpts: HashCommands.RunHSetNx(hsetnxOpts, config); break;
case HStrLenOptions hstrlenOpts: HashCommands.RunHStrLen(hstrlenOpts, config); break;
case HScanOptions hscanOpts: HashCommands.RunHScan(hscanOpts, config); break;
case PFAddOptions pfaddOpts: HyperLogLogCommands.RunPFAdd(pfaddOpts, config); break;
case PFCountOptions pfcountOpts: HyperLogLogCommands.RunPFCount(pfcountOpts, config); break;
case PFMergeOptions pfmergeOpts: HyperLogLogCommands.RunPFMerge(pfmergeOpts, config); break;
case GeoAddOptions geoaddOpts: GeoCommands.RunGeoAdd(geoaddOpts, config); break;
case GeoDistOptions geodistOpts: GeoCommands.RunGeoDist(geodistOpts, config); break;
case GeoHashOptions geohashOpts: GeoCommands.RunGeoHash(geohashOpts, config); break;
case GeoPosOptions geoposOpts: GeoCommands.RunGeoPos(geoposOpts, config); break;
case GeoRadiusOptions georadiusOpts: GeoCommands.RunGeoRadius(georadiusOpts, config); break;
case BitCountOptions bitcountOpts: BitCommands.RunBitCount(bitcountOpts, config); break;
case BitFieldOptions bitfieldOpts: BitCommands.RunBitField(bitfieldOpts, config); break;
case BitOpOptions bitopOpts: BitCommands.RunBitOp(bitopOpts, config); break;
case BitPosOptions bitposOpts: BitCommands.RunBitPos(bitposOpts, config); break;
case GetBitOptions getbitOpts: BitCommands.RunGetBit(getbitOpts, config); break;
case SetBitOptions setbitOpts: BitCommands.RunSetBit(setbitOpts, config); break;
case ModuleListOptions moduleListOpts: ModuleCommands.RunModuleList(moduleListOpts, config); break;
case ModuleLoadOptions moduleLoadOpts: ModuleCommands.RunModuleLoad(moduleLoadOpts, config); break;
case ModuleUnloadOptions moduleUnloadOpts: ModuleCommands.RunModuleUnload(moduleUnloadOpts, config); break;
case LPushOptions lpushOpts: ListCommands.RunLPush(lpushOpts, config); break;
case RPushOptions rpushOpts: ListCommands.RunRPush(rpushOpts, config); break;
case LLenOptions llenOpts: ListCommands.RunLLen(llenOpts, config); break;
case LRangeOptions lrangeOpts: ListCommands.RunLRange(lrangeOpts, config); break;
case LIndexOptions lindexOpts: ListCommands.RunLIndex(lindexOpts, config); break;
case LSetOptions lsetOpts: ListCommands.RunLSet(lsetOpts, config); break;
case LInsertOptions linsertOpts: ListCommands.RunLInsert(linsertOpts, config); break;
case LRemOptions lremOpts: ListCommands.RunLRem(lremOpts, config); break;
case LTrimOptions ltrimOpts: ListCommands.RunLTrim(ltrimOpts, config); break;
case LPopOptions lpopOpts: ListCommands.RunLPop(lpopOpts, config); break;
case RPopOptions rpopOpts: ListCommands.RunRPop(rpopOpts, config); break;
case BLPopOptions blpopOpts: ListCommands.RunBLPop(blpopOpts, config); break;
case BRPopOptions brpopOpts: ListCommands.RunBRPop(brpopOpts, config); break;
case RPopLPushOptions rpoplpushOpts: ListCommands.RunRPopLPush(rpoplpushOpts, config); break;
case SAddOptions saddOpts: SetCommands.RunSAdd(saddOpts, config); break;
case SMembersOptions smembersOpts: SetCommands.RunSMembers(smembersOpts, config); break;
case SIsMemberOptions sismemberOpts: SetCommands.RunSIsMember(sismemberOpts, config); break;
case SCardOptions scardOpts: SetCommands.RunSCard(scardOpts, config); break;
case SPopOptions spopOpts: SetCommands.RunSPop(spopOpts, config); break;
case SRandMemberOptions srandmemberOpts: SetCommands.RunSRandMember(srandmemberOpts, config); break;
case SRemOptions sremOpts: SetCommands.RunSRem(sremOpts, config); break;
case SInterOptions sinterOpts: SetCommands.RunSInter(sinterOpts, config); break;
case SUnionOptions sunionOpts: SetCommands.RunSUnion(sunionOpts, config); break;
case SDiffOptions sdiffOpts: SetCommands.RunSDiff(sdiffOpts, config); break;
case SInterStoreOptions sinterstoreOpts: SetCommands.RunSInterStore(sinterstoreOpts, config); break;
case SUnionStoreOptions sunionstoreOpts: SetCommands.RunSUnionStore(sunionstoreOpts, config); break;
case SDiffStoreOptions sdiffstoreOpts: SetCommands.RunSDiffStore(sdiffstoreOpts, config); break;
case SScanOptions sscanOpts: SetCommands.RunSScan(sscanOpts, config); break;
case SMoveOptions smoveOpts: SetCommands.RunSMove(smoveOpts, config); break;
case ZAddOptions zaddOpts: SortedSetCommands.RunZAdd(zaddOpts, config); break;
case ZRemOptions zremOpts: SortedSetCommands.RunZRem(zremOpts, config); break;
case ZRangeOptions zrangeOpts: SortedSetCommands.RunZRange(zrangeOpts, config); break;
case ZRevRangeOptions zrevrangeOpts: SortedSetCommands.RunZRevRange(zrevrangeOpts, config); break;
case ZRangeByScoreOptions zrangebyscoreOpts: SortedSetCommands.RunZRangeByScore(zrangebyscoreOpts, config); break;
case ZCardOptions zcardOpts: SortedSetCommands.RunZCard(zcardOpts, config); break;
case ZScoreOptions zscoreOpts: SortedSetCommands.RunZScore(zscoreOpts, config); break;
case ZRankOptions zrankOpts: SortedSetCommands.RunZRank(zrankOpts, config); break;
case ZRevRankOptions zrevrankOpts: SortedSetCommands.RunZRevRank(zrevrankOpts, config); break;
case ZIncrByOptions zincrbyOpts: SortedSetCommands.RunZIncrBy(zincrbyOpts, config); break;
case ZRevRangeByScoreOptions zrevrangebyscoreOpts: SortedSetCommands.RunZRevRangeByScore(zrevrangebyscoreOpts, config); break;
case ZCountOptions zcountOpts: SortedSetCommands.RunZCount(zcountOpts, config); break;
case ZUnionStoreOptions zunionstoreOpts: SortedSetCommands.RunZUnionStore(zunionstoreOpts, config); break;
case ZInterStoreOptions zinterstoreOpts: SortedSetCommands.RunZInterStore(zinterstoreOpts, config); break;
case ZScanOptions zscanOpts: SortedSetCommands.RunZScan(zscanOpts, config); break;
case ZPopMaxOptions zpopmaxOpts: SortedSetCommands.RunZPopMax(zpopmaxOpts, config); break;
case ZPopMinOptions zpopminOpts: SortedSetCommands.RunZPopMin(zpopminOpts, config); break;
case ZRemRangeByRankOptions zremrangebyrankOpts: SortedSetCommands.RunZRemRangeByRank(zremrangebyrankOpts, config); break;
case ZRemRangeByScoreOptions zremrangebyscoreOpts: SortedSetCommands.RunZRemRangeByScore(zremrangebyscoreOpts, config); break;
case FlushDbOptions flushdbOpts: DatabaseCommands.RunFlushDb(flushdbOpts, config); break;
case DbSizeOptions dbsizeOpts: DatabaseCommands.RunDbSize(dbsizeOpts, config); break;
case SelectOptions selectOpts: DatabaseCommands.RunSelect(selectOpts, config); break;
case FlushAllOptions flushallOpts: DatabaseCommands.RunFlushAll(flushallOpts, config); break;
case ScanOptions scanOpts: KeyCommands.RunScan(scanOpts, config); break;
case KeysOptions keysOpts: KeyCommands.RunKeys(keysOpts, config); break;
case ExistsOptions existsOpts: KeyCommands.RunExists(existsOpts, config); break;
case TypeOptions typeOpts: KeyCommands.RunType(typeOpts, config); break;
case TtlOptions ttlOpts: KeyCommands.RunTtl(ttlOpts, config); break;
case ExpireOptions expireOpts: KeyCommands.RunExpire(expireOpts, config); break;
case PersistOptions persistOpts: KeyCommands.RunPersist(persistOpts, config); break;
case RenameOptions renameOpts: KeyCommands.RunRename(renameOpts, config); break;
case ConfigOptions configOpts: ServerCommands.RunConfig(configOpts, config); break;
case AuthOptions authOpts: ConnectionCommands.RunAuth(authOpts, config); break;
case QuitOptions quitOpts: ConnectionCommands.RunQuit(quitOpts, config); break;
case ClientListOptions clientListOpts: ConnectionCommands.RunClientList(clientListOpts, config); break;
case ClientKillOptions clientKillOpts: ConnectionCommands.RunClientKill(clientKillOpts, config); break;
case AppendOptions appendOpts: AdvancedStringCommands.RunAppend(appendOpts, config); break;
case IncrOptions incrOpts: AdvancedStringCommands.RunIncr(incrOpts, config); break;
case DecrOptions decrOpts: AdvancedStringCommands.RunDecr(decrOpts, config); break;
case IncrByOptions incrbyOpts: AdvancedStringCommands.RunIncrBy(incrbyOpts, config); break;
case DecrByOptions decrbyOpts: AdvancedStringCommands.RunDecrBy(decrbyOpts, config); break;
case IncrByFloatOptions incrbyfloatOpts: AdvancedStringCommands.RunIncrByFloat(incrbyfloatOpts, config); break;
case GetRangeOptions getrangeOpts: AdvancedStringCommands.RunGetRange(getrangeOpts, config); break;
case SetRangeOptions setrangeOpts: AdvancedStringCommands.RunSetRange(setrangeOpts, config); break;
case StrLenOptions strlenOpts: AdvancedStringCommands.RunStrLen(strlenOpts, config); break;
case MGetOptions mgetOpts: AdvancedStringCommands.RunMGet(mgetOpts, config); break;
case MSetOptions msetOpts: AdvancedStringCommands.RunMSet(msetOpts, config); break;
case XAddOptions xaddOpts: StreamCommands.RunXAdd(xaddOpts, config); break;
case XRangeOptions xrangeOpts: StreamCommands.RunXRange(xrangeOpts, config); break;
case XLenOptions xlenOpts: StreamCommands.RunXLen(xlenOpts, config); break;
case XDelOptions xdelOpts: StreamCommands.RunXDel(xdelOpts, config); break;
case MultiOptions multiOpts: TransactionCommands.RunMulti(multiOpts, config); break;
default: break;
}
});
parserResult
.WithNotParsed(errors =>
{
parseError = true;
});
Console.SetOut(originalOut);
var output = stringWriter.ToString();
if (!commandRan || parseError)
{
string attemptedCommand = args.Length > 0 ? args[0] : "<none>";
return (false, $"Unknown or invalid command: '{attemptedCommand}'. Arguments: [{string.Join(", ", args)}]");
}
return (true, output);
}
catch (Exception ex)
{
return (false, $"[Daemon] Exception: {ex}");
}
}
/// <summary>
/// Returns an array of all command option types supported by the service.
/// This array is used by the command line parser to determine which commands are available.
/// </summary>
/// <returns>Array of Type objects representing all supported command options</returns>
private Type[] GetCommandTypes()
{
return new Type[] {
typeof(StatusOptions),
typeof(ListInstancesOptions), typeof(AddInstanceOptions), typeof(UpdateInstanceOptions), typeof(DeleteInstanceOptions),
typeof(GetOptions), typeof(SetOptions), typeof(DelOptions),
typeof(HGetOptions), typeof(HSetOptions), typeof(HDelOptions), typeof(HGetAllOptions), typeof(HKeysOptions), typeof(HValsOptions), typeof(HLenOptions), typeof(HExistsOptions), typeof(HIncrByOptions), typeof(HIncrByFloatOptions), typeof(HMSetOptions), typeof(HMGetOptions), typeof(HSetNxOptions), typeof(HStrLenOptions), typeof(HScanOptions),
typeof(PFAddOptions), typeof(PFCountOptions), typeof(PFMergeOptions),
typeof(GeoAddOptions), typeof(GeoDistOptions), typeof(GeoHashOptions), typeof(GeoPosOptions), typeof(GeoRadiusOptions),
typeof(BitCountOptions), typeof(BitFieldOptions), typeof(BitOpOptions), typeof(BitPosOptions), typeof(GetBitOptions), typeof(SetBitOptions),
typeof(ModuleListOptions), typeof(ModuleLoadOptions), typeof(ModuleUnloadOptions),
typeof(LPushOptions), typeof(RPushOptions), typeof(LLenOptions), typeof(LRangeOptions), typeof(LIndexOptions), typeof(LSetOptions), typeof(LInsertOptions), typeof(LRemOptions), typeof(LTrimOptions), typeof(LPopOptions), typeof(RPopOptions), typeof(BLPopOptions), typeof(BRPopOptions), typeof(RPopLPushOptions),
typeof(SAddOptions), typeof(SMembersOptions), typeof(SIsMemberOptions), typeof(SCardOptions), typeof(SPopOptions), typeof(SRandMemberOptions), typeof(SRemOptions), typeof(SInterOptions), typeof(SUnionOptions), typeof(SDiffOptions), typeof(SInterStoreOptions), typeof(SUnionStoreOptions), typeof(SDiffStoreOptions), typeof(SScanOptions), typeof(SMoveOptions),
typeof(ZAddOptions), typeof(ZRemOptions), typeof(ZRangeOptions), typeof(ZRevRangeOptions), typeof(ZRangeByScoreOptions), typeof(ZCardOptions), typeof(ZScoreOptions), typeof(ZRankOptions), typeof(ZRevRankOptions), typeof(ZIncrByOptions), typeof(ZRevRangeByScoreOptions), typeof(ZCountOptions), typeof(ZUnionStoreOptions), typeof(ZInterStoreOptions), typeof(ZScanOptions), typeof(ZPopMaxOptions), typeof(ZPopMinOptions), typeof(ZRemRangeByRankOptions), typeof(ZRemRangeByScoreOptions),
typeof(FlushDbOptions), typeof(DbSizeOptions), typeof(SelectOptions), typeof(FlushAllOptions),
typeof(ScanOptions), typeof(KeysOptions), typeof(ExistsOptions), typeof(TypeOptions), typeof(TtlOptions), typeof(ExpireOptions), typeof(PersistOptions), typeof(RenameOptions),
typeof(ConfigOptions),
typeof(AuthOptions), typeof(QuitOptions), typeof(ClientListOptions), typeof(ClientKillOptions),
typeof(AppendOptions), typeof(IncrOptions), typeof(DecrOptions), typeof(IncrByOptions), typeof(DecrByOptions), typeof(IncrByFloatOptions), typeof(GetRangeOptions), typeof(SetRangeOptions), typeof(StrLenOptions), typeof(MGetOptions), typeof(MSetOptions),
typeof(XAddOptions), typeof(XRangeOptions), typeof(XLenOptions), typeof(XDelOptions),
typeof(MultiOptions)
};
}
}
/// <summary>
/// Represents a command request sent from a client to the RedisManager service.
/// Contains the command name and arguments to be executed.
/// </summary>
public class ServiceRequest
{
/// <summary>
/// Gets or sets the name of the command to execute.
/// </summary>
public string Command { get; set; }
/// <summary>
/// Gets or sets the array of arguments for the command.
/// </summary>
public string[] Arguments { get; set; }
}
/// <summary>
/// Represents a response from the RedisManager service to a client request.
/// Contains the success status, result data, and any error information.
/// </summary>
public class ServiceResponse
{
/// <summary>
/// Gets or sets whether the command execution was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets the result data from the command execution.
/// Contains the output when Success is true.
/// </summary>
public string Data { get; set; }
/// <summary>
/// Gets or sets the error message if the command execution failed.
/// Contains the error details when Success is false.
/// </summary>
public string Error { get; set; }
/// <summary>
/// Gets or sets the error code (e.g., INVALID_REQUEST, REDIS_ERROR, INTERNAL_ERROR).
/// </summary>
public string ErrorCode { get; set; }
/// <summary>
/// Gets or sets additional error details (optional, can be any object).
/// </summary>
public object ErrorDetails { get; set; }
}
}

View File

@ -0,0 +1,14 @@
#!/bin/bash
# Usage: ./start_daemon_with_config.sh /path/to/redismanager.json
CONFIG_PATH="$1"
if [ -z "$CONFIG_PATH" ]; then
echo "Usage: $0 /path/to/redismanager.json"
exit 1
fi
# Start the daemon with the specified config file
# Assumes the daemon reads config via an environment variable
export REDISMANAGER_CONFIG="$CONFIG_PATH"
dotnet run --project ../RedisManager

View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -e
# Test SET command
set_output=$(dotnet run --project ../RedisManager.csproj set -i default testkey testvalue)
if [[ "$set_output" != *"OK"* ]]; then
echo "SET command failed: $set_output"
exit 1
fi
# Test GET command
get_output=$(dotnet run --project ../RedisManager.csproj get -i default testkey)
if [[ "$get_output" != *"testvalue"* ]]; then
echo "GET command failed: $get_output"
exit 1
fi
echo "String command test passed"

60
Utils/Output.cs Normal file
View File

@ -0,0 +1,60 @@
using System;
namespace RedisManager.Utils
{
/// <summary>
/// Utility class for formatting console output with ANSI color codes.
/// Provides methods to wrap text with different colors for enhanced readability.
/// </summary>
public static class Output
{
/// <summary>
/// Wraps the specified text with ANSI green color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with green ANSI color codes</returns>
public static string Green(string text) => $"\u001b[32m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI red color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with red ANSI color codes</returns>
public static string Red(string text) => $"\u001b[31m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI yellow color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with yellow ANSI color codes</returns>
public static string Yellow(string text) => $"\u001b[33m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI blue color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with blue ANSI color codes</returns>
public static string Blue(string text) => $"\u001b[34m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI cyan color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with cyan ANSI color codes</returns>
public static string Cyan(string text) => $"\u001b[36m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI magenta color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with magenta ANSI color codes</returns>
public static string Magenta(string text) => $"\u001b[35m{text}\u001b[0m";
/// <summary>
/// Wraps the specified text with ANSI white color codes.
/// </summary>
/// <param name="text">The text to colorize</param>
/// <returns>The text wrapped with white ANSI color codes</returns>
public static string White(string text) => $"\u001b[37m{text}\u001b[0m";
}
}

112
Utils/RedisUtils.cs Normal file
View File

@ -0,0 +1,112 @@
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
namespace RedisManager.Utils
{
/// <summary>
/// Provides utility methods for Redis operations, including instance retrieval, connection, array conversion, and table printing.
/// </summary>
public static class RedisUtils
{
/// <summary>
/// Retrieves an InstanceConfig by name from the provided configuration. Exits if not found.
/// </summary>
/// <param name="config">The configuration containing Redis instances.</param>
/// <param name="name">The name of the instance to retrieve.</param>
/// <returns>The matching InstanceConfig.</returns>
public static InstanceConfig GetInstance(Config config, string name)
{
var instance = config.Instances.Find(i => i.Name == name);
if (instance == null)
{
Console.WriteLine(Output.Red($"Instance '{name}' not found."));
Environment.Exit(1);
}
return instance;
}
/// <summary>
/// Connects to a Redis instance using the provided configuration.
/// </summary>
/// <param name="instance">The instance configuration to connect with.</param>
/// <returns>A connected ConnectionMultiplexer.</returns>
public static ConnectionMultiplexer ConnectRedis(InstanceConfig instance)
{
var options = new ConfigurationOptions
{
EndPoints = { $"{instance.Host}:{instance.Port}" },
Password = instance.Password
};
return ConnectionMultiplexer.Connect(options);
}
/// <summary>
/// Converts an enumerable of strings to a RedisValue array.
/// </summary>
/// <param name="values">The string values to convert.</param>
/// <returns>An array of RedisValue.</returns>
public static RedisValue[] ArrayFrom(IEnumerable<string> values)
{
var list = new List<RedisValue>();
foreach (var v in values)
list.Add(v);
return list.ToArray();
}
/// <summary>
/// Converts an enumerable of strings to a RedisKey array.
/// </summary>
/// <param name="values">The string values to convert.</param>
/// <returns>An array of RedisKey.</returns>
public static RedisKey[] ArrayFromKeys(IEnumerable<string> values)
{
var list = new List<RedisKey>();
foreach (var v in values)
list.Add(v);
return list.ToArray();
}
/// <summary>
/// Prints a formatted table to the console with headers and rows.
/// </summary>
/// <param name="headers">The column headers.</param>
/// <param name="rows">The table rows.</param>
public static void PrintTable(string[] headers, List<string[]> rows)
{
if (rows.Count == 0)
{
Console.WriteLine(Output.Yellow("No data"));
return;
}
// Calculate column widths
var widths = new int[headers.Length];
for (int i = 0; i < headers.Length; i++)
{
widths[i] = headers[i].Length;
foreach (var row in rows)
{
if (i < row.Length && row[i] != null)
widths[i] = Math.Max(widths[i], row[i].Length);
}
}
// Print header
Console.WriteLine(Output.Blue(string.Join(" | ", headers.Select((h, i) => h.PadRight(widths[i])))));
Console.WriteLine(Output.Blue(new string('-', widths.Sum() + (headers.Length - 1) * 3)));
// Print rows
foreach (var row in rows)
{
var formattedRow = new string[headers.Length];
for (int i = 0; i < headers.Length; i++)
{
formattedRow[i] = i < row.Length && row[i] != null ? row[i].PadRight(widths[i]) : "".PadRight(widths[i]);
}
Console.WriteLine(Output.Green(string.Join(" | ", formattedRow)));
}
}
}
}