diff --git a/src/west/app/config.py b/src/west/app/config.py index a3729b13..19dcc507 100644 --- a/src/west/app/config.py +++ b/src/west/app/config.py @@ -48,6 +48,14 @@ To set a value for , type: west config +To append to a value for , type: + west config -a +A value must exist in the selected configuration file in order to be able +to append to it. The existing value can be empty. +Examples: + west config -a build.cmake-args -- " -DEXTRA_CFLAGS='-Wextra -g0' -DFOO=BAR" + west config -a manifest.group-filter ,+optional + To list all options and their values: west config -l @@ -64,7 +72,7 @@ CONFIG_EPILOG = '''\ If the configuration file to use is not set, reads use all three in -precedence order, and writes use the local file.''' +precedence order, and writes (including appends) use the local file.''' ALL = ConfigFile.ALL SYSTEM = ConfigFile.SYSTEM @@ -88,12 +96,18 @@ def do_add_parser(self, parser_adder): description=self.description, epilog=CONFIG_EPILOG) - parser.add_argument('-l', '--list', action='store_true', - help='list all options and their values') - parser.add_argument('-d', '--delete', action='store_true', - help='delete an option in one config file') - parser.add_argument('-D', '--delete-all', action='store_true', - help="delete an option everywhere it's set") + group = parser.add_argument_group( + "action to perform (give at most one)" + ).add_mutually_exclusive_group() + + group.add_argument('-l', '--list', action='store_true', + help='list all options and their values') + group.add_argument('-d', '--delete', action='store_true', + help='delete an option in one config file') + group.add_argument('-D', '--delete-all', action='store_true', + help="delete an option everywhere it's set") + group.add_argument('-a', '--append', action='store_true', + help='append to an existing value') group = parser.add_argument_group( "configuration file to use (give at most one)" @@ -121,13 +135,12 @@ def do_run(self, args, user_args): if args.list: if args.name: self.parser.error('-l cannot be combined with name argument') - elif delete: - self.parser.error('-l cannot be combined with -d or -D') elif not args.name: self.parser.error('missing argument name ' '(to list all options and values, use -l)') - elif args.delete and args.delete_all: - self.parser.error('-d cannot be combined with -D') + elif args.append: + if args.value is None: + self.parser.error('-a requires both name and value') if args.list: self.list(args) @@ -135,6 +148,8 @@ def do_run(self, args, user_args): self.delete(args) elif args.value is None: self.read(args) + elif args.append: + self.append(args) else: self.write(args) @@ -179,6 +194,16 @@ def read(self, args): self.dbg(f'{args.name} is unset') raise CommandError(returncode=1) + def append(self, args): + self.check_config(args.name) + where = args.configfile or LOCAL + value = self.config.get(args.name, configfile=where) + if value is None: + self.die(f'option {args.name} not found in the {where.name.lower()} ' + 'configuration file') + args.value = value + args.value + self.write(args) + def write(self, args): self.check_config(args.name) what = args.configfile or LOCAL diff --git a/tests/test_config.py b/tests/test_config.py index e31d7cd4..fb3a569a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -229,6 +229,45 @@ def test_local_creation_with_topdir(): assert 'pytest' not in cfg(f=GLOBAL) assert cfg(f=LOCAL, topdir=str(topdir))['pytest']['key'] == 'val' +def test_append(): + update_testcfg('pytest', 'key', 'system', configfile=SYSTEM) + update_testcfg('pytest', 'key', 'global', configfile=GLOBAL) + update_testcfg('pytest', 'key', 'local', configfile=LOCAL) + # Appending with no configfile specified should modify the local one + cmd('config -a pytest.key ,bar') + + # Only the local one will be modified + assert cfg(f=SYSTEM)['pytest']['key'] == 'system' + assert cfg(f=GLOBAL)['pytest']['key'] == 'global' + assert cfg(f=LOCAL)['pytest']['key'] == 'local,bar' + + # Test a more complex one, and at a particular configfile level + update_testcfg('build', 'cmake-args', '-DCONF_FILE=foo.conf', configfile=GLOBAL) + assert cfg(f=GLOBAL)['build']['cmake-args'] == '-DCONF_FILE=foo.conf' + + # Use a list instead of a string to avoid one level of nested quoting + cmd(['config', '--global', '-a', 'build.cmake-args', '--', + ' -DEXTRA_CFLAGS=\'-Wextra -g0\' -DFOO=BAR']) + + assert cfg(f=GLOBAL)['build']['cmake-args'] == \ + '-DCONF_FILE=foo.conf -DEXTRA_CFLAGS=\'-Wextra -g0\' -DFOO=BAR' + +def test_append_novalue(): + with pytest.raises(subprocess.CalledProcessError) as exc_info: + cmd('config -a pytest.foo', stderr=subprocess.STDOUT) + # Get the output into a variable to simplify pytest error messages + err_msg = exc_info.value.output.decode("utf-8") + assert '-a requires both name and value' in err_msg + +def test_append_notfound(): + update_testcfg('pytest', 'key', 'val', configfile=LOCAL) + with pytest.raises(subprocess.CalledProcessError) as exc_info: + cmd('config -a pytest.foo bar', stderr=subprocess.STDOUT) + # Get the output into a variable to simplify pytest error messages + err_msg = exc_info.value.output.decode("utf-8") + assert 'option pytest.foo not found in the local configuration file' in err_msg + + def test_delete_basic(): # Basic deletion test: write local, verify global and system deletions # don't work, then delete local does work.