diff --git a/README.md b/README.md
index a6ae291f..d20a100d 100644
--- a/README.md
+++ b/README.md
@@ -235,6 +235,23 @@ usage: get_lastRun options
+
+ getPVAliases |
+
+usage: gatherPVAliases [-h] [-d] patt hutch
+positional arguments:
+ patt | Regex pattern to match IOCs with.
+ -->Can match anything in the IOC procmanager object. e.g. "lm2k2" or "mcs2" or "ek9000"
+ hutch | 3 letter hutch code. Use "all" to search through all hutches.
+ -->Valid arguments: all, aux, cxi, det, hpl, icl, kfe, las, lfe, mec,
+ mfx, rix, rrl, thz, tmo, tst, txi, ued, xcs, xpp, xrt
+
+optional arguments:
+ -h, --help | show this help message and exit
+ -d, --dry_run | Forces a dry run for the script. No files are saved.
+
+ |
+
grep_ioc |
@@ -245,6 +262,39 @@ usage: grep_ioc KEYWORD [hutch]
|
+
+ grep_more_ioc |
+
+usage: grep_more_ioc [-h] [-d] patt hutch {print,search}
+ positional arguments:
+ patt Regex str to search through iocmanager.cfg
+ e.g. 'mcs2', 'lm2k2-atm.*', 'ek9000', 'gige.*'
+ hutch 3 letter hutch code to search through.
+ Use 'all' to search through all hutches.
+ Valid arguments: all, aux, cxi, det, hpl, icl, kfe,
+ las, lfe, mec, mfx, rix, rrl, thz, tmo, tst, txi, ued,
+ xcs, xpp, xrt
+ -h, --help Show help message and exit
+ -d, --ignore_disabled Exclude IOCs based on disabled state
+ Necessary subcommands.
+ Use: grep_more_ioc . all [subcommand] --help for more information
+ {print, search}
+ print | Prints all the matching IOCs in a dataframe
+ usage: grep_more_ioc patt hutch print [-h] [-c] [-r] [-s] [-y]
+ -h, --help | Show help message and exit
+ -c, --skip_comments | Prints IOC.cfg file with comments skipped
+ -r, --release | Includes the parent IOC release in the dataframe
+ -s, --print_dirs | Dump child & parent directors to the terminal
+ -y, --print_history | Dump child IOC's history to terminal, if it exists
+ search | Regex-like search of child IOCs
+ usage: grep_more_ioc patt hutch search [-h] [-q] [-o] PATT
+ PATT | The regex str to use in the search
+ -h, --help | Show help message and exit
+ -q, --quiet | Surpresses file warning for paths that do not exist
+ -o, --only_search | Skip printing dataframe, only print search results
+ |
+
+
grep_pv |
@@ -308,7 +358,8 @@ usage: ioctool <ioc>|<pv> [option]
telnet : starts a telnet session with the ioc
pvs : opens the IOC.pvlist file in less
|
-
+
+
ipmConfigEpics |
@@ -640,9 +691,9 @@ Optional arguments:
set_gem_timing |
-
+ |
Usage: set_gem_timing [SC or NC]
|
-
\ No newline at end of file
+
diff --git a/scripts/constants.py b/scripts/constants.py
new file mode 100644
index 00000000..ee0b34f7
--- /dev/null
+++ b/scripts/constants.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+"""
+Constants used in grep_more_ioc and getPVAliases
+"""
+###############################################################################
+# %% Imports
+###############################################################################
+
+import glob as gb
+
+###############################################################################
+# %% Constants
+###############################################################################
+
+# Check the directories for the iocmanager config file
+VALID_HUTCH = sorted([d for d in gb.glob('/cds/group/pcds/pyps/config'
+ + '/*/')
+ if gb.glob(d + 'iocmanager.cfg')])
+# Trim to 3 letter hutch code, include 'all' = '*'
+VALID_HUTCH = ['all'] + [s.rsplit(r'/', maxsplit=2)[-2] for s in VALID_HUTCH]
+
+# Keys from iocmanager. Found in /cds/group/pcds/config/*/iocmanager/utils.py
+# Update this as needed
+DEF_IMGR_KEYS = ['procmgr_config', 'hosts', 'dir', 'id', 'cmd',
+ 'flags', 'port', 'host', 'disable', 'history',
+ 'delay', 'alias', 'hard']
diff --git a/scripts/getPVAliases b/scripts/getPVAliases
new file mode 100755
index 00000000..1f5bb1f4
--- /dev/null
+++ b/scripts/getPVAliases
@@ -0,0 +1,6 @@
+#!/usr/bin/bash
+
+# execute python script
+THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+
+/cds/group/pcds/pyps/conda/py39/envs/pcds-5.8.2/bin/python "${THIS_DIR}/getPVAliases.py" "$@"
diff --git a/scripts/getPVAliases.py b/scripts/getPVAliases.py
new file mode 100644
index 00000000..1e27c1ea
--- /dev/null
+++ b/scripts/getPVAliases.py
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+"""
+A script for gathering alias associations from an IOC
+"""
+###############################################################################
+# %% Imports
+###############################################################################
+
+import argparse
+import copy
+import os.path
+import re
+import sys
+
+from colorama import Fore, Style
+from constants import VALID_HUTCH
+from grep_more_ioc import (clean_ansi, find_ioc, find_parent_ioc, fix_dir,
+ search_file, simple_prompt)
+from prettytable import PrettyTable
+
+###############################################################################
+# %% Functions
+###############################################################################
+
+
+def request_dir(prompt: str,
+ default: str = os.getcwd(),) -> str:
+ """
+ Requests the user for a destination to save a file.
+
+ Parameters
+ ----------
+ prompt: str
+ Prompt to show the user
+ default: str
+ File destination to default to.
+ Default is '{current_dir}'
+ Returns
+ -------
+ str
+ Path to file destination.
+ """
+ while True:
+ p = input(prompt+f'(default = {default}): ')
+ # Check for default
+ if p in ['']:
+ result = default
+ else:
+ result = p
+ confirm = simple_prompt('Is '
+ + Fore.LIGHTYELLOW_EX + f'{result} '
+ + Style.RESET_ALL
+ + 'correct? (y/N): ')
+ if confirm is True:
+ break
+ return result
+
+
+def build_table(input_data: list[dict], columns: list[str] = None,
+ **kwargs) -> PrettyTable:
+ """
+ Build a prettytable from a list of dicts/JSON.
+ input_data must be a list(dict)
+ Parameters
+ ----------
+ input_data: list[dict]
+ The data to generate a PrettyTable from.
+ columns: list, optional
+ Columns for the PrettyTable headers. The default is None.
+ **kwargs:
+ kwargs to pass tohe PrettyTable() for extra customization.
+
+ Returns
+ -------
+ PrettyTable
+ PrettyTable object ready for terminal printing.
+
+ """
+
+ if columns is None:
+ col_list = []
+ # First get all unique key values from the dict
+ for _d in input_data:
+ col_list.extend(list(_d.keys()))
+ cols = sorted(list(set(col_list)))
+ else:
+ cols = columns
+ # initialize the table
+ _tbl = PrettyTable()
+ # add the headers
+ for c in cols:
+ _tbl.add_column(c, [], **kwargs)
+ # add the data, strip ANSI color from color headers if any
+ for _d in input_data:
+ _tbl.add_row([_d.get(clean_ansi(c), '') for c in cols])
+ return _tbl
+
+
+def acquire_aliases(dir_path: str, ioc: str) -> list[dict]:
+ """
+ Scans the st.cmd of the child IOC for the main PV aliases.
+ Returns a list of dicts for the associations. This is the
+ top level PV name.
+ E.g. LM1K2:MCS2:01:m1 <--> LM2K2:INJ_MP1_MR1
+
+ Parameters
+ ----------
+ dir_path : str
+ Path to the child IOC's release.
+ ioc : str
+ Child IOC's cfg file name.
+
+ Returns
+ -------
+ list[dict]
+ List of dicts for record <--> alias associations.
+
+ """
+ _d = fix_dir(dir_path)
+ _f = f'{_d}build/iocBoot/{ioc}/st.cmd'
+ if os.path.exists(_f) is False:
+ print(f'{_f} does not exist')
+ return ''
+ search_result = search_file(file=_f, patt=r'db/alias.db')
+ _temp = re.findall(r'"RECORD=.*"', search_result)
+ output = [re.sub(r'\"|\)|RECORD\=|ALIAS\=', '', s).split(',')
+ for s in _temp]
+ return [{'record': s[0], 'alias': s[-1]} for s in output]
+
+
+def process_alias_template(parent_release: str, record: str,
+ alias: str) -> list[str]:
+ """
+ Opens the parent db/alias.db file and processes the
+ substitutions.
+ This is the second level of PV names (like in motor records).
+ E.g. LM1K2:MCS2:01:m1 <--> LM1K2:INJ_MP1_MR1.RBV
+
+ Parameters
+ ----------
+ parent_release : str
+ Path to the parent IOC's release.
+ record : str
+ The EPICS record for substitution to generate PV names.
+ alias : str
+ The alias for the EPICS record.
+
+ Returns
+ -------
+ list[str]
+ DESCRIPTION.
+
+ """
+
+ _target_file = f'{parent_release}/db/alias.db'
+ if os.path.exists(_target_file):
+ with open(_target_file, encoding='utf-8') as _f:
+ _temp = _f.read()
+ else:
+ print(f'{parent_release} does not exist')
+ return None
+ # remove the 'alias' prefix from the tuple
+ _temp = re.sub(r'alias\(| +', '', _temp)
+ _temp = re.sub(r'\)\s*\n', '\n', _temp)
+ # then make the substitutions
+ _temp = re.sub(r'\$\(RECORD\)', record, _temp)
+ _temp = re.sub(r'\$\(ALIAS\)', alias, _temp)
+ return [s.replace('"', '').split(',') for s in _temp.split()]
+
+
+def show_temp_table(input_data: list, col_list: list):
+ """
+ Formats the 'disable' column in the find_ioc json output for clarity
+ and prints the pretty table to the terminal.
+ """
+ # color code the disable state for easier comprehensions
+ temp = copy.deepcopy(input_data)
+ for _d in temp:
+ if _d.get('disable') is not None:
+ if _d.get('disable') is True:
+ _d['disable'] = f'{Fore.LIGHTGREEN_EX}True{Style.RESET_ALL}'
+ else:
+ _d['disable'] = f'{Fore.RED}False{Style.RESET_ALL}'
+
+ # prompt user for initial confirmation
+ print(f'{Fore.LIGHTGREEN_EX}Found the following:{Style.RESET_ALL}')
+ print(build_table(temp, col_list))
+
+
+###############################################################################
+# %% Argparser
+###############################################################################
+# parser obj configuration
+parser = argparse.ArgumentParser(
+ prog='gatherPVAliases',
+ formatter_class=argparse.RawTextHelpFormatter,
+ description="gathers all record <-> alias associations from a child's "
+ "ioc.cfg, st.cmd, and parent ioc.cfg and then optionally "
+ "saves it to a record_alias_dump.txt file.",
+ epilog='')
+# main command arguments
+parser.add_argument('patt', type=str,
+ help='Regex pattern to match IOCs with. '
+ '\nCan match anything in the IOC procmanager object. '
+ 'e.g. "lm2k2" or "mcs2" or "ek9000"')
+parser.add_argument('hutch', type=str,
+ help='3 letter hutch code. Use "all" to search through '
+ 'all hutches.\n'
+ f'Valid arguments: {", ".join(VALID_HUTCH)}')
+parser.add_argument('-d', '--dry_run', action='store_true',
+ default=False,
+ help="Forces a dry run for the script. "
+ "No files are saved.")
+
+###############################################################################
+# %% Main
+###############################################################################
+
+
+def main():
+ """
+ Main function entry point
+ """
+ # parse args
+ args = parser.parse_args()
+ # search ioc_cfg and build the dataset
+ data = find_ioc(args.hutch, args.patt)
+ if data is None:
+ print(f'{Fore.RED}No results found for {Style.RESET_ALL}{args.patt}'
+ + f'{Fore.RED} in{Style.RESET_ALL}'
+ + f'{args.hutch}')
+ sys.exit()
+
+ # find the parent directories
+ for _d in data:
+ _d['parent_ioc'] = find_parent_ioc(_d['id'], _d['dir'])
+
+ # Hard code the column order for the find_ioc output
+ column_list = ['id', 'dir',
+ Fore.LIGHTYELLOW_EX + 'parent_ioc' + Style.RESET_ALL,
+ 'host', 'port',
+ Fore.LIGHTBLUE_EX + 'alias' + Style.RESET_ALL,
+ Fore.RED + 'disable' + Style.RESET_ALL]
+ if args.hutch == 'all':
+ column_list = ['hutch'] + column_list
+ show_temp_table(data, column_list)
+
+ ans = simple_prompt('Proceed? (Y/n): ', default='Y')
+ # Abort if user gets cold feet
+ if ans is False:
+ sys.exit()
+ print(f'{Fore.RED}Skipping disabled child IOCs{Style.RESET_ALL}')
+
+ # initialize the final output to write to file
+ final_output = []
+
+ # iterate through all the child ioc dictionaries
+ for _ioc in data:
+ if _ioc.get('disable') is not True:
+ # first acquire the base alias dictionary
+ alias_dicts = acquire_aliases(_ioc['dir'], _ioc['id'])
+ # show the record aliases to the user
+ print(Fore.LIGHTGREEN_EX
+ + 'The following substitutions were found in the st.cmd:'
+ + Style.RESET_ALL)
+ print(build_table(alias_dicts, ['record', 'alias'], align='l'))
+ # optional skip for all resulting PV aliases
+ save_all = (simple_prompt(
+ 'Do you want to save all resulting PV <--> alias '
+ + 'associations found in this st.cmd?\n'
+ + 'This will append '
+ + Fore.LIGHTYELLOW_EX
+ + f'{len(alias_dicts)}'
+ + Style.RESET_ALL
+ + ' record <--> alias sets to your final output (y/N): '))
+
+ # initialize flags
+ skip_all = None
+ show_pvs = None
+ save_data = None
+
+ # initialize a default file directory for dumping aliases
+ default_dest = os.getcwd() + '/' + f"{_ioc['id']}_alias"
+ # now iterate through the alias dicts for PV alias substitutions
+ for i, a in enumerate(alias_dicts):
+ # then iterate through all the PVs from root PV
+ alias_list = process_alias_template(_ioc['parent_ioc'],
+ a['record'], a['alias'])
+ # capture output based on 61 char max record name
+ _chunk = [f"{al[0]:<61}{al[-1]:<61}" for al in alias_list]
+ # Demonstrate PV aliases on first iteration
+ if (i == 0) | ((show_pvs is True) & (skip_all is False)):
+ # show output to user, building a temp list of dict first
+ _temp = [{'PV': al[0], 'Alias': al[-1]}
+ for al in alias_list]
+ print(Fore.LIGHTGREEN_EX
+ + 'The following PV aliases are built:'
+ + Style.RESET_ALL)
+ print(build_table(_temp, ['PV', 'Alias'], align='l'))
+ del _temp
+
+ # If doing a dry run, skip this block
+ if args.dry_run is False:
+ # Respect the skip flag
+ if skip_all is True:
+ continue
+ # ask user for input
+ if save_all is False:
+ save_data = (simple_prompt(
+ 'Would you like to save this PV set? (y/N): '))
+ if save_data is True:
+ # give the user an option to be lazy again
+ save_all = (simple_prompt(
+ 'Would you like to apply this for'
+ + ' ALL remaining sets? (y/N): '))
+ # Avoid some terminal spam using these flags
+ show_pvs = not save_all
+ skip_all = False
+ if save_data is False:
+ skip_all = (simple_prompt(
+ 'Skip all further substitutions? (Y/n): ',
+ default='Y'))
+ # Avoid some terminal spam using this flag
+ show_pvs = not skip_all
+ continue
+ else:
+ # Set flags to surpress prompts during dry run
+ save_data = False
+ if (save_data or save_all) and (args.dry_run is False):
+ final_output.append('\n'.join(_chunk))
+ del _chunk
+
+ # write to file, else do nothing
+ if (len(final_output) > 0) & (args.dry_run is False):
+ dest = request_dir('Choose base file destination',
+ default_dest)
+ # make sure the destination exists and mkdir if it doesn't
+ if os.path.exists(dest) is False:
+ print(Fore.LIGHTBLUE_EX
+ + f'Making directory: {dest}' + Style.RESET_ALL)
+ os.mkdir(dest)
+ file_dest = dest + "/record_alias_dump.txt"
+ with open(file_dest, 'w', encoding='utf-8') as f:
+ f.write('\n'.join(final_output))
+ default_dest = dest
+ del final_output
+
+ sys.exit()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/grep_more_ioc b/scripts/grep_more_ioc
new file mode 100755
index 00000000..c20c434e
--- /dev/null
+++ b/scripts/grep_more_ioc
@@ -0,0 +1,6 @@
+#!/usr/bin/bash
+
+# execute python script
+THIS_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+
+/cds/group/pcds/pyps/conda/py39/envs/pcds-5.8.4/bin/python "${THIS_DIR}/grep_more_ioc.py" "$@"
diff --git a/scripts/grep_more_ioc.py b/scripts/grep_more_ioc.py
new file mode 100644
index 00000000..a683d01b
--- /dev/null
+++ b/scripts/grep_more_ioc.py
@@ -0,0 +1,581 @@
+# -*- coding: utf-8 -*-
+"""
+A tool for enhancing the grep_ioc CLI tool for ECS-Delivery team
+"""
+###############################################################################
+# %% Imports
+###############################################################################
+
+import argparse
+import glob as gb
+import json
+import os.path
+import re
+import sys
+from shutil import get_terminal_size
+
+import pandas as pd
+from colorama import Fore, Style
+from constants import DEF_IMGR_KEYS, VALID_HUTCH
+
+###############################################################################
+# %% Global settings
+###############################################################################
+# Change max rows displayed to prevent truncating the dataframe
+# We'll assume 1000 rows as an upper limit
+
+pd.set_option("display.max_rows", 1000)
+
+###############################################################################
+# %% Functions
+###############################################################################
+
+
+def search_file(*, file: str, output: list = None,
+ patt: str = None, prefix: str = '',
+ quiet: bool = False, color_wrap: Fore = None) -> str:
+ """
+ Searches file for regex match and appends the result to a list,
+ then formats back into a str with the prefix prepended.
+
+ Parameters
+ ----------
+ file: str
+ The file to read and search. Encoding must be utf-8
+ output: list, optional
+ A list to appead your results to. The default is None.
+ patt: str, optional
+ The regex pattern to search for. The default is None.
+ prefix: str, optional
+ A str prefix to add to each line. The default is ''.
+ color_wrap: Fore, optional
+ Color wrapping using Colorama.Fore. The default is None.
+ quiet: bool, optional
+ Whether to surpress the warning printed to terminal when "file"
+ does not exist. The default is False.
+
+ Returns
+ -------
+ list[str]
+ A list of the search results with the prefix prepended.
+ """
+ if output is None:
+ output = []
+ color = ''
+ reset = ''
+ if color_wrap is not None:
+ color = color_wrap
+ reset = Style.RESET_ALL
+ if os.path.isfile(file) is False:
+ if not quiet:
+ print(f'{file} does not exist')
+ return ''
+ with open(file, 'r', encoding='utf-8') as _f:
+ for line in _f.readlines():
+ if re.search(patt, line):
+ output.append(re.sub(patt, color + r'\g<0>' + reset, line))
+ return prefix + prefix.join(output)
+
+
+def search_procmgr(*, file: str, patt: str = None, output: list = None,
+ prefix: str = '') -> str:
+ """
+ Very similar to search_file, except it is to be used exclusively with
+ iocmanager.cfg files. Grabs the procmgr_cfg lists and searches the
+ regex 'patt' there. Can prepend text with 'prefix' and/or append
+ the results in 'output'.
+
+ Parameters
+ ----------
+ file : str
+ The iocmanager.cfg file to search.
+ patt : str, optional
+ Regex pattern to search with. The default is None.
+ output : list, optional
+ A list to append the results to. The default is None.
+ prefix : str, optional
+ A prefix to add to the start of each result. The default is ''.
+
+ Returns
+ -------
+ str
+ A list[str] that is flattened back into a single body with the prefix
+ prepended. Each result is separated by 'prefix' and '\n'.
+
+ """
+ # Some initialization
+ if output is None:
+ output = []
+ _patt = r'{.*' + patt + r'.*}'
+ # First open the iocmanager.cfg, if it exists
+ if not (os.path.exists(file) and ('iocmanager.cfg' in file)):
+ print(f'{file} does not exist or is otherwise invalid.')
+ return ''
+ with open(file, 'r', encoding='utf-8') as _f:
+ raw_text = _f.read()
+ # then only grab the procmgr_cfg for the search
+ pmgr_key = r'procmgr_config = [\n '
+ pmgr = raw_text[(raw_text.find(pmgr_key)+len(pmgr_key)):-3]
+ # get rid of those pesty inline breaks within the JSOB obj
+ pmgr = pmgr.replace(',\n ', ',').replace('},{', '},\n{')
+ # now let we'll finally search through the IOCs and insert into output
+ output.extend(re.findall(_patt, pmgr))
+ # now return the searches with the prefix prepended and the necessary
+ # line break for later JSONification
+ return prefix + prefix.join([s + '\n' for s in output])
+
+
+def print_skip_comments(file: str):
+ """Prints contents of a file while ignoring comments"""
+ try:
+ with open(file, 'r', encoding='utf_8') as _f:
+ for line in _f.readlines():
+ if not line.strip().startswith('#'):
+ print(line.strip())
+ except FileNotFoundError:
+ print(f'{Fore.RED}Could not open {Style.RESET_ALL}'
+ + f'{file}.'
+ + f'{Fore.RED} Does not exist{Style.RESET_ALL}')
+
+
+def simple_prompt(prompt: str, default: str = 'N'):
+ """Simple yes/no prompt which defaults to No"""
+ while True:
+ p = input(prompt).strip().lower()
+ if p in ['']:
+ p = default.lower()
+ if p[0] == 'y':
+ result = True
+ break
+ if p.lower()[0] == 'n':
+ result = False
+ break
+ print('Invalid Entry. Please choose again.')
+ return result
+
+
+def clean_ansi(text: str = None) -> str:
+ """
+ Removes ANSI escape sequences from a str, including fg/bg formatting.
+ """
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
+ return ansi_escape.sub('', text)
+
+
+def fix_json(raw_data: str, keys: list[str] = None) -> list[str]:
+ """
+ Fixes JSON format of find_ioc/grep_ioc output.
+
+ Parameters
+ ----------
+ raw_data: str
+ Str output generated by find_ioc/grep_ioc, which is pseudo-JSON.
+ keys: list[str]
+ A list of valid keys to use for scraping the IOC.cfg file.
+ Returns
+ -------
+ list[str]
+ The list of str ready for JSON loading
+ """
+ if keys is None:
+ valid_keys = re.compile(r'(?=\s?:\s?)|'.join(DEF_IMGR_KEYS))
+ # clean empty rows and white space
+ _temp = raw_data.replace(' ', '').strip()
+ # capture and fix the keys not properly formatted to str
+ _temp = re.sub(valid_keys, r"'\g<0>'", raw_data)
+ # capture boolean tokens and fix them for json format
+ _temp = re.sub("True", "true", _temp)
+ _temp = re.sub("False", "false", _temp)
+ # then capture and fix digits not formatted to str
+ _temp = re.sub(r"(?<=:)\d+", r"'\g<0>'", _temp)
+ # then properly convert to list of json obj
+ result = (_temp
+ .replace('\'', '\"')
+ .replace('},', '}')
+ .replace(' {', '{')
+ .strip()
+ .split('\n'))
+ return result
+
+
+def find_ioc(hutch: str = None, patt: str = None,
+ valid_hutch: list[str] = VALID_HUTCH) -> list[dict]:
+ """
+ A pythonic grep_ioc for gathering IOC details from the cfg file
+
+ Parameters
+ ----------
+ hutch: str, optional
+ 3 letter lowercase hutch code. May also include 'all'.
+ The default is None.
+ patt: str, optional
+ Regex pattern to search for. The default is None.
+ valid_hutch: list[str], optional
+ List of valid hutch codes to use. The default is taken
+ from the directories in '/cds/group/pcds/pyps/config'
+
+ Raises
+ ------
+ ValueError
+ Hutch code is invalid or regex pattern is missing.
+
+ Returns
+ -------
+ list[dict]
+ List of dictionaries generated by the JSON loading
+
+ """
+ # check hutches
+ if (hutch is None) | (hutch not in tuple(valid_hutch)):
+ print('Invalid entry. Please choose a valid hutch:\n'
+ + ','.join(valid_hutch))
+ raise ValueError
+ # create file paths
+ if hutch in tuple(valid_hutch):
+ if hutch == 'all':
+ path = gb.glob('/cds/group/pcds/pyps/config/*/iocmanager.cfg')
+ else:
+ path = [f'/cds/group/pcds/pyps/config/{hutch}/iocmanager.cfg']
+ # check patt and generate the regex pattern
+ if patt is None:
+ print('No regex pattern supplied')
+ raise ValueError
+ # initialize output list
+ result = []
+ # iterate and capture results.
+ for _file in path:
+ prefix = ''
+ if len(path) != 1:
+ prefix = _file+':'
+ output = search_procmgr(file=_file, patt=patt, prefix=prefix)
+ if output != prefix:
+ result.append(output)
+ # reconstruct the list of str
+ _temp = ''.join(result)
+ if len(_temp) == 0:
+ print(f'{Fore.RED}No results found for {Style.RESET_ALL}{patt}'
+ + f'{Fore.RED} in{Style.RESET_ALL} '
+ + f'{hutch}')
+ return None
+ # capture the hutch from the cfg path if hutch = all
+ if hutch == 'all':
+ hutch_cfgs = re.findall(r'/.*cfg\:', _temp)
+ hutch_cfgs = [''.join(re.findall(r'(?<=/)\w+(?=/ioc)', s))
+ for s in hutch_cfgs]
+ # strip the file information
+ _temp = re.sub(r'.*cfg\:', '', _temp)
+ # now convert back to json and load
+ output = [json.loads(s) for s in fix_json(_temp)]
+ # and add the hutches back into the dicts if searching across all cfgs
+ if hutch == 'all':
+ for _i, _d in enumerate(output):
+ _d['hutch'] = hutch_cfgs[_i]
+ return output
+
+
+def fix_dir(dir_path: str) -> str:
+ """
+ Simple function for repairing the child release IOC path based on
+ the ioc_cfg output. Returns the proper dir for the child IOC.cfg file.
+
+ Parameters
+ ----------
+ dir_path : str
+ The path to the child IOC's directory as a str.
+
+ Returns
+ -------
+ str
+ The typo-corrected path as a str.
+
+ """
+
+ # catches the short form path
+ if dir_path.startswith('ioc/'):
+ output_dir = '/cds/group/pcds/epics/'+dir_path
+ # for the rare, old child IOCs that only exist in their parent's release
+ elif 'common' in dir_path:
+ output_dir = dir_path + '/children'
+ # Otherwise fine!
+ else:
+ output_dir = dir_path
+ # Make sure the end of the path is a folder!
+ if output_dir[-1] != '/':
+ output_dir += '/'
+ return output_dir
+
+
+def find_parent_ioc(file: str, path: str) -> str:
+ """
+ Searches the child IOC for the parent's release pointer
+ Returns the parent's IOC as a str.
+
+ Parameters
+ ----------
+ file : str, optional
+ DESCRIPTION. The default is None.
+ path : str, optional
+ DESCRIPTION. The default is None.
+
+ Returns
+ -------
+ str
+ Path to the parent IOC's release.
+
+ """
+ file_dir = fix_dir(path)
+ if os.path.exists(f'{file_dir}{file}.cfg') is False:
+ return 'Invalid. Child does not exist.'
+ parent_ioc_release = search_file(file=f'{file_dir}{file}.cfg',
+ patt='^RELEASE').strip()
+ return parent_ioc_release.rsplit('=', maxsplit=1)[-1]
+
+
+def print_frame2term(dataframe: pd.DataFrame = None,):
+ """Wrapper for displaying the dataframe to proper terminal size"""
+ with pd.option_context('display.max_rows', None,
+ 'display.max_columns', None,
+ 'display.width',
+ get_terminal_size(fallback=(120, 50))[0],
+ ):
+ print(dataframe)
+
+###############################################################################
+# %% Arg Parser
+###############################################################################
+
+
+def build_parser():
+ """
+ Builds the parser & subparsers for the main function
+ """
+ # parser obj configuration
+ parser = argparse.ArgumentParser(
+ prog='grep_more_ioc',
+ formatter_class=argparse.RawTextHelpFormatter,
+ description='Transforms grep_ioc output to json object'
+ + ' and prints in pandas.DataFrame',
+ epilog='For more information on subcommands, use: '
+ 'grep_more_ioc . all [subcommand] --help')
+ # main command arguments
+ parser.add_argument('patt', type=str,
+ help='Regex pattern to match IOCs with. '
+ '\nCan match anything in the IOC procmanager object. '
+ 'e.g. "lm2k2" or "mcs2" or "gige"')
+ parser.add_argument('hutch', type=str,
+ help='3 letter hutch code. Use "all" to search through'
+ ' all hutches.\n'
+ f'Valid arguments: {", ".join(VALID_HUTCH)}')
+ parser.add_argument('-d', '--ignore_disabled',
+ action='store_true',
+ default=False,
+ help='Flag for excluding based'
+ + ' on the "disabled" state.')
+ # subparsers
+ subparsers = parser.add_subparsers(
+ help='Required subcommands after capturing IOC information:')
+# --------------------------------------------------------------------------- #
+# print subarguments
+# --------------------------------------------------------------------------- #
+ print_frame = (subparsers
+ .add_parser('print',
+ help='Just a simple print of the dataframe'
+ + ' to the terminal.'))
+
+ print_frame.add_argument('print',
+ action='store_true', default=False)
+
+ print_frame.add_argument('-c', '--skip_comments', action='store_true',
+ default=False,
+ help='Prints the IOC.cfg'
+ + ' file with comments skipped')
+
+ print_frame.add_argument('-r', '--release', action='store_true',
+ default=False,
+ help="Adds the parent IOC's"
+ + " release to the dataframe")
+
+ print_frame.add_argument('-s', '--print_dirs', action='store_true',
+ default=False,
+ help='Prints the child & parent IOC'
+ + ' directories as the final output')
+ print_frame.add_argument('-y', '--print_history', action='store_true',
+ default=False,
+ help="Prints the child IOC's history to terminal")
+# --------------------------------------------------------------------------- #
+# search subarguments
+# --------------------------------------------------------------------------- #
+ search = subparsers.add_parser('search',
+ help='For using regex-like searches in the'
+ + ' child IOC.cfg captured by grep_ioc.'
+ + '\nUseful for quickly gathering instance'
+ + ' information, IP addr, etc.')
+
+ search.add_argument('search',
+ type=str, help='PATT to use for regex search in file',
+ metavar='PATT')
+ search.add_argument('-q', '--quiet', action='store_true', default=False,
+ help='Surpresses file warning for paths that do not'
+ + ' exist.')
+ search.add_argument('-o', '--only_search', action='store_true',
+ default=False,
+ help="Don't print the dataframe, just search results.")
+ return parser
+
+###############################################################################
+# %% Main
+###############################################################################
+
+
+def main():
+ """
+ Main entry point of the program. For using with CLI tools.
+ """
+ parser = build_parser()
+ args = parser.parse_args()
+ # read grep_ioc output
+ data = find_ioc(args.hutch, args.patt)
+
+ # exit if grep_ioc finds nothing
+ if (data is None or len(data) == 0):
+ print(f'{Fore.RED}No IOCs were found.\nExiting . . .{Style.RESET_ALL}')
+ sys.exit()
+
+ # create the dataframe after fixing the json format
+ df = pd.json_normalize(data)
+
+ # reorder the dataframe if searching all hutches
+ if args.hutch == 'all':
+ df.insert(0, 'hutch', df.pop('hutch'))
+
+ # pad the disable column based on the grep_ioc output
+ if 'disable' not in df.columns:
+ df['disable'] = df.index.size*[False]
+ if 'disable' in df.columns:
+ df.disable.fillna(False, inplace=True)
+
+ # Fill the NaN with empty strings for rarely used keys
+ for _col in df.columns:
+ if _col not in ['delay']:
+ df[_col].fillna('', inplace=True)
+ else:
+ df[_col].fillna(0, inplace=True)
+
+ # check for the ignore_disabled flag
+ if args.ignore_disabled is True:
+ df = df[~df.disable].reset_index(drop=True)
+
+# --------------------------------------------------------------------------- #
+# %%% print
+# --------------------------------------------------------------------------- #
+ # print the dataframe
+ if hasattr(args, 'print'):
+ if args.release is True:
+ # intialize list for adding a new column
+ output_list = []
+ # iterate through ioc and directory pairs
+ for f, d in df.loc[:, ['id', 'dir']].values:
+ search_result = find_parent_ioc(f, d)
+ # catch parent IOCs running out of dev
+ if 'epics-dev' in search_result:
+ output_str = search_result
+ # abbreviate path for standard IOC releases
+ elif 'common' in search_result:
+ output_str = (search_result
+ .rsplit(r'common/', maxsplit=1)[-1])
+ # check for children living in parent's dir
+ elif '$$UP(PATH)' in search_result:
+ output_str = d.rsplit(r'/children', maxsplit=1)[0]
+ # else use the full path that's found
+ else:
+ output_str = search_result
+ # add it to the list
+ output_list.append(output_str)
+ # Then, finally, add the column to the dataframe
+ df['Release Version'] = output_list
+ # put it next to the child dirs
+ df.insert(df.columns.tolist().index('dir')+1,
+ 'Release Version',
+ df.pop('Release Version'))
+
+ print_frame2term(df)
+
+ if args.skip_comments is True:
+ for ioc, d in df.loc[:, ['id', 'dir']].values:
+ # fixes dirs if ioc_manager truncates the path due to
+ # common ioc dir path
+ target_dir = fix_dir(d)
+ print(f'{Fore.LIGHTBLUE_EX}Now in: {target_dir}'
+ + Style.RESET_ALL)
+ print(f'{Fore.LIGHTYELLOW_EX}{ioc}:{Style.RESET_ALL}')
+ # prints the contents of the file while ignoring comments
+ print_skip_comments(file=f'{target_dir}{ioc}.cfg')
+
+ if args.print_dirs is True:
+ print(f'{Fore.LIGHTBLUE_EX}\nDumping directories:\n'
+ + Style.RESET_ALL)
+ for f, d in df.loc[:, ['id', 'dir']].values:
+ search_result = find_parent_ioc(f, d)
+ d = fix_dir(d)
+ # check for cases where child IOC.cfg DNE
+ if not os.path.exists(f'{d}{f}.cfg'):
+ child_ioc = ''
+ color_prefix = Fore.LIGHTRED_EX
+ else:
+ child_ioc = f'{f}.cfg'
+ color_prefix = Fore.LIGHTBLUE_EX
+ # Print this for easier cd / pushd shenanigans
+ print(f'{d}{Fore.LIGHTYELLOW_EX}{child_ioc}{Style.RESET_ALL}'
+ + '\n\t\t|-->'
+ + f'{Fore.LIGHTGREEN_EX}RELEASE={Style.RESET_ALL}'
+ + f'{color_prefix}{search_result}{Style.RESET_ALL}'
+ )
+
+ if args.print_history is True:
+ print(f'{Fore.LIGHTMAGENTA_EX}\nDumping histories:\n'
+ + Style.RESET_ALL)
+ if 'history' in df.columns:
+ for f, h in df.loc[:, ['id', 'history']].values:
+ print(f'{Fore.LIGHTYELLOW_EX}{f}{Style.RESET_ALL}'
+ + '\nhistory:\n\t'
+ + '\n\t'.join(h))
+ else:
+ print(f'{Fore.LIGHTRED_EX}No histories found in captured IOCs.'
+ + Style.RESET_ALL)
+
+# --------------------------------------------------------------------------- #
+# %%% search
+# --------------------------------------------------------------------------- #
+ # do a local grep on each file in their corresponding directory
+ if hasattr(args, 'search'):
+ # optionally print the dataframe
+ if not args.only_search:
+ print_frame2term(df)
+ check_search = []
+ for ioc, d in df.loc[:, ['id', 'dir']].values:
+ target_dir = fix_dir(d)
+ # Search for pattern after moving into the directory
+ if args.search is not None:
+ search_result = (search_file(file=f'{target_dir}{ioc}.cfg',
+ patt=args.search,
+ color_wrap=Fore.LIGHTRED_EX,
+ quiet=args.quiet)
+ .strip()
+ )
+ if len(search_result) > 0:
+ print(f'{Fore.LIGHTYELLOW_EX}{ioc}:{Style.RESET_ALL}')
+ print(''.join(search_result.strip()))
+ check_search.append(len(search_result))
+ if len(check_search) == 0:
+ print(Fore.RED + 'No search results found' + Style.RESET_ALL)
+# --------------------------------------------------------------------------- #
+# %%% Exit
+# --------------------------------------------------------------------------- #
+ sys.exit()
+
+
+# --------------------------------------------------------------------------- #
+# %%% Entry point
+# --------------------------------------------------------------------------- #
+if __name__ == '__main__':
+ main()