88import sys
99import warnings
1010from pathlib import Path
11- from typing import Any , Dict
11+ from typing import Any , Dict , Sequence
1212
1313try :
1414 import yaml
1717
1818from bib_lookup ._const import CONFIG_FILE as _CONFIG_FILE
1919from bib_lookup ._const import DEFAULT_CONFIG as _DEFAULT_CONFIG
20+ from bib_lookup ._const import STYLE_PARAMETERS as _STYLE_PARAMETERS
2021from bib_lookup .bib_lookup import BibLookup
2122from bib_lookup .utils import str2bool
2223from bib_lookup .version import __version__ as bl_version
2324
2425
26+ def _valid_config_keys () -> set :
27+ """Return the set of all recognised configuration keys."""
28+ keys = set (_DEFAULT_CONFIG .keys ())
29+ for style_params in _STYLE_PARAMETERS .values ():
30+ keys .update (style_params .keys ())
31+ return keys
32+
33+
34+ def _parse_config_value (value : str ) -> Any :
35+ """Parse a string config value into the appropriate Python type.
36+
37+ - ``none``/``null`` → ``None``
38+ - booleans (``true``/``false``/``yes``/``no``/…) → ``bool``
39+ - ``[a, b, c]`` → ``list`` (quotes stripped, empty list ``[]`` handled)
40+ - otherwise → ``str``
41+ """
42+ if value .lower () in ("none" , "null" ):
43+ return None
44+ try :
45+ return str2bool (value )
46+ except ValueError :
47+ pass
48+ if value .startswith ("[" ) and value .endswith ("]" ):
49+ inner = value .strip ("[]" ).strip ()
50+ if not inner :
51+ return []
52+ return [v .strip ().strip ("'\" " ) for v in inner .split ("," )]
53+ return value
54+
55+
56+ def _read_user_config () -> dict :
57+ """Read the user config file, returning an empty dict if it does not exist."""
58+ if _CONFIG_FILE .is_file ():
59+ return json .loads (_CONFIG_FILE .read_text ())
60+ return {}
61+
62+
63+ def _write_user_config (config : dict ) -> None :
64+ """Write the config dict to the user config file."""
65+ _CONFIG_FILE .write_text (json .dumps (config , indent = 4 ))
66+
67+
2568def _handle_config (config_arg : str ) -> None :
2669 """Handle configuration commands."""
2770 if config_arg == "show" :
@@ -50,10 +93,11 @@ def _handle_config(config_arg: str) -> None:
5093 return
5194 else :
5295 if "=" in config_arg :
53- config = dict ([ kv .strip ().split ("=" ) for kv in config_arg .split (";" )])
96+ config = { k . strip (): _parse_config_value ( v .strip ()) for k , v in ( kv .split ("=" , 1 ) for kv in config_arg .split (";" ))}
5497 else :
5598 config_path = Path (config_arg )
56- assert config_path .is_file (), f"Configuration file ``{ config_arg } `` does not exist. Please check and try again."
99+ if not config_path .is_file ():
100+ raise ValueError (f"Configuration file ``{ config_arg } `` does not exist. Please check and try again." )
57101
58102 if config_path .suffix == ".json" :
59103 config = json .loads (config_path .read_text ())
@@ -68,24 +112,50 @@ def _handle_config(config_arg: str) -> None:
68112 f"Unknown configuration file type ``{ config_path .suffix } ``. " "Only json and yaml files are supported."
69113 )
70114
71- # discard unknown keys
72- unknown_keys = set (config .keys ()) - set (_DEFAULT_CONFIG .keys ())
73- config = {k : v for k , v in config .items () if k in _DEFAULT_CONFIG }
74- # parse lists in the config
75- for k , v in config .items ():
76- if isinstance (v , str ) and v .startswith ("[" ) and v .endswith ("]" ):
77- config [k ] = [vv .strip () for vv in v .strip ("[]" ).split ("," )]
115+ # Warn about unknown keys (but keep them — they may be style-specific)
116+ unknown_keys = set (config .keys ()) - _valid_config_keys ()
78117 if len (unknown_keys ) > 0 :
79118 verb = "are" if len (unknown_keys ) > 1 else "is"
80119 warnings .warn (
81- f"Unknown configuration keys ``{ unknown_keys } `` { verb } discarded ." ,
120+ f"Unknown configuration keys ``{ unknown_keys } `` { verb } kept but might have no effect ." ,
82121 RuntimeWarning ,
83122 )
123+
124+ # Merge with existing user config and write back
125+ existing = _read_user_config ()
126+ existing .update (config )
127+ _write_user_config (existing )
84128 print (f"Setting configurations:\n { json .dumps (config , indent = 4 )} " )
85- _CONFIG_FILE .write_text (json .dumps (config , indent = 4 ))
86129 return
87130
88131
132+ def _handle_set (args_set : Sequence [str ]) -> None :
133+ """Handle the ``set`` subcommand.
134+
135+ Usage: ``bib-lookup set KEY VALUE``
136+ ``VALUE`` of ``none``/``null`` resets the key to ``None`` (built-in logic).
137+ """
138+ if len (args_set ) != 2 :
139+ print ("Error: ``set`` accepts exactly 2 arguments: KEY VALUE." )
140+ sys .exit (1 )
141+
142+ key , value = args_set
143+
144+ if key not in _valid_config_keys ():
145+ verb = "is"
146+ warnings .warn (
147+ f"Unknown configuration key ``{ key } `` { verb } set anyway." ,
148+ RuntimeWarning ,
149+ )
150+
151+ parsed_value = _parse_config_value (value )
152+
153+ existing = _read_user_config ()
154+ existing [key ] = parsed_value
155+ _write_user_config (existing )
156+ print (f"Set ``{ key } `` to ``{ parsed_value } ``" )
157+
158+
89159def _handle_gather (args : Dict [str , Any ]) -> None :
90160 """Handle the gather command."""
91161 from bib_lookup .utils import gather_tex_source_files_in_one
@@ -151,6 +221,13 @@ def _handle_simplify_bib(args: Dict[str, Any]) -> None:
151221
152222def main ():
153223 """Command-line interface for the bib_lookup package."""
224+ # Intercept the ``set`` subcommand before argparse runs,
225+ # since the main parser uses a variable-length positional arg
226+ # that would otherwise swallow ``set`` as an identifier.
227+ if len (sys .argv ) > 1 and sys .argv [1 ] == "set" :
228+ _handle_set (sys .argv [2 :])
229+ return
230+
154231 parser = argparse .ArgumentParser (description = "Look up a BibTeX entry from a DOI identifier, PMID (URL) or arXiv ID (URL)." )
155232 parser .add_argument (
156233 "identifiers" ,
0 commit comments