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()