diff --git a/README.md b/README.md index abc21ba..e391710 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,10 @@ If you want to use bash autocompletion function with sshch, copy autocompletion ``` sudo cp sshch_bash_completion.sh /etc/bash_completion.d/sshch ``` -(changes will come into effect with new bash session) \ No newline at end of file +(changes will come into effect with new bash session) + +If you want to use zsh autocompletion: +1) Place File in a Directory where ZSH can find it + -> Search Path is Stored in $fpath + -> echo $fpath +2) Rename File to '_sshch' \ No newline at end of file diff --git a/setup.py b/setup.py index 073dc9d..6dd4e31 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def main(): url='https://github.com/zlaxy/sshch/', description='Ssh connection manager', license='DWTWL 2.5', - version='0.8', + version='0.993', py_modules=['sshch'], scripts=['sshch/sshch'], diff --git a/sshch/sshch b/sshch/sshch index be845d9..016c048 100755 --- a/sshch/sshch +++ b/sshch/sshch @@ -20,9 +20,11 @@ import curses from curses import textpad, panel # https://github.com/zlaxy/sshch -version = "0.9" +version = "0.993" # path to conf file, default: ~/.config/sshch.conf conf_file = path.expanduser("~") + '/.config/sshch.conf' +# expand groups by default +expand_default = False def AddNewAlias(alias): @@ -39,6 +41,11 @@ def SetAliasString(alias, string): conf.write(open(conf_file, "w")) +def SetGroupString(alias, string): + conf.set(alias, "group", string) + conf.write(open(conf_file, "w")) + + def SetPassword(alias, string): string = base64.b64encode(base64.b16encode( base64.b32encode(string))) @@ -56,22 +63,29 @@ def ConnectAlias(alias, command=False): exec_string = "" if conf.has_option(alias, "password"): password = base64.b32decode(base64.b16decode( - base64.b64decode(conf.get(alias, "password")))) + base64.b64decode(conf.get(alias, "password")))) exec_string = 'sshpass -p "' + password + '" ' if conf.has_option(alias, "exec_string"): exec_string = exec_string + conf.get(alias, "exec_string") if command: exec_string = exec_string + " " + command - # Variables bellow is newer used subprocess.Popen(exec_string, shell=True).communicate()[0] def HoldConnection(alias): - print("Connecting to " + alias + ". Press CTRL+C to cancel.") - time.sleep(1) - while True: - ConnectAlias(alias) - time.sleep(5) + groups = [] + connectaliases = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if alias in groups: + print("Can't hold connection with group.") + else: + print("Connecting to " + alias + ". Press CTRL+C to cancel.") + time.sleep(1) + while True: + ConnectAlias(alias) + time.sleep(5) def CMDAdd(alias): @@ -87,66 +101,161 @@ def CMDAdd(alias): print(result) -def CMDEdit(alias): - if conf.has_section(alias): - prompt_edit = ("".join(["Enter connection string for existing alias ", - "(example: ssh user@somehost.com):\n"])) +def CMDGroup(group): + result = AddNewAlias(group) + if result: + prompt_add = ("".join(["Enter aliases for new group ", + "(example: alias1 alias2):\n"])) string = "" while string == "": - string = input(prompt_edit) - SetAliasString(alias, string) + string = input(prompt_add) + SetGroupString(group, string) else: - print("error: '" + alias + "' alias is not exists") + print(result) + + +def CMDEdit(alias): + if conf.has_section(alias): + if conf.has_option(alias, "string"): + prompt_edit = ("".join(["Enter connection string for existing alias ", + "(example: ssh user@somehost.com):\n"])) + string = "" + while string == "": + string = input(prompt_edit) + SetAliasString(alias, string) + elif conf.has_option(alias, "group"): + prompt_edit = ("".join(["Enter aliases for existing group ", + "(example: alias1 alias2):\n"])) + string = "" + while string == "": + string = input(prompt_edit) + SetGroupString(alias, string) + else: + print("error: '" + alias + "' is not correct alias or group") + else: + print("error: '" + alias + "' alias or group does not exists") def CMDPassword(alias): - if conf.has_section(alias): - prompt_pass = ("[UNSAFE] Enter password for sshpass: ") - string = "" - string = getpass(prompt_pass) - if not string == "": - SetPassword(alias, string) + groups = [] + connectaliases = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if alias in groups: + print("Can't set password for group.") else: - print("error: '" + alias + "' alias is not exists") + if conf.has_section(alias): + prompt_pass = ("[UNSAFE] Enter password for sshpass: ") + string = "" + string = getpass(prompt_pass) + if not string == "": + SetPassword(alias, string) + else: + print("error: '" + alias + "' alias does not exists") def CMDRemove(alias): if conf.has_section(alias): - prompt_remove = ("Type 'yes' if you sure to remove '" + alias + "' alias: ") + prompt_remove = ("Type 'yes' if you sure to remove '" + alias + "' alias or group: ") string = input(prompt_remove) if string == "yes": RemoveAliases([alias]) else: - print("'" + alias + "' alias was not deleted.") + print("'" + alias + "' alias or group does not deleted.") else: - print("error: '" + alias + "' alias is not exists.") + print("error: '" + alias + "' alias or group does not exists.") def CMDConnect(aliases, command=False): + groups = [] + connectaliases = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) for alias in aliases: + if alias in groups: + group_aliases = conf.get(alias, "group").split() + for ga in group_aliases: + if not ga in connectaliases: + connectaliases.append(ga) + else: + if not alias in connectaliases: + connectaliases.append(alias) + for alias in connectaliases: if conf.has_section(alias): print("Connecting to " + alias + "...") ConnectAlias(alias, command) print("... " + alias + " session finished.") else: - print("error: '" + alias + "' alias is not exists") + print("error: '" + alias + "' alias does not exists") def CMDList(option, opt, value, parser): print(' '.join(str(p) for p in conf.sections())) +def GetTreeList(strings=True, expandlist=True): + aliases = [] + groups = [] + resultalias = [] + resultstring = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + elif conf.has_option(a, "exec_string"): + aliases.append(a) + for g in groups: + resultalias.append(g) + resultstring.append(">> "+g) + group_aliases = conf.get(g, "group").split() + for ga in group_aliases: + if expandlist == False: + pass + elif expandlist == True or g in expandlist: + if conf.has_option(ga, "exec_string"): + resultalias.append(ga+" "+g) + result = "".join([" ", str(ga), " (", (conf.get(ga, "exec_string") if + conf.has_option(ga, "exec_string") else ""), ")", + (" [password]" if conf.has_option(ga, "password") else "")]) + resultstring.append(result) + try: + aliases.remove(ga) + except ValueError: + pass + for a in aliases: + resultalias.append(a) + result = "".join([str(a), " (", (conf.get(a, "exec_string") if + conf.has_option(a, "exec_string") else ""), ")", + (" [password]" if conf.has_option(a, "password") else "")]) + resultstring.append(result) + if strings: + return resultstring; + else: + return resultalias; + def CMDFullList(option, opt, value, parser): - for p in conf.sections(): - to_print = "".join([str(p), " - ", (conf.get(p, "exec_string") if - conf.has_option(p, "exec_string") else ""), - (" [password]" if conf.has_option(p, "password") else "")]) - print(to_print) + for p in GetTreeList(): + print (p) def CursesConnect(screen, aliases, command=False): curses.endwin() + groups = [] + connectaliases = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) for alias in aliases: + if alias in groups: + group_aliases = conf.get(alias, "group").split() + for ga in group_aliases: + if not ga in connectaliases: + connectaliases.append(ga) + else: + if not alias in connectaliases: + connectaliases.append(alias) + for alias in connectaliases: print("Connecting to " + alias + "...") ConnectAlias(alias, command) print("... " + alias + " session finished.") @@ -259,24 +368,29 @@ def CMDOptions(): opts.add_option('-a', '--add', action="store", type="string", dest="add", metavar="alias", default=False, help="add new alias for connection string") + opts.add_option('-g', '--group', action="store", type="string", + dest="group", metavar="group", default=False, + help="add new group for aliases") opts.add_option('-c', '--command', action="store", type="string", dest="command", metavar="command", default=False, - help="add command for executing alias") + help="execute command for executing aliases or group") opts.add_option('-k', '--keep', action="store", type="string", dest="keep", metavar="alias", default=False, help="hold connection with specified alias") opts.add_option('-e', '--edit', action="store", type="string", dest='edit', metavar="alias", default=False, - help="edit existing connection string") + help="edit existing alias or group") opts.add_option('-p', '--password', action="store", type="string", dest='password', metavar="alias", default=False, help="set and store password for sshpass [UNSAFE]") opts.add_option('-r', '--remove', action="store", type="string", dest='remove', metavar="alias", default=False, - help="remove existing alias of connection string") + help="remove existing alias or group") options, alias = opts.parse_args() if options.add: CMDAdd(options.add) + if options.group: + CMDGroup(options.group) if options.edit: CMDEdit(options.edit) if options.password: @@ -295,18 +409,32 @@ def CursesMain(): help_screen = ("".join([" Press:\n", " 'z'/'x' or arrows - navigation\n", " 'a'/'F2' - add new alias\n", - " 'e'/'F4' - edit existing alias\n", + " 'g'/'F5' - add new group\n", + " 'e'/'F4' - edit existing alias/group\n", " 'p'/'F6' - set alias's password for sshpass [UNSAFE]\n", " 'space'/'insert' - select\n", " 'r'/'F8' - remove selected alias/aliases\n", " 'c'/'F3' - execute specific command with selected alias/aliases\n", " 'k'/'F7' - hold connection with selected alias\n", - " 'enter'/'F9' - connect to selected alias/aliases\n", + " 'enter'/'F9' - connect to selected alias/aliases,\n", + " expand/collapse group\n", " 'q'/'F10' - quit\n", " Run program with '--help' option to view command line help.\n", " Also, you can edit config file manually:\n", " ", conf_file])) - strings = conf.sections() + if expand_default == True: + groups = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + expanded = groups + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) + elif expand_default == False: + groups = [] + expanded = groups + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) row_num = len(strings) selected_strings = [" " for i in range(0, row_num + 1)] screen = curses.initscr() @@ -333,14 +461,7 @@ def CursesMain(): box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", width - 6, highlight_text) else: - if conf.has_option(strings[i - 1], "password"): - password = " [password]" - else: - password = "" - exec_string = ["[", selected_strings[i], "] ", str(i), " ", - strings[i - 1], " (", (conf.get(strings[i - 1], - "exec_string") if conf.has_option(strings[i - 1], - "exec_string") else ""), ")", password] + exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]] if (i == position): box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text) else: @@ -382,34 +503,95 @@ def CursesMain(): CursesTextpadConfirm) SetAliasString(add_alias.rstrip(), add_string.replace("\n", "").rstrip()) - strings = conf.sections() + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) row_num = len(strings) selected_strings.append(" ") pages = int(ceil(row_num / max_row)) box.refresh() + if key_pressed == ord('g') or key_pressed == ord( + 'G') or key_pressed == curses.KEY_F5: + new_group_textpad = CursesTextpad(screen, 1, width - 8, + (height // 2) - 1, 4, "Enter name for new group:", "", + normal_text, highlight_text) + add_group = new_group_textpad.edit(CursesTextpadConfirm) + if not add_group.rstrip() == "": + add_result = AddNewAlias(add_group.rstrip()) + if not add_result: + CursesPanel(screen, 3, + width - 6, (height // 2) - 1, 3, add_result, + normal_text, highlight_text) + else: + add_string = "" + while add_string.rstrip() == "": + string_textpad = CursesTextpad(screen, 3, + width - 8, (height // 2) - 1, 4, + "Enter aliases for new group (example: alias1 alias2):", + "", normal_text, highlight_text) + add_string = string_textpad.edit( + CursesTextpadConfirm) + SetGroupString(add_group.rstrip(), + add_string.replace("\n", "").rstrip()) + expanded.append(add_group.rstrip()) + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) + row_num = len(strings) + selected_strings = [" " for i in range(0, row_num + 1)] + pages = int(ceil(row_num / max_row)) + box.refresh() if (key_pressed == ord('e') or key_pressed == ord( 'E') or key_pressed == curses.KEY_F4) and row_num != 0: edit_string = "" - while edit_string.rstrip() == "": - string_textpad = CursesTextpad(screen, 3, width - 8, - (height // 2) - 1, 4, "Enter new execution string:", - (conf.get(strings[position - 1], - "exec_string") if conf.has_option(strings[position - 1], - "exec_string") else ""), - normal_text, highlight_text) - edit_string = string_textpad.edit(CursesTextpadConfirm) - SetAliasString(strings[position - 1], - edit_string.replace("\n", "").rstrip()) - strings = conf.sections() + groups = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if strings[position - 1] in groups: + while edit_string.rstrip() == "": + string_textpad = CursesTextpad(screen, 3, width - 8, + (height // 2) - 1, 4, "Enter new aliases:", + (conf.get(strings[position - 1], + "group") if conf.has_option(strings[position - 1], + "group") else ""), + normal_text, highlight_text) + edit_string = string_textpad.edit(CursesTextpadConfirm) + SetGroupString(strings[position - 1], + edit_string.replace("\n", "").rstrip()) + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) + row_num = len(strings) + selected_strings.append(" ") + pages = int(ceil(row_num / max_row)) + box.refresh() + else: + while edit_string.rstrip() == "": + string_textpad = CursesTextpad(screen, 3, width - 8, + (height // 2) - 1, 4, "Enter new execution string:", + (conf.get(strings[position - 1].split()[0].strip(), + "exec_string") if conf.has_option(strings[position - 1].split()[0].strip(), + "exec_string") else ""), + normal_text, highlight_text) + edit_string = string_textpad.edit(CursesTextpadConfirm) + SetAliasString(strings[position - 1].split()[0].strip(), + edit_string.replace("\n", "").rstrip()) + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) if (key_pressed == ord('p') or key_pressed == ord( 'P') or key_pressed == curses.KEY_F6) and row_num != 0: - set_password = "" - set_password = CursesPanel(screen, 4, width - 6, - (height // 2) - 1, 3, - " Enter user password for sshpass and press 'enter':\n>", - normal_text, highlight_text, "password") - if not set_password == "": - SetPassword(strings[position - 1], set_password) + groups = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if not strings[position - 1].split()[0].strip() in groups: + set_password = "" + set_password = CursesPanel(screen, 4, width - 6, + (height // 2) - 1, 3, + " Enter user password for sshpass and press 'enter':\n>", + normal_text, highlight_text, "password") + if not set_password == "": + SetPassword(strings[position - 1].split()[0].strip(), set_password) + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) if (key_pressed == ord('r') or key_pressed == ord( 'R') or key_pressed == curses.KEY_F8 or key_pressed == ( curses.KEY_DC)) and row_num != 0: @@ -423,14 +605,15 @@ def CursesMain(): str(len(selected)), " selected aliases? (y/N)"])) else: remove_confirm = ("".join(["Are you sure to remove '", - strings[position - 1], "' alias? (y/N)"])) - selected.append(strings[position - 1]) + strings[position - 1].split()[0].strip(), "' alias? (y/N)"])) + selected.append(strings[position - 1].split()[0].strip()) remove_result = CursesPanel(screen, 4, width - 6, (height // 2) - 1, 3, remove_confirm, normal_text, highlight_text, "remove") if remove_result == "confirm": RemoveAliases(selected) - strings = conf.sections() + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) row_num = len(strings) selected_strings = [" " for i in range(0, row_num + 1)] pages = int(ceil(row_num / max_row)) @@ -444,7 +627,7 @@ def CursesMain(): if selected_strings[i] == "*": selected.append(strings[i - 1]) if not len(selected) > 0: - selected.append(strings[position - 1]) + selected.append(strings[position - 1].split()[0].strip()) command_textpad = CursesTextpad(screen, 3, width - 8, (height // 2) - 1, 4, "".join([ @@ -456,17 +639,38 @@ def CursesMain(): command_string.replace("\n", "").rstrip()) if (key_pressed == ord('k') or key_pressed == ord('K') or key_pressed == (curses.KEY_F7)) and row_num != 0: - curses.endwin() - HoldConnection(strings[position - 1]) + groups = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if not strings[position - 1] in groups: + curses.endwin() + HoldConnection(strings[position - 1].split()[0].strip()) if (key_pressed == ord("\n") or key_pressed == ( curses.KEY_F9)) and row_num != 0: - selected = [] - for i in range(1, row_num + 1): - if selected_strings[i] == "*": - selected.append(strings[i - 1]) - if not len(selected) > 0: - selected.append(strings[position - 1]) - CursesConnect(screen, selected) + groups = [] + for a in conf.sections(): + if conf.has_option(a, "group"): + groups.append(a) + if strings[position - 1] in groups: + if strings[position - 1] in expanded: + expanded.remove(strings[position - 1]) + else: + expanded.append(strings[position - 1]) + strings = GetTreeList(False, expanded) + stringsfull = GetTreeList(True, expanded) + row_num = len(strings) + selected_strings.append(" ") + pages = int(ceil(row_num / max_row)) + box.refresh() + else: + selected = [] + for i in range(1, row_num + 1): + if selected_strings[i] == "*": + selected.append(strings[i - 1]) + if not len(selected) > 0: + selected.append(strings[position - 1].split()[0].strip()) + CursesConnect(screen, selected) if (key_pressed == 32 or key_pressed == ( curses.KEY_IC)) and row_num != 0: if selected_strings[position] == ' ': @@ -537,14 +741,7 @@ def CursesMain(): box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.", width - 6, highlight_text) else: - if conf.has_option(strings[i - 1], "password"): - password = " [password]" - else: - password = "" - exec_string = ["[", selected_strings[i], "] ", str(i), " ", - strings[i - 1], " (", (conf.get(strings[i - 1], - "exec_string") if conf.has_option(strings[i - 1], - "exec_string") else ""), ")", password] + exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]] if (i + (max_row * (page - 1)) == (position + (max_row * (page - 1)))): box.addnstr(i - (max_row * (page - 1)), 2, "".join( exec_string), width - 6, highlight_text)