From 11aac6ca4db67d651284cc4cbebdbde92fdc5398 Mon Sep 17 00:00:00 2001 From: Chris Herrera Date: Sat, 15 Nov 2025 16:16:03 -0800 Subject: [PATCH 1/5] Add split_esc function globals Extends the functionality of str.split by adding an escape character. This allows ignoring separators preceded by the escape character. --- glances/globals.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/glances/globals.py b/glances/globals.py index 86fa9020..b9062d78 100644 --- a/glances/globals.py +++ b/glances/globals.py @@ -631,3 +631,84 @@ def exit_after(seconds, default=None): return wraps return decorator + + +def split_esc(string, /, sep=None, maxsplit=-1, esc='\\'): + """ + Return a list of the substrings in the string, using sep as the separator string + and esc as the escape character. + + sep + The separator used to split the string. + + When set to None (the default value), will split on any whitespace + character (including \n \r \t \f and spaces) unless the character is escaped + and will discard empty strings from the result. + maxsplit + Maximum number of splits. + -1 (the default value) means no limit. + esc + The character used to escape the separator. + + When set to None, this behaves equivalently to `str.split`. + Defaults to '\\\\' i.e. backslash. + + Splitting starts at the front of the string and works to the end. + + Note: escape characters in the substrings returned are removed. However, if + maxsplit is reached, escape characters in the remaining, unprocessed substring + are not removed, which allows split_esc to be called on it again. + """ + # Input validation + if not isinstance(string, str): + raise TypeError(f'must be str, not {string.__class__.__name__}') + str.split('', sep=sep, maxsplit=maxsplit) # Use str.split to validate sep and maxsplit + if esc is None: + return string.split( + sep=sep, maxsplit=maxsplit + ) # Short circuit to default implementation if the escape character is None + elif not isinstance(esc, str): + raise TypeError(f'must be str or None, not {esc.__class__.__name__}') + elif len(esc) == 0: + raise ValueError('empty escape character') + elif len(esc) > 1: + raise ValueError('escape must be a single character') + + # Set up a simple state machine keeping track of whether we have seen an escape character + ret, esc_seen, i = [''], False, 0 + while i < len(string) and len(ret) - 1 != maxsplit: + if not esc_seen: + if string[i] == esc: + # Consume the escape character and transition state + esc_seen = True + i += 1 + elif sep is None and string[i].isspace(): + # Consume as much whitespace as possible + n = 1 + while i + n + 1 < len(string) and string[i + n : i + n + 1].isspace(): + n += 1 + ret.append('') + i += n + elif sep is not None and string[i : i + len(sep)] == sep: + # Consume the separator + ret.append('') + i += len(sep) + else: + # Otherwise just add the current char + ret[-1] += string[i] + i += 1 + else: + # Add the current char and transition state back + ret[-1] += string[i] + esc_seen = False + i += 1 + + # Append any remaining string if we broke early because of maxsplit + if i < len(string): + ret[-1] += string[i:] + + # If splitting on whitespace, discard empty strings from result + if sep is None: + ret = [sub for sub in ret if len(sub) > 0] + + return ret From 89c8fc125c704bc74ae334ed672566595842ec81 Mon Sep 17 00:00:00 2001 From: Chris Herrera Date: Sat, 15 Nov 2025 16:17:53 -0800 Subject: [PATCH 2/5] Add unit test for split_esc --- tests/test_core.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 8081be21..23401c76 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -552,6 +552,30 @@ class TestGlances(unittest.TestCase): self.assertEqual(stats.get_plugin('cpu').get_alert(75, minimum=0, maximum=100, header='total'), 'WARNING_LOG') self.assertEqual(stats.get_plugin('cpu').get_alert(85, minimum=0, maximum=100, header='total'), 'CRITICAL_LOG') + def test_024_split_esc(self): + """Test split_esc function""" + print('INFO: [TEST_024] split_esc') + self.assertEqual(split_esc(r''), []) + self.assertEqual(split_esc('\\'), []) + self.assertEqual(split_esc(r'abcd'), [r'abcd']) + self.assertEqual(split_esc(r'abcd efg'), [r'abcd', r'efg']) + self.assertEqual(split_esc('abcd \n\t\f efg'), [r'abcd', r'efg']) + self.assertEqual(split_esc(r'abcd\ efg'), [r'abcd efg']) + self.assertEqual(split_esc(r'', ':'), ['']) + self.assertEqual(split_esc(r'abcd', ':'), [r'abcd']) + self.assertEqual(split_esc(r'abcd:efg', ':'), [r'abcd', r'efg']) + self.assertEqual(split_esc(r'abcd\:efg', ':'), [r'abcd:efg']) + self.assertEqual(split_esc(r'abcd:efg:hijk', ':'), [r'abcd', r'efg', r'hijk']) + self.assertEqual(split_esc(r'abcd\:efg:hijk', ':'), [r'abcd:efg', r'hijk']) + self.assertEqual(split_esc(r'abcd\:efg:hijk\:lmnop:qrs', ':', maxsplit=0), [r'abcd\:efg:hijk\:lmnop:qrs']) + self.assertEqual(split_esc(r'abcd\:efg:hijk\:lmnop:qrs', ':', maxsplit=1), [r'abcd:efg', r'hijk\:lmnop:qrs']) + self.assertEqual( + split_esc(r'abcd\:efg:hijk\:lmnop:qrs', ':', maxsplit=10), [r'abcd:efg', r'hijk:lmnop', r'qrs'] + ) + self.assertEqual(split_esc(r'ahellobhelloc', r'hello'), [r'a', r'b', r'c']) + self.assertEqual(split_esc(r'a\hellobhelloc', r'hello'), [r'ahellob', r'c']) + self.assertEqual(split_esc(r'ahe\llobhelloc', r'hello'), [r'ahellob', r'c']) + def test_093_auto_unit(self): """Test auto_unit classe""" print('INFO: [TEST_093] Auto unit') From 93120b9779d09c35e1639b798b85481870507a3c Mon Sep 17 00:00:00 2001 From: Chris Herrera Date: Sat, 15 Nov 2025 16:19:07 -0800 Subject: [PATCH 3/5] Use split_esc in read_alias Allows for ':' to be used in sensor names or alias names --- glances/plugins/plugin/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/glances/plugins/plugin/model.py b/glances/plugins/plugin/model.py index f7c9c18b..d3dd0cb4 100644 --- a/glances/plugins/plugin/model.py +++ b/glances/plugins/plugin/model.py @@ -27,6 +27,7 @@ from glances.globals import ( listkeys, mean, nativestr, + split_esc, ) from glances.history import GlancesHistory from glances.logger import logger @@ -933,7 +934,7 @@ class GlancesPluginModel: def read_alias(self): if self.plugin_name + '_' + 'alias' in self._limits: - return {i.split(':')[0].lower(): i.split(':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']} + return {split_esc(i, ':')[0].lower(): split_esc(i, ':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']} return {} def has_alias(self, header): From c70321c3fb6d38651886e44035a38c96cf2cba34 Mon Sep 17 00:00:00 2001 From: Chris Herrera Date: Sat, 15 Nov 2025 16:19:53 -0800 Subject: [PATCH 4/5] Formatting changes from running make format --- glances/plugins/plugin/model.py | 5 ++++- tests/test_core.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/glances/plugins/plugin/model.py b/glances/plugins/plugin/model.py index d3dd0cb4..8a8b3b00 100644 --- a/glances/plugins/plugin/model.py +++ b/glances/plugins/plugin/model.py @@ -934,7 +934,10 @@ class GlancesPluginModel: def read_alias(self): if self.plugin_name + '_' + 'alias' in self._limits: - return {split_esc(i, ':')[0].lower(): split_esc(i, ':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']} + return { + split_esc(i, ':')[0].lower(): split_esc(i, ':')[1] + for i in self._limits[self.plugin_name + '_' + 'alias'] + } return {} def has_alias(self, header): diff --git a/tests/test_core.py b/tests/test_core.py index 23401c76..5929003d 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -28,7 +28,18 @@ except ImportError: from glances import __version__ from glances.events_list import GlancesEventsList from glances.filter import GlancesFilter, GlancesFilterList -from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS, auto_unit, pretty_date, string_value_to_float, subsample +from glances.globals import ( + BSD, + LINUX, + MACOS, + SUNOS, + WINDOWS, + auto_unit, + pretty_date, + string_value_to_float, + subsample, + split_esc, +) from glances.main import GlancesMain from glances.outputs.glances_bars import Bar from glances.plugins.fs.zfs import zfs_enable, zfs_stats From df684c64c28007ed8695e4acd368e1232ceaf2ae Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sun, 16 Nov 2025 10:51:49 +0100 Subject: [PATCH 5/5] Remove unuse parameter in split_esc --- glances/globals.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/glances/globals.py b/glances/globals.py index b9062d78..977c50f3 100644 --- a/glances/globals.py +++ b/glances/globals.py @@ -633,13 +633,13 @@ def exit_after(seconds, default=None): return decorator -def split_esc(string, /, sep=None, maxsplit=-1, esc='\\'): +def split_esc(input_string, sep=None, maxsplit=-1, esc='\\'): """ - Return a list of the substrings in the string, using sep as the separator string + Return a list of the substrings in the input_string, using sep as the separator char and esc as the escape character. sep - The separator used to split the string. + The separator used to split the input_string. When set to None (the default value), will split on any whitespace character (including \n \r \t \f and spaces) unless the character is escaped @@ -653,59 +653,59 @@ def split_esc(string, /, sep=None, maxsplit=-1, esc='\\'): When set to None, this behaves equivalently to `str.split`. Defaults to '\\\\' i.e. backslash. - Splitting starts at the front of the string and works to the end. + Splitting starts at the front of the input_string and works to the end. Note: escape characters in the substrings returned are removed. However, if maxsplit is reached, escape characters in the remaining, unprocessed substring are not removed, which allows split_esc to be called on it again. """ # Input validation - if not isinstance(string, str): - raise TypeError(f'must be str, not {string.__class__.__name__}') + if not isinstance(input_string, str): + raise TypeError(f'must be str, not {input_string.__class__.__name__}') str.split('', sep=sep, maxsplit=maxsplit) # Use str.split to validate sep and maxsplit if esc is None: - return string.split( + return input_string.split( sep=sep, maxsplit=maxsplit ) # Short circuit to default implementation if the escape character is None - elif not isinstance(esc, str): + if not isinstance(esc, str): raise TypeError(f'must be str or None, not {esc.__class__.__name__}') - elif len(esc) == 0: + if len(esc) == 0: raise ValueError('empty escape character') - elif len(esc) > 1: + if len(esc) > 1: raise ValueError('escape must be a single character') # Set up a simple state machine keeping track of whether we have seen an escape character ret, esc_seen, i = [''], False, 0 - while i < len(string) and len(ret) - 1 != maxsplit: + while i < len(input_string) and len(ret) - 1 != maxsplit: if not esc_seen: - if string[i] == esc: + if input_string[i] == esc: # Consume the escape character and transition state esc_seen = True i += 1 - elif sep is None and string[i].isspace(): + elif sep is None and input_string[i].isspace(): # Consume as much whitespace as possible n = 1 - while i + n + 1 < len(string) and string[i + n : i + n + 1].isspace(): + while i + n + 1 < len(input_string) and input_string[i + n : i + n + 1].isspace(): n += 1 ret.append('') i += n - elif sep is not None and string[i : i + len(sep)] == sep: + elif sep is not None and input_string[i : i + len(sep)] == sep: # Consume the separator ret.append('') i += len(sep) else: # Otherwise just add the current char - ret[-1] += string[i] + ret[-1] += input_string[i] i += 1 else: # Add the current char and transition state back - ret[-1] += string[i] + ret[-1] += input_string[i] esc_seen = False i += 1 # Append any remaining string if we broke early because of maxsplit - if i < len(string): - ret[-1] += string[i:] + if i < len(input_string): + ret[-1] += input_string[i:] # If splitting on whitespace, discard empty strings from result if sep is None: