add project
This commit is contained in:
11
CommandTests/01_setup.sh
Executable file
11
CommandTests/01_setup.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Redis Manager Test Setup ==="
|
||||||
|
echo "Verifying existing Redis instances..."
|
||||||
|
|
||||||
|
# List instances to verify
|
||||||
|
echo "Listing configured instances:"
|
||||||
|
dotnet run -- list-instances
|
||||||
|
|
||||||
|
echo "Setup complete!"
|
||||||
|
echo ""
|
||||||
46
CommandTests/02_basic_string_tests.sh
Executable file
46
CommandTests/02_basic_string_tests.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Basic String Commands Test ==="
|
||||||
|
|
||||||
|
echo "1. Testing SET and GET..."
|
||||||
|
dotnet run -- set "test:string" "Hello World" -i default
|
||||||
|
dotnet run -- get "test:string" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Testing APPEND..."
|
||||||
|
dotnet run -- append "test:string" " - Appended!" -i default
|
||||||
|
dotnet run -- get "test:string" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Testing INCR/DECR..."
|
||||||
|
dotnet run -- set "test:counter" "10" -i default
|
||||||
|
dotnet run -- incr "test:counter" -i default
|
||||||
|
dotnet run -- incrby "test:counter" "5" -i default
|
||||||
|
dotnet run -- decr "test:counter" -i default
|
||||||
|
dotnet run -- get "test:counter" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. Testing INCRBYFLOAT..."
|
||||||
|
dotnet run -- set "test:float" "10.5" -i default
|
||||||
|
dotnet run -- incrbyfloat "test:float" "2.3" -i default
|
||||||
|
dotnet run -- get "test:float" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "5. Testing GETRANGE/SETRANGE..."
|
||||||
|
dotnet run -- set "test:range" "Hello World" -i default
|
||||||
|
dotnet run -- getrange "test:range" "0" "4" -i default
|
||||||
|
dotnet run -- setrange "test:range" "6" "Redis" -i default
|
||||||
|
dotnet run -- get "test:range" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "6. Testing STRLEN..."
|
||||||
|
dotnet run -- strlen "test:range" -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "7. Testing MGET/MSET..."
|
||||||
|
dotnet run -- mset --pairs key1=value1,key2=value2,key3=value3 -i default
|
||||||
|
dotnet run -- mget key1 key2 key3 -i default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Basic String Commands Test Complete!"
|
||||||
|
echo ""
|
||||||
62
CommandTests/03_hash_tests.sh
Executable file
62
CommandTests/03_hash_tests.sh
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing Hash Commands ==="
|
||||||
|
|
||||||
|
# Test HSET and HGET
|
||||||
|
echo "Testing HSET and HGET..."
|
||||||
|
dotnet run -- hset user:1000 name "John Doe" -i localhost
|
||||||
|
dotnet run -- hset user:1000 email "john@example.com" -i localhost
|
||||||
|
dotnet run -- hset user:1000 age "30" -i localhost
|
||||||
|
|
||||||
|
dotnet run -- hget user:1000 name -i localhost
|
||||||
|
dotnet run -- hget user:1000 email -i localhost
|
||||||
|
dotnet run -- hget user:1000 age -i localhost
|
||||||
|
|
||||||
|
# Test HGETALL
|
||||||
|
echo "Testing HGETALL..."
|
||||||
|
dotnet run -- hgetall user:1000 -i localhost
|
||||||
|
|
||||||
|
# Test HKEYS and HVALS
|
||||||
|
echo "Testing HKEYS and HVALS..."
|
||||||
|
dotnet run -- hkeys user:1000 -i localhost
|
||||||
|
dotnet run -- hvals user:1000 -i localhost
|
||||||
|
|
||||||
|
# Test HEXISTS
|
||||||
|
echo "Testing HEXISTS..."
|
||||||
|
dotnet run -- hexists user:1000 name -i localhost
|
||||||
|
dotnet run -- hexists user:1000 phone -i localhost
|
||||||
|
|
||||||
|
# Test HLEN
|
||||||
|
echo "Testing HLEN..."
|
||||||
|
dotnet run -- hlen user:1000 -i localhost
|
||||||
|
|
||||||
|
# Test HINCRBY and HINCRBYFLOAT
|
||||||
|
echo "Testing HINCRBY and HINCRBYFLOAT..."
|
||||||
|
dotnet run -- hset user:1000 score "100" -i localhost
|
||||||
|
dotnet run -- hincrby user:1000 score "10" -i localhost
|
||||||
|
dotnet run -- hincrbyfloat user:1000 score "5.5" -i localhost
|
||||||
|
|
||||||
|
# Test HMSET and HMGET
|
||||||
|
echo "Testing HMSET and HMGET..."
|
||||||
|
dotnet run -- hmset user:1001 name "Jane Smith" email "jane@example.com" age "25" -i localhost
|
||||||
|
dotnet run -- hmget user:1001 name email age -i localhost
|
||||||
|
|
||||||
|
# Test HSETNX
|
||||||
|
echo "Testing HSETNX..."
|
||||||
|
dotnet run -- hsetnx user:1000 name "New Name" -i localhost
|
||||||
|
dotnet run -- hsetnx user:1000 phone "123-456-7890" -i localhost
|
||||||
|
|
||||||
|
# Test HSTRLEN
|
||||||
|
echo "Testing HSTRLEN..."
|
||||||
|
dotnet run -- hstrlen user:1000 name -i localhost
|
||||||
|
|
||||||
|
# Test HDEL
|
||||||
|
echo "Testing HDEL..."
|
||||||
|
dotnet run -- hdel user:1000 age -i localhost
|
||||||
|
dotnet run -- hgetall user:1000 -i localhost
|
||||||
|
|
||||||
|
# Test HSCAN
|
||||||
|
echo "Testing HSCAN..."
|
||||||
|
dotnet run -- hscan user:1000 0 -i localhost
|
||||||
|
|
||||||
|
echo "Hash tests completed!"
|
||||||
35
CommandTests/04_geo_tests.sh
Executable file
35
CommandTests/04_geo_tests.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Geo Commands Test ==="
|
||||||
|
|
||||||
|
echo "1. Testing GEOADD..."
|
||||||
|
dotnet run -- geoadd cities 2.3522 48.8566 London 2.2945 48.8584 Paris 13.4050 52.5200 Berlin 12.4964 41.9028 Rome -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Testing GEOHASH..."
|
||||||
|
dotnet run -- geohash cities London Paris -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Testing GEOPOS..."
|
||||||
|
dotnet run -- geopos cities London Berlin Rome -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. Testing GEODIST..."
|
||||||
|
dotnet run -- geodist cities London Paris --unit km -i localhost
|
||||||
|
dotnet run -- geodist cities London Berlin --unit mi -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "5. Testing GEORADIUS..."
|
||||||
|
dotnet run -- georadius cities 2.3522 48.8566 1000 --unit km -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "6. Testing GEOHASH with multiple members..."
|
||||||
|
dotnet run -- geohash cities London Paris Berlin Rome -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "7. Testing GEOPOS with multiple members..."
|
||||||
|
dotnet run -- geopos cities London Paris Berlin Rome -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Geo Commands Test Complete!"
|
||||||
|
echo ""
|
||||||
62
CommandTests/05_bit_tests.sh
Executable file
62
CommandTests/05_bit_tests.sh
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Bit Commands Test ==="
|
||||||
|
|
||||||
|
echo "1. Testing SETBIT and GETBIT..."
|
||||||
|
dotnet run -- setbit flags 0 1 -i localhost
|
||||||
|
dotnet run -- setbit flags 1 1 -i localhost
|
||||||
|
dotnet run -- setbit flags 2 0 -i localhost
|
||||||
|
dotnet run -- setbit flags 3 1 -i localhost
|
||||||
|
dotnet run -- getbit flags 0 -i localhost
|
||||||
|
dotnet run -- getbit flags 1 -i localhost
|
||||||
|
dotnet run -- getbit flags 2 -i localhost
|
||||||
|
dotnet run -- getbit flags 3 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Testing BITCOUNT..."
|
||||||
|
dotnet run -- bitcount flags -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Testing BITPOS..."
|
||||||
|
dotnet run -- bitpos flags 1 -i localhost
|
||||||
|
dotnet run -- bitpos flags 0 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. Testing BITFIELD..."
|
||||||
|
dotnet run -- bitfield numbers SET u8 0 255 GET u8 0 -i localhost
|
||||||
|
|
||||||
|
dotnet run -- bitfield numbers SET i16 8 32767 GET i16 8 -i localhost
|
||||||
|
|
||||||
|
dotnet run -- bitfield numbers INCRBY u8 0 1 GET u8 0 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "5. Testing BITOP..."
|
||||||
|
dotnet run -- setbit flags1 0 1 -i localhost
|
||||||
|
dotnet run -- setbit flags1 1 0 -i localhost
|
||||||
|
dotnet run -- setbit flags2 0 0 -i localhost
|
||||||
|
dotnet run -- setbit flags2 1 1 -i localhost
|
||||||
|
dotnet run -- bitop AND result flags1 flags2 -i localhost
|
||||||
|
dotnet run -- getbit result 0 -i localhost
|
||||||
|
dotnet run -- getbit result 1 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "6. Testing BITOP OR..."
|
||||||
|
dotnet run -- bitop OR result_or flags1 flags2 -i localhost
|
||||||
|
dotnet run -- getbit result_or 0 -i localhost
|
||||||
|
dotnet run -- getbit result_or 1 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "7. Testing BITOP XOR..."
|
||||||
|
dotnet run -- bitop XOR result_xor flags1 flags2 -i localhost
|
||||||
|
dotnet run -- getbit result_xor 0 -i localhost
|
||||||
|
dotnet run -- getbit result_xor 1 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "8. Testing BITOP NOT..."
|
||||||
|
dotnet run -- bitop NOT result_not flags1 -i localhost
|
||||||
|
dotnet run -- getbit result_not 0 -i localhost
|
||||||
|
dotnet run -- getbit result_not 1 -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Bit Commands Test Complete!"
|
||||||
|
echo ""
|
||||||
25
CommandTests/06_module_tests.sh
Executable file
25
CommandTests/06_module_tests.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Module Commands Test ==="
|
||||||
|
|
||||||
|
echo "1. Testing MODULE LIST..."
|
||||||
|
dotnet run -- module-list -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Testing MODULE LOAD (this may fail if no modules are available)..."
|
||||||
|
# Note: This will likely fail unless you have Redis modules available
|
||||||
|
dotnet run -- module-load "/path/to/module.so" "arg1" "arg2" -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Testing MODULE UNLOAD (this may fail if no modules are loaded)..."
|
||||||
|
# Note: This will likely fail unless you have modules loaded
|
||||||
|
dotnet run -- module-unload "testmodule" -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. Testing MODULE LIST again..."
|
||||||
|
dotnet run -- module-list -i localhost
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Module Commands Test Complete!"
|
||||||
|
echo "Note: Module commands may fail if no modules are available/loaded."
|
||||||
|
echo ""
|
||||||
70
CommandTests/07_list_tests.sh
Executable file
70
CommandTests/07_list_tests.sh
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing List Commands ==="
|
||||||
|
|
||||||
|
# Test LPUSH and RPUSH
|
||||||
|
echo "Testing LPUSH and RPUSH..."
|
||||||
|
dotnet run -- lpush mylist "first" -i localhost
|
||||||
|
dotnet run -- lpush mylist "second" -i localhost
|
||||||
|
dotnet run -- rpush mylist "third" -i localhost
|
||||||
|
dotnet run -- rpush mylist "fourth" -i localhost
|
||||||
|
|
||||||
|
# Test LLEN
|
||||||
|
echo "Testing LLEN..."
|
||||||
|
dotnet run -- llen mylist -i localhost
|
||||||
|
|
||||||
|
# Test LRANGE
|
||||||
|
echo "Testing LRANGE..."
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 1 -i localhost
|
||||||
|
dotnet run -- lrange mylist 1 2 -i localhost
|
||||||
|
|
||||||
|
# Test LINDEX
|
||||||
|
echo "Testing LINDEX..."
|
||||||
|
dotnet run -- lindex mylist 0 -i localhost
|
||||||
|
dotnet run -- lindex mylist 1 -i localhost
|
||||||
|
dotnet run -- lindex mylist -1 -i localhost
|
||||||
|
|
||||||
|
# Test LSET
|
||||||
|
echo "Testing LSET..."
|
||||||
|
dotnet run -- lset mylist 1 "updated_second" -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test LINSERT
|
||||||
|
echo "Testing LINSERT..."
|
||||||
|
dotnet run -- linsert mylist before "updated_second" "before_second" -i localhost
|
||||||
|
dotnet run -- linsert mylist after "updated_second" "after_second" -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test LREM
|
||||||
|
echo "Testing LREM..."
|
||||||
|
dotnet run -- lpush mylist "duplicate" -i localhost
|
||||||
|
dotnet run -- lpush mylist "duplicate" -i localhost
|
||||||
|
dotnet run -- lrem mylist 2 "duplicate" -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test LTRIM
|
||||||
|
echo "Testing LTRIM..."
|
||||||
|
dotnet run -- ltrim mylist 1 3 -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test LPOP and RPOP
|
||||||
|
echo "Testing LPOP and RPOP..."
|
||||||
|
dotnet run -- lpop mylist -i localhost
|
||||||
|
dotnet run -- rpop mylist -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test BLPOP and BRPOP (with timeout)
|
||||||
|
echo "Testing BLPOP and BRPOP..."
|
||||||
|
dotnet run -- blpop mylist 1 -i localhost
|
||||||
|
dotnet run -- brpop mylist 1 -i localhost
|
||||||
|
|
||||||
|
# Test RPOPLPUSH
|
||||||
|
echo "Testing RPOPLPUSH..."
|
||||||
|
dotnet run -- rpush mylist "last_item" -i localhost
|
||||||
|
dotnet run -- rpush otherlist "existing_item" -i localhost
|
||||||
|
dotnet run -- rpoplpush mylist otherlist -i localhost
|
||||||
|
dotnet run -- lrange mylist 0 -1 -i localhost
|
||||||
|
dotnet run -- lrange otherlist 0 -1 -i localhost
|
||||||
|
|
||||||
|
echo "List tests completed!"
|
||||||
72
CommandTests/08_set_tests.sh
Executable file
72
CommandTests/08_set_tests.sh
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing Set Commands ==="
|
||||||
|
|
||||||
|
# Test SADD and SMEMBERS
|
||||||
|
echo "Testing SADD and SMEMBERS..."
|
||||||
|
dotnet run -- sadd set1 "apple" -i localhost
|
||||||
|
dotnet run -- sadd set1 "banana" -i localhost
|
||||||
|
dotnet run -- sadd set1 "cherry" -i localhost
|
||||||
|
dotnet run -- smembers set1 -i localhost
|
||||||
|
|
||||||
|
# Test SISMEMBER
|
||||||
|
echo "Testing SISMEMBER..."
|
||||||
|
dotnet run -- sismember set1 "apple" -i localhost
|
||||||
|
dotnet run -- sismember set1 "orange" -i localhost
|
||||||
|
|
||||||
|
# Test SCARD
|
||||||
|
echo "Testing SCARD..."
|
||||||
|
dotnet run -- scard set1 -i localhost
|
||||||
|
|
||||||
|
# Test SPOP and SRANDMEMBER
|
||||||
|
echo "Testing SPOP and SRANDMEMBER..."
|
||||||
|
dotnet run -- sadd set2 "one" "two" "three" "four" "five" -i localhost
|
||||||
|
dotnet run -- spop set2 -i localhost
|
||||||
|
dotnet run -- srandmember set2 -i localhost
|
||||||
|
dotnet run -- srandmember set2 2 -i localhost
|
||||||
|
|
||||||
|
# Test SREM
|
||||||
|
echo "Testing SREM..."
|
||||||
|
dotnet run -- srem set1 "banana" -i localhost
|
||||||
|
dotnet run -- smembers set1 -i localhost
|
||||||
|
|
||||||
|
# Test Set operations with multiple sets
|
||||||
|
echo "Testing Set operations..."
|
||||||
|
dotnet run -- sadd set3 "apple" "banana" "grape" -i localhost
|
||||||
|
dotnet run -- sadd set4 "banana" "cherry" "date" -i localhost
|
||||||
|
|
||||||
|
# Test SINTER
|
||||||
|
echo "Testing SINTER..."
|
||||||
|
dotnet run -- sinter set1 set3 set4 -i localhost
|
||||||
|
|
||||||
|
# Test SUNION
|
||||||
|
echo "Testing SUNION..."
|
||||||
|
dotnet run -- sunion set1 set3 set4 -i localhost
|
||||||
|
|
||||||
|
# Test SDIFF
|
||||||
|
echo "Testing SDIFF..."
|
||||||
|
dotnet run -- sdiff set3 set4 -i localhost
|
||||||
|
|
||||||
|
# Test SINTERSTORE, SUNIONSTORE, SDIFFSTORE
|
||||||
|
echo "Testing SINTERSTORE, SUNIONSTORE, SDIFFSTORE..."
|
||||||
|
dotnet run -- sinterstore result1 set1 set3 -i localhost
|
||||||
|
dotnet run -- sunionstore result2 set1 set3 -i localhost
|
||||||
|
dotnet run -- sdiffstore result3 set3 set4 -i localhost
|
||||||
|
|
||||||
|
dotnet run -- smembers result1 -i localhost
|
||||||
|
dotnet run -- smembers result2 -i localhost
|
||||||
|
dotnet run -- smembers result3 -i localhost
|
||||||
|
|
||||||
|
# Test SSCAN
|
||||||
|
echo "Testing SSCAN..."
|
||||||
|
dotnet run -- sscan set3 0 -i localhost
|
||||||
|
|
||||||
|
# Test SMOVE
|
||||||
|
echo "Testing SMOVE..."
|
||||||
|
dotnet run -- sadd source_set "item1" "item2" "item3" -i localhost
|
||||||
|
dotnet run -- sadd dest_set "existing_item" -i localhost
|
||||||
|
dotnet run -- smove source_set dest_set "item2" -i localhost
|
||||||
|
dotnet run -- smembers source_set -i localhost
|
||||||
|
dotnet run -- smembers dest_set -i localhost
|
||||||
|
|
||||||
|
echo "Set tests completed!"
|
||||||
82
CommandTests/09_sorted_set_tests.sh
Executable file
82
CommandTests/09_sorted_set_tests.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Testing Sorted Set Commands ==="
|
||||||
|
|
||||||
|
# Test ZADD and ZRANGE
|
||||||
|
echo "Testing ZADD and ZRANGE..."
|
||||||
|
dotnet run -- zadd leaderboard 100 "player1" -i localhost
|
||||||
|
dotnet run -- zadd leaderboard 200 "player2" -i localhost
|
||||||
|
dotnet run -- zadd leaderboard 150 "player3" -i localhost
|
||||||
|
dotnet run -- zrange leaderboard 0 -1 -i localhost
|
||||||
|
dotnet run -- zrange leaderboard 0 -1 withscores -i localhost
|
||||||
|
|
||||||
|
# Test ZSCORE
|
||||||
|
echo "Testing ZSCORE..."
|
||||||
|
dotnet run -- zscore leaderboard "player1" -i localhost
|
||||||
|
dotnet run -- zscore leaderboard "player2" -i localhost
|
||||||
|
|
||||||
|
# Test ZRANK and ZREVRANK
|
||||||
|
echo "Testing ZRANK and ZREVRANK..."
|
||||||
|
dotnet run -- zrank leaderboard "player1" -i localhost
|
||||||
|
dotnet run -- zrevrank leaderboard "player1" -i localhost
|
||||||
|
|
||||||
|
# Test ZCARD
|
||||||
|
echo "Testing ZCARD..."
|
||||||
|
dotnet run -- zcard leaderboard -i localhost
|
||||||
|
|
||||||
|
# Test ZREM
|
||||||
|
echo "Testing ZREM..."
|
||||||
|
dotnet run -- zrem leaderboard "player2" -i localhost
|
||||||
|
dotnet run -- zrange leaderboard 0 -1 -i localhost
|
||||||
|
|
||||||
|
# Test ZRANGEBYSCORE and ZREVRANGEBYSCORE
|
||||||
|
echo "Testing ZRANGEBYSCORE and ZREVRANGEBYSCORE..."
|
||||||
|
dotnet run -- zadd scores 50 "user1" -i localhost
|
||||||
|
dotnet run -- zadd scores 75 "user2" -i localhost
|
||||||
|
dotnet run -- zadd scores 90 "user3" -i localhost
|
||||||
|
dotnet run -- zadd scores 120 "user4" -i localhost
|
||||||
|
|
||||||
|
dotnet run -- zrangebyscore scores 60 100 -i localhost
|
||||||
|
dotnet run -- zrangebyscore scores 60 100 withscores -i localhost
|
||||||
|
dotnet run -- zrevrangebyscore scores 100 60 -i localhost
|
||||||
|
|
||||||
|
# Test ZCOUNT
|
||||||
|
echo "Testing ZCOUNT..."
|
||||||
|
dotnet run -- zcount scores 60 100 -i localhost
|
||||||
|
|
||||||
|
# Test ZINCRBY
|
||||||
|
echo "Testing ZINCRBY..."
|
||||||
|
dotnet run -- zincrby scores 25 "user1" -i localhost
|
||||||
|
dotnet run -- zscore scores "user1" -i localhost
|
||||||
|
|
||||||
|
# Test ZUNIONSTORE and ZINTERSTORE
|
||||||
|
echo "Testing ZUNIONSTORE and ZINTERSTORE..."
|
||||||
|
dotnet run -- zadd set1 1 "a" 2 "b" 3 "c" -i localhost
|
||||||
|
dotnet run -- zadd set2 2 "b" 3 "c" 4 "d" -i localhost
|
||||||
|
|
||||||
|
dotnet run -- zunionstore union_result 2 set1 set2 -i localhost
|
||||||
|
dotnet run -- zinterstore inter_result 2 set1 set2 -i localhost
|
||||||
|
|
||||||
|
dotnet run -- zrange union_result 0 -1 withscores -i localhost
|
||||||
|
dotnet run -- zrange inter_result 0 -1 withscores -i localhost
|
||||||
|
|
||||||
|
# Test ZSCAN
|
||||||
|
echo "Testing ZSCAN..."
|
||||||
|
dotnet run -- zscan scores 0 -i localhost
|
||||||
|
|
||||||
|
# Test ZPOPMAX and ZPOPMIN
|
||||||
|
echo "Testing ZPOPMAX and ZPOPMIN..."
|
||||||
|
dotnet run -- zpopmax scores -i localhost
|
||||||
|
dotnet run -- zpopmin scores -i localhost
|
||||||
|
|
||||||
|
# Test ZREMRANGEBYRANK and ZREMRANGEBYSCORE
|
||||||
|
echo "Testing ZREMRANGEBYRANK and ZREMRANGEBYSCORE..."
|
||||||
|
dotnet run -- zadd test_set 10 "item1" 20 "item2" 30 "item3" 40 "item4" -i localhost
|
||||||
|
dotnet run -- zremrangebyrank test_set 1 2 -i localhost
|
||||||
|
dotnet run -- zrange test_set 0 -1 -i localhost
|
||||||
|
|
||||||
|
dotnet run -- zadd test_set2 10 "item1" 20 "item2" 30 "item3" 40 "item4" -i localhost
|
||||||
|
dotnet run -- zremrangebyscore test_set2 15 35 -i localhost
|
||||||
|
dotnet run -- zrange test_set2 0 -1 -i localhost
|
||||||
|
|
||||||
|
echo "Sorted Set tests completed!"
|
||||||
4
CommandTests/10_cleanup.sh
Executable file
4
CommandTests/10_cleanup.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Flushing the current Redis database..."
|
||||||
|
dotnet run -- flushdb -i localhost --yes
|
||||||
0
CommandTests/README.md
Normal file
0
CommandTests/README.md
Normal file
53
CommandTests/run_all_tests.sh
Executable file
53
CommandTests/run_all_tests.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Redis Manager - Complete Test Suite"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Make all scripts executable
|
||||||
|
chmod +x CommandTests/*.sh
|
||||||
|
|
||||||
|
echo "Running setup..."
|
||||||
|
./CommandTests/01_setup.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running basic string tests..."
|
||||||
|
./CommandTests/02_basic_string_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running hash tests..."
|
||||||
|
./CommandTests/03_hash_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running geo tests..."
|
||||||
|
./CommandTests/04_geo_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running bit tests..."
|
||||||
|
./CommandTests/05_bit_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running module tests..."
|
||||||
|
./CommandTests/06_module_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running list tests..."
|
||||||
|
./CommandTests/07_list_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running set tests..."
|
||||||
|
./CommandTests/08_set_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running sorted set tests..."
|
||||||
|
./CommandTests/09_sorted_set_tests.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running cleanup..."
|
||||||
|
./CommandTests/10_cleanup.sh
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "All tests completed!"
|
||||||
|
echo "=========================================="
|
||||||
302
Commands/AdvancedStringCommands.cs
Normal file
302
Commands/AdvancedStringCommands.cs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for advanced Redis string operations.
|
||||||
|
/// Provides functionality for APPEND, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT, GETRANGE, SETRANGE, STRLEN, MGET, and MSET commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("append", HelpText = "Append a value to a key.")]
|
||||||
|
public class AppendOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "value", Required = true, HelpText = "Value to append.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("incr", HelpText = "Increment the integer value of a key by one.")]
|
||||||
|
public class IncrOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("decr", HelpText = "Decrement the integer value of a key by one.")]
|
||||||
|
public class DecrOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("incrby", HelpText = "Increment the integer value of a key by a given amount.")]
|
||||||
|
public class IncrByOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to increment by.")]
|
||||||
|
public long Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("decrby", HelpText = "Decrement the integer value of a key by a given amount.")]
|
||||||
|
public class DecrByOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to decrement by.")]
|
||||||
|
public long Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("incrbyfloat", HelpText = "Increment the float value of a key by a given amount.")]
|
||||||
|
public class IncrByFloatOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "amount", Required = true, HelpText = "Amount to increment by.")]
|
||||||
|
public double Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("getrange", HelpText = "Get a substring of the string stored at a key.")]
|
||||||
|
public class GetRangeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start offset.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "end", Required = true, HelpText = "End offset.")]
|
||||||
|
public long End { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("setrange", HelpText = "Overwrite part of a string at key starting at the specified offset.")]
|
||||||
|
public class SetRangeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "offset", Required = true, HelpText = "Offset.")]
|
||||||
|
public long Offset { get; set; }
|
||||||
|
[Value(2, MetaName = "value", Required = true, HelpText = "Value to set.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("strlen", HelpText = "Get the length of the value stored in a key.")]
|
||||||
|
public class StrLenOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("mget", HelpText = "Get the values of all the given keys.")]
|
||||||
|
public class MGetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("mset", HelpText = "Set multiple keys to multiple values.")]
|
||||||
|
public class MSetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("pairs", Required = true, HelpText = "Comma-separated key=value pairs.")]
|
||||||
|
public string Pairs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AdvancedStringCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the APPEND command to append a value to a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The AppendOptions containing instance, key, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunAppend(AppendOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var len = db.StringAppend(opts.Key, opts.Value);
|
||||||
|
Console.WriteLine(Output.Green(len.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the INCR command to increment the integer value of a key by one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The IncrOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunIncr(IncrOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringIncrement(opts.Key);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the DECR command to decrement the integer value of a key by one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The DecrOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunDecr(DecrOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringDecrement(opts.Key);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the INCRBY command to increment the integer value of a key by a given amount.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The IncrByOptions containing instance, key, and amount.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunIncrBy(IncrByOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringIncrement(opts.Key, opts.Amount);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the DECRBY command to decrement the integer value of a key by a given amount.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The DecrByOptions containing instance, key, and amount.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunDecrBy(DecrByOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringDecrement(opts.Key, opts.Amount);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the INCRBYFLOAT command to increment the float value of a key by a given amount.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The IncrByFloatOptions containing instance, key, and amount.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunIncrByFloat(IncrByFloatOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringIncrement(opts.Key, opts.Amount);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GETRANGE command to get a substring of the string stored at a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GetRangeOptions containing instance, key, start, and end.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGetRange(GetRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var val = db.StringGetRange(opts.Key, opts.Start, opts.End);
|
||||||
|
Console.WriteLine(Output.Green(val.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SETRANGE command to overwrite part of a string at key starting at the specified offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SetRangeOptions containing instance, key, offset, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSetRange(SetRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var len = db.StringSetRange(opts.Key, opts.Offset, opts.Value);
|
||||||
|
Console.WriteLine(Output.Green(len.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the STRLEN command to get the length of the value stored in a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The StrLenOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunStrLen(StrLenOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var len = db.StringLength(opts.Key);
|
||||||
|
Console.WriteLine(Output.Green(len.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the MGET command to get the values of all the given keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The MGetOptions containing instance and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunMGet(MGetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var vals = db.StringGet(keys);
|
||||||
|
for (int i = 0; i < keys.Length; i++)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{keys[i]}: {vals[i]}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the MSET command to set multiple keys to multiple values.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The MSetOptions containing instance and pairs.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunMSet(MSetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var pairs = opts.Pairs.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2).Select(p => new KeyValuePair<RedisKey, RedisValue>(p[0].Trim(), p[1].Trim())).ToArray();
|
||||||
|
db.StringSet(pairs);
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
Commands/BitCommands.cs
Normal file
207
Commands/BitCommands.cs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("bitcount", HelpText = "Count set bits in a string.")]
|
||||||
|
public class BitCountOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("start", Default = 0L, HelpText = "Start byte.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Option("end", Default = -1L, HelpText = "End byte.")]
|
||||||
|
public long End { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("bitfield", HelpText = "Perform arbitrary bitfield integer operations on strings.")]
|
||||||
|
public class BitFieldOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "operations", Min = 1, Required = true, HelpText = "Bitfield operations.")]
|
||||||
|
public IEnumerable<string> Operations { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("bitop", HelpText = "Perform bitwise operations between strings.")]
|
||||||
|
public class BitOpOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "operation", Required = true, HelpText = "Operation (AND, OR, XOR, NOT).")]
|
||||||
|
public string Operation { get; set; }
|
||||||
|
[Value(1, MetaName = "dest-key", Required = true, HelpText = "Destination key.")]
|
||||||
|
public string DestKey { get; set; }
|
||||||
|
[Value(2, MetaName = "source-keys", Min = 1, Required = true, HelpText = "Source keys.")]
|
||||||
|
public IEnumerable<string> SourceKeys { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("bitpos", HelpText = "Find first bit set or clear in a string.")]
|
||||||
|
public class BitPosOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "bit", Required = true, HelpText = "Bit value (0 or 1).")]
|
||||||
|
public int Bit { get; set; }
|
||||||
|
[Option("start", Default = 0L, HelpText = "Start byte.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Option("end", Default = -1L, HelpText = "End byte.")]
|
||||||
|
public long End { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("getbit", HelpText = "Returns the bit value at offset in the string value stored at key.")]
|
||||||
|
public class GetBitOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "offset", Required = true, HelpText = "Bit offset.")]
|
||||||
|
public long Offset { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("setbit", HelpText = "Sets or clears the bit at offset in the string value stored at key.")]
|
||||||
|
public class SetBitOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "offset", Required = true, HelpText = "Bit offset.")]
|
||||||
|
public long Offset { get; set; }
|
||||||
|
[Value(2, MetaName = "bit", Required = true, HelpText = "Bit value (0 or 1).")]
|
||||||
|
public int Bit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BitCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BITCOUNT command to count set bits in a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BitCountOptions containing instance, key, start, and end.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBitCount(BitCountOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key };
|
||||||
|
if (opts.Start != 0 || opts.End != -1) { args.Add(opts.Start); args.Add(opts.End); }
|
||||||
|
var result = db.Execute("BITCOUNT", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"Bit count: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BITFIELD command to perform arbitrary bitfield integer operations on strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BitFieldOptions containing instance, key, and operations.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBitField(BitFieldOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key };
|
||||||
|
args.AddRange(opts.Operations);
|
||||||
|
var result = db.Execute("BITFIELD", args.ToArray());
|
||||||
|
RedisResult[] results;
|
||||||
|
if (result is Array)
|
||||||
|
results = (RedisResult[])result;
|
||||||
|
else
|
||||||
|
results = new RedisResult[] { result };
|
||||||
|
|
||||||
|
Console.WriteLine(Output.Green("Bitfield results:"));
|
||||||
|
var operationsList = opts.Operations.ToList();
|
||||||
|
for (int i = 0; results != null && i < results.Length; i++)
|
||||||
|
{
|
||||||
|
var res = results[i];
|
||||||
|
if (res != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Cyan($"{operationsList[i]}: {res}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"{operationsList[i]}: null"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BITOP command to perform bitwise operations between strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BitOpOptions containing instance, operation, destination key, and source keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBitOp(BitOpOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Operation, opts.DestKey };
|
||||||
|
args.AddRange(opts.SourceKeys);
|
||||||
|
var result = db.Execute("BITOP", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"BITOP result: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BITPOS command to find the first bit set or clear in a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BitPosOptions containing instance, key, bit, start, and end.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBitPos(BitPosOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key, opts.Bit };
|
||||||
|
if (opts.Start != 0 || opts.End != -1) { args.Add(opts.Start); args.Add(opts.End); }
|
||||||
|
var result = db.Execute("BITPOS", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"First {opts.Bit} bit at position: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GETBIT command to return the bit value at offset in the string value stored at key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GetBitOptions containing instance, key, and offset.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGetBit(GetBitOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.Execute("GETBIT", new object[] { opts.Key, opts.Offset });
|
||||||
|
Console.WriteLine(Output.Green($"Bit at offset {opts.Offset}: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SETBIT command to set or clear the bit at offset in the string value stored at key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SetBitOptions containing instance, key, offset, and bit value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSetBit(SetBitOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.Execute("SETBIT", new object[] { opts.Key, opts.Offset, opts.Bit });
|
||||||
|
Console.WriteLine(Output.Green($"Set bit at offset {opts.Offset} to {opts.Bit}: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
Commands/ConnectionCommands.cs
Normal file
160
Commands/ConnectionCommands.cs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis connection management operations.
|
||||||
|
/// Provides functionality for AUTH, QUIT, CLIENT LIST, CLIENT KILL, CLIENT GETNAME, and CLIENT SETNAME commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("auth", HelpText = "Authenticate to the server.")]
|
||||||
|
public class AuthOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "password", Required = true, HelpText = "Password.")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("quit", HelpText = "Close the connection.")]
|
||||||
|
public class QuitOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("clientlist", HelpText = "Get the list of client connections.")]
|
||||||
|
public class ClientListOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("clientkill", HelpText = "Kill a client connection.")]
|
||||||
|
public class ClientKillOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "client_id", Required = true, HelpText = "Client ID.")]
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("clientgetname", HelpText = "Get the current connection name.")]
|
||||||
|
public class ClientGetNameOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("clientsetname", HelpText = "Set the current connection name.")]
|
||||||
|
public class ClientSetNameOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "name", Required = true, HelpText = "Connection name.")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ConnectionCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the AUTH command to authenticate to the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The AuthOptions containing instance and password.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunAuth(AuthOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("AUTH", opts.Password);
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the QUIT command to close the connection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The QuitOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunQuit(QuitOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("QUIT");
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the CLIENT LIST command to get the list of client connections.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ClientListOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunClientList(ClientListOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.Execute("CLIENT", "LIST");
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the CLIENT KILL command to kill a client connection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ClientKillOptions containing instance and client ID.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunClientKill(ClientKillOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("CLIENT", "KILL", opts.ClientId);
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the CLIENT GETNAME command to get the current connection name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ClientGetNameOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunClientGetName(ClientGetNameOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.Execute("CLIENT", "GETNAME");
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the CLIENT SETNAME command to set the current connection name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ClientSetNameOptions containing instance and name.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunClientSetName(ClientSetNameOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("CLIENT", "SETNAME", opts.Name);
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
Commands/DatabaseCommands.cs
Normal file
121
Commands/DatabaseCommands.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("select", HelpText = "Change the selected database.")]
|
||||||
|
public class SelectOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "database", Required = true, HelpText = "Database number.")]
|
||||||
|
public int Database { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("flushdb", HelpText = "Remove all keys from the current database.")]
|
||||||
|
public class FlushDbOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Option("yes", Required = false, HelpText = "Skip confirmation prompt.")]
|
||||||
|
public bool Yes { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("flushall", HelpText = "Remove all keys from all databases.")]
|
||||||
|
public class FlushAllOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Option("yes", Required = false, HelpText = "Skip confirmation prompt.")]
|
||||||
|
public bool Yes { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("dbsize", HelpText = "Return the number of keys in the current database.")]
|
||||||
|
public class DbSizeOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DatabaseCommands
|
||||||
|
{
|
||||||
|
public static int RunSelect(SelectOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase(opts.Database);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Database", "Status" }, new List<string[]> { new[] { opts.Database.ToString(), "Selected" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunFlushDb(FlushDbOptions opts, Config config)
|
||||||
|
{
|
||||||
|
if (!opts.Yes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Are you sure you want to flush the current database? (y/N)"));
|
||||||
|
var response = Console.ReadLine()?.ToLower();
|
||||||
|
if (response != "y" && response != "yes")
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Operation cancelled."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("FLUSHDB");
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "FLUSHDB", "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunFlushAll(FlushAllOptions opts, Config config)
|
||||||
|
{
|
||||||
|
if (!opts.Yes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Are you sure you want to flush ALL databases? (y/N)"));
|
||||||
|
var response = Console.ReadLine()?.ToLower();
|
||||||
|
if (response != "y" && response != "yes")
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Operation cancelled."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("FLUSHALL");
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "FLUSHALL", "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunDbSize(DbSizeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var size = db.Execute("DBSIZE");
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Database", "Size" }, new List<string[]> { new[] { "Current", size.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(size.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
242
Commands/GeoCommands.cs
Normal file
242
Commands/GeoCommands.cs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis geospatial operations.
|
||||||
|
/// Provides functionality for GEOADD, GEORADIUS, GEODIST, GEOHASH, and GEOPOS commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("geoadd", HelpText = "Add one or more geospatial items to a key.")]
|
||||||
|
public class GeoAddOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 3, Required = true, HelpText = "Members with longitude,latitude,name format.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("georadius", HelpText = "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point.")]
|
||||||
|
public class GeoRadiusOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "longitude", Required = true, HelpText = "Longitude.")]
|
||||||
|
public double Longitude { get; set; }
|
||||||
|
[Value(2, MetaName = "latitude", Required = true, HelpText = "Latitude.")]
|
||||||
|
public double Latitude { get; set; }
|
||||||
|
[Value(3, MetaName = "radius", Required = true, HelpText = "Radius.")]
|
||||||
|
public double Radius { get; set; }
|
||||||
|
[Option("unit", Default = "m", HelpText = "Unit (m, km, mi, ft).")]
|
||||||
|
public string Unit { get; set; }
|
||||||
|
[Option("withcoord", HelpText = "Include coordinates in output.")]
|
||||||
|
public bool WithCoord { get; set; }
|
||||||
|
[Option("withdist", HelpText = "Include distance in output.")]
|
||||||
|
public bool WithDist { get; set; }
|
||||||
|
[Option("withhash", HelpText = "Include hash in output.")]
|
||||||
|
public bool WithHash { get; set; }
|
||||||
|
[Option("count", HelpText = "Limit results count.")]
|
||||||
|
public int Count { get; set; }
|
||||||
|
[Option("asc", HelpText = "Sort ascending by distance.")]
|
||||||
|
public bool Asc { get; set; }
|
||||||
|
[Option("desc", HelpText = "Sort descending by distance.")]
|
||||||
|
public bool Desc { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("geodist", HelpText = "Return the distance between two members of a geospatial index.")]
|
||||||
|
public class GeoDistOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "member1", Required = true, HelpText = "First member.")]
|
||||||
|
public string Member1 { get; set; }
|
||||||
|
[Value(2, MetaName = "member2", Required = true, HelpText = "Second member.")]
|
||||||
|
public string Member2 { get; set; }
|
||||||
|
[Option("unit", Default = "m", HelpText = "Unit (m, km, mi, ft).")]
|
||||||
|
public string Unit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("geohash", HelpText = "Returns members of a geospatial index as standard geohash strings.")]
|
||||||
|
public class GeoHashOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("geopos", HelpText = "Returns longitude and latitude of members of a geospatial index.")]
|
||||||
|
public class GeoPosOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GeoCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GEOADD command to add one or more geospatial items to a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GeoAddOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGeoAdd(GeoAddOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key };
|
||||||
|
var membersList = opts.Members.ToList();
|
||||||
|
for (int i = 0; i + 2 < membersList.Count; i += 3)
|
||||||
|
{
|
||||||
|
args.Add(membersList[i]); // longitude
|
||||||
|
args.Add(membersList[i + 1]); // latitude
|
||||||
|
args.Add(membersList[i + 2]); // name
|
||||||
|
}
|
||||||
|
var result = db.Execute("GEOADD", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"Added: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GEORADIUS command to query a geospatial index for members within a radius.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GeoRadiusOptions containing instance, key, longitude, latitude, radius, and options.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGeoRadius(GeoRadiusOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key, opts.Longitude, opts.Latitude, opts.Radius, opts.Unit };
|
||||||
|
if (opts.WithCoord) args.Add("WITHCOORD");
|
||||||
|
if (opts.WithDist) args.Add("WITHDIST");
|
||||||
|
if (opts.WithHash) args.Add("WITHHASH");
|
||||||
|
if (opts.Count > 0) { args.Add("COUNT"); args.Add(opts.Count); }
|
||||||
|
if (opts.Asc) args.Add("ASC");
|
||||||
|
if (opts.Desc) args.Add("DESC");
|
||||||
|
var result = db.Execute("GEORADIUS", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"Raw result: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GEODIST command to return the distance between two members of a geospatial index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GeoDistOptions containing instance, key, member1, member2, and unit.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGeoDist(GeoDistOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.Execute("GEODIST", new object[] { opts.Key, opts.Member1, opts.Member2, opts.Unit });
|
||||||
|
Console.WriteLine(Output.Green($"Distance: {result} {opts.Unit}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GEOHASH command to return geohash strings for members of a geospatial index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GeoHashOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGeoHash(GeoHashOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key };
|
||||||
|
args.AddRange(opts.Members);
|
||||||
|
var result = db.Execute("GEOHASH", args.ToArray());
|
||||||
|
RedisResult[] hashes;
|
||||||
|
if (result is Array)
|
||||||
|
hashes = (RedisResult[])result;
|
||||||
|
else
|
||||||
|
hashes = new RedisResult[] { result };
|
||||||
|
var membersList = opts.Members.ToList();
|
||||||
|
for (int i = 0; i < membersList.Count; i++)
|
||||||
|
{
|
||||||
|
var hash = hashes != null && i < hashes.Length ? hashes[i]?.ToString() : null;
|
||||||
|
if (!string.IsNullOrEmpty(hash))
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{membersList[i]}: {hash}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"{membersList[i]}: not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GEOPOS command to return longitude and latitude of members of a geospatial index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GeoPosOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunGeoPos(GeoPosOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Key };
|
||||||
|
args.AddRange(opts.Members);
|
||||||
|
var result = db.Execute("GEOPOS", args.ToArray());
|
||||||
|
RedisResult[] positions;
|
||||||
|
if (result is Array)
|
||||||
|
positions = (RedisResult[])result;
|
||||||
|
else
|
||||||
|
positions = new RedisResult[] { result };
|
||||||
|
var membersList = opts.Members.ToList();
|
||||||
|
for (int i = 0; i < membersList.Count; i++)
|
||||||
|
{
|
||||||
|
var pos = positions != null && i < positions.Length ? positions[i]?.ToString() : null;
|
||||||
|
if (!string.IsNullOrEmpty(pos))
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{membersList[i]}: {pos}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"{membersList[i]}: not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GeoUnit GetGeoUnit(string unit)
|
||||||
|
{
|
||||||
|
return unit.ToLower() switch
|
||||||
|
{
|
||||||
|
"km" => GeoUnit.Kilometers,
|
||||||
|
"mi" => GeoUnit.Miles,
|
||||||
|
"ft" => GeoUnit.Feet,
|
||||||
|
_ => GeoUnit.Meters
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
583
Commands/HashCommands.cs
Normal file
583
Commands/HashCommands.cs
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis hash operations.
|
||||||
|
/// Provides functionality for HGET, HSET, HDEL, HGETALL, HKEYS, HVALS, HLEN, HEXISTS, HINCRBY, HINCRBYFLOAT, HMSET, HMGET, HSETNX, HSTRLEN, and HSCAN commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("hget", HelpText = "Get value of a field in a hash.")]
|
||||||
|
public class HGetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hset", HelpText = "Set value of a field in a hash.")]
|
||||||
|
public class HSetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Value(2, MetaName = "value", Required = true, HelpText = "Value.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[Option("ttl", Required = false, HelpText = "Time to live in seconds.")]
|
||||||
|
public int? TTL { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hdel", HelpText = "Delete a field in a hash.")]
|
||||||
|
public class HDelOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hgetall", HelpText = "Get all fields and values in a hash.")]
|
||||||
|
public class HGetAllOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hkeys", HelpText = "List all fields in a hash.")]
|
||||||
|
public class HKeysOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hvals", HelpText = "List all values in a hash.")]
|
||||||
|
public class HValsOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hlen", HelpText = "Get the number of fields in a hash.")]
|
||||||
|
public class HLenOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hexists", HelpText = "Check if a field exists in a hash.")]
|
||||||
|
public class HExistsOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hincrby", HelpText = "Increment field value in hash by integer.")]
|
||||||
|
public class HIncrByOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Value(2, MetaName = "increment", Required = true, HelpText = "Increment value.")]
|
||||||
|
public long Increment { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hincrbyfloat", HelpText = "Increment field value in hash by float.")]
|
||||||
|
public class HIncrByFloatOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Value(2, MetaName = "increment", Required = true, HelpText = "Increment value.")]
|
||||||
|
public double Increment { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hmset", HelpText = "Set multiple fields in a hash.")]
|
||||||
|
public class HMSetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "fields", Min = 1, Required = true, HelpText = "Field-value pairs (field1 value1 field2 value2...).")]
|
||||||
|
public IEnumerable<string> Fields { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hmget", HelpText = "Get multiple field values from a hash.")]
|
||||||
|
public class HMGetOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "fields", Min = 1, Required = true, HelpText = "Field names to get.")]
|
||||||
|
public IEnumerable<string> Fields { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hsetnx", HelpText = "Set field value in hash only if field does not exist.")]
|
||||||
|
public class HSetNxOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Value(2, MetaName = "value", Required = true, HelpText = "Value.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hstrlen", HelpText = "Get the string length of a hash field value.")]
|
||||||
|
public class HStrLenOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "field", Required = true, HelpText = "Field name.")]
|
||||||
|
public string Field { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("hscan", HelpText = "Incrementally iterate hash fields.")]
|
||||||
|
public class HScanOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "hash", Required = true, HelpText = "Hash key.")]
|
||||||
|
public string Hash { get; set; }
|
||||||
|
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
|
||||||
|
public long Cursor { get; set; }
|
||||||
|
[Option("match", Required = false, HelpText = "Pattern to match.")]
|
||||||
|
public string Match { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Count hint.")]
|
||||||
|
public long? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HashCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HGET command to get the value of a field in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HGetOptions containing instance, hash, and field.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHGet(HGetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var value = db.HashGet(opts.Hash, opts.Field);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, new List<string[]> { new[] { opts.Hash, opts.Field, value.IsNull ? "[null]" : value.ToString() } });
|
||||||
|
else if (value.IsNull)
|
||||||
|
Console.WriteLine(Output.Yellow($"[null]"));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HSET command to set the value of a field in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HSetOptions containing instance, hash, field, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHSet(HSetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var ok = db.HashSet(opts.Hash, opts.Field, opts.Value);
|
||||||
|
if (opts.TTL.HasValue)
|
||||||
|
db.KeyExpire(opts.Hash, TimeSpan.FromSeconds(opts.TTL.Value));
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Value, ok ? "OK" : "Failed" } });
|
||||||
|
else if (ok)
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Red("Failed to set field."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HDEL command to delete a field from a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HDelOptions containing instance, hash, and field.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHDel(HDelOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var ok = db.HashDelete(opts.Hash, opts.Field);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, ok ? "Deleted" : "Not found" } });
|
||||||
|
else if (ok)
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Yellow("Field not found."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HGETALL command to get all fields and values in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HGetAllOptions containing instance and hash.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHGetAll(HGetAllOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var entries = db.HashGetAll(opts.Hash);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new[] { e.Name.ToString(), e.Value.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Field", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var entry in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Name}: {entry.Value}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HKEYS command to list all fields in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HKeysOptions containing instance and hash.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHKeys(HKeysOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = db.HashKeys(opts.Hash);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = keys.Select(k => new[] { k.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Field" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var key in keys)
|
||||||
|
Console.WriteLine(Output.Green(key.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HVALS command to list all values in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HValsOptions containing instance and hash.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHVals(HValsOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var values = db.HashValues(opts.Hash);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = values.Select(v => new[] { v.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var value in values)
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HLEN command to get the number of fields in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HLenOptions containing instance and hash.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHLen(HLenOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var len = db.HashLength(opts.Hash);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Length" }, new List<string[]> { new[] { opts.Hash, len.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(len.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HEXISTS command to check if a field exists in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HExistsOptions containing instance, hash, and field.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHExists(HExistsOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var exists = db.HashExists(opts.Hash, opts.Field);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Exists" }, new List<string[]> { new[] { opts.Hash, opts.Field, exists ? "Yes" : "No" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(exists ? "1" : "0"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HINCRBY command to increment a field value in a hash by an integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HIncrByOptions containing instance, hash, field, and increment.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHIncrBy(HIncrByOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.HashIncrement(opts.Hash, opts.Field, opts.Increment);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Increment", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HINCRBYFLOAT command to increment a field value in a hash by a float.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HIncrByFloatOptions containing instance, hash, field, and increment.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHIncrByFloat(HIncrByFloatOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.HashIncrement(opts.Hash, opts.Field, opts.Increment);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Increment", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Increment.ToString(), result.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HMSET command to set multiple fields in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HMSetOptions containing instance, hash, and field-value pairs.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHMSet(HMSetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var fieldsList = opts.Fields.ToList();
|
||||||
|
if (fieldsList.Count % 2 != 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Error: Fields must be provided as field-value pairs."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new HashEntry[fieldsList.Count / 2];
|
||||||
|
for (int i = 0; i < fieldsList.Count; i += 2)
|
||||||
|
{
|
||||||
|
entries[i / 2] = new HashEntry(fieldsList[i], fieldsList[i + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.HashSet(opts.Hash, entries);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = new List<string[]>();
|
||||||
|
for (int i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
rows.Add(new[] { opts.Hash, entries[i].Name.ToString(), entries[i].Value.ToString() });
|
||||||
|
}
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HMGET command to get multiple field values from a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HMGetOptions containing instance, hash, and fields.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHMGet(HMGetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var fields = opts.Fields.Select(f => (RedisValue)f).ToArray();
|
||||||
|
var values = db.HashGet(opts.Hash, fields);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = new List<string[]>();
|
||||||
|
var fieldsList = opts.Fields.ToList();
|
||||||
|
for (int i = 0; i < fieldsList.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i].IsNull ? "[null]" : values[i].ToString();
|
||||||
|
rows.Add(new[] { opts.Hash, fieldsList[i], value });
|
||||||
|
}
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var fieldsList = opts.Fields.ToList();
|
||||||
|
for (int i = 0; i < fieldsList.Count; i++)
|
||||||
|
{
|
||||||
|
var value = values[i].IsNull ? "[null]" : values[i].ToString();
|
||||||
|
Console.WriteLine(Output.Green($"{fieldsList[i]}: {value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HSETNX command to set a field value in a hash only if the field does not exist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HSetNxOptions containing instance, hash, field, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHSetNx(HSetNxOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.HashSet(opts.Hash, opts.Field, opts.Value, When.NotExists);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Value", "Result" }, new List<string[]> { new[] { opts.Hash, opts.Field, opts.Value, result ? "Set" : "Not set (field exists)" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(result ? "1" : "0"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HSTRLEN command to get the string length of a hash field value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HStrLenOptions containing instance, hash, and field.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHStrLen(HStrLenOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var length = db.HashStringLength(opts.Hash, opts.Field);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Hash", "Field", "Length" }, new List<string[]> { new[] { opts.Hash, opts.Field, length.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(length.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the HSCAN command to incrementally iterate hash fields.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The HScanOptions containing instance, hash, and scan options.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunHScan(HScanOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var pattern = opts.Match;
|
||||||
|
var pageSize = opts.Count.HasValue ? (int)opts.Count.Value : 10;
|
||||||
|
var scanResult = db.HashScan(opts.Hash, pattern, pageSize);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = scanResult.Select(e => new[] { e.Name.ToString(), e.Value.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Field", "Value" }, rows);
|
||||||
|
Console.WriteLine(Output.Yellow($"Scanned {rows.Count} entries"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
foreach (var entry in scanResult)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Name}: {entry.Value}"));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
Console.WriteLine(Output.Yellow($"Scanned {count} entries"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
Commands/HyperLogLogCommands.cs
Normal file
117
Commands/HyperLogLogCommands.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("pfadd", HelpText = "Add elements to a HyperLogLog.")]
|
||||||
|
public class PFAddOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "HyperLogLog key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "elements", Min = 1, Required = true, HelpText = "Elements to add.")]
|
||||||
|
public IEnumerable<string> Elements { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("pfcount", HelpText = "Get the approximated cardinality of HyperLogLog(s).")]
|
||||||
|
public class PFCountOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "HyperLogLog key(s).")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("pfmerge", HelpText = "Merge multiple HyperLogLogs into a single one.")]
|
||||||
|
public class PFMergeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "destkey", Required = true, HelpText = "Destination key.")]
|
||||||
|
public string DestKey { get; set; }
|
||||||
|
[Value(1, MetaName = "sourcekeys", Min = 1, Required = true, HelpText = "Source HyperLogLog keys.")]
|
||||||
|
public IEnumerable<string> SourceKeys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HyperLogLogCommands
|
||||||
|
{
|
||||||
|
public static int RunPFAdd(PFAddOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var elements = opts.Elements.ToArray();
|
||||||
|
|
||||||
|
// Use PFADD command directly
|
||||||
|
var batch = db.CreateBatch();
|
||||||
|
var tasks = elements.Select(e => batch.ExecuteAsync("PFADD", opts.Key, e)).ToArray();
|
||||||
|
batch.Execute();
|
||||||
|
|
||||||
|
var count = tasks.Count(t => (bool)t.Result);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = elements.Select(e => new[] { e, "Added" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Element", "Result" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Added {count} new element(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunPFCount(PFCountOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = opts.Keys.ToArray();
|
||||||
|
|
||||||
|
// Use PFCOUNT command directly
|
||||||
|
var count = db.Execute("PFCOUNT", keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
if (keys.Length == 1)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { keys[0], count.ToString() } });
|
||||||
|
else
|
||||||
|
RedisUtils.PrintTable(new[] { "Keys", "Combined Cardinality" }, new List<string[]> { new[] { string.Join(", ", keys), count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(count.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunPFMerge(PFMergeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var sourceKeys = opts.SourceKeys.ToArray();
|
||||||
|
|
||||||
|
// Use PFMERGE command directly
|
||||||
|
db.Execute("PFMERGE", opts.DestKey, sourceKeys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = sourceKeys.Select(k => new[] { k, "Merged" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Source Key", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination Key", "Status" }, new List<string[]> { new[] { opts.DestKey, "OK" } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Commands/InstanceCommands.cs
Normal file
126
Commands/InstanceCommands.cs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("list-instances", HelpText = "List all configured Redis instances.")]
|
||||||
|
public class ListInstancesOptions { }
|
||||||
|
|
||||||
|
[Verb("add-instance", HelpText = "Add a new Redis instance.")]
|
||||||
|
public class AddInstanceOptions
|
||||||
|
{
|
||||||
|
[Option('n', "name", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[Option('h', "host", Required = true, HelpText = "Redis host.")]
|
||||||
|
public string Host { get; set; }
|
||||||
|
[Option('p', "port", Required = false, HelpText = "Redis port.")]
|
||||||
|
public int Port { get; set; } = 6379;
|
||||||
|
[Option('w', "password", Required = false, HelpText = "Redis password.")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("update-instance", HelpText = "Update an existing Redis instance.")]
|
||||||
|
public class UpdateInstanceOptions
|
||||||
|
{
|
||||||
|
[Option('n', "name", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
[Option('h', "host", Required = false, HelpText = "Redis host.")]
|
||||||
|
public string Host { get; set; }
|
||||||
|
[Option('p', "port", Required = false, HelpText = "Redis port.")]
|
||||||
|
public int Port { get; set; } = 6379;
|
||||||
|
[Option('w', "password", Required = false, HelpText = "Redis password.")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("delete-instance", HelpText = "Delete a Redis instance.")]
|
||||||
|
public class DeleteInstanceOptions
|
||||||
|
{
|
||||||
|
[Option('n', "name", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InstanceCommands
|
||||||
|
{
|
||||||
|
public static int RunListInstances(Config config)
|
||||||
|
{
|
||||||
|
if (config.Instances.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("No instances configured."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = config.Instances.Select(i => new[] { i.Name, i.Host, i.Port.ToString(), i.Password ?? "" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Name", "Host", "Port", "Password" }, rows);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunAddInstance(AddInstanceOptions opts, Config config)
|
||||||
|
{
|
||||||
|
if (config.Instances.Any(i => i.Name == opts.Name))
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Instance '{opts.Name}' already exists."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = new InstanceConfig
|
||||||
|
{
|
||||||
|
Name = opts.Name,
|
||||||
|
Host = opts.Host,
|
||||||
|
Port = opts.Port,
|
||||||
|
Password = opts.Password
|
||||||
|
};
|
||||||
|
|
||||||
|
config.Instances.Add(instance);
|
||||||
|
SaveConfig(config, "redismanager.json");
|
||||||
|
Console.WriteLine(Output.Green($"Instance '{opts.Name}' added successfully."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunUpdateInstance(UpdateInstanceOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = config.Instances.Find(i => i.Name == opts.Name);
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Instance '{opts.Name}' not found."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(opts.Host))
|
||||||
|
instance.Host = opts.Host;
|
||||||
|
if (opts.Port != 6379)
|
||||||
|
instance.Port = opts.Port;
|
||||||
|
if (!string.IsNullOrEmpty(opts.Password))
|
||||||
|
instance.Password = opts.Password;
|
||||||
|
|
||||||
|
SaveConfig(config, "redismanager.json");
|
||||||
|
Console.WriteLine(Output.Green($"Instance '{opts.Name}' updated successfully."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunDeleteInstance(DeleteInstanceOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = config.Instances.Find(i => i.Name == opts.Name);
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Instance '{opts.Name}' not found."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Instances.Remove(instance);
|
||||||
|
SaveConfig(config, "redismanager.json");
|
||||||
|
Console.WriteLine(Output.Green($"Instance '{opts.Name}' deleted successfully."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveConfig(Config config, string path)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Commands/InstanceOptions.cs
Normal file
13
Commands/InstanceOptions.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using CommandLine;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base options for commands that require a Redis instance.
|
||||||
|
/// </summary>
|
||||||
|
public class InstanceOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Name of the Redis instance to use.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
227
Commands/KeyCommands.cs
Normal file
227
Commands/KeyCommands.cs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("exists", HelpText = "Check if key(s) exist.")]
|
||||||
|
public class ExistsOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys to check.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("expire", HelpText = "Set a timeout on a key.")]
|
||||||
|
public class ExpireOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key to expire.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "seconds", Required = true, HelpText = "Expire time in seconds.")]
|
||||||
|
public int Seconds { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("ttl", HelpText = "Get the time to live for a key.")]
|
||||||
|
public class TtlOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key to check.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("persist", HelpText = "Remove the expiration from a key.")]
|
||||||
|
public class PersistOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key to persist.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("rename", HelpText = "Rename a key.")]
|
||||||
|
public class RenameOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Current key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "newkey", Required = true, HelpText = "New key name.")]
|
||||||
|
public string NewKey { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("type", HelpText = "Get the type of a key.")]
|
||||||
|
public class TypeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key to check.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("keys", HelpText = "Find all keys matching pattern.")]
|
||||||
|
public class KeysOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "pattern", Required = true, HelpText = "Pattern to match.")]
|
||||||
|
public string Pattern { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("scan", HelpText = "Incrementally iterate the keyspace.")]
|
||||||
|
public class ScanOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("cursor", Required = false, HelpText = "Cursor to start from.")]
|
||||||
|
public ulong Cursor { get; set; } = 0;
|
||||||
|
[Option("pattern", Required = false, HelpText = "Pattern to match.")]
|
||||||
|
public string Pattern { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Count of keys to return.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class KeyCommands
|
||||||
|
{
|
||||||
|
public static int RunExists(ExistsOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.KeyExists(keys);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Keys", "Exist Count" }, new List<string[]> { new[] { string.Join(", ", opts.Keys), count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(count.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunExpire(ExpireOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var ok = db.KeyExpire(opts.Key, TimeSpan.FromSeconds(opts.Seconds));
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Expire Set" }, new List<string[]> { new[] { opts.Key, ok ? "Yes" : "No" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(ok ? "1" : "0"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunTtl(TtlOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var ttl = db.KeyTimeToLive(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "TTL (s)" }, new List<string[]> { new[] { opts.Key, ttl?.TotalSeconds.ToString() ?? "-1" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(ttl?.TotalSeconds.ToString() ?? "-1"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunPersist(PersistOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var ok = db.KeyPersist(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Persisted" }, new List<string[]> { new[] { opts.Key, ok ? "Yes" : "No" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(ok ? "1" : "0"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunRename(RenameOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.KeyRename(opts.Key, opts.NewKey);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Old Key", "New Key", "Status" }, new List<string[]> { new[] { opts.Key, opts.NewKey, "Renamed" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunType(TypeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var type = db.KeyType(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Type" }, new List<string[]> { new[] { opts.Key, type.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(type.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunKeys(KeysOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = db.Execute("KEYS", opts.Pattern).ToString().Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = keys.Select(k => new[] { k }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Key" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var key in keys)
|
||||||
|
Console.WriteLine(Output.Green(key));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
public static int RunScan(ScanOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.Execute("SCAN", opts.Cursor.ToString(),
|
||||||
|
string.IsNullOrEmpty(opts.Pattern) ? null : "MATCH",
|
||||||
|
string.IsNullOrEmpty(opts.Pattern) ? null : opts.Pattern,
|
||||||
|
opts.Count.HasValue ? "COUNT" : null,
|
||||||
|
opts.Count.HasValue ? opts.Count.Value.ToString() : null);
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
var nextCursor = arr[0].ToString();
|
||||||
|
var keys = ((RedisResult[])arr[1]).Select(x => x.ToString()).ToArray();
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = keys.Select(k => new[] { k }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Key" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Next Cursor" }, new List<string[]> { new[] { nextCursor } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var key in keys)
|
||||||
|
Console.WriteLine(Output.Green(key));
|
||||||
|
Console.WriteLine(Output.Yellow($"Next Cursor: {nextCursor}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
528
Commands/ListCommands.cs
Normal file
528
Commands/ListCommands.cs
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("lpush", HelpText = "Push elements to the left of a list.")]
|
||||||
|
public class LPushOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "values", Min = 1, Required = true, HelpText = "Values to push.")]
|
||||||
|
public IEnumerable<string> Values { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("rpush", HelpText = "Push elements to the right of a list.")]
|
||||||
|
public class RPushOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "values", Min = 1, Required = true, HelpText = "Values to push.")]
|
||||||
|
public IEnumerable<string> Values { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lpop", HelpText = "Pop element from the left of a list.")]
|
||||||
|
public class LPopOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("rpop", HelpText = "Pop element from the right of a list.")]
|
||||||
|
public class RPopOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lrange", HelpText = "Get range of elements from a list.")]
|
||||||
|
public class LRangeOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start index.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop index.")]
|
||||||
|
public long Stop { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("llen", HelpText = "Get the length of a list.")]
|
||||||
|
public class LLenOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lindex", HelpText = "Get element at index in a list.")]
|
||||||
|
public class LIndexOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "index", Required = true, HelpText = "Element index.")]
|
||||||
|
public long Index { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lset", HelpText = "Set element at index in a list.")]
|
||||||
|
public class LSetOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "index", Required = true, HelpText = "Element index.")]
|
||||||
|
public long Index { get; set; }
|
||||||
|
[Value(2, MetaName = "value", Required = true, HelpText = "New value.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lrem", HelpText = "Remove elements from a list.")]
|
||||||
|
public class LRemOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "count", Required = true, HelpText = "Number of elements to remove (0=all, positive=from left, negative=from right).")]
|
||||||
|
public long Count { get; set; }
|
||||||
|
[Value(2, MetaName = "value", Required = true, HelpText = "Value to remove.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("linsert", HelpText = "Insert element before or after pivot in a list.")]
|
||||||
|
public class LInsertOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "position", Required = true, HelpText = "Position: BEFORE or AFTER.")]
|
||||||
|
public string Position { get; set; }
|
||||||
|
[Value(2, MetaName = "pivot", Required = true, HelpText = "Pivot element.")]
|
||||||
|
public string Pivot { get; set; }
|
||||||
|
[Value(3, MetaName = "value", Required = true, HelpText = "Value to insert.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("ltrim", HelpText = "Trim list to specified range.")]
|
||||||
|
public class LTrimOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "List key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start index.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop index.")]
|
||||||
|
public long Stop { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("blpop", HelpText = "Blocking pop from the left of a list.")]
|
||||||
|
public class BLPopOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "List keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Value(1, MetaName = "timeout", Required = true, HelpText = "Timeout in seconds.")]
|
||||||
|
public int Timeout { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("brpop", HelpText = "Blocking pop from the right of a list.")]
|
||||||
|
public class BRPopOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "List keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Value(1, MetaName = "timeout", Required = true, HelpText = "Timeout in seconds.")]
|
||||||
|
public int Timeout { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("rpoplpush", HelpText = "Pop from right of source, push to left of destination.")]
|
||||||
|
public class RPopLPushOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "source", Required = true, HelpText = "Source list key.")]
|
||||||
|
public string Source { get; set; }
|
||||||
|
[Value(1, MetaName = "destination", Required = true, HelpText = "Destination list key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ListCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LPUSH command to push elements to the left of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LPushOptions containing instance, key, and values.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLPush(LPushOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var values = opts.Values.ToArray();
|
||||||
|
var length = db.ListLeftPush(opts.Key, values.Select(v => (RedisValue)v).ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = values.Select(v => new[] { v, "Pushed" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Value", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "New Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the RPUSH command to push elements to the right of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The RPushOptions containing instance, key, and values.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunRPush(RPushOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var values = opts.Values.ToArray();
|
||||||
|
var length = db.ListRightPush(opts.Key, values.Select(v => (RedisValue)v).ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = values.Select(v => new[] { v, "Pushed" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Value", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "New Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Pushed {values.Length} element(s), new length: {length}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LPOP command to pop an element from the left of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LPopOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLPop(LPopOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var value = db.ListLeftPop(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { opts.Key, value.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the RPOP command to pop an element from the right of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The RPopOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunRPop(RPopOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var value = db.ListRightPop(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { opts.Key, value.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LRANGE command to get a range of elements from a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LRangeOptions containing instance, key, start, and stop.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLRange(LRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var values = db.ListRange(opts.Key, opts.Start, opts.Stop);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = values.Select((v, i) => new[] { (opts.Start + i).ToString(), v.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Index", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var value in values)
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LLEN command to get the length of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LLenOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLLen(LLenOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var length = db.ListLength(opts.Key);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Length" }, new List<string[]> { new[] { opts.Key, length.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(length.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LINDEX command to get the element at a specific index in a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LIndexOptions containing instance, key, and index.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLIndex(LIndexOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var value = db.ListGetByIndex(opts.Key, opts.Index);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Index", "Value" }, new List<string[]> { new[] { opts.Key, opts.Index.ToString(), value.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(value.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LSET command to set the element at a specific index in a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LSetOptions containing instance, key, index, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLSet(LSetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
db.ListSetByIndex(opts.Key, opts.Index, opts.Value);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Index", "New Value", "Status" }, new List<string[]> { new[] { opts.Key, opts.Index.ToString(), opts.Value, "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LREM command to remove elements from a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LRemOptions containing instance, key, count, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLRem(LRemOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.ListRemove(opts.Key, opts.Value, opts.Count);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Value", "Removed Count" }, new List<string[]> { new[] { opts.Key, opts.Value, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Removed {count} element(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LINSERT command to insert an element before or after a pivot in a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LInsertOptions containing instance, key, position, pivot, and value.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLInsert(LInsertOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
long result;
|
||||||
|
if (opts.Position.ToUpper() == "BEFORE")
|
||||||
|
result = db.ListInsertBefore(opts.Key, opts.Pivot, opts.Value);
|
||||||
|
else
|
||||||
|
result = db.ListInsertAfter(opts.Key, opts.Pivot, opts.Value);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Position", "Pivot", "Value", "New Length" }, new List<string[]> { new[] { opts.Key, opts.Position, opts.Pivot, opts.Value, result.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"New length: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LTRIM command to trim a list to a specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LTrimOptions containing instance, key, start, and stop.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLTrim(LTrimOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
db.ListTrim(opts.Key, opts.Start, opts.Stop);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Start", "Stop", "Status" }, new List<string[]> { new[] { opts.Key, opts.Start.ToString(), opts.Stop.ToString(), "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BLPOP command to perform a blocking pop from the left of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BLPopOptions containing instance, keys, and timeout.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBLPop(BLPopOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = opts.Keys.Select(k => (RedisKey)k).Cast<object>().ToList();
|
||||||
|
args.Add(opts.Timeout);
|
||||||
|
var result = db.Execute("BLPOP", args.ToArray());
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Timeout - no elements available to pop."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
if (arr.Length == 2)
|
||||||
|
{
|
||||||
|
var key = arr[0]?.ToString();
|
||||||
|
var value = arr[1]?.ToString();
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { key ?? "", value ?? "" } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{key}: {value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Unexpected result format."));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BRPOP command to perform a blocking pop from the right of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BRPopOptions containing instance, keys, and timeout.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBRPop(BRPopOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = opts.Keys.Select(k => (RedisKey)k).Cast<object>().ToList();
|
||||||
|
args.Add(opts.Timeout);
|
||||||
|
var result = db.Execute("BRPOP", args.ToArray());
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Timeout - no elements available to pop."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
if (arr.Length == 2)
|
||||||
|
{
|
||||||
|
var key = arr[0]?.ToString();
|
||||||
|
var value = arr[1]?.ToString();
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Popped Value" }, new List<string[]> { new[] { key ?? "", value ?? "" } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green($"{key}: {value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Unexpected result format."));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the RPOPLPUSH command to pop from the right of the source and push to the left of the destination list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The RPopLPushOptions containing instance, source, and destination.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunRPopLPush(RPopLPushOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.ListRightPopLeftPush(opts.Source, opts.Destination);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Source", "Destination", "Popped Value" }, new List<string[]> { new[] { opts.Source, opts.Destination, result.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
Commands/ModuleCommands.cs
Normal file
99
Commands/ModuleCommands.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("module-load", HelpText = "Load a Redis module.")]
|
||||||
|
public class ModuleLoadOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "path", Required = true, HelpText = "Module path.")]
|
||||||
|
public string Path { get; set; }
|
||||||
|
[Value(1, MetaName = "args", HelpText = "Module arguments.")]
|
||||||
|
public IEnumerable<string> Args { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("module-unload", HelpText = "Unload a Redis module.")]
|
||||||
|
public class ModuleUnloadOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "name", Required = true, HelpText = "Module name.")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("module-list", HelpText = "List loaded Redis modules.")]
|
||||||
|
public class ModuleListOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ModuleCommands
|
||||||
|
{
|
||||||
|
public static int RunModuleLoad(ModuleLoadOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var args = new List<object> { "LOAD", opts.Path };
|
||||||
|
if (opts.Args != null)
|
||||||
|
args.AddRange(opts.Args);
|
||||||
|
var result = db.Execute("MODULE", args.ToArray());
|
||||||
|
Console.WriteLine(Output.Green($"Module loaded: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Failed to load module: {ex.Message}"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunModuleUnload(ModuleUnloadOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = db.Execute("MODULE", new object[] { "UNLOAD", opts.Name });
|
||||||
|
Console.WriteLine(Output.Green($"Module unloaded: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Failed to unload module: {ex.Message}"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunModuleList(ModuleListOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = db.Execute("MODULE", new object[] { "LIST" });
|
||||||
|
Console.WriteLine(Output.Green($"Module list: {result}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Failed to list modules: {ex.Message}"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Commands/PubSubCommands.cs
Normal file
119
Commands/PubSubCommands.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis Pub/Sub operations.
|
||||||
|
/// Provides functionality for PUBLISH, SUBSCRIBE, and UNSUBSCRIBE commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("publish", HelpText = "Publish a message to a channel.")]
|
||||||
|
public class PublishOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "channel", Required = true, HelpText = "Channel name.")]
|
||||||
|
public string Channel { get; set; }
|
||||||
|
[Value(1, MetaName = "message", Required = true, HelpText = "Message to publish.")]
|
||||||
|
public string Message { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("subscribe", HelpText = "Subscribe to one or more channels.")]
|
||||||
|
public class SubscribeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "channels", Min = 1, Required = true, HelpText = "Channels to subscribe to.")]
|
||||||
|
public IEnumerable<string> Channels { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("unsubscribe", HelpText = "Unsubscribe from one or more channels.")]
|
||||||
|
public class UnsubscribeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "channels", Min = 1, Required = true, HelpText = "Channels to unsubscribe from.")]
|
||||||
|
public IEnumerable<string> Channels { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PubSubCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the PUBLISH command to publish a message to a channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The PublishOptions containing instance, channel, and message.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunPublish(PublishOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var sub = redis.GetSubscriber();
|
||||||
|
var count = sub.Publish(opts.Channel, opts.Message);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Channel", "Receivers" }, new List<string[]> { new[] { opts.Channel, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Published to {opts.Channel}, receivers: {count}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SUBSCRIBE command to subscribe to one or more channels.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SubscribeOptions containing instance and channels.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSubscribe(SubscribeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var sub = redis.GetSubscriber();
|
||||||
|
var channels = opts.Channels;
|
||||||
|
Console.WriteLine(Output.Yellow($"Subscribed to: {string.Join(", ", channels)}. Press Ctrl+C to exit."));
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); };
|
||||||
|
foreach (var channel in channels)
|
||||||
|
{
|
||||||
|
sub.Subscribe(channel, (ch, msg) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Cyan($"[{ch}] {msg}"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
Console.WriteLine(Output.Yellow("Unsubscribed and exiting."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the UNSUBSCRIBE command to unsubscribe from one or more channels.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The UnsubscribeOptions containing instance and channels.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunUnsubscribe(UnsubscribeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var sub = redis.GetSubscriber();
|
||||||
|
foreach (var channel in opts.Channels)
|
||||||
|
{
|
||||||
|
sub.Unsubscribe(channel);
|
||||||
|
Console.WriteLine(Output.Yellow($"Unsubscribed from {channel}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
Commands/ScriptingCommands.cs
Normal file
156
Commands/ScriptingCommands.cs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis scripting operations.
|
||||||
|
/// Provides functionality for EVAL, EVALSHA, SCRIPT LOAD, SCRIPT EXISTS, and SCRIPT FLUSH commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("eval", HelpText = "Evaluate a Lua script.")]
|
||||||
|
public class EvalOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "script", Required = true, HelpText = "Lua script.")]
|
||||||
|
public string Script { get; set; }
|
||||||
|
[Option("keys", Required = false, HelpText = "Comma-separated keys.")]
|
||||||
|
public string Keys { get; set; }
|
||||||
|
[Option("args", Required = false, HelpText = "Comma-separated arguments.")]
|
||||||
|
public string Args { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("evalsha", HelpText = "Evaluate a Lua script by its SHA1 hash.")]
|
||||||
|
public class EvalShaOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "sha1", Required = true, HelpText = "SHA1 hash.")]
|
||||||
|
public string Sha1 { get; set; }
|
||||||
|
[Option("keys", Required = false, HelpText = "Comma-separated keys.")]
|
||||||
|
public string Keys { get; set; }
|
||||||
|
[Option("args", Required = false, HelpText = "Comma-separated arguments.")]
|
||||||
|
public string Args { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("scriptload", HelpText = "Load a Lua script into the script cache.")]
|
||||||
|
public class ScriptLoadOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "script", Required = true, HelpText = "Lua script.")]
|
||||||
|
public string Script { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("scriptexists", HelpText = "Check if scripts exist in the script cache.")]
|
||||||
|
public class ScriptExistsOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "sha1s", Min = 1, Required = true, HelpText = "SHA1 hashes.")]
|
||||||
|
public IEnumerable<string> Sha1s { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("scriptflush", HelpText = "Remove all scripts from the script cache.")]
|
||||||
|
public class ScriptFlushOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ScriptingCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the EVAL command to evaluate a Lua script.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The EvalOptions containing instance, script, keys, and arguments.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunEval(EvalOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = string.IsNullOrEmpty(opts.Keys) ? Array.Empty<RedisKey>() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray();
|
||||||
|
var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty<RedisValue>() : opts.Args.Split(',').Select(a => (RedisValue)a.Trim()).ToArray();
|
||||||
|
var result = db.ScriptEvaluate(opts.Script, keys, args);
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the EVALSHA command to evaluate a Lua script by its SHA1 hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The EvalShaOptions containing instance, sha1, keys, and arguments.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunEvalSha(EvalShaOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = string.IsNullOrEmpty(opts.Keys) ? Array.Empty<RedisKey>() : opts.Keys.Split(',').Select(k => (RedisKey)k.Trim()).ToArray();
|
||||||
|
var args = string.IsNullOrEmpty(opts.Args) ? Array.Empty<RedisValue>() : opts.Args.Split(',').Select(a => (RedisValue)a.Trim()).ToArray();
|
||||||
|
var result = db.ScriptEvaluate(opts.Sha1, keys, args);
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SCRIPT LOAD command to load a Lua script into the script cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ScriptLoadOptions containing instance and script.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunScriptLoad(ScriptLoadOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var server = redis.GetServer(redis.GetEndPoints().First());
|
||||||
|
var sha1Bytes = server.ScriptLoad(opts.Script);
|
||||||
|
// Convert byte[] to hex string
|
||||||
|
var sha1 = BitConverter.ToString(sha1Bytes).Replace("-", string.Empty).ToLower();
|
||||||
|
Console.WriteLine(Output.Green(sha1));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SCRIPT EXISTS command to check if scripts exist in the script cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ScriptExistsOptions containing instance and SHA1 hashes.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunScriptExists(ScriptExistsOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var server = redis.GetServer(redis.GetEndPoints().First());
|
||||||
|
foreach (var sha in opts.Sha1s)
|
||||||
|
{
|
||||||
|
var exists = server.ScriptExists(sha);
|
||||||
|
Console.WriteLine(Output.Green($"{sha}: {(exists ? "YES" : "NO")}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SCRIPT FLUSH command to remove all scripts from the script cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ScriptFlushOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunScriptFlush(ScriptFlushOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var server = redis.GetServer(redis.GetEndPoints().First());
|
||||||
|
server.ScriptFlush();
|
||||||
|
Console.WriteLine(Output.Green("Script cache flushed."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
Commands/ServerCommands.cs
Normal file
296
Commands/ServerCommands.cs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis server operations.
|
||||||
|
/// Provides functionality for INFO, CONFIG, SAVE, BGSAVE, LASTSAVE, TIME, and PING commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("info", HelpText = "Get information and statistics about the server.")]
|
||||||
|
public class InfoOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("section", Required = false, HelpText = "Info section (server, clients, memory, etc.).")]
|
||||||
|
public string Section { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("config", HelpText = "Get/set Redis configuration parameters.")]
|
||||||
|
public class ConfigOptions
|
||||||
|
{
|
||||||
|
[Option("list-custom", Required = false, HelpText = "List all custom config parameters for this instance.")]
|
||||||
|
public bool ListCustom { get; set; }
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("get", Required = false, HelpText = "Get configuration parameter.")]
|
||||||
|
public string Get { get; set; }
|
||||||
|
[Option("set", Required = false, HelpText = "Set configuration parameter (format: parameter value).")]
|
||||||
|
public string Set { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("save", HelpText = "Synchronously save the dataset to disk.")]
|
||||||
|
public class SaveOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("bgsave", HelpText = "Asynchronously save the dataset to disk.")]
|
||||||
|
public class BgSaveOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("lastsave", HelpText = "Get the timestamp of the last successful save.")]
|
||||||
|
public class LastSaveOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("time", HelpText = "Return the current server time.")]
|
||||||
|
public class TimeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("ping", HelpText = "Ping the server.")]
|
||||||
|
public class PingOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServerCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the INFO command to get information and statistics about the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The InfoOptions containing instance and section.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunInfo(InfoOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var info = db.Execute("INFO", string.IsNullOrEmpty(opts.Section) ? null : opts.Section).ToString();
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var lines = info.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var rows = lines.Select(l => l.Split(':', 2)).Where(parts => parts.Length == 2)
|
||||||
|
.Select(parts => new[] { parts[0], parts[1] }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green(info));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the CONFIG command to get or set Redis configuration parameters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ConfigOptions containing instance, get, set, and table options.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunConfig(ConfigOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.ListCustom)
|
||||||
|
{
|
||||||
|
if (instance.CustomConfig != null && instance.CustomConfig.Count > 0)
|
||||||
|
{
|
||||||
|
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, instance.CustomConfig.Select(kv => new[] { kv.Key, kv.Value }).ToList());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("No custom config parameters set for this instance."));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(opts.Get))
|
||||||
|
{
|
||||||
|
var result = db.Execute("CONFIG", "GET", opts.Get);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
var rows = new List<string[]>();
|
||||||
|
for (int i = 0; i < arr.Length; i += 2)
|
||||||
|
{
|
||||||
|
if (i + 1 < arr.Length)
|
||||||
|
rows.Add(new[] { arr[i].ToString(), arr[i + 1].ToString() });
|
||||||
|
}
|
||||||
|
RedisUtils.PrintTable(new[] { "Parameter", "Value" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
for (int i = 0; i < arr.Length; i += 2)
|
||||||
|
{
|
||||||
|
if (i + 1 < arr.Length)
|
||||||
|
Console.WriteLine(Output.Green($"{arr[i]} {arr[i + 1]}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(opts.Set))
|
||||||
|
{
|
||||||
|
var parts = opts.Set.Split(' ', 2);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
db.Execute("CONFIG", "SET", parts[0], parts[1]);
|
||||||
|
|
||||||
|
// Persist custom config in InstanceConfig and save config file
|
||||||
|
var instanceConfig = config.Instances.FirstOrDefault(i => i.Name == opts.Instance);
|
||||||
|
if (instanceConfig != null)
|
||||||
|
{
|
||||||
|
instanceConfig.CustomConfig[parts[0]] = parts[1];
|
||||||
|
RedisManager.ConfigManager.SaveConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "CONFIG SET", "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Invalid format. Use: parameter value"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Please specify --get or --set"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SAVE command to synchronously save the dataset to disk.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SaveOptions containing instance and table option.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSave(SaveOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("SAVE");
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "SAVE", "OK" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the BGSAVE command to asynchronously save the dataset to disk.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The BgSaveOptions containing instance and table option.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunBgSave(BgSaveOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("BGSAVE");
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Operation", "Status" }, new List<string[]> { new[] { "BGSAVE", "Background save started" } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green("Background save started"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the LASTSAVE command to get the timestamp of the last successful save.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The LastSaveOptions containing instance and table option.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunLastSave(LastSaveOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var timestamp = db.Execute("LASTSAVE").ToString();
|
||||||
|
var dateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp));
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Last Save", "Timestamp", "DateTime" }, new List<string[]> { new[] { "Last Save", timestamp, dateTime.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"{timestamp} ({dateTime})"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the TIME command to return the current server time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The TimeOptions containing instance and table option.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunTime(TimeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.Execute("TIME");
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
var timestamp = arr[0].ToString();
|
||||||
|
var microseconds = arr[1].ToString();
|
||||||
|
var dateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp));
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Timestamp", "Microseconds", "DateTime" }, new List<string[]> { new[] { timestamp, microseconds, dateTime.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"{timestamp} {microseconds} ({dateTime})"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the PING command to ping the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The PingOptions containing instance and table option.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunPing(PingOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var response = db.Ping();
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Ping", "Response" }, new List<string[]> { new[] { "PING", response.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(response.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
573
Commands/SetCommands.cs
Normal file
573
Commands/SetCommands.cs
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("sadd", HelpText = "Add members to a set.")]
|
||||||
|
public class SAddOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to add.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("srem", HelpText = "Remove members from a set.")]
|
||||||
|
public class SRemOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to remove.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("smembers", HelpText = "Get all members of a set.")]
|
||||||
|
public class SMembersOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("scard", HelpText = "Get the number of members in a set.")]
|
||||||
|
public class SCardOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sismember", HelpText = "Check if a member exists in a set.")]
|
||||||
|
public class SIsMemberOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "member", Required = true, HelpText = "Member to check.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("srandmember", HelpText = "Get random member(s) from a set.")]
|
||||||
|
public class SRandMemberOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Number of random members to get.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("spop", HelpText = "Remove and return random member(s) from a set.")]
|
||||||
|
public class SPopOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Number of members to pop.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sinter", HelpText = "Get intersection of multiple sets.")]
|
||||||
|
public class SInterOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sunion", HelpText = "Get union of multiple sets.")]
|
||||||
|
public class SUnionOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sdiff", HelpText = "Get difference of multiple sets.")]
|
||||||
|
public class SDiffOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "keys", Min = 2, Required = true, HelpText = "Set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sinterstore", HelpText = "Store intersection of multiple sets.")]
|
||||||
|
public class SInterStoreOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sunionstore", HelpText = "Store union of multiple sets.")]
|
||||||
|
public class SUnionStoreOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sdiffstore", HelpText = "Store difference of multiple sets.")]
|
||||||
|
public class SDiffStoreOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(1, MetaName = "keys", Min = 2, Required = true, HelpText = "Source set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("sscan", HelpText = "Scan set members.")]
|
||||||
|
public class SScanOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
|
||||||
|
public int Cursor { get; set; }
|
||||||
|
[Option("match", Required = false, HelpText = "Pattern to match.")]
|
||||||
|
public string Match { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Count hint.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("smove", HelpText = "Move member from one set to another.")]
|
||||||
|
public class SMoveOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
[Value(0, MetaName = "source", Required = true, HelpText = "Source set key.")]
|
||||||
|
public string Source { get; set; }
|
||||||
|
[Value(1, MetaName = "destination", Required = true, HelpText = "Destination set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(2, MetaName = "member", Required = true, HelpText = "Member to move.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SetCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SADD command to add members to a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SAddOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSAdd(SAddOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var members = opts.Members.ToArray();
|
||||||
|
var count = db.SetAdd(opts.Key, members.Select(m => (RedisValue)m).ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m, "Added" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Added {count} new member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SREM command to remove members from a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SRemOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSRem(SRemOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var members = opts.Members.ToArray();
|
||||||
|
var count = db.SetRemove(opts.Key, members.Select(m => (RedisValue)m).ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m, "Removed" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SMEMBERS command to get all members of a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SMembersOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSMembers(SMembersOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var members = db.SetMembers(opts.Key);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SCARD command to get the number of members in a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SCardOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSCard(SCardOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SetLength(opts.Key);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(count.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SISMEMBER command to check if a member exists in a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SIsMemberOptions containing instance, key, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSIsMember(SIsMemberOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var exists = db.SetContains(opts.Key, opts.Member);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Member", "Exists" }, new List<string[]> { new[] { opts.Key, opts.Member, exists.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(exists.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SRANDMEMBER command to get random member(s) from a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SRandMemberOptions containing instance, key, and count.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSRandMember(SRandMemberOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
var members = db.SetRandomMembers(opts.Key, opts.Count.Value);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Random Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var member = db.SetRandomMember(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Random Member" }, new List<string[]> { new[] { opts.Key, member.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SPOP command to remove and return random member(s) from a set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SPopOptions containing instance, key, and count.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSPop(SPopOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
var members = db.SetPop(opts.Key, opts.Count.Value);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Popped Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var member = db.SetPop(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Popped Member" }, new List<string[]> { new[] { opts.Key, member.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SINTER command to get the intersection of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SInterOptions containing instance and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSInter(SInterOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var members = db.SetCombine(SetOperation.Intersect, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Intersection Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SUNION command to get the union of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SUnionOptions containing instance and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSUnion(SUnionOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var members = db.SetCombine(SetOperation.Union, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Union Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SDIFF command to get the difference of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SDiffOptions containing instance and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSDiff(SDiffOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var members = db.SetCombine(SetOperation.Difference, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Difference Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SINTERSTORE command to store the intersection of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SInterStoreOptions containing instance, destination, and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSInterStore(SInterStoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.SetCombineAndStore(SetOperation.Intersect, opts.Destination, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SUNIONSTORE command to store the union of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SUnionStoreOptions containing instance, destination, and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSUnionStore(SUnionStoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.SetCombineAndStore(SetOperation.Union, opts.Destination, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SDIFFSTORE command to store the difference of multiple sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SDiffStoreOptions containing instance, destination, and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSDiffStore(SDiffStoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.SetCombineAndStore(SetOperation.Difference, opts.Destination, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SSCAN command to scan set members.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SScanOptions containing instance, key, cursor, and scan options.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSScan(SScanOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var args = new List<object> { opts.Cursor };
|
||||||
|
if (!string.IsNullOrEmpty(opts.Match))
|
||||||
|
{
|
||||||
|
args.Add("MATCH");
|
||||||
|
args.Add(opts.Match);
|
||||||
|
}
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
args.Add("COUNT");
|
||||||
|
args.Add(opts.Count.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = db.Execute("SSCAN", opts.Key, args.ToArray());
|
||||||
|
var arr = (RedisResult[])result;
|
||||||
|
var newCursor = (int)arr[0];
|
||||||
|
var members = (RedisResult[])arr[1];
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Next Cursor" }, new List<string[]> { new[] { newCursor.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
Console.WriteLine(Output.Yellow($"Next cursor: {newCursor}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SMOVE command to move a member from one set to another.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SMoveOptions containing instance, source, destination, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunSMove(SMoveOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var moved = db.SetMove(opts.Source, opts.Destination, opts.Member);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Source", "Destination", "Member", "Moved" }, new List<string[]> { new[] { opts.Source, opts.Destination, opts.Member, moved.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(moved.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
836
Commands/SortedSetCommands.cs
Normal file
836
Commands/SortedSetCommands.cs
Normal file
@ -0,0 +1,836 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis sorted set operations.
|
||||||
|
/// Provides functionality for ZADD, ZREM, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZCARD, ZSCORE, ZRANK, ZREVRANK, ZINCRBY, ZREVRANGEBYSCORE, ZCOUNT, ZUNIONSTORE, ZINTERSTORE, ZSCAN, ZPOPMAX, ZPOPMIN, ZREMRANGEBYRANK, and ZREMRANGEBYSCORE commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("zadd", HelpText = "Add members to a sorted set.")]
|
||||||
|
public class ZAddOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "score", Required = true, HelpText = "Score for the member.")]
|
||||||
|
public double Score { get; set; }
|
||||||
|
[Value(2, MetaName = "member", Required = true, HelpText = "Member to add.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrem", HelpText = "Remove members from a sorted set.")]
|
||||||
|
public class ZRemOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "members", Min = 1, Required = true, HelpText = "Members to remove.")]
|
||||||
|
public IEnumerable<string> Members { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrange", HelpText = "Get range of members by rank.")]
|
||||||
|
public class ZRangeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
|
||||||
|
public long Stop { get; set; }
|
||||||
|
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
|
||||||
|
public bool WithScores { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrevrange", HelpText = "Get range of members by rank (reverse order).")]
|
||||||
|
public class ZRevRangeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
|
||||||
|
public long Stop { get; set; }
|
||||||
|
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
|
||||||
|
public bool WithScores { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrangebyscore", HelpText = "Get range of members by score.")]
|
||||||
|
public class ZRangeByScoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
|
||||||
|
public double Min { get; set; }
|
||||||
|
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
|
||||||
|
public double Max { get; set; }
|
||||||
|
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
|
||||||
|
public bool WithScores { get; set; }
|
||||||
|
[Option("limit", Required = false, HelpText = "Limit results (offset count).")]
|
||||||
|
public string Limit { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zcard", HelpText = "Get the number of members in a sorted set.")]
|
||||||
|
public class ZCardOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zscore", HelpText = "Get the score of a member.")]
|
||||||
|
public class ZScoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrank", HelpText = "Get the rank of a member.")]
|
||||||
|
public class ZRankOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrevrank", HelpText = "Get the reverse rank of a member.")]
|
||||||
|
public class ZRevRankOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "member", Required = true, HelpText = "Member name.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zincrby", HelpText = "Increment the score of a member.")]
|
||||||
|
public class ZIncrByOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "increment", Required = true, HelpText = "Score increment.")]
|
||||||
|
public double Increment { get; set; }
|
||||||
|
[Value(2, MetaName = "member", Required = true, HelpText = "Member name.")]
|
||||||
|
public string Member { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zrevrangebyscore", HelpText = "Get range of members by score (reverse order).")]
|
||||||
|
public class ZRevRangeByScoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "max", Required = true, HelpText = "Maximum score.")]
|
||||||
|
public double Max { get; set; }
|
||||||
|
[Value(2, MetaName = "min", Required = true, HelpText = "Minimum score.")]
|
||||||
|
public double Min { get; set; }
|
||||||
|
[Option("withscores", Required = false, HelpText = "Include scores in output.")]
|
||||||
|
public bool WithScores { get; set; }
|
||||||
|
[Option("limit", Required = false, HelpText = "Limit results (offset count).")]
|
||||||
|
public string Limit { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zcount", HelpText = "Count members within a score range.")]
|
||||||
|
public class ZCountOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
|
||||||
|
public double Min { get; set; }
|
||||||
|
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
|
||||||
|
public double Max { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zunionstore", HelpText = "Store union of multiple sorted sets.")]
|
||||||
|
public class ZUnionStoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination sorted set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(1, MetaName = "numkeys", Required = true, HelpText = "Number of source keys.")]
|
||||||
|
public int NumKeys { get; set; }
|
||||||
|
[Value(2, MetaName = "keys", Min = 1, Required = true, HelpText = "Source sorted set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("weights", Required = false, HelpText = "Weights for each source set.")]
|
||||||
|
public IEnumerable<double> Weights { get; set; }
|
||||||
|
[Option("aggregate", Required = false, HelpText = "Aggregation method (sum, min, max).")]
|
||||||
|
public string Aggregate { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zinterstore", HelpText = "Store intersection of multiple sorted sets.")]
|
||||||
|
public class ZInterStoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "destination", Required = true, HelpText = "Destination sorted set key.")]
|
||||||
|
public string Destination { get; set; }
|
||||||
|
[Value(1, MetaName = "numkeys", Required = true, HelpText = "Number of source keys.")]
|
||||||
|
public int NumKeys { get; set; }
|
||||||
|
[Value(2, MetaName = "keys", Min = 1, Required = true, HelpText = "Source sorted set keys.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
[Option("weights", Required = false, HelpText = "Weights for each source set.")]
|
||||||
|
public IEnumerable<double> Weights { get; set; }
|
||||||
|
[Option("aggregate", Required = false, HelpText = "Aggregation method (sum, min, max).")]
|
||||||
|
public string Aggregate { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zscan", HelpText = "Scan sorted set members.")]
|
||||||
|
public class ZScanOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "cursor", Required = true, HelpText = "Cursor position.")]
|
||||||
|
public int Cursor { get; set; }
|
||||||
|
[Option("match", Required = false, HelpText = "Pattern to match.")]
|
||||||
|
public string Match { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Count hint.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zpopmax", HelpText = "Remove and return members with highest scores.")]
|
||||||
|
public class ZPopMaxOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Number of members to pop.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zpopmin", HelpText = "Remove and return members with lowest scores.")]
|
||||||
|
public class ZPopMinOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Option("count", Required = false, HelpText = "Number of members to pop.")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zremrangebyrank", HelpText = "Remove members by rank range.")]
|
||||||
|
public class ZRemRangeByRankOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "start", Required = true, HelpText = "Start rank.")]
|
||||||
|
public long Start { get; set; }
|
||||||
|
[Value(2, MetaName = "stop", Required = true, HelpText = "Stop rank.")]
|
||||||
|
public long Stop { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("zremrangebyscore", HelpText = "Remove members by score range.")]
|
||||||
|
public class ZRemRangeByScoreOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Sorted set key.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
[Value(1, MetaName = "min", Required = true, HelpText = "Minimum score.")]
|
||||||
|
public double Min { get; set; }
|
||||||
|
[Value(2, MetaName = "max", Required = true, HelpText = "Maximum score.")]
|
||||||
|
public double Max { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SortedSetCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZADD command to add members to a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZAddOptions containing instance, key, score, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZAdd(ZAddOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SortedSetAdd(opts.Key, opts.Member, opts.Score);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Added Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Added {count} new member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREM command to remove members from a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRemOptions containing instance, key, and members.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRem(ZRemOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var members = opts.Members.ToArray();
|
||||||
|
var count = db.SortedSetRemove(opts.Key, members.Select(m => (RedisValue)m).ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m, "Removed" }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Status" }, rows);
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZRANGE command to get a range of members by rank.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRangeOptions containing instance, key, start, and stop.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRange(ZRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
if (opts.WithScores)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetRangeByRankWithScores(opts.Key, opts.Start, opts.Stop, Order.Ascending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var members = db.SortedSetRangeByRank(opts.Key, opts.Start, opts.Stop, Order.Ascending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREVRANGE command to get a range of members by rank in reverse order.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRevRangeOptions containing instance, key, start, and stop.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRevRange(ZRevRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
if (opts.WithScores)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetRangeByRankWithScores(opts.Key, opts.Start, opts.Stop, Order.Descending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var members = db.SortedSetRangeByRank(opts.Key, opts.Start, opts.Stop, Order.Descending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZRANGEBYSCORE command to get a range of members by score.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRangeByScoreOptions containing instance, key, min, and max.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRangeByScore(ZRangeByScoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
if (opts.WithScores)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetRangeByScoreWithScores(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Ascending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{e.Element} {e.Score}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var members = db.SortedSetRangeByScore(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Ascending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZCARD command to get the number of members in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZCardOptions containing instance and key.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZCard(ZCardOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SortedSetLength(opts.Key);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Cardinality" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(count.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZSCORE command to get the score of a member in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZScoreOptions containing instance, key, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZScore(ZScoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var score = db.SortedSetScore(opts.Key, opts.Member);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Member", "Score" }, new List<string[]> { new[] { opts.Key, opts.Member, score?.ToString() ?? "[null]" } });
|
||||||
|
else if (score.HasValue)
|
||||||
|
Console.WriteLine(Output.Green(score.Value.ToString()));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Yellow("[null]"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZRANK command to get the rank of a member in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRankOptions containing instance, key, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRank(ZRankOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var rank = db.SortedSetRank(opts.Key, opts.Member);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Member", "Rank" }, new List<string[]> { new[] { opts.Key, opts.Member, rank?.ToString() ?? "[null]" } });
|
||||||
|
else if (rank.HasValue)
|
||||||
|
Console.WriteLine(Output.Green(rank.Value.ToString()));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Yellow("[null]"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREVRANK command to get the reverse rank of a member in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRevRankOptions containing instance, key, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRevRank(ZRevRankOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var rank = db.SortedSetRank(opts.Key, opts.Member, Order.Descending);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Member", "Reverse Rank" }, new List<string[]> { new[] { opts.Key, opts.Member, rank?.ToString() ?? "[null]" } });
|
||||||
|
else if (rank.HasValue)
|
||||||
|
Console.WriteLine(Output.Green(rank.Value.ToString()));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Yellow("[null]"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZINCRBY command to increment the score of a member in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZIncrByOptions containing instance, key, increment, and member.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZIncrBy(ZIncrByOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var newScore = db.SortedSetIncrement(opts.Key, opts.Member, opts.Increment);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Member", "New Score" }, new List<string[]> { new[] { opts.Key, opts.Member, newScore.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(newScore.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREVRANGEBYSCORE command to get a range of members by score in reverse order.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRevRangeByScoreOptions containing instance, key, max, and min.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRevRangeByScore(ZRevRangeByScoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.WithScores)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetRangeByScoreWithScores(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Descending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var entry in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var members = db.SortedSetRangeByScore(opts.Key, opts.Min, opts.Max, Exclude.None, Order.Descending);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = members.Select(m => new string[] { m.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var member in members)
|
||||||
|
Console.WriteLine(Output.Green(member.ToString()));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZCOUNT command to count members within a score range in a sorted set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZCountOptions containing instance, key, min, and max.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZCount(ZCountOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SortedSetLengthByValue(opts.Key, opts.Min, opts.Max);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(count.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZUNIONSTORE command to store the union of multiple sorted sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZUnionStoreOptions containing instance, destination, numkeys, and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZUnionStore(ZUnionStoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.SortedSetCombineAndStore(SetOperation.Union, opts.Destination, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZINTERSTORE command to store the intersection of multiple sorted sets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZInterStoreOptions containing instance, destination, numkeys, and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZInterStore(ZInterStoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
var count = db.SortedSetCombineAndStore(SetOperation.Intersect, opts.Destination, keys);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Destination", "Stored Count" }, new List<string[]> { new[] { opts.Destination, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Stored {count} member(s) in {opts.Destination}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZSCAN command to scan sorted set members.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZScanOptions containing instance, key, cursor, and scan options.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZScan(ZScanOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var result = db.SortedSetScan(opts.Key, opts.Match ?? "*", pageSize: opts.Count ?? 10, cursor: opts.Cursor);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = result.Select(e => new[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var entry in result)
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZPOPMAX command to remove and return members with the highest scores.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZPopMaxOptions containing instance, key, and count.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZPopMax(ZPopMaxOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetPop(opts.Key, opts.Count.Value);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var entry in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var entry = db.SortedSetPop(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, new List<string[]> { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZPOPMIN command to remove and return members with the lowest scores.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZPopMinOptions containing instance, key, and count.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZPopMin(ZPopMinOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
var entries = db.SortedSetPop(opts.Key, opts.Count.Value);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
var rows = entries.Select(e => new string[] { e.Element.ToString(), e.Score.ToString() }).ToList();
|
||||||
|
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, rows);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
foreach (var entry in entries)
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Element}: {entry.Score}"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var entry = db.SortedSetPop(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Popped Member", "Score" }, new List<string[]> { new[] { entry.Value.Element.ToString(), entry.Value.Score.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"{entry.Value.Element}: {entry.Value.Score}"));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREMRANGEBYRANK command to remove members by rank range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRemRangeByRankOptions containing instance, key, start, and stop.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRemRangeByRank(ZRemRangeByRankOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SortedSetRemoveRangeByRank(opts.Key, opts.Start, opts.Stop);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the ZREMRANGEBYSCORE command to remove members by score range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ZRemRangeByScoreOptions containing instance, key, min, and max.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunZRemRangeByScore(ZRemRangeByScoreOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
var count = db.SortedSetRemoveRangeByScore(opts.Key, opts.Min, opts.Max);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Removed Count" }, new List<string[]> { new[] { opts.Key, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Removed {count} member(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Commands/StatusCommand.cs
Normal file
46
Commands/StatusCommand.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command line options for the status command.
|
||||||
|
/// Used to check the connectivity and response time of a Redis instance.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("status", HelpText = "Check status of a Redis instance.")]
|
||||||
|
public class StatusOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the Redis instance to check.
|
||||||
|
/// This must match a configured instance name in the configuration file.
|
||||||
|
/// </summary>
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static class containing the implementation for the status command.
|
||||||
|
/// Provides functionality to check Redis instance connectivity and response time.
|
||||||
|
/// </summary>
|
||||||
|
public static class StatusCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the status command to check Redis instance connectivity.
|
||||||
|
/// Connects to the specified Redis instance and measures the ping response time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The status command options containing the instance name</param>
|
||||||
|
/// <param name="config">The RedisManager configuration containing instance details</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
|
||||||
|
public static int RunStatus(StatusOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var pong = db.Ping();
|
||||||
|
Console.WriteLine(Output.Green($"PONG: {pong.TotalMilliseconds}ms"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Commands/StreamCommands.cs
Normal file
154
Commands/StreamCommands.cs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
[Verb("xadd", HelpText = "Add an entry to a stream.")]
|
||||||
|
public class XAddOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
|
||||||
|
public string Stream { get; set; }
|
||||||
|
[Option("id", Required = false, HelpText = "Entry ID (default: *)")]
|
||||||
|
public string Id { get; set; } = "*";
|
||||||
|
[Option("fields", Required = true, HelpText = "Field-value pairs as field1:value1,field2:value2...")]
|
||||||
|
public IEnumerable<string> Fields { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("xrange", HelpText = "Get entries from a stream within a range.")]
|
||||||
|
public class XRangeOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
|
||||||
|
public string Stream { get; set; }
|
||||||
|
[Option("start", Required = false, HelpText = "Start ID (default: -)")]
|
||||||
|
public string Start { get; set; } = "-";
|
||||||
|
[Option("end", Required = false, HelpText = "End ID (default: +)")]
|
||||||
|
public string End { get; set; } = "+";
|
||||||
|
[Option("count", Required = false, HelpText = "Maximum number of entries")]
|
||||||
|
public int? Count { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("xlen", HelpText = "Get the length of a stream.")]
|
||||||
|
public class XLenOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
|
||||||
|
public string Stream { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("xdel", HelpText = "Remove entries from a stream.")]
|
||||||
|
public class XDelOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "stream", Required = true, HelpText = "Stream key.")]
|
||||||
|
public string Stream { get; set; }
|
||||||
|
[Value(1, MetaName = "ids", Min = 1, Required = true, HelpText = "Entry IDs to delete.")]
|
||||||
|
public IEnumerable<string> Ids { get; set; }
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StreamCommands
|
||||||
|
{
|
||||||
|
public static int RunXAdd(XAddOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
// Use XADD command directly
|
||||||
|
var args = new List<RedisValue> { opts.Stream, opts.Id };
|
||||||
|
foreach (var field in opts.Fields)
|
||||||
|
{
|
||||||
|
var parts = field.Split(':', 2);
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
args.Add(parts[0]);
|
||||||
|
args.Add(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var id = db.Execute("XADD", args.ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Stream", "ID", "Fields" }, new List<string[]> { new[] { opts.Stream, id.ToString(), string.Join(", ", opts.Fields) } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(id.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunXRange(XRangeOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
// Use XRANGE command directly
|
||||||
|
var args = new List<RedisValue> { opts.Stream, opts.Start, opts.End };
|
||||||
|
if (opts.Count.HasValue)
|
||||||
|
{
|
||||||
|
args.Add("COUNT");
|
||||||
|
args.Add(opts.Count.Value);
|
||||||
|
}
|
||||||
|
var result = db.Execute("XRANGE", args.ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("Table output not supported for XRANGE yet"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunXLen(XLenOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
// Use XLEN command directly
|
||||||
|
var length = db.Execute("XLEN", opts.Stream);
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Stream", "Length" }, new List<string[]> { new[] { opts.Stream, length.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green(length.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RunXDel(XDelOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
|
||||||
|
// Use XDEL command directly
|
||||||
|
var args = new List<RedisValue> { opts.Stream };
|
||||||
|
args.AddRange(opts.Ids.Select(id => (RedisValue)id));
|
||||||
|
var count = db.Execute("XDEL", args.ToArray());
|
||||||
|
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Stream", "Deleted Count" }, new List<string[]> { new[] { opts.Stream, count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Deleted {count} entry(ies)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
201
Commands/StringCommands.cs
Normal file
201
Commands/StringCommands.cs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command line options for the GET command.
|
||||||
|
/// Used to retrieve the value of a Redis key.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("get", HelpText = "Get value of a key.")]
|
||||||
|
public class GetOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the key name to retrieve the value for.
|
||||||
|
/// </summary>
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to output the result in table format.
|
||||||
|
/// </summary>
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command line options for the SET command.
|
||||||
|
/// Used to set the value of a Redis key with optional TTL.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("set", HelpText = "Set value of a key.")]
|
||||||
|
public class SetOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the key name to set the value for.
|
||||||
|
/// </summary>
|
||||||
|
[Value(0, MetaName = "key", Required = true, HelpText = "Key name.")]
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value to store in the key.
|
||||||
|
/// </summary>
|
||||||
|
[Value(1, MetaName = "value", Required = true, HelpText = "Value.")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the time to live in seconds for the key.
|
||||||
|
/// </summary>
|
||||||
|
[Option("ttl", Required = false, HelpText = "Time to live in seconds.")]
|
||||||
|
public int? TTL { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command line options for the DEL command.
|
||||||
|
/// Used to delete one or more Redis keys with optional confirmation.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("del", HelpText = "Delete one or more keys.")]
|
||||||
|
public class DelOptions : InstanceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to skip the confirmation prompt.
|
||||||
|
/// </summary>
|
||||||
|
[Option("yes", Required = false, HelpText = "Skip confirmation.")]
|
||||||
|
public bool Yes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of keys to delete.
|
||||||
|
/// </summary>
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Key(s) to delete.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether to output the result in table format.
|
||||||
|
/// </summary>
|
||||||
|
[Option("table", Required = false, HelpText = "Output as table.")]
|
||||||
|
public bool Table { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static class containing implementations for basic Redis string operations.
|
||||||
|
/// Provides functionality for GET, SET, and DEL commands with various output formats.
|
||||||
|
/// </summary>
|
||||||
|
public static class StringCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the GET command to retrieve a value from Redis.
|
||||||
|
/// Supports table output format and JSON pretty-printing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The GET command options</param>
|
||||||
|
/// <param name="config">The RedisManager configuration</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
|
||||||
|
public static int RunGet(GetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var value = db.StringGet(opts.Key);
|
||||||
|
if (opts.Table)
|
||||||
|
{
|
||||||
|
RedisUtils.PrintTable(new[] { "Key", "Value" }, new List<string[]> { new[] { opts.Key, value.IsNull ? "[null]" : value.ToString() } });
|
||||||
|
}
|
||||||
|
else if (value.IsNull)
|
||||||
|
Console.WriteLine(Output.Yellow($"[null]"));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try pretty-print JSON
|
||||||
|
string str = value.ToString();
|
||||||
|
if (IsJson(str))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = JsonDocument.Parse(str);
|
||||||
|
var pretty = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
Console.WriteLine(Output.Green(pretty));
|
||||||
|
}
|
||||||
|
catch { Console.WriteLine(Output.Green(str)); }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Green(str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the SET command to store a value in Redis.
|
||||||
|
/// Supports optional TTL (time to live) for the key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The SET command options</param>
|
||||||
|
/// <param name="config">The RedisManager configuration</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
|
||||||
|
public static int RunSet(SetOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
bool ok;
|
||||||
|
if (opts.TTL.HasValue)
|
||||||
|
ok = db.StringSet(opts.Key, opts.Value, TimeSpan.FromSeconds(opts.TTL.Value));
|
||||||
|
else
|
||||||
|
ok = db.StringSet(opts.Key, opts.Value);
|
||||||
|
if (ok)
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Red("Failed to set key."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the DEL command to delete one or more keys from Redis.
|
||||||
|
/// Supports confirmation prompts and table output format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The DEL command options</param>
|
||||||
|
/// <param name="config">The RedisManager configuration</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure)</returns>
|
||||||
|
public static int RunDel(DelOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
|
||||||
|
if (!opts.Yes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow($"Are you sure you want to delete {keys.Length} key(s)? (y/N)"));
|
||||||
|
var response = Console.ReadLine()?.ToLower();
|
||||||
|
if (response != "y" && response != "yes")
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Operation cancelled."));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = db.KeyDelete(keys);
|
||||||
|
if (opts.Table)
|
||||||
|
RedisUtils.PrintTable(new[] { "Keys", "Deleted" }, new List<string[]> { new[] { string.Join(", ", opts.Keys), count.ToString() } });
|
||||||
|
else
|
||||||
|
Console.WriteLine(Output.Green($"Deleted {count} key(s)"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a string appears to be JSON formatted.
|
||||||
|
/// Checks if the string starts and ends with curly braces or square brackets.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to check for JSON formatting</param>
|
||||||
|
/// <returns>True if the string appears to be JSON, false otherwise</returns>
|
||||||
|
private static bool IsJson(string str)
|
||||||
|
{
|
||||||
|
str = str.Trim();
|
||||||
|
return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Commands/TransactionCommands.cs
Normal file
134
Commands/TransactionCommands.cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains command line options and implementations for Redis transaction operations.
|
||||||
|
/// Provides functionality for MULTI, EXEC, DISCARD, WATCH, and UNWATCH commands.
|
||||||
|
/// </summary>
|
||||||
|
[Verb("multi", HelpText = "Mark the start of a transaction block.")]
|
||||||
|
public class MultiOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("exec", HelpText = "Execute all commands issued after MULTI.")]
|
||||||
|
public class ExecOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("discard", HelpText = "Flush all commands issued after MULTI.")]
|
||||||
|
public class DiscardOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("watch", HelpText = "Watch the given keys to determine execution of the MULTI/EXEC block.")]
|
||||||
|
public class WatchOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
[Value(0, MetaName = "keys", Min = 1, Required = true, HelpText = "Keys to watch.")]
|
||||||
|
public IEnumerable<string> Keys { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("unwatch", HelpText = "Forget about all watched keys.")]
|
||||||
|
public class UnwatchOptions
|
||||||
|
{
|
||||||
|
[Option('i', "instance", Required = true, HelpText = "Instance name.")]
|
||||||
|
public string Instance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TransactionCommands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the MULTI command to mark the start of a transaction block.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The MultiOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunMulti(MultiOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("MULTI");
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the EXEC command to execute all commands issued after MULTI.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The ExecOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunExec(ExecOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var result = db.Execute("EXEC");
|
||||||
|
Console.WriteLine(Output.Green(result.ToString()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the DISCARD command to flush all commands issued after MULTI.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The DiscardOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunDiscard(DiscardOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("DISCARD");
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the WATCH command to watch the given keys for a transaction block.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The WatchOptions containing instance and keys.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunWatch(WatchOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
var keys = opts.Keys.Select(k => (RedisKey)k).ToArray();
|
||||||
|
db.Execute("WATCH", keys);
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the UNWATCH command to forget about all watched keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">The UnwatchOptions containing instance.</param>
|
||||||
|
/// <param name="config">The RedisManager configuration.</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
|
||||||
|
public static int RunUnwatch(UnwatchOptions opts, Config config)
|
||||||
|
{
|
||||||
|
var instance = RedisUtils.GetInstance(config, opts.Instance);
|
||||||
|
using var redis = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = redis.GetDatabase();
|
||||||
|
db.Execute("UNWATCH");
|
||||||
|
Console.WriteLine(Output.Green("OK"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
Models/Config.cs
Normal file
108
Models/Config.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace RedisManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents configuration for a single Redis instance, including connection details.
|
||||||
|
/// </summary>
|
||||||
|
public class InstanceConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom Redis configuration parameters to be applied at startup.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> CustomConfig { get; set; } = new Dictionary<string, string>();
|
||||||
|
/// <summary>
|
||||||
|
/// Optional: Path to the redis/valkey server binary for this instance.
|
||||||
|
/// If not set, the global default will be used.
|
||||||
|
/// </summary>
|
||||||
|
public string ServerBinaryPath { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the Redis instance.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The host address of the Redis instance.
|
||||||
|
/// </summary>
|
||||||
|
public string Host { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The port number for the Redis instance (default 6379).
|
||||||
|
/// </summary>
|
||||||
|
public int Port { get; set; } = 6379;
|
||||||
|
/// <summary>
|
||||||
|
/// The password for authenticating with the Redis instance.
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration containing all Redis instances.
|
||||||
|
/// </summary>
|
||||||
|
public class Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optional: Default path to the redis/valkey server binary for all instances.
|
||||||
|
/// </summary>
|
||||||
|
public string ServerBinaryPath { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The list of Redis instance configurations.
|
||||||
|
/// </summary>
|
||||||
|
public List<InstanceConfig> Instances { get; set; } = new List<InstanceConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides methods to load and save RedisManager configuration from disk.
|
||||||
|
/// </summary>
|
||||||
|
public static class ConfigManager
|
||||||
|
{
|
||||||
|
public static string ConfigPath = "redismanager.json";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the configuration from disk, or creates a default if not present.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The loaded or default Config object.</returns>
|
||||||
|
public static Config LoadConfig(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
var defaultConfig = new Config
|
||||||
|
{
|
||||||
|
Instances = new List<InstanceConfig>
|
||||||
|
{
|
||||||
|
new InstanceConfig
|
||||||
|
{
|
||||||
|
Name = "default",
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 6379
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SaveConfig(defaultConfig, path);
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return JsonSerializer.Deserialize<Config>(json) ?? new Config();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Config LoadConfig()
|
||||||
|
{
|
||||||
|
return LoadConfig(ConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the provided configuration to disk as JSON.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">The configuration to save.</param>
|
||||||
|
public static void SaveConfig(Config config, string path)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
public static void SaveConfig(Config config)
|
||||||
|
{
|
||||||
|
SaveConfig(config, ConfigPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
Program.cs
Normal file
87
Program.cs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RedisManager.Commands;
|
||||||
|
using Crayon;
|
||||||
|
|
||||||
|
namespace RedisManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Main entry point for the RedisManager application.
|
||||||
|
/// Supports service (daemon) mode and client mode only.
|
||||||
|
/// </summary>
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Main entry point that determines the operational mode based on command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command line arguments</param>
|
||||||
|
/// <returns>Exit code (0 for success, non-zero for errors)</returns>
|
||||||
|
static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
// No arguments: run as a daemon (service mode)
|
||||||
|
// Service mode: allow optional --config <path>
|
||||||
|
if (args.Length == 0 || (args.Length >= 2 && args[0] == "--config"))
|
||||||
|
{
|
||||||
|
string configPath = null;
|
||||||
|
if (args.Length >= 2 && args[0] == "--config")
|
||||||
|
{
|
||||||
|
configPath = args[1];
|
||||||
|
// Remove --config and path from args for service mode
|
||||||
|
args = args.Skip(2).ToArray();
|
||||||
|
}
|
||||||
|
return await RunServiceMode();
|
||||||
|
} // Standalone mode removed: all commands require daemon/service.
|
||||||
|
|
||||||
|
// With arguments: try to connect to the daemon (client mode)
|
||||||
|
var client = new RedisManagerClient();
|
||||||
|
var clientResult = await client.ExecuteCommandAsync(args);
|
||||||
|
|
||||||
|
// A specific exit code (2) from the client indicates the service is not running
|
||||||
|
if (clientResult == 2)
|
||||||
|
{
|
||||||
|
// Allow help output even if no daemon and no --instance
|
||||||
|
bool wantsHelp = args.Any(arg => arg == "help" || arg == "--help" || arg == "-h");
|
||||||
|
if (!wantsHelp)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red("Error: RedisManager daemon/service is not running. Please start the service (no arguments) and retry your command."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the result from the client (0 for success, 1 for other errors)
|
||||||
|
return clientResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs RedisManager in service mode, starting a TCP server that accepts client connections.
|
||||||
|
/// The service runs indefinitely until interrupted by Ctrl+C or other termination signals.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Exit code (0 for successful shutdown)</returns>
|
||||||
|
static async Task<int> RunServiceMode()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Starting RedisManager Service...");
|
||||||
|
var service = new RedisManagerService();
|
||||||
|
await service.StartAsync();
|
||||||
|
|
||||||
|
// Block the main thread until Ctrl+C is pressed
|
||||||
|
var cts = new System.Threading.CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (sender, e) => {
|
||||||
|
e.Cancel = true;
|
||||||
|
cts.Cancel();
|
||||||
|
};
|
||||||
|
Console.WriteLine("RedisManager daemon is running. Press Ctrl+C to exit.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(-1, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Shutting down RedisManager Service...");
|
||||||
|
}
|
||||||
|
await service.StopAsync();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
462
README.md
462
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 -- <command>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
- **Multi-Mode Operation**: Standalone CLI, Service mode, and Client mode
|
||||||
|
- **Instance Management**: Add, update, delete, and list Redis instances
|
||||||
|
- **Comprehensive Redis Commands**: Support for all major Redis data types and operations
|
||||||
|
- **Formatted Output**: Table formatting, JSON pretty-printing, and colored output
|
||||||
|
- **Persistent Redis Configuration**: Set, list, and persist custom Redis config parameters per instance; parameters are automatically applied on instance startup
|
||||||
|
- **Configuration Management**: JSON-based configuration with automatic defaults
|
||||||
|
|
||||||
|
### Supported Redis Operations
|
||||||
|
|
||||||
|
#### String Operations
|
||||||
|
- `get` - Retrieve key values with JSON formatting
|
||||||
|
- `set` - Set key values with optional TTL
|
||||||
|
- `del` - Delete keys with confirmation prompts
|
||||||
|
- Advanced operations: `append`, `incr`, `decr`, `getrange`, `setrange`, `strlen`, `mget`, `mset`
|
||||||
|
|
||||||
|
#### Hash Operations
|
||||||
|
- `hget`, `hset`, `hdel`, `hgetall`, `hkeys`, `hvals`, `hlen`, `hexists`
|
||||||
|
- `hincrby`, `hincrbyfloat`, `hmset`, `hmget`, `hsetnx`, `hstrlen`, `hscan`
|
||||||
|
|
||||||
|
#### List Operations
|
||||||
|
- `lpush`, `rpush`, `lpop`, `rpop`, `lrange`, `llen`, `lindex`, `lset`
|
||||||
|
- `linsert`, `lrem`, `ltrim`, `blpop`, `brpop`, `rpoplpush`
|
||||||
|
|
||||||
|
#### Set Operations
|
||||||
|
- `sadd`, `srem`, `smembers`, `scard`, `sismember`, `srandmember`, `spop`
|
||||||
|
- `sinter`, `sunion`, `sdiff`, `sinterstore`, `sunionstore`, `sdiffstore`, `sscan`, `smove`
|
||||||
|
|
||||||
|
#### Sorted Set Operations
|
||||||
|
- `zadd`, `zrem`, `zrange`, `zrevrange`, `zrangebyscore`, `zcard`, `zscore`
|
||||||
|
- `zrank`, `zrevrank`, `zincrby`, `zrevrangebyscore`, `zcount`
|
||||||
|
- `zunionstore`, `zinterstore`, `zscan`, `zpopmax`, `zpopmin`
|
||||||
|
- `zremrangebyrank`, `zremrangebyscore`
|
||||||
|
|
||||||
|
#### Key Management
|
||||||
|
- `exists`, `expire`, `ttl`, `persist`, `rename`, `type`, `keys`, `scan`
|
||||||
|
|
||||||
|
#### Database Operations
|
||||||
|
- `select`, `flushdb`, `flushall`, `dbsize`
|
||||||
|
|
||||||
|
#### Server Information
|
||||||
|
- `info`, `config`, `save`, `bgsave`, `lastsave`, `time`, `ping`
|
||||||
|
|
||||||
|
#### Persistent Config Management
|
||||||
|
- `config --set` - Persistently set a Redis config parameter for an instance (e.g., maxmemory, save, etc.)
|
||||||
|
- `config --list-custom` - List all custom config parameters persisted for an instance
|
||||||
|
- Custom config parameters are saved in `redismanager.json` and automatically applied to the Redis/Valkey server on instance startup
|
||||||
|
|
||||||
|
#### Connection Management
|
||||||
|
- `auth`, `quit`, `client list`, `client kill`, `client getname`, `client setname`
|
||||||
|
|
||||||
|
#### Pub/Sub
|
||||||
|
- `publish`, `subscribe`, `unsubscribe`
|
||||||
|
|
||||||
|
#### Scripting
|
||||||
|
- `eval`, `evalsha`, `script load`, `script exists`, `script flush`
|
||||||
|
|
||||||
|
#### Transactions
|
||||||
|
- `multi`, `exec`, `discard`, `watch`, `unwatch`
|
||||||
|
|
||||||
|
#### Geographic Operations
|
||||||
|
- `geoadd`, `geodist`, `geohash`, `geopos`, `georadius`
|
||||||
|
|
||||||
|
#### Bit Operations
|
||||||
|
- `bitcount`, `bitfield`, `bitop`, `bitpos`, `getbit`, `setbit`
|
||||||
|
|
||||||
|
#### HyperLogLog
|
||||||
|
- `pfadd`, `pfcount`, `pfmerge`
|
||||||
|
|
||||||
|
#### Streams
|
||||||
|
- `xadd`, `xrange`, `xlen`, `xdel`
|
||||||
|
|
||||||
|
#### Module Management
|
||||||
|
- `module list`, `module load`, `module unload`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 9.0 or later
|
||||||
|
- Redis server(s) to connect to
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
cd RedisManager
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command Usage Examples
|
||||||
|
|
||||||
|
Below are examples for all major command types. All commands must be run with the daemon/service running.
|
||||||
|
|
||||||
|
### String Commands
|
||||||
|
```bash
|
||||||
|
# Set a value
|
||||||
|
dotnet run -- set -i default mykey "hello world"
|
||||||
|
# Get a value
|
||||||
|
dotnet run -- get -i default mykey
|
||||||
|
# Delete a key
|
||||||
|
dotnet run -- del -i default mykey
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash Commands
|
||||||
|
```bash
|
||||||
|
# Set multiple fields
|
||||||
|
dotnet run -- hset -i default user:1 name "Alice" age "30"
|
||||||
|
# Get all fields
|
||||||
|
dotnet run -- hgetall -i default user:1
|
||||||
|
# Get a field
|
||||||
|
dotnet run -- hget -i default user:1 name
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Commands
|
||||||
|
```bash
|
||||||
|
# Push to a list
|
||||||
|
dotnet run -- lpush -i default mylist "one" "two"
|
||||||
|
# Get range
|
||||||
|
dotnet run -- lrange -i default mylist 0 -1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Commands
|
||||||
|
```bash
|
||||||
|
# Add members
|
||||||
|
dotnet run -- sadd -i default myset "a" "b"
|
||||||
|
# List members
|
||||||
|
dotnet run -- smembers -i default myset
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sorted Set Commands
|
||||||
|
```bash
|
||||||
|
# Add with scores
|
||||||
|
dotnet run -- zadd -i default myzset 1 "a" 2 "b"
|
||||||
|
# Range by score
|
||||||
|
dotnet run -- zrangebyscore -i default myzset 0 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Management
|
||||||
|
```bash
|
||||||
|
# List keys
|
||||||
|
dotnet run -- keys -i default "*"
|
||||||
|
# Set expiration
|
||||||
|
dotnet run -- expire -i default mykey 60
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Commands
|
||||||
|
```bash
|
||||||
|
# Select DB
|
||||||
|
dotnet run -- select -i default 0
|
||||||
|
# Get DB size
|
||||||
|
dotnet run -- dbsize -i default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Information
|
||||||
|
```bash
|
||||||
|
# Get server info
|
||||||
|
dotnet run -- info -i default
|
||||||
|
# Ping server
|
||||||
|
dotnet run -- ping -i default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistent Config Management
|
||||||
|
```bash
|
||||||
|
# Set and persist a config parameter
|
||||||
|
dotnet run -- config --instance default --set "maxmemory 128mb"
|
||||||
|
# List custom config parameters
|
||||||
|
dotnet run -- config --instance default --list-custom
|
||||||
|
# Get current value from Redis
|
||||||
|
dotnet run -- config --instance default --get maxmemory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pub/Sub
|
||||||
|
```bash
|
||||||
|
# Publish a message
|
||||||
|
dotnet run -- publish -i default mychannel "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripting
|
||||||
|
```bash
|
||||||
|
# Run a Lua script
|
||||||
|
dotnet run -- eval -i default "return redis.call('set', KEYS[1], ARGV[1])" 1 mykey myvalue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
```bash
|
||||||
|
# Start a transaction
|
||||||
|
dotnet run -- multi -i default
|
||||||
|
# Execute transaction
|
||||||
|
dotnet run -- exec -i default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geo Commands
|
||||||
|
```bash
|
||||||
|
# Add geo data
|
||||||
|
dotnet run -- geoadd -i default places 13.361389 38.115556 "Palermo"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bit Operations
|
||||||
|
```bash
|
||||||
|
# Set and get bit
|
||||||
|
dotnet run -- setbit -i default mykey 7 1
|
||||||
|
dotnet run -- getbit -i default mykey 7
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Management
|
||||||
|
```bash
|
||||||
|
# List modules
|
||||||
|
dotnet run -- module list -i default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Management
|
||||||
|
```bash
|
||||||
|
# List instances
|
||||||
|
dotnet run -- list-instances
|
||||||
|
# Add a new instance
|
||||||
|
dotnet run -- add-instance -n newinst -h localhost -p 6381
|
||||||
|
# Delete an instance
|
||||||
|
dotnet run -- delete-instance -n newinst
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Service (Daemon) Mode
|
||||||
|
Run as a TCP service that accepts client connections:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the daemon/service
|
||||||
|
cd RedisManager
|
||||||
|
dotnet run
|
||||||
|
# The service runs on port 6380 by default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Mode
|
||||||
|
All commands must be executed via the daemon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set a value
|
||||||
|
cd RedisManager
|
||||||
|
dotnet run -- set -i default mykey "hello world"
|
||||||
|
|
||||||
|
# Get a value
|
||||||
|
dotnet run -- get -i default mykey
|
||||||
|
|
||||||
|
# List all configured instances
|
||||||
|
dotnet run -- list-instances
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The daemon/service must be running for any command to work (except --help).
|
||||||
|
|
||||||
|
### Service Mode
|
||||||
|
Run as a TCP service that accepts client connections:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the service
|
||||||
|
dotnet run -- --service
|
||||||
|
|
||||||
|
# The service runs on port 6379 by default
|
||||||
|
# Clients can connect and send JSON-formatted requests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Mode
|
||||||
|
Connect to a running service instance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute commands through the service
|
||||||
|
dotnet run -- --client get -i default mykey
|
||||||
|
dotnet run -- --client set -i default mykey "value"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The tool uses a `redismanager.json` configuration file that is automatically created with default settings:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Instances": [
|
||||||
|
{
|
||||||
|
"Name": "default",
|
||||||
|
"Host": "localhost",
|
||||||
|
"Port": 6379,
|
||||||
|
"Password": null,
|
||||||
|
"CustomConfig": {
|
||||||
|
"maxmemory": "128mb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `CustomConfig` dictionary stores all persistent Redis config parameters for each instance.
|
||||||
|
- When the daemon or instance starts, all parameters in `CustomConfig` are automatically applied to the Redis/Valkey server.
|
||||||
|
- You can edit these parameters using the `config --set` and `config --list-custom` commands.
|
||||||
|
|
||||||
|
### Persistent Redis Config Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Persistently set a Redis config parameter (e.g., maxmemory) for an instance
|
||||||
|
# This will save the parameter in redismanager.json and apply it on instance startup
|
||||||
|
|
||||||
|
dotnet run -- config --instance default --set "maxmemory 128mb"
|
||||||
|
|
||||||
|
# List all custom config parameters persisted for an instance
|
||||||
|
|
||||||
|
dotnet run -- config --instance default --list-custom
|
||||||
|
|
||||||
|
# Get the current value of a config parameter from the running Redis instance
|
||||||
|
|
||||||
|
dotnet run -- config --instance default --get maxmemory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Management Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all configured instances
|
||||||
|
dotnet run -- list-instances
|
||||||
|
|
||||||
|
# Add a new instance
|
||||||
|
dotnet run -- add-instance -n production -h redis.prod.com -p 6379 -w "password"
|
||||||
|
|
||||||
|
# Update an existing instance
|
||||||
|
dotnet run -- update-instance -n production -h new-redis.prod.com
|
||||||
|
|
||||||
|
# Delete an instance
|
||||||
|
dotnet run -- delete-instance -n production
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
|
||||||
|
### Table Format
|
||||||
|
Many commands support `--table` output for structured data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run -- get -i default mykey --table
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Formatting
|
||||||
|
String values that appear to be JSON are automatically pretty-printed.
|
||||||
|
|
||||||
|
### Colored Output
|
||||||
|
- Green: Success messages and data
|
||||||
|
- Red: Error messages
|
||||||
|
- Yellow: Warnings and confirmations
|
||||||
|
- Blue: Headers and navigation
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
#### Program.cs
|
||||||
|
Main entry point that handles three operational modes:
|
||||||
|
- **Standalone Mode**: Direct command execution
|
||||||
|
- **Service Mode**: TCP server for client connections
|
||||||
|
- **Client Mode**: Client for communicating with service
|
||||||
|
|
||||||
|
#### RedisManagerService.cs
|
||||||
|
TCP server implementation that:
|
||||||
|
- Accepts client connections
|
||||||
|
- Processes JSON-formatted requests
|
||||||
|
- Executes Redis commands
|
||||||
|
- Returns formatted responses
|
||||||
|
|
||||||
|
#### RedisManagerClient.cs
|
||||||
|
Client implementation that:
|
||||||
|
- Connects to the service
|
||||||
|
- Sends command requests
|
||||||
|
- Displays responses
|
||||||
|
|
||||||
|
#### Command Classes
|
||||||
|
Organized by Redis data type and functionality:
|
||||||
|
- `StringCommands.cs` - Basic string operations
|
||||||
|
- `HashCommands.cs` - Hash operations
|
||||||
|
- `ListCommands.cs` - List operations
|
||||||
|
- `SetCommands.cs` - Set operations
|
||||||
|
- `SortedSetCommands.cs` - Sorted set operations
|
||||||
|
- `KeyCommands.cs` - Key management
|
||||||
|
- `DatabaseCommands.cs` - Database operations
|
||||||
|
- `ServerCommands.cs` - Server information
|
||||||
|
- `ConnectionCommands.cs` - Connection management
|
||||||
|
- `PubSubCommands.cs` - Pub/Sub operations
|
||||||
|
- `ScriptingCommands.cs` - Lua scripting
|
||||||
|
- `TransactionCommands.cs` - Transaction support
|
||||||
|
- `GeoCommands.cs` - Geographic operations
|
||||||
|
- `BitCommands.cs` - Bit operations
|
||||||
|
- `HyperLogLogCommands.cs` - HyperLogLog operations
|
||||||
|
- `StreamCommands.cs` - Stream operations
|
||||||
|
- `ModuleCommands.cs` - Module management
|
||||||
|
- `AdvancedStringCommands.cs` - Advanced string operations
|
||||||
|
- `InstanceCommands.cs` - Instance management
|
||||||
|
- `StatusCommand.cs` - Connection status
|
||||||
|
|
||||||
|
#### Utility Classes
|
||||||
|
|
||||||
|
##### RedisUtils.cs
|
||||||
|
Provides common Redis functionality:
|
||||||
|
- Instance configuration retrieval
|
||||||
|
- Redis connection establishment
|
||||||
|
- Data type conversions
|
||||||
|
- Table formatting
|
||||||
|
|
||||||
|
##### Output.cs
|
||||||
|
ANSI color formatting utilities for console output.
|
||||||
|
|
||||||
|
##### Config.cs
|
||||||
|
Configuration management:
|
||||||
|
- `InstanceConfig` - Individual Redis instance settings
|
||||||
|
- `Config` - Main configuration container
|
||||||
|
- `ConfigManager` - Configuration loading/saving utilities
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The tool provides comprehensive error handling:
|
||||||
|
- Connection failures with helpful messages
|
||||||
|
- Invalid command syntax with usage hints
|
||||||
|
- Missing instances with configuration guidance
|
||||||
|
- Redis operation failures with detailed error messages
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Passwords are stored in plain text in the configuration file
|
||||||
|
- No encryption for client-server communication
|
||||||
|
- Consider using Redis ACLs for production environments
|
||||||
|
- Use secure connections (TLS) for sensitive data
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Add comprehensive documentation for new commands
|
||||||
|
4. Ensure all tests pass
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is part of a larger trading platform and follows the same licensing terms.
|
||||||
16
RedisManager.csproj
Normal file
16
RedisManager.csproj
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
|
<PackageReference Include="Crayon" Version="2.0.69" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
79
RedisManagerClient.cs
Normal file
79
RedisManagerClient.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace RedisManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for communicating with the RedisManager service.
|
||||||
|
/// Connects to a running service instance and sends command requests over TCP.
|
||||||
|
/// </summary>
|
||||||
|
public class RedisManagerClient
|
||||||
|
{
|
||||||
|
private readonly string _host = "localhost";
|
||||||
|
private readonly int _port = 6380; // Same port as service
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a Redis command by sending it to the RedisManager service.
|
||||||
|
/// Connects to the service, sends the command arguments, and displays the result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command line arguments to execute on the service</param>
|
||||||
|
/// <returns>Exit code (0 for success, 1 for failure)</returns>
|
||||||
|
public async Task<int> ExecuteCommandAsync(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(_host, _port);
|
||||||
|
|
||||||
|
var request = new ServiceRequest
|
||||||
|
{
|
||||||
|
Command = "execute",
|
||||||
|
Arguments = args
|
||||||
|
};
|
||||||
|
|
||||||
|
var requestJson = JsonSerializer.Serialize(request) + "\n";
|
||||||
|
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
|
||||||
|
await writer.WriteAsync(requestJson);
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream, new UTF8Encoding(false));
|
||||||
|
var responseJson = await reader.ReadLineAsync();
|
||||||
|
if (responseJson != null)
|
||||||
|
{
|
||||||
|
var serviceResponse = JsonSerializer.Deserialize<ServiceResponse>(responseJson);
|
||||||
|
if (serviceResponse.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine(serviceResponse.Data);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error: {serviceResponse.Error}");
|
||||||
|
if (!string.IsNullOrEmpty(serviceResponse.ErrorCode))
|
||||||
|
Console.WriteLine($"Error Code: {serviceResponse.ErrorCode}");
|
||||||
|
if (serviceResponse.ErrorDetails != null)
|
||||||
|
Console.WriteLine($"Error Details: {JsonSerializer.Serialize(serviceResponse.ErrorDetails)}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("No response from service");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
// Special code to indicate the service is not running
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error communicating with service: {ex.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
RedisManagerService.ApplyCustomConfig.cs
Normal file
31
RedisManagerService.ApplyCustomConfig.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RedisManager;
|
||||||
|
using RedisManager.Utils;
|
||||||
|
|
||||||
|
namespace RedisManager
|
||||||
|
{
|
||||||
|
public static class CustomConfigApplier
|
||||||
|
{
|
||||||
|
public static async Task ApplyCustomConfigAsync(InstanceConfig instance)
|
||||||
|
{
|
||||||
|
if (instance?.CustomConfig == null || instance.CustomConfig.Count == 0)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mux = RedisUtils.ConnectRedis(instance);
|
||||||
|
var db = mux.GetDatabase();
|
||||||
|
foreach (var kv in instance.CustomConfig)
|
||||||
|
{
|
||||||
|
db.Execute("CONFIG", "SET", kv.Key, kv.Value);
|
||||||
|
}
|
||||||
|
mux.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Warning: Failed to apply custom config for instance '{instance.Name}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
931
RedisManagerService.cs
Normal file
931
RedisManagerService.cs
Normal file
@ -0,0 +1,931 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommandLine;
|
||||||
|
using RedisManager.Commands;
|
||||||
|
|
||||||
|
namespace RedisManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TCP server that provides Redis command execution as a service.
|
||||||
|
/// Accepts client connections and executes Redis commands on their behalf.
|
||||||
|
/// Clients send JSON-formatted requests and receive JSON responses.
|
||||||
|
/// </summary>
|
||||||
|
public class RedisManagerService
|
||||||
|
{
|
||||||
|
private TcpListener _listener;
|
||||||
|
private readonly ConcurrentDictionary<string, DateTime> _clients = new();
|
||||||
|
private Config _config;
|
||||||
|
private readonly object _configLock = new();
|
||||||
|
// Track Valkey/Redis server processes started by the daemon
|
||||||
|
private readonly ConcurrentDictionary<string, System.Diagnostics.Process> _instanceProcesses = new();
|
||||||
|
private bool _isRunning = false;
|
||||||
|
private readonly int _port = 6380; // Port for service communication
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the RedisManagerService.
|
||||||
|
/// Loads configuration and prepares the service for operation.
|
||||||
|
/// </summary>
|
||||||
|
public RedisManagerService()
|
||||||
|
{
|
||||||
|
_config = ConfigManager.LoadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the TCP server and begins accepting client connections.
|
||||||
|
/// The server runs indefinitely until StopAsync is called.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous start operation</returns>
|
||||||
|
public Task StartAsync()
|
||||||
|
{
|
||||||
|
if (_isRunning) return Task.CompletedTask;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_listener = new TcpListener(IPAddress.Any, _port);
|
||||||
|
_listener.Start();
|
||||||
|
_isRunning = true;
|
||||||
|
}
|
||||||
|
catch (SocketException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error starting service on port {_port}: {ex.Message}");
|
||||||
|
Console.WriteLine("Another instance of the service may be running, or the port is in use.");
|
||||||
|
_isRunning = false;
|
||||||
|
return Task.CompletedTask; // Exit if we can't start the listener
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"RedisManager Service started on port {_port}");
|
||||||
|
|
||||||
|
// Start Valkey/Redis servers for all configured instances at startup
|
||||||
|
Task.Run(async () => {
|
||||||
|
bool anySuccess = false;
|
||||||
|
List<InstanceConfig> instancesSnapshot;
|
||||||
|
lock (_configLock)
|
||||||
|
{
|
||||||
|
instancesSnapshot = new List<InstanceConfig>(_config.Instances);
|
||||||
|
}
|
||||||
|
foreach (var instance in instancesSnapshot)
|
||||||
|
{
|
||||||
|
var result = await StartValkeyServerIfNeeded(instance);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Started Valkey/Redis for instance '{instance.Name}' on port {instance.Port}.");
|
||||||
|
anySuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Failed to start Valkey/Redis for instance '{instance.Name}' on port {instance.Port}: {result.Output}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anySuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Daemon] FATAL: Could not start any Valkey/Redis server. Check your ServerBinaryPath configuration and ensure the binary exists and is executable.");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the client accepting loop in the background
|
||||||
|
_ = AcceptClientsAsync();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AcceptClientsAsync()
|
||||||
|
{
|
||||||
|
while (_isRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = await _listener.AcceptTcpClientAsync();
|
||||||
|
_ = Task.Run(() => HandleClientAsync(client));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!_isRunning)
|
||||||
|
{
|
||||||
|
// Expected when stopping
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Error accepting client: {ex}");
|
||||||
|
// Continue loop; do not exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the TCP server and closes all client connections.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous stop operation</returns>
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
if (!_isRunning) return;
|
||||||
|
|
||||||
|
_isRunning = false;
|
||||||
|
_listener?.Stop();
|
||||||
|
Console.WriteLine("RedisManager Service stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles a single client connection asynchronously.
|
||||||
|
/// Processes JSON-formatted command requests and sends back responses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The TCP client connection to handle</param>
|
||||||
|
/// <returns>A task that represents the asynchronous client handling operation</returns>
|
||||||
|
private async Task HandleClientAsync(TcpClient client)
|
||||||
|
{
|
||||||
|
var clientId = Guid.NewGuid().ToString();
|
||||||
|
_clients[clientId] = DateTime.UtcNow;
|
||||||
|
Console.WriteLine($"[Daemon] Client connected: {clientId}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
using var reader = new StreamReader(stream, new UTF8Encoding(false));
|
||||||
|
using var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
|
||||||
|
|
||||||
|
while (client.Connected)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var line = await reader.ReadLineAsync();
|
||||||
|
if (line == null) break;
|
||||||
|
Console.WriteLine($"[Daemon] Received command: {line}");
|
||||||
|
var response = await ExecuteCommandAsync(line);
|
||||||
|
var responseJson = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = false });
|
||||||
|
await writer.WriteLineAsync(responseJson);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Error processing client command: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] Error handling client: {ex}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_clients.TryRemove(clientId, out _);
|
||||||
|
Console.WriteLine($"[Daemon] Client disconnected: {clientId}");
|
||||||
|
client.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a Redis command from a JSON-formatted request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandJson">JSON string containing the command request</param>
|
||||||
|
/// <returns>A ServiceResponse object with the command execution results</returns>
|
||||||
|
private async Task<ServiceResponse> ExecuteCommandAsync(string commandJson)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = JsonSerializer.Deserialize<ServiceRequest>(commandJson);
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = null,
|
||||||
|
Error = "Invalid request format",
|
||||||
|
ErrorCode = "INVALID_REQUEST",
|
||||||
|
ErrorDetails = new { RawInput = commandJson }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for reload-config
|
||||||
|
if (request.Arguments != null && request.Arguments.Length > 0 && string.Equals(request.Arguments[0], "reload-config", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (request.Arguments.Length > 1)
|
||||||
|
{
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = null,
|
||||||
|
Error = "'reload-config' takes no arguments",
|
||||||
|
ErrorCode = "ARGUMENT_ERROR",
|
||||||
|
ErrorDetails = new { Command = "reload-config", Args = request.Arguments }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newConfig = ConfigManager.LoadConfig();
|
||||||
|
lock (_configLock)
|
||||||
|
{
|
||||||
|
_config = newConfig;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[Daemon] Config reloaded from disk at {DateTime.Now:O}.");
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = "Configuration reloaded successfully.",
|
||||||
|
Error = null,
|
||||||
|
ErrorCode = null,
|
||||||
|
ErrorDetails = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Daemon] ERROR: Failed to reload config: {ex.Message}");
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = null,
|
||||||
|
Error = $"Failed to reload configuration: {ex.Message}",
|
||||||
|
ErrorCode = "CONFIG_RELOAD_FAILED",
|
||||||
|
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (success, output) = await ExecuteRedisCommandAsync(request.Arguments);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = null,
|
||||||
|
Error = output,
|
||||||
|
ErrorCode = "COMMAND_EXECUTION_FAILED",
|
||||||
|
ErrorDetails = new { Arguments = request.Arguments }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = output,
|
||||||
|
Error = null,
|
||||||
|
ErrorCode = null,
|
||||||
|
ErrorDetails = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ServiceResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = null,
|
||||||
|
Error = ex.Message,
|
||||||
|
ErrorCode = "EXCEPTION",
|
||||||
|
ErrorDetails = new { ExceptionType = ex.GetType().Name, ExceptionMessage = ex.Message }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Check if a TCP port is open
|
||||||
|
private bool IsPortOpen(string host, int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
var result = client.BeginConnect(host, port, null, null);
|
||||||
|
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(300));
|
||||||
|
return success && client.Connected;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Check if a Redis/Valkey server is running on host:port
|
||||||
|
private bool IsRedisServer(string host, int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var client = new TcpClient(host, port);
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
var ping = Encoding.ASCII.GetBytes("*1\r\n$4\r\nPING\r\n");
|
||||||
|
stream.Write(ping, 0, ping.Length);
|
||||||
|
stream.Flush();
|
||||||
|
var buffer = new byte[64];
|
||||||
|
int read = stream.Read(buffer, 0, buffer.Length);
|
||||||
|
var response = Encoding.ASCII.GetString(buffer, 0, read);
|
||||||
|
return response.Contains("PONG");
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Start Valkey/Redis server for an instance if not running
|
||||||
|
private async Task<(bool Success, string Output)> StartValkeyServerIfNeeded(InstanceConfig instance)
|
||||||
|
{
|
||||||
|
// Already started by us?
|
||||||
|
if (_instanceProcesses.TryGetValue(instance.Name, out var existingProc) && !existingProc.HasExited)
|
||||||
|
return (true, "");
|
||||||
|
|
||||||
|
// Is port open?
|
||||||
|
if (IsPortOpen(instance.Host, instance.Port))
|
||||||
|
{
|
||||||
|
if (IsRedisServer(instance.Host, instance.Port))
|
||||||
|
return (true, "");
|
||||||
|
else
|
||||||
|
return (false, $"Port {instance.Port} is in use but is not a Redis/Valkey server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine binary path
|
||||||
|
string binaryPath = instance.ServerBinaryPath ?? _config.ServerBinaryPath;
|
||||||
|
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
|
||||||
|
return (false, $"Redis/Valkey server binary not found at '{binaryPath}'. Please set 'ServerBinaryPath' in redismanager.json to the correct path for your Valkey or Redis server binary.");
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = binaryPath,
|
||||||
|
Arguments = $"--port {instance.Port}",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var proc = System.Diagnostics.Process.Start(psi);
|
||||||
|
if (proc == null)
|
||||||
|
return (false, $"Failed to start Valkey/Redis server for instance '{instance.Name}'.");
|
||||||
|
_instanceProcesses[instance.Name] = proc;
|
||||||
|
// Wait briefly for the server to start
|
||||||
|
await Task.Delay(500);
|
||||||
|
if (!IsPortOpen(instance.Host, instance.Port))
|
||||||
|
return (false, $"Valkey/Redis server did not start on port {instance.Port}.");
|
||||||
|
|
||||||
|
// Apply custom config options after server is confirmed up
|
||||||
|
await CustomConfigApplier.ApplyCustomConfigAsync(instance);
|
||||||
|
|
||||||
|
return (true, $"Started Valkey/Redis server for instance '{instance.Name}'.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, $"Exception starting Valkey/Redis server: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes Redis commands by parsing arguments and routing to appropriate command handlers.
|
||||||
|
/// Captures console output and returns it as a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command line arguments to execute</param>
|
||||||
|
/// <returns>The captured console output from command execution</returns>
|
||||||
|
private async Task<(bool Success, string Output, string ErrorCode, object ErrorDetails)> ValidateArguments(string[] args)
|
||||||
|
{
|
||||||
|
// Special validation for reload-config
|
||||||
|
if (args.Length > 0 && string.Equals(args[0], "reload-config", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (args.Length == 1)
|
||||||
|
return (true, null, null, null);
|
||||||
|
else
|
||||||
|
return (false, "'reload-config' takes no arguments", "ARGUMENT_ERROR", new { Command = "reload-config", Args = args });
|
||||||
|
}
|
||||||
|
if (args == null || args.Length == 0)
|
||||||
|
{
|
||||||
|
return (false, "No command arguments provided.", "ARGUMENT_ERROR", new { Args = args });
|
||||||
|
}
|
||||||
|
// Instance flag validation
|
||||||
|
string instanceName = null;
|
||||||
|
var filteredArgs = new List<string>();
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
if ((args[i] == "-i" || args[i] == "--instance") && i + 1 < args.Length && !string.IsNullOrWhiteSpace(args[i + 1]))
|
||||||
|
{
|
||||||
|
instanceName = args[i + 1];
|
||||||
|
i++; // Skip instance name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filteredArgs.Add(args[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Whitelist of commands that do NOT require instance
|
||||||
|
var noInstanceRequired = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"list-instances", "add-instance", "update-instance", "delete-instance", "reload-config", "help", "version"
|
||||||
|
};
|
||||||
|
string commandName = filteredArgs.Count > 0 ? filteredArgs[0].ToLowerInvariant() : null;
|
||||||
|
bool needsInstance = !noInstanceRequired.Contains(commandName);
|
||||||
|
if (needsInstance && string.IsNullOrEmpty(instanceName))
|
||||||
|
{
|
||||||
|
return (false, "Instance name must be specified with --instance or -i.", "INSTANCE_REQUIRED", new { Command = commandName, Args = args });
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(instanceName))
|
||||||
|
{
|
||||||
|
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
|
||||||
|
if (instanceConfig == null)
|
||||||
|
{
|
||||||
|
return (false, $"Instance '{instanceName}' not found in config.", "ARGUMENT_ERROR", new { Instance = instanceName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Command-specific validation
|
||||||
|
if (filteredArgs.Count > 0)
|
||||||
|
{
|
||||||
|
string cmd = filteredArgs[0].ToLowerInvariant();
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case "get":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'get' command requires exactly 1 argument: get <key>", "ARGUMENT_ERROR", new { Command = "get", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "set":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'set' command requires at least 2 arguments: set <key> <value> [EX seconds] [PX milliseconds] [NX|XX]", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
// Validate optional flags for set
|
||||||
|
var setFlags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "EX", "PX", "NX", "XX" };
|
||||||
|
bool seenNX = false, seenXX = false;
|
||||||
|
int iSet = 3;
|
||||||
|
while (iSet < filteredArgs.Count)
|
||||||
|
{
|
||||||
|
string flag = filteredArgs[iSet];
|
||||||
|
if (flag.Equals("EX", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
|
||||||
|
return (false, "'set' EX flag must be followed by an integer (seconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
iSet += 2;
|
||||||
|
}
|
||||||
|
else if (flag.Equals("PX", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (iSet + 1 >= filteredArgs.Count || !int.TryParse(filteredArgs[iSet + 1], out _))
|
||||||
|
return (false, "'set' PX flag must be followed by an integer (milliseconds)", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
iSet += 2;
|
||||||
|
}
|
||||||
|
else if (flag.Equals("NX", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (seenXX)
|
||||||
|
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
seenNX = true;
|
||||||
|
iSet++;
|
||||||
|
}
|
||||||
|
else if (flag.Equals("XX", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (seenNX)
|
||||||
|
return (false, "'set' cannot have both NX and XX flags", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
seenXX = true;
|
||||||
|
iSet++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (false, $"Unknown or misplaced flag '{flag}' in 'set' command", "ARGUMENT_ERROR", new { Command = "set", Args = filteredArgs });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "del":
|
||||||
|
if (filteredArgs.Count < 2)
|
||||||
|
return (false, "'del' command requires at least 1 argument: del <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "del", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hget":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'hget' command requires exactly 2 arguments: hget <hash> <field>", "ARGUMENT_ERROR", new { Command = "hget", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hset":
|
||||||
|
if (filteredArgs.Count != 4)
|
||||||
|
return (false, "'hset' command requires exactly 3 arguments: hset <hash> <field> <value>", "ARGUMENT_ERROR", new { Command = "hset", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "mget":
|
||||||
|
if (filteredArgs.Count < 2)
|
||||||
|
return (false, "'mget' command requires at least 1 argument: mget <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "mget", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "mset":
|
||||||
|
if (filteredArgs.Count < 3 || filteredArgs.Count % 2 != 1)
|
||||||
|
return (false, "'mset' command requires an even number of arguments ≥ 2: mset <key1> <value1> [key2 value2 ...]", "ARGUMENT_ERROR", new { Command = "mset", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "lpush":
|
||||||
|
case "rpush":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, $"'{cmd}' command requires at least 2 arguments: {cmd} <list> <value1> [value2 ...]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "lrange":
|
||||||
|
if (filteredArgs.Count != 4)
|
||||||
|
return (false, "'lrange' command requires exactly 3 arguments: lrange <list> <start> <stop>", "ARGUMENT_ERROR", new { Command = "lrange", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zadd":
|
||||||
|
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
|
||||||
|
return (false, "'zadd' command requires at least 3 arguments and pairs: zadd <zset> <score1> <member1> [score2 member2 ...]", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
|
||||||
|
// Validate that all scores are valid numbers
|
||||||
|
for (int i = 2; i < filteredArgs.Count; i += 2)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(filteredArgs[i], out _))
|
||||||
|
return (false, $"'zadd' score argument at position {i} must be a valid number", "ARGUMENT_ERROR", new { Command = "zadd", Args = filteredArgs });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "sadd":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'sadd' command requires at least 2 arguments: sadd <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "sadd", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "exists":
|
||||||
|
if (filteredArgs.Count < 2)
|
||||||
|
return (false, "'exists' command requires at least 1 argument: exists <key1> [key2 ...]", "ARGUMENT_ERROR", new { Command = "exists", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "expire":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'expire' command requires exactly 2 arguments: expire <key> <seconds>", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
|
||||||
|
if (!int.TryParse(filteredArgs[2], out _))
|
||||||
|
return (false, "'expire' seconds argument must be a valid integer", "ARGUMENT_ERROR", new { Command = "expire", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "rename":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'rename' command requires exactly 2 arguments: rename <key> <newkey>", "ARGUMENT_ERROR", new { Command = "rename", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "select":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'select' command requires exactly 1 argument: select <dbindex>", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
|
||||||
|
if (!int.TryParse(filteredArgs[1], out int dbidx) || dbidx < 0)
|
||||||
|
return (false, "'select' dbindex must be a non-negative integer", "ARGUMENT_ERROR", new { Command = "select", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "flushdb":
|
||||||
|
case "flushall":
|
||||||
|
case "dbsize":
|
||||||
|
case "info":
|
||||||
|
case "ping":
|
||||||
|
case "quit":
|
||||||
|
if (filteredArgs.Count != 1)
|
||||||
|
return (false, $"'{cmd}' command takes no arguments", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "auth":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'auth' command requires exactly 1 argument: auth <password>", "ARGUMENT_ERROR", new { Command = "auth", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "keys":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'keys' command requires exactly 1 argument: keys <pattern>", "ARGUMENT_ERROR", new { Command = "keys", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "ttl":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'ttl' command requires exactly 1 argument: ttl <key>", "ARGUMENT_ERROR", new { Command = "ttl", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "type":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'type' command requires exactly 1 argument: type <key>", "ARGUMENT_ERROR", new { Command = "type", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "persist":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'persist' command requires exactly 1 argument: persist <key>", "ARGUMENT_ERROR", new { Command = "persist", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "scard":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'scard' command requires exactly 1 argument: scard <set>", "ARGUMENT_ERROR", new { Command = "scard", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "smembers":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'smembers' command requires exactly 1 argument: smembers <set>", "ARGUMENT_ERROR", new { Command = "smembers", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "sismember":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'sismember' command requires exactly 2 arguments: sismember <set> <member>", "ARGUMENT_ERROR", new { Command = "sismember", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "srem":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'srem' command requires at least 2 arguments: srem <set> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "srem", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zcard":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'zcard' command requires exactly 1 argument: zcard <zset>", "ARGUMENT_ERROR", new { Command = "zcard", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zrange":
|
||||||
|
case "zrevrange":
|
||||||
|
if (filteredArgs.Count != 4 && filteredArgs.Count != 5)
|
||||||
|
return (false, $"'{cmd}' command requires 3 or 4 arguments: {cmd} <zset> <start> <stop> [WITHSCORES]", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
if (filteredArgs.Count == 5 && !filteredArgs[4].Equals("WITHSCORES", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return (false, $"'{cmd}' 5th argument must be 'WITHSCORES' if present.", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zscore":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'zscore' command requires exactly 2 arguments: zscore <zset> <member>", "ARGUMENT_ERROR", new { Command = "zscore", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zrem":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'zrem' command requires at least 2 arguments: zrem <zset> <member1> [member2 ...]", "ARGUMENT_ERROR", new { Command = "zrem", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zcount":
|
||||||
|
if (filteredArgs.Count != 4)
|
||||||
|
return (false, "'zcount' command requires exactly 3 arguments: zcount <zset> <min> <max>", "ARGUMENT_ERROR", new { Command = "zcount", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "zrank":
|
||||||
|
case "zrevrank":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, $"'{cmd}' command requires exactly 2 arguments: {cmd} <zset> <member>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hincrby":
|
||||||
|
if (filteredArgs.Count != 4)
|
||||||
|
return (false, "'hincrby' command requires exactly 3 arguments: hincrby <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
|
||||||
|
if (!int.TryParse(filteredArgs[3], out _))
|
||||||
|
return (false, "'hincrby' increment must be an integer", "ARGUMENT_ERROR", new { Command = "hincrby", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hincrbyfloat":
|
||||||
|
if (filteredArgs.Count != 4)
|
||||||
|
return (false, "'hincrbyfloat' command requires exactly 3 arguments: hincrbyfloat <hash> <field> <increment>", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
|
||||||
|
if (!double.TryParse(filteredArgs[3], out _))
|
||||||
|
return (false, "'hincrbyfloat' increment must be a number", "ARGUMENT_ERROR", new { Command = "hincrbyfloat", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hdel":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'hdel' command requires at least 2 arguments: hdel <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hdel", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hmget":
|
||||||
|
if (filteredArgs.Count < 3)
|
||||||
|
return (false, "'hmget' command requires at least 2 arguments: hmget <hash> <field1> [field2 ...]", "ARGUMENT_ERROR", new { Command = "hmget", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hmset":
|
||||||
|
if (filteredArgs.Count < 4 || (filteredArgs.Count - 2) % 2 != 0)
|
||||||
|
return (false, "'hmset' command requires at least 2 field-value pairs: hmset <hash> <field1> <value1> [field2 value2 ...]", "ARGUMENT_ERROR", new { Command = "hmset", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "hkeys":
|
||||||
|
case "hvals":
|
||||||
|
case "hlen":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, $"'{cmd}' command requires exactly 1 argument: {cmd} <hash>", "ARGUMENT_ERROR", new { Command = cmd, Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "move":
|
||||||
|
if (filteredArgs.Count != 3)
|
||||||
|
return (false, "'move' command requires exactly 2 arguments: move <key> <dbindex>", "ARGUMENT_ERROR", new { Command = "move", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "randomkey":
|
||||||
|
if (filteredArgs.Count != 1)
|
||||||
|
return (false, "'randomkey' command takes no arguments", "ARGUMENT_ERROR", new { Command = "randomkey", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
case "echo":
|
||||||
|
if (filteredArgs.Count != 2)
|
||||||
|
return (false, "'echo' command requires exactly 1 argument: echo <message>", "ARGUMENT_ERROR", new { Command = "echo", Args = filteredArgs });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (true, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Output)> ExecuteRedisCommandAsync(string[] args)
|
||||||
|
{
|
||||||
|
// (reload-config now handled in ExecuteCommandAsync)
|
||||||
|
|
||||||
|
// Validate input arguments
|
||||||
|
var validation = await ValidateArguments(args);
|
||||||
|
if (!validation.Success)
|
||||||
|
{
|
||||||
|
return (false, validation.Output);
|
||||||
|
}
|
||||||
|
// Extract instance name if present in args (-i or --instance)
|
||||||
|
string instanceName = null;
|
||||||
|
for (int i = 0; i < args.Length - 1; i++)
|
||||||
|
{
|
||||||
|
if ((args[i] == "-i" || args[i] == "--instance") && !string.IsNullOrWhiteSpace(args[i + 1]))
|
||||||
|
{
|
||||||
|
instanceName = args[i + 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(instanceName))
|
||||||
|
{
|
||||||
|
var instanceConfig = _config.Instances.Find(x => x.Name == instanceName);
|
||||||
|
if (instanceConfig == null)
|
||||||
|
return (false, $"Instance '{instanceName}' not found in config.");
|
||||||
|
|
||||||
|
// Check/start Valkey server if needed
|
||||||
|
var startResult = await StartValkeyServerIfNeeded(instanceConfig);
|
||||||
|
if (!startResult.Success)
|
||||||
|
return (false, startResult.Output);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Capture console output
|
||||||
|
var originalOut = Console.Out;
|
||||||
|
var stringWriter = new StringWriter();
|
||||||
|
Console.SetOut(stringWriter);
|
||||||
|
|
||||||
|
// Always use the full in-memory config for all command handlers
|
||||||
|
Config config = _config;
|
||||||
|
|
||||||
|
bool commandRan = false;
|
||||||
|
bool parseError = false;
|
||||||
|
var parserResult = Parser.Default.ParseArguments(args, GetCommandTypes());
|
||||||
|
parserResult
|
||||||
|
.WithParsed<object>(o =>
|
||||||
|
{
|
||||||
|
commandRan = true;
|
||||||
|
switch (o)
|
||||||
|
{
|
||||||
|
case StatusOptions statusOpts: StatusCommand.RunStatus(statusOpts, config); break;
|
||||||
|
case ListInstancesOptions listInstancesOpts: InstanceCommands.RunListInstances(config); break;
|
||||||
|
case AddInstanceOptions addInstanceOpts: InstanceCommands.RunAddInstance(addInstanceOpts, config); break;
|
||||||
|
case UpdateInstanceOptions updateInstanceOpts: InstanceCommands.RunUpdateInstance(updateInstanceOpts, config); break;
|
||||||
|
case DeleteInstanceOptions deleteInstanceOpts: InstanceCommands.RunDeleteInstance(deleteInstanceOpts, config); break;
|
||||||
|
case GetOptions getOpts: StringCommands.RunGet(getOpts, config); break;
|
||||||
|
case SetOptions setOpts: StringCommands.RunSet(setOpts, config); break;
|
||||||
|
case DelOptions delOpts: StringCommands.RunDel(delOpts, config); break;
|
||||||
|
case HGetOptions hgetOpts: HashCommands.RunHGet(hgetOpts, config); break;
|
||||||
|
case HSetOptions hsetOpts: HashCommands.RunHSet(hsetOpts, config); break;
|
||||||
|
case HDelOptions hdelOpts: HashCommands.RunHDel(hdelOpts, config); break;
|
||||||
|
case HGetAllOptions hgetAllOpts: HashCommands.RunHGetAll(hgetAllOpts, config); break;
|
||||||
|
case HKeysOptions hkeysOpts: HashCommands.RunHKeys(hkeysOpts, config); break;
|
||||||
|
case HValsOptions hvalsOpts: HashCommands.RunHVals(hvalsOpts, config); break;
|
||||||
|
case HLenOptions hlenOpts: HashCommands.RunHLen(hlenOpts, config); break;
|
||||||
|
case HExistsOptions hexistsOpts: HashCommands.RunHExists(hexistsOpts, config); break;
|
||||||
|
case HIncrByOptions hincrByOpts: HashCommands.RunHIncrBy(hincrByOpts, config); break;
|
||||||
|
case HIncrByFloatOptions hincrByFloatOpts: HashCommands.RunHIncrByFloat(hincrByFloatOpts, config); break;
|
||||||
|
case HMSetOptions hmsetOpts: HashCommands.RunHMSet(hmsetOpts, config); break;
|
||||||
|
case HMGetOptions hmgetOpts: HashCommands.RunHMGet(hmgetOpts, config); break;
|
||||||
|
case HSetNxOptions hsetnxOpts: HashCommands.RunHSetNx(hsetnxOpts, config); break;
|
||||||
|
case HStrLenOptions hstrlenOpts: HashCommands.RunHStrLen(hstrlenOpts, config); break;
|
||||||
|
case HScanOptions hscanOpts: HashCommands.RunHScan(hscanOpts, config); break;
|
||||||
|
case PFAddOptions pfaddOpts: HyperLogLogCommands.RunPFAdd(pfaddOpts, config); break;
|
||||||
|
case PFCountOptions pfcountOpts: HyperLogLogCommands.RunPFCount(pfcountOpts, config); break;
|
||||||
|
case PFMergeOptions pfmergeOpts: HyperLogLogCommands.RunPFMerge(pfmergeOpts, config); break;
|
||||||
|
case GeoAddOptions geoaddOpts: GeoCommands.RunGeoAdd(geoaddOpts, config); break;
|
||||||
|
case GeoDistOptions geodistOpts: GeoCommands.RunGeoDist(geodistOpts, config); break;
|
||||||
|
case GeoHashOptions geohashOpts: GeoCommands.RunGeoHash(geohashOpts, config); break;
|
||||||
|
case GeoPosOptions geoposOpts: GeoCommands.RunGeoPos(geoposOpts, config); break;
|
||||||
|
case GeoRadiusOptions georadiusOpts: GeoCommands.RunGeoRadius(georadiusOpts, config); break;
|
||||||
|
case BitCountOptions bitcountOpts: BitCommands.RunBitCount(bitcountOpts, config); break;
|
||||||
|
case BitFieldOptions bitfieldOpts: BitCommands.RunBitField(bitfieldOpts, config); break;
|
||||||
|
case BitOpOptions bitopOpts: BitCommands.RunBitOp(bitopOpts, config); break;
|
||||||
|
case BitPosOptions bitposOpts: BitCommands.RunBitPos(bitposOpts, config); break;
|
||||||
|
case GetBitOptions getbitOpts: BitCommands.RunGetBit(getbitOpts, config); break;
|
||||||
|
case SetBitOptions setbitOpts: BitCommands.RunSetBit(setbitOpts, config); break;
|
||||||
|
case ModuleListOptions moduleListOpts: ModuleCommands.RunModuleList(moduleListOpts, config); break;
|
||||||
|
case ModuleLoadOptions moduleLoadOpts: ModuleCommands.RunModuleLoad(moduleLoadOpts, config); break;
|
||||||
|
case ModuleUnloadOptions moduleUnloadOpts: ModuleCommands.RunModuleUnload(moduleUnloadOpts, config); break;
|
||||||
|
case LPushOptions lpushOpts: ListCommands.RunLPush(lpushOpts, config); break;
|
||||||
|
case RPushOptions rpushOpts: ListCommands.RunRPush(rpushOpts, config); break;
|
||||||
|
case LLenOptions llenOpts: ListCommands.RunLLen(llenOpts, config); break;
|
||||||
|
case LRangeOptions lrangeOpts: ListCommands.RunLRange(lrangeOpts, config); break;
|
||||||
|
case LIndexOptions lindexOpts: ListCommands.RunLIndex(lindexOpts, config); break;
|
||||||
|
case LSetOptions lsetOpts: ListCommands.RunLSet(lsetOpts, config); break;
|
||||||
|
case LInsertOptions linsertOpts: ListCommands.RunLInsert(linsertOpts, config); break;
|
||||||
|
case LRemOptions lremOpts: ListCommands.RunLRem(lremOpts, config); break;
|
||||||
|
case LTrimOptions ltrimOpts: ListCommands.RunLTrim(ltrimOpts, config); break;
|
||||||
|
case LPopOptions lpopOpts: ListCommands.RunLPop(lpopOpts, config); break;
|
||||||
|
case RPopOptions rpopOpts: ListCommands.RunRPop(rpopOpts, config); break;
|
||||||
|
case BLPopOptions blpopOpts: ListCommands.RunBLPop(blpopOpts, config); break;
|
||||||
|
case BRPopOptions brpopOpts: ListCommands.RunBRPop(brpopOpts, config); break;
|
||||||
|
case RPopLPushOptions rpoplpushOpts: ListCommands.RunRPopLPush(rpoplpushOpts, config); break;
|
||||||
|
case SAddOptions saddOpts: SetCommands.RunSAdd(saddOpts, config); break;
|
||||||
|
case SMembersOptions smembersOpts: SetCommands.RunSMembers(smembersOpts, config); break;
|
||||||
|
case SIsMemberOptions sismemberOpts: SetCommands.RunSIsMember(sismemberOpts, config); break;
|
||||||
|
case SCardOptions scardOpts: SetCommands.RunSCard(scardOpts, config); break;
|
||||||
|
case SPopOptions spopOpts: SetCommands.RunSPop(spopOpts, config); break;
|
||||||
|
case SRandMemberOptions srandmemberOpts: SetCommands.RunSRandMember(srandmemberOpts, config); break;
|
||||||
|
case SRemOptions sremOpts: SetCommands.RunSRem(sremOpts, config); break;
|
||||||
|
case SInterOptions sinterOpts: SetCommands.RunSInter(sinterOpts, config); break;
|
||||||
|
case SUnionOptions sunionOpts: SetCommands.RunSUnion(sunionOpts, config); break;
|
||||||
|
case SDiffOptions sdiffOpts: SetCommands.RunSDiff(sdiffOpts, config); break;
|
||||||
|
case SInterStoreOptions sinterstoreOpts: SetCommands.RunSInterStore(sinterstoreOpts, config); break;
|
||||||
|
case SUnionStoreOptions sunionstoreOpts: SetCommands.RunSUnionStore(sunionstoreOpts, config); break;
|
||||||
|
case SDiffStoreOptions sdiffstoreOpts: SetCommands.RunSDiffStore(sdiffstoreOpts, config); break;
|
||||||
|
case SScanOptions sscanOpts: SetCommands.RunSScan(sscanOpts, config); break;
|
||||||
|
case SMoveOptions smoveOpts: SetCommands.RunSMove(smoveOpts, config); break;
|
||||||
|
case ZAddOptions zaddOpts: SortedSetCommands.RunZAdd(zaddOpts, config); break;
|
||||||
|
case ZRemOptions zremOpts: SortedSetCommands.RunZRem(zremOpts, config); break;
|
||||||
|
case ZRangeOptions zrangeOpts: SortedSetCommands.RunZRange(zrangeOpts, config); break;
|
||||||
|
case ZRevRangeOptions zrevrangeOpts: SortedSetCommands.RunZRevRange(zrevrangeOpts, config); break;
|
||||||
|
case ZRangeByScoreOptions zrangebyscoreOpts: SortedSetCommands.RunZRangeByScore(zrangebyscoreOpts, config); break;
|
||||||
|
case ZCardOptions zcardOpts: SortedSetCommands.RunZCard(zcardOpts, config); break;
|
||||||
|
case ZScoreOptions zscoreOpts: SortedSetCommands.RunZScore(zscoreOpts, config); break;
|
||||||
|
case ZRankOptions zrankOpts: SortedSetCommands.RunZRank(zrankOpts, config); break;
|
||||||
|
case ZRevRankOptions zrevrankOpts: SortedSetCommands.RunZRevRank(zrevrankOpts, config); break;
|
||||||
|
case ZIncrByOptions zincrbyOpts: SortedSetCommands.RunZIncrBy(zincrbyOpts, config); break;
|
||||||
|
case ZRevRangeByScoreOptions zrevrangebyscoreOpts: SortedSetCommands.RunZRevRangeByScore(zrevrangebyscoreOpts, config); break;
|
||||||
|
case ZCountOptions zcountOpts: SortedSetCommands.RunZCount(zcountOpts, config); break;
|
||||||
|
case ZUnionStoreOptions zunionstoreOpts: SortedSetCommands.RunZUnionStore(zunionstoreOpts, config); break;
|
||||||
|
case ZInterStoreOptions zinterstoreOpts: SortedSetCommands.RunZInterStore(zinterstoreOpts, config); break;
|
||||||
|
case ZScanOptions zscanOpts: SortedSetCommands.RunZScan(zscanOpts, config); break;
|
||||||
|
case ZPopMaxOptions zpopmaxOpts: SortedSetCommands.RunZPopMax(zpopmaxOpts, config); break;
|
||||||
|
case ZPopMinOptions zpopminOpts: SortedSetCommands.RunZPopMin(zpopminOpts, config); break;
|
||||||
|
case ZRemRangeByRankOptions zremrangebyrankOpts: SortedSetCommands.RunZRemRangeByRank(zremrangebyrankOpts, config); break;
|
||||||
|
case ZRemRangeByScoreOptions zremrangebyscoreOpts: SortedSetCommands.RunZRemRangeByScore(zremrangebyscoreOpts, config); break;
|
||||||
|
case FlushDbOptions flushdbOpts: DatabaseCommands.RunFlushDb(flushdbOpts, config); break;
|
||||||
|
case DbSizeOptions dbsizeOpts: DatabaseCommands.RunDbSize(dbsizeOpts, config); break;
|
||||||
|
case SelectOptions selectOpts: DatabaseCommands.RunSelect(selectOpts, config); break;
|
||||||
|
case FlushAllOptions flushallOpts: DatabaseCommands.RunFlushAll(flushallOpts, config); break;
|
||||||
|
case ScanOptions scanOpts: KeyCommands.RunScan(scanOpts, config); break;
|
||||||
|
case KeysOptions keysOpts: KeyCommands.RunKeys(keysOpts, config); break;
|
||||||
|
case ExistsOptions existsOpts: KeyCommands.RunExists(existsOpts, config); break;
|
||||||
|
case TypeOptions typeOpts: KeyCommands.RunType(typeOpts, config); break;
|
||||||
|
case TtlOptions ttlOpts: KeyCommands.RunTtl(ttlOpts, config); break;
|
||||||
|
case ExpireOptions expireOpts: KeyCommands.RunExpire(expireOpts, config); break;
|
||||||
|
case PersistOptions persistOpts: KeyCommands.RunPersist(persistOpts, config); break;
|
||||||
|
case RenameOptions renameOpts: KeyCommands.RunRename(renameOpts, config); break;
|
||||||
|
case ConfigOptions configOpts: ServerCommands.RunConfig(configOpts, config); break;
|
||||||
|
case AuthOptions authOpts: ConnectionCommands.RunAuth(authOpts, config); break;
|
||||||
|
case QuitOptions quitOpts: ConnectionCommands.RunQuit(quitOpts, config); break;
|
||||||
|
case ClientListOptions clientListOpts: ConnectionCommands.RunClientList(clientListOpts, config); break;
|
||||||
|
case ClientKillOptions clientKillOpts: ConnectionCommands.RunClientKill(clientKillOpts, config); break;
|
||||||
|
case AppendOptions appendOpts: AdvancedStringCommands.RunAppend(appendOpts, config); break;
|
||||||
|
case IncrOptions incrOpts: AdvancedStringCommands.RunIncr(incrOpts, config); break;
|
||||||
|
case DecrOptions decrOpts: AdvancedStringCommands.RunDecr(decrOpts, config); break;
|
||||||
|
case IncrByOptions incrbyOpts: AdvancedStringCommands.RunIncrBy(incrbyOpts, config); break;
|
||||||
|
case DecrByOptions decrbyOpts: AdvancedStringCommands.RunDecrBy(decrbyOpts, config); break;
|
||||||
|
case IncrByFloatOptions incrbyfloatOpts: AdvancedStringCommands.RunIncrByFloat(incrbyfloatOpts, config); break;
|
||||||
|
case GetRangeOptions getrangeOpts: AdvancedStringCommands.RunGetRange(getrangeOpts, config); break;
|
||||||
|
case SetRangeOptions setrangeOpts: AdvancedStringCommands.RunSetRange(setrangeOpts, config); break;
|
||||||
|
case StrLenOptions strlenOpts: AdvancedStringCommands.RunStrLen(strlenOpts, config); break;
|
||||||
|
case MGetOptions mgetOpts: AdvancedStringCommands.RunMGet(mgetOpts, config); break;
|
||||||
|
case MSetOptions msetOpts: AdvancedStringCommands.RunMSet(msetOpts, config); break;
|
||||||
|
case XAddOptions xaddOpts: StreamCommands.RunXAdd(xaddOpts, config); break;
|
||||||
|
case XRangeOptions xrangeOpts: StreamCommands.RunXRange(xrangeOpts, config); break;
|
||||||
|
case XLenOptions xlenOpts: StreamCommands.RunXLen(xlenOpts, config); break;
|
||||||
|
case XDelOptions xdelOpts: StreamCommands.RunXDel(xdelOpts, config); break;
|
||||||
|
case MultiOptions multiOpts: TransactionCommands.RunMulti(multiOpts, config); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parserResult
|
||||||
|
.WithNotParsed(errors =>
|
||||||
|
{
|
||||||
|
parseError = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.SetOut(originalOut);
|
||||||
|
var output = stringWriter.ToString();
|
||||||
|
if (!commandRan || parseError)
|
||||||
|
{
|
||||||
|
string attemptedCommand = args.Length > 0 ? args[0] : "<none>";
|
||||||
|
return (false, $"Unknown or invalid command: '{attemptedCommand}'. Arguments: [{string.Join(", ", args)}]");
|
||||||
|
}
|
||||||
|
return (true, output);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, $"[Daemon] Exception: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array of all command option types supported by the service.
|
||||||
|
/// This array is used by the command line parser to determine which commands are available.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Array of Type objects representing all supported command options</returns>
|
||||||
|
private Type[] GetCommandTypes()
|
||||||
|
{
|
||||||
|
return new Type[] {
|
||||||
|
typeof(StatusOptions),
|
||||||
|
typeof(ListInstancesOptions), typeof(AddInstanceOptions), typeof(UpdateInstanceOptions), typeof(DeleteInstanceOptions),
|
||||||
|
typeof(GetOptions), typeof(SetOptions), typeof(DelOptions),
|
||||||
|
typeof(HGetOptions), typeof(HSetOptions), typeof(HDelOptions), typeof(HGetAllOptions), typeof(HKeysOptions), typeof(HValsOptions), typeof(HLenOptions), typeof(HExistsOptions), typeof(HIncrByOptions), typeof(HIncrByFloatOptions), typeof(HMSetOptions), typeof(HMGetOptions), typeof(HSetNxOptions), typeof(HStrLenOptions), typeof(HScanOptions),
|
||||||
|
typeof(PFAddOptions), typeof(PFCountOptions), typeof(PFMergeOptions),
|
||||||
|
typeof(GeoAddOptions), typeof(GeoDistOptions), typeof(GeoHashOptions), typeof(GeoPosOptions), typeof(GeoRadiusOptions),
|
||||||
|
typeof(BitCountOptions), typeof(BitFieldOptions), typeof(BitOpOptions), typeof(BitPosOptions), typeof(GetBitOptions), typeof(SetBitOptions),
|
||||||
|
typeof(ModuleListOptions), typeof(ModuleLoadOptions), typeof(ModuleUnloadOptions),
|
||||||
|
typeof(LPushOptions), typeof(RPushOptions), typeof(LLenOptions), typeof(LRangeOptions), typeof(LIndexOptions), typeof(LSetOptions), typeof(LInsertOptions), typeof(LRemOptions), typeof(LTrimOptions), typeof(LPopOptions), typeof(RPopOptions), typeof(BLPopOptions), typeof(BRPopOptions), typeof(RPopLPushOptions),
|
||||||
|
typeof(SAddOptions), typeof(SMembersOptions), typeof(SIsMemberOptions), typeof(SCardOptions), typeof(SPopOptions), typeof(SRandMemberOptions), typeof(SRemOptions), typeof(SInterOptions), typeof(SUnionOptions), typeof(SDiffOptions), typeof(SInterStoreOptions), typeof(SUnionStoreOptions), typeof(SDiffStoreOptions), typeof(SScanOptions), typeof(SMoveOptions),
|
||||||
|
typeof(ZAddOptions), typeof(ZRemOptions), typeof(ZRangeOptions), typeof(ZRevRangeOptions), typeof(ZRangeByScoreOptions), typeof(ZCardOptions), typeof(ZScoreOptions), typeof(ZRankOptions), typeof(ZRevRankOptions), typeof(ZIncrByOptions), typeof(ZRevRangeByScoreOptions), typeof(ZCountOptions), typeof(ZUnionStoreOptions), typeof(ZInterStoreOptions), typeof(ZScanOptions), typeof(ZPopMaxOptions), typeof(ZPopMinOptions), typeof(ZRemRangeByRankOptions), typeof(ZRemRangeByScoreOptions),
|
||||||
|
typeof(FlushDbOptions), typeof(DbSizeOptions), typeof(SelectOptions), typeof(FlushAllOptions),
|
||||||
|
typeof(ScanOptions), typeof(KeysOptions), typeof(ExistsOptions), typeof(TypeOptions), typeof(TtlOptions), typeof(ExpireOptions), typeof(PersistOptions), typeof(RenameOptions),
|
||||||
|
typeof(ConfigOptions),
|
||||||
|
typeof(AuthOptions), typeof(QuitOptions), typeof(ClientListOptions), typeof(ClientKillOptions),
|
||||||
|
typeof(AppendOptions), typeof(IncrOptions), typeof(DecrOptions), typeof(IncrByOptions), typeof(DecrByOptions), typeof(IncrByFloatOptions), typeof(GetRangeOptions), typeof(SetRangeOptions), typeof(StrLenOptions), typeof(MGetOptions), typeof(MSetOptions),
|
||||||
|
typeof(XAddOptions), typeof(XRangeOptions), typeof(XLenOptions), typeof(XDelOptions),
|
||||||
|
typeof(MultiOptions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a command request sent from a client to the RedisManager service.
|
||||||
|
/// Contains the command name and arguments to be executed.
|
||||||
|
/// </summary>
|
||||||
|
public class ServiceRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of the command to execute.
|
||||||
|
/// </summary>
|
||||||
|
public string Command { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the array of arguments for the command.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Arguments { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a response from the RedisManager service to a client request.
|
||||||
|
/// Contains the success status, result data, and any error information.
|
||||||
|
/// </summary>
|
||||||
|
public class ServiceResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the command execution was successful.
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the result data from the command execution.
|
||||||
|
/// Contains the output when Success is true.
|
||||||
|
/// </summary>
|
||||||
|
public string Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the error message if the command execution failed.
|
||||||
|
/// Contains the error details when Success is false.
|
||||||
|
/// </summary>
|
||||||
|
public string Error { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the error code (e.g., INVALID_REQUEST, REDIS_ERROR, INTERNAL_ERROR).
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets additional error details (optional, can be any object).
|
||||||
|
/// </summary>
|
||||||
|
public object ErrorDetails { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
ServiceTest/start_daemon_with_config.sh
Normal file
14
ServiceTest/start_daemon_with_config.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Usage: ./start_daemon_with_config.sh /path/to/redismanager.json
|
||||||
|
|
||||||
|
CONFIG_PATH="$1"
|
||||||
|
if [ -z "$CONFIG_PATH" ]; then
|
||||||
|
echo "Usage: $0 /path/to/redismanager.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the daemon with the specified config file
|
||||||
|
# Assumes the daemon reads config via an environment variable
|
||||||
|
export REDISMANAGER_CONFIG="$CONFIG_PATH"
|
||||||
|
dotnet run --project ../RedisManager
|
||||||
18
ServiceTest/string_command_test.sh
Executable file
18
ServiceTest/string_command_test.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Test SET command
|
||||||
|
set_output=$(dotnet run --project ../RedisManager.csproj set -i default testkey testvalue)
|
||||||
|
if [[ "$set_output" != *"OK"* ]]; then
|
||||||
|
echo "SET command failed: $set_output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test GET command
|
||||||
|
get_output=$(dotnet run --project ../RedisManager.csproj get -i default testkey)
|
||||||
|
if [[ "$get_output" != *"testvalue"* ]]; then
|
||||||
|
echo "GET command failed: $get_output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "String command test passed"
|
||||||
60
Utils/Output.cs
Normal file
60
Utils/Output.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace RedisManager.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utility class for formatting console output with ANSI color codes.
|
||||||
|
/// Provides methods to wrap text with different colors for enhanced readability.
|
||||||
|
/// </summary>
|
||||||
|
public static class Output
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI green color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with green ANSI color codes</returns>
|
||||||
|
public static string Green(string text) => $"\u001b[32m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI red color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with red ANSI color codes</returns>
|
||||||
|
public static string Red(string text) => $"\u001b[31m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI yellow color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with yellow ANSI color codes</returns>
|
||||||
|
public static string Yellow(string text) => $"\u001b[33m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI blue color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with blue ANSI color codes</returns>
|
||||||
|
public static string Blue(string text) => $"\u001b[34m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI cyan color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with cyan ANSI color codes</returns>
|
||||||
|
public static string Cyan(string text) => $"\u001b[36m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI magenta color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with magenta ANSI color codes</returns>
|
||||||
|
public static string Magenta(string text) => $"\u001b[35m{text}\u001b[0m";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps the specified text with ANSI white color codes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to colorize</param>
|
||||||
|
/// <returns>The text wrapped with white ANSI color codes</returns>
|
||||||
|
public static string White(string text) => $"\u001b[37m{text}\u001b[0m";
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Utils/RedisUtils.cs
Normal file
112
Utils/RedisUtils.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
using StackExchange.Redis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace RedisManager.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides utility methods for Redis operations, including instance retrieval, connection, array conversion, and table printing.
|
||||||
|
/// </summary>
|
||||||
|
public static class RedisUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves an InstanceConfig by name from the provided configuration. Exits if not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">The configuration containing Redis instances.</param>
|
||||||
|
/// <param name="name">The name of the instance to retrieve.</param>
|
||||||
|
/// <returns>The matching InstanceConfig.</returns>
|
||||||
|
public static InstanceConfig GetInstance(Config config, string name)
|
||||||
|
{
|
||||||
|
var instance = config.Instances.Find(i => i.Name == name);
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Red($"Instance '{name}' not found."));
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects to a Redis instance using the provided configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance configuration to connect with.</param>
|
||||||
|
/// <returns>A connected ConnectionMultiplexer.</returns>
|
||||||
|
public static ConnectionMultiplexer ConnectRedis(InstanceConfig instance)
|
||||||
|
{
|
||||||
|
var options = new ConfigurationOptions
|
||||||
|
{
|
||||||
|
EndPoints = { $"{instance.Host}:{instance.Port}" },
|
||||||
|
Password = instance.Password
|
||||||
|
};
|
||||||
|
return ConnectionMultiplexer.Connect(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an enumerable of strings to a RedisValue array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The string values to convert.</param>
|
||||||
|
/// <returns>An array of RedisValue.</returns>
|
||||||
|
public static RedisValue[] ArrayFrom(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
var list = new List<RedisValue>();
|
||||||
|
foreach (var v in values)
|
||||||
|
list.Add(v);
|
||||||
|
return list.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an enumerable of strings to a RedisKey array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The string values to convert.</param>
|
||||||
|
/// <returns>An array of RedisKey.</returns>
|
||||||
|
public static RedisKey[] ArrayFromKeys(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
var list = new List<RedisKey>();
|
||||||
|
foreach (var v in values)
|
||||||
|
list.Add(v);
|
||||||
|
return list.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prints a formatted table to the console with headers and rows.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="headers">The column headers.</param>
|
||||||
|
/// <param name="rows">The table rows.</param>
|
||||||
|
public static void PrintTable(string[] headers, List<string[]> rows)
|
||||||
|
{
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine(Output.Yellow("No data"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
var widths = new int[headers.Length];
|
||||||
|
for (int i = 0; i < headers.Length; i++)
|
||||||
|
{
|
||||||
|
widths[i] = headers[i].Length;
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
if (i < row.Length && row[i] != null)
|
||||||
|
widths[i] = Math.Max(widths[i], row[i].Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header
|
||||||
|
Console.WriteLine(Output.Blue(string.Join(" | ", headers.Select((h, i) => h.PadRight(widths[i])))));
|
||||||
|
Console.WriteLine(Output.Blue(new string('-', widths.Sum() + (headers.Length - 1) * 3)));
|
||||||
|
|
||||||
|
// Print rows
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var formattedRow = new string[headers.Length];
|
||||||
|
for (int i = 0; i < headers.Length; i++)
|
||||||
|
{
|
||||||
|
formattedRow[i] = i < row.Length && row[i] != null ? row[i].PadRight(widths[i]) : "".PadRight(widths[i]);
|
||||||
|
}
|
||||||
|
Console.WriteLine(Output.Green(string.Join(" | ", formattedRow)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user