Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/ifthen.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2026 Northern.tech AS
# This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
# To the extent this program is licensed as part of the Enterprise
# versions of Cfengine, the applicable Commercial Open Source License
# (COSL) may apply to this file if you as a licensee so wish it. See
# included file COSL.txt.
#+begin_src cfengine3
bundle agent __main__
{
classes:
"webserver" expression => "any";

vars:
# The value of the first true class expression is used.
"port" string => ifthen("dbserver", "5432", "webserver", "80");

# No class expression is true, so the variable is not defined.
"unset" string => ifthen("dbserver", "5432", "mailserver", "25");

reports:
"port is $(port)";
"unset is not defined" if => not(isvariable("unset"));
}

#+end_src
###############################################################################
#+begin_src example_output
#@ ```
#@ R: port is 80
#@ R: unset is not defined
#@ ```
#+end_src
47 changes: 47 additions & 0 deletions libpromises/evalfunction.c
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,44 @@ static FnCallResult FnCallIfElse(EvalContext *ctx,

/*********************************************************************/

static FnCallResult FnCallIfThen(EvalContext *ctx,
ARG_UNUSED const Policy *policy,
const FnCall *fp,
const Rlist *finalargs)
{
assert(fp != NULL);

/* We need to check all the arguments, ArgTemplate does not check varadic functions */
for (const Rlist *arg = finalargs; arg != NULL; arg = arg->next)
{
SyntaxTypeMatch err = CheckConstraintTypeMatch(fp->name, arg->val, CF_DATA_TYPE_STRING, "", 1);
if (err != SYNTAX_TYPE_MATCH_OK && err != SYNTAX_TYPE_MATCH_ERROR_UNEXPANDED)
{
FatalError(ctx, "Function '%s', %s", fp->name, SyntaxTypeMatchToString(err));
}
}

/* Unlike ifelse(), there is no fall-through value. */
const size_t argcount = RlistLen(finalargs);
if ((argcount == 0) || ((argcount % 2) != 0))
{
FatalError(ctx, "Function '%s' requires pairs of class expressions and values", fp->name);
}

for (const Rlist *arg = finalargs; arg != NULL; arg = arg->next->next)
{
if (IsDefinedClass(ctx, RlistScalarValue(arg)))
{
return FnReturn(RlistScalarValue(arg->next));
}
}

/* None is true, leave the variable undefined. */
return FnFailure();
}

/*********************************************************************/

static FnCallResult FnCallClassesMatching(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs)
{
assert(finalargs != NULL);
Expand Down Expand Up @@ -11071,6 +11109,13 @@ static const FnCallArg IFELSE_ARGS[] =
{NULL, CF_DATA_TYPE_NONE, NULL}
};

static const FnCallArg IFTHEN_ARGS[] =
{
{CF_ANYSTRING, CF_DATA_TYPE_STRING, "Class expression"},
{CF_ANYSTRING, CF_DATA_TYPE_STRING, "Value if the preceding class expression is true"},
{NULL, CF_DATA_TYPE_NONE, NULL}
};

static const FnCallArg INT_ARGS[] =
{
{CF_ANYSTRING, CF_DATA_TYPE_STRING, "Numeric string to convert to integer"},
Expand Down Expand Up @@ -11981,6 +12026,8 @@ const FnCallType CF_FNCALL_TYPES[] =
FNCALL_OPTION_CACHED, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC),
FnCallTypeNew("ifelse", CF_DATA_TYPE_STRING, IFELSE_ARGS, &FnCallIfElse, "Do If-ElseIf-ElseIf-...-Else evaluation of arguments",
FNCALL_OPTION_VARARG, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, ARGC(1, -1)),
FnCallTypeNew("ifthen", CF_DATA_TYPE_STRING, IFTHEN_ARGS, &FnCallIfThen, "Return the value of the first true class expression, or nothing if none is true",
FNCALL_OPTION_VARARG, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, ARGC(2, -1)),
FnCallTypeNew("int", CF_DATA_TYPE_INT, INT_ARGS, &FnCallInt, "Convert numeric string to int",
FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC),
FnCallTypeNew("intersection", CF_DATA_TYPE_STRING_LIST, SETOP_ARGS, &FnCallSetop, "Returns all the unique elements of list or array or data container arg1 that are also in list or array or data container arg2",
Expand Down
54 changes: 54 additions & 0 deletions tests/acceptance/01_vars/02_functions/ifthen.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
######################################################
#
# Test that ifthen() behaves as expected
#
#####################################################
body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

#######################################################
bundle agent test
{
meta:
"description" -> { "ENT-14719" }
string => "Test that ifthen() returns the value of the first true class expression and leaves the variable undefined if none is true";

vars:
"single" string => ifthen("any", "single value");
"first" string => ifthen("!any", "wrong", "any", "first", "any", "second");
"nested" string => ifthen(not("!any"), "nested");
"expanded_var" string => "expanded";
"expanded" string => ifthen("any", "$(expanded_var)");
"none" string => ifthen("!any", "wrong");

# Unlike 3-argument ifelse(), not evaluated while an argument has an
# unresolved variable, even if an earlier pair is true.
"unresolved"
string => ifthen("any", "value", "any", "$(no_such_bundle.no_such_var)");
}

#######################################################
bundle agent check
{
classes:
"ok"
and => {
strcmp("$(test.single)", "single value"),
strcmp("$(test.first)", "first"),
strcmp("$(test.nested)", "nested"),
strcmp("$(test.expanded)", "expanded"),
not(isvariable("test.none")),
not(isvariable("test.unresolved")),
};

reports:
ok::
"$(this.promise_filename) Pass";

!ok::
"$(this.promise_filename) FAIL";
}
29 changes: 29 additions & 0 deletions tests/acceptance/01_vars/02_functions/ifthen_no_args.x.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
######################################################
#
# Test that ifthen() with no arguments is an error
#
#####################################################
body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

#######################################################
bundle agent test
{
meta:
"description" -> { "ENT-14719" }
string => "Test that ifthen() with no arguments is an error";

vars:
"x" string => ifthen();
}

#######################################################
bundle agent check
{
reports:
"$(this.promise_filename) FAIL";
}
29 changes: 29 additions & 0 deletions tests/acceptance/01_vars/02_functions/ifthen_odd_args.x.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
######################################################
#
# Test that ifthen() with an odd number of arguments is an error
#
#####################################################
body common control
{
inputs => { "../../default.sub.cf" };
bundlesequence => { default("$(this.promise_filename)") };
version => "1.0";
}

#######################################################
bundle agent test
{
meta:
"description" -> { "ENT-14719" }
string => "Test that ifthen() with an odd number of arguments is an error";

vars:
"x" string => ifthen("any", "value", "fallthrough");
}

#######################################################
bundle agent check
{
reports:
"$(this.promise_filename) FAIL";
}
Loading