Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Added support for negative alt condition #522

Closed
wants to merge 9 commits into from
71 changes: 71 additions & 0 deletions test/test_unit_score_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,74 @@ def test_underscores_and_upper_case_in_distro_and_family(runner, yadm):
assert run.success
assert run.err == ""
assert run.out == expected

def test_negative_class_condition(runner, yadm):
"""Test negative class condition: returns 0 when matching and proper score when not matching."""
script = f"""
YADM_TEST=1 source {yadm}
local_class="testclass"
local_classes=("testclass")

# 0
score=0
score_file "filename##~class.testclass" "dest"
echo "score: $score"

# 16
score=0
score_file "filename##~class.badclass" "dest"
echo "score2: $score"

# 16
score=0
score_file "filename##~c.badclass" "dest"
echo "score3: $score"
"""
run = runner(command=["bash"], inp=script)
assert run.success
output = run.out.strip().splitlines()
assert output[0] == "score: 0"
assert output[1] == "score2: 16"
assert output[2] == "score3: 16"

def test_negative_combined_conditions(runner, yadm):
"""Test negative conditions for multiple alt types: returns 0 when matching and proper score when not matching."""
script = f"""
YADM_TEST=1 source {yadm}
local_class="testclass"
local_classes=("testclass")
local_distro="testdistro"

# (0) + (0) = 0
score=0
score_file "filename##~class.testclass,~distro.testdistro" "dest"
echo "score: $score"

# (1000 + 16) + (1000 + 4) = 2020
score=0
score_file "filename##class.testclass,distro.testdistro" "dest"
echo "score2: $score"

# 0 (negated class condition)
score=0
score_file "filename##~class.badclass,~distro.testdistro" "dest"
echo "score3: $score"

# (1000 + 16) + (4) = 1020
score=0
score_file "filename##class.testclass,~distro.baddistro" "dest"
echo "score4: $score"

# (1000 + 16) + (16) = 1032
score=0
score_file "filename##class.testclass,~class.badclass" "dest"
echo "score5: $score"
"""
run = runner(command=["bash"], inp=script)
assert run.success
output = run.out.strip().splitlines()
assert output[0] == "score: 0"
assert output[1] == "score2: 2020"
assert output[2] == "score3: 0"
assert output[3] == "score4: 1020"
assert output[4] == "score5: 1032"
27 changes: 18 additions & 9 deletions yadm
Original file line number Diff line number Diff line change
Expand Up @@ -179,32 +179,39 @@ function score_file() {
local value=${field#*.}
[ "$field" = "$label" ] && value="" # when .value is omitted

# Check for negative condition prefix (e.g., "~<label>")
local negate=0
if [ "${label:0:1}" = "~" ]; then
negate=1
label="${label:1}"
fi

shopt -s nocasematch
local -i delta=-1
local -i delta=$(( negate ? 1 : -1 ))
case "$label" in
default)
delta=0
;;
a | arch)
[[ "$value" = "$local_arch" ]] && delta=1
[[ "$value" = "$local_arch" ]] && delta=1 || delta=-1
;;
o | os)
[[ "$value" = "$local_system" ]] && delta=2
[[ "$value" = "$local_system" ]] && delta=2 || delta=-2
;;
d | distro)
[[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4
[[ "${value// /_}" = "${local_distro// /_}" ]] && delta=4 || delta=-4
;;
f | distro_family)
[[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8
[[ "${value// /_}" = "${local_distro_family// /_}" ]] && delta=8 || delta=-8
;;
c | class)
in_list "$value" "${local_classes[@]}" && delta=16
in_list "$value" "${local_classes[@]}" && delta=16 || delta=-16
;;
h | hostname)
[[ "$value" = "$local_host" ]] && delta=32
[[ "$value" = "$local_host" ]] && delta=32 || delta=-32
;;
u | user)
[[ "$value" = "$local_user" ]] && delta=64
[[ "$value" = "$local_user" ]] && delta=64 || delta=-64
;;
e | extension)
# extension isn't a condition and doesn't affect the score
Expand All @@ -230,11 +237,13 @@ function score_file() {
esac
shopt -u nocasematch

(( negate )) && delta=$((-delta))
if ((delta < 0)); then
score=0
return
fi
score=$((score + 1000 + delta))
(( negate )) || delta=$((delta + 1000))
score=$((score + delta))
done

record_score "$score" "$target" "$source" "$template_processor"
Expand Down
28 changes: 20 additions & 8 deletions yadm.1
Original file line number Diff line number Diff line change
Expand Up @@ -475,9 +475,11 @@ commas.

Each condition is an attribute/value pair, separated by a period. Some
conditions do not require a "value", and in that case, the period and value can
be omitted. Most attributes can be abbreviated as a single letter.
be omitted. Most attributes can be abbreviated as a single letter. Prefixing an
attribute with "~" negates the condition, meaning the condition is considered
only if the attribute/value pair evaluates to false.

<attribute>[.<value>]
[~]<attribute>[.<value>]

.BR NOTE :
Value is compared case-insensitive.
Expand Down Expand Up @@ -552,11 +554,12 @@ For all files managed by yadm's repository or listed in
if they match this naming convention,
symbolic links will be created for the most appropriate version.

The "most appropriate" version is determined by calculating a score for each
version of a file. A template is always scored higher than any symlink
condition. The number of conditions is the next largest factor in scoring.
Files with more conditions will always be favored. Any invalid condition will
disqualify that file completely.
The "most appropriate" version is determined by calculating a score for each
version of a file. A template is always scored higher than any symlink
condition. The number of conditions is the next largest factor in scoring;
files with more conditions will always be favored. Negative conditions (prefixed
with "~") are scored only relative to the number of non-negated conditions.
Any invalid condition will disqualify that file completely.

If you don't care to have all versions of alternates stored in the same
directory as the generated symlink, you can place them in the
Expand All @@ -575,6 +578,7 @@ files are managed by yadm's repository:
- $HOME/path/example.txt##os.Linux
- $HOME/path/example.txt##os.Linux,hostname.host1
- $HOME/path/example.txt##os.Linux,hostname.host2
- $HOME/path/example.txt##class.Work,~os.Darwin

If running on a Macbook named "host2",
yadm will create a symbolic link which looks like this:
Expand All @@ -597,10 +601,18 @@ If running on a Solaris server, the link will use the default version:

.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##default

If running on a system, with class set to "Work", the link will be:
If running on a Macbook with class set to "Work", the link will be:

.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##class.Work

Negative conditions are supported via the "~" prefix. If again running on a system
with class set to "Work", but instead within Windows Subsystem for Linux, where the
os is reported as WSL, the link will be:

.IR $HOME/path/example.txt " -> " $HOME/path/example.txt##class.Work,~os.Darwin

Negative conditions use the same weight which corresponds to the attached attribute.

If no "##default" version exists and no files have valid conditions, then no
link will be created.

Expand Down