1
+ import json
1
2
import os
2
3
import re
3
4
import sys
@@ -25,7 +26,7 @@ def bitcoin():
25
26
@click .argument ("method" , type = str )
26
27
@click .argument ("params" , type = str , nargs = - 1 ) # this will capture all remaining arguments
27
28
@click .option ("--namespace" , default = None , show_default = True )
28
- def rpc (tank : str , method : str , params : str , namespace : Optional [str ]):
29
+ def rpc (tank : str , method : str , params : list [ str ] , namespace : Optional [str ]):
29
30
"""
30
31
Call bitcoin-cli <method> [params] on <tank pod name>
31
32
"""
@@ -37,12 +38,49 @@ def rpc(tank: str, method: str, params: str, namespace: Optional[str]):
37
38
print (result )
38
39
39
40
40
- def _rpc (tank : str , method : str , params : str , namespace : Optional [str ] = None ):
41
+ def _rpc (tank : str , method : str , params : list [ str ] , namespace : Optional [str ] = None ):
41
42
# bitcoin-cli should be able to read bitcoin.conf inside the container
42
43
# so no extra args like port, chain, username or password are needed
43
44
namespace = get_default_namespace_or (namespace )
44
- if params :
45
- cmd = f"kubectl -n { namespace } exec { tank } --container { BITCOINCORE_CONTAINER } -- bitcoin-cli { method } { ' ' .join (map (str , params ))} "
45
+
46
+ # Reconstruct JSON parameters that may have been split by shell parsing
47
+ # This fixes issues where JSON arrays like ["network"] get split into separate arguments
48
+ reconstructed_params = _reconstruct_json_params (params )
49
+
50
+ if reconstructed_params :
51
+ # Process each parameter to handle different data types correctly for bitcoin-cli
52
+ processed_params = []
53
+ for param in reconstructed_params :
54
+ # Handle boolean and primitive values that should not be quoted
55
+ if param .lower () in ["true" , "false" , "null" ]:
56
+ processed_params .append (param .lower ())
57
+ elif param .isdigit () or (param .startswith ("-" ) and param [1 :].isdigit ()):
58
+ # Numeric values (integers, negative numbers)
59
+ processed_params .append (param )
60
+ else :
61
+ try :
62
+ # Try to parse as JSON to handle complex data structures
63
+ parsed_json = json .loads (param )
64
+ if isinstance (parsed_json , list ):
65
+ # If it's a list, extract the elements and add them individually
66
+ # This ensures bitcoin-cli receives each list element as a separate argument
67
+ for element in parsed_json :
68
+ if isinstance (element , str ):
69
+ processed_params .append (f'"{ element } "' )
70
+ else :
71
+ processed_params .append (str (element ))
72
+ elif isinstance (parsed_json , dict ):
73
+ # If it's a dict, pass it as a single JSON argument
74
+ # bitcoin-cli expects objects to be passed as JSON strings
75
+ processed_params .append (param )
76
+ else :
77
+ # If it's a primitive value (number, boolean), pass it as-is
78
+ processed_params .append (str (parsed_json ))
79
+ except json .JSONDecodeError :
80
+ # Not valid JSON, pass as-is (treat as plain string)
81
+ processed_params .append (param )
82
+
83
+ cmd = f"kubectl -n { namespace } exec { tank } --container { BITCOINCORE_CONTAINER } -- bitcoin-cli { method } { ' ' .join (map (str , processed_params ))} "
46
84
else :
47
85
cmd = f"kubectl -n { namespace } exec { tank } --container { BITCOINCORE_CONTAINER } -- bitcoin-cli { method } "
48
86
return run_command (cmd )
@@ -346,3 +384,96 @@ def to_jsonable(obj: str):
346
384
return obj .hex ()
347
385
else :
348
386
return obj
387
+
388
+
389
+ def _reconstruct_json_params (params : list [str ]) -> list [str ]:
390
+ """
391
+ Reconstruct JSON parameters that may have been split by shell parsing.
392
+
393
+ This function detects when parameters look like they should be JSON and
394
+ reconstructs them properly. For example:
395
+ - ['[network]'] -> ['["network"]']
396
+ - ['[network,', 'message_type]'] -> ['["network", "message_type"]']
397
+ - ['[{"key":', '"value"}]'] -> ['[{"key": "value"}]']
398
+
399
+ This fixes the issue described in GitHub issue #714 where shell parsing
400
+ breaks JSON parameters into separate arguments.
401
+ """
402
+ if not params :
403
+ return params
404
+
405
+ reconstructed = []
406
+ i = 0
407
+
408
+ while i < len (params ):
409
+ param = params [i ]
410
+
411
+ # Check if this looks like the start of a JSON array or object
412
+ # that was split across multiple arguments by shell parsing
413
+ if (param .startswith ("[" ) and not param .endswith ("]" )) or (
414
+ param .startswith ("{" ) and not param .endswith ("}" )
415
+ ):
416
+ # This is the start of a JSON structure, collect all parts
417
+ json_parts = [param ]
418
+ i += 1
419
+
420
+ # Collect all parts until we find the closing bracket/brace
421
+ while i < len (params ):
422
+ next_param = params [i ]
423
+ json_parts .append (next_param )
424
+
425
+ if (param .startswith ("[" ) and next_param .endswith ("]" )) or (
426
+ param .startswith ("{" ) and next_param .endswith ("}" )
427
+ ):
428
+ break
429
+ i += 1
430
+
431
+ # Reconstruct the JSON string by joining all parts
432
+ json_str = " " .join (json_parts )
433
+
434
+ # Validate that it's valid JSON before adding
435
+ try :
436
+ json .loads (json_str )
437
+ reconstructed .append (json_str )
438
+ except json .JSONDecodeError :
439
+ # If it's not valid JSON, add parts as separate parameters
440
+ # This preserves the original behavior for non-JSON arguments
441
+ reconstructed .extend (json_parts )
442
+
443
+ elif param .startswith ("[" ) and param .endswith ("]" ):
444
+ # Single parameter that looks like JSON array
445
+ # Check if it's missing quotes around string elements
446
+ if "[" in param and "]" in param and '"' not in param :
447
+ # This looks like [value] without quotes, try to add them
448
+ inner_content = param [1 :- 1 ] # Remove brackets
449
+ if "," in inner_content :
450
+ # Multiple values: [val1, val2] -> ["val1", "val2"]
451
+ values = [v .strip () for v in inner_content .split ("," )]
452
+ quoted_values = [f'"{ v } "' for v in values ]
453
+ reconstructed_param = f"[{ ', ' .join (quoted_values )} ]"
454
+ else :
455
+ # Single value: [value] -> ["value"]
456
+ reconstructed_param = f'["{ inner_content .strip ()} "]'
457
+
458
+ # Validate the reconstructed JSON
459
+ try :
460
+ json .loads (reconstructed_param )
461
+ reconstructed .append (reconstructed_param )
462
+ except json .JSONDecodeError :
463
+ # If reconstruction fails, keep original parameter
464
+ reconstructed .append (param )
465
+ else :
466
+ # Already has quotes or is not a string array
467
+ try :
468
+ json .loads (param )
469
+ reconstructed .append (param )
470
+ except json .JSONDecodeError :
471
+ reconstructed .append (param )
472
+
473
+ else :
474
+ # Regular parameter, add as-is
475
+ reconstructed .append (param )
476
+
477
+ i += 1
478
+
479
+ return reconstructed
0 commit comments