function [config, fn] = cosmo_config(fn, config)
% return a struct with configuration settings, or store such settings
%
% Usages:
% - get the configuration (either default, or what is in '.cosmomvpa.cfg'):
% >> config=cosmo_config();
%
% - read configuration from a specified file
% >> config=cosmo_config(fn)
%
% - store configuration in a file
% >> cosmo_config(fn, to_store)
%
% - related function (in CoSMoMVPA's 'examples/' directory):
% >> cosmo_wizard_set_config
%
% Inputs:
% fn optional filename of a configuration file.
% This can either be a path of a file, or (on Unix platforms)
% a filename in the user's home directory.
% If fn is omitted, then it defaults to '.cosmomvpa.cfg'
% and will read the configuration from that file if it
% exists in one of the aforementioned locations; see the
% notes below for details.
% to_store optional struct with configuration to store.
% If omitted then the configuration is not stored.
%
% Returns:
% config Struct with configurations
%
% Notes:
% - the rationale for this function is to keep the example code fixed
% (that is, without any paths hard-coded) and still allow each
% user to store the example data in a directory of their choice.
%
% - the format for a configuration file is of the form <key>=<value>,
% where <key> cannot contain the '=' character and <value> cannot start
% or end with a white-space character.
% - if the .cosmomvpa.cfg file is in the same directory as this file
% (presently, 'mvpa/'), it will be found there as well.
%
% - an example configuration file (of three lines):
%
% # path for runnable examples
% tutorial_data_path=/Users/nick/organized/_datasets/CoSMoMVPA/tutorial_data
% output_data_path=/Users/nick/organized/_datasets/CoSMoMVPA/tutorial_data
%
% - If a configuration file with the name '.cosmomvpa.cfg' is stored
% in a directory that is in the matlab path, the user path, or (on Unix
% platforms) the user's home directory, then calling this function with
% no arguments will read that file and return the configuration stored
% in it.
%
% - The configuration can also be changed as follows:
% >> % get default configuration
% >> % If the following command gives an error, do: config=struct()
% >> config=cosmo_config();
% >>
% >> % update settings
% >> config.tutorial_data_path='/path/to/some/data';
% >> config.output_data_path='/where/i/want/output';
% >>
% >> % get first directory in user path
% >> matlab_path=cosmo_strsplit (userpath,':',1);
% >>
% >> % set configuration filename
% >> config_fn=fullfile(matlab_path,'.cosmomvpa.cfg');
% >> cosmo_config(config_fn, config);
% >> fprintf('Configuration stored in %s\n', config_fn);
% >>
% >> % now check they are the same
% >> loaded_config=cosmo_config();
% >> assert(isequal(loaded_config,config));
%
% - The configuration can be set using cosmo_wizard_set_config in
% CoSMoMVPA's 'examples/' directory
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
default_config_fn = '.cosmomvpa.cfg';
if nargin == 1 && ischar(fn)
fn = find_config_file(fn);
config = read_config(fn);
elseif nargin == 0
% see if the configuration file can be found
fn = find_config_file(default_config_fn);
if isempty(fn)
config = struct();
else
config = read_config(fn);
end
elseif nargin == 2 && ischar(fn) && isstruct(config)
write_config(fn, config);
else
error('Illegal input');
end
expected_fields = {'tutorial_data_path', 'output_data_path'};
missing_fields = setdiff(expected_fields, fieldnames(config));
if numel(missing_fields) > 0
missing_fields_str = cosmo_strjoin(missing_fields, ''', ''');
cosmo_warning(['Using %s, fields ''%s'' are missing. This makes '...
'it more complicated to run CoSMoMVPA''s '...
'exercises and examples.\n'...
'To set the configuration, consider running\n\n'...
' cosmo_wizard_set_config\n\n'...
'in the CoSMoMVPA ''examples/'' directory'], ...
mfilename(), missing_fields_str);
end
validate_config(config);
function validate_config(config)
% simple validation of config
% poor-man version of OO
path_exists = struct();
path_exists.match = @(x)isempty(cosmo_strsplit(x, '_path', -1));
path_exists.test = @(p) exist(p, 'file');
path_exists.msg = @(key, p) sprintf('%s: path "%s" not found. ', key, p);
checks = {path_exists};
add_msg = sprintf('To set the configuration, run: help %s', mfilename());
% perform checks on fieldnames present in 'checks'.
fns = fieldnames(config);
for k = 1:numel(fns)
fn = fns{k};
for j = 1:numel(checks)
check = checks{j};
if check.match(fn)
test_func = check.test;
value = config.(fn);
if ~test_func(value)
msg_func = check.msg;
cosmo_warning('%s\n%s', msg_func(fn, value), add_msg);
end
end
end
end
function path_fn = find_config_file(fn, raise_)
% tries to find a configuration file by looking:
% - for the path of the file
% - in the matlab path
% - in the user's home directory (on Unix)
if nargin < 2
raise_ = false;
end
exist_ = @(fn_) exist(fn_, 'file');
path_fn = [];
% simulate 'go-to' statement using a while loop with break at the end
while true
% does the file exist 'as is'?
if exist_(fn)
path_fn = fn;
break
end
% is it in the mvpa directory?
parent_dir = fileparts(mfilename('fullpath'));
m_fn = fullfile(parent_dir, fn);
if exist_(m_fn)
path_fn = m_fn;
break
end
% is it in the user path?
% (not supported on octave)
if cosmo_wtf('is_matlab')
upaths = cosmo_strsplit(userpath(), pathsep());
for k = 1:numel(upaths)
u_fn = fullfile(upaths{k}, fn);
if exist_(u_fn)
path_fn = u_fn;
break
end
end
if ~isempty(path_fn)
break
end
end
if isunix()
% is it in the home directory?
u_fn = fullfile(getenv('HOME'), fn);
if exist_(u_fn)
path_fn = u_fn;
break
end
end
if raise_
error('Cannot find config file "%s"', fn);
end
break
end
function config = read_config(fn)
% reads configuration from a file fn
config = struct(); % space for output
if isempty(fn)
error('Input file not specified - cannot open');
end
fid = fopen(fn);
if fid == -1
error('Unable to open file %s', fn);
end
line_number = 0;
while true
% read each line
line = fgetl(fid);
if ~ischar(line)
% end of file
break
end
line_number = line_number + 1;
% ignore empty lines or lines starting with '#'
if isempty(line) || line(1) == '#'
continue
end
% look for lines of form '<key>=<value>'.
% white spaces around key or value are ignored.
m = regexp(line, '(?<key>[^=]+)\s*=\s*(?<value>.*)\s*', 'names');
if isempty(m)
cosmo_warning('Skipping non-recognized line "%s"', line);
continue
end
% get key and value
key = m.key;
value = m.value;
verify_no_illegal_char(fn, line_number, [key value]);
% see if it can be converted to numeric
value_num = str2double(value);
if ~isnan(value_num)
value = value_num;
end
config.(key) = value;
end
fclose(fid);
function verify_no_illegal_char(fn, line_number, s)
illegal_chars = sprintf('"''');
matching = bsxfun(@eq, s, illegal_chars(:));
if any(matching(:))
error(['File %s, line %d: found illegal character. '...
'note that quote characters are not allowed '...
'for keys or values'], fn, line_number);
end
function write_config(fn, config)
% writes the config to a file fn
% no support for comments or empty lines
fid = fopen(fn, 'w');
fns = fieldnames(config);
for k = 1:numel(fns)
fn = fns{k};
v = config.(fn);
if isnumeric(v)
% convert numeric to string
v = sprintf('%d ', v);
elseif ischar(v)
% no conversion
else
cosmo_warning('Skipping unsupported data type for key "%s"', ...
fn);
end
fprintf(fid, '%s=%s\n', fn, v);
end
fclose(fid);