Compare commits

...

48 Commits
v0.5 ... master

Author SHA1 Message Date
ivan 8ab5a13cca exception handling when a session subprocess is interrupted (f.e. ctr+c on password entry) 2024-04-05 14:28:53 +03:00
ivan fc9640a491 Merge branch 'fix-escape-sequence' into 'master'
Fix escape sequence

See merge request zlax/sshch!2
2024-04-05 11:18:39 +00:00
Anton Samofal 8ee868ab69 Fix escape sequence 2024-04-05 11:18:39 +00:00
ivan 2c5c87f555 add curses texpad del, home, end handlers 2022-06-17 21:55:01 +03:00
ivan cd501bdc1e add curses texpad Esc handler 2022-06-14 20:36:15 +03:00
ivan 37b3926880 merging CMDConnect and CursesConnect into the single Connect function 2022-06-14 12:33:32 +03:00
ivan abb2fcfa4c remove ThreadConnect func and help fixes 2022-06-14 11:52:00 +03:00
ivan 7d7e2ff775 py2 curses utf8 display support 2022-06-12 16:20:59 +03:00
ivan eddfedb6c3 parallel command execution for aliases and groups 2022-06-11 11:51:19 +03:00
ivan e11978957f parallel command execution for aliases and groups 2022-06-11 11:46:47 +03:00
ivan 032b4b1ab0 Add w/s as navigation 2021-09-22 18:58:18 +03:00
ivan 7fa8bdb6f9 Fix -f cmd output 2021-09-22 18:19:17 +03:00
ivan 10ca6ad15b Add GroupChildAliases function 2021-09-22 17:39:54 +03:00
ivan 48d491312a Add group tree (nested groups) 2021-09-22 16:31:11 +03:00
ivan 0bbf06340a Update 'README.md' 2019-09-05 13:42:24 +00:00
ivan ed10aa8f03 add exceptions 2018-11-28 20:46:18 +03:00
ivan 0fd4ff9b74 issue 10 fix 2018-11-28 14:30:35 +03:00
ivan a39cd7ae68 interface fixes 2018-11-28 14:27:06 +03:00
ivan ba9fa303ba newlines in readme 2018-04-23 22:23:54 +03:00
ivan 1b9acdcc4a first stable release 2018-04-23 22:07:22 +03:00
ivan b7fdcf119d blank password for remove password field 2018-04-22 19:53:12 +03:00
ivan b48081be65 fixed TypeError: must be str, not bytes with python3 2018-04-22 18:10:22 +03:00
ivan 32013bdb8d Add home/end handlers, some minor fixes 2018-04-22 15:02:36 +03:00
ivan 450b30b196 fixed 'selected' list out of range 2018-04-08 16:27:16 +03:00
ivan 8e930115a5 fixed add new group name string 2018-04-08 12:33:27 +03:00
ivan ec8a2ca14a minor fixes 2018-04-08 12:23:29 +03:00
ivan 9c361bbe00 removed possibility to create alias and group with spaces 2018-04-08 12:12:04 +03:00
ivan 37a20a0c48 add cursor to curses 2018-04-08 11:31:20 +03:00
ivan e430142fdc groups added 2018-04-05 00:31:37 +03:00
ivan c8069554eb
Merge pull request #5 from GHPS/ZSH-Completion
Added full ZSH completion
2018-01-20 11:02:54 +03:00
GHPS 6fdcf8ae6d Added full ZSH completion 2018-01-19 16:20:33 +01:00
ivan c528af6414
Merge pull request #4 from GHPS/Better-Scriptability
Better Scriptability by Removing Unnecessary Comma in List Output
2017-11-22 12:15:07 +03:00
GS 9ea667f201 Better Scriptability by Removing Unnecessary Comma in List Output 2017-11-21 21:24:24 +01:00
ivan 2c8f072499
Merge pull request #3 from GHPS/master
Added Python 3.x support
2017-11-20 09:57:30 +03:00
GHPS 68bb493c44
Added Python 3.x support 2017-11-19 23:34:25 +01:00
ivan e75a733d0b README fix 2017-09-13 12:05:17 +03:00
ivan c057e1e8ed Adding autocompetion instructions to README and fixing lack of 'exec_string' parameter crashing 2017-09-13 12:03:00 +03:00
ivan 0266d29308 Merge pull request #2 from jeff-99/master
added bash autocompletion script for sshch aliases
2017-09-12 20:48:32 +03:00
Jeffrey Slort de2f544178 added bash autocompletion script for sshch aliases 2017-09-11 10:53:03 +02:00
ivan 083144d656 add quotes for sshpass (for & symbol in password) 2017-09-05 15:16:23 +03:00
ivan afa565104c missprint 2017-07-25 17:39:18 +03:00
ivan d5844ab267 add -k feature (hold connection) 2017-07-25 17:21:54 +03:00
ivan 48eea214fa Merge pull request #1 from Difrex/master
Code cleanup
2017-07-25 16:48:14 +03:00
Denis Zheleztsov 99374da328 PEP8 2017-07-25 16:39:02 +03:00
ivan 37b6f6d5e0 fixed screenshot 2017-07-25 13:39:45 +03:00
ivan 4fb25041f9 misprint in readme 2017-07-25 13:10:53 +03:00
ivan 4ec5c96775 add setup file and update readme 2017-07-25 13:07:47 +03:00
ivan 980f3ac9be add screenshot 2017-07-25 12:47:03 +03:00
8 changed files with 1148 additions and 527 deletions

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
sshch
sshch is released under the DWTW license
This program is free software; you can redistribute it and/or modify it under the terms of the Do What Thou Wilt License.
Boundless Public License
DO WHAT THOU WILT
TO PUBLIC LICENSE
Version 2.55
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it in full or in part is allowed without any restrictions.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. Do what thou wilt shall be the whole of the Law.
DWTWL a license with a single requirement: DO WHAT THOU WILT
The license provides more freedom than any other one (such as GPL or BSD) and does not require saving the license text on copying.
DWTWL an accomplished and eligible license for free text, code and any other symbols (including the software, documentation and artwork).
The license does not contain a "no warranty" clause. DWTWL can be used in countries that do not legally acknowledge the transition to public domain.
Summary:
An author-creator gives their source code to the world for free, without becoming distracted by worldly thinking regarding how and why the others will use it.

View File

@ -1,3 +1,49 @@
Simpe python script for fast access to ssh hosts.
SSH connection and aliases manager with curses and command line interface
======
sshch is released under DWTWL 2.55 license
under DWTWL 2.5 license: https://soundragon.su/license/license.html
sshch compatible with pyhon2 and python3, no additional libraries are required
### Screenshot
![sshch](https://dev.ussr.win/zlax/sshch/raw/branch/master/sshch_screenshot.png)
### Installing
**You can install a release version from pip:**
```bash
pip install sshch
```
**Manual installation from the package or git repository also available:**
To install for all users:
```bash
sudo python setup.py install
```
To install just for current user:
```bash
mkdir ~/.local/bin
cp sshch/sshch ~/.local/bin/
```
### Using
To run curses interface:
```bash
sshch
```
To run command line help:
```bash
sshch -h
```
For exit from current ssh session press `Ctrl+D`.
**Additional Features**
- If you want to use unsafe 'password' feature you must install `sshpass` first.
- If you want to use bash autocompletion function with sshch, copy autocompletion script to /etc/bash_completion.d/:
```bash
sudo cp completion/sshch_bash_completion.sh /etc/bash_completion.d/sshch
```
(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'

View File

@ -0,0 +1,11 @@
_sshch_complete()
{
local cur_word alias_list
cur_word="${COMP_WORDS[COMP_CWORD]}"
alias_list=`sshch -l`
COMPREPLY=($(compgen -W "$alias_list" -- $cur_word))
return 0
}
complete -F _sshch_complete sshch

View File

@ -0,0 +1,27 @@
#compdef sshch
#
# ZSH Completion for SSHCH
# Usage:
# 1) Place File in a Directory where ZSH can find it
# -> Search Path is Stored in $fpath
# -> echo $fpath
# 2) Rename File to '_sshch'
#
_arguments '::aliasname:->getAlias' \
'-e[Edit Alias]:aliasname:->getAlias' '--edit[Edit Alias]:aliasname:->getAlias' \
'-p[Set Password]:aliasname:->getAlias' '--password[Set Password]:aliasname:->getAlias' \
'-r[Remove Alias]:aliasname:->getAlias' '--remove[Remove Alias]:aliasname:->getAlias' \
'-k[Keep Connection]:aliasname:->getAlias' '--keep[Keep Connection]:aliasname:->getAlias' \
'-a[Add Alias]' '--add[Add Alias]'\
'-c[Add Command for Executing Alias]' '--command[Add Command for Executing Alias]'\
'-h[Show Help Message]' '--help[Show Help Message]'\
'-l[List Existing Alias]' '--list[List Existing Alias]'\
'-f[List Existing Alias with Connection String]' '--fulllist[List Existing Alias with Connection String]'\
'--version[Show Program Version]'
case "$state" in
getAlias)
local -a alias_list
alias_list=($(sshch -l))
_values -s ' ' 'Aliases' $alias_list
;;
esac

40
setup.py Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
"""Setup script for sshch"""
def main():
from distutils.core import setup
setup(name='sshch',
author='zlaxy',
author_email='zlaxyi@gmail.com',
url='https://gitlab.com/zlax/sshch',
description='Ssh connection and aliases manager',
long_description='SSH connection and aliases manager with curses and command line interface',
long_description_content_type='text/x-rst',
license='DWTWL 2.55',
version='1.09.7',
py_modules=['sshch'],
scripts=['sshch/sshch'],
keywords='sshch ssh aliases curses manager',
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4',
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console :: Curses',
'Intended Audience :: System Administrators',
'License :: Freeware',
'Natural Language :: English',
'Operating System :: POSIX',
'Programming Language :: Python',
'Topic :: Internet',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration',
'Topic :: Utilities'])
return 0
if __name__ == '__main__':
import sys
sys.exit(main())

525
sshch.py
View File

@ -1,525 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from os import path
from sys import argv
from math import *
from getpass import getpass
from optparse import OptionParser
from curses import textpad, panel
import ConfigParser
import subprocess
import base64
import curses
# https://github.com/zlaxy/sshch
version="0.5"
# path to conf file, default: ~/.config/sshch.conf
conf_file = path.expanduser("~") + '/.config/sshch.conf'
def AddNewAlias(alias):
if not conf.has_section(alias):
conf.add_section(alias)
conf.write(open(conf_file, "w"))
return True
else:
return "error: '" + alias + "' already exists"
def SetAliasString(alias, string):
conf.set(alias, "exec_string", string)
conf.write(open(conf_file, "w"))
def SetPassword(alias, string):
string = base64.b64encode(base64.b16encode(
base64.b32encode(string)))
conf.set(alias, "password", string)
conf.write(open(conf_file, "w"))
def RemoveAliases(aliases):
for alias in aliases:
conf.remove_section(alias)
conf.write(open(conf_file, "w"))
def ConnectAlias(alias, command=False):
print "Connecting to " + alias + "..."
exec_string = ""
if conf.has_option(alias, "password"):
password = base64.b32decode(base64.b16decode(
base64.b64decode(conf.get(alias, "password"))))
exec_string = "sshpass -p " + password + " "
exec_string = exec_string + conf.get(alias, "exec_string")
if command:
exec_string = exec_string + " " + command
p = subprocess.Popen(exec_string, shell=True)
streamdata = p.communicate()[0]
print "... " + alias + " session finished."
def CMDAdd(alias):
result = AddNewAlias(alias)
if result == True:
prompt_add = ("Enter connection string for new alias " +
"(example: ssh user@somehost.com):\n")
string = ""
while string == "":
string = raw_input (prompt_add)
SetAliasString(alias, string)
else:
print result
def CMDEdit(alias):
if conf.has_section(alias):
prompt_edit = ("Enter connection string for existing alias " +
"(example: ssh user@somehost.com):\n")
string = ""
while string == "":
string = raw_input (prompt_edit)
SetAliasString(alias, string)
else:
print "error: '" + alias + "' alias is 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)
else:
print "error: '" + alias + "' alias is not exists"
def CMDRemove(alias):
if conf.has_section(alias):
prompt_remove = ("Type 'yes' if you sure to remove '" +
alias + "' alias: ")
string = raw_input (prompt_remove)
if string == "yes":
RemoveAliases([alias])
else:
print "'" + alias + "' alias was not deleted."
else:
print "error: '" + alias + "' alias is not exists."
def CMDConnect(aliases, command=False):
for alias in aliases:
if conf.has_section(alias):
ConnectAlias(alias, command)
else:
print "error: '" + alias + "' alias is not exists"
def CMDList(option, opt, value, parser):
print ', '.join(str(p) for p in conf.sections())
def CMDFullList(option, opt, value, parser):
print '\n'.join((str(p) + " - " + conf.get(p, "exec_string") +
(" [password]" if conf.has_option(p, "password") else ""))
for p in conf.sections())
def CursesConnect(screen, aliases, command=False):
curses.endwin()
for alias in aliases:
ConnectAlias(alias, command)
print "Press 'enter' to continue."
screen.getch()
def CursesExit(error=False):
curses.endwin()
if error: print error
exit()
def CursesTextpadConfirm(value):
if value == 10:
value = 7
return value
def CursesTextpad(screen, h, w, y, x, title="", value="",
text_colorpair=0, deco_colorpair=0):
new_window = curses.newwin(h + 3, w + 2, y - 1, x - 1)
title_window = new_window.subwin(1, w , y , x)
title_window.addnstr(0, 0, title, w , text_colorpair)
title_window.refresh()
sub_window = new_window.subwin(h, w, y + 1, x)
textbox_field = textpad.Textbox(sub_window, insert_mode=True)
new_window.attron(deco_colorpair)
new_window.box()
new_window.attroff(deco_colorpair)
new_window.refresh()
sub_window.addnstr(0, 0 , value, h * w, text_colorpair)
sub_window.attron(text_colorpair)
return textbox_field
def CursesPanel(screen, h, w, y, x, text,
text_colorpair=0, deco_colorpair=0, confirm=0):
new_window = curses.newwin(h, w, y, x)
new_window.erase()
new_window.attron(deco_colorpair)
new_window.box()
new_window.attroff(deco_colorpair)
sub_window = new_window.subwin(h - 2, w - 2 , y + 1 , x + 1)
sub_window.insstr(0, 0, text)
# sub_window.addnstr(0, 0, text, ((h - 2) * (w - 2) - 1))
panel = curses.panel.new_panel(new_window)
curses.panel.update_panels()
screen.refresh()
if confirm == "password":
hidden_password = ""
keych = ""
position = 2
while True:
keych = screen.getch()
if keych == ord("\n"):
break
if keych == 27:
hidden_password = ""
break
if keych == curses.KEY_BACKSPACE:
if position > 2:
if position == len(hidden_password) + 2:
sub_window.addstr(1, position - 1, " ",
text_colorpair)
sub_window.refresh()
position = position - 1
hidden_password = hidden_password[0:-1]
if keych > 31 and keych < 127:
hidden_password += curses.keyname(keych)
sub_window.addstr(1, position, "*", text_colorpair)
if position < w - 4:
position += 1
sub_window.refresh()
return hidden_password
elif confirm == "remove":
keych = screen.getch()
if keych == ord("y") or keych == ord("Y"):
return "confirm"
return False
else:
screen.getch()
def CMDOptions():
class FormatedParser(OptionParser):
def format_epilog(self, formatter):
return self.epilog
usage = "usage: %prog [options] [aliases]"
progname = path.basename(__file__)
epilog = ("Examples:\n " +
progname + " existingalias\n " +
progname + " -a newremoteserver\n " +
progname + " --edit=newremoteserver -p newremoteserver\n " +
progname + ' -c "ls -l" newremoteserver\n ' +
progname + " -c reboot existingalias newremoteserver\n"
"Examples of connection string:\n " +
"ssh user@somehost.com\n " +
"ssh gates@8.8.8.8 -p 667\n " +
"ssh root@somehost.com -t tmux a\n" +
"Also, you can edit config file manually: " + conf_file + "\n")
opts = FormatedParser(usage=usage, version="%prog " + version,
epilog=epilog)
opts.add_option('-l', '--list', action = "callback",
callback=CMDList, help="show list of all existing aliases")
opts.add_option('-f', '--fulllist', action = "callback",
callback=CMDFullList, help=("show list of all existing " +
"aliases with connection strings"))
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('-c', '--command', action="store", type="string",
dest="command", metavar="command", default=False,
help="add command for executing alias")
opts.add_option('-e', '--edit', action="store", type="string",
dest='edit', metavar="alias", default=False,
help="edit existing connection string")
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")
options, alias = opts.parse_args()
if options.add: CMDAdd(options.add)
if options.edit: CMDEdit(options.edit)
if options.password: CMDPassword(options.password)
if options.remove: CMDRemove(options.remove)
if alias: CMDConnect(alias, options.command)
# curses template from: https://stackoverflow.com/a/30828805/6224462
def CursesMain():
help_screen = (" Press:\n" +
" 'z'/'x' or arrows - navigation\n" +
" 'a'/'F2' - add new alias\n" +
" 'e'/'F4' - edit existing alias\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" +
" 'enter'/'F9' - connect to selected alias/aliases\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()
row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)]
screen = curses.initscr()
curses.noecho()
curses.cbreak()
curses.start_color()
screen.keypad(1)
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN)
highlight_text = curses.color_pair(1)
normal_text = curses.A_NORMAL
height, width = screen.getmaxyx()
screen.border(0)
curses.curs_set(0)
max_row = height - 5
screen.addnstr(1, 2, "sshch " + version + ", press 'h' for help",
width - 4)
box = curses.newwin(max_row + 2, width - 2, 2, 1)
box.box()
pages = int(ceil(row_num / max_row))
position = 1
page = 1
for i in range(1, max_row + 1):
if row_num == 0:
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a'" +
" to add new one.", width - 6, highlight_text)
else:
if (i == position):
box.addnstr(i, 2, "[" + selected_strings[i] + "] " +
str(i) + " " + strings[i - 1] + " (" +
conf.get(strings[i - 1], "exec_string") + ")" +
(" [password]" if conf.has_option(strings[i - 1],
"password") else ""), width - 6, highlight_text)
else:
box.addnstr(i, 2, "[" + selected_strings[i] + "] " +
str(i) + " " + strings[i - 1] + " (" +
conf.get(strings[i - 1], "exec_string") + ")" +
(" [password]" if conf.has_option(strings[i - 1],
"password") else ""), width - 6, normal_text)
if i == row_num:
break
screen.refresh()
box.refresh()
key_pressed = screen.getch()
while True:
if key_pressed == ord('q') or key_pressed == ord(
'Q') or key_pressed == (
27) or key_pressed == curses.KEY_F10:
CursesExit()
if key_pressed == ord('h') or key_pressed == ord(
'H') or key_pressed == curses.KEY_F1:
CursesPanel(screen, height - 4, width - 6, 2, 3,
help_screen, normal_text, highlight_text)
if key_pressed == ord('a') or key_pressed == ord(
'A') or key_pressed == curses.KEY_F2:
new_alias_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter new alias:", "",
normal_text, highlight_text)
add_alias = new_alias_textpad.edit(CursesTextpadConfirm)
if not add_alias.rstrip() == "":
add_result = AddNewAlias(add_alias.rstrip())
if not add_result == True: 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 full execution string:",
"ssh ", normal_text, highlight_text)
add_string = string_textpad.edit(
CursesTextpadConfirm)
SetAliasString(add_alias.rstrip(),
add_string.replace("\n", "").rstrip())
strings = conf.sections()
row_num = len(strings)
selected_strings.append(" ")
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"),
normal_text, highlight_text)
edit_string = string_textpad.edit(CursesTextpadConfirm)
SetAliasString(strings[position - 1],
edit_string.replace("\n", "").rstrip())
strings = conf.sections()
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)
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:
selected = []
for i in range(1, row_num + 1):
if selected_strings[i] == "*":
selected.append(strings[i - 1])
if len(selected) > 0:
remove_confirm = ("Are you sure to remove " +
str(len(selected)) + " selected aliases? (y/N)")
else:
remove_confirm = ("Are you sure to remove '" +
strings[position - 1] + "' alias? (y/N)")
selected.append(strings[position - 1])
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()
row_num = len(strings)
selected_strings = [" " for i in range(0, row_num + 1)]
pages = int(ceil(row_num / max_row))
position = 1
page = 1
box.refresh()
if (key_pressed == ord('c') or key_pressed == ord(
'C') or key_pressed == curses.KEY_F3) 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])
command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4,
"Enter specific command to execute with selected " +
"alias/aliases:", "", normal_text, highlight_text)
command_string = command_textpad.edit(CursesTextpadConfirm)
CursesConnect(screen, selected,
command_string.replace("\n", "").rstrip())
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)
if (key_pressed == 32 or key_pressed == (
curses.KEY_IC)) and row_num != 0:
if selected_strings[position] == ' ':
selected_strings[position] = '*'
else: selected_strings[position] = ' '
if page == 1:
if position < i:
position = position + 1
else:
if pages > 1:
page = page + 1
position = 1 + (max_row * (page - 1))
elif page == pages:
if position < row_num:
position = position + 1
else:
if position < max_row + (max_row * (page - 1)):
position = position + 1
else:
page = page + 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_DOWN or key_pressed == ord(
'x') or key_pressed == ord('X'):
if page == 1:
if position < i:
position = position + 1
else:
if pages > 1:
page = page + 1
position = 1 + (max_row * (page - 1))
elif page == pages:
if position < row_num:
position = position + 1
else:
if position < max_row + (max_row * (page - 1)):
position = position + 1
else:
page = page + 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_UP or key_pressed == ord(
'z') or key_pressed == ord('Z'):
if page == 1:
if position > 1:
position = position - 1
else:
if position > (1 + (max_row * (page - 1))):
position = position - 1
else:
page = page - 1
position = max_row + (max_row * (page - 1))
if key_pressed == curses.KEY_LEFT or (key_pressed ==
curses.KEY_PPAGE):
if page > 1:
page = page - 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_RIGHT or (key_pressed ==
curses.KEY_NPAGE):
if page < pages:
page = page + 1
position = (1 + (max_row * (page - 1)))
box.erase()
screen.border(0)
box.border(0)
for i in range(1 + (max_row * (page - 1)), max_row + 1 +
(max_row * (page - 1))):
if row_num == 0:
box.addnstr(1, 1, "There aren't any aliases yet. " +
"Press 'a' to add new one.", width - 6,
highlight_text)
else:
if (i + (max_row * (page - 1)) == (position +
(max_row * (page - 1)))):
box.addnstr(i - (max_row * (page - 1)), 2, "[" +
selected_strings[i] + "] " + str(i) + " " +
strings[i - 1] + " (" + conf.get(strings[i - 1],
"exec_string") + ")" + (" [password]" if
conf.has_option(strings[i - 1], "password") else
""), width - 6, highlight_text)
else:
box.addnstr(i - (max_row * (page - 1)), 2, "[" +
selected_strings[i] + "] " + str(i) + " " +
strings[i - 1] + " (" + conf.get(strings[i - 1],
"exec_string") + ")" + (" [password]" if
conf.has_option(strings[i - 1], "password") else
""), width - 6, normal_text)
if i == row_num:
break
screen.refresh()
box.refresh()
key_pressed = screen.getch()
CursesExit()
if __name__ == "__main__":
conf = ConfigParser.RawConfigParser()
if not path.exists(conf_file):
open(conf_file, 'w')
conf.read(conf_file)
if len(argv) > 1:
try:
CMDOptions()
except KeyboardInterrupt:
exit()
except ConfigParser.Error:
print ("Error: can't parse your config file, please check" +
" it manually or make new one")
exit()
else:
try:
CursesMain()
except KeyboardInterrupt:
CursesExit()
except ConfigParser.NoOptionError:
CursesExit("Error: can't parse your config file, please " +
"check it manually or make new one")
except curses.error:
CursesExit("Error: can't show some curses element, maybe " +
"your terminal is too small")

993
sshch/sshch Executable file
View File

@ -0,0 +1,993 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
try:
import configparser
except ImportError:
import ConfigParser as configparser # Python 2.x import
import locale
locale.setlocale(locale.LC_ALL, '')
from os import path
from sys import argv
from math import ceil
from getpass import getpass
from optparse import OptionParser
import subprocess
import base64
import time
import curses
from curses import textpad, panel
from threading import Thread
# https://gitlab.com/zlax/sshch
version = "1.09.7"
# expand groups by default
expand_default = True
# path to conf dir and file, default: ~/.config/sshch.conf
conf_dir = path.expanduser("~") + '/.config'
conf_file = conf_dir + '/' + 'sshch.conf'
class GroupTree(object):
"""Group object with relatives information"""
def __init__(self, group):
self.group = group
self.aliases = []
self.children = []
self.parent = []
def AddNewAlias(alias):
if not conf.has_section(alias):
conf.add_section(alias)
conf.write(open(conf_file, "w"))
return True
else:
return "error: '" + alias + "' alias or group already exists"
def SetAliasString(alias, string):
conf.set(alias, "exec_string", 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):
if string == "" or string == b'':
conf.remove_option(alias, "password")
else:
string = string.encode()
string = base64.b64encode(base64.b16encode(
base64.b32encode(string)))
string = string.decode('utf-8')
conf.set(alias, "password", string)
conf.write(open(conf_file, "w"))
def RemoveAliases(aliases):
for alias in aliases:
conf.remove_section(alias)
conf.write(open(conf_file, "w"))
def ConvertPassword(password):
password_string = "'"
for char in password:
if char == "'":
password_string += "'"+'"'+"'"+'"'+"'"
elif char == '"':
password_string += "''"+'"'+"''"
elif char == ';':
password_string += "'"+r'\;'+"'"
elif char == "\\":
password_string += "'"+'"'+"\\"+"\\"+'"'+"'"
else:
password_string += char
password_string += "'"
return password_string
def Connect(aliases, command=False, threading=False, screen=False):
if screen:
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 = GroupChildAliases(alias)
for ga in group_aliases:
connectaliases.append(ga)
else:
connectaliases.append(alias)
connectaliases = setSeq(connectaliases)
threads = {}
for alias in connectaliases:
if conf.has_section(alias):
print("Connecting to " + alias + "...")
if threading:
threads[alias] = Thread(target=ConnectAlias, args=(alias,
command, True))
threads[alias].start()
else:
ConnectAlias(alias, command)
if not threading:
print("... " + alias + " session finished.")
else:
print("error: '" + alias + "' alias does not exists")
if screen:
print("Press 'enter' to continue.")
screen.getch()
def ConnectAlias(alias, command=False, threading=False):
exec_string = ""
if conf.has_option(alias, "password"):
password = base64.b32decode(base64.b16decode(
base64.b64decode(conf.get(alias, "password"))))
exec_string = "sshpass -p " + ConvertPassword(password.decode('utf-8')) + " "
if conf.has_option(alias, "exec_string"):
exec_string = exec_string + conf.get(alias, "exec_string")
if command:
exec_string = exec_string + " " + command
try:
subprocess.Popen(exec_string, shell=True).communicate()[0]
except:
pass
if threading:
print ("... "+alias+" session output finished.")
def HoldConnection(alias):
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):
alias = alias.split()[0].strip()
result = AddNewAlias(alias)
if result == True:
prompt_add = ("".join(["Enter connection string for new alias ",
"(example: ssh user@somehost.com):\n"]))
string = ""
while string == "":
string = input(prompt_add)
SetAliasString(alias, string)
else:
print(result)
def CMDGroup(group):
group = group.split()[0].strip()
result = AddNewAlias(group)
if result == True:
prompt_add = ("".join(["Enter aliases for new group ",
"(example: alias1 alias2):\n"]))
string = ""
while string == "":
string = input(prompt_add)
SetGroupString(group, string)
else:
print(result)
def CMDEdit(alias):
if conf.has_section(alias):
if conf.has_option(alias, "exec_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):
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:
if conf.has_section(alias):
prompt_pass = ("[UNSAFE] Enter password for sshpass (Ctrl+C" +
" - cancel, blank - clear password):\n")
string = ""
string = getpass(prompt_pass)
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 or group: ")
string = input(prompt_remove)
if string == "yes":
RemoveAliases([alias])
else:
print("'" + alias + "' alias or group was not deleted.")
else:
print("error: '" + alias + "' alias or group does not exists.")
def CMDList(option, opt, value, parser):
print(' '.join(str(p) for p in conf.sections()))
def setSeq(seq):
# py2&3 fastest way to remove duplicates from a list:
# https://www.rupython.com/x432-4-496.html
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
def GroupChildRecursion(group, childaliases, treelist):
if treelist[group].children:
for a in treelist[group].children:
GroupChildRecursion(a, childaliases, treelist)
if treelist[group].aliases:
for a in treelist[group].aliases:
childaliases.append(a)
return childaliases
def GroupChildAliases(group):
aliases = []
groups = []
rootgroups = []
treelist = {}
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
elif conf.has_option(a, "exec_string"):
aliases.append(a)
rootaliases = list(aliases)
for g in groups:
treelist[g] = GroupTree(g)
group_aliases = conf.get(g, "group").split()
for ga in group_aliases:
if ga in groups:
treelist[g].children.append(ga)
elif ga in aliases:
treelist[g].aliases.append(ga)
try:
rootaliases.remove(ga)
except ValueError:
pass
for g in groups:
if treelist[g].children:
for child in treelist[g].children:
treelist[child].parent.append(g)
for g in groups:
if not treelist[g].parent:
rootgroups.append(g)
if group == False:
return aliases, groups, rootaliases, rootgroups, treelist
else:
childaliases = GroupChildRecursion(group, [], treelist)
return setSeq(childaliases)
def GroupTreeRecursion(level, group, treelist, resultalias, resultstring,
expandlist, previousgroups):
if group in previousgroups:
return resultalias, resultstring
previousgroups.append(group)
resultalias.append(group)
if expandlist == True or group in expandlist:
resultstring.append(' '*(level-1)+">> "+group)
for g in treelist[group].children:
resultalias, resultstring = GroupTreeRecursion(level+1, g,
treelist, resultalias, resultstring, expandlist, previousgroups)
for ga in treelist[group].aliases:
if conf.has_option(ga, "exec_string"):
resultalias.append(ga)
result = "".join([' '*(level-1)+" ~ ", 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)
else:
resultstring.append(' '*(level-1)+"<> "+group)
return resultalias, resultstring
def GetTreeList(strings=True, expandlist=True):
resultalias = []
resultstring = []
aliases, groups, rootaliases, rootgroups, treelist = GroupChildAliases(False)
for g in rootgroups:
resultalias, resultstring = GroupTreeRecursion(1, g, treelist,
resultalias, resultstring, expandlist, [])
for a in rootaliases:
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 GetTreeList():
print (p)
def CursesExit(error=False):
curses.endwin()
if error:
print(error)
exit()
class CursesTextpadEsc(Exception):
"ESC key has been pressed"
def CursesTextpadConfirm(value):
if value == 10: # Enter
value = 7
elif value == 27: # Esc
raise CursesTextpadEsc()
elif value == curses.KEY_DC: # Del
value = curses.ascii.EOT
elif value == curses.KEY_HOME: # Home
value = curses.ascii.SOH
elif value == curses.KEY_END: # End
value = curses.ascii.ENQ
return value
def CursesTextpad(screen, h, w, y, x, title="", value="",
text_colorpair=0, deco_colorpair=0):
new_window = curses.newwin(h + 3, w + 2, y - 1, x - 1)
title_window = new_window.subwin(1, w, y, x)
title_window.addnstr(0, 0, title, w, text_colorpair)
title_window.refresh()
sub_window = new_window.subwin(h, w, y + 1, x)
textbox_field = textpad.Textbox(sub_window, insert_mode=True)
new_window.attron(deco_colorpair)
new_window.box()
new_window.attroff(deco_colorpair)
new_window.refresh()
sub_window.addnstr(0, 0, value, h * w, text_colorpair)
sub_window.attron(text_colorpair)
return textbox_field
def CursesPanel(screen, h, w, y, x, text,
text_colorpair=0, deco_colorpair=0, confirm=0):
new_window = curses.newwin(h, w, y, x)
new_window.erase()
new_window.attron(deco_colorpair)
new_window.box()
new_window.attroff(deco_colorpair)
sub_window = new_window.subwin(h - 2, w - 2, y + 1, x + 1)
sub_window.insstr(0, 0, text)
panel = curses.panel.new_panel(new_window)
curses.panel.update_panels()
screen.refresh()
if confirm == "password":
hidden_password = ""
keych = ""
position = 2
while True:
keych = screen.getch()
if keych == ord("\n"):
break
if keych == 27:
hidden_password = ""
break
if keych == curses.KEY_BACKSPACE:
if position > 2:
if position == len(hidden_password) + 2:
sub_window.addstr(1, position - 1, " ",
text_colorpair)
sub_window.refresh()
position = position - 1
hidden_password = hidden_password[0:-1]
if keych > 31 and keych < 127:
hidden_password += curses.keyname(keych).decode('utf-8')
sub_window.addstr(1, position, "*", text_colorpair)
if position < w - 4:
position += 1
sub_window.refresh()
return hidden_password
elif confirm == "remove":
keych = screen.getch()
if keych == ord("y") or keych == ord("Y"):
return "confirm"
return False
else:
screen.getch()
def CMDOptions():
class FormatedParser(OptionParser):
def format_epilog(self, formatter):
return self.epilog
usage = "usage: %prog [options] [aliases]"
progname = path.basename(__file__)
epilog = ("".join(["Examples:\n ",
progname, " existingalias\n ",
progname, " -a newremoteserver\n ",
progname, " --edit=newremoteserver -p newremoteserver\n ",
progname, ' -c "ls -l" newremoteserver\n ',
progname, " -c reboot existingalias newremoteserver\n",
"Examples of connection string:\n ",
"ssh user@somehost.com\n ",
"ssh gates@8.8.8.8 -p 667\n ",
"ssh root@somehost.com -t tmux a\n",
"Also, you can edit the config file manually: ", conf_file, "\n"]))
opts = FormatedParser(usage=usage, version="%prog " + version,
epilog=epilog)
opts.add_option('-l', '--list', action="callback",
callback=CMDList, help="show list of all existing aliases")
opts.add_option('-f', '--fulllist', action="callback",
callback=CMDFullList, help=("show list of all existing " +
"aliases with connection strings"))
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="execute command for aliases and groups")
opts.add_option('-t', '--thread', action="store", type="string",
dest="thread", metavar="command", default=False,
help="parallel command execution for aliases and groups " +
"(ssh key authentication or set password required)" )
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 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 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:
CMDPassword(options.password)
if options.remove:
CMDRemove(options.remove)
if options.keep:
HoldConnection(options.keep)
if options.thread:
Connect(alias, options.thread, True)
elif alias:
Connect(alias, options.command)
# curses template from: https://stackoverflow.com/a/30828805/6224462
def CursesMain():
help_screen = ("".join([" Press:\n",
" 'z'/'x', 'w'/'s' or arrows - navigation\n",
" 'a'/'F2' - add new alias (without spaces)\n",
" 'g'/'F5' - add new group (spaces will be stripped)\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' - command execution for alias (group/aliases - in turn)\n",
" 't'/'F11' - parallel command execution for aliases and groups\n",
" (ssh key authentication or set password required)\n",
" 'k'/'F7' - hold connection with selected alias\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 the config file manually:\n",
" ", conf_file]))
if expand_default == True:
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
expanded = list(groups)
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
elif expand_default == False:
groups = []
expanded = []
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()
curses.noecho()
curses.cbreak()
curses.start_color()
screen.keypad(1)
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN)
highlight_text = curses.color_pair(1)
normal_text = curses.A_NORMAL
height, width = screen.getmaxyx()
screen.border(0)
curses.curs_set(0)
max_row = height - 5
screen.addnstr(1, 2, "sshch " + version + ", press 'h' for help",
width - 4)
box = curses.newwin(max_row + 2, width - 2, 2, 1)
box.box()
pages = int(ceil(row_num / max_row))
position = 1
page = 1
for i in range(1, max_row + 1):
if row_num == 0:
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text)
else:
exec_string = ["[", selected_strings[i], "] ", stringsfull[i - 1]]
if (i == position):
box.addnstr(i, 2, "".join(exec_string), width - 6, highlight_text)
else:
box.addnstr(i, 2, "".join(exec_string), width - 6, normal_text)
if i == row_num:
break
screen.refresh()
box.refresh()
key_pressed = screen.getch()
while True:
if key_pressed == ord('q') or key_pressed == ord(
'Q') or key_pressed == (
27) or key_pressed == curses.KEY_F10:
CursesExit()
if key_pressed == ord('h') or key_pressed == ord(
'H') or key_pressed == curses.KEY_F1:
CursesPanel(screen, height - 4, width - 6, 2, 3,
help_screen, normal_text, highlight_text)
if key_pressed == ord('a') or key_pressed == ord(
'A') or key_pressed == curses.KEY_F2:
curses.curs_set(1)
new_alias_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter new alias:", "",
normal_text, highlight_text)
try:
add_alias = new_alias_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
add_alias = ""
if not add_alias == "":
add_alias = add_alias.split()[0].strip()
if not add_alias == "":
add_result = AddNewAlias(add_alias)
if add_result == True:
add_string = ""
try:
while add_string == "":
string_textpad = CursesTextpad(screen, 3,
width - 8, (height // 2) - 1, 4,
"Enter full execution string:",
"ssh ", normal_text, highlight_text)
add_string = string_textpad.edit(
CursesTextpadConfirm)
SetAliasString(add_alias,
add_string.replace("\n", "").rstrip())
except CursesTextpadEsc:
RemoveAliases([add_alias])
add_alias = ""
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:
curses.curs_set(0)
CursesPanel(screen, 3,
width - 6, (height // 2) - 1, 3, add_result,
normal_text, highlight_text)
curses.curs_set(0)
if key_pressed == ord('g') or key_pressed == ord(
'G') or key_pressed == curses.KEY_F5:
curses.curs_set(1)
new_group_textpad = CursesTextpad(screen, 1, width - 8,
(height // 2) - 1, 4, "Enter group name (without spaces):", "",
normal_text, highlight_text)
try:
add_group = new_group_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
add_group = ""
if not add_group == "":
add_group = add_group.split()[0].strip()
if not add_group == "":
add_result = AddNewAlias(add_group)
if add_result == True:
add_string = ""
try:
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,
add_string.replace("\n", "").rstrip())
expanded.append(add_group)
except CursesTextpadEsc:
RemoveAliases([add_group])
add_group = ""
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()
else:
curses.curs_set(0)
CursesPanel(screen, 3,
width - 6, (height // 2) - 1, 3, add_result,
normal_text, highlight_text)
curses.curs_set(0)
if (key_pressed == ord('e') or key_pressed == ord(
'E') or key_pressed == curses.KEY_F4) and row_num != 0:
edit_string = ""
groups = []
for a in conf.sections():
if conf.has_option(a, "group"):
groups.append(a)
curses.curs_set(1)
if strings[position - 1] in groups:
while edit_string.rstrip() == "":
string_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4, "Enter new aliases for existing group:",
(conf.get(strings[position - 1],
"group") if conf.has_option(strings[position - 1],
"group") else ""),
normal_text, highlight_text)
try:
edit_string = string_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
edit_string = conf.get(strings[position - 1],
"group") if conf.has_option(strings[position - 1],
"group") else ""
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)
try:
edit_string = string_textpad.edit(CursesTextpadConfirm)
except CursesTextpadEsc:
edit_string = conf.get(strings[position - 1].split()[0].strip(),
"exec_string") if conf.has_option(strings[position - 1].split()[0].strip(),
"exec_string") else ""
SetAliasString(strings[position - 1].split()[0].strip(),
edit_string.replace("\n", "").rstrip())
strings = GetTreeList(False, expanded)
stringsfull = GetTreeList(True, expanded)
curses.curs_set(0)
if (key_pressed == ord('p') or key_pressed == ord(
'P') or key_pressed == curses.KEY_F6) and row_num != 0:
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")
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:
selected = []
for i in range(1, row_num + 1):
if selected_strings[i] == "*":
selected.append(strings[i - 1])
if len(selected) > 0:
remove_confirm = (
"".join(["Are you sure to remove ",
str(len(selected)), " selected aliases? (y/N)"]))
else:
remove_confirm = ("".join(["Are you sure to remove '",
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 = 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))
position = 1
page = 1
box.refresh()
if (key_pressed == ord('c') or key_pressed == ord(
'C') or key_pressed == curses.KEY_F3) 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].split()[0].strip())
curses.curs_set(1)
command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4,
"".join([
"Enter specific command to execute with selected ",
"alias/aliases:"]
), "", normal_text, highlight_text)
try:
command_string = command_textpad.edit(CursesTextpadConfirm)
Connect(selected, command_string.replace("\n", "").rstrip(),
False, screen)
except CursesTextpadEsc:
command_string = ""
curses.curs_set(0)
if (key_pressed == ord('t') or key_pressed == ord(
'T') or key_pressed == curses.KEY_F11) 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].split()[0].strip())
curses.curs_set(1)
command_textpad = CursesTextpad(screen, 3, width - 8,
(height // 2) - 1, 4,
"".join([
"Enter specific command to execute with selected ",
"alias/aliases:"]
), "", normal_text, highlight_text)
try:
command_string = command_textpad.edit(CursesTextpadConfirm)
Connect(selected, command_string.replace("\n", "").rstrip(),
True, screen)
except CursesTextpadEsc:
command_string = ""
curses.curs_set(0)
if (key_pressed == ord('k') or key_pressed == ord('K') or
key_pressed == (curses.KEY_F7)) and row_num != 0:
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:
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())
Connect(selected, False, False, screen)
selected_strings = [" " for i in range(0, row_num + 1)]
if (key_pressed == 32 or key_pressed == (
curses.KEY_IC)) and row_num != 0:
if selected_strings[position] == ' ':
selected_strings[position] = '*'
else:
selected_strings[position] = ' '
if page == 1:
if position < i:
position = position + 1
else:
if pages > 1:
page = page + 1
position = 1 + (max_row * (page - 1))
elif page == pages:
if position < row_num:
position = position + 1
else:
if position < max_row + (max_row * (page - 1)):
position = position + 1
else:
page = page + 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_DOWN or key_pressed == ord(
'x') or key_pressed == ord('X') or key_pressed == ord(
's') or key_pressed == ord('S'):
if page == 1:
if position < i:
position = position + 1
else:
if pages > 1:
page = page + 1
position = 1 + (max_row * (page - 1))
elif page == pages:
if position < row_num:
position = position + 1
else:
if position < max_row + (max_row * (page - 1)):
position = position + 1
else:
page = page + 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_UP or key_pressed == ord(
'z') or key_pressed == ord('Z') or key_pressed == ord(
'w') or key_pressed == ord('W'):
if page == 1:
if position > 1:
position = position - 1
else:
if position > (1 + (max_row * (page - 1))):
position = position - 1
else:
page = page - 1
position = max_row + (max_row * (page - 1))
if key_pressed == curses.KEY_LEFT or (key_pressed ==
curses.KEY_PPAGE):
if page > 1:
page = page - 1
position = 1 + (max_row * (page - 1))
if key_pressed == curses.KEY_RIGHT or (key_pressed ==
curses.KEY_NPAGE):
if page < pages:
page = page + 1
position = (1 + (max_row * (page - 1)))
if key_pressed == curses.KEY_HOME:
page = 1
position = 1
if key_pressed == curses.KEY_END:
page = pages
position = row_num
box.erase()
screen.border(0)
box.border(0)
for i in range(1 + (max_row * (page - 1)), max_row + 1 +
(max_row * (page - 1))):
if row_num == 0:
box.addnstr(1, 1, "There aren't any aliases yet. Press 'a' to add new one.",
width - 6, highlight_text)
else:
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)
else:
box.addnstr(i - (max_row * (page - 1)), 2, "".join(
exec_string), width - 6, normal_text)
if i == row_num:
break
screen.refresh()
box.refresh()
key_pressed = screen.getch()
CursesExit()
if __name__ == "__main__":
try:
input = raw_input # Fix for Python 2.x
except NameError:
pass
if not path.exists(conf_dir):
try:
from os import makedirs
makedirs(conf_dir)
except:
print("Can't make dir " + conf_dir)
exit()
conf = configparser.RawConfigParser()
if not path.exists(conf_file):
try:
open(conf_file, 'w')
except:
print("Can't make file at " + conf_dir)
exit()
try:
conf.read(conf_file)
except:
print("Error: can't read config file " + conf_file)
exit()
if len(argv) > 1:
try:
CMDOptions()
except KeyboardInterrupt:
exit()
except configparser.Error:
print("Error: can't parse your config file, please check it manually or make new one")
exit()
except IOError:
print("Error: can't use your config file, please check permissionss of " + conf_file)
exit()
else:
try:
CursesMain()
except KeyboardInterrupt:
CursesExit()
except configparser.NoOptionError:
CursesExit("".join(["Error: can't parse your config file, please ",
"check it manually or make new one"]))
except curses.error:
CursesExit("".join(["Error: can't show some curses element, maybe ",
"your terminal is too small"]))
except IOError:
CursesExit("".join(["Error: can't use your config file, please ",
"check permissionss of ", conf_file]))

BIN
sshch_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB