2025-10-23
:::tip Quote At sunset my heart is still strong, though autumn winds loosen a failing frame. — “Jianghan” · Du Fu, Tang Dynasty :::
Details
Full Text
A homesick traveler on the Yangtze and Han; a pedant adrift beneath heaven and earth.A wisp of cloud, far as the sky; a lonely moon through an endless night.
At sunset my heart is still strong, though autumn winds loosen a failing frame.
Since ancient times, old horses have been kept—no need to take the long road.
Exit tmux
There are several ways to exit a session in tmux, depending on whether you want to close the entire session or only leave the current window/pane:
🧱 1) Exit the current pane
If you only want to leave the current shell (e.g., bash/zsh), simply type:
exit
Or use the shortcut:
Ctrl + D
👉 If all panes in the current window are closed, that window will disappear automatically;
if all windows in the session are closed, tmux will exit automatically.
🪟 2) Close the entire window
Inside a tmux window, run:
exit
Or press:
Ctrl + D
Multiple times until the window is gone. Or force close with the shortcut:
Ctrl + B & → confirm y
This closes the current window.
🧩 3) Kill the entire session
If you want to completely exit a tmux session (say it’s called my-session):
tmux kill-session -t my-session
Or inside the session, press:
Ctrl + B :kill-session
You can also list and close from outside:
tmux ls # list sessions
tmux kill-session -t 0 # kill session 0
💡 4) Detach temporarily (leave running)
If you just want to “hang” tmux and come back later (keep programs running):
Ctrl + B D
You’ll see:
[detached (from session 0)]
Reattach later with:
tmux attach -t 0
Or:
tmux a
🚀 5) Kill all tmux sessions completely
If you want to “wipe out” all tmux instances:
tmux kill-server
This terminates all sessions and processes.
Absolutely ✅
Below is a version with headings starting from ##, shifting all heading levels down by one—handy for dropping into a project doc hierarchy (e.g., under docs/ansible/).
🧩 Ansible Jinja2 Regex Handling & Exception-Safe Practices
1. Background
When writing Ansible Playbooks, we often need to auto-compute the next available index based on existing filenames, e.g.:
2025-10-22.tar.gz
2025-10-22-1.tar.gz
2025-10-22-2.tar.gz
Goal:
If the file exists, automatically append an incrementing suffix (like
-1,-2,-3) until it’s unique.
Original idea:
set_fact:
nums: >-
{{
_found.files
| map(attribute='path')
| map('basename')
| map('regex_search', '^' ~ _date ~ '(?:-(\\d+))?\\.tar\\.gz$', '\\1')
| map('default', '0', true)
| map('int')
| list
}}
next_num: "{{ (nums | max) + 1 if (_found.matched | int) > 0 else 0 }}"
But it failed with:
'NoneType' object has no attribute 'group'
2. Root Cause Analysis
Jinja2’s regex_search returns None when there’s no match.
Subsequent operations like map('int') expect strings or numbers, not None, which triggers:
AttributeError: 'NoneType' object has no attribute 'group'
Even with default('0', true), it only applies when a variable is undefined, not when it’s None, so the error persists.
3. Correct Approach: Safe Regex + Filters
Use select('match') + regex_replace for safe handling.
- name: Compute next available dest name (safe)
ansible.builtin.set_fact:
archive_dest: "{{ base_dir }}/{{ _date }}{{ suffix }}.tar.gz"
vars:
basenames: >-
{{ _found.files | map(attribute='path') | map('basename') | list }}
# Whether the base file (_date.tar.gz) exists
base_exists: >-
{{ basenames | select('equalto', _date ~ '.tar.gz') | list | length > 0 }}
# Only select files matching "-number.tar.gz"
numbers: >-
{{
basenames
| select('match', '^' ~ _date ~ '-\\d+\\.tar\\.gz$')
| map('regex_replace', '^' ~ _date ~ '-(\\d+)\\.tar\\.gz$', '\\1')
| map('int')
| list
}}
# Treat the base file as index 0
taken: >-
{{
(base_exists | ternary([0], [])) + numbers
}}
# Max + 1
next_num: >-
{{ 0 if (taken | length) == 0 else (taken | max) + 1 }}
# Build final filename suffix
suffix: "{{ '' if next_num == 0 else '-' ~ next_num }}"
✅ Key improvements:
select('match')ensures regex ops only run on matching strings;regex_replacesafely extracts digits;ternarymerges the base-file index 0;- Avoids
regex_searchentirely to preventNoneTypeissues.
4. Execution Flow
| Step | Action | Result |
|---|---|---|
| 1 | Read existing .tar.gz filenames | ['2025-10-22.tar.gz', '2025-10-22-1.tar.gz'] |
| 2 | Check for “no-suffix” base file | base_exists = true |
| 3 | Extract numeric parts from suffixed files | numbers = [1] |
| 4 | Merge taken indices | taken = [0,1] |
| 5 | Max + 1 | next_num = 2 |
| 6 | Assemble suffix | suffix = -2 |
| 7 | Produce new filename | 2025-10-22-2.tar.gz |
5. Jinja2 Regex Filter Reference
| Function | Description | Return |
|---|---|---|
regex_search(pattern, [group]) | Find match and return group | Returns None if no match |
regex_replace(pattern, replace) | Replace matched part | Always returns a string |
select('match', pattern) | Filter elements matching regex | Safely filters lists |
map('int') | Convert string to integer | Valid only for numeric strings |
🚨 Note:
regex_searcheasily returnsNoneon no-match; preferselect('match')beforeregex_replace.
6. Practical Tips
- Avoid chaining
regex_search+map('int')→ Filter first withselect('match'). - Use
ternaryanddefaultfor empty cases Ensure safe defaults to avoidmax()errors. - Use
debug: var=while testing Inspect actual variable values to verify template logic. - Follow “filter → extract → convert”
Don’t call string methods on
None.
7. Full Task Context Example
- name: Find existing archives for this date
find:
paths: "{{ base_dir }}"
patterns: "{{ _date }}*.tar.gz"
file_type: file
register: _found
- name: Compute next available dest name (safe)
set_fact:
archive_dest: "{{ base_dir }}/{{ _date }}{{ suffix }}.tar.gz"
vars:
basenames: "{{ _found.files | map(attribute='path') | map('basename') | list }}"
base_exists: "{{ basenames | select('equalto', _date ~ '.tar.gz') | list | length > 0 }}"
numbers: >-
{{
basenames
| select('match', '^' ~ _date ~ '-\\d+\\.tar\\.gz$')
| map('regex_replace', '^' ~ _date ~ '-(\\d+)\\.tar\\.gz$', '\\1')
| map('int')
| list
}}
taken: "{{ (base_exists | ternary([0], [])) + numbers }}"
next_num: "{{ 0 if (taken | length) == 0 else (taken | max) + 1 }}"
suffix: "{{ '' if next_num == 0 else '-' ~ next_num }}"
- name: Compress the files
archive:
path: "{{ base_dir }}/{{ _date }}"
dest: "{{ archive_dest }}"
format: gz
remove: true
8. Quick Reference
| Topic | Key Point |
|---|---|
| Jinja2 template exceptions | regex_search returns None → chained ops fail |
| Solution | Filter first (select('match')), then regex_replace |
| Empty results | Use ternary or default for safe defaults |
| Extract digits | regex_replace + capture groups is robust |
| Auto-numbering | Use max()+1, or loop to find gaps (as needed) |
| Debugging | - debug: var=... to inspect rendered values |
9. Further Reading
- Ansible Docs — Jinja2 Filters
- Jinja2 Docs — Regex Filters
- Ansible Best Practices — Template Resilience
🔚 10. Summary
| Concept | Error Cause | Fix Approach |
|---|---|---|
regex_search | Returns None when no match | Filter first with select('match') |
max() on empty | Empty list raises error | Provide default [0] or use ternary |
| String → number | None can’t be int | Filter before conversion |
| Template robustness | Don’t assume perfect input | Use defaults and type-safe ops |
✅ Best-practice pipeline:
Filter → Extract → Convert → Aggregate → Concatenate Ensure no input path can trigger an exception.