Skip to main content

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_replace safely extracts digits;
  • ternary merges the base-file index 0;
  • Avoids regex_search entirely to prevent NoneType issues.

4. Execution Flow

StepActionResult
1Read existing .tar.gz filenames['2025-10-22.tar.gz', '2025-10-22-1.tar.gz']
2Check for “no-suffix” base filebase_exists = true
3Extract numeric parts from suffixed filesnumbers = [1]
4Merge taken indicestaken = [0,1]
5Max + 1next_num = 2
6Assemble suffixsuffix = -2
7Produce new filename2025-10-22-2.tar.gz

5. Jinja2 Regex Filter Reference

FunctionDescriptionReturn
regex_search(pattern, [group])Find match and return groupReturns None if no match
regex_replace(pattern, replace)Replace matched partAlways returns a string
select('match', pattern)Filter elements matching regexSafely filters lists
map('int')Convert string to integerValid only for numeric strings

🚨 Note: regex_search easily returns None on no-match; prefer select('match') before regex_replace.


6. Practical Tips

  1. Avoid chaining regex_search + map('int') → Filter first with select('match').
  2. Use ternary and default for empty cases Ensure safe defaults to avoid max() errors.
  3. Use debug: var= while testing Inspect actual variable values to verify template logic.
  4. 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

TopicKey Point
Jinja2 template exceptionsregex_search returns None → chained ops fail
SolutionFilter first (select('match')), then regex_replace
Empty resultsUse ternary or default for safe defaults
Extract digitsregex_replace + capture groups is robust
Auto-numberingUse max()+1, or loop to find gaps (as needed)
Debugging- debug: var=... to inspect rendered values

9. Further Reading


🔚 10. Summary

ConceptError CauseFix Approach
regex_searchReturns None when no matchFilter first with select('match')
max() on emptyEmpty list raises errorProvide default [0] or use ternary
String → numberNone can’t be intFilter before conversion
Template robustnessDon’t assume perfect inputUse defaults and type-safe ops

Best-practice pipeline:

Filter → Extract → Convert → Aggregate → Concatenate Ensure no input path can trigger an exception.