diff --git a/CommandTests/01_setup.sh b/CommandTests/01_setup.sh new file mode 100755 index 0000000..1a001e9 --- /dev/null +++ b/CommandTests/01_setup.sh @@ -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 "" diff --git a/CommandTests/02_basic_string_tests.sh b/CommandTests/02_basic_string_tests.sh new file mode 100755 index 0000000..d59d3ad --- /dev/null +++ b/CommandTests/02_basic_string_tests.sh @@ -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 "" diff --git a/CommandTests/03_hash_tests.sh b/CommandTests/03_hash_tests.sh new file mode 100755 index 0000000..5db942a --- /dev/null +++ b/CommandTests/03_hash_tests.sh @@ -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!" diff --git a/CommandTests/04_geo_tests.sh b/CommandTests/04_geo_tests.sh new file mode 100755 index 0000000..3f41ddc --- /dev/null +++ b/CommandTests/04_geo_tests.sh @@ -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 "" diff --git a/CommandTests/05_bit_tests.sh b/CommandTests/05_bit_tests.sh new file mode 100755 index 0000000..a2f0caf --- /dev/null +++ b/CommandTests/05_bit_tests.sh @@ -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 "" diff --git a/CommandTests/06_module_tests.sh b/CommandTests/06_module_tests.sh new file mode 100755 index 0000000..6e30352 --- /dev/null +++ b/CommandTests/06_module_tests.sh @@ -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 "" diff --git a/CommandTests/07_list_tests.sh b/CommandTests/07_list_tests.sh new file mode 100755 index 0000000..c8c3a73 --- /dev/null +++ b/CommandTests/07_list_tests.sh @@ -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!" diff --git a/CommandTests/08_set_tests.sh b/CommandTests/08_set_tests.sh new file mode 100755 index 0000000..57de0fe --- /dev/null +++ b/CommandTests/08_set_tests.sh @@ -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!" diff --git a/CommandTests/09_sorted_set_tests.sh b/CommandTests/09_sorted_set_tests.sh new file mode 100755 index 0000000..511756e --- /dev/null +++ b/CommandTests/09_sorted_set_tests.sh @@ -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!" diff --git a/CommandTests/10_cleanup.sh b/CommandTests/10_cleanup.sh new file mode 100755 index 0000000..f716da7 --- /dev/null +++ b/CommandTests/10_cleanup.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Flushing the current Redis database..." +dotnet run -- flushdb -i localhost --yes diff --git a/CommandTests/README.md b/CommandTests/README.md new file mode 100644 index 0000000..e69de29 diff --git a/CommandTests/run_all_tests.sh b/CommandTests/run_all_tests.sh new file mode 100755 index 0000000..9e29fd3 --- /dev/null +++ b/CommandTests/run_all_tests.sh @@ -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 "==========================================" diff --git a/Commands/AdvancedStringCommands.cs b/Commands/AdvancedStringCommands.cs new file mode 100644 index 0000000..f8d7e98 --- /dev/null +++ b/Commands/AdvancedStringCommands.cs @@ -0,0 +1,302 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// 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. + /// + [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 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 + { + /// + /// Executes the APPEND command to append a value to a key. + /// + /// The AppendOptions containing instance, key, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the INCR command to increment the integer value of a key by one. + /// + /// The IncrOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the DECR command to decrement the integer value of a key by one. + /// + /// The DecrOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the INCRBY command to increment the integer value of a key by a given amount. + /// + /// The IncrByOptions containing instance, key, and amount. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the DECRBY command to decrement the integer value of a key by a given amount. + /// + /// The DecrByOptions containing instance, key, and amount. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the INCRBYFLOAT command to increment the float value of a key by a given amount. + /// + /// The IncrByFloatOptions containing instance, key, and amount. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the GETRANGE command to get a substring of the string stored at a key. + /// + /// The GetRangeOptions containing instance, key, start, and end. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the SETRANGE command to overwrite part of a string at key starting at the specified offset. + /// + /// The SetRangeOptions containing instance, key, offset, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the STRLEN command to get the length of the value stored in a key. + /// + /// The StrLenOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the MGET command to get the values of all the given keys. + /// + /// The MGetOptions containing instance and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + /// + /// Executes the MSET command to set multiple keys to multiple values. + /// + /// The MSetOptions containing instance and pairs. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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(p[0].Trim(), p[1].Trim())).ToArray(); + db.StringSet(pairs); + Console.WriteLine(Output.Green("OK")); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/BitCommands.cs b/Commands/BitCommands.cs new file mode 100644 index 0000000..6742983 --- /dev/null +++ b/Commands/BitCommands.cs @@ -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 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 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 + { + /// + /// Executes the BITCOUNT command to count set bits in a string. + /// + /// The BitCountOptions containing instance, key, start, and end. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the BITFIELD command to perform arbitrary bitfield integer operations on strings. + /// + /// The BitFieldOptions containing instance, key, and operations. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the BITOP command to perform bitwise operations between strings. + /// + /// The BitOpOptions containing instance, operation, destination key, and source keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { opts.Operation, opts.DestKey }; + args.AddRange(opts.SourceKeys); + var result = db.Execute("BITOP", args.ToArray()); + Console.WriteLine(Output.Green($"BITOP result: {result}")); + return 0; + } + + /// + /// Executes the BITPOS command to find the first bit set or clear in a string. + /// + /// The BitPosOptions containing instance, key, bit, start, and end. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the GETBIT command to return the bit value at offset in the string value stored at key. + /// + /// The GetBitOptions containing instance, key, and offset. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SETBIT command to set or clear the bit at offset in the string value stored at key. + /// + /// The SetBitOptions containing instance, key, offset, and bit value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Commands/ConnectionCommands.cs b/Commands/ConnectionCommands.cs new file mode 100644 index 0000000..3f2ebee --- /dev/null +++ b/Commands/ConnectionCommands.cs @@ -0,0 +1,160 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// 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. + /// + [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 + { + /// + /// Executes the AUTH command to authenticate to the server. + /// + /// The AuthOptions containing instance and password. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the QUIT command to close the connection. + /// + /// The QuitOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the CLIENT LIST command to get the list of client connections. + /// + /// The ClientListOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the CLIENT KILL command to kill a client connection. + /// + /// The ClientKillOptions containing instance and client ID. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the CLIENT GETNAME command to get the current connection name. + /// + /// The ClientGetNameOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the CLIENT SETNAME command to set the current connection name. + /// + /// The ClientSetNameOptions containing instance and name. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Commands/DatabaseCommands.cs b/Commands/DatabaseCommands.cs new file mode 100644 index 0000000..7542076 --- /dev/null +++ b/Commands/DatabaseCommands.cs @@ -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 { 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 { 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 { 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 { new[] { "Current", size.ToString() } }); + else + Console.WriteLine(Output.Green(size.ToString())); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/GeoCommands.cs b/Commands/GeoCommands.cs new file mode 100644 index 0000000..ec806c6 --- /dev/null +++ b/Commands/GeoCommands.cs @@ -0,0 +1,242 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Contains command line options and implementations for Redis geospatial operations. + /// Provides functionality for GEOADD, GEORADIUS, GEODIST, GEOHASH, and GEOPOS commands. + /// + [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 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 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 Members { get; set; } + } + + public static class GeoCommands + { + /// + /// Executes the GEOADD command to add one or more geospatial items to a key. + /// + /// The GeoAddOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the GEORADIUS command to query a geospatial index for members within a radius. + /// + /// The GeoRadiusOptions containing instance, key, longitude, latitude, radius, and options. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the GEODIST command to return the distance between two members of a geospatial index. + /// + /// The GeoDistOptions containing instance, key, member1, member2, and unit. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the GEOHASH command to return geohash strings for members of a geospatial index. + /// + /// The GeoHashOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the GEOPOS command to return longitude and latitude of members of a geospatial index. + /// + /// The GeoPosOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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 + }; + } + } +} \ No newline at end of file diff --git a/Commands/HashCommands.cs b/Commands/HashCommands.cs new file mode 100644 index 0000000..d21ed82 --- /dev/null +++ b/Commands/HashCommands.cs @@ -0,0 +1,583 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// 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. + /// + [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 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 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 + { + /// + /// Executes the HGET command to get the value of a field in a hash. + /// + /// The HGetOptions containing instance, hash, and field. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the HSET command to set the value of a field in a hash. + /// + /// The HSetOptions containing instance, hash, field, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the HDEL command to delete a field from a hash. + /// + /// The HDelOptions containing instance, hash, and field. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the HGETALL command to get all fields and values in a hash. + /// + /// The HGetAllOptions containing instance and hash. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the HKEYS command to list all fields in a hash. + /// + /// The HKeysOptions containing instance and hash. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the HVALS command to list all values in a hash. + /// + /// The HValsOptions containing instance and hash. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the HLEN command to get the number of fields in a hash. + /// + /// The HLenOptions containing instance and hash. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, len.ToString() } }); + else + Console.WriteLine(Output.Green(len.ToString())); + return 0; + } + + /// + /// Executes the HEXISTS command to check if a field exists in a hash. + /// + /// The HExistsOptions containing instance, hash, and field. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, opts.Field, exists ? "Yes" : "No" } }); + else + Console.WriteLine(Output.Green(exists ? "1" : "0")); + return 0; + } + + /// + /// Executes the HINCRBY command to increment a field value in a hash by an integer. + /// + /// The HIncrByOptions containing instance, hash, field, and increment. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } }); + else + Console.WriteLine(Output.Green(result.ToString())); + return 0; + } + + /// + /// Executes the HINCRBYFLOAT command to increment a field value in a hash by a float. + /// + /// The HIncrByFloatOptions containing instance, hash, field, and increment. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } }); + else + Console.WriteLine(Output.Green(result.ToString())); + return 0; + } + + /// + /// Executes the HMSET command to set multiple fields in a hash. + /// + /// The HMSetOptions containing instance, hash, and field-value pairs. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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(); + 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; + } + + /// + /// Executes the HMGET command to get multiple field values from a hash. + /// + /// The HMGetOptions containing instance, hash, and fields. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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(); + 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; + } + + /// + /// Executes the HSETNX command to set a field value in a hash only if the field does not exist. + /// + /// The HSetNxOptions containing instance, hash, field, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, opts.Field, opts.Value, result ? "Set" : "Not set (field exists)" } }); + else + Console.WriteLine(Output.Green(result ? "1" : "0")); + return 0; + } + + /// + /// Executes the HSTRLEN command to get the string length of a hash field value. + /// + /// The HStrLenOptions containing instance, hash, and field. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Hash, opts.Field, length.ToString() } }); + else + Console.WriteLine(Output.Green(length.ToString())); + return 0; + } + + /// + /// Executes the HSCAN command to incrementally iterate hash fields. + /// + /// The HScanOptions containing instance, hash, and scan options. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Commands/HyperLogLogCommands.cs b/Commands/HyperLogLogCommands.cs new file mode 100644 index 0000000..01dac0d --- /dev/null +++ b/Commands/HyperLogLogCommands.cs @@ -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 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 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 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 { 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 { new[] { keys[0], count.ToString() } }); + else + RedisUtils.PrintTable(new[] { "Keys", "Combined Cardinality" }, new List { 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 { new[] { opts.DestKey, "OK" } }); + } + else + Console.WriteLine(Output.Green("OK")); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/InstanceCommands.cs b/Commands/InstanceCommands.cs new file mode 100644 index 0000000..79a05a3 --- /dev/null +++ b/Commands/InstanceCommands.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Commands/InstanceOptions.cs b/Commands/InstanceOptions.cs new file mode 100644 index 0000000..46b370a --- /dev/null +++ b/Commands/InstanceOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace RedisManager.Commands +{ + /// + /// Base options for commands that require a Redis instance. + /// + public class InstanceOptions + { + [Option('i', "instance", Required = true, HelpText = "Name of the Redis instance to use.")] + public string Instance { get; set; } + } +} diff --git a/Commands/KeyCommands.cs b/Commands/KeyCommands.cs new file mode 100644 index 0000000..24b6f06 --- /dev/null +++ b/Commands/KeyCommands.cs @@ -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 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 { 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 { 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 { 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 { 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 { 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 { 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 { new[] { nextCursor } }); + } + else + { + foreach (var key in keys) + Console.WriteLine(Output.Green(key)); + Console.WriteLine(Output.Yellow($"Next Cursor: {nextCursor}")); + } + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/ListCommands.cs b/Commands/ListCommands.cs new file mode 100644 index 0000000..6f00cb9 --- /dev/null +++ b/Commands/ListCommands.cs @@ -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 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 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 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 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 + { + /// + /// Executes the LPUSH command to push elements to the left of a list. + /// + /// The LPushOptions containing instance, key, and values. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, length.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}")); + return 0; + } + + /// + /// Executes the RPUSH command to push elements to the right of a list. + /// + /// The RPushOptions containing instance, key, and values. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, length.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}")); + return 0; + } + + /// + /// Executes the LPOP command to pop an element from the left of a list. + /// + /// The LPopOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, value.ToString() } }); + else + Console.WriteLine(Output.Green(value.ToString())); + return 0; + } + + /// + /// Executes the RPOP command to pop an element from the right of a list. + /// + /// The RPopOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, value.ToString() } }); + else + Console.WriteLine(Output.Green(value.ToString())); + return 0; + } + + /// + /// Executes the LRANGE command to get a range of elements from a list. + /// + /// The LRangeOptions containing instance, key, start, and stop. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the LLEN command to get the length of a list. + /// + /// The LLenOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, length.ToString() } }); + else + Console.WriteLine(Output.Green(length.ToString())); + return 0; + } + + /// + /// Executes the LINDEX command to get the element at a specific index in a list. + /// + /// The LIndexOptions containing instance, key, and index. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Index.ToString(), value.ToString() } }); + else + Console.WriteLine(Output.Green(value.ToString())); + return 0; + } + + /// + /// Executes the LSET command to set the element at a specific index in a list. + /// + /// The LSetOptions containing instance, key, index, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Index.ToString(), opts.Value, "OK" } }); + else + Console.WriteLine(Output.Green("OK")); + return 0; + } + + /// + /// Executes the LREM command to remove elements from a list. + /// + /// The LRemOptions containing instance, key, count, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Value, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Removed {count} element(s)")); + return 0; + } + + /// + /// Executes the LINSERT command to insert an element before or after a pivot in a list. + /// + /// The LInsertOptions containing instance, key, position, pivot, and value. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Position, opts.Pivot, opts.Value, result.ToString() } }); + else + Console.WriteLine(Output.Green($"New length: {result}")); + return 0; + } + + /// + /// Executes the LTRIM command to trim a list to a specified range. + /// + /// The LTrimOptions containing instance, key, start, and stop. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Start.ToString(), opts.Stop.ToString(), "OK" } }); + else + Console.WriteLine(Output.Green("OK")); + return 0; + } + + /// + /// Executes the BLPOP command to perform a blocking pop from the left of a list. + /// + /// The BLPopOptions containing instance, keys, and timeout. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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().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 { new[] { key ?? "", value ?? "" } }); + } + else + { + Console.WriteLine(Output.Green($"{key}: {value}")); + } + } + else + { + Console.WriteLine(Output.Yellow("Unexpected result format.")); + } + return 0; + } + + /// + /// Executes the BRPOP command to perform a blocking pop from the right of a list. + /// + /// The BRPopOptions containing instance, keys, and timeout. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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().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 { new[] { key ?? "", value ?? "" } }); + } + else + { + Console.WriteLine(Output.Green($"{key}: {value}")); + } + } + else + { + Console.WriteLine(Output.Yellow("Unexpected result format.")); + } + return 0; + } + + /// + /// Executes the RPOPLPUSH command to pop from the right of the source and push to the left of the destination list. + /// + /// The RPopLPushOptions containing instance, source, and destination. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Source, opts.Destination, result.ToString() } }); + else + Console.WriteLine(Output.Green(result.ToString())); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/ModuleCommands.cs b/Commands/ModuleCommands.cs new file mode 100644 index 0000000..4066539 --- /dev/null +++ b/Commands/ModuleCommands.cs @@ -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 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 { "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; + } + } + } +} \ No newline at end of file diff --git a/Commands/PubSubCommands.cs b/Commands/PubSubCommands.cs new file mode 100644 index 0000000..a6e8b2b --- /dev/null +++ b/Commands/PubSubCommands.cs @@ -0,0 +1,119 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Threading; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Contains command line options and implementations for Redis Pub/Sub operations. + /// Provides functionality for PUBLISH, SUBSCRIBE, and UNSUBSCRIBE commands. + /// + [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 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 Channels { get; set; } + } + + public static class PubSubCommands + { + /// + /// Executes the PUBLISH command to publish a message to a channel. + /// + /// The PublishOptions containing instance, channel, and message. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Channel, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Published to {opts.Channel}, receivers: {count}")); + return 0; + } + + /// + /// Executes the SUBSCRIBE command to subscribe to one or more channels. + /// + /// The SubscribeOptions containing instance and channels. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the UNSUBSCRIBE command to unsubscribe from one or more channels. + /// + /// The UnsubscribeOptions containing instance and channels. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Commands/ScriptingCommands.cs b/Commands/ScriptingCommands.cs new file mode 100644 index 0000000..ad538a9 --- /dev/null +++ b/Commands/ScriptingCommands.cs @@ -0,0 +1,156 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Contains command line options and implementations for Redis scripting operations. + /// Provides functionality for EVAL, EVALSHA, SCRIPT LOAD, SCRIPT EXISTS, and SCRIPT FLUSH commands. + /// + [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 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 + { + /// + /// Executes the EVAL command to evaluate a Lua script. + /// + /// The EvalOptions containing instance, script, keys, and arguments. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray(); + var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty() : 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; + } + + /// + /// Executes the EVALSHA command to evaluate a Lua script by its SHA1 hash. + /// + /// The EvalShaOptions containing instance, sha1, keys, and arguments. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray(); + var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty() : 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; + } + + /// + /// Executes the SCRIPT LOAD command to load a Lua script into the script cache. + /// + /// The ScriptLoadOptions containing instance and script. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SCRIPT EXISTS command to check if scripts exist in the script cache. + /// + /// The ScriptExistsOptions containing instance and SHA1 hashes. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SCRIPT FLUSH command to remove all scripts from the script cache. + /// + /// The ScriptFlushOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Commands/ServerCommands.cs b/Commands/ServerCommands.cs new file mode 100644 index 0000000..3a25fd5 --- /dev/null +++ b/Commands/ServerCommands.cs @@ -0,0 +1,296 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Contains command line options and implementations for Redis server operations. + /// Provides functionality for INFO, CONFIG, SAVE, BGSAVE, LASTSAVE, TIME, and PING commands. + /// + [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 + { + /// + /// Executes the INFO command to get information and statistics about the server. + /// + /// The InfoOptions containing instance and section. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the CONFIG command to get or set Redis configuration parameters. + /// + /// The ConfigOptions containing instance, get, set, and table options. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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(); + 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 { 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; + } + + /// + /// Executes the SAVE command to synchronously save the dataset to disk. + /// + /// The SaveOptions containing instance and table option. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { "SAVE", "OK" } }); + else + Console.WriteLine(Output.Green("OK")); + return 0; + } + + /// + /// Executes the BGSAVE command to asynchronously save the dataset to disk. + /// + /// The BgSaveOptions containing instance and table option. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { "BGSAVE", "Background save started" } }); + else + Console.WriteLine(Output.Green("Background save started")); + return 0; + } + + /// + /// Executes the LASTSAVE command to get the timestamp of the last successful save. + /// + /// The LastSaveOptions containing instance and table option. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { "Last Save", timestamp, dateTime.ToString() } }); + else + Console.WriteLine(Output.Green($"{timestamp} ({dateTime})")); + return 0; + } + + /// + /// Executes the TIME command to return the current server time. + /// + /// The TimeOptions containing instance and table option. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { timestamp, microseconds, dateTime.ToString() } }); + else + Console.WriteLine(Output.Green($"{timestamp} {microseconds} ({dateTime})")); + return 0; + } + + /// + /// Executes the PING command to ping the server. + /// + /// The PingOptions containing instance and table option. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { "PING", response.ToString() } }); + else + Console.WriteLine(Output.Green(response.ToString())); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/SetCommands.cs b/Commands/SetCommands.cs new file mode 100644 index 0000000..e6e026d --- /dev/null +++ b/Commands/SetCommands.cs @@ -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 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 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 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 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 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 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 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 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 + { + /// + /// Executes the SADD command to add members to a set. + /// + /// The SAddOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Added {count} new member(s)")); + return 0; + } + + /// + /// Executes the SREM command to remove members from a set. + /// + /// The SRemOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Removed {count} member(s)")); + return 0; + } + + /// + /// Executes the SMEMBERS command to get all members of a set. + /// + /// The SMembersOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SCARD command to get the number of members in a set. + /// + /// The SCardOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + else + Console.WriteLine(Output.Green(count.ToString())); + return 0; + } + + /// + /// Executes the SISMEMBER command to check if a member exists in a set. + /// + /// The SIsMemberOptions containing instance, key, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Member, exists.ToString() } }); + else + Console.WriteLine(Output.Green(exists.ToString())); + return 0; + } + + /// + /// Executes the SRANDMEMBER command to get random member(s) from a set. + /// + /// The SRandMemberOptions containing instance, key, and count. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, member.ToString() } }); + else + Console.WriteLine(Output.Green(member.ToString())); + } + return 0; + } + + /// + /// Executes the SPOP command to remove and return random member(s) from a set. + /// + /// The SPopOptions containing instance, key, and count. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, member.ToString() } }); + else + Console.WriteLine(Output.Green(member.ToString())); + } + return 0; + } + + /// + /// Executes the SINTER command to get the intersection of multiple sets. + /// + /// The SInterOptions containing instance and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SUNION command to get the union of multiple sets. + /// + /// The SUnionOptions containing instance and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SDIFF command to get the difference of multiple sets. + /// + /// The SDiffOptions containing instance and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the SINTERSTORE command to store the intersection of multiple sets. + /// + /// The SInterStoreOptions containing instance, destination, and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Destination, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}")); + return 0; + } + + /// + /// Executes the SUNIONSTORE command to store the union of multiple sets. + /// + /// The SUnionStoreOptions containing instance, destination, and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Destination, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}")); + return 0; + } + + /// + /// Executes the SDIFFSTORE command to store the difference of multiple sets. + /// + /// The SDiffStoreOptions containing instance, destination, and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Destination, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}")); + return 0; + } + + /// + /// Executes the SSCAN command to scan set members. + /// + /// The SScanOptions containing instance, key, cursor, and scan options. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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 { new[] { newCursor.ToString() } }); + } + else + { + foreach (var member in members) + Console.WriteLine(Output.Green(member.ToString())); + Console.WriteLine(Output.Yellow($"Next cursor: {newCursor}")); + } + return 0; + } + + /// + /// Executes the SMOVE command to move a member from one set to another. + /// + /// The SMoveOptions containing instance, source, destination, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Source, opts.Destination, opts.Member, moved.ToString() } }); + else + Console.WriteLine(Output.Green(moved.ToString())); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/SortedSetCommands.cs b/Commands/SortedSetCommands.cs new file mode 100644 index 0000000..6058835 --- /dev/null +++ b/Commands/SortedSetCommands.cs @@ -0,0 +1,836 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// 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. + /// + [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 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 Keys { get; set; } + [Option("weights", Required = false, HelpText = "Weights for each source set.")] + public IEnumerable 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 Keys { get; set; } + [Option("weights", Required = false, HelpText = "Weights for each source set.")] + public IEnumerable 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 + { + /// + /// Executes the ZADD command to add members to a sorted set. + /// + /// The ZAddOptions containing instance, key, score, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Added {count} new member(s)")); + return 0; + } + + /// + /// Executes the ZREM command to remove members from a sorted set. + /// + /// The ZRemOptions containing instance, key, and members. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + } + else + Console.WriteLine(Output.Green($"Removed {count} member(s)")); + return 0; + } + + /// + /// Executes the ZRANGE command to get a range of members by rank. + /// + /// The ZRangeOptions containing instance, key, start, and stop. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the ZREVRANGE command to get a range of members by rank in reverse order. + /// + /// The ZRevRangeOptions containing instance, key, start, and stop. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the ZRANGEBYSCORE command to get a range of members by score. + /// + /// The ZRangeByScoreOptions containing instance, key, min, and max. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the ZCARD command to get the number of members in a sorted set. + /// + /// The ZCardOptions containing instance and key. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + else + Console.WriteLine(Output.Green(count.ToString())); + return 0; + } + + /// + /// Executes the ZSCORE command to get the score of a member in a sorted set. + /// + /// The ZScoreOptions containing instance, key, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the ZRANK command to get the rank of a member in a sorted set. + /// + /// The ZRankOptions containing instance, key, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the ZREVRANK command to get the reverse rank of a member in a sorted set. + /// + /// The ZRevRankOptions containing instance, key, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { 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; + } + + /// + /// Executes the ZINCRBY command to increment the score of a member in a sorted set. + /// + /// The ZIncrByOptions containing instance, key, increment, and member. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, opts.Member, newScore.ToString() } }); + else + Console.WriteLine(Output.Green(newScore.ToString())); + return 0; + } + + /// + /// Executes the ZREVRANGEBYSCORE command to get a range of members by score in reverse order. + /// + /// The ZRevRangeByScoreOptions containing instance, key, max, and min. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the ZCOUNT command to count members within a score range in a sorted set. + /// + /// The ZCountOptions containing instance, key, min, and max. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + else + Console.WriteLine(Output.Green(count.ToString())); + return 0; + } + + /// + /// Executes the ZUNIONSTORE command to store the union of multiple sorted sets. + /// + /// The ZUnionStoreOptions containing instance, destination, numkeys, and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Destination, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}")); + return 0; + } + + /// + /// Executes the ZINTERSTORE command to store the intersection of multiple sorted sets. + /// + /// The ZInterStoreOptions containing instance, destination, numkeys, and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Destination, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}")); + return 0; + } + + /// + /// Executes the ZSCAN command to scan sorted set members. + /// + /// The ZScanOptions containing instance, key, cursor, and scan options. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the ZPOPMAX command to remove and return members with the highest scores. + /// + /// The ZPopMaxOptions containing instance, key, and count. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } }); + else + Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}")); + } + return 0; + } + + /// + /// Executes the ZPOPMIN command to remove and return members with the lowest scores. + /// + /// The ZPopMinOptions containing instance, key, and count. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } }); + else + Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}")); + } + return 0; + } + + /// + /// Executes the ZREMRANGEBYRANK command to remove members by rank range. + /// + /// The ZRemRangeByRankOptions containing instance, key, start, and stop. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Removed {count} member(s)")); + return 0; + } + + /// + /// Executes the ZREMRANGEBYSCORE command to remove members by score range. + /// + /// The ZRemRangeByScoreOptions containing instance, key, min, and max. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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 { new[] { opts.Key, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Removed {count} member(s)")); + return 0; + } + } +} \ No newline at end of file diff --git a/Commands/StatusCommand.cs b/Commands/StatusCommand.cs new file mode 100644 index 0000000..8ed76ef --- /dev/null +++ b/Commands/StatusCommand.cs @@ -0,0 +1,46 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Command line options for the status command. + /// Used to check the connectivity and response time of a Redis instance. + /// + [Verb("status", HelpText = "Check status of a Redis instance.")] + public class StatusOptions + { + /// + /// Gets or sets the name of the Redis instance to check. + /// This must match a configured instance name in the configuration file. + /// + [Option('i', "instance", Required = true, HelpText = "Instance name.")] + public string Instance { get; set; } + } + + /// + /// Static class containing the implementation for the status command. + /// Provides functionality to check Redis instance connectivity and response time. + /// + public static class StatusCommand + { + /// + /// Executes the status command to check Redis instance connectivity. + /// Connects to the specified Redis instance and measures the ping response time. + /// + /// The status command options containing the instance name + /// The RedisManager configuration containing instance details + /// Exit code (0 for success, non-zero for failure) + 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; + } + } +} \ No newline at end of file diff --git a/Commands/StreamCommands.cs b/Commands/StreamCommands.cs new file mode 100644 index 0000000..1ef224f --- /dev/null +++ b/Commands/StreamCommands.cs @@ -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 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 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 { 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 { 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 { 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 { 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 { 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 { new[] { opts.Stream, count.ToString() } }); + else + Console.WriteLine(Output.Green($"Deleted {count} entry(ies)")); + return 0; + } + + + } +} \ No newline at end of file diff --git a/Commands/StringCommands.cs b/Commands/StringCommands.cs new file mode 100644 index 0000000..41c5b8a --- /dev/null +++ b/Commands/StringCommands.cs @@ -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 +{ + /// + /// Command line options for the GET command. + /// Used to retrieve the value of a Redis key. + /// + [Verb("get", HelpText = "Get value of a key.")] + public class GetOptions : InstanceOptions + { + /// + /// Gets or sets the key name to retrieve the value for. + /// + [Value(0, MetaName = "key", Required = true, HelpText = "Key name.")] + public string Key { get; set; } + + /// + /// Gets or sets whether to output the result in table format. + /// + [Option("table", Required = false, HelpText = "Output as table.")] + public bool Table { get; set; } + } + + /// + /// Command line options for the SET command. + /// Used to set the value of a Redis key with optional TTL. + /// + [Verb("set", HelpText = "Set value of a key.")] + public class SetOptions : InstanceOptions + { + /// + /// Gets or sets the key name to set the value for. + /// + [Value(0, MetaName = "key", Required = true, HelpText = "Key name.")] + public string Key { get; set; } + + /// + /// Gets or sets the value to store in the key. + /// + [Value(1, MetaName = "value", Required = true, HelpText = "Value.")] + public string Value { get; set; } + + /// + /// Gets or sets the time to live in seconds for the key. + /// + [Option("ttl", Required = false, HelpText = "Time to live in seconds.")] + public int? TTL { get; set; } + } + + /// + /// Command line options for the DEL command. + /// Used to delete one or more Redis keys with optional confirmation. + /// + [Verb("del", HelpText = "Delete one or more keys.")] + public class DelOptions : InstanceOptions + { + /// + /// Gets or sets whether to skip the confirmation prompt. + /// + [Option("yes", Required = false, HelpText = "Skip confirmation.")] + public bool Yes { get; set; } + + /// + /// Gets or sets the collection of keys to delete. + /// + [Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Key(s) to delete.")] + public IEnumerable Keys { get; set; } + + /// + /// Gets or sets whether to output the result in table format. + /// + [Option("table", Required = false, HelpText = "Output as table.")] + public bool Table { get; set; } + } + + /// + /// Static class containing implementations for basic Redis string operations. + /// Provides functionality for GET, SET, and DEL commands with various output formats. + /// + public static class StringCommands + { + /// + /// Executes the GET command to retrieve a value from Redis. + /// Supports table output format and JSON pretty-printing. + /// + /// The GET command options + /// The RedisManager configuration + /// Exit code (0 for success, non-zero for failure) + 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 { 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; + } + + /// + /// Executes the SET command to store a value in Redis. + /// Supports optional TTL (time to live) for the key. + /// + /// The SET command options + /// The RedisManager configuration + /// Exit code (0 for success, non-zero for failure) + 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; + } + + /// + /// Executes the DEL command to delete one or more keys from Redis. + /// Supports confirmation prompts and table output format. + /// + /// The DEL command options + /// The RedisManager configuration + /// Exit code (0 for success, non-zero for failure) + 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 { new[] { string.Join(", ", opts.Keys), count.ToString() } }); + else + Console.WriteLine(Output.Green($"Deleted {count} key(s)")); + return 0; + } + + /// + /// Determines if a string appears to be JSON formatted. + /// Checks if the string starts and ends with curly braces or square brackets. + /// + /// The string to check for JSON formatting + /// True if the string appears to be JSON, false otherwise + private static bool IsJson(string str) + { + str = str.Trim(); + return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]")); + } + } +} \ No newline at end of file diff --git a/Commands/TransactionCommands.cs b/Commands/TransactionCommands.cs new file mode 100644 index 0000000..6b546ab --- /dev/null +++ b/Commands/TransactionCommands.cs @@ -0,0 +1,134 @@ +using CommandLine; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using RedisManager.Utils; + +namespace RedisManager.Commands +{ + /// + /// Contains command line options and implementations for Redis transaction operations. + /// Provides functionality for MULTI, EXEC, DISCARD, WATCH, and UNWATCH commands. + /// + [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 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 + { + /// + /// Executes the MULTI command to mark the start of a transaction block. + /// + /// The MultiOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the EXEC command to execute all commands issued after MULTI. + /// + /// The ExecOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the DISCARD command to flush all commands issued after MULTI. + /// + /// The DiscardOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the WATCH command to watch the given keys for a transaction block. + /// + /// The WatchOptions containing instance and keys. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + + /// + /// Executes the UNWATCH command to forget about all watched keys. + /// + /// The UnwatchOptions containing instance. + /// The RedisManager configuration. + /// Exit code (0 for success, non-zero for failure). + 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; + } + } +} \ No newline at end of file diff --git a/Models/Config.cs b/Models/Config.cs new file mode 100644 index 0000000..9b785ad --- /dev/null +++ b/Models/Config.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace RedisManager +{ + /// + /// Represents configuration for a single Redis instance, including connection details. + /// + public class InstanceConfig + { + /// + /// Custom Redis configuration parameters to be applied at startup. + /// + public Dictionary CustomConfig { get; set; } = new Dictionary(); + /// + /// Optional: Path to the redis/valkey server binary for this instance. + /// If not set, the global default will be used. + /// + public string ServerBinaryPath { get; set; } + /// + /// The name of the Redis instance. + /// + public string Name { get; set; } + /// + /// The host address of the Redis instance. + /// + public string Host { get; set; } + /// + /// The port number for the Redis instance (default 6379). + /// + public int Port { get; set; } = 6379; + /// + /// The password for authenticating with the Redis instance. + /// + public string Password { get; set; } + } + + /// + /// Represents the configuration containing all Redis instances. + /// + public class Config + { + /// + /// Optional: Default path to the redis/valkey server binary for all instances. + /// + public string ServerBinaryPath { get; set; } + /// + /// The list of Redis instance configurations. + /// + public List Instances { get; set; } = new List(); + } + + /// + /// Provides methods to load and save RedisManager configuration from disk. + /// + public static class ConfigManager + { + public static string ConfigPath = "redismanager.json"; + + /// + /// Loads the configuration from disk, or creates a default if not present. + /// + /// The loaded or default Config object. + public static Config LoadConfig(string path) + { + if (!File.Exists(path)) + { + var defaultConfig = new Config + { + Instances = new List + { + new InstanceConfig + { + Name = "default", + Host = "localhost", + Port = 6379 + } + } + }; + SaveConfig(defaultConfig, path); + return defaultConfig; + } + + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json) ?? new Config(); + } + + public static Config LoadConfig() + { + return LoadConfig(ConfigPath); + } + + /// + /// Saves the provided configuration to disk as JSON. + /// + /// The configuration to save. + 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); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..3f28e58 --- /dev/null +++ b/Program.cs @@ -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 +{ + /// + /// Main entry point for the RedisManager application. + /// Supports service (daemon) mode and client mode only. + /// + class Program + { + /// + /// Main entry point that determines the operational mode based on command line arguments. + /// + /// Command line arguments + /// Exit code (0 for success, non-zero for errors) + static async Task Main(string[] args) + { + // No arguments: run as a daemon (service mode) + // Service mode: allow optional --config + 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; + } + + /// + /// 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. + /// + /// Exit code (0 for successful shutdown) + static async Task 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; + } + } +} diff --git a/README.md b/README.md index e69de29..9608ee4 100644 --- a/README.md +++ b/README.md @@ -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 -- `. + +--- + +## 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. \ No newline at end of file diff --git a/RedisManager.csproj b/RedisManager.csproj new file mode 100644 index 0000000..2cfe571 --- /dev/null +++ b/RedisManager.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + diff --git a/RedisManagerClient.cs b/RedisManagerClient.cs new file mode 100644 index 0000000..5f39178 --- /dev/null +++ b/RedisManagerClient.cs @@ -0,0 +1,79 @@ +using System; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace RedisManager +{ + /// + /// Client for communicating with the RedisManager service. + /// Connects to a running service instance and sends command requests over TCP. + /// + public class RedisManagerClient + { + private readonly string _host = "localhost"; + private readonly int _port = 6380; // Same port as service + + /// + /// Executes a Redis command by sending it to the RedisManager service. + /// Connects to the service, sends the command arguments, and displays the result. + /// + /// Command line arguments to execute on the service + /// Exit code (0 for success, 1 for failure) + public async Task 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(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; + } + } + } +} \ No newline at end of file diff --git a/RedisManagerService.ApplyCustomConfig.cs b/RedisManagerService.ApplyCustomConfig.cs new file mode 100644 index 0000000..d4ba338 --- /dev/null +++ b/RedisManagerService.ApplyCustomConfig.cs @@ -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}"); + } + } + } +} diff --git a/RedisManagerService.cs b/RedisManagerService.cs new file mode 100644 index 0000000..4cc521e --- /dev/null +++ b/RedisManagerService.cs @@ -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 +{ + /// + /// 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. + /// + public class RedisManagerService + { + private TcpListener _listener; + private readonly ConcurrentDictionary _clients = new(); + private Config _config; + private readonly object _configLock = new(); + // Track Valkey/Redis server processes started by the daemon + private readonly ConcurrentDictionary _instanceProcesses = new(); + private bool _isRunning = false; + private readonly int _port = 6380; // Port for service communication + + /// + /// Initializes a new instance of the RedisManagerService. + /// Loads configuration and prepares the service for operation. + /// + public RedisManagerService() + { + _config = ConfigManager.LoadConfig(); + } + + /// + /// Starts the TCP server and begins accepting client connections. + /// The server runs indefinitely until StopAsync is called. + /// + /// A task that represents the asynchronous start operation + 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 instancesSnapshot; + lock (_configLock) + { + instancesSnapshot = new List(_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 + } + } + } + + /// + /// Stops the TCP server and closes all client connections. + /// + /// A task that represents the asynchronous stop operation + public async Task StopAsync() + { + if (!_isRunning) return; + + _isRunning = false; + _listener?.Stop(); + Console.WriteLine("RedisManager Service stopped"); + } + + /// + /// Handles a single client connection asynchronously. + /// Processes JSON-formatted command requests and sends back responses. + /// + /// The TCP client connection to handle + /// A task that represents the asynchronous client handling operation + 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(); + } + } + + /// + /// Executes a Redis command from a JSON-formatted request. + /// + /// JSON string containing the command request + /// A ServiceResponse object with the command execution results + private async Task ExecuteCommandAsync(string commandJson) + { + try + { + var request = JsonSerializer.Deserialize(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}"); + } + } + + /// + /// Executes Redis commands by parsing arguments and routing to appropriate command handlers. + /// Captures console output and returns it as a string. + /// + /// Command line arguments to execute + /// The captured console output from command execution + 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(); + 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(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 ", "ARGUMENT_ERROR", new { Command = "get", Args = filteredArgs }); + break; + case "set": + if (filteredArgs.Count < 3) + return (false, "'set' command requires at least 2 arguments: set [EX seconds] [PX milliseconds] [NX|XX]", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs }); + // Validate optional flags for set + var setFlags = new HashSet(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 [key2 ...]", "ARGUMENT_ERROR", new { Command = "del", Args = filteredArgs }); + break; + case "hget": + if (filteredArgs.Count != 3) + return (false, "'hget' command requires exactly 2 arguments: hget ", "ARGUMENT_ERROR", new { Command = "hget", Args = filteredArgs }); + break; + case "hset": + if (filteredArgs.Count != 4) + return (false, "'hset' command requires exactly 3 arguments: hset ", "ARGUMENT_ERROR", new { Command = "hset", Args = filteredArgs }); + break; + case "mget": + if (filteredArgs.Count < 2) + return (false, "'mget' command requires at least 1 argument: mget [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 [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} [value2 ...]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs }); + break; + case "lrange": + if (filteredArgs.Count != 4) + return (false, "'lrange' command requires exactly 3 arguments: lrange ", "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 [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 [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 [key2 ...]", "ARGUMENT_ERROR", new { Command = "exists", Args = filteredArgs }); + break; + case "expire": + if (filteredArgs.Count != 3) + return (false, "'expire' command requires exactly 2 arguments: expire ", "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 ", "ARGUMENT_ERROR", new { Command = "rename", Args = filteredArgs }); + break; + case "select": + if (filteredArgs.Count != 2) + return (false, "'select' command requires exactly 1 argument: select ", "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 ", "ARGUMENT_ERROR", new { Command = "auth", Args = filteredArgs }); + break; + case "keys": + if (filteredArgs.Count != 2) + return (false, "'keys' command requires exactly 1 argument: keys ", "ARGUMENT_ERROR", new { Command = "keys", Args = filteredArgs }); + break; + case "ttl": + if (filteredArgs.Count != 2) + return (false, "'ttl' command requires exactly 1 argument: ttl ", "ARGUMENT_ERROR", new { Command = "ttl", Args = filteredArgs }); + break; + case "type": + if (filteredArgs.Count != 2) + return (false, "'type' command requires exactly 1 argument: type ", "ARGUMENT_ERROR", new { Command = "type", Args = filteredArgs }); + break; + case "persist": + if (filteredArgs.Count != 2) + return (false, "'persist' command requires exactly 1 argument: persist ", "ARGUMENT_ERROR", new { Command = "persist", Args = filteredArgs }); + break; + case "scard": + if (filteredArgs.Count != 2) + return (false, "'scard' command requires exactly 1 argument: scard ", "ARGUMENT_ERROR", new { Command = "scard", Args = filteredArgs }); + break; + case "smembers": + if (filteredArgs.Count != 2) + return (false, "'smembers' command requires exactly 1 argument: smembers ", "ARGUMENT_ERROR", new { Command = "smembers", Args = filteredArgs }); + break; + case "sismember": + if (filteredArgs.Count != 3) + return (false, "'sismember' command requires exactly 2 arguments: sismember ", "ARGUMENT_ERROR", new { Command = "sismember", Args = filteredArgs }); + break; + case "srem": + if (filteredArgs.Count < 3) + return (false, "'srem' command requires at least 2 arguments: srem [member2 ...]", "ARGUMENT_ERROR", new { Command = "srem", Args = filteredArgs }); + break; + case "zcard": + if (filteredArgs.Count != 2) + return (false, "'zcard' command requires exactly 1 argument: zcard ", "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} [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 ", "ARGUMENT_ERROR", new { Command = "zscore", Args = filteredArgs }); + break; + case "zrem": + if (filteredArgs.Count < 3) + return (false, "'zrem' command requires at least 2 arguments: zrem [member2 ...]", "ARGUMENT_ERROR", new { Command = "zrem", Args = filteredArgs }); + break; + case "zcount": + if (filteredArgs.Count != 4) + return (false, "'zcount' command requires exactly 3 arguments: zcount ", "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} ", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs }); + break; + case "hincrby": + if (filteredArgs.Count != 4) + return (false, "'hincrby' command requires exactly 3 arguments: hincrby ", "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 ", "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 [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 [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 [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} ", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs }); + break; + case "move": + if (filteredArgs.Count != 3) + return (false, "'move' command requires exactly 2 arguments: move ", "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 ", "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(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] : ""; + return (false, $"Unknown or invalid command: '{attemptedCommand}'. Arguments: [{string.Join(", ", args)}]"); + } + return (true, output); + } + catch (Exception ex) + { + return (false, $"[Daemon] Exception: {ex}"); + } + } + + /// + /// 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. + /// + /// Array of Type objects representing all supported command options + 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) + }; + } + } + + /// + /// Represents a command request sent from a client to the RedisManager service. + /// Contains the command name and arguments to be executed. + /// + public class ServiceRequest + { + /// + /// Gets or sets the name of the command to execute. + /// + public string Command { get; set; } + + /// + /// Gets or sets the array of arguments for the command. + /// + public string[] Arguments { get; set; } + } + + /// + /// Represents a response from the RedisManager service to a client request. + /// Contains the success status, result data, and any error information. + /// + public class ServiceResponse + { + /// + /// Gets or sets whether the command execution was successful. + /// + public bool Success { get; set; } + + /// + /// Gets or sets the result data from the command execution. + /// Contains the output when Success is true. + /// + public string Data { get; set; } + + /// + /// Gets or sets the error message if the command execution failed. + /// Contains the error details when Success is false. + /// + public string Error { get; set; } + + /// + /// Gets or sets the error code (e.g., INVALID_REQUEST, REDIS_ERROR, INTERNAL_ERROR). + /// + public string ErrorCode { get; set; } + + /// + /// Gets or sets additional error details (optional, can be any object). + /// + public object ErrorDetails { get; set; } + } +} \ No newline at end of file diff --git a/ServiceTest/start_daemon_with_config.sh b/ServiceTest/start_daemon_with_config.sh new file mode 100644 index 0000000..dfc21a4 --- /dev/null +++ b/ServiceTest/start_daemon_with_config.sh @@ -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 diff --git a/ServiceTest/string_command_test.sh b/ServiceTest/string_command_test.sh new file mode 100755 index 0000000..fe6d31c --- /dev/null +++ b/ServiceTest/string_command_test.sh @@ -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" \ No newline at end of file diff --git a/Utils/Output.cs b/Utils/Output.cs new file mode 100644 index 0000000..af2b0e1 --- /dev/null +++ b/Utils/Output.cs @@ -0,0 +1,60 @@ +using System; + +namespace RedisManager.Utils +{ + /// + /// Utility class for formatting console output with ANSI color codes. + /// Provides methods to wrap text with different colors for enhanced readability. + /// + public static class Output + { + /// + /// Wraps the specified text with ANSI green color codes. + /// + /// The text to colorize + /// The text wrapped with green ANSI color codes + public static string Green(string text) => $"\u001b[32m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI red color codes. + /// + /// The text to colorize + /// The text wrapped with red ANSI color codes + public static string Red(string text) => $"\u001b[31m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI yellow color codes. + /// + /// The text to colorize + /// The text wrapped with yellow ANSI color codes + public static string Yellow(string text) => $"\u001b[33m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI blue color codes. + /// + /// The text to colorize + /// The text wrapped with blue ANSI color codes + public static string Blue(string text) => $"\u001b[34m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI cyan color codes. + /// + /// The text to colorize + /// The text wrapped with cyan ANSI color codes + public static string Cyan(string text) => $"\u001b[36m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI magenta color codes. + /// + /// The text to colorize + /// The text wrapped with magenta ANSI color codes + public static string Magenta(string text) => $"\u001b[35m{text}\u001b[0m"; + + /// + /// Wraps the specified text with ANSI white color codes. + /// + /// The text to colorize + /// The text wrapped with white ANSI color codes + public static string White(string text) => $"\u001b[37m{text}\u001b[0m"; + } +} \ No newline at end of file diff --git a/Utils/RedisUtils.cs b/Utils/RedisUtils.cs new file mode 100644 index 0000000..0bd374a --- /dev/null +++ b/Utils/RedisUtils.cs @@ -0,0 +1,112 @@ +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RedisManager.Utils +{ + /// + /// Provides utility methods for Redis operations, including instance retrieval, connection, array conversion, and table printing. + /// + public static class RedisUtils + { + /// + /// Retrieves an InstanceConfig by name from the provided configuration. Exits if not found. + /// + /// The configuration containing Redis instances. + /// The name of the instance to retrieve. + /// The matching InstanceConfig. + 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; + } + + /// + /// Connects to a Redis instance using the provided configuration. + /// + /// The instance configuration to connect with. + /// A connected ConnectionMultiplexer. + public static ConnectionMultiplexer ConnectRedis(InstanceConfig instance) + { + var options = new ConfigurationOptions + { + EndPoints = { $"{instance.Host}:{instance.Port}" }, + Password = instance.Password + }; + return ConnectionMultiplexer.Connect(options); + } + + /// + /// Converts an enumerable of strings to a RedisValue array. + /// + /// The string values to convert. + /// An array of RedisValue. + public static RedisValue[] ArrayFrom(IEnumerable values) + { + var list = new List(); + foreach (var v in values) + list.Add(v); + return list.ToArray(); + } + + /// + /// Converts an enumerable of strings to a RedisKey array. + /// + /// The string values to convert. + /// An array of RedisKey. + public static RedisKey[] ArrayFromKeys(IEnumerable values) + { + var list = new List(); + foreach (var v in values) + list.Add(v); + return list.ToArray(); + } + + /// + /// Prints a formatted table to the console with headers and rows. + /// + /// The column headers. + /// The table rows. + public static void PrintTable(string[] headers, List 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))); + } + } + } +} \ No newline at end of file