├── man ├── license.url ├── readme.url ├── changelog.url ├── development.url ├── git_tutorial.url ├── monkeypatching │ ├── page_1.txt │ ├── page_2.txt │ ├── page_3.txt │ ├── page_4.txt │ └── page_5.txt ├── mounting │ ├── page_1.txt │ ├── page_2.txt │ ├── page_4.txt │ ├── page_5.txt │ ├── page_6.txt │ └── page_3.txt ├── grammar.txt └── manpages.txt ├── tests ├── cat │ ├── data │ │ ├── nonascii.txt │ │ ├── otherfile.txt │ │ └── somefile.txt │ ├── __init__.py │ └── test_cat.py ├── install │ └── __init__.py ├── lib │ ├── __init__.py │ ├── stashutils │ │ └── __init__.py │ └── test_libdist.py ├── md5sum │ ├── data │ │ ├── four.txt │ │ ├── five.txt │ │ ├── 5eb63bbbe01eeed093cb22bb8f5acdc3 │ │ ├── 6acbfd6f40c416db43890607b9cd9b7a │ │ ├── results.md5sum │ │ └── wrong_results.md5sum │ ├── __init__.py │ └── test_md5sum.py ├── sha1sum │ ├── data │ │ ├── four.txt │ │ ├── five.txt │ │ ├── 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed │ │ ├── 85e65a61b3f3a0fb03eed6ead8f88f78f1170129 │ │ ├── results.sha1sum │ │ └── wrong_results.sha1sum │ ├── __init__.py │ └── test_sha1sum.py ├── sha256sum │ ├── data │ │ ├── four.txt │ │ ├── five.txt │ │ ├── b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 │ │ ├── fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9 │ │ ├── results.sha256sum │ │ └── wrong_results.sha256sum │ ├── __init__.py │ └── test_sha256sum.py ├── ls │ ├── data │ │ ├── file1.txt │ │ ├── file2.txt │ │ ├── .hidden │ │ └── otherfile.txt │ └── __init__.py ├── pbcopy_pbpaste │ ├── __init__.py │ ├── data │ │ └── testfile.txt │ └── test_pbcopy_pbpaste.py ├── system │ ├── __init__.py │ ├── data │ │ ├── old_history.txt │ │ ├── test07_1.sh │ │ ├── test10_1.sh │ │ ├── test05_2.sh │ │ ├── test_12_1.py │ │ ├── test05_1.sh │ │ ├── test11.sh │ │ ├── tobesourced │ │ ├── test08.sh │ │ ├── test09.sh │ │ ├── test10.sh │ │ ├── test_201.py │ │ ├── test_204.py │ │ ├── test_12_2.py │ │ ├── test_202.py │ │ ├── test_203.py │ │ ├── test_101_1.py │ │ ├── test_102_1.py │ │ ├── test_102_2.py │ │ ├── test07.sh │ │ ├── test03.sh │ │ ├── test05.py │ │ ├── test06.sh │ │ └── test_12.py │ ├── test_termemu.py │ ├── test_threads.py │ ├── test_completer.py │ └── test_runtime.py ├── __init__.py ├── pip │ ├── __init__.py │ └── data │ │ └── stpkg │ │ ├── readme.md │ │ ├── stpkg │ │ ├── __init__.py │ │ └── submod.py │ │ └── setup.py ├── misc │ ├── __init__.py │ ├── test_pwd.py │ ├── test_exit.py │ ├── test_version.py │ ├── test_echo.py │ ├── test_totd.py │ ├── test_alias.py │ ├── test_ping.py │ ├── test_wget.py │ └── test_cowsay.py └── ui │ └── test_tkui.py ├── system ├── __init__.py ├── dummyconsole.py ├── shiowrapper.py ├── dummyobjc_util.py └── shui │ ├── stubui.py │ ├── dummyui.py │ └── __init__.py ├── lib ├── wakeonlan │ ├── __init__.py │ ├── LICENSE │ └── wol.py ├── git │ └── __init__.py ├── stashutils │ ├── __init__.py │ ├── fsi │ │ ├── __init__.py │ │ ├── errors.py │ │ └── interfaces.py │ ├── mount_ctrl.py │ ├── core.py │ └── extensions.py ├── mlpatches │ ├── __init__.py │ ├── modulepatches.py │ ├── mount_patches.py │ ├── os_process.py │ ├── tests │ │ └── sp_test.py │ ├── tl_patches.py │ ├── patches.py │ └── l2c.py ├── pythonista_add_action.py └── libcore.py ├── tools ├── __init__.py ├── README.md ├── common.py ├── pythonista_reinstall.py ├── yapf.ini └── colortest.py ├── __init__.py ├── bin ├── clear.py ├── jobs.py ├── exit.py ├── openin.py ├── quicklook.py ├── logout.py ├── which.py ├── kill.py ├── printenv.py ├── totd.py ├── mkdir.py ├── pwd.py ├── cat.py ├── echo.py ├── touch.py ├── fg.py ├── uniq.py ├── rmdir.py ├── sort.py ├── cd.py ├── pbcopy.py ├── source.py ├── ln.py ├── xargs.py ├── printhex.py ├── stashconf.py ├── alias.py ├── pbpaste.py ├── webviewer.py ├── zip.py ├── dropbox_setup.py ├── cut.py ├── umount.py ├── cowsay.py ├── wc.py ├── diff.py ├── ssh-keygen.py ├── curl.py ├── python3.py ├── mv.py ├── python.py ├── crypt.py ├── grep.py ├── wget.py ├── wol.py └── sha1sum.py ├── pyproject.toml ├── .github ├── dependabot.yml └── workflows │ └── check-code-and-run-tests.yml ├── docs ├── dev.md ├── py3_todo.md ├── user_input_sequence.txt └── pip_blocklist.md ├── LICENSE ├── .gitignore ├── data ├── pip_blocklist.json └── stash_tips.json └── .pre-commit-config.yaml /man/license.url: -------------------------------------------------------------------------------- 1 | stash://LICENSE 2 | -------------------------------------------------------------------------------- /man/readme.url: -------------------------------------------------------------------------------- 1 | stash://README.md 2 | -------------------------------------------------------------------------------- /tests/cat/data/nonascii.txt: -------------------------------------------------------------------------------- 1 | äöüß 2 | -------------------------------------------------------------------------------- /tests/install/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /tests/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # dummy file 2 | -------------------------------------------------------------------------------- /tests/md5sum/data/four.txt: -------------------------------------------------------------------------------- 1 | 2 + 2 = 4 -------------------------------------------------------------------------------- /tests/sha1sum/data/four.txt: -------------------------------------------------------------------------------- 1 | 2 + 2 = 4 -------------------------------------------------------------------------------- /tests/sha256sum/data/four.txt: -------------------------------------------------------------------------------- 1 | 2 + 2 = 4 -------------------------------------------------------------------------------- /man/changelog.url: -------------------------------------------------------------------------------- 1 | stash://CHANGES.md 2 | -------------------------------------------------------------------------------- /tests/ls/data/file1.txt: -------------------------------------------------------------------------------- 1 | file1 content 2 | -------------------------------------------------------------------------------- /tests/ls/data/file2.txt: -------------------------------------------------------------------------------- 1 | file2 content 2 | -------------------------------------------------------------------------------- /tests/md5sum/data/five.txt: -------------------------------------------------------------------------------- 1 | 2 + 3 = 5 2 | -------------------------------------------------------------------------------- /tests/pbcopy_pbpaste/__init__.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /tests/sha1sum/data/five.txt: -------------------------------------------------------------------------------- 1 | 2 + 3 = 5 2 | -------------------------------------------------------------------------------- /tests/sha256sum/data/five.txt: -------------------------------------------------------------------------------- 1 | 2 + 3 = 5 2 | -------------------------------------------------------------------------------- /system/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/ls/data/.hidden: -------------------------------------------------------------------------------- 1 | hidden file content 2 | -------------------------------------------------------------------------------- /lib/wakeonlan/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/ls/data/otherfile.txt: -------------------------------------------------------------------------------- 1 | other file content 2 | -------------------------------------------------------------------------------- /tests/system/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /lib/git/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /man/development.url: -------------------------------------------------------------------------------- 1 | https://github.com/ywangd/stash 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/ls/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/pip/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/system/data/old_history.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | 4 5 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/md5sum/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/md5sum/data/5eb63bbbe01eeed093cb22bb8f5acdc3: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/misc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/sha1sum/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/system/data/test07_1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo A is $A 3 | -------------------------------------------------------------------------------- /tests/pip/data/stpkg/readme.md: -------------------------------------------------------------------------------- 1 | local test package for stash pip 2 | -------------------------------------------------------------------------------- /tests/sha256sum/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | pass 3 | -------------------------------------------------------------------------------- /tests/system/data/test10_1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep bash 4 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import core as stash 3 | -------------------------------------------------------------------------------- /tests/md5sum/data/6acbfd6f40c416db43890607b9cd9b7a: -------------------------------------------------------------------------------- 1 | pythonista + StaSh = <3 -------------------------------------------------------------------------------- /tests/sha1sum/data/2aae6c35c94fcfb415dbe95f408b9ce91ee846ed: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/sha1sum/data/85e65a61b3f3a0fb03eed6ead8f88f78f1170129: -------------------------------------------------------------------------------- 1 | pythonista + StaSh = <3 -------------------------------------------------------------------------------- /tests/cat/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """dummy file.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /tests/system/data/test05_2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 2 3 | echo B is $B 4 | pwd -b 5 | -------------------------------------------------------------------------------- /tests/lib/stashutils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """dummy file.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /tests/sha256sum/data/b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/system/data/test_12_1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | os.chdir("bin") 5 | -------------------------------------------------------------------------------- /man/git_tutorial.url: -------------------------------------------------------------------------------- 1 | https://github.com/jsbain/stash_git_tutorial/blob/master/stash_git_tutorial.md 2 | -------------------------------------------------------------------------------- /tests/sha256sum/data/fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9: -------------------------------------------------------------------------------- 1 | pythonista + StaSh = <3 -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # StaSh tools 2 | This directory contains tools for working with the StaSh source code. 3 | -------------------------------------------------------------------------------- /lib/stashutils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """utility functions and modules for StaSh-scripts""" 3 | -------------------------------------------------------------------------------- /tests/system/data/test05_1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 1 3 | echo B is $B 4 | B=89 5 | echo B is $B 6 | cd bin 7 | pwd -b 8 | -------------------------------------------------------------------------------- /tests/cat/data/otherfile.txt: -------------------------------------------------------------------------------- 1 | Yeah, this is 2 | yet another 3 | test file 4 | for the 5 | 'cat' command. 6 | BTW, cats are cute. 7 | -------------------------------------------------------------------------------- /tests/system/data/test11.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure multiple commands on a single line work as expected 4 | A=42; echo A is $A 5 | -------------------------------------------------------------------------------- /tests/system/data/tobesourced: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | AA=sourced 3 | alias l1='ls -1' 4 | echo From tobesourced AA is $AA 5 | alias | sort 6 | -------------------------------------------------------------------------------- /tests/pip/data/stpkg/stpkg/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """local test package for StaSh pip.""" 3 | 4 | from stpkg.submod import main 5 | -------------------------------------------------------------------------------- /tests/system/data/test08.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The value of A should not be carried to the 2nd command in the pipe 4 | A=100 test07_1.sh | test07_1.sh 5 | -------------------------------------------------------------------------------- /tests/pip/data/stpkg/stpkg/submod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """submodule test""" 3 | 4 | 5 | def main(): 6 | print("local pip test successfull!") 7 | -------------------------------------------------------------------------------- /tests/system/data/test09.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The value of A should not be carried to the 2nd command in the pipe 4 | A=100 test07_1.sh | echo A is $A 5 | -------------------------------------------------------------------------------- /tests/system/data/test10.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensure a script can correctly receive piped input 4 | 5 | cat tests/system/data/test10.sh | test10_1.sh 6 | -------------------------------------------------------------------------------- /tests/pbcopy_pbpaste/data/testfile.txt: -------------------------------------------------------------------------------- 1 | Dear reader, 2 | this is a revolitonary, futuristic, unprecendented test file. 3 | No, not really. 4 | It is a 'normal' one. 5 | -------------------------------------------------------------------------------- /bin/clear.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Clear the stash console output window""" 3 | 4 | from __future__ import print_function 5 | 6 | print("\u009bc", end="") 7 | -------------------------------------------------------------------------------- /tests/cat/data/somefile.txt: -------------------------------------------------------------------------------- 1 | This is 2 | the content 3 | of a 4 | test-file 5 | for the 6 | 'cat'-command. 7 | 8 | TODO: 9 | should we test non-ascii chars? 10 | -------------------------------------------------------------------------------- /tests/system/data/test_201.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | print("The first line\nA quick brown fox jumps over the lazy dog\r", end="") 5 | -------------------------------------------------------------------------------- /tests/system/data/test_204.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | print("The first line\nA quick brown fox jumps over the lazy dog\b", end="") 5 | -------------------------------------------------------------------------------- /tests/system/data/test_12_2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import os 4 | 5 | print("from child script 2 {}".format(os.path.basename(os.getcwd()))) 6 | -------------------------------------------------------------------------------- /tests/system/data/test_202.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | print("The first line\nA quick brown fox jumps over the lazy dog\r\033[0P", end="") 5 | -------------------------------------------------------------------------------- /tests/system/data/test_203.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | print("The first line\nA quick brown fox jumps over the lazy dog\r\033[2K", end="") 5 | -------------------------------------------------------------------------------- /tests/system/data/test_101_1.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | import time 4 | 5 | for i in range(2): 6 | print("sleeping ... {}".format(i)) 7 | time.sleep(1) 8 | -------------------------------------------------------------------------------- /system/dummyconsole.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def hud_alert(msg, icon, duration): 5 | pass 6 | 7 | 8 | def show_activity(): 9 | pass 10 | 11 | 12 | def hide_activity(): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/system/data/test_102_1.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | import os 4 | import time 5 | 6 | for i in range(5): 7 | print("{}".format(os.path.basename(__file__))) 8 | time.sleep(0.5) 9 | -------------------------------------------------------------------------------- /tests/system/data/test_102_2.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function 3 | import os 4 | import time 5 | 6 | for i in range(5): 7 | print("{}".format(os.path.basename(__file__))) 8 | time.sleep(0.5) 9 | -------------------------------------------------------------------------------- /lib/stashutils/fsi/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This package contains classes used for accessing filesystems or servers. 4 | These classes have a common interface, which means they can easily be 5 | switched. 6 | """ 7 | -------------------------------------------------------------------------------- /lib/mlpatches/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Dummy File. 4 | This directory contains the patches "monkeyloard" uses. 5 | Later there will/may be a subdirectory called "modules", which contains modules added to sys.path. 6 | """ 7 | -------------------------------------------------------------------------------- /tests/md5sum/data/results.md5sum: -------------------------------------------------------------------------------- 1 | 5eb63bbbe01eeed093cb22bb8f5acdc3 5eb63bbbe01eeed093cb22bb8f5acdc3 2 | 6acbfd6f40c416db43890607b9cd9b7a 6acbfd6f40c416db43890607b9cd9b7a 3 | 903eb40461bb012a4a51ed7ec4516516 five.txt 4 | 1e49137f22b369d84f0e06de47448d61 four.txt 5 | 6 | -------------------------------------------------------------------------------- /tests/md5sum/data/wrong_results.md5sum: -------------------------------------------------------------------------------- 1 | 5eb63bbbe01eeed093cb22bb8f5acdc3 5eb63bbbe01eeed093cb22bb8f5acdc3 2 | 6acbfd6f40c416db43890607b9cd9b7a 6acbfd6f40c416db43890607b9cd9b7a 3 | 903eb40461bb012a4a51ed7ec4510000 five.txt 4 | 1e49137f22b369d84f0e06de47448d61 four.txt 5 | 6 | -------------------------------------------------------------------------------- /tests/system/data/test07.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The leading assignment of the first command should not affect the 2nd command 4 | 5 | A=999 test07_1.sh 6 | # A should be undefined this time 7 | test07_1.sh 8 | 9 | # after the script is finished, parent shell should not be affected 10 | -------------------------------------------------------------------------------- /tests/sha1sum/data/results.sha1sum: -------------------------------------------------------------------------------- 1 | 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed 2 | 85e65a61b3f3a0fb03eed6ead8f88f78f1170129 85e65a61b3f3a0fb03eed6ead8f88f78f1170129 3 | 163e43ac1683899021cb87470eeac55538453fe9 five.txt 4 | 37124e4b62399f824c248fd27f66bac30285f474 four.txt 5 | -------------------------------------------------------------------------------- /tests/sha1sum/data/wrong_results.sha1sum: -------------------------------------------------------------------------------- 1 | 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed 2 | 85e65a61b3f3a0fb03eed6ead8f88f78f1170129 85e65a61b3f3a0fb03eed6ead8f88f78f1170129 3 | 163e43ac1683899021cb87470eeac55538450000 five.txt 4 | 37124e4b62399f824c248fd27f66bac30285f474 four.txt 5 | 6 | -------------------------------------------------------------------------------- /tests/system/data/test03.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo $1 $2 3 | # If parent shell does not have A, first echo should expand A to empty 4 | echo A is $A # end-of-line comment 5 | 6 | A=8 7 | # A should now be set 8 | echo A is $A 9 | 10 | cd bin 11 | pwd -b 12 | # After the script, A should NOT be set in parent shell and parent cwd 13 | # should remain unchanged. 14 | -------------------------------------------------------------------------------- /man/monkeypatching/page_1.txt: -------------------------------------------------------------------------------- 1 | monkeypatching(1) --- INDEX 2 | ============================ 3 | 4 | Content 5 | Index........................1 6 | Warnings.....................2 7 | Managing Patches.............3 8 | Patches......................4 9 | Creating new Patches.........5 10 | 11 | To view a page, type "man monkeypatching(n)" where "n" is the pagenumber. 12 | -------------------------------------------------------------------------------- /man/mounting/page_1.txt: -------------------------------------------------------------------------------- 1 | mounting(1) --- INDEX 2 | ===================== 3 | 4 | Content 5 | Index........................1 6 | Warnings.....................2 7 | Informations.................3 8 | Usage........................4 9 | FSIs.........................5 10 | Developer Informations.......6 11 | 12 | 13 | To view a page, type "man mounting(n)" where "n" is the pagenumber. 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # - run: ruff check --ignore=E401,E402,E501,E701,E721,E722,E731,E741,F401,F403,F405,F523,F524,F811,F841 2 | # --output-format=github --target-version=py310 . 3 | # - run: ruff format --target-version=py310 . 4 | 5 | [tool.ruff] 6 | target-version = "py310" 7 | lint.ignore = [ 8 | "E402", 9 | "E721", 10 | "E722", 11 | "E731", 12 | "F401", 13 | "F841", 14 | ] 15 | -------------------------------------------------------------------------------- /tests/sha256sum/data/results.sha256sum: -------------------------------------------------------------------------------- 1 | b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 2 | fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9 fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9 3 | a1cb858b7ad98eb21fe62f3d567ed2ee5987eed1bc8347be001b36e3bc14d813 five.txt 4 | e93dff0d1076b537cd1bd659d14bb77d5fd47db13204a227cb3cd66e81dd454c four.txt 5 | -------------------------------------------------------------------------------- /tests/sha256sum/data/wrong_results.sha256sum: -------------------------------------------------------------------------------- 1 | b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 2 | fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9 fc6d8618769bc8aae948b89504efdf74597acf53727bb52472b7cfc0e167f6a9 3 | a1cb858b7ad98eb21fe62f3d567ed2ee5987eed1bc8347be001b36e3bc140000 five.txt 4 | e93dff0d1076b537cd1bd659d14bb77d5fd47db13204a227cb3cd66e81dd454c four.txt 5 | 6 | -------------------------------------------------------------------------------- /tests/system/data/test05.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | s = globals()["_stash"] 5 | 6 | # following statements should be correlated 7 | s("echo AA is $AA") 8 | s("AA=Hello") 9 | # AA should now be set 10 | s("echo AA is $AA") 11 | s("pwd -b") 12 | s("cd bin") 13 | # cwd should now be changed 14 | s("pwd -b") 15 | s("cd ..") 16 | 17 | print() 18 | # following two scripts should not interfere each other 19 | s("test05_1.sh") 20 | 21 | s("test05_2.sh") 22 | -------------------------------------------------------------------------------- /tools/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """common methods""" 3 | 4 | import os 5 | 6 | 7 | def get_stash_dir(): 8 | """ 9 | Returns the StaSh root directory, detected from this file. 10 | :return: the StaSh root directory 11 | :rtype: str 12 | """ 13 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | 16 | def main(): 17 | """ 18 | The main function. 19 | """ 20 | print("StaSh root directory: {}".format(get_stash_dir())) 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /tests/system/data/test06.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo AA is $AA 4 | # Direct execution without sourcing should not define the variables in parent shell 5 | echo --- direct execution without sourcing --- 6 | tobesourced 7 | # AA should still be undefined 8 | echo AA is $AA 9 | alias | sort 10 | 11 | echo 12 | 13 | # sourcing the file creating the variable in parent shell 14 | echo --- source the file --- 15 | source tests/system/data/tobesourced 16 | # AA should now be defined 17 | echo AA is $AA 18 | alias | sort 19 | 20 | # Parent main shell should not be affected at all 21 | -------------------------------------------------------------------------------- /tests/pip/data/stpkg/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """setup.py for a local test package.""" 3 | 4 | from setuptools import setup 5 | 6 | setup( 7 | name="stpkg", 8 | version="1.0.0", 9 | author="bennr01", 10 | description="local test package for StaSh pip. DO NOT UPLOAD!", 11 | keywords="test", 12 | url="https://github.com/ywangd/stash/", 13 | classifiers=[ 14 | "Topic :: Test", 15 | ], 16 | py_modules=[ 17 | "stpkg", 18 | ], 19 | entry_points={ 20 | "console_scripts": ["stash_pip_test = stpkg:main"], 21 | }, 22 | ) 23 | -------------------------------------------------------------------------------- /tests/system/data/test_12.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import os 4 | 5 | _stash = globals()["_stash"] 6 | 7 | print("parent script {}".format(os.path.basename(os.getcwd()))) 8 | # Change directory in sub-shell 9 | _stash("test_12_1.py") 10 | # Directory in parent shell is not changed 11 | print("parent script {}".format(os.path.basename(os.getcwd()))) 12 | # Following calls to sub-shell remembers the directory change in last call 13 | _stash("test_12_2.py") 14 | # Directory in parent shell is still the same 15 | print("parent script {}".format(os.path.basename(os.getcwd()))) 16 | -------------------------------------------------------------------------------- /lib/stashutils/mount_ctrl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module keeps track of the current MountManager. 4 | This can not be done in stashutils.mount_manager, because this would create some import problems. 5 | """ 6 | 7 | # global: current mount manager 8 | 9 | MANAGER = None 10 | 11 | 12 | def get_manager(): 13 | """ 14 | returns the current mount manager. 15 | Use the function instead of the constant to prevent import problems/ 16 | """ 17 | return MANAGER 18 | 19 | 20 | def set_manager(manager): 21 | """sets the current manager.""" 22 | global MANAGER 23 | MANAGER = manager 24 | -------------------------------------------------------------------------------- /bin/jobs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | List all jobs that are currently running. 4 | """ 5 | 6 | from __future__ import print_function 7 | import sys 8 | import argparse 9 | import threading 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.parse_args(args) 15 | 16 | current_worker = threading.currentThread() 17 | 18 | _stash = globals()["_stash"] 19 | """:type : StaSh""" 20 | 21 | for worker in _stash.get_workers(): 22 | if worker.job_id != current_worker.job_id: 23 | print(worker) 24 | 25 | 26 | if __name__ == "__main__": 27 | main(sys.argv[1:]) 28 | -------------------------------------------------------------------------------- /bin/exit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Exit the current subshell, optionally with a specific status. If no 4 | status is given, the default of 0 is used, indicating successful 5 | execution with no errors. 6 | """ 7 | 8 | import argparse 9 | import os 10 | import sys 11 | 12 | 13 | def main(args): 14 | p = argparse.ArgumentParser(description=__doc__) 15 | p.add_argument( 16 | "status", action="store", nargs="?", default=0, type=int, help="status code" 17 | ) 18 | ns = p.parse_args(args) 19 | sys.exit(ns.status) 20 | 21 | 22 | if __name__ == "__main__": 23 | main(sys.argv[1:]) 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /lib/stashutils/fsi/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Errors and Exceptions.""" 3 | 4 | 5 | class OperationFailure(IOError): 6 | """raise this if a operation (e.g. cd) fails. 7 | The FSI is responsible for undoing errors.""" 8 | 9 | pass 10 | 11 | 12 | class IsDir(OperationFailure): 13 | """raise this if a command only works on a file but a dirname is passed.""" 14 | 15 | pass 16 | 17 | 18 | class IsFile(OperationFailure): 19 | """raise this if a command only works on a dir but a filename is passed.""" 20 | 21 | pass 22 | 23 | 24 | class AlreadyExists(OperationFailure): 25 | """raise this if something already exists.""" 26 | 27 | pass 28 | -------------------------------------------------------------------------------- /bin/openin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Open file in an external app.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import sys 9 | 10 | 11 | _stash = globals()["_stash"] 12 | 13 | 14 | class ConsoleOpenin(object): 15 | def __init__(self, args): 16 | p = argparse.ArgumentParser(description=__doc__) 17 | p.add_argument("file", action="store", help="file to open") 18 | ns = p.parse_args(args) 19 | self.filename = ns.file 20 | 21 | def open_in(self): 22 | _stash.libdist.open_in(self.filename) 23 | 24 | 25 | if __name__ == "__main__": 26 | ConsoleOpenin(sys.argv[1:]).open_in() 27 | -------------------------------------------------------------------------------- /man/monkeypatching/page_2.txt: -------------------------------------------------------------------------------- 1 | monkeypatching(2) --- WARNINGS 2 | ============================== 3 | 4 | Monkeypatching is dangerous and may cause one or several of the following problems: 5 | - break commands or scripts 6 | - freeze or crash StaSh 7 | - freeze or crash Pythonista 8 | 9 | The monkeypatches are only temporary enabled. All monkeypatches are disabled by default. 10 | In case anything goes wrong, double tap the home button and remove Pythonista from the RAM. 11 | Restarting Pythonista afterwards resets all modules. 12 | 13 | I recommend only enabling required monkeypatches. 14 | I also recommend to not enable any of the INSTABLE patches unless you know what you are doing. 15 | -------------------------------------------------------------------------------- /bin/quicklook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Open file in Quick Look.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import sys 9 | 10 | 11 | _stash = globals()["_stash"] 12 | 13 | 14 | class ConsoleQuicklook(object): 15 | def __init__(self, args): 16 | p = argparse.ArgumentParser(description=__doc__) 17 | p.add_argument("file", action="store", help="file to open") 18 | ns = p.parse_args(args) 19 | self.filename = ns.file 20 | 21 | def quicklook(self): 22 | _stash.libdist.quicklook(self.filename) 23 | 24 | 25 | if __name__ == "__main__": 26 | ConsoleQuicklook(sys.argv[1:]).quicklook() 27 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | # Challenges 2 | 3 | * The application is able to run as a Tab UI and stay active indefinitely. 4 | - This means no active thread can be used. The application can only wait 5 | idle till triggered by user interactions. 6 | - Solution: command execution is directly invoked by TextView delegate. 7 | No active reading thread for main level user inputs (unlike running 8 | scripts which have an active reading thread to wait for user data). 9 | 10 | * The ObjC calls are slow especially for large text building and rendering 11 | - It is very inefficient if we simply rebuild and replace the entire text 12 | buffer for every text changes. 13 | - Solution: sequential editing. 14 | 15 | 16 | # Design 17 | -------------------------------------------------------------------------------- /lib/wakeonlan/LICENSE: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/remcohaszing/pywakeonlan as of 2015-08-13 2 | # Adapted and extended by Georg Viehoever, 2015-08-13 3 | # 4 | # License as in original: 5 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 6 | # Version 2, December 2004 7 | # 8 | # Copyright (C) 2012 Remco Haszing 9 | # 10 | # Everyone is permitted to copy and distribute verbatim or modified 11 | # copies of this license document, and changing it is allowed as long 12 | # as the name is changed. 13 | # 14 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 15 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 16 | # 17 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 18 | -------------------------------------------------------------------------------- /man/mounting/page_2.txt: -------------------------------------------------------------------------------- 1 | MOUNTING(2) --- WARNINGS 2 | ======================== 3 | 4 | This documentation was written by a non-native english-spreaker and may contain terrible spelling mistakes and/or grammatical bugs. I do not take any responsibility for any braindamage caused by such. 5 | 6 | The 'mount' and 'unmount'-commands use monkeypatches. 7 | This may cause bugs. 8 | For more detailed warnings about monkeypatching, see 'monkeypatching(2)'. 9 | 10 | Some scripts may not work correctly when used on a mounted FS. 11 | 12 | Sometimes FilesystemInterfaces may become useless while in use. 13 | In this case, you may need to restart StaSh. 14 | For example, the FTP-FSI may get a connection-timeout and break, preventing you from executing any commands. 15 | -------------------------------------------------------------------------------- /bin/logout.py: -------------------------------------------------------------------------------- 1 | """ 2 | Quit a shell. 3 | """ 4 | 5 | import argparse 6 | 7 | 8 | _stash = globals()["_stash"] 9 | 10 | 11 | def logout(n): 12 | """ 13 | Quit StaSh 14 | :param n: exitcode for the shell (not implemented) 15 | :type n: int 16 | """ 17 | import threading 18 | 19 | t = threading.Thread(target=_stash.close, name="close thread") 20 | t.daemon = True 21 | t.start() 22 | 23 | 24 | if __name__ == "__main__": 25 | parser = argparse.ArgumentParser(description="Quits a shell") 26 | parser.add_argument( 27 | "n", 28 | nargs="?", 29 | default=0, 30 | type=int, 31 | help="exit the shell with this code. Not implemented.", 32 | ) 33 | ns = parser.parse_args() 34 | logout(ns.n) 35 | -------------------------------------------------------------------------------- /lib/mlpatches/modulepatches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """this module contains a dictionary of all modulepatches.""" 3 | 4 | from mlpatches import base 5 | 6 | 7 | class Popen2Patch(base.ModulePatch): 8 | """the patch for the popen2 module.""" 9 | 10 | PY2 = True 11 | PY3 = False 12 | relpath = "popen2.py" 13 | name = "popen2" 14 | 15 | 16 | class SubprocessPatch(base.ModulePatch): 17 | """the patch for the subprocess module.""" 18 | 19 | PY2 = True 20 | PY3 = False # uses unicode 21 | relpath = "subprocess.py" 22 | name = "subprocess" 23 | 24 | 25 | # create instances 26 | POPEN2PATCH = Popen2Patch() 27 | SUBPROCESSPATCH = SubprocessPatch() 28 | 29 | # name -> ModulePatch() 30 | MODULE_PATCHES = { 31 | "popen2": POPEN2PATCH, 32 | "subprocess": SUBPROCESSPATCH, 33 | } 34 | -------------------------------------------------------------------------------- /bin/which.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Locate a command script in BIN_PATH. No output if command is not found.""" 3 | 4 | from __future__ import print_function 5 | 6 | 7 | def main(command, fullname=False): 8 | global _stash 9 | rt = globals()["_stash"].runtime 10 | try: 11 | filename = rt.find_script_file(command) 12 | if not fullname: 13 | filename = _stash.libcore.collapseuser(filename) 14 | print(filename) 15 | except Exception: 16 | pass 17 | 18 | 19 | if __name__ == "__main__": 20 | import argparse 21 | 22 | ap = argparse.ArgumentParser() 23 | ap.add_argument("command", help="name of the command to be located") 24 | ap.add_argument("-f", "--fullname", action="store_true", help="show full path") 25 | ns = ap.parse_args() 26 | main(ns.command, ns.fullname) 27 | -------------------------------------------------------------------------------- /man/grammar.txt: -------------------------------------------------------------------------------- 1 | 2 | ----------------------------------------------------------------------------- 3 | Shell Grammar Simplified 4 | ----------------------------------------------------------------------------- 5 | complete_command : pipe_sequence (punctuator pipe_sequence)* [punctuator] 6 | punctuator : ';' | '&' 7 | pipe_sequence : simple_command ('|' simple_command)* 8 | simple_command : cmd_prefix [cmd_word] [cmd_suffix] 9 | | cmd_word [cmd_suffix] 10 | cmd_prefix : assignment_word+ 11 | cmd_suffix : word + [io_redirect] 12 | | io_redirect 13 | io_redirect : ('>' | '>>') filename 14 | modifier : '!' | '\' 15 | cmd_word : [modifier] word 16 | filename : word 17 | word : escaped | uq_word | bq_word | dq_word | sq_word 18 | uq_word : (WORD_CHARS)+ | '&3' 19 | -------------------------------------------------------------------------------- /docs/py3_todo.md: -------------------------------------------------------------------------------- 1 | # Python 3 TODO 2 | List of known issues, bugs and todos for stash py3 (and py2) compatibility. 3 | 4 | # General 5 | - more commands need to be ported 6 | - sys.argv needs to be bytestr in py2 and unistt in py3. There is only a quick fix in place (somewhere in `shruntime.py`), which should be replaced as it only works for common usage situations 7 | - i/o seems to switch between jobs from time to time 8 | 9 | 10 | # `crypt.py` 11 | - in py3, the key is shown as `b''` instead of ``. Also test solution with py2 12 | 13 | # `curl.py` 14 | - more testing 15 | 16 | # `pip.py` 17 | - many bugs 18 | - installation using `setupy.py` fails most of the time, always falls back to package detection 19 | - maybe `pip3` for py3 instead of a single `pip` command. 20 | - maybe only an alias? 21 | 22 | # Non py3-issues 23 | - `cp.py` has no `-r` argument 24 | -------------------------------------------------------------------------------- /bin/kill.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Terminate a running job. 4 | """ 5 | 6 | from __future__ import print_function 7 | import sys 8 | import argparse 9 | import time 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("job_ids", nargs="+", type=int, help="ID of a running job") 15 | 16 | ns = ap.parse_args(args) 17 | 18 | _stash = globals()["_stash"] 19 | """:type : StaSh""" 20 | 21 | for job_id in ns.job_ids: 22 | if job_id in _stash.runtime.worker_registry: 23 | print("killing job {} ...".format(job_id)) 24 | worker = _stash.runtime.worker_registry.get_worker(job_id) 25 | worker.kill() 26 | time.sleep(1) 27 | 28 | else: 29 | print("error: no such job with id: {}".format(job_id)) 30 | break 31 | 32 | 33 | if __name__ == "__main__": 34 | main(sys.argv[1:]) 35 | -------------------------------------------------------------------------------- /bin/printenv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """List current environment variables and values.""" 4 | 5 | from __future__ import division, print_function, unicode_literals 6 | 7 | import argparse 8 | import os 9 | import sys 10 | 11 | 12 | def main(args): 13 | p = argparse.ArgumentParser(description=__doc__) 14 | p.add_argument( 15 | "variables", action="store", nargs="*", help="variables to be printed" 16 | ) 17 | ns = p.parse_args(args) 18 | 19 | if ns.variables: 20 | vardict = {k: v for k, v in os.environ.items() if k in ns.variables} 21 | else: 22 | vardict = os.environ 23 | 24 | vardict = {k: v for k, v in vardict.items() if k[0] not in "$@?!#*0123456789"} 25 | 26 | for k, v in vardict.items(): 27 | print("{}={}".format(k, v)) 28 | 29 | sys.exit(0) 30 | 31 | 32 | if __name__ == "__main__": 33 | main(sys.argv[1:]) 34 | -------------------------------------------------------------------------------- /man/monkeypatching/page_3.txt: -------------------------------------------------------------------------------- 1 | monkeypatching(3) --- MANAGING PATCHES 2 | ====================================== 3 | 4 | StaSh provides the "monkeylord" command for managing monkeypatches. 5 | 6 | Usage: 7 | "monkeylord -h" to get help 8 | "monkeylord list" to get a list of all patches and their state 9 | "monkeylord enable [NAME]" to enable patches 10 | "monkeylord disable [NAME]" to disable patches 11 | "monkeylord saveconf PATH" to save the current configuration 12 | "monkeylord loadconf PATH" to load a configuration from a file. 13 | 14 | The optional argument NAME defines the patch(es) to enable/disable. 15 | It defaults to "STABLE" for "monkeylord enable" and "ALL" for "monkeylord disable" 16 | You can save your monkeypatch configuration using "monkeylord saveconf PATH" and later load it by using "monkeylord loadconf PATH". 17 | To get a list of available patches and some help, see the "Patches"-Chapter. 18 | -------------------------------------------------------------------------------- /bin/totd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tip of the day""" 3 | 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import json 8 | import random 9 | import argparse 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument( 15 | "-n", "--count", action="store_true", help="show total number of tips" 16 | ) 17 | 18 | ns = ap.parse_args(args) 19 | 20 | filename = os.path.join(os.environ["STASH_ROOT"], "data", "stash_tips.json") 21 | if not os.path.exists(filename): 22 | return 1 23 | 24 | _stash = globals()["_stash"] 25 | 26 | with open(filename) as ins: 27 | tips = json.load(ins) 28 | 29 | if ns.count: 30 | print("Total available tips: %s" % len(tips)) 31 | else: 32 | idx = random.randint(0, len(tips) - 1) 33 | print("%s: %s" % (_stash.text_bold("Tip"), _stash.text_italic(tips[idx]))) 34 | 35 | 36 | if __name__ == "__main__": 37 | main(sys.argv[1:]) 38 | -------------------------------------------------------------------------------- /lib/mlpatches/mount_patches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module contains the group definition for the mount patches.""" 3 | 4 | from mlpatches.base import PatchGroup 5 | from mlpatches import mount_base 6 | from stashutils import mount_ctrl 7 | 8 | _BASE_PATCHES = list( 9 | filter( 10 | None, 11 | [ 12 | getattr(mount_base, p) if p.endswith("PATCH") else None 13 | for p in dir(mount_base) 14 | ], 15 | ) 16 | ) 17 | 18 | 19 | class MountPatches(PatchGroup): 20 | """All mount patches.""" 21 | 22 | patches = [] + _BASE_PATCHES 23 | 24 | def pre_enable(self): 25 | # ensure a manager is set 26 | manager = mount_ctrl.get_manager() 27 | if manager is None: 28 | from stashutils import mount_manager # import here to prevent an error 29 | 30 | manager = mount_manager.MountManager() 31 | mount_ctrl.set_manager(manager) 32 | 33 | 34 | # create patchgroup instances 35 | MOUNT_PATCHES = MountPatches() 36 | -------------------------------------------------------------------------------- /bin/mkdir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Create a new directory. The parent directory must already exist, 4 | unless -p is specified. 5 | """ 6 | 7 | from __future__ import print_function 8 | 9 | import argparse 10 | import os 11 | import sys 12 | 13 | 14 | def main(args): 15 | p = argparse.ArgumentParser(description=__doc__) 16 | p.add_argument( 17 | "-p", 18 | "--parents", 19 | action="store_true", 20 | help="create parent directories as necessary", 21 | ) 22 | p.add_argument("dir", action="store", nargs="+", help="the directory to be created") 23 | ns = p.parse_args(args) 24 | 25 | status = 0 26 | 27 | for dir in ns.dir: 28 | try: 29 | (os.makedirs if ns.parents else os.mkdir)(dir) 30 | except Exception as err: 31 | print("mkdir: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 32 | status = 1 33 | 34 | sys.exit(status) 35 | 36 | 37 | if __name__ == "__main__": 38 | main(sys.argv[1:]) 39 | -------------------------------------------------------------------------------- /bin/pwd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print the current working directory.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import os 9 | import sys 10 | 11 | _stash = globals()["_stash"] 12 | collapseuser = _stash.libcore.collapseuser 13 | 14 | 15 | def main(args): 16 | p = argparse.ArgumentParser(description=__doc__) 17 | p.add_argument("-b", "--basename", action="store_true", help="show basename only") 18 | p.add_argument("-f", "--fullname", action="store_true", help="show full path") 19 | ns = p.parse_args(args) 20 | 21 | status = 0 22 | 23 | try: 24 | if ns.fullname: 25 | print(os.getcwd()) 26 | elif ns.basename: 27 | print(os.path.basename(os.getcwd())) 28 | else: 29 | print(collapseuser(os.getcwd())) 30 | except Exception as err: 31 | print("pwd: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 32 | status = 1 33 | 34 | sys.exit(status) 35 | 36 | 37 | if __name__ == "__main__": 38 | main(sys.argv[1:]) 39 | -------------------------------------------------------------------------------- /bin/cat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print the contents of the given files.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import string 9 | import sys 10 | import fileinput 11 | 12 | 13 | def filter_non_printable(s): 14 | return "".join( 15 | [c if c.isalnum() or c.isspace() or c in string.punctuation else " " for c in s] 16 | ) 17 | 18 | 19 | def main(args): 20 | p = argparse.ArgumentParser(description=__doc__) 21 | p.add_argument("files", action="store", nargs="*", help="files to print") 22 | ns = p.parse_args(args) 23 | 24 | status = 0 25 | 26 | fileinput.close() # in case it is not closed 27 | try: 28 | for line in fileinput.input(ns.files, openhook=fileinput.hook_encoded("utf-8")): 29 | print(filter_non_printable(line), end="") 30 | except Exception as e: 31 | print("cat: %s" % str(e)) 32 | status = 1 33 | finally: 34 | fileinput.close() 35 | 36 | sys.exit(status) 37 | 38 | 39 | if __name__ == "__main__": 40 | main(sys.argv[1:]) 41 | -------------------------------------------------------------------------------- /bin/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print all arguments to stdout, separated by spaces.""" 4 | 5 | from __future__ import print_function 6 | 7 | import sys 8 | 9 | 10 | def main(args): 11 | # Not using argparse here, because echo should echo anything that is not a 12 | # valid and usable flag. 13 | end = "\n" 14 | escapes = False # NYI 15 | remove = [] 16 | for i, arg in enumerate(args): 17 | if arg.startswith("-"): 18 | if set(arg[1:]) < set("neE"): 19 | for char in arg[1:]: 20 | if char == "n": 21 | end = "" 22 | elif char == "e": 23 | escapes = True 24 | elif char == "E": 25 | escapes = False 26 | remove.append(i) 27 | else: 28 | continue 29 | 30 | for i in reversed(remove): 31 | del args[i] 32 | 33 | print(*args, end=end) 34 | 35 | sys.exit(0) 36 | 37 | 38 | if __name__ == "__main__": 39 | main(sys.argv[1:]) 40 | -------------------------------------------------------------------------------- /bin/touch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Update the modification times of the given files, and create them if 4 | they do not yet exist. 5 | """ 6 | 7 | from __future__ import print_function 8 | 9 | import argparse 10 | import os 11 | import sys 12 | 13 | 14 | def main(args): 15 | p = argparse.ArgumentParser(description=__doc__) 16 | p.add_argument( 17 | "-c", "--no-create", action="store_true", help="do not create nonexistant files" 18 | ) 19 | p.add_argument( 20 | "file", action="store", nargs="+", help="one or more files to be touched" 21 | ) 22 | ns = p.parse_args(args) 23 | 24 | status = 0 25 | 26 | for filename in ns.file: 27 | try: 28 | if not os.path.exists(filename) and not ns.no_create: 29 | open(filename, "wb").close() 30 | os.utime(filename, None) 31 | except Exception as err: 32 | print("touch: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 33 | status = 1 34 | 35 | sys.exit(status) 36 | 37 | 38 | if __name__ == "__main__": 39 | main(sys.argv[1:]) 40 | -------------------------------------------------------------------------------- /tests/lib/test_libdist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test for 'libdist'. 3 | """ 4 | 5 | from stash.tests.stashtest import StashTestCase 6 | 7 | 8 | class LibDistTests(StashTestCase): 9 | """ 10 | Tests for 'libdist' 11 | """ 12 | 13 | def test_libdist_is_loaded(self): 14 | """ 15 | Test that 'libdist' is loaded. 16 | """ 17 | loaded_libs = [an for an in dir(self.stash) if an.startswith("lib")] 18 | self.assertIn("libdist", loaded_libs) 19 | 20 | def test_clipboard_api_available(self): 21 | """ 22 | Test that the clipboard api is provided by libdist 23 | """ 24 | defs = dir(self.stash.libdist) 25 | self.assertIn("clipboard_get", defs) 26 | self.assertIn("clipboard_set", defs) 27 | 28 | def test_pip_definitions_available(self): 29 | """ 30 | Test that the libdist provides the definitions required by 'pip'. 31 | """ 32 | defs = dir(self.stash.libdist) 33 | required = ["SITE_PACKAGES_FOLDER", "SITE_PACKAGES_FOLDER_6", "BUNDLED_MODULES"] 34 | for an in required: 35 | self.assertIn(an, defs) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 ywangd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/system/test_termemu.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from stash.tests.stashtest import StashTestCase 4 | 5 | 6 | class TermemuTests(StashTestCase): 7 | setup_commands = ["BIN_PATH=$STASH_ROOT/tests/system/data:$BIN_PATH"] 8 | 9 | def test_201(self): 10 | self.stash("test_201.py") 11 | cmp_str = """[stash]$ The first line 12 | [stash]$ rown fox jumps over the lazy dog""" 13 | assert self.stash.main_screen.text == cmp_str, "output not identical" 14 | 15 | def test_202(self): 16 | self.stash("test_202.py") 17 | cmp_str = """[stash]$ The first line 18 | [stash]$ """ 19 | assert self.stash.main_screen.text == cmp_str, "output not identical" 20 | 21 | def test_203(self): 22 | self.stash("test_203.py") 23 | cmp_str = """[stash]$ The first line 24 | [stash]$ """ 25 | assert self.stash.main_screen.text == cmp_str 26 | 27 | def test_204(self): 28 | self.stash("test_204.py") 29 | cmp_str = """[stash]$ The first line 30 | A quick brown fox jumps over the lazy do[stash]$ """ 31 | assert self.stash.main_screen.text == cmp_str 32 | -------------------------------------------------------------------------------- /bin/fg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Bring a background job to foreground. 4 | """ 5 | 6 | from __future__ import print_function 7 | import sys 8 | import argparse 9 | import threading 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument( 15 | "job_id", nargs="?", type=int, help="ID of a running background job" 16 | ) 17 | ns = ap.parse_args(args) 18 | 19 | _stash = globals()["_stash"] 20 | worker_registry = _stash.runtime.worker_registry 21 | 22 | if ns.job_id is None: 23 | worker = worker_registry.get_first_bg_worker() 24 | else: 25 | worker = worker_registry.get_worker(ns.job_id) 26 | 27 | if worker is None: 28 | print( 29 | "no background job running" 30 | + (" with id {}".format(ns.job_id) if ns.job_id else "") 31 | ) 32 | return 33 | 34 | def f(): 35 | _stash.runtime.push_to_foreground(worker) 36 | 37 | t = threading.Timer(1.0, f) 38 | print("pushing job {} to foreground ...".format(worker.job_id)) 39 | t.start() 40 | 41 | 42 | if __name__ == "__main__": 43 | main(sys.argv[1:]) 44 | -------------------------------------------------------------------------------- /lib/stashutils/fsi/interfaces.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module contains a dictionary mapping the identifiers to the fsi-classes 4 | """ 5 | 6 | from stash.system.shcommon import _STASH_EXTENSION_FSI_PATH 7 | from stashutils.fsi.local import LocalFSI 8 | from stashutils.fsi.FTP import FTPFSI 9 | from stashutils.fsi.DropBox import DropboxFSI 10 | from stashutils.fsi.zip import ZipfileFSI 11 | from stashutils.core import load_from_dir 12 | 13 | # map type -> FSI_class 14 | FILESYSTEM_TYPES = { 15 | "local": LocalFSI, 16 | "Local": LocalFSI, 17 | "FTP": FTPFSI, 18 | "ftp": FTPFSI, 19 | "dropbox": DropboxFSI, 20 | "DropBox": DropboxFSI, 21 | "Dropbox": DropboxFSI, 22 | "zip": ZipfileFSI, 23 | "Zip": ZipfileFSI, 24 | "ZIP": ZipfileFSI, 25 | "zipfile": ZipfileFSI, 26 | } 27 | 28 | # update with extensions 29 | extensions = load_from_dir( 30 | dirpath=_STASH_EXTENSION_FSI_PATH, 31 | varname="FSIS", 32 | ) 33 | for ext in extensions: 34 | if not isinstance(ext, dict): 35 | continue 36 | else: 37 | FILESYSTEM_TYPES.update(ext) 38 | 39 | # alias used by mc 40 | INTERFACES = FILESYSTEM_TYPES 41 | -------------------------------------------------------------------------------- /bin/uniq.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Print standard input or files, omitting repeated lines""" 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import sys 8 | import fileinput 9 | import argparse 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("files", nargs="*", help="files to unique (must be sorted first)") 15 | ns = ap.parse_args(args) 16 | 17 | def _print(lines): 18 | if lines is not None: 19 | print("".join(lines)) 20 | 21 | fileinput.close() # in case it is not closed 22 | try: 23 | prev_line = None 24 | lines = None 25 | for line in fileinput.input(ns.files, openhook=fileinput.hook_encoded("utf-8")): 26 | if fileinput.isfirstline(): 27 | _print(lines) 28 | lines = [] 29 | prev_line = None 30 | if prev_line is None or line != prev_line: 31 | lines.append(line) 32 | prev_line = line 33 | 34 | _print(lines) 35 | 36 | finally: 37 | fileinput.close() 38 | 39 | 40 | if __name__ == "__main__": 41 | main(sys.argv[1:]) 42 | -------------------------------------------------------------------------------- /bin/rmdir.py: -------------------------------------------------------------------------------- 1 | #! python2 2 | # -*- coding: utf-8 -*- 3 | # StaSh utility - Dutcho, 17 Apr 2017 4 | """Remove empty directory""" 5 | 6 | from __future__ import print_function 7 | 8 | import argparse 9 | import os 10 | import sys 11 | 12 | 13 | def rmdir(dirnames, verbose=False): 14 | for dirname in dirnames: 15 | try: 16 | os.rmdir(dirname) 17 | if verbose: 18 | print("Removed directory {!r}".format(dirname)) 19 | except OSError as e: 20 | print( 21 | "Cannot remove directory {!r}: {}".format(dirname, e), file=sys.stderr 22 | ) 23 | 24 | 25 | # --- main 26 | def main(args): 27 | parser = argparse.ArgumentParser( 28 | description=__doc__, epilog='Use "rm -r" to remove non-empty directory tree' 29 | ) 30 | parser.add_argument("dir", help="directories to remove", action="store", nargs="+") 31 | parser.add_argument( 32 | "-v", 33 | "--verbose", 34 | help="display info for each processed directory", 35 | action="store_true", 36 | ) 37 | ns = parser.parse_args(args) 38 | rmdir(ns.dir, ns.verbose) 39 | 40 | 41 | if __name__ == "__main__": 42 | main(sys.argv[1:]) 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | #lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | .pytest_cache 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | # IDE 58 | .idea 59 | 60 | # Vim 61 | *.swp 62 | 63 | # Misc 64 | tmp* 65 | debug 66 | 67 | # StaSh dot files 68 | .stash_history 69 | .stashrc 70 | .stash_config 71 | 72 | .mailrc 73 | .pcsm.json 74 | .pypi_packages 75 | 76 | # from tests 77 | history_test_s_l 78 | tests/pip/data/stpkg.zip 79 | -------------------------------------------------------------------------------- /docs/user_input_sequence.txt: -------------------------------------------------------------------------------- 1 | title: User Input 2 | participant User [icon=human] 3 | User->TvDelegate: type text 4 | TvDelegate->MiniBuffer: feed(range, replacement) 5 | MiniBuffer->MiniBuffer: adjust_range 6 | MiniBuffer->MainScreen: lock 7 | MiniBuffer->MainScreen: ensure consistency 8 | alt: [range span > 1] 9 | MiniBuffer->MiniBuffer: delete 10 | MiniBuffer->MainScreen: delete 11 | end 12 | MiniBuffer->MainScreen: release lock 13 | alt: [replacement == ''] 14 | MiniBuffer->Renderer: render 15 | elsealt: [replacement == '\\t'] 16 | MiniBuffer->Completer: complete 17 | MiniBuffer->MainScreen: lock 18 | MiniBuffer->MiniBuffer: modify chars 19 | MiniBuffer->MainScreen: draw chars 20 | MiniBuffer->MainScreen: release lock 21 | MiniBuffer->Renderer: render 22 | else: 23 | MiniBuffer->MainScreen: lock 24 | MiniBuffer->MiniBuffer: add chars 25 | MiniBuffer->MainScreen: draw chars 26 | MiniBuffer->MainScreen: release lock 27 | MiniBuffer->Renderer: render 28 | alt: [chars contains '\\0' or '\\n'] 29 | MiniBuffer->IO: push chars 30 | MiniBuffer->Runtime: callback 31 | Runtime-->>Right: execute command 32 | end 33 | end 34 | MiniBuffer-->TvDelegate: done 35 | TvDelegate-->User: 36 | -------------------------------------------------------------------------------- /man/manpages.txt: -------------------------------------------------------------------------------- 1 | The 'man' command 2 | ------------------ 3 | - If no arguments are given, man wil list all commands in '$BIN_PATH'. 4 | - If the argument "topics" is given, list all non-command topics. 5 | - If one argument is given and a command has this name, man will print the docstring of this command. 6 | - If one argument is given and no command has this name, man will search for files and folders with this name in '$STASH_ROOT/man/' and '$HOME2/stash_extensions/man/'. 7 | man will ignore file-extensions and numbers in brackets when searching for files. 8 | - If a file with this name has been found, man will check the file extenson. 9 | - If the extension is '.txt' or unknown, man will print the content of the file. 10 | - If the extension is '.html', man will open the file in quicklook. 11 | - If the extension is '.url': 12 | - If the content of the file starts with 'stash://', man will restart it's search in the path specified in the content of the file, replacing 'stash://' with '$STASH_ROOT/'. 13 | - If a dir with this name has been found, man will check whether the argument passed contains brackets with a number between. If no number is given, man will assume it to be 1. man will then show the page starting with page_N, where n is the number. 14 | -------------------------------------------------------------------------------- /man/mounting/page_4.txt: -------------------------------------------------------------------------------- 1 | MOUNTING(4) --- USAGE 2 | ===================== 3 | 4 | This page only contains a few explanations. Some arguments will be not be explained. 5 | To see the detailed usage of a command, pass '--help' as argument. 6 | 7 | mount: 8 | -v, --verbose: 9 | this does not only show more informations, it also sets the FSI in debug mode. 10 | -r, --read-only: 11 | This option disables the use of some I/O-operations. 12 | -f --fake: 13 | This does not mount the FSI, but disconnects it directly after it was connected. 14 | -y, --yes: 15 | By default, mount asks the user if he really wants to enable the monkeypatches. When this argument is passed, it wont ask. 16 | -t TYPE, --type TYPE: 17 | Required. The name of the FSI to use. 18 | options: 19 | options to pass to the FSI's connect()-method. These are often used for hostnames or usernames. 20 | dir: 21 | Required. A dir to mount the FS on. The content of the dir will appear to be the contentd of the FS. The dir needs to exists. 22 | 23 | 24 | umount/unmount: 25 | -f, --force: 26 | Usefull when the FSI is not responding. This does remove the FS from the dir it was mounted on, but wont call the FSI's close()-method. This may result in data-loss, but will ensure that the dir is accessible again. 27 | -------------------------------------------------------------------------------- /tools/pythonista_reinstall.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This tool will uninstall StaSh from pythonista and then download&run getstash.py 4 | """ 5 | 6 | import os 7 | import shutil 8 | import tempfile 9 | 10 | import requests 11 | import six 12 | 13 | 14 | def get_stash_dir(): 15 | return os.path.join(os.path.expanduser("~"), "Documents", "site-packages", "stash") 16 | 17 | 18 | def remove_stash(): 19 | shutil.rmtree(get_stash_dir()) 20 | 21 | 22 | def install_stash(repo="ywangd", branch="master"): 23 | if "TMPDIR" not in os.environ: 24 | os.environ["TMPDIR"] = tempfile.gettempdir() 25 | ns = {"_owner": repo, "_br": branch} 26 | exec(requests.get("https://bit.ly/get-stash").content, ns, ns) 27 | 28 | 29 | def parse_gh_target(s): 30 | if s == "": 31 | return "ywangd", "master" 32 | s = s.replace("/", ":") 33 | if ":" not in s: 34 | s = "ywangd:" + s 35 | repo, branch = s.split(":") 36 | return repo, branch 37 | 38 | 39 | def main(): 40 | ts = six.moves.input("New target (repo:branch, empty for default): ") 41 | t = parse_gh_target(ts) 42 | if os.path.exists(get_stash_dir()): 43 | remove_stash() 44 | install_stash(*t) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /bin/sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Sort standard input or given files to standard output""" 3 | 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import fileinput 8 | import argparse 9 | 10 | 11 | def main(args): 12 | ap = argparse.ArgumentParser() 13 | ap.add_argument("files", nargs="*", help="files to sort") 14 | ap.add_argument( 15 | "-r", 16 | "--reverse", 17 | action="store_true", 18 | default=False, 19 | help="reverse the result of comparisons", 20 | ) 21 | ns = ap.parse_args(args) 22 | 23 | def _print(lines): 24 | if lines is not None: 25 | lines = sorted(lines) 26 | if ns.reverse: 27 | lines = lines[::-1] 28 | print("".join(lines)) 29 | 30 | fileinput.close() # in case it is not closed 31 | try: 32 | lines = None 33 | for line in fileinput.input(ns.files, openhook=fileinput.hook_encoded("utf-8")): 34 | if fileinput.isfirstline(): 35 | _print(lines) 36 | lines = [] 37 | lines.append(line) 38 | 39 | _print(lines) 40 | 41 | finally: 42 | fileinput.close() 43 | 44 | 45 | if __name__ == "__main__": 46 | main(sys.argv[1:]) 47 | -------------------------------------------------------------------------------- /lib/mlpatches/os_process.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module contains patches for the 'os'-module to make StaSh's thread-system like a process-system (from the view of the script)""" 4 | 5 | import os 6 | import threading 7 | from mlpatches import base, os_popen 8 | 9 | _stash = base._stash 10 | 11 | 12 | def getpid(patch): 13 | """Return the current process id.""" 14 | ct = threading.current_thread() 15 | if isinstance(ct, _stash.runtime.ShThread): 16 | return ct.job_id 17 | else: 18 | return -1 19 | 20 | 21 | def getppid(patch): 22 | """Return the parents process id.""" 23 | ct = threading.current_thread() 24 | if isinstance(ct, _stash.runtime.ShThread): 25 | pt = ct.parent 26 | else: 27 | return -1 28 | if hasattr(pt, "job_id"): 29 | return pt.job_id 30 | else: 31 | # ShRuntime 32 | return 0 33 | 34 | 35 | def kill(patch, pid, sig): 36 | """Send signal sig to the process pid. Constants for the specific signals available on the host platform are defined in the signal module""" 37 | io = os_popen.VoidIO() 38 | _stash( 39 | "kill {pid}".format(pid=pid), 40 | add_to_history=False, 41 | final_errs=io, 42 | final_outs=io, 43 | final_ins=io, 44 | ) 45 | -------------------------------------------------------------------------------- /bin/cd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Change the current working directory.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import os 9 | import sys 10 | 11 | 12 | def main(args): 13 | p = argparse.ArgumentParser(description=__doc__) 14 | p.add_argument( 15 | "dir", 16 | action="store", 17 | nargs="?", 18 | default=os.environ["HOME2"], 19 | help="the new working directory", 20 | ) 21 | ns = p.parse_args(args) 22 | 23 | status = 0 24 | 25 | try: 26 | if os.path.exists(ns.dir): 27 | if os.path.isdir(ns.dir): 28 | # chdir does not raise exception until listdir is called, so check for access here 29 | if os.access(ns.dir, os.R_OK): 30 | os.chdir(ns.dir) 31 | else: 32 | print("cd: {} access denied".format(ns.dir)) 33 | else: 34 | print("cd: %s: Not a directory" % ns.dir) 35 | else: 36 | print("cd: %s: No such file or directory" % ns.dir) 37 | except Exception as err: 38 | print("cd: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 39 | status = 1 40 | 41 | sys.exit(status) 42 | 43 | 44 | if __name__ == "__main__": 45 | main(sys.argv[1:]) 46 | -------------------------------------------------------------------------------- /bin/pbcopy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Copy one or more files to the system clipboard""" 3 | 4 | from __future__ import print_function 5 | 6 | import argparse 7 | import fileinput 8 | import os 9 | import sys 10 | 11 | 12 | _stash = globals()["_stash"] 13 | 14 | 15 | def main(args): 16 | """ 17 | The main function. 18 | """ 19 | ap = argparse.ArgumentParser() 20 | ap.add_argument("file", nargs="*", help="one or more files to be copied") 21 | ns = ap.parse_args(args) 22 | 23 | if not hasattr(_stash, "libdist"): 24 | print(_stash.text_color("Error: libdist not loaded.", "red")) 25 | sys.exit(1) 26 | 27 | fileinput.close() # in case it is not closed 28 | try: 29 | _stash.libdist.clipboard_set( 30 | "".join( 31 | line 32 | for line in fileinput.input( 33 | ns.file, openhook=fileinput.hook_encoded("utf-8") 34 | ) 35 | ) 36 | ) 37 | except Exception as err: 38 | print( 39 | _stash.text_color( 40 | "pbcopy: {}: {!s}".format(type(err).__name__, err), "red" 41 | ), 42 | file=sys.stderr, 43 | ) 44 | sys.exit(1) 45 | finally: 46 | fileinput.close() 47 | 48 | 49 | if __name__ == "__main__": 50 | main(sys.argv[1:]) 51 | -------------------------------------------------------------------------------- /tests/misc/test_pwd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'pwd' command.""" 3 | 4 | import os 5 | 6 | from stash.tests.stashtest import StashTestCase 7 | 8 | 9 | class PwdTests(StashTestCase): 10 | """tests for the 'pwd' command.""" 11 | 12 | cwd = os.path.expanduser("~") 13 | 14 | def test_help(self): 15 | """test 'pwd --help'.""" 16 | output = self.run_command("pwd --help") 17 | self.assertIn("pwd", output) 18 | self.assertIn("-h", output) 19 | self.assertIn("--help", output) 20 | self.assertIn("-b", output) 21 | self.assertIn("--basename", output) 22 | self.assertIn("-f", output) 23 | self.assertIn("--fullname", output) 24 | 25 | def test_pwd_collapseuser(self): 26 | """tests 'pwd'.""" 27 | output = self.run_command("pwd").replace("\n", "").replace("/", "") 28 | self.assertEqual(output, "~") 29 | 30 | def test_pwd_fullname(self): 31 | """tests 'pwd --fullname'.""" 32 | output = self.run_command("pwd --fullname").replace("\n", "") 33 | self.assertEqual(output, os.path.abspath(os.getcwd())) 34 | 35 | def test_pwd_basename(self): 36 | """tests 'pwd --basename'.""" 37 | output = self.run_command("pwd --basename").replace("\n", "") 38 | self.assertEqual(output, os.path.basename(os.getcwd())) 39 | -------------------------------------------------------------------------------- /lib/stashutils/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """core utilities for StaSh-scripts""" 3 | 4 | import threading 5 | import imp 6 | import os 7 | 8 | from stash.system import shthreads 9 | 10 | 11 | def get_stash(): 12 | """ 13 | returns the currently active StaSh-instance. 14 | returns None if it can not be found. 15 | This is useful for modules. 16 | """ 17 | if "_stash" in globals(): 18 | return globals()["_stash"] 19 | for thr in threading.enumerate(): 20 | if isinstance(thr, shthreads.ShBaseThread): 21 | ct = thr 22 | while not ct.is_top_level(): 23 | ct = ct.parent 24 | return ct.parent.stash 25 | return None 26 | 27 | 28 | def load_from_dir(dirpath, varname): 29 | """ 30 | returns a list of all variables named 'varname' in .py files in a directofy 'dirname'. 31 | """ 32 | if not os.path.isdir(dirpath): 33 | return [] 34 | ret = [] 35 | for fn in os.listdir(dirpath): 36 | fp = os.path.join(dirpath, fn) 37 | if not os.path.isfile(fp): 38 | continue 39 | with open(fp, "r") as fin: 40 | mod = imp.load_source(fn[: fn.index(".")], fp, fin) 41 | if not hasattr(mod, varname): 42 | continue 43 | else: 44 | ret.append(getattr(mod, varname)) 45 | return ret 46 | -------------------------------------------------------------------------------- /tests/misc/test_exit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'exit' command.""" 3 | 4 | from stash.tests.stashtest import StashTestCase 5 | 6 | 7 | class ExitTests(StashTestCase): 8 | """Tests for the 'exit' command.""" 9 | 10 | def test_help(self): 11 | """test 'exit --help'.""" 12 | output = self.run_command("exit --help", exitcode=0) 13 | self.assertIn("-h", output) 14 | self.assertIn("--help", output) 15 | self.assertIn("status", output) 16 | self.assertIn("exit", output) 17 | 18 | def test_exit_default(self): 19 | """test 'exit'.""" 20 | output = self.run_command("exit", exitcode=0).replace("\n", "") 21 | self.assertEqual(output, "") 22 | 23 | def test_exit_0(self): 24 | """test 'exit 0'.""" 25 | output = self.run_command("exit 0", exitcode=0).replace("\n", "") 26 | self.assertEqual(output, "") 27 | 28 | def test_exit_1(self): 29 | """test 'exit 1'.""" 30 | output = self.run_command("exit 1", exitcode=1).replace("\n", "") 31 | self.assertEqual(output, "") 32 | 33 | def test_exit_0_to_255(self): 34 | """test 'exit {i}' where i = 0, ..., 255.""" 35 | for i in range(256): 36 | output = self.run_command("exit " + str(i), exitcode=i).replace("\n", "") 37 | self.assertEqual(output, "") 38 | -------------------------------------------------------------------------------- /data/pip_blocklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "reasons": { 3 | "C": "This package uses C-Code, which can not be compiled by StaSh.", 4 | "unknown": "Reason unknown.", 5 | "is_stash": "The name of the package conflicts with StaSh.", 6 | "is_pip": "StaSh uses a custom version of PIP. Installing the 'pip' package would likely break StaSh.", 7 | "lowlevel_os": "This package uses (relatively) low-level OS functions, which can not be used with Pythonista.", 8 | "processes": "This package requires process interaction, which is not possible on iOS", 9 | "bundled": "This package is already bundled with Pythonista", 10 | "incompatible_dependency": "This package has one or more dependencies which are incompatible with Pythonista.", 11 | "bugged": "Installation of this packages is currently critically bugged." 12 | }, 13 | "blocklist": { 14 | "stash": ["is_stash", true, null], 15 | "pip": ["is_pip", true, null], 16 | "selenium": ["processes", true, null], 17 | "matplotlib": ["bundled", false, null], 18 | "mypy": ["C", true, null], 19 | "eventlet": ["C", true, null], 20 | "scipy": ["C", true, null], 21 | "tensorflow": ["C", true, null], 22 | "pyobjc": ["C", true, null], 23 | "pyttsx3": ["incompatible_dependency", true, null], 24 | "futures": ["bugged", true, null], 25 | "regex": ["C", true, null], 26 | "gevent": ["C", true, null], 27 | "grequests": ["incompatible_dependency", true, null] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/misc/test_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'version' command.""" 3 | 4 | import platform 5 | 6 | from stash.tests.stashtest import StashTestCase 7 | 8 | 9 | class VersionTests(StashTestCase): 10 | """Tests for the 'version' command.""" 11 | 12 | def test_keys(self): 13 | """ensure keys like 'core.py' are in the output of 'version'""" 14 | output = self.run_command("version", exitcode=0) 15 | self.assertIn("StaSh", output) 16 | self.assertIn("Python", output) 17 | self.assertIn("UI", output) 18 | self.assertIn("root", output) 19 | self.assertIn("core.py", output) 20 | # skip iOS version because we run the tests on linux (i think) 21 | self.assertIn("Platform", output) 22 | self.assertIn("SELFUPDATE_TARGET", output) 23 | self.assertIn("BIN_PATH", output) 24 | self.assertIn("PYTHONPATH", output) 25 | self.assertIn("Loaded libraries", output) 26 | 27 | def test_correct_py_version(self): 28 | """test that the correct python version will be reported.""" 29 | output = self.run_command("version", exitcode=0) 30 | self.assertIn(platform.python_version(), output) 31 | 32 | def test_correct_stash_version(self): 33 | """test that the correct stash version will be reported.""" 34 | output = self.run_command("version", exitcode=0) 35 | self.assertIn(self.stash.__version__, output) 36 | -------------------------------------------------------------------------------- /lib/mlpatches/tests/sp_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """subprocess tests.""" 3 | 4 | from __future__ import print_function 5 | import subprocess 6 | 7 | _stash = globals()["_stash"] 8 | text = _stash.text_color 9 | 10 | # exit test 11 | print(text("starting exit test...", "yellow")) 12 | try: 13 | subprocess.check_call("exit 0") 14 | print(text(" 0 OK", "green")) 15 | except subprocess.CalledProcessError: 16 | print(text(" 0 ERROR", "red")) 17 | try: 18 | subprocess.check_call("exit 1") 19 | print(text(" 1 ERROR", "red")) 20 | except subprocess.CalledProcessError: 21 | print(text(" 1 OK", "green")) 22 | 23 | # parent I/O 24 | print(text("\nstarting parent I/O test...", "yellow")) 25 | print(" please check for I/O") 26 | subprocess.call("man readme") 27 | 28 | # output test 29 | print(text("\nstarting check_output test...", "yellow")) 30 | string = "Test" 31 | try: 32 | output = subprocess.check_output("echo " + string) 33 | if output.endswith("\n") and (not string.endswith("\n")): 34 | output = output[:-1] 35 | if output != string: 36 | print(text(" 0 ERROR output does not match!\n " + output, "red")) 37 | else: 38 | print(text(" 0 OK", "green")) 39 | except subprocess.CalledProcessError: 40 | print(text(" 0 ERROR", "red")) 41 | try: 42 | output = subprocess.check_output("gci wasd") 43 | print(text(" 1 ERROR", "red")) 44 | except subprocess.CalledProcessError: 45 | print(text(" 1 OK", "green")) 46 | -------------------------------------------------------------------------------- /tests/misc/test_echo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'echo' command.""" 3 | 4 | from unittest import expectedFailure 5 | 6 | from stash.tests.stashtest import StashTestCase 7 | 8 | 9 | class EchoTests(StashTestCase): 10 | """tests for the 'echo' command.""" 11 | 12 | def do_echo(self, s): 13 | """echo a string and return the echoed output.""" 14 | return self.run_command("echo " + s, exitcode=0) 15 | 16 | def test_simple(self): 17 | """test 'echo test'""" 18 | o = self.do_echo("test") 19 | self.assertEqual(o, "test\n") 20 | 21 | def test_multi(self): 22 | """test 'echo test1 test2 test:'""" 23 | o = self.do_echo("test1 test2 test3") 24 | self.assertEqual(o, "test1 test2 test3\n") 25 | 26 | def test_help_ignore(self): 27 | """test that -h and --help will be ignored by echo.""" 28 | ho = self.do_echo("-h") 29 | self.assertEqual(ho, "-h\n") 30 | helpo = self.do_echo("--help") 31 | self.assertEqual(helpo, "--help\n") 32 | 33 | def test_empty(self): 34 | """test the behavior without arguments.""" 35 | output = self.run_command("echo", exitcode=0) 36 | self.assertEqual(output, "\n") 37 | 38 | @expectedFailure 39 | def test_non_ascii(self): 40 | """test echo with non-ascii characters.""" 41 | output = self.do_echo("Non-Ascii: äöüß end") 42 | self.assertEqual(output, "Non-Ascii: äöüß end\n") 43 | -------------------------------------------------------------------------------- /tests/misc/test_totd.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the 'totd' command. 3 | """ 4 | 5 | from stash.tests.stashtest import StashTestCase 6 | 7 | 8 | class TotdTests(StashTestCase): 9 | """ 10 | Tests for the 'totd' command. 11 | """ 12 | 13 | def test_help(self): 14 | """ 15 | Test 'totd --help'. 16 | """ 17 | output = self.run_command("totd --help", exitcode=0) 18 | self.assertIn("totd", output) 19 | self.assertIn("-h", output) 20 | self.assertIn("--help", output) 21 | self.assertIn("-n", output) 22 | self.assertIn("--count", output) 23 | 24 | def test_count(self): 25 | """ 26 | Test 'totd --count'. 27 | """ 28 | output = self.run_command("totd --count", exitcode=0).replace("\n", "") 29 | # ensure that the string is correct 30 | self.assertTrue(output.startswith("Total available tips: ")) 31 | # ensure that number of tips is not zero 32 | self.assertFalse(output.endswith(" ")) 33 | 34 | def test_simple(self): 35 | """ 36 | Test a simple 'totd' execution. 37 | Ensure that different totds are returned. 38 | """ 39 | known = [] 40 | n_unique = 0 41 | for i in range(100): 42 | output = self.run_command("totd", exitcode=0).replace("\n", "") 43 | if output not in known: 44 | known.append(output) 45 | n_unique += 1 46 | self.assertGreater(n_unique, 3) 47 | -------------------------------------------------------------------------------- /bin/source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Read and execute commands from a shell script in the current environment. 4 | 5 | usage: source.py [-h] file [args [args ...]] 6 | 7 | positional arguments: 8 | file file to be sourced 9 | args arguments to the file being sourced 10 | 11 | optional arguments: 12 | -h, --help show this help message and exit 13 | """ 14 | 15 | from __future__ import print_function 16 | 17 | import sys 18 | from argparse import ArgumentParser 19 | 20 | 21 | def main(args): 22 | ap = ArgumentParser( 23 | description="Read and execute commands from a shell script in the current environment" 24 | ) 25 | ap.add_argument("file", action="store", help="file to be sourced") 26 | ap.add_argument("args", nargs="*", help="arguments to the file being sourced") 27 | ns = ap.parse_args(args) 28 | 29 | _stash = globals()["_stash"] 30 | """:type : StaSh""" 31 | 32 | _, current_state = _stash.runtime.get_current_worker_and_state() 33 | 34 | # The special thing about source is it persists any environmental changes 35 | # in the sub-shell to the parent shell. 36 | try: 37 | with open(ns.file) as ins: 38 | _stash(ins.readlines(), persistent_level=1) 39 | 40 | except IOError as e: 41 | print("%s: %s" % (e.filename, e.strerror)) 42 | except Exception as e: 43 | print("error: %s" % str(e)) 44 | finally: 45 | pass 46 | 47 | 48 | if __name__ == "__main__": 49 | main(sys.argv[1:]) 50 | -------------------------------------------------------------------------------- /bin/ln.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | import re 6 | import json 7 | import argparse 8 | import time 9 | import pytz 10 | import console 11 | from datetime import datetime, timedelta 12 | from difflib import unified_diff, ndiff 13 | 14 | 15 | def argue(): 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument("-v", "--verbose", action="store_true") 18 | parser.add_argument("-s", "--symbolic", action="store_true") 19 | parser.add_argument("-f", "--force", action="store_true") 20 | parser.add_argument("lhs") 21 | parser.add_argument("rhs") 22 | args = parser.parse_args() 23 | if args.verbose: 24 | json.dump(vars(args), sys.stderr, indent=4) 25 | return args 26 | 27 | 28 | def ln(lhs, rhs, symbolic=False): 29 | if not os.path.exists(lhs): 30 | sys.stderr.write("%s not found\n" % lhs) 31 | sys.exit(1) 32 | if os.path.isdir(rhs): 33 | rhs = "%s/%s" % (rhs, os.path.basename(lhs)) 34 | if os.path.isfile(rhs): 35 | sys.stderr.write("%s already exists\n" % rhs) 36 | sys.exit(1) 37 | if os.path.islink(rhs): 38 | sys.stderr.write("%s already linked\n" % rhs) 39 | sys.exit(1) 40 | 41 | if symbolic: 42 | os.symlink(lhs, rhs) 43 | else: 44 | os.link(lhs, rhs) 45 | return 46 | 47 | 48 | def main(): 49 | console.clear() 50 | args = argue() 51 | ln(args.lhs, args.rhs, args.symbolic) 52 | return 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /bin/xargs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Construct argument list(s) and execute utility""" 3 | 4 | import os 5 | import sys 6 | import argparse 7 | 8 | _stash = globals()["_stash"] 9 | 10 | 11 | def main(args): 12 | ap = argparse.ArgumentParser() 13 | ap.add_argument( 14 | "-n", 15 | nargs="?", 16 | metavar="number", 17 | type=int, 18 | help="maximum number of arguments taken from standard input for each invocation of utility", 19 | ) 20 | 21 | ap.add_argument("-I", dest="replstr", nargs="?", help="replacement string") 22 | 23 | ap.add_argument("utility", nargs="?", default="echo", help="utility to invoke") 24 | 25 | ap.add_argument( 26 | "args_to_pass", 27 | metavar="arguments", 28 | nargs=argparse.REMAINDER, 29 | help="arguments to the utility", 30 | ) 31 | 32 | ns = ap.parse_args(args) 33 | 34 | lines = [line.strip() for line in sys.stdin.readlines()] 35 | n = ns.n if ns.n else len(lines) 36 | if ns.replstr: 37 | n = 1 38 | 39 | while lines: 40 | rest = " ".join(lines[:n]) 41 | lines = lines[n:] 42 | args_to_pass = " ".join(ns.args_to_pass) 43 | 44 | if rest.strip(): 45 | if ns.replstr: 46 | args_to_pass = args_to_pass.replace(ns.replstr, rest) 47 | rest = "" 48 | 49 | cmdline = "%s %s %s" % (ns.utility, args_to_pass, rest) 50 | 51 | _stash(cmdline) 52 | 53 | 54 | if __name__ == "__main__": 55 | main(sys.argv[1:]) 56 | -------------------------------------------------------------------------------- /bin/printhex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Print the given files' content and hexadecimal byte values.""" 4 | 5 | from __future__ import print_function 6 | 7 | import argparse 8 | import sys 9 | 10 | INVISIBLE = range(0x20) + [0x81, 0x8D, 0x8F, 0x90, 0x9D] 11 | 12 | 13 | def main(args): 14 | p = argparse.ArgumentParser(description=__doc__) 15 | p.add_argument( 16 | "file", action="store", nargs="+", help="one or more files to be printed" 17 | ) 18 | ns = p.parse_args(args) 19 | 20 | status = 0 21 | 22 | for filename in ns.file: 23 | try: 24 | with open(filename, "rb") as f: 25 | i = 0 26 | chunk = f.read(16) 27 | while chunk: 28 | # Decoding as Latin-1 to get a visual representation for most 29 | # bytes that would otherwise be non-printable. 30 | strchunk = "".join( 31 | "_" if ord(c) in INVISIBLE else c.decode("windows-1252") 32 | for c in chunk 33 | ) 34 | hexchunk = " ".join("{:0>2X}".format(ord(c)) for c in chunk) 35 | print("0x{:>08X} | {:<48} | {:<16}".format(i, hexchunk, strchunk)) 36 | i += 16 37 | chunk = f.read(16) 38 | 39 | except Exception as err: 40 | print("printhex: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 41 | status = 1 42 | 43 | sys.exit(status) 44 | 45 | 46 | if __name__ == "__main__": 47 | main(sys.argv[1:]) 48 | -------------------------------------------------------------------------------- /bin/stashconf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """List and set StaSh configuration options""" 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import argparse 8 | 9 | _stash = globals()["_stash"] 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("name", nargs="?", help="variable name") 15 | ap.add_argument("value", nargs="?", type=int, help="variable value") 16 | ap.add_argument( 17 | "-l", 18 | "--list", 19 | action="store_true", 20 | help="list all config variables and their values", 21 | ) 22 | ns = ap.parse_args(args) 23 | 24 | config = { 25 | "py_traceback": _stash.runtime, 26 | "py_pdb": _stash.runtime, 27 | "input_encoding_utf8": _stash.runtime, 28 | "ipython_style_history_search": _stash.runtime.history, 29 | "enable_styles": _stash, 30 | "colored_errors": _stash.runtime, 31 | } 32 | 33 | if ns.list: 34 | for name in sorted(config.keys()): 35 | print("%s=%s" % (name, config[name].__dict__[name])) 36 | 37 | else: 38 | try: 39 | if ns.name is not None and ns.value is not None: 40 | config[ns.name].__dict__[ns.name] = ns.value 41 | elif ns.name is not None: 42 | print("%s=%s" % (ns.name, config[ns.name].__dict__[ns.name])) 43 | else: 44 | ap.print_help() 45 | 46 | except KeyError: 47 | print("%s: invalid config option name" % ns.name) 48 | sys.exit(1) 49 | 50 | 51 | if __name__ == "__main__": 52 | main(sys.argv[1:]) 53 | -------------------------------------------------------------------------------- /man/mounting/page_5.txt: -------------------------------------------------------------------------------- 1 | MOUNTING(5) --- FSIs 2 | ===================== 3 | 4 | An FSI is a class which defines an API to access the target filesystem. 5 | This page describes the usage of them. 6 | 7 | local, Local: 8 | Description: 9 | FSI for the local filesystem. Usefull for debugging. 10 | Arguments: 11 | 0: 12 | The rootpath. All other paths are seen as childs of this path. 13 | Example: 14 | FSI mounted on 'local' with arg0 '$STASH_ROOT'. 15 | 'local/bin/mount.py' points to '$STASH_ROOT/bin/mount.py'. 16 | Defaults to '/'. 17 | 18 | dropbox, Dropbox, DropBox: 19 | Description: 20 | FSI for the Dropbox. May be slow. 21 | Arguments: 22 | 0: 23 | Required. Name of the Dropbox-config as set in the 'dropbox_setup'-command. 24 | 25 | ftp, FTP: 26 | Description: 27 | FSI for FTP-Servers. May not work on all FTP-Servers. 28 | Arguments: 29 | 0: 30 | Required. The IP-address of the host. 31 | 1: 32 | The port. Defaults to 21. 33 | 2: 34 | The username to use for login. Default: anonymous login. 35 | 3: 36 | Required if username was passed. The password the use for login. 37 | 4. 38 | Mode. One of: 39 | -n: default. Insecure connection. 40 | -s: secure connection. Not supported by all servers. 41 | -d: debug mode and secure connection. 42 | 43 | zip, ZIP, zipfile, Zip: 44 | Description: 45 | FSI which can be used to mount a '.zip'-file as a filesystem. 46 | Arguments: 47 | 0: 48 | Required. Zipfile to open (creating one if none was found.) 49 | 1: 50 | Password for zip. Only usable when reading an existing zip. Password-protection may be removed when changing the zip. 51 | -------------------------------------------------------------------------------- /.github/workflows/check-code-and-run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Check code and run tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | pre-commit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: actions/setup-python@v6 15 | with: 16 | python-version: 3.x 17 | - uses: pre-commit/action@v3.0.1 18 | 19 | build: 20 | runs-on: macos-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | python-version: ["3.10"] # ["2.7", "3.6", "3.10"] 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v6 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v6 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip wheel setuptools 35 | pip install ".[testing]" 36 | - name: Analysing the code with flake8 37 | run: | 38 | # F824 `global name` / `nonlocal name` is unused: name is never assigned in scope 39 | flake8 . --count --select=E9,F63,F7,F82 --ignore=F824 --show-source --statistics 40 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics 41 | - name: Running tests 42 | run: | 43 | pytest --version 44 | # make install test work... 45 | export PYTHONPATH=$(python -m site --user-site) 46 | pytest tests/ --ignore=tests/system/data/ --showlocals --verbose --show-capture=all --log-level=debug 47 | -------------------------------------------------------------------------------- /bin/alias.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Example of accessing the shell object from script 3 | # This ability completely removes the need of plugins 4 | """List or define shell aliases.""" 5 | 6 | from __future__ import print_function 7 | 8 | import sys 9 | import argparse 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("expr", nargs="?", help="name=value") 15 | 16 | ns = ap.parse_args(args) 17 | 18 | app = globals()["_stash"] 19 | """:type : StaSh""" 20 | 21 | _, current_state = app.runtime.get_current_worker_and_state() 22 | 23 | if ns.expr is None: 24 | for k, v in current_state.aliases.items(): 25 | print("{}={}".format(k, v[0])) 26 | 27 | else: 28 | if "=" in ns.expr: 29 | name, value = ns.expr.split("=", 1) 30 | if name == "" or value == "": 31 | raise ValueError("alias: invalid name=value expression") 32 | 33 | tokens, parsed = app.runtime.parser.parse(value) 34 | # Ensure the actual form of an alias is fully expanded 35 | tokens, _ = app.runtime.expander.alias_subs(tokens, parsed, exclude=name) 36 | value_expanded = " ".join(t.tok for t in tokens) 37 | current_state.aliases[name] = (value, value_expanded) 38 | sys.exit(0) 39 | else: 40 | try: 41 | print("{}={}".format(ns.expr, current_state.aliases[ns.expr])) 42 | except KeyError as err: 43 | raise KeyError("alias: {} not found".format(err.message)) 44 | 45 | 46 | if __name__ == "__main__": 47 | main(sys.argv[1:]) 48 | -------------------------------------------------------------------------------- /bin/pbpaste.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Writes the contents of the system clipboard to a file.""" 3 | 4 | from __future__ import print_function 5 | 6 | import argparse 7 | import os 8 | import sys 9 | import io 10 | 11 | import six 12 | 13 | 14 | _stash = globals()["_stash"] 15 | 16 | 17 | def main(args): 18 | ap = argparse.ArgumentParser() 19 | ap.add_argument("file", nargs="?", help="the file to be pasted") 20 | ns = ap.parse_args(args) 21 | 22 | status = 0 23 | 24 | if not hasattr(_stash, "libdist"): 25 | print(_stash.text_color("Error: libdist not loaded.", "red")) 26 | sys.exit(1) 27 | 28 | content = _stash.libdist.clipboard_get() 29 | if ns.file: 30 | if os.path.exists(ns.file): 31 | print( 32 | _stash.text_color("pbpaste: {}: file exists".format(ns.file), "red"), 33 | file=sys.stderr, 34 | ) 35 | status = 1 36 | else: 37 | try: 38 | if isinstance(content, six.binary_type): 39 | with io.open(ns.file, "wb") as f: 40 | f.write(content) 41 | else: 42 | with io.open(ns.file, "w", encoding="utf-8") as f: 43 | f.write(content) 44 | except Exception as err: 45 | print( 46 | "pbpaste: {}: {!s}".format(type(err).__name__, err), file=sys.stderr 47 | ) 48 | status = 1 49 | else: 50 | print(content, end="") 51 | 52 | sys.exit(status) 53 | 54 | 55 | if __name__ == "__main__": 56 | main(sys.argv[1:]) 57 | -------------------------------------------------------------------------------- /tests/misc/test_alias.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'alias' command""" 3 | 4 | from stash.tests.stashtest import StashTestCase 5 | 6 | 7 | class AliasTests(StashTestCase): 8 | """Tests for the 'alias' command.""" 9 | 10 | def test_help(self): 11 | """test 'alias --help'""" 12 | output = self.run_command("alias --help", exitcode=0) 13 | self.assertIn("alias", output) 14 | self.assertIn("-h", output) 15 | self.assertIn("--help", output) 16 | self.assertIn("name=", output) 17 | 18 | def test_la_alias(self): 19 | """tests the unmount alias""" 20 | # assert existence 21 | output = self.run_command("alias", exitcode=0) 22 | self.assertIn("la=", output) 23 | 24 | # assert output identical 25 | output = self.run_command("la", exitcode=0) 26 | output_full = self.run_command("ls -a", exitcode=0) 27 | self.assertEqual(output, output_full) 28 | 29 | def test_alias(self): 30 | """create and test alias""" 31 | # ensure alias not yet defined 32 | output = self.run_command("alias", exitcode=0) 33 | self.assertNotIn("testalias", output) 34 | 35 | # create alias 36 | output = self.run_command( 37 | "alias 'testalias=echo alias test successfull!'", exitcode=0 38 | ) 39 | 40 | # ensure alias is defined 41 | output = self.run_command("alias", exitcode=0) 42 | self.assertIn("testalias=", output) 43 | 44 | # check output 45 | output = self.run_command("testalias", exitcode=0) 46 | self.assertIn("alias test successfull!", output) 47 | -------------------------------------------------------------------------------- /bin/webviewer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Opens the given URL in the webbrowser or an App.""" 3 | 4 | import argparse 5 | import webbrowser 6 | import ui 7 | from objc_util import on_main_thread 8 | 9 | 10 | @on_main_thread 11 | def open_webbrowser(url, modal=False): 12 | """opens the url in the webbrowser""" 13 | webbrowser.open(url, modal) 14 | 15 | 16 | def open_webview(url, modal=False): 17 | """opens the url in a view.""" 18 | v = ui.WebView() 19 | v.present("fullscreen") 20 | v.load_url(url) 21 | if modal: 22 | v.wait_modal() 23 | 24 | 25 | if __name__ == "__main__": 26 | parser = argparse.ArgumentParser(description=__doc__) 27 | parser.add_argument("url", help="url to open", action="store") 28 | parser.add_argument( 29 | "-m", 30 | "--modal", 31 | help="wait until the user closed the webbrowser", 32 | action="store_true", 33 | dest="modal", 34 | ) 35 | parser.add_argument( 36 | "-n", 37 | "--insecure", 38 | help="prefix the url with http:// instead of https:// if no prefix is given", 39 | action="store_const", 40 | const="http://", 41 | default="https://", 42 | dest="prefix", 43 | ) 44 | parser.add_argument( 45 | "-f", 46 | "--foreground", 47 | help="Open the url in the foreground", 48 | action="store_true", 49 | dest="foreground", 50 | ) 51 | ns = parser.parse_args() 52 | url = ns.url 53 | if "://" not in url: 54 | url = ns.prefix + url 55 | if not ns.foreground: 56 | open_webbrowser(url, ns.modal) 57 | else: 58 | open_webview(url, ns.modal) 59 | -------------------------------------------------------------------------------- /lib/mlpatches/tl_patches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """patches for making some vars thread-local""" 3 | 4 | import threading 5 | import copy 6 | from mlpatches import base 7 | 8 | 9 | class ThreadLocalVar(object): 10 | """creates a proxy to a thread-local version of passee var.""" 11 | 12 | # todo: maybe add lock? 13 | def __init__(self, var): 14 | self.__var = var 15 | self.__local = threading.local() 16 | self.__setattr__ = self.__setattr_ # set __settattr__ here 17 | 18 | def __getattr__(self, name): 19 | try: 20 | v = self.__local.var 21 | except AttributeError: 22 | v = self.__local.var = copy.deepcopy(self.__var) 23 | return getattr(v, name) 24 | 25 | def __setattr_(self, name, value): # keep missing "_" 26 | try: 27 | v = self.__local.var 28 | except AttributeError: 29 | v = self.__local.var = copy.deepcopy(self.__var) 30 | return setattr(v, name, value) 31 | 32 | def __delattr__(self, name): 33 | try: 34 | v = self.__local.var 35 | except AttributeError: 36 | v = self.__local.var = copy.deepcopy(self.__var) 37 | return delattr(v, name) 38 | 39 | def __del__(self): 40 | try: 41 | del self.__local.var 42 | except AttributeError: 43 | pass 44 | 45 | 46 | # define patches 47 | 48 | 49 | class ThreadLocalArgv(base.FunctionPatch): 50 | """Patches sys.argv to be thread-local.""" 51 | 52 | PY2 = True 53 | PY3 = True 54 | module = "sys" 55 | function = "argv" 56 | replacement = ThreadLocalVar([]) 57 | 58 | 59 | # create patch instances 60 | TL_ARGV_PATCH = ThreadLocalArgv() 61 | -------------------------------------------------------------------------------- /system/shiowrapper.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | The wrappers dispatch io requests based on current thread. 4 | 5 | If the thread is an instance of ShBaseThread, the io should be dispatched to ShIO. 6 | Otherwise, it should be dispatched to regular sys io. 7 | """ 8 | 9 | import sys 10 | import threading 11 | 12 | from .shcommon import _SYS_STDIN, _SYS_STDOUT, _SYS_STDERR 13 | from .shthreads import ShBaseThread 14 | 15 | 16 | class ShStdinWrapper(object): 17 | def __getattribute__(self, item): 18 | thread = threading.currentThread() 19 | 20 | if isinstance(thread, ShBaseThread): 21 | return getattr(thread.state.sys_stdin, item) 22 | else: 23 | return getattr(_SYS_STDIN, item) 24 | 25 | 26 | class ShStdoutWrapper(object): 27 | def __getattribute__(self, item): 28 | thread = threading.currentThread() 29 | 30 | if isinstance(thread, ShBaseThread): 31 | return getattr(thread.state.sys_stdout, item) 32 | else: 33 | return getattr(_SYS_STDOUT, item) 34 | 35 | 36 | class ShStderrWrapper(object): 37 | def __getattribute__(self, item): 38 | thread = threading.currentThread() 39 | 40 | if isinstance(thread, ShBaseThread): 41 | return getattr(thread.state.sys_stderr, item) 42 | else: 43 | return getattr(_SYS_STDERR, item) 44 | 45 | 46 | stdinWrapper = ShStdinWrapper() 47 | stdoutWrapper = ShStdoutWrapper() 48 | stderrWrapper = ShStderrWrapper() 49 | 50 | 51 | def enable(): 52 | sys.stdin = stdinWrapper 53 | sys.stdout = stdoutWrapper 54 | sys.stderr = stderrWrapper 55 | 56 | 57 | def disable(): 58 | sys.stdin = _SYS_STDIN 59 | sys.stdout = _SYS_STDOUT 60 | sys.stderr = _SYS_STDERR 61 | -------------------------------------------------------------------------------- /bin/zip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Package and compress (archive) files and directories""" 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import sys 8 | import argparse 9 | import zipfile 10 | 11 | 12 | def main(args): 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("zipfile", help="") 15 | ap.add_argument("list", nargs="+", help="") 16 | ap.add_argument("-v", "--verbose", action="store_true", help="be more chatty") 17 | ns = ap.parse_args(args) 18 | 19 | relroot = os.path.abspath(os.path.dirname(ns.zipfile)) 20 | 21 | with zipfile.ZipFile(ns.zipfile, "w", zipfile.ZIP_DEFLATED) as outs: 22 | for path in ns.list: 23 | if os.path.isfile(path): 24 | if ns.verbose: 25 | print(path) 26 | arcname = os.path.relpath(path, relroot) 27 | outs.write(path, arcname=arcname) 28 | 29 | elif os.path.isdir(path): 30 | for root, dirs, files in os.walk(path): 31 | this_relroot = os.path.relpath(root, relroot) 32 | # add directory (needed for empty dirs) 33 | outs.write(root, arcname=this_relroot) 34 | if ns.verbose: 35 | print(this_relroot) 36 | for f in files: 37 | filename = os.path.join(root, f) 38 | if os.path.isfile(filename): # regular files only 39 | if ns.verbose: 40 | print(filename) 41 | arcname = os.path.join(this_relroot, f) 42 | outs.write(filename, arcname=arcname) 43 | 44 | 45 | if __name__ == "__main__": 46 | main(sys.argv[1:]) 47 | -------------------------------------------------------------------------------- /bin/dropbox_setup.py: -------------------------------------------------------------------------------- 1 | #! python2 2 | # -*- coding: utf-8 -*- 3 | # StaSh utility 4 | """manage your dropbox configuration.""" 5 | 6 | import cmd 7 | import keychain 8 | import sys 9 | 10 | from stashutils import dbutils 11 | 12 | _stash = globals()["_stash"] 13 | 14 | 15 | class DropboxSetupCmd(cmd.Cmd): 16 | """The command loop for managing the dropbox""" 17 | 18 | intro = _stash.text_color( 19 | "Welcome to the Dropbox Setup. Type 'help' for help.", "yellow" 20 | ) 21 | prompt = _stash.text_color("(dbs)", "red") 22 | use_rawinput = False 23 | 24 | def do_exit(self, cmd): 25 | """exit: quits the setup.""" 26 | sys.exit(0) 27 | 28 | do_quit = do_EOF = do_exit 29 | 30 | def do_list(self, cmd): 31 | """list: lists the dropbox usernames.""" 32 | self.stdout.write("\n") 33 | for service, account in keychain.get_services(): 34 | if service == dbutils.DB_SERVICE: 35 | self.stdout.write(account + "\n") 36 | self.stdout.write("\n") 37 | 38 | def do_del(self, cmd): 39 | """del USERNAME: resets the dropbox for USERNAME.""" 40 | dbutils.reset_dropbox(cmd) 41 | 42 | do_reset = do_del 43 | 44 | def do_add(self, cmd): 45 | """add USERNAME: starts the setup for USERNAME.""" 46 | if len(cmd) == 0: 47 | self.stdout.write( 48 | _stash.text_color("Error: expected an username.\n", "red") 49 | ) 50 | return 51 | try: 52 | dbutils.dropbox_setup(cmd, self.stdin, self.stdout) 53 | except KeyboardInterrupt: 54 | self.stdout.write("\nSetup aborted.\n") 55 | 56 | do_edit = do_add 57 | 58 | 59 | if __name__ == "__main__": 60 | cmdo = DropboxSetupCmd() 61 | cmdo.cmdloop() 62 | -------------------------------------------------------------------------------- /docs/pip_blocklist.md: -------------------------------------------------------------------------------- 1 | #PIP blocklist 2 | ----------------------- 3 | Starting with version 0.7.5, StaSh pip includes a blocklist. 4 | 5 | 6 | It indicates whether a package should not be installed and what reasons 7 | should be given. 8 | 9 | 10 | ## Motivation 11 | 12 | The reason for this blocklist is the high number of issues regarding 13 | installation problems of known incompatible packages. I hope that by 14 | slowly adding packages to the blocklist we can reduce the amount of 15 | these issues. 16 | 17 | 18 | The blocklist was initially discussed in issue #376. 19 | 20 | 21 | ## Details 22 | This blocklist is stored as a JSON file at `$STASH_ROOT/data/pip_blocklist.json`. 23 | It is a dict with two top-level keys: 24 | 25 | 26 | **`reasons`** Is a dict mapping a reasonID (str) to a message (str). 27 | It is used to reduce redundancy of error messages. 28 | 29 | 30 | **`blocklist`** Is a dict mapping packagename (str) to details (list). 31 | Every package mentioned in a key of the dict is considered blocklisted. 32 | 33 | The first (`i=0`) element of the list is the reasonID (str). Use the `reasons` toplevel 34 | key to determine the actual reason. 35 | 36 | The second (`i=1`) element of the list is a bool indicating whether this 37 | blocklisting is fatal. If true, it is considered fatal. This means that 38 | `pip` should abort the installation. Otherwise it is considered nonfatal. 39 | This means that `pip` should skip the installation, but continue the install. 40 | This is useful if the package can not be installed, but Pythonista already 41 | bundles the module. 42 | 43 | The third (`i=2`) is either `null`/`None` or a string. If it is a string 44 | and the package is marked non-fatal, use this string as the packagename instead. 45 | In this case, requested extras and version specifier are discarded. 46 | -------------------------------------------------------------------------------- /man/mounting/page_6.txt: -------------------------------------------------------------------------------- 1 | MOUNTING(6) --- DEVELOPER INFORMATIONS 2 | ====================================== 3 | 4 | This page explains how you can help expanding the mount-system. 5 | IMPORTANT: many changes to the mount-system requires a restart of pythonista. 6 | 7 | If you want to add builtin support for another filesystem/service: 8 | 1. Create a '.py' file in '$STASH_ROOT/lib/stashutils/fsi/'. 9 | 2. In this file, define a class which implements the API defined in '$STASH_ROOT/lib/stashutils/fsi/base.py/BaseFSI'. You mad also want to take a look at the utility functions this module provide. Your class should be a subclass of the BaseFSI class. 10 | 3. modify '$STASH_ROOT/lib/stashutils/fsi/interfaces.py' to import the class, then register the class in the 'FILESYSTEM_TYPES'-dictionary. 11 | 4. Test it and fix it if required. 12 | 5. Done. You may want to create a pull-request. 13 | 14 | If you want to add a FSI without modifying the '$STASH_ROOT' directory: 15 | 1. Create a '.py' file in '$HOME2/stash_extensions/fsi/'. 16 | 2. Do step 2 of 'adding builtin support' (see above). 17 | 3. In the file, define a dictionary mapping the FSI-names to the FSI-classes. 18 | 4. Done. 19 | 20 | If you want to define more patches: 21 | 1. read the 'monkeypatching' documentation using 'man'. 22 | 2. take a look at the functions and classes in '$STASH_ROOT/lib/mlpatches/mount_base.py'. Add your own function, define a patch and create an instance of it. IMPORTANT: follow the naming-convention of 'mount_base.py'. The '$STASH_ROOT/lib/mlpatches/mount_patches.py'-module scans the namespace and automatically registers the patches. 23 | 3. Test it, fixing it if required. 24 | 4. Done. You may want to create a pull-request. 25 | 26 | TODO: 27 | - add FSI for samba (windows filesystem access). See 'pysamba'. 28 | - add FSI for '.iso'-files. See the github repo of 'pyiso'. 29 | 30 | Thanks for your interest. 31 | -------------------------------------------------------------------------------- /system/dummyobjc_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ObjCClass(object): 5 | def __init__(self, *args, **kwargs): 6 | pass 7 | 8 | def __call__(self, *args, **kwargs): 9 | return ObjCClass() 10 | 11 | def __getattr__(self, item): 12 | return ObjCClass() 13 | 14 | 15 | class ObjCInstance(ObjCClass): 16 | pass 17 | 18 | 19 | class UIColor(ObjCClass): 20 | @classmethod 21 | def blackColor(cls): 22 | pass 23 | 24 | @classmethod 25 | def redColor(cls): 26 | pass 27 | 28 | @classmethod 29 | def greenColor(cls): 30 | pass 31 | 32 | @classmethod 33 | def brownColor(cls): 34 | pass 35 | 36 | @classmethod 37 | def blueColor(cls): 38 | pass 39 | 40 | @classmethod 41 | def magentaColor(cls): 42 | pass 43 | 44 | @classmethod 45 | def cyanColor(cls): 46 | pass 47 | 48 | @classmethod 49 | def whiteColor(cls): 50 | pass 51 | 52 | @classmethod 53 | def grayColor(cls): 54 | pass 55 | 56 | @classmethod 57 | def yellowColor(cls): 58 | pass 59 | 60 | @classmethod 61 | def colorWithRed_green_blue_alpha_(cls, *args, **kwargs): 62 | pass 63 | 64 | 65 | class NSRange(ObjCClass): 66 | pass 67 | 68 | 69 | def create_objc_class(*args, **kwargs): 70 | return ObjCClass() 71 | 72 | 73 | def ns(*args, **kwargs): 74 | return ObjCInstance() 75 | 76 | 77 | def on_main_thread(func): 78 | return func 79 | 80 | 81 | class ctypes(object): 82 | class pythonapi(object): 83 | @staticmethod 84 | def PyThreadState_SetAsyncExc( 85 | tid, 86 | exectype, 87 | ): 88 | return 1 89 | 90 | @staticmethod 91 | def c_long(val): 92 | return val 93 | 94 | @staticmethod 95 | def py_object(val): 96 | return val 97 | -------------------------------------------------------------------------------- /bin/cut.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Print selected parts of lines from each FILE to standard output.""" 3 | 4 | from __future__ import print_function 5 | import sys 6 | import argparse 7 | 8 | _stash = globals()["_stash"] 9 | 10 | 11 | def construct_indices_from_list_spec(list_spec): 12 | # Note unlike python, cut's indices start from 1 13 | indices = [] 14 | for fld in list_spec.split(","): 15 | if "-" in fld: 16 | sidx, eidx = fld.split("-") 17 | sidx = int(sidx) - 1 18 | eidx = int(eidx) # -1 + 1 because base is 1 and eidx is inclusive 19 | else: 20 | sidx = int(fld) - 1 21 | eidx = sidx + 1 22 | 23 | indices.append((sidx, eidx)) 24 | return indices 25 | 26 | 27 | def main(args): 28 | ap = argparse.ArgumentParser() 29 | 30 | ap.add_argument( 31 | "-d", 32 | "--delimiter", 33 | nargs="?", 34 | metavar="DELIM", 35 | help="use DELIM instead of SPACE for field delimiter", 36 | ) 37 | ap.add_argument( 38 | "-f", "--fields", required=True, metavar="LIST", help="select only these fields" 39 | ) 40 | ap.add_argument("files", nargs="*", help="files to cut") 41 | ns = ap.parse_args(args) 42 | 43 | indices = construct_indices_from_list_spec(ns.fields) 44 | 45 | for infields in _stash.libcore.input_stream(ns.files): 46 | if infields[0] is None: 47 | _, filename, e = infields 48 | print("%s: %s" % (filename, repr(e))) 49 | else: 50 | line, filename, lineno = infields 51 | fields = line.split(ns.delimiter) 52 | if len(fields) == 1: 53 | print(fields[0]) 54 | else: 55 | out = " ".join((" ".join(fields[sidx:eidx]) for sidx, eidx in indices)) 56 | print(out) 57 | 58 | 59 | if __name__ == "__main__": 60 | main(sys.argv[1:]) 61 | -------------------------------------------------------------------------------- /bin/umount.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """unmount a filesystem.""" 3 | 4 | from __future__ import print_function 5 | import argparse 6 | import sys 7 | 8 | from stashutils import mount_manager, mount_ctrl 9 | 10 | _stash = globals()["_stash"] 11 | 12 | if __name__ == "__main__": 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument( 15 | "-a", "--all", action="store_true", dest="all", help="unmount all filesystems" 16 | ) 17 | parser.add_argument( 18 | "-v", "--verbose", action="store_true", dest="v", help="be more chatty" 19 | ) 20 | parser.add_argument( 21 | "-f", 22 | "--force", 23 | action="store_true", 24 | dest="force", 25 | help="force unmount; do not call fsi.close()", 26 | ) 27 | parser.add_argument( 28 | "directory", 29 | action="store", 30 | nargs="?", 31 | default=None, 32 | help="directory to remove mounted filesystem from", 33 | ) 34 | ns = parser.parse_args() 35 | 36 | if not (ns.directory or ("-a" in sys.argv) or ("--all" in sys.argv)): 37 | print(_stash.text_color("Error: no target directory specified!", "red")) 38 | sys.exit(1) 39 | 40 | manager = mount_ctrl.get_manager() 41 | 42 | if manager is None: 43 | manager = mount_manager.MountManager() 44 | mount_ctrl.set_manager(manager) 45 | 46 | if ns.all: 47 | to_unmount = [m[0] for m in manager.get_mounts()] 48 | else: 49 | to_unmount = [ns.directory] 50 | 51 | exitcode = 0 52 | 53 | for path in to_unmount: 54 | if ns.v: 55 | print("Unmounting '{p}'...".format(p=path)) 56 | try: 57 | manager.unmount_fsi(path, force=ns.force) 58 | except mount_manager.MountError as e: 59 | exitcode = 1 60 | print(_stash.text_color("Error: {e}".format(e=e.message), "red")) 61 | if ns.v: 62 | print("Done.") 63 | sys.exit(exitcode) 64 | -------------------------------------------------------------------------------- /bin/cowsay.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | # by Siddharth Duahantha 5 | # 28 July 2017 6 | import sys 7 | import argparse 8 | 9 | COW = r""" \ ^__^ 10 | \ (oo)\_______ 11 | (__)\ )\/\\ 12 | ||----w | 13 | || || 14 | """ 15 | 16 | 17 | def get_cow(text): 18 | """create a string of a cow saying things.""" 19 | lines = text.split("\n") 20 | nlines = len(lines) 21 | longest_line = max([len(line) for line in lines]) 22 | lenght_of_lines = longest_line + 2 23 | ret = " " + "_" * lenght_of_lines + "\n" 24 | if nlines == 1: 25 | formated = text.center(longest_line + 2) 26 | ret += formated.join("<>") + "\n" 27 | else: 28 | t = "" 29 | for i in range(nlines): 30 | line = lines[i].center(longest_line + 2) 31 | if i == 0: 32 | t += "/" + line + "\\\n" 33 | elif i == (nlines - 1): 34 | t += "\\" + line + "/\n" 35 | else: 36 | t += "|" + line + "|\n" 37 | ret += t 38 | ret += " " + "-" * lenght_of_lines + "\n" 39 | ret += COW 40 | return ret 41 | 42 | 43 | def main(): 44 | """main function""" 45 | # todo: lookuo real description 46 | parser = argparse.ArgumentParser(description="Let a cow speak for you") 47 | parser.add_argument("text", nargs="*", default=None, help="text to say") 48 | ns = parser.parse_args() 49 | 50 | if (ns.text is None) or (len(ns.text) == 0): 51 | text = "" 52 | while True: 53 | inp = sys.stdin.read(4096) 54 | if inp.endswith("\n"): 55 | inp = inp[:-1] 56 | if not inp: 57 | break 58 | text += inp 59 | else: 60 | text = " ".join(ns.text) 61 | 62 | cow = get_cow(text) 63 | print(cow) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /tools/yapf.ini: -------------------------------------------------------------------------------- 1 | # configuration file for YAPF (https://github.com/google/yapf) 2 | 3 | [style] 4 | # basic settings 5 | based_on_style = pep8 6 | USE_TABS = false 7 | 8 | # brackets 9 | ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT = true 10 | COALESCE_BRACKETS = false 11 | DEDENT_CLOSING_BRACKETS = true 12 | 13 | # lambdas 14 | ALLOW_MULTILINE_LAMBDAS = false 15 | 16 | # dictionaries 17 | ALLOW_MULTILINE_DICTIONARY_KEYS = false 18 | ALLOW_SPLIT_BEFORE_DICT_VALUE = false 19 | EACH_DICT_ENTRY_ON_SEPARATE_LINE = true 20 | INDENT_DICTIONARY_VALUE = true 21 | 22 | # assignments 23 | ALLOW_SPLIT_BEFORE_DEFAULT_OR_NAMED_ASSIGNS = false 24 | SPACES_AROUND_DEFAULT_OR_NAMED_ASSIGN = false 25 | 26 | # arithmetic 27 | ARITHMETIC_PRECEDENCE_INDICATION = false 28 | # NO_SPACES_AROUND_SELECTED_BINARY_OPERATORS = false 29 | SPACES_AROUND_POWER_OPERATOR = true 30 | 31 | # blank lines 32 | BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = false 33 | BLANK_LINE_BEFORE_MODULE_DOCSTRING = false 34 | BLANK_LINE_BEFORE_CLASS_DOCSTRING = false 35 | BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION = 2 36 | INDENT_BLANK_LINES = false 37 | 38 | # max line length 39 | COLUMN_LIMIT = 127 40 | 41 | # continuation 42 | CONTINUATION_ALIGN_STYLE = SPACE 43 | CONTINUATION_INDENT_WIDTH = 4 44 | 45 | # lists 46 | DISABLE_ENDING_COMMA_HEURISTIC = false 47 | 48 | # indentation 49 | INDENT_WIDTH = 4 50 | 51 | # simplification 52 | JOIN_MULTIPLE_LINES = true 53 | 54 | # comments 55 | SPACES_BEFORE_COMMENT = 2 56 | 57 | # other spaces 58 | SPACE_BETWEEN_ENDING_COMMA_AND_CLOSING_BRACKET = 1 59 | 60 | # splits 61 | SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED = true 62 | SPLIT_ALL_COMMA_SEPARATED_VALUES = true 63 | # SPLIT_ALL_TOP_LEVEL_COMMA_SEPARATED_VALUES = false 64 | SPLIT_BEFORE_BITWISE_OPERATOR = true 65 | SPLIT_BEFORE_ARITHMETIC_OPERATOR = true 66 | SPLIT_BEFORE_CLOSING_BRACKET = true 67 | SPLIT_BEFORE_DICT_SET_GENERATOR = true 68 | SPLIT_BEFORE_DOT = false 69 | SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN = true 70 | SPLIT_BEFORE_FIRST_ARGUMENT = true 71 | SPLIT_BEFORE_LOGICAL_OPERATOR = true 72 | SPLIT_BEFORE_NAMED_ASSIGNS = true 73 | SPLIT_COMPLEX_COMPREHENSION = true 74 | -------------------------------------------------------------------------------- /tests/system/test_threads.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import time 3 | 4 | from six import StringIO 5 | 6 | from stash.tests.stashtest import StashTestCase 7 | 8 | 9 | class ThreadsTests(StashTestCase): 10 | setup_commands = ["BIN_PATH=$STASH_ROOT/tests/system/data:$BIN_PATH"] 11 | 12 | def test_101(self): 13 | """ 14 | background thread clears properly 15 | """ 16 | self.stash("test_101_1.py &") 17 | time.sleep(4) 18 | cmp_str = r"""[stash]$ [stash]$ sleeping ... 0 19 | sleeping ... 1 20 | """ 21 | assert self.stash.main_screen.text == cmp_str, "output not identical" 22 | 23 | def test_102(self): 24 | """ 25 | Two parallel threads with same stdout should interleave 26 | """ 27 | outs = StringIO() 28 | self.stash("test_102_1.py &", final_outs=outs) 29 | self.stash("test_102_2.py &", final_outs=outs) 30 | time.sleep(5) 31 | s = outs.getvalue() 32 | 33 | # Count the number of times the output switches between threads 34 | change_cnt = 0 35 | prev_line = None 36 | for cur_line in outs.getvalue().splitlines(): 37 | if prev_line is None: 38 | prev_line = cur_line 39 | elif prev_line != cur_line: 40 | change_cnt += 1 41 | prev_line = cur_line 42 | 43 | self.assertTrue(change_cnt > 2, "Output do not interleave") 44 | 45 | def test_103(self): 46 | """ 47 | Two threads in parallel with different stdout do not interfere 48 | """ 49 | outs1 = StringIO() 50 | 51 | self.stash("test_102_1.py &", final_outs=outs1) 52 | self.stash("test_102_2.py") 53 | time.sleep(1) 54 | 55 | cmp_str1 = r"""[stash]$ [stash]$ test_102_2.py 56 | test_102_2.py 57 | test_102_2.py 58 | test_102_2.py 59 | test_102_2.py 60 | [stash]$ """ 61 | assert self.stash.main_screen.text == cmp_str1, "output not identical" 62 | 63 | cmp_str2 = r"""test_102_1.py 64 | test_102_1.py 65 | test_102_1.py 66 | test_102_1.py 67 | test_102_1.py 68 | """ 69 | assert outs1.getvalue() == cmp_str2, "output not identical" 70 | -------------------------------------------------------------------------------- /system/shui/stubui.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stub ui and terminal for testing. 3 | """ 4 | 5 | import six 6 | 7 | from .base import ShBaseUI, ShBaseTerminal, ShBaseSequentialRenderer 8 | 9 | 10 | class ShUI(ShBaseUI): 11 | """ 12 | Stub UI for testing. 13 | """ 14 | 15 | def __init__(self, *args, **kwargs): 16 | ShBaseUI.__init__(self, *args, **kwargs) 17 | self.terminal = ShTerminal(self.stash, self) 18 | 19 | def show(self): 20 | pass 21 | 22 | def close(self): 23 | self.on_exit() 24 | 25 | 26 | class ShTerminal(ShBaseTerminal): 27 | """ 28 | Stub terminal for testing. 29 | """ 30 | 31 | def __init__(self, *args, **kwargs): 32 | ShBaseTerminal.__init__(self, *args, **kwargs) 33 | self._text = "" 34 | 35 | @property 36 | def text(self): 37 | return self._text 38 | 39 | @text.setter 40 | def text(self, value): 41 | assert isinstance(value, (six.text_type, six.binary_type)) 42 | self._text = value 43 | 44 | @property 45 | def selected_range(self): 46 | # always at the end 47 | return (self.text_length, self.text_length) 48 | 49 | @selected_range.setter 50 | def selected_range(self, value): 51 | raise NotImplementedError() 52 | 53 | @property 54 | def text_length(self): 55 | return len(self.text) # default implementation 56 | 57 | def scroll_to_end(self): 58 | pass 59 | 60 | def set_focus(self): 61 | pass 62 | 63 | def lose_focus(self): 64 | pass 65 | 66 | def get_wh(self): 67 | return (80, 24) 68 | 69 | 70 | class ShSequentialRenderer(ShBaseSequentialRenderer): 71 | """ 72 | Stub renderer for testing 73 | """ 74 | 75 | def render(self, no_wait=False): 76 | # Lock screen to get atomic information 77 | with self.screen.acquire_lock(): 78 | intact_left_bound, intact_right_bound = self.screen.get_bounds() 79 | screen_buffer_length = self.screen.text_length 80 | cursor_xs, cursor_xe = self.screen.cursor_x 81 | renderable_chars = self.screen.renderable_chars 82 | self.screen.clean() 83 | 84 | self.terminal.text = self.screen.text 85 | -------------------------------------------------------------------------------- /man/monkeypatching/page_4.txt: -------------------------------------------------------------------------------- 1 | monkeypatching(4) --- PATCHES 2 | ============================= 3 | 4 | There are 3 types of Patches (actually 5, but the other two types are only important to developers): 5 | A "FunctionPatch" modifies a module, importing it if neccessary. 6 | Regardless of their name, they may also patch other attributes of a module. 7 | Reloading a module will break any "FunctionPatch" 8 | A "ModulePatch" adds or overwrites a whole module. 9 | This can result in missing attributes (in case they are not implemented). 10 | However, the most patches will try to implement everything documented in the docs. 11 | A "PatchGroup" consists of other Patches. 12 | 13 | To make it easier to find the right patch, most patchnames are following this schema: 14 | PatchGroups have a uppercase name, for example "STABLE". 15 | FunctionPatches have a lowercase name constructed this way: 16 | _ 17 | module_patches are named like the module. 18 | 19 | There are two subtypes of patches: 20 | "STABLE"-Patches a more-or-less working and should be pretty stable: 21 | "INSTABLE"-Patches are still in developement, not working or buggy. They often crash. 22 | "ModulePatches" are always seen as stable. 23 | 24 | 25 | Available PatchGroups 26 | --------------------- 27 | 28 | ALL: This Group contains all Patches and is generated automatically. 29 | STABLE: All stable patches and all ModulePatches 30 | INSTABLE: All instable Patches. Do not use these unless you know what you are doing! 31 | 32 | OS: All patches for the "os"-module. 33 | OS_POPEN: The patches for "os.popen*". 34 | OS_PROCESSING: patches to emulate process-like behavior 35 | 36 | 37 | Some of the Available Patches: 38 | ------------------------------ 39 | 40 | os_popen: make "os.popen" use StaSh. 41 | os_popen2: make "os.popen2" use StaSh. 42 | os_popen3: make "os.popen3" use StaSh. 43 | os_popen4: make "os.popen4" use StaSh. 44 | os_system: make "os.system" use StaSh. 45 | os_getpid: make "os.getpid" return the current job_id instead of the pid. 46 | os_getppid: like os_getpid, but for the parent-job. 47 | os_kill: make "os.kill" kill the job_id. 48 | popen2: the "popen2"-module. 49 | subprocess: the "subprocess" module. 50 | -------------------------------------------------------------------------------- /system/shui/dummyui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Stub ui to allow debug on PC 4 | """ 5 | 6 | AUTOCAPITALIZE_NONE = 0 7 | 8 | 9 | def measure_string(*args, **kwargs): 10 | return 12.0 11 | 12 | 13 | def in_background(func): 14 | return func 15 | 16 | 17 | def get_screen_size(): 18 | return 100, 100 19 | 20 | 21 | class View(object): 22 | def __init__(self, *args, **kwargs): 23 | self.on_screen = True 24 | self.width = 100 25 | self.height = 100 26 | self.content_size = (100, 100) 27 | self.content_offset = (0, 0) 28 | self.superview = None 29 | self.subviews = [] 30 | self.delegate = None 31 | 32 | def add_subview(self, v): 33 | self.subviews.append(v) 34 | v.superview = self 35 | 36 | def remove_subview(self, v): 37 | self.subviews.remove(v) 38 | 39 | def present(self, style="popover"): 40 | pass 41 | 42 | def wait_modal(self): 43 | pass 44 | 45 | def size_to_fit(self): 46 | pass 47 | 48 | def send_to_back(self): 49 | pass 50 | 51 | def bring_to_front(self): 52 | pass 53 | 54 | 55 | class TextField(View): 56 | def __init__(self, *args, **kwargs): 57 | super(TextField, self).__init__(*args, **kwargs) 58 | self.text = "" 59 | 60 | 61 | class TextView(View): 62 | def __init__(self, *args, **kwargs): 63 | super(TextView, self).__init__(*args, **kwargs) 64 | self.text = "" 65 | self.selected_range = (0, 0) 66 | 67 | def replace_range(self, rng, s): 68 | self.text = self.text[: rng[0]] + s + self.text[rng[1] :] 69 | tot_len = len(self.text) 70 | self.selected_range = (tot_len, tot_len) 71 | 72 | def begin_editing(self): 73 | pass 74 | 75 | def end_editing(self): 76 | pass 77 | 78 | 79 | class ScrollView(View): 80 | pass 81 | 82 | 83 | class Button(View): 84 | def __init__(self, *args, **kwargs): 85 | super(Button, self).__init__(*args, **kwargs) 86 | 87 | 88 | class TableView(View): 89 | def __init__(self, *args, **kwargs): 90 | super(TableView, self).__init__(*args, **kwargs) 91 | 92 | 93 | class ListDataSource(object): 94 | def __init__(self, lst): 95 | pass 96 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Learn more about this config here: https://pre-commit.com/ 2 | 3 | # To enable these pre-commit hooks run: 4 | # `pipx install pre-commit` or `brew install pre-commit` 5 | # Then in the project root directory run `pre-commit install` 6 | 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v6.0.0 10 | hooks: 11 | - id: check-added-large-files 12 | - id: check-ast 13 | - id: check-builtin-literals 14 | - id: check-case-conflict 15 | - id: check-docstring-first 16 | exclude: ^(bin/ssh\.py|bin/telnet\.py|bin/mc\.py)$ 17 | - id: check-executables-have-shebangs 18 | - id: check-json 19 | - id: check-merge-conflict 20 | # - id: check-shebang-scripts-are-executable 21 | - id: check-symlinks 22 | - id: check-toml 23 | - id: check-vcs-permalinks 24 | - id: check-xml 25 | - id: check-yaml 26 | - id: debug-statements 27 | exclude: system/shruntime.py 28 | - id: destroyed-symlinks 29 | - id: detect-private-key 30 | - id: end-of-file-fixer 31 | exclude: ^tests/(md5sum|sha1sum|sha256sum)/data/ 32 | - id: file-contents-sorter 33 | - id: fix-byte-order-marker 34 | - id: forbid-new-submodules 35 | - id: forbid-submodules 36 | - id: mixed-line-ending 37 | args: 38 | - --fix=lf 39 | # - id: pretty-format-json 40 | - id: requirements-txt-fixer 41 | - id: sort-simple-yaml 42 | - id: trailing-whitespace 43 | 44 | #- repo: https://github.com/MarcoGorelli/auto-walrus 45 | # rev: 0.3.4 46 | # hooks: 47 | # - id: auto-walrus 48 | 49 | #- repo: https://github.com/codespell-project/codespell 50 | # rev: v2.4.1 51 | # hooks: 52 | # - id: codespell # See pyproject.toml for args 53 | # additional_dependencies: 54 | # - tomli 55 | 56 | - repo: https://github.com/astral-sh/ruff-pre-commit 57 | rev: v0.12.9 58 | hooks: 59 | - id: ruff-check 60 | - id: ruff-format 61 | 62 | - repo: https://github.com/tox-dev/pyproject-fmt 63 | rev: v2.6.0 64 | hooks: 65 | - id: pyproject-fmt 66 | 67 | - repo: https://github.com/abravalheri/validate-pyproject 68 | rev: v0.24.1 69 | hooks: 70 | - id: validate-pyproject 71 | -------------------------------------------------------------------------------- /bin/wc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Print newline, word, and byte counts for each FILE, and a total line if 3 | more than one FILE is specified. 4 | """ 5 | 6 | from __future__ import print_function 7 | import os 8 | import sys 9 | import argparse 10 | 11 | _stash = globals()["_stash"] 12 | 13 | 14 | def main(args): 15 | ap = argparse.ArgumentParser() 16 | 17 | ap.add_argument( 18 | "-l", 19 | "--lines", 20 | action="store_true", 21 | default=False, 22 | help="print the newline counts", 23 | ) 24 | ap.add_argument("files", nargs="*", help="files to count") 25 | ns = ap.parse_args(args) 26 | 27 | if ns.lines: 28 | 29 | def _print_res(res): 30 | print("%6d %s" % (res[0], res[-1])) 31 | else: 32 | 33 | def _print_res(res): 34 | print("%6d %8d %8d %s" % res) 35 | 36 | results = [] 37 | nl_count = 0 38 | wd_count = 0 39 | bt_count = 0 40 | filename_old = None 41 | for infields in _stash.libcore.input_stream(ns.files): 42 | if filename_old is None: 43 | filename_old = infields[1] 44 | 45 | if infields[0] is None: 46 | _, filename, e = infields 47 | print("%s: %s" % (filename, repr(e))) 48 | filename_old = None 49 | else: 50 | line, filename, lineno = infields 51 | if filename != filename_old: 52 | results.append((nl_count, wd_count, bt_count, filename_old)) 53 | nl_count = 0 54 | wd_count = 0 55 | bt_count = 0 56 | filename_old = filename 57 | 58 | nl_count += 1 59 | # TODO: This is not a very accurate for words 60 | wd_count += len(line.split()) 61 | bt_count += len(line) 62 | 63 | # last file 64 | results.append((nl_count, wd_count, bt_count, filename_old)) 65 | 66 | tot_nl_count = 0 67 | tot_wd_count = 0 68 | tot_bt_count = 0 69 | for res in results: 70 | _print_res(res) 71 | tot_nl_count += res[0] 72 | tot_wd_count += res[1] 73 | tot_bt_count += res[2] 74 | 75 | if len(results) > 1: 76 | _print_res((tot_nl_count, tot_wd_count, tot_bt_count, "total")) 77 | 78 | 79 | if __name__ == "__main__": 80 | main(sys.argv[1:]) 81 | -------------------------------------------------------------------------------- /man/mounting/page_3.txt: -------------------------------------------------------------------------------- 1 | MOUNTING(3) --- INFORMATIONS 2 | ============================ 3 | 4 | Currently, the mounting-system contains two commands: 'mount' and 'umount'/'unmount'. 5 | 6 | The mount-system depends on 'monkeylord' to manage its monkeypatches. 7 | All the monkeypatches used by the mount-system are only accessible using the 'MOUNT'-PatchGroup. 8 | The 'MOUNT'-PatchGroup itself is part of the 'INSTABLE'-PatchGroup, which is part of the 'ALL'-PatchGroup. 9 | Due to this, the required monkeypatches will only be enabled when the 'monkeylord'-command is explicitly told to enable these. 10 | For Example: 11 | 'monkeylord enable' will not enable the patches. 12 | 'monkeylord enable ALL' will enable the patches. 13 | 'monkeylord enable INSTABLE' will enable the patches. 14 | 'monkeylord enable MOUNT' will enable the patches. 15 | 'monkeylord enable STABLE' will not enable the patches. 16 | 'monkeylord enable POPEN' will not enable the patches. 17 | 'monkeylord disable' will disable the patches. 18 | 'monkeylord disable POPEN' will not disable the patches. 19 | All required patches will automatically be enabled whenever the 'mount'-command is executed. 20 | The monkeypatches are defined in '$STASH_ROOT/lib/mlpatches/mount_base.py' and registered in '$STASH_ROOT/lib/mlpatches/mount_patches.py'. 21 | 22 | The 'mount'-system uses Filesystem-Interfaces (short: FSIs) to handle the filesystems. 23 | Most of the FSIs were first implemented in the 'mc'-command's sourcecode and have been extracted. 24 | Because of this, the FSI-API was initially not designed for the 'mount'-system, but has been extended for the 'mount'-command. 25 | Also, the FSI-API is designed to uses at least methods as possible, which may lead to performance-issues, but is required in order to easily support many different filesystem-types. 26 | All builtin FSIs are defined in '$STASH_ROOT/lib/stashutils/fsi/' and are registered in '$STASH_ROOT/lib/stashutils/fsi/interfaces.py'. 27 | You can easily extend the FSIs by adding a file in '$HOME2/stash_extensions/fsi/' which defines a dictionary named 'FSIS'. 28 | The Keys of this dictionary should be the names of the FSIs, the values the FSI. 29 | 30 | If you want to access another filesystem, but dont want to use monkeypatches, use the 'mc'-command. They use the same FSIs, but 'mc' defines its own commands while mount allows the use of StaSh's commands. 31 | -------------------------------------------------------------------------------- /lib/wakeonlan/wol.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Small module for use with the wake on lan protocol. 4 | 5 | """ 6 | 7 | from __future__ import absolute_import 8 | from __future__ import unicode_literals 9 | 10 | import socket 11 | import struct 12 | 13 | BROADCAST_IP = "255.255.255.255" 14 | DEFAULT_PORT = 9 15 | 16 | 17 | def create_magic_packet(macaddress): 18 | """ 19 | Create a magic packet which can be used for wake on lan using the 20 | mac address given as a parameter. 21 | 22 | Keyword arguments: 23 | :arg macaddress: the mac address that should be parsed into a magic 24 | packet. 25 | 26 | """ 27 | if len(macaddress) == 12: 28 | pass 29 | elif len(macaddress) == 17: 30 | sep = macaddress[2] 31 | macaddress = macaddress.replace(sep, "") 32 | else: 33 | raise ValueError("Incorrect MAC address format") 34 | 35 | # Pad the synchronization stream 36 | data = b"FFFFFFFFFFFF" + (macaddress * 20).encode() 37 | send_data = b"" 38 | 39 | # Split up the hex values in pack 40 | for i in range(0, len(data), 2): 41 | send_data += struct.pack(b"B", int(data[i : i + 2], 16)) 42 | return send_data 43 | 44 | 45 | def send_magic_packet(*macs, **kwargs): 46 | """ 47 | Wakes the computer with the given mac address if wake on lan is 48 | enabled on that host. 49 | 50 | Keyword arguments: 51 | :arguments macs: One or more macaddresses of machines to wake. 52 | :key ip_address: the ip address of the host to send the magic packet 53 | to (default "255.255.255.255") 54 | :key port: the port of the host to send the magic packet to 55 | (default 9) 56 | 57 | """ 58 | packets = [] 59 | ip = kwargs.pop("ip_address", BROADCAST_IP) 60 | port = kwargs.pop("port", DEFAULT_PORT) 61 | for k in kwargs: 62 | raise TypeError( 63 | "send_magic_packet() got an unexpected keyword argument {!r}".format(k) 64 | ) 65 | 66 | for mac in macs: 67 | packet = create_magic_packet(mac) 68 | packets.append(packet) 69 | 70 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 71 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 72 | sock.connect((ip, port)) 73 | for packet in packets: 74 | sock.send(packet) 75 | sock.close() 76 | -------------------------------------------------------------------------------- /lib/mlpatches/patches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module contains a dictionary containing all patches and their name.""" 3 | 4 | from stash.system.shcommon import _STASH_EXTENSION_PATCH_PATH 5 | 6 | from stashutils.core import load_from_dir 7 | 8 | from mlpatches import base, os_patches, modulepatches, tl_patches 9 | from mlpatches import mount_patches 10 | 11 | STABLE_PATCHES = { # name -> Patch() 12 | "os_popen": os_patches.POPEN_PATCH, 13 | "os_popen2": os_patches.POPEN2_PATCH, 14 | "os_popen3": os_patches.POPEN3_PATCH, 15 | "os_popen4": os_patches.POPEN4_PATCH, 16 | "os_system": os_patches.SYSTEM_PATCH, 17 | "os_getpid": os_patches.GETPID_PATCH, 18 | "os_getppid": os_patches.GETPPID_PATCH, 19 | "os_kill": os_patches.KILL_PATCH, 20 | "OS_POPEN": os_patches.POPEN_PATCHES, 21 | "OS_PROCESSING": os_patches.PROCESSING_PATCHES, 22 | "OS": os_patches.OS_PATCHES, 23 | } 24 | 25 | INSTABLE_PATCHES = { # name -> Patch() 26 | # "tl_argv": tl_patches.TL_ARGV_PATCH, 27 | "MOUNT": mount_patches.MOUNT_PATCHES, 28 | } 29 | 30 | # update with extensions 31 | extensions = load_from_dir( 32 | dirpath=_STASH_EXTENSION_PATCH_PATH, 33 | varname="STABLE_PATCHES", 34 | ) 35 | for ext in extensions: 36 | if not isinstance(ext, dict): 37 | continue 38 | else: 39 | STABLE_PATCHES.update(ext) 40 | 41 | extensions = load_from_dir( 42 | dirpath=_STASH_EXTENSION_PATCH_PATH, 43 | varname="INSTABLE_PATCHES", 44 | ) 45 | for ext in extensions: 46 | if not isinstance(ext, dict): 47 | continue 48 | else: 49 | INSTABLE_PATCHES.update(ext) 50 | 51 | # create an empty dict and update it 52 | 53 | PATCHES = {} 54 | 55 | STABLE_PATCHES.update( 56 | modulepatches.MODULE_PATCHES 57 | ) # modulepatches should be available in STABLE 58 | PATCHES.update(INSTABLE_PATCHES) # update with INSTABLE first 59 | PATCHES.update( 60 | STABLE_PATCHES 61 | ) # update with STABLE patches (overwriting INSTABLE patches if required) 62 | 63 | # define a PatchGroup with all patches 64 | STABLE_GROUP = base.VariablePatchGroup(list(STABLE_PATCHES.values())) 65 | INSTABLE_GROUP = base.VariablePatchGroup(list(INSTABLE_PATCHES.values())) 66 | ALL_GROUP = base.VariablePatchGroup(list(PATCHES.values())) 67 | 68 | PATCHES["ALL"] = ALL_GROUP 69 | PATCHES["STABLE"] = STABLE_GROUP 70 | PATCHES["INSTABLE"] = INSTABLE_GROUP 71 | -------------------------------------------------------------------------------- /system/shui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the UI for StaSh. 3 | """ 4 | 5 | import os 6 | 7 | from stash.system.shcommon import IN_PYTHONISTA 8 | 9 | # check if running on travis 10 | ON_TRAVIS = "TRAVIS" in os.environ 11 | 12 | 13 | def get_platform(): 14 | """ 15 | Return a string describing the UI implementation to use. 16 | :return: platform identifier 17 | :rtype: str 18 | """ 19 | # platform specific UIs 20 | if IN_PYTHONISTA: 21 | return "pythonista" 22 | elif ON_TRAVIS: 23 | return "stub" 24 | 25 | # attempt to fall back to tkinter 26 | try: 27 | from six.moves import tkinter 28 | except ImportError: 29 | # can not import tkinter 30 | # ignore this case. If this executes successfully, it is handled in the 'else' clause 31 | pass 32 | else: 33 | return "tkinter" 34 | 35 | # this function has still not returned. 36 | # this means that all UIs tried above failed. 37 | # we raise an error in this case. 38 | raise NotImplementedError( 39 | "There is no UI implemented for this platform. If you are on a PC, you may be able to fix this by installing tkinter." 40 | ) 41 | 42 | 43 | def get_ui_implementation(platform=None): 44 | """ 45 | Return the classes implementing the UI for the platform. 46 | :param platform: identifier describing the platform to get the UI implementation for. Defaults to None, in which case it tries to find the best UI. 47 | :type platform: str 48 | :return: (ShUI, ShSequentialRenderer) 49 | :rtype: tuple of (stash.shui.base.ShBaseUI, stash.shui.base.ShBaseSequentialRenderer) 50 | """ 51 | if platform is None: 52 | platform = get_platform() 53 | if platform == "pythonista": 54 | from .pythonista_ui import ShUI, ShTerminal, ShSequentialRenderer 55 | 56 | return (ShUI, ShSequentialRenderer) 57 | elif platform == "stub": 58 | from .stubui import ShUI, ShTerminal, ShSequentialRenderer 59 | 60 | return (ShUI, ShSequentialRenderer) 61 | elif platform == "tkinter": 62 | from .tkui import ShUI, ShTerminal, ShSequentialRenderer 63 | 64 | return (ShUI, ShSequentialRenderer) 65 | else: 66 | raise NotImplementedError( 67 | "No UI implemented for platform {}!".format(repr(platform)) 68 | ) 69 | 70 | 71 | __all__ = ["get_platform", "get_ui_implementation"] 72 | -------------------------------------------------------------------------------- /data/stash_tips.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Output can be redirected to a file, e.g. ls > file_name", 3 | "You can redirect output to Pythonista console, e.g. ls > &3", 4 | "You can alias a long command to a shorter name", 5 | "Use pipes to connect IOs of multiple commands, e.g. ls | sort", 6 | "Use pip to search, install and uninstall PyPI packages", 7 | "You can omit .py when typing command name, e.g. pwd is the same as pwd.py", 8 | "Manage repos with the git command", 9 | "Talk to remote machines with ssh and scp", 10 | "Usage of a command can often be checked with the -h option, e.g. git -h", 11 | "A background job can be issued by appending & at the end of a normal command, e.g. httpserver &", 12 | "Keep updated with development by simply run the selfupdate command", 13 | "A branch name can be specified to allow selfupdate from a branch other than master", 14 | "Download files from an URL with wget", 15 | "Check disk usage with du -s", 16 | "Stop a running command by pressing the CC button (Ctrl-C on external keyboard)", 17 | "Send a running command to background by pressing the CZ button (Ctrl-Z on external keyboard)", 18 | "Bring a background job to foreground with fg JOB_ID", 19 | "List all background jobs with jobs", 20 | "Command httpserver starts a HTTP server with upload function", 21 | "You can use the callable StaSh object to issue more commands from a script, e.g. _stash('some_command')", 22 | "Customize appearances and behaviours of StaSh by editing the .stash_config file", 23 | ".stashrc is the StaSh resource file similar to what .bashrc is to Bash", 24 | "You can invoke almost any Python scripts, including UI and Scene, directly from StaSh", 25 | "Check the value of BIN_PATH, i.e. echo $BIN_PATH, to see where StaSh looks for command scripts", 26 | "Check the current StaSh installation and system information with version", 27 | "See a list of available commands and their short descriptions with man", 28 | "Press the Tab button to auto-complete command and file names", 29 | "Show detailed traceback when a script fails with stashconf py_traceback 1", 30 | "Show a random tip with command totd", 31 | "Edit files with unconventional names using edit, e.g. edit .stash_config", 32 | "Use mc to access files in your Dropbox or on a FTP-Server", 33 | "Improve script-compatibility with the monkeylord command", 34 | "Access remote filesystems using the mount and umount commands" 35 | ] 36 | -------------------------------------------------------------------------------- /bin/diff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | import re 6 | import json 7 | import argparse 8 | import time 9 | import pytz 10 | import console 11 | 12 | from datetime import datetime, timedelta 13 | from difflib import unified_diff, ndiff 14 | 15 | 16 | # _____________________________________________________ 17 | def argue(): 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument("-v", "--verbose", action="store_true") 20 | parser.add_argument("lhs") 21 | parser.add_argument("rhs") 22 | args = parser.parse_args() 23 | if args.verbose: 24 | json.dump(vars(args), sys.stderr, indent=4) 25 | return args 26 | 27 | 28 | # _____________________________________________________ 29 | def sn(x): 30 | return "%s\n" % x 31 | 32 | 33 | # _____________________________________________________ 34 | def modified(f): 35 | lmt = os.path.getmtime(f) 36 | est = pytz.timezone("Australia/Sydney") 37 | gmt = pytz.timezone("GMT") 38 | tzf = "%Y-%m-%d %H:%M:%S" 39 | gdt = datetime.utcfromtimestamp(lmt) 40 | gdt = gmt.localize(gdt) 41 | adt = est.normalize(gdt.astimezone(est)) 42 | return adt.strftime(tzf) 43 | 44 | 45 | # _____________________________________________________ 46 | def diff(lhs, rhs): 47 | if not os.path.isfile(lhs): 48 | sys.stderr.write("%s not a file\n" % lhs) 49 | sys.exit(1) 50 | if os.path.isdir(rhs): 51 | rhs = "%s/%s" % (rhs, os.path.basename(lhs)) 52 | if not os.path.isfile(rhs): 53 | sys.stderr.write("%s not a file\n" % rhs) 54 | sys.exit(1) 55 | 56 | flhs = open(lhs).readlines() 57 | frhs = open(rhs).readlines() 58 | 59 | diffs = unified_diff( 60 | flhs, 61 | frhs, 62 | fromfile=lhs, 63 | tofile=rhs, 64 | fromfiledate=modified(lhs), 65 | tofiledate=modified(rhs), 66 | ) 67 | for line in diffs: 68 | if line.startswith("+"): 69 | console.set_color(0, 1, 0) 70 | if line.startswith("-"): 71 | console.set_color(0, 0, 1) 72 | sys.stdout.write(line) 73 | console.set_color(1, 1, 1) 74 | return 75 | 76 | 77 | # _____________________________________________________ 78 | def main(): 79 | console.clear() 80 | args = argue() 81 | diff(args.lhs.rstrip("/"), args.rhs.rstrip("/")) 82 | return 83 | 84 | 85 | # _____________________________________________________ 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /tests/md5sum/test_md5sum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from stash.tests.stashtest import StashTestCase 5 | 6 | 7 | class Md5sumTests(StashTestCase): 8 | """tests for the md5sum command.""" 9 | 10 | def setUp(self): 11 | """setup the tests""" 12 | self.cwd = self.get_data_path() 13 | StashTestCase.setUp(self) 14 | 15 | def get_data_path(self): 16 | """return the data/ sibling path""" 17 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) 18 | 19 | def test_help(self): 20 | """test md5sum --help""" 21 | output = self.run_command("md5sum --help", exitcode=0) 22 | # check for code words in output 23 | self.assertIn("md5sum", output) 24 | self.assertIn("-h", output) 25 | self.assertIn("-c", output) 26 | 27 | def test_filehash(self): 28 | """tests the hashes of the files in data/""" 29 | fp = self.get_data_path() 30 | for fn in os.listdir(fp): 31 | if "." in fn: 32 | # file used for something else 33 | continue 34 | expected_hash = fn 35 | fullp = os.path.join(fp, fn) 36 | output = self.run_command("md5sum " + fullp, exitcode=0) 37 | result = output.split(" ")[0] 38 | self.assertEqual(result, expected_hash) 39 | 40 | def test_checkhash(self): 41 | """test md5sum -c""" 42 | output = self.run_command("md5sum -c results.md5sum", exitcode=0) 43 | self.assertIn("Pass", output) 44 | self.assertNotIn("Fail", output) 45 | 46 | def test_checkhash_fail(self): 47 | """test failure md5sum -c with invalid data""" 48 | output = self.run_command("md5sum -c wrong_results.md5sum", exitcode=1) 49 | self.assertIn("Pass", output) # some files should have the correct hash 50 | self.assertIn("Fail", output) 51 | 52 | def test_hash_stdin_implicit(self): 53 | """test hashing of stdin without arg""" 54 | output = self.run_command("echo test | md5sum", exitcode=0).replace("\n", "") 55 | expected = "d8e8fca2dc0f896fd7cb4cb0031ba249" 56 | self.assertEqual(output, expected) 57 | 58 | def test_hash_stdin_explicit(self): 59 | """test hashing of stdin with '-' arg""" 60 | output = self.run_command("echo test | md5sum -", exitcode=0).replace("\n", "") 61 | expected = "d8e8fca2dc0f896fd7cb4cb0031ba249" 62 | self.assertEqual(output, expected) 63 | -------------------------------------------------------------------------------- /tests/system/test_completer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from stash.tests.stashtest import StashTestCase 4 | 5 | 6 | class CompleterTests(StashTestCase): 7 | def setUp(self): 8 | StashTestCase.setUp(self) 9 | self.complete = self.stash.completer.complete 10 | 11 | def test_completion_01(self): 12 | newline, possibilities = self.complete("pw") 13 | assert newline == "pwd.py " 14 | 15 | def test_completion_03(self): 16 | newline, possibilities = self.complete("ls ") 17 | assert newline == "ls " 18 | assert "README.md" in possibilities 19 | assert "source.py" not in possibilities 20 | 21 | def test_completion_04(self): 22 | newline, possibilities = self.complete("") 23 | assert newline == "" 24 | assert "source.py" in possibilities 25 | assert "README.md" not in possibilities 26 | 27 | def test_completion_05(self): 28 | newline, possibilities = self.complete("ls README.md ") 29 | assert newline == "ls README.md " 30 | assert "CHANGES.md" in possibilities 31 | assert "source.py" not in possibilities 32 | 33 | def test_completion_06(self): 34 | newline, possibilities = self.complete("git ") 35 | assert newline == "git " 36 | assert "branch" in possibilities 37 | assert "clone" in possibilities 38 | assert "README.md" not in possibilities 39 | 40 | def test_completion_07(self): 41 | newline, possibilities = self.complete("ls -") 42 | assert newline == "ls -" 43 | assert "--all" in possibilities 44 | assert "README.md" not in possibilities 45 | 46 | def test_completion_08(self): 47 | newline, possibilities = self.complete("git br") 48 | assert newline == "git branch " 49 | 50 | def test_completion_09(self): 51 | newline, possibilities = self.complete("$STASH_R") 52 | assert newline == "$STASH_ROOT " 53 | 54 | def test_completion_10(self): 55 | newline, possibilities = self.complete("$STASH_ROOT/bi") 56 | assert newline.replace("\\", "/") == "$STASH_ROOT/bin/" 57 | 58 | def test_completion_11(self): 59 | newline, possibilities = self.complete("ls $STASH_ROOT/bi") 60 | assert newline.replace("\\", "/") == "ls $STASH_ROOT/bin/" 61 | 62 | def test_completion_12(self): 63 | newline, possibilities = self.complete("ls $STASH_ROOT/bin/ls.") 64 | assert newline.replace("\\", "/") == "ls $STASH_ROOT/bin/ls.py " 65 | -------------------------------------------------------------------------------- /tests/sha1sum/test_sha1sum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from stash.tests.stashtest import StashTestCase 5 | 6 | 7 | class Sha1sumTests(StashTestCase): 8 | """tests for the sha1sum command.""" 9 | 10 | def setUp(self): 11 | """setup the tests""" 12 | self.cwd = self.get_data_path() 13 | StashTestCase.setUp(self) 14 | 15 | def get_data_path(self): 16 | """return the data/ sibling path""" 17 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) 18 | 19 | def test_help(self): 20 | """test sha1sum --help""" 21 | output = self.run_command("sha1sum --help", exitcode=0) 22 | # check for code words in output 23 | self.assertIn("sha1sum", output) 24 | self.assertIn("-h", output) 25 | self.assertIn("-c", output) 26 | 27 | def test_filehash(self): 28 | """tests the hashes of the files in data/""" 29 | fp = self.get_data_path() 30 | for fn in os.listdir(fp): 31 | if "." in fn: 32 | # file used for something else 33 | continue 34 | expected_hash = fn 35 | fullp = os.path.join(fp, fn) 36 | output = self.run_command("sha1sum " + fullp, exitcode=0) 37 | result = output.split(" ")[0] 38 | self.assertEqual(result, expected_hash) 39 | 40 | def test_checkhash(self): 41 | """test sha1sum -c""" 42 | output = self.run_command("sha1sum -c results.sha1sum", exitcode=0) 43 | self.assertIn("Pass", output) 44 | self.assertNotIn("Fail", output) 45 | 46 | def test_checkhash_fail(self): 47 | """test failure sha1sum -c with invalid data""" 48 | output = self.run_command("sha1sum -c wrong_results.sha1sum", exitcode=1) 49 | self.assertIn("Pass", output) # some files should have the correct hash 50 | self.assertIn("Fail", output) 51 | 52 | def test_hash_stdin_implicit(self): 53 | """test hashing of stdin without arg""" 54 | output = self.run_command("echo test | sha1sum", exitcode=0).replace("\n", "") 55 | expected = "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83" 56 | self.assertEqual(output, expected) 57 | 58 | def test_hash_stdin_explicit(self): 59 | """test hashing of stdin with '-' arg""" 60 | output = self.run_command("echo test | sha1sum -", exitcode=0).replace("\n", "") 61 | expected = "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83" 62 | self.assertEqual(output, expected) 63 | -------------------------------------------------------------------------------- /bin/ssh-keygen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Generates RSA/DSA SSH Keys. 4 | 5 | Keys stored in stash/.ssh/ 6 | 7 | usage: 8 | [-t] - Key type (rsa/dsa) 9 | [-b] - bits default: 1028 10 | [-N] - password default:None 11 | [-f] - output file name. default: id_ssh_key 12 | """ 13 | 14 | from __future__ import print_function 15 | import os 16 | import sys 17 | import argparse 18 | import paramiko 19 | 20 | SSH_DIRS = [ 21 | os.path.expanduser("~/.ssh"), 22 | os.path.join(os.environ["STASH_ROOT"], ".ssh"), 23 | ] 24 | 25 | key_mode = {"rsa": "rsa", "dsa": "dss"} 26 | 27 | 28 | def main(args): 29 | ap = argparse.ArgumentParser(args) 30 | ap.add_argument( 31 | "-t", 32 | choices=("rsa", "dsa"), 33 | default="rsa", 34 | action="store", 35 | dest="type", 36 | help="Key Type: (rsa,dsa)", 37 | ) 38 | ap.add_argument( 39 | "-b", 40 | action="store", 41 | dest="bits", 42 | default=1024, 43 | type=int, 44 | help="bits for key gen. default: 1024", 45 | ) 46 | ap.add_argument( 47 | "-N", 48 | dest="password", 49 | default=None, 50 | action="store", 51 | help="password default: None", 52 | ) 53 | ap.add_argument( 54 | "-f", 55 | dest="filename", 56 | default=False, 57 | action="store", 58 | help="Filename default: id_rsa/dsa", 59 | ) 60 | ns = ap.parse_args() 61 | 62 | # Keygen for keypair 63 | for SSH_DIR in SSH_DIRS: 64 | if not os.path.isdir(SSH_DIR): 65 | os.mkdir(SSH_DIR) 66 | 67 | try: 68 | k = None 69 | if ns.type == "rsa": 70 | k = paramiko.RSAKey.generate(ns.bits) 71 | filename = ns.filename or "id_rsa" 72 | elif ns.type == "dsa": 73 | k = paramiko.DSSKey.generate(ns.bits) 74 | filename = ns.filename or "id_dsa" 75 | 76 | if k: 77 | for SSH_DIR in SSH_DIRS: 78 | filepath = os.path.join(SSH_DIR, filename) 79 | k.write_private_key_file(filepath, password=ns.password) 80 | with open(filepath + ".pub", "w") as outs: 81 | outs.write("ssh-" + key_mode[ns.type] + " " + k.get_base64()) 82 | print("ssh keys generated with %s encryption" % ns.type) 83 | else: 84 | print("Keys not generated") 85 | except Exception as e: 86 | print(e) 87 | 88 | 89 | if __name__ == "__main__": 90 | main(sys.argv[1:]) 91 | -------------------------------------------------------------------------------- /bin/curl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Transfer a URL""" 3 | 4 | from __future__ import print_function 5 | import sys 6 | import argparse 7 | import requests 8 | 9 | from six.moves.urllib.parse import urlparse 10 | 11 | try: 12 | import clipboard 13 | except ImportError: 14 | clipboard = None 15 | 16 | 17 | def main(args): 18 | ap = argparse.ArgumentParser() 19 | ap.add_argument("url", nargs="?", help="the url to read (default to clipboard") 20 | ap.add_argument( 21 | "-o", "--output-file", help="write output to file instead of stdout" 22 | ) 23 | ap.add_argument( 24 | "-O", 25 | "--remote-name", 26 | action="store_true", 27 | help="write output to a local file named like the remote file we get", 28 | ) 29 | ap.add_argument( 30 | "-L", 31 | "--location", 32 | action="store_true", 33 | help="follow redirects to other web pages (if the URL has a 3XX response code)", 34 | ) 35 | ap.add_argument( 36 | "-X", 37 | "--request-method", 38 | default="GET", 39 | choices=["GET", "POST", "HEAD"], 40 | help="specify request method to use (default to GET)", 41 | ) 42 | ap.add_argument("-H", "--header", help="Custom header to pass to server (H)") 43 | ap.add_argument("-d", "--data", help="HTTP POST data (H)") 44 | 45 | ns = ap.parse_args(args) 46 | url = ns.url or clipboard.get() 47 | 48 | headers = {} 49 | if ns.header: 50 | for h in ns.header.split(";"): 51 | name, value = h.split(":") 52 | headers[name.strip()] = value.strip() 53 | 54 | if ns.request_method == "GET": 55 | r = requests.get(url, headers=headers, allow_redirects=ns.location) 56 | elif ns.request_method == "POST": 57 | r = requests.post( 58 | url, data=ns.data, headers=headers, allow_redirects=ns.location 59 | ) 60 | elif ns.request_method == "HEAD": 61 | r = requests.head(url, headers=headers, allow_redirects=ns.location) 62 | else: 63 | print("unknown request method: {}".format(ns.request_method)) 64 | return 65 | 66 | if ns.output_file: 67 | with open(ns.output_file, "wb") as outs: 68 | outs.write(r.content) 69 | elif ns.remote_name: 70 | # get basename of url 71 | url_path = urlparse(url).path 72 | filename = url_path.split("/")[-1] 73 | with open(filename, "wb") as outs: 74 | outs.write(r.content) 75 | else: 76 | print(r.text) 77 | 78 | 79 | if __name__ == "__main__": 80 | main(sys.argv[1:]) 81 | -------------------------------------------------------------------------------- /tools/colortest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test color support 3 | """ 4 | 5 | _stash = globals()["_stash"] 6 | 7 | 8 | def get_all_bg_colors(): 9 | """ 10 | Return a list of all known bg colors 11 | """ 12 | return _stash.renderer.BG_COLORS.keys() 13 | 14 | 15 | def get_all_fg_colors(): 16 | """ 17 | Return a list of all known fg colors 18 | """ 19 | return _stash.renderer.FG_COLORS.keys() 20 | 21 | 22 | def main(): 23 | """ 24 | The main function 25 | """ 26 | print("============ COLOR TEST ===================") 27 | bg_colors = get_all_bg_colors() 28 | fg_colors = get_all_fg_colors() 29 | print("------------ available colors -------------") 30 | print("Known FG colors: " + ", ".join(fg_colors)) 31 | print("Known BG colors: " + ", ".join(bg_colors)) 32 | print("------- showing all combinations ----------") 33 | for fg in _stash.renderer.FG_COLORS: 34 | for bg in _stash.renderer.BG_COLORS: 35 | for bold in (False, True): 36 | for italics in (False, True): 37 | for underscore in (False, True): 38 | for strikethrough in (False, True): 39 | for reverse in (False, True): 40 | traits = [] 41 | if bold: 42 | traits.append("bold") 43 | if italics: 44 | traits.append("italic") 45 | if underscore: 46 | traits.append("underline") 47 | if strikethrough: 48 | traits.append("strikethrough") 49 | desc = "{}-{}{}{}".format( 50 | fg, 51 | bg, 52 | ("-" if len(traits) > 0 else ""), 53 | "-".join(traits), 54 | ) 55 | s = _stash.text_style( 56 | desc, 57 | dict( 58 | color=fg, 59 | bgcolor=bg, 60 | traits=traits, 61 | ), 62 | ) 63 | print(s) 64 | print("================= Done =====================") 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /tests/ui/test_tkui.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests for the tkui 3 | """ 4 | 5 | import logging 6 | 7 | from unittest import skipIf 8 | 9 | from stash.tests.stashtest import StashTestCase 10 | 11 | try: 12 | from stash.system.shui.tkui import ShTerminal 13 | except ImportError: 14 | ShTerminal = None 15 | 16 | 17 | class NoInitTkTerminal(ShTerminal): 18 | """ 19 | Subclass of ShTerminal which does not initiate the superclass 20 | """ 21 | 22 | def __init__(self, text=""): 23 | self._text = "" 24 | self.text = text 25 | self.logger = logging.getLogger("StaSh.Terminal") 26 | 27 | @property 28 | def text(self): 29 | return self._text 30 | 31 | @text.setter 32 | def text(self, value): 33 | self._text = value 34 | 35 | 36 | @skipIf(ShTerminal is None, "No Tk-GUI available") 37 | class TkTerminalTests(StashTestCase): 38 | """ 39 | Tests for stash.system.shui.tkui.ShTerminal 40 | """ 41 | 42 | tc = NoInitTkTerminal 43 | 44 | def test_tk_index_conversion(self): 45 | """ 46 | Test conversion to and from a tk index to a tuple 47 | """ 48 | values = { # tk index -> expected 49 | "1.0": (0, 0), 50 | "1.1": (0, 1), 51 | "2.0": (1, 0), 52 | "2.2": (1, 2), 53 | "10.11": (9, 11), 54 | "9.2": (8, 2), 55 | } 56 | terminal = self.tc() 57 | for tki in values: 58 | expected = values[tki] 59 | converted = terminal._tk_index_to_tuple(tki) 60 | self.assertEqual(converted, expected) 61 | # convert back 62 | back = terminal._tuple_to_tk_index(converted) 63 | self.assertEqual(back, tki) 64 | 65 | def test_abs_rel_conversion_1(self): 66 | """ 67 | First test for conversion of absolute and relative indexes 68 | """ 69 | s = """0123 70 | 567 71 | 9 72 | """ 73 | values = { # rel -> abs 74 | 0: (0, 0), 75 | 1: (0, 1), 76 | 2: (0, 2), 77 | 3: (0, 3), 78 | 4: (0, 4), 79 | 5: (1, 0), 80 | 6: (1, 1), 81 | 7: (1, 2), 82 | 8: (1, 3), 83 | 9: (2, 0), 84 | 10: (2, 1), 85 | } 86 | terminal = self.tc(s) 87 | for rel in values: 88 | expected = values[rel] 89 | ab = terminal._rel_cursor_pos_to_abs_pos(rel) 90 | self.assertEqual(ab, expected) 91 | # convert back 92 | back = terminal._abs_cursor_pos_to_rel_pos(ab) 93 | self.assertEqual(back, rel) 94 | -------------------------------------------------------------------------------- /tests/sha256sum/test_sha256sum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from stash.tests.stashtest import StashTestCase 5 | 6 | 7 | class Sha256sumTests(StashTestCase): 8 | """tests for the sha256sum command.""" 9 | 10 | def setUp(self): 11 | """setup the tests""" 12 | self.cwd = self.get_data_path() 13 | StashTestCase.setUp(self) 14 | 15 | def get_data_path(self): 16 | """return the data/ sibling path""" 17 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) 18 | 19 | def test_help(self): 20 | """test sha256sum --help""" 21 | output = self.run_command("sha256sum --help", exitcode=0) 22 | # check for code words in output 23 | self.assertIn("sha256sum", output) 24 | self.assertIn("-h", output) 25 | self.assertIn("-c", output) 26 | 27 | def test_filehash(self): 28 | """tests the hashes of the files in data/""" 29 | fp = self.get_data_path() 30 | for fn in os.listdir(fp): 31 | if "." in fn: 32 | # file used for something else 33 | continue 34 | expected_hash = fn 35 | fullp = os.path.join(fp, fn) 36 | output = self.run_command("sha256sum " + fullp, exitcode=0) 37 | result = output.split(" ")[0] 38 | self.assertEqual(result, expected_hash) 39 | 40 | def test_checkhash(self): 41 | """test sha256sum -c""" 42 | output = self.run_command("sha256sum -c results.sha256sum", exitcode=0) 43 | self.assertIn("Pass", output) 44 | self.assertNotIn("Fail", output) 45 | 46 | def test_checkhash_fail(self): 47 | """test failure sha256sum -c with invalid data""" 48 | output = self.run_command("sha256sum -c wrong_results.sha256sum", exitcode=1) 49 | self.assertIn("Pass", output) # some files should have the correct hash 50 | self.assertIn("Fail", output) 51 | 52 | def test_hash_stdin_implicit(self): 53 | """test hashing of stdin without arg""" 54 | output = self.run_command("echo test | sha256sum", exitcode=0).replace("\n", "") 55 | expected = "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" 56 | self.assertEqual(output, expected) 57 | 58 | def test_hash_stdin_explicit(self): 59 | """test hashing of stdin with '-' arg""" 60 | output = self.run_command("echo test | sha256sum -", exitcode=0).replace( 61 | "\n", "" 62 | ) 63 | expected = "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" 64 | self.assertEqual(output, expected) 65 | -------------------------------------------------------------------------------- /lib/pythonista_add_action.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Manipulate action (wrench) menu 4 | example: 5 | add_action('/stash/launch_stash.py','monitor') 6 | save_defaults() # so it is stored for next launch 7 | 8 | """ 9 | 10 | # This module was created by jsbain. Thanks for sharing it! 11 | 12 | from objc_util import ObjCClass, ns 13 | 14 | NSUserDefaults = ObjCClass("NSUserDefaults") 15 | 16 | 17 | def add_action(scriptName, iconName="python", iconColor="", title=""): 18 | """adds an editor action. scriptName should start with / 19 | (e.g /stash/stash.py) 20 | iconName should be an icon without leading prefix, 21 | or trailing size. i.e alert instead of iob:alert_256 22 | iconColor should be a web style hex string, eg aa00ff 23 | title is the alternative title 24 | Call save_defaults() to store defaults 25 | ')""" 26 | defaults = NSUserDefaults.standardUserDefaults() 27 | kwargs = locals() 28 | entry = { 29 | key: kwargs[key] 30 | for key in ("scriptName", "iconName", "iconColor", "title", "arguments") 31 | if key in kwargs and kwargs[key] 32 | } 33 | editoractions = get_actions() 34 | editoractions.append(ns(entry)) 35 | defaults.setObject_forKey_(editoractions, "EditorActionInfos") 36 | 37 | 38 | def remove_action(scriptName): 39 | """remove all instances of a given scriptname. 40 | Call save_defaults() to store for next session 41 | """ 42 | defaults = NSUserDefaults.standardUserDefaults() 43 | editoractions = get_actions() 44 | [ 45 | editoractions.remove(x) 46 | for x in editoractions 47 | if str(x["scriptName"]) == scriptName 48 | ] 49 | defaults.setObject_forKey_(editoractions, "EditorActionInfos") 50 | 51 | 52 | def remove_action_at_index(index): 53 | """remove action at index. Call save_defaults() to save result.""" 54 | defaults = NSUserDefaults.standardUserDefaults() 55 | editoractions = get_actions() 56 | del editoractions[index] 57 | defaults.setObject_forKey_(editoractions, "EditorActionInfos") 58 | 59 | 60 | def get_defaults_dict(): 61 | """return NSdictionary of defaults""" 62 | defaults = NSUserDefaults.standardUserDefaults() 63 | return defaults.dictionaryRepresentation() 64 | 65 | 66 | def get_actions(): 67 | """return action list""" 68 | defaults = NSUserDefaults.standardUserDefaults() 69 | return list(defaults.arrayForKey_("EditorActionInfos") or ()) 70 | 71 | 72 | def save_defaults(): 73 | """save current set of defaults""" 74 | defaults = NSUserDefaults.standardUserDefaults() 75 | NSUserDefaults.setStandardUserDefaults_(defaults) 76 | -------------------------------------------------------------------------------- /bin/python3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Simulates a console call to python3 [-m module][-c cmd] [file] [args] 4 | 5 | Used for running standard library python3 modules such as: 6 | unittest and .py files. 7 | 8 | Can also be used to run a script in the background, such as a server, with the bash character & at the end. 9 | usage: 10 | python3 11 | python3 -m module_name [args] 12 | python3 -c command 13 | python3 python_file.py [args] 14 | """ 15 | 16 | from __future__ import print_function 17 | 18 | # check for py2/3 19 | _stash = globals()["_stash"] 20 | if not _stash.PY3: 21 | print( 22 | _stash.text_color( 23 | "You are running StaSh in python 2.\nRunning python3 from python 2 is not (yet) supported.\nPlease use the 'python' command instead.", 24 | "red", 25 | ) 26 | ) 27 | import sys 28 | 29 | sys.exit(1) 30 | 31 | import runpy 32 | import sys 33 | import argparse 34 | import code 35 | import builtins 36 | 37 | args = sys.argv[1:] 38 | 39 | passing_h = False 40 | if "-h" in args and len(args) > 1: 41 | args.remove("-h") 42 | passing_h = True 43 | 44 | ap = argparse.ArgumentParser() 45 | 46 | group = ap.add_mutually_exclusive_group() 47 | group.add_argument("-m", "--module", action="store", default=None, help="run module") 48 | group.add_argument( 49 | "-c", 50 | "--cmd", 51 | action="store", 52 | default=None, 53 | help="program passed in as string (terminates option list)", 54 | ) 55 | 56 | ap.add_argument( 57 | "args_to_pass", 58 | metavar="[file] args_to_pass", 59 | default=[], 60 | nargs=argparse.REMAINDER, 61 | help="Python script and arguments", 62 | ) 63 | 64 | ns = ap.parse_args(args) 65 | if passing_h: 66 | ns.args_to_pass.append("-h") 67 | 68 | if ns.module: 69 | sys.argv = [ns.module] + ns.args_to_pass 70 | try: 71 | runpy.run_module(ns.module, run_name="__main__") 72 | except ImportError as e: 73 | print("ImportError: " + str(e)) 74 | sys.exit(1) 75 | sys.exit(0) 76 | 77 | elif ns.cmd: 78 | exec(ns.cmd) 79 | sys.exit(0) 80 | 81 | else: 82 | if ns.args_to_pass: 83 | sys.argv = ns.args_to_pass 84 | try: 85 | runpy.run_path(str(sys.argv[0]), run_name="__main__") 86 | except Exception as e: 87 | print("Error: " + str(e)) 88 | else: 89 | locals = { 90 | "__name__": "__main__", 91 | "__doc__": None, 92 | "__package__": None, 93 | "__debug__": True, 94 | "__builtins__": builtins, 95 | "_stash": _stash, 96 | } 97 | code.interact(local=locals) 98 | -------------------------------------------------------------------------------- /bin/mv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Move (rename) a file or directory to a new name, or into a new 4 | directory. Multiple source files may be specified if the destination is 5 | an existing directory. 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | import argparse 11 | import os 12 | import shutil 13 | import sys 14 | 15 | 16 | def main(args): 17 | p = argparse.ArgumentParser(description=__doc__) 18 | p.add_argument( 19 | "src", action="store", nargs="+", help="one or more source files or folders" 20 | ) 21 | p.add_argument("dest", action="store", help="the destination name or folder") 22 | ns = p.parse_args(args) 23 | 24 | status = 0 25 | 26 | if len(ns.src) > 1: 27 | # Multiple source files 28 | if os.path.exists(ns.dest): 29 | # Destination must exist... 30 | if os.path.isdir(ns.dest): 31 | # ...and be a directory 32 | for src in ns.src: 33 | try: 34 | # Attempt to move every source into destination 35 | shutil.move(src, ns.dest) 36 | except Exception as err: 37 | print( 38 | "mv: {}: {!s}".format(type(err).__name__, err), 39 | file=sys.stderr, 40 | ) 41 | status = 1 42 | else: 43 | print("mv: {}: not a directory".format(ns.dest), file=sys.stderr) 44 | else: 45 | print("mv: {}: no such file or directory".format(ns.dest), file=sys.stderr) 46 | status = 1 47 | else: 48 | # Single source file 49 | src = ns.src[0] 50 | if os.path.exists(src): 51 | # Source must exist 52 | if not os.path.isfile(ns.dest): 53 | # Python will rename source if it doesn't exists 54 | # And will move source into destination if it is a directory 55 | try: 56 | shutil.move(src, ns.dest) 57 | except Exception as err: 58 | print( 59 | "mv: {}: {!s}".format(type(err).__name__, err), file=sys.stderr 60 | ) 61 | status = 1 62 | else: 63 | # Won't overwrite unasked 64 | print("mv: {}: file exists".format(ns.dest), file=sys.stderr) 65 | else: 66 | print("mv: {}: no such file or directory".format(src), file=sys.stderr) 67 | status = 1 68 | 69 | sys.exit(status) 70 | 71 | 72 | if __name__ == "__main__": 73 | main(sys.argv[1:]) 74 | -------------------------------------------------------------------------------- /bin/python.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Simulates a console call to python [-m module][-c cmd] [file] [args] 4 | 5 | Used for running standard library python modules such as: 6 | SimpleHTTPServer, unittest and .py files. 7 | 8 | Can also be used to run a script in the background, such as a server, with the bash character & at the end. 9 | usage: 10 | python 11 | python -m module_name [args] 12 | python -c command 13 | python python_file.py [args] 14 | """ 15 | 16 | from __future__ import print_function 17 | 18 | # check for py2/3 19 | _stash = globals()["_stash"] 20 | if _stash.PY3: 21 | print( 22 | _stash.text_color( 23 | "You are running StaSh in python3.\nRunning python 2 from python 3 is not (yet) supported.\nPlease use the 'python3' command instead.", 24 | "red", 25 | ) 26 | ) 27 | import sys 28 | 29 | sys.exit(1) 30 | 31 | import runpy 32 | import sys 33 | import argparse 34 | import code 35 | import __builtin__ 36 | 37 | args = sys.argv[1:] 38 | 39 | passing_h = False 40 | if "-h" in args and len(args) > 1: 41 | args.remove("-h") 42 | passing_h = True 43 | 44 | ap = argparse.ArgumentParser() 45 | 46 | group = ap.add_mutually_exclusive_group() 47 | group.add_argument("-m", "--module", action="store", default=None, help="run module") 48 | group.add_argument( 49 | "-c", 50 | "--cmd", 51 | action="store", 52 | default=None, 53 | help="program passed in as string (terminates option list)", 54 | ) 55 | 56 | ap.add_argument( 57 | "args_to_pass", 58 | metavar="[file] args_to_pass", 59 | default=[], 60 | nargs=argparse.REMAINDER, 61 | help="Python script and arguments", 62 | ) 63 | 64 | ns = ap.parse_args(args) 65 | if passing_h: 66 | ns.args_to_pass.append("-h") 67 | 68 | if ns.module: 69 | sys.argv = [ns.module] + ns.args_to_pass 70 | try: 71 | runpy.run_module(str(ns.module), run_name="__main__") 72 | except ImportError as e: 73 | print("ImportError: " + str(e)) 74 | sys.exit(1) 75 | sys.exit(0) 76 | 77 | elif ns.cmd: 78 | exec(ns.cmd) 79 | sys.exit(0) 80 | 81 | else: 82 | if ns.args_to_pass: 83 | sys.argv = ns.args_to_pass 84 | try: 85 | runpy.run_path(str(sys.argv[0]), run_name="__main__") 86 | except Exception as e: 87 | print("Error: " + str(e)) 88 | else: 89 | locals = { 90 | "__name__": "__main__", 91 | "__doc__": None, 92 | "__package__": None, 93 | "__debug__": True, 94 | "__builtins__": __builtin__, # yes, __builtins__ 95 | "_stash": _stash, 96 | } 97 | code.interact(local=locals) 98 | -------------------------------------------------------------------------------- /tests/cat/test_cat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'cat' command.""" 3 | 4 | import os 5 | from unittest import expectedFailure 6 | 7 | from stash.tests.stashtest import StashTestCase 8 | 9 | 10 | class CatTests(StashTestCase): 11 | """Tests for the 'cat' command.""" 12 | 13 | def setUp(self): 14 | """setup the tests""" 15 | self.cwd = self.get_data_path() 16 | StashTestCase.setUp(self) 17 | 18 | def get_data_path(self): 19 | """return the data/ sibling path""" 20 | return os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) 21 | 22 | def read_data_file(self, fn): 23 | """returns the content of the file 'fn' in the data sibling dir.""" 24 | fp = os.path.join(self.get_data_path(), fn) 25 | with open(fp, "r") as fin: 26 | content = fin.read() 27 | return content 28 | 29 | def test_help(self): 30 | """test 'cat --help'.""" 31 | output = self.run_command("cat --help", exitcode=0) 32 | self.assertIn("cat", output) 33 | self.assertIn("-h", output) 34 | self.assertIn("--help", output) 35 | self.assertIn("files", output) 36 | 37 | def test_cat_file(self): 38 | """test 'cat '.""" 39 | output = self.run_command("cat somefile.txt", exitcode=0) 40 | expected = self.read_data_file("somefile.txt") 41 | self.assertEqual(output, expected) 42 | 43 | def test_cat_multi_files(self): 44 | """test 'cat '.""" 45 | output = self.run_command("cat somefile.txt otherfile.txt", exitcode=0) 46 | expected = self.read_data_file("somefile.txt") + self.read_data_file( 47 | "otherfile.txt" 48 | ) 49 | self.assertEqual(output, expected) 50 | 51 | def test_cat_stdin(self): 52 | """test 'cat | cat -'.""" 53 | # we test 'cat ' in a seperate test, so we can use it here 54 | output = self.run_command("cat somefile.txt | cat -", exitcode=0) 55 | expected = self.read_data_file("somefile.txt") 56 | self.assertEqual(output, expected) 57 | 58 | def test_cat_nonexistent(self): 59 | """test 'cat '.""" 60 | output = self.run_command("cat invalid.txt", exitcode=1) 61 | self.assertIn("cat: ", output) 62 | self.assertIn("No such file or directory: ", output) 63 | self.assertIn("invalid.txt", output) 64 | 65 | def test_cat_nonascii(self): 66 | """test 'cat '.""" 67 | output = self.run_command("cat nonascii.txt", exitcode=0).replace("\n", "") 68 | self.assertEqual(output, "äöüß") 69 | -------------------------------------------------------------------------------- /lib/mlpatches/l2c.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """convert a list of args to a cmd; copied from subprocess""" 3 | # info: this replaces the _get_str() used previously by mlpatches 4 | 5 | from six import string_types 6 | 7 | 8 | def list2cmdline(seq): 9 | """ 10 | Translate a sequence of arguments into a command line 11 | string, using the same rules as the MS C runtime: 12 | 13 | 1) Arguments are delimited by white space, which is either a 14 | space or a tab. 15 | 16 | 2) A string surrounded by double quotation marks is 17 | interpreted as a single argument, regardless of white space 18 | contained within. A quoted string can be embedded in an 19 | argument. 20 | 21 | 3) A double quotation mark preceded by a backslash is 22 | interpreted as a literal double quotation mark. 23 | 24 | 4) Backslashes are interpreted literally, unless they 25 | immediately precede a double quotation mark. 26 | 27 | 5) If backslashes immediately precede a double quotation mark, 28 | every pair of backslashes is interpreted as a literal 29 | backslash. If the number of backslashes is odd, the last 30 | backslash escapes the next double quotation mark as 31 | described in rule 3. 32 | """ 33 | 34 | # See 35 | # http://msdn.microsoft.com/en-us/library/17w5ykft.aspx 36 | # or search http://msdn.microsoft.com for 37 | # "Parsing C++ Command-Line Arguments" 38 | if isinstance(seq, string_types): 39 | return seq 40 | result = [] 41 | needquote = False 42 | for arg in seq: 43 | bs_buf = [] 44 | 45 | # Add a space to separate this argument from the others 46 | if result: 47 | result.append(" ") 48 | 49 | needquote = (" " in arg) or ("\t" in arg) or not arg 50 | if needquote: 51 | result.append('"') 52 | 53 | for c in arg: 54 | if c == "\\": 55 | # Don't know if we need to double yet. 56 | bs_buf.append(c) 57 | elif c == '"': 58 | # Double backslashes. 59 | result.append("\\" * len(bs_buf) * 2) 60 | bs_buf = [] 61 | result.append('\\"') 62 | else: 63 | # Normal char 64 | if bs_buf: 65 | result.extend(bs_buf) 66 | bs_buf = [] 67 | result.append(c) 68 | 69 | # Add remaining backslashes, if any. 70 | if bs_buf: 71 | result.extend(bs_buf) 72 | 73 | if needquote: 74 | result.extend(bs_buf) 75 | result.append('"') 76 | 77 | return "".join(result) 78 | 79 | 80 | _get_str = list2cmdline 81 | -------------------------------------------------------------------------------- /tests/misc/test_ping.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the ping command.""" 3 | 4 | import time 5 | import unittest 6 | 7 | from stash.tests.stashtest import StashTestCase, requires_network 8 | 9 | 10 | class PingTests(StashTestCase): 11 | """tests for the 'ping' command.""" 12 | 13 | def test_help(self): 14 | """test 'ping --help'""" 15 | output = self.run_command("ping --help", exitcode=0) 16 | self.assertIn("ping", output) 17 | self.assertIn("-h", output) 18 | self.assertIn("--help", output) 19 | self.assertIn("-c", output) 20 | self.assertIn("--count", output) 21 | self.assertIn("-W", output) 22 | self.assertIn("--timeout", output) 23 | 24 | @unittest.expectedFailure 25 | @requires_network 26 | def test_ping_normal(self): 27 | """test 'ping '.""" 28 | target = "8.8.8.8" 29 | output = self.run_command("ping " + target, exitcode=0) 30 | self.assertIn("got ping in " + target, output) 31 | self.assertNotIn("failed", output) 32 | 33 | @unittest.expectedFailure 34 | @requires_network 35 | def test_count(self): 36 | """test 'ping --count '.""" 37 | target = "8.8.8.8" 38 | for n in (1, 3, 5): 39 | output = self.run_command( 40 | "ping " + target + " --count " + str(n), exitcode=0 41 | ) 42 | self.assertIn("got ping in " + target, output) 43 | self.assertNotIn("failed", output) 44 | c = output.count("got ping in") 45 | self.assertEqaual(n, c) 46 | 47 | @unittest.expectedFailure 48 | @requires_network 49 | def test_interval(self): 50 | """test 'ping --interval '.""" 51 | target = "8.8.8.8" 52 | c = 3 53 | for t in (1, 5, 10): 54 | st = time.time() 55 | output = self.run_command( 56 | "ping " + target + " --count " + str(c) + " --interval " + str(t), 57 | exitcode=0, 58 | ) 59 | et = time.time() 60 | dt = et - st 61 | self.assertIn("got ping in " + target, output) 62 | self.assertNotIn("failed", output) 63 | n = output.count("got ping in") 64 | self.assertEqaual(n, c) 65 | mintime = c * t 66 | maxtime = c * t + 5 67 | self.assertGreaterEqual(dt, mintime) 68 | self.assertLessEqual(dt, maxtime) 69 | 70 | @unittest.expectedFailure 71 | @unittest.skip("Test not implemented") 72 | def test_timeout(): 73 | """test 'ping --timeout '.""" 74 | # no idea how to implement a test for this case 75 | raise NotImplementedError 76 | -------------------------------------------------------------------------------- /tests/pbcopy_pbpaste/test_pbcopy_pbpaste.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for pbcopy/pbpaste commands. 3 | """ 4 | 5 | import os 6 | import tempfile 7 | from io import open 8 | 9 | from stash.tests.stashtest import StashTestCase 10 | 11 | 12 | class CopyPasteTests(StashTestCase): 13 | """ 14 | Test class for the 'pbcopy' and 'pbpaste' commands. 15 | """ 16 | 17 | def test_pbcopy_help(self): 18 | """ 19 | test 'pbcopy --help'. 20 | """ 21 | output_1 = self.run_command("pbcopy -h", exitcode=0) 22 | output_2 = self.run_command("pbcopy --help", exitcode=0) 23 | self.assertEqual(output_1, output_2) 24 | self.assertIn("-h", output_1) 25 | self.assertIn("--help", output_1) 26 | self.assertIn("file", output_1) 27 | self.assertIn("pbcopy", output_1) 28 | self.assertIn("...", output_1) 29 | 30 | def test_pbpaste_help(self): 31 | """ 32 | test 'pbpaste --help'. 33 | """ 34 | output_1 = self.run_command("pbpaste -h", exitcode=0) 35 | output_2 = self.run_command("pbpaste --help", exitcode=0) 36 | self.assertEqual(output_1, output_2) 37 | self.assertIn("-h", output_1) 38 | self.assertIn("--help", output_1) 39 | self.assertIn("file", output_1) 40 | self.assertIn("pbpaste", output_1) 41 | 42 | def test_copy_paste_stdin(self): 43 | """ 44 | Test copy of stdin & paste 45 | """ 46 | self.run_command("echo teststring | pbcopy", exitcode=0) 47 | output = self.run_command("pbpaste", exitcode=0) 48 | self.assertEqual("teststring\n", output) 49 | 50 | def test_copy_paste_file(self): 51 | """ 52 | Test copy of a file & paste 53 | """ 54 | p = os.path.join(self.get_data_path(), "testfile.txt") 55 | self.run_command("pbcopy " + p, exitcode=0) 56 | output = self.run_command("pbpaste", exitcode=0) 57 | with open(p, "r", encoding="utf-8") as fin: 58 | content = fin.read() 59 | self.assertEqual(output, content) 60 | 61 | def test_paste_into_file(self): 62 | """ 63 | Test copy of a file & paste into a file. 64 | Comparsion is done using 'md5sum' 65 | """ 66 | pin = os.path.join(self.get_data_path(), "testfile.txt") 67 | pout = os.path.join(tempfile.gettempdir(), "testpastefile.txt") 68 | if os.path.exists(pout): 69 | os.remove(pout) 70 | self.run_command("pbcopy " + pin, exitcode=0) 71 | self.run_command("pbpaste " + pout, exitcode=0) 72 | org_hash = self.run_command("md5sum " + pin, exitcode=0).split()[0] 73 | paste_hash = self.run_command("md5sum " + pout, exitcode=0).split()[0] 74 | self.assertEqual(org_hash, paste_hash) 75 | -------------------------------------------------------------------------------- /bin/crypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | File encryption for stash 4 | Uses AES in CBC mode. 5 | 6 | usage: crypt.py [-h] [-k KEY] [-d] infile [outfile] 7 | 8 | positional arguments: 9 | infile File to encrypt/decrypt. 10 | outfile Output file. 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | -k KEY, --key KEY Encrypt/Decrypt Key. 15 | -d, --decrypt Flag to decrypt. 16 | """ 17 | 18 | from __future__ import print_function 19 | import argparse 20 | import base64 21 | import os 22 | 23 | _stash = globals()["_stash"] 24 | try: 25 | import pyaes 26 | except ImportError: 27 | print("Installing Required packages...") 28 | _stash("pip install pyaes") 29 | import pyaes 30 | 31 | 32 | class Crypt(object): 33 | def __init__(self, in_filename, out_filename=None): 34 | self.in_filename = in_filename 35 | self.out_filename = out_filename 36 | 37 | def aes_encrypt(self, key=None, chunksize=64 * 1024): 38 | self.out_filename = self.out_filename or self.in_filename + ".enc" 39 | if key is None: 40 | key = base64.b64encode(os.urandom(32))[:32] 41 | aes = pyaes.AESModeOfOperationCTR(key) 42 | with open(self.in_filename, "rb") as infile: 43 | with open(self.out_filename, "wb") as outfile: 44 | pyaes.encrypt_stream(aes, infile, outfile) 45 | return key 46 | 47 | def aes_decrypt(self, key, chunksize=64 * 1024): 48 | self.out_filename = self.out_filename or os.path.splitext(self.in_filename)[0] 49 | aes = pyaes.AESModeOfOperationCTR(key) 50 | 51 | with open(self.in_filename, "rb") as infile: 52 | with open(self.out_filename, "wb") as outfile: 53 | pyaes.decrypt_stream(aes, infile, outfile) 54 | 55 | 56 | if __name__ == "__main__": 57 | ap = argparse.ArgumentParser() 58 | ap.add_argument( 59 | "-k", 60 | "--key", 61 | action="store", 62 | default=None, 63 | help="Encrypt/Decrypt Key.", 64 | ) 65 | ap.add_argument( 66 | "-d", 67 | "--decrypt", 68 | action="store_true", 69 | default=False, 70 | help="Flag to decrypt.", 71 | ) 72 | # ap.add_argument('-t','--type',action='store',choices={'aes','rsa'},default='aes') 73 | ap.add_argument("infile", action="store", help="File to encrypt/decrypt.") 74 | ap.add_argument("outfile", action="store", nargs="?", help="Output file.") 75 | args = ap.parse_args() 76 | crypt = Crypt(args.infile, args.outfile) 77 | if args.decrypt: 78 | crypt.aes_decrypt(args.key.encode()) 79 | else: 80 | nk = crypt.aes_encrypt(args.key) 81 | if args.key is None: 82 | print("Key: %s" % nk.decode()) 83 | -------------------------------------------------------------------------------- /man/monkeypatching/page_5.txt: -------------------------------------------------------------------------------- 1 | monkeypatching(5) --- CREATING NEW PATCHES 2 | ========================================== 3 | 4 | All builtin Patches are contained in modules in "$STASH_ROOT/lib/mlpatches". 5 | 6 | The following Patchtypes are available: 7 | BasePatch(): 8 | - does nothing 9 | - superclass for other patches 10 | - overwrite "PY2" and "PY3" with bools to define compatibility 11 | - overwrite "do_enable()" to enable the patch 12 | - overwrite "do_disable()" to disable the patch 13 | - overwrite "dependencies" with a list of patches to enable first 14 | - Most overwrite are optional and already done in the available subclasses. 15 | FunctionPatch(): 16 | - replaces one attribute (=function, var, ...) in a module 17 | - overwrite "module" with the name of the module to apply the patch on 18 | - overwrite "function" with the name of the function go replace 19 | - overwrite "replacement" with the function to install 20 | INFO: due to a bug, the function needs to accept the patch as the first argument! 21 | ModulePatch(): 22 | - replaces or insert the module in "sys.modules" 23 | - overwrite "name" with the name the module has in "sys.modules" 24 | - (OPTIONAL) overwrite "BASEPATH" to the parentdir of the module 25 | DEFAULT: "$STASH_ROOT/lib/mlpatches/modules/" 26 | - overwrite "relpath" with the path of the new module relative to "BASEPATH" 27 | PatchGroup(): 28 | - overwrite "patches" with a list of patches 29 | VariablePatchGroup(patches): 30 | - like PatchGroup(), but patches passed to __init__() 31 | 32 | The recommended way to define a Patch is the following way (example): 33 | 34 | ''' 35 | from mlpatches import base 36 | class MyPatch(base.FunctionPatch): 37 | PY2 = True 38 | PY3 = False 39 | module = "os" 40 | function = "system" 41 | replacement = my_function 42 | 43 | MY_PATCH = MyPatch() 44 | ''' 45 | 46 | Builtin ModulePatches should be defined in the "modulepatches"-submodule of mlpatches. 47 | They need to be registered in "modulepatches.MODULE_PATCHES" dictionary. 48 | This should be done in a hardcoded way. 49 | 50 | All other builtin Patches should be defined and instanciated in their own module. 51 | They should then be imported in the "patches" submodule and registered in the "patches.(IN)STABLE_PATCHES" dictionary. 52 | 53 | You can also add your own patches in "$HOME2/stash_extension/patches/" if you dont want to modify StaSh. 54 | In this case you can define a dictionary named "STABLE_PATCHES" and/or a dictionary named "INSTABLE_PATCHES". 55 | They should map a name to a patch instance. 56 | Extensions are only loaded the first time the "monkeylord"-command is executed. 57 | If you want to reload the extensions, force-quit Pythonista and restart StaSh. 58 | -------------------------------------------------------------------------------- /lib/stashutils/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module defines functions to interact with stash extensions.""" 3 | 4 | import os 5 | import shutil 6 | import io 7 | 8 | from stash.system.shcommon import _STASH_EXTENSION_BIN_PATH as EBP 9 | from stash.system.shcommon import _STASH_EXTENSION_MAN_PATH as EMP 10 | from stash.system.shcommon import _STASH_EXTENSION_FSI_PATH as EFP 11 | from stash.system.shcommon import _STASH_EXTENSION_PATCH_PATH as EPP 12 | 13 | from stashutils.core import load_from_dir 14 | 15 | from six import text_type, binary_type 16 | 17 | # alias load_from_dir (so you can access it trough this namespace) 18 | load_from_dir = load_from_dir 19 | 20 | 21 | def create_file(dest, content): 22 | """ 23 | Creates a file at dest with content. 24 | If content is a string or unicode, use it as the content. 25 | Otherwise, use content.read() as the content. 26 | """ 27 | if not isinstance(content, (binary_type, text_type)): 28 | content = content.read() 29 | parent = os.path.dirname(dest) 30 | if not os.path.exists(parent): 31 | os.makedirs(parent) 32 | if isinstance(content, binary_type): 33 | with io.open(dest, "wb") as f: 34 | f.write(content) 35 | elif isinstance(content, text_type): 36 | with io.open(dest, "w", encoding="utf-8") as f: 37 | f.write(content) 38 | return dest 39 | 40 | 41 | def create_page(name, content): 42 | """ 43 | Creates a manpage with name filled with content. 44 | If content is a list or tuple, instead create a dir and fill it with pages 45 | created from the elements of this list. 46 | The list should consist of tuples of (ending, content) 47 | """ 48 | path = os.path.join(EMP, name) 49 | if isinstance(content, (list, tuple)): 50 | # create a bunch of pages 51 | if os.path.exists(path): 52 | shutil.rmtree(path) 53 | os.mkdir(path) 54 | for n, element in enumerate(content, 1): 55 | ending, elementcontent = element 56 | pagename = "{b}/page_{n}.{e}".format(n=n, e=ending, b=path) 57 | create_page(pagename, elementcontent) 58 | return path 59 | else: 60 | return create_file(path, content) 61 | 62 | 63 | def create_command(name, content): 64 | """creates a script named name filled with content""" 65 | path = os.path.join(EBP, name) 66 | return create_file(path, content) 67 | 68 | 69 | def create_fsi_file(name, content): 70 | """creates a fsi extension named name filled with content""" 71 | path = os.path.join(EFP, name) 72 | return create_file(path, content) 73 | 74 | 75 | def create_patch_file(name, content): 76 | """creates a patch extension named name filled with content""" 77 | path = os.path.join(EPP, name) 78 | return create_file(path, content) 79 | -------------------------------------------------------------------------------- /bin/grep.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Search a regular expression pattern in one or more files""" 3 | 4 | from __future__ import print_function 5 | 6 | import argparse 7 | import collections 8 | import fileinput 9 | import os 10 | import re 11 | import sys 12 | 13 | 14 | def main(args): 15 | global _stash 16 | ap = argparse.ArgumentParser() 17 | ap.add_argument("pattern", help="the pattern to match") 18 | ap.add_argument("files", nargs="*", help="files to be searched") 19 | ap.add_argument( 20 | "-i", "--ignore-case", action="store_true", help="ignore case while searching" 21 | ) 22 | ap.add_argument( 23 | "-v", "--invert", action="store_true", help="invert the search result" 24 | ) 25 | ap.add_argument( 26 | "-c", 27 | "--count", 28 | action="store_true", 29 | help="count the search results instead of normal output", 30 | ) 31 | ns = ap.parse_args(args) 32 | 33 | flags = 0 34 | if ns.ignore_case: 35 | flags |= re.IGNORECASE 36 | 37 | pattern = re.compile(ns.pattern, flags=flags) 38 | 39 | # Do not try to grep directories 40 | files = [f for f in ns.files if not os.path.isdir(f)] 41 | 42 | fileinput.close() # in case it is not closed 43 | try: 44 | counts = collections.defaultdict(int) 45 | for line in fileinput.input(files, openhook=fileinput.hook_encoded("utf-8")): 46 | if bool(pattern.search(line)) != ns.invert: 47 | if ns.count: 48 | counts[fileinput.filename()] += 1 49 | else: 50 | if ns.invert: # optimize: if ns.invert, then no match, so no highlight color needed 51 | newline = line 52 | else: 53 | newline = re.sub( 54 | pattern, lambda m: _stash.text_color(m.group(), "red"), line 55 | ) 56 | if fileinput.isstdin(): 57 | fmt = "{lineno}: {line}" 58 | else: 59 | fmt = "{filename}: {lineno}: {line}" 60 | 61 | print( 62 | fmt.format( 63 | filename=fileinput.filename(), 64 | lineno=fileinput.filelineno(), 65 | line=newline.rstrip(), 66 | ) 67 | ) 68 | 69 | if ns.count: 70 | for filename, count in counts.items(): 71 | fmt = "{count:6} {filename}" 72 | print(fmt.format(filename=filename, count=count)) 73 | 74 | except Exception as err: 75 | print("grep: {}: {!s}".format(type(err).__name__, err), file=sys.stderr) 76 | finally: 77 | fileinput.close() 78 | 79 | 80 | if __name__ == "__main__": 81 | main(sys.argv[1:]) 82 | -------------------------------------------------------------------------------- /tests/misc/test_wget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """tests for the 'wget' command.""" 3 | 4 | import os 5 | import tempfile 6 | 7 | from stash.tests.stashtest import StashTestCase, requires_network 8 | 9 | 10 | class WgetTests(StashTestCase): 11 | """tests for the 'wget' command.""" 12 | 13 | cwd = tempfile.gettempdir() 14 | download_target = "https://github.com/ywangd/stash/archive/master.zip" 15 | invalid_target = "https://github.com/bennr01/stash/archive/does_not_exist.zip" 16 | 17 | def tearDown(self): 18 | """clean up a test.""" 19 | for fn in ("master.zip", "downloaded.zip", "does_not_exist.zip"): 20 | if os.path.exists(fn): 21 | os.remove(fn) 22 | StashTestCase.tearDown(self) 23 | 24 | def test_help(self): 25 | """test 'wget --help'.""" 26 | output = self.run_command("wget --help", exitcode=0) 27 | self.assertIn("wget", output) 28 | self.assertIn("-h", output) 29 | self.assertIn("--help", output) 30 | self.assertIn("url", output) 31 | self.assertIn("-o", output) 32 | self.assertIn("--output-file", output) 33 | 34 | @requires_network 35 | def test_simple(self): 36 | """test 'wget '.""" 37 | self.assertNotIn( 38 | "master.zip", os.listdir(self.cwd) 39 | ) # file should not already exists 40 | output = self.run_command("wget " + self.download_target, exitcode=0) 41 | self.assertIn("Opening: ", output) 42 | self.assertIn("Save as: master.zip", output) 43 | self.assertNotIn("Invalid url: ", output) 44 | self.assertIn("master.zip", os.listdir(self.cwd)) 45 | 46 | @requires_network 47 | def test_outfile(self): 48 | """test 'wget -o '.""" 49 | self.assertNotIn( 50 | "downloaded.zip", os.listdir(self.cwd) 51 | ) # file should not already exists 52 | output = self.run_command( 53 | "wget -o downloaded.zip " + self.download_target, exitcode=0 54 | ) 55 | self.assertIn("Save as: downloaded.zip", output) 56 | self.assertNotIn("Save as: master.zip", output) 57 | self.assertNotIn("Invalid url: ", output) 58 | self.assertIn("downloaded.zip", os.listdir(self.cwd)) 59 | self.assertNotIn("Save as: master.zip", output) 60 | 61 | @requires_network 62 | def test_invalid_url(self): 63 | """test 'wget '.""" 64 | self.assertNotIn( 65 | "does_not_exist.zip", os.listdir(self.cwd) 66 | ) # file should not already exists 67 | output = self.run_command("wget " + self.invalid_target, exitcode=1) 68 | self.assertIn("Opening: ", output) 69 | self.assertNotIn("Save as: does_not_exist.zip", output) 70 | self.assertIn("Invalid url: ", output) 71 | self.assertNotIn("does_not_exist.zip", os.listdir(self.cwd)) 72 | -------------------------------------------------------------------------------- /tests/misc/test_cowsay.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from stash.tests.stashtest import StashTestCase 3 | 4 | 5 | class CowsayTests(StashTestCase): 6 | """tests for cowsay""" 7 | 8 | def test_help(self): 9 | """test help output""" 10 | output = self.run_command("cowsay --help", exitcode=0) 11 | self.assertIn("cowsay", output) 12 | self.assertIn("--help", output) 13 | self.assertIn("usage:", output) 14 | 15 | def test_singleline_1(self): 16 | """test for correct text in output""" 17 | output = self.run_command("cowsay test", exitcode=0) 18 | self.assertIn("test", output) 19 | self.assertNotIn("Hello, World!", output) 20 | self.assertEqual(output.count("<"), 1) 21 | self.assertEqual(output.count(">"), 1) 22 | 23 | def test_singleline_2(self): 24 | """test for correct text in output""" 25 | output = self.run_command("cowsay Hello, World!", exitcode=0) 26 | self.assertIn("Hello, World!", output) 27 | self.assertNotIn("test", output) 28 | self.assertEqual(output.count("<"), 1) 29 | self.assertEqual(output.count(">"), 1) 30 | 31 | def test_stdin_read(self): 32 | """test 'echo test | cowsay' printing 'test'""" 33 | output = self.run_command("echo test | cowsay", exitcode=0) 34 | self.assertIn("test", output) 35 | self.assertNotIn("Hello, World!", output) 36 | 37 | def test_stdin_ignore(self): 38 | """test 'echo test | cowsay Hello, World!' printing 'Hello World!'""" 39 | output = self.run_command("echo test | cowsay Hello, World!", exitcode=0) 40 | self.assertIn("Hello, World!", output) 41 | self.assertNotIn("test", output) 42 | 43 | def test_multiline_1(self): 44 | """test for correct multiline output""" 45 | output = self.run_command("cowsay Hello,\\nWorld!", exitcode=0) 46 | self.assertIn("Hello,", output) 47 | self.assertIn("World!", output) 48 | self.assertNotIn( 49 | "Hello,\nWorld!", output 50 | ) # text should be splitted allong the lines 51 | self.assertIn("/", output) 52 | self.assertIn("\\", output) 53 | self.assertNotIn("<", output) 54 | self.assertNotIn(">", output) 55 | 56 | def test_multiline_2(self): 57 | """test for correct multiline output""" 58 | output = self.run_command("cowsay Hello,\\nWorld!\\nPython4Ever", exitcode=0) 59 | self.assertIn("Hello,", output) 60 | self.assertIn("World!", output) 61 | self.assertIn("Python4Ever", output) 62 | self.assertNotIn( 63 | "Hello,\nWorld!\nPython4Ever", output 64 | ) # text should be splitted allong the lines 65 | self.assertIn("/", output) 66 | self.assertIn("\\", output) 67 | self.assertIn("|", output) 68 | self.assertNotIn("<", output) 69 | self.assertNotIn(">", output) 70 | -------------------------------------------------------------------------------- /bin/wget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Download a file from a url.""" 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import argparse 8 | 9 | from six.moves.urllib.request import urlopen 10 | 11 | try: 12 | import console 13 | except ImportError: 14 | console = None 15 | 16 | _stash = globals()["_stash"] 17 | 18 | 19 | def get_status_string(downloaded, total): 20 | """Return a string showing the current progress""" 21 | if _stash is not None and hasattr(_stash, "libcore"): 22 | hdr = _stash.libcore.sizeof_fmt(downloaded) 23 | else: 24 | hdr = "%10d" % downloaded 25 | if total: 26 | total = float(total) 27 | percent = min((downloaded / total) * 100.0, 100.0) 28 | total_c = 20 29 | nc = int(total_c * (downloaded / total)) 30 | sh = ">" if downloaded != total else "=" 31 | bar = "[" + "=" * (nc - 1) + sh + " " * (total_c - nc) + "]" 32 | # status = r"%10d [%3.2f%%]" % downloaded, percent 33 | status = r"%s %3.2f%% | %s" % (bar, percent, hdr) 34 | else: 35 | status = hdr 36 | return status 37 | 38 | 39 | def main(args): 40 | ap = argparse.ArgumentParser() 41 | ap.add_argument("-o", "--output-file", nargs="?", help="save content as file") 42 | ap.add_argument( 43 | "url", nargs="?", help="the url to read from (default to clipboard)" 44 | ) 45 | 46 | ns = ap.parse_args(args) 47 | url = ns.url or _stash.libdist.clipboard_get() 48 | output_file = ns.output_file or url.split("/")[-1] 49 | 50 | if console is not None: 51 | console.show_activity() 52 | 53 | try: 54 | print("Opening: %s\n" % url) 55 | u = urlopen(url) 56 | 57 | meta = u.info() 58 | try: 59 | if _stash.PY3: 60 | file_size = int(meta["Content-Length"]) 61 | else: 62 | file_size = int(meta.getheaders("Content-Length")[0]) 63 | except (IndexError, ValueError, TypeError): 64 | file_size = 0 65 | 66 | print("Save as: {} ".format(output_file), end="") 67 | print("({} bytes)".format(file_size if file_size else "???")) 68 | 69 | with open(output_file, "wb") as f: 70 | file_size_dl = 0.0 71 | block_sz = 8192 72 | while True: 73 | buf = u.read(block_sz) 74 | if not buf: 75 | break 76 | file_size_dl += len(buf) 77 | f.write(buf) 78 | status = get_status_string(file_size_dl, file_size) 79 | print("\r" + status + " " * 10, end="") 80 | print("") 81 | 82 | except Exception as e: 83 | print("Invalid url: %s" % url) 84 | sys.exit(1) 85 | 86 | finally: 87 | if console is not None: 88 | console.hide_activity() 89 | 90 | sys.exit(0) 91 | 92 | 93 | if __name__ == "__main__": 94 | main(sys.argv[1:]) 95 | -------------------------------------------------------------------------------- /bin/wol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # Based on https://github.com/remcohaszing/pywakeonlan as of 2015-08-13 4 | # Adapted and extended by Georg Viehoever, 2015-08-13 5 | # 6 | # License as in original: 7 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 8 | # Version 2, December 2004 9 | # 10 | # Copyright (C) 2012 Remco Haszing 11 | # 12 | # Everyone is permitted to copy and distribute verbatim or modified 13 | # copies of this license document, and changing it is allowed as long 14 | # as the name is changed. 15 | # 16 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 17 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 18 | # 19 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 20 | """usage: wol.py [-h] [-i ip] [-p port] mac addresses [mac addresses ...] 21 | 22 | Wake one or more computers using the wake on lan protocol. Note that this 23 | requires suitable configuration of the target system, and only works within a 24 | network segment (i.e. not across routers or VPN). 25 | 26 | positional arguments: 27 | mac addresses The mac addresses or of the computers you are trying to wake, 28 | for instance 40:16:7e:ae:af:43 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -i ip The ip address of the host to send the magic packet to. 33 | (default 255.255.255.255) 34 | -p port The port of the host to send the magic packet to (default 9) 35 | """ 36 | 37 | from __future__ import absolute_import 38 | from __future__ import unicode_literals 39 | 40 | import sys 41 | import os 42 | import os.path 43 | 44 | if sys.platform == "win32": 45 | # for tests on Windows 46 | stashLibPath = os.path.join(os.environ["STASH_ROOT"], "lib") 47 | sys.path.insert(0, stashLibPath) 48 | sys.path.remove(os.getcwd()) 49 | 50 | import argparse 51 | from wakeonlan import wol 52 | 53 | parser = argparse.ArgumentParser( 54 | description="""Wake one or more computers using the wake on lan protocol. 55 | Note that this requires suitable configuration of the target system, and only 56 | works within a network segment (i.e. not across routers or VPN).""" 57 | ) 58 | parser.add_argument( 59 | "macs", 60 | metavar="mac addresses", 61 | nargs="+", 62 | help="The mac addresses or of the computers you are trying to wake, for instance 40:16:7e:ae:af:43", 63 | ) 64 | parser.add_argument( 65 | "-i", 66 | metavar="ip", 67 | default=wol.BROADCAST_IP, 68 | help="The ip address of the host to send the magic packet to. (default {})".format( 69 | wol.BROADCAST_IP 70 | ), 71 | ) 72 | parser.add_argument( 73 | "-p", 74 | metavar="port", 75 | default=wol.DEFAULT_PORT, 76 | help="The port of the host to send the magic packet to (default 9)", 77 | ) 78 | args = parser.parse_args() 79 | wol.send_magic_packet(*args.macs, ip_address=args.i, port=args.p) 80 | -------------------------------------------------------------------------------- /lib/libcore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import fileinput 4 | 5 | try: 6 | unicode 7 | except NameError: 8 | unicode = str 9 | 10 | 11 | def collapseuser(path): 12 | """Reverse of os.path.expanduser: return path relative to ~, if 13 | such representation is meaningful. If path is not ~ or a 14 | subdirectory, the absolute path will be returned. 15 | """ 16 | path = os.path.abspath(unicode(path)) 17 | home = os.path.expanduser("~") 18 | if os.path.exists(os.path.expanduser("~/Pythonista.app")): 19 | althome = os.path.dirname( 20 | os.path.realpath(os.path.expanduser("~/Pythonista.app")) 21 | ) 22 | else: 23 | althome = home 24 | 25 | if path.startswith(home): 26 | collapsed = os.path.relpath(path, home) 27 | elif path.startswith(althome): 28 | collapsed = os.path.relpath(path, althome) 29 | else: 30 | collapsed = path 31 | 32 | return "~" if collapsed == "." else os.path.join("~", collapsed) 33 | 34 | 35 | def get_lan_ip(): 36 | try: 37 | from objc_util import ObjCClass 38 | 39 | NSHost = ObjCClass("NSHost") 40 | addresses = [] 41 | for address in NSHost.currentHost().addresses(): 42 | address = str(address) 43 | if 48 <= ord(address[0]) <= 57 and address != "127.0.0.1": 44 | addresses.append(address) 45 | return " ".join(addresses) 46 | 47 | except ImportError: 48 | return "" 49 | 50 | 51 | def input_stream(files=()): 52 | """Handles input files similar to fileinput. 53 | The advantage of this function is it recovers from errors if one 54 | file is invalid and proceed with the next file 55 | """ 56 | fileinput.close() 57 | try: 58 | if not files: 59 | for line in fileinput.input(files): 60 | yield line, "", fileinput.filelineno() 61 | 62 | else: 63 | while files: 64 | thefile = files.pop(0) 65 | try: 66 | for line in fileinput.input(thefile): 67 | yield line, fileinput.filename(), fileinput.filelineno() 68 | except IOError as e: 69 | yield None, fileinput.filename(), e 70 | finally: 71 | fileinput.close() 72 | 73 | 74 | def sizeof_fmt(num): 75 | """ 76 | Return a human readable string describing the size of something. 77 | :param num: the number in machine-readble form 78 | :type num: int 79 | :param base: base of each unit (e.g. 1024 for KiB -> MiB) 80 | :type base: int 81 | :param suffix: suffix to add. By default, the string returned by sizeof_fmt() does not contain a suffix other than 'K', 'M', ... 82 | :type suffix: str 83 | """ 84 | for unit in ["B", "KiB", "MiB", "GiB"]: 85 | if num < 1024: 86 | return "%3.1f%s" % (num, unit) 87 | num /= 1024.0 88 | return "%3.1f%s" % (num, "Ti") 89 | -------------------------------------------------------------------------------- /tests/system/test_runtime.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from stash.tests.stashtest import StashTestCase 4 | 5 | 6 | class RuntimeTests(StashTestCase): 7 | setup_commands = ["BIN_PATH=$STASH_ROOT/tests/system/data:$BIN_PATH"] 8 | 9 | def test_03(self): 10 | cmp_str = r"""[stash]$ x y 11 | A is{0} 12 | A is 8 13 | bin 14 | [stash]$ """.format(" ") 15 | self.do_test("test03.sh x y", cmp_str, ensure_undefined=("A",)) 16 | 17 | def test_05(self): 18 | cmp_str = r"""[stash]$ AA is{0} 19 | AA is Hello 20 | stash 21 | bin 22 | 23 | 1 24 | B is{0} 25 | B is 89 26 | bin 27 | 2 28 | B is{0} 29 | stash 30 | [stash]$ """.format(" ") 31 | self.do_test("test05.py", cmp_str, ensure_undefined=("AA", "B")) 32 | 33 | def test_06(self): 34 | cmp_str = r"""[stash]$ AA is{0} 35 | --- direct execution without sourcing --- 36 | From tobesourced AA is sourced 37 | copy=pbcopy 38 | env=printenv 39 | help=man 40 | l1=ls -1 41 | la=ls -a 42 | ll=ls -la 43 | paste=pbpaste 44 | unmount=umount 45 | 46 | AA is{0} 47 | copy=pbcopy 48 | env=printenv 49 | help=man 50 | la=ls -a 51 | ll=ls -la 52 | paste=pbpaste 53 | unmount=umount 54 | 55 | 56 | --- source the file --- 57 | From tobesourced AA is sourced 58 | copy=pbcopy 59 | env=printenv 60 | help=man 61 | l1=ls -1 62 | la=ls -a 63 | ll=ls -la 64 | paste=pbpaste 65 | unmount=umount 66 | 67 | AA is sourced 68 | copy=pbcopy 69 | env=printenv 70 | help=man 71 | l1=ls -1 72 | la=ls -a 73 | ll=ls -la 74 | paste=pbpaste 75 | unmount=umount 76 | 77 | [stash]$ """.format(" ") 78 | self.do_test("test06.sh", cmp_str, ensure_undefined=("A",)) 79 | 80 | def test_07(self): 81 | cmp_str = r"""[stash]$ A is 999 82 | A is{0} 83 | [stash]$ """.format(" ") 84 | self.do_test("test07.sh", cmp_str, ensure_undefined=("A",)) 85 | 86 | def test_08(self): 87 | cmp_str = r"""[stash]$ A is{0} 88 | [stash]$ """.format(" ") 89 | self.do_test("test08.sh", cmp_str, ensure_undefined=("A",)) 90 | 91 | def test_09(self): 92 | cmp_str = r"""[stash]$ A is{0} 93 | [stash]$ """.format(" ") 94 | self.do_test("test09.sh", cmp_str, ensure_undefined=("A",)) 95 | 96 | def test_10(self): 97 | cmp_str = r"""[stash]$ 1: #!/bin/bash 98 | [stash]$ """ 99 | self.do_test("test10.sh", cmp_str) 100 | 101 | def test_11(self): 102 | cmp_str = r"""[stash]$ A is 42 103 | [stash]$ """ 104 | self.do_test("test11.sh", cmp_str, ensure_undefined=("A",)) 105 | 106 | def test_12(self): 107 | """ 108 | Directory changes in script via callable interface should not 109 | affect parent shell but is persistent for any following calls 110 | from the same parent shell. 111 | """ 112 | cmp_str = r"""[stash]$ parent script stash 113 | parent script stash 114 | from child script 2 bin 115 | parent script stash 116 | [stash]$ """ 117 | self.do_test("test_12.py", cmp_str) 118 | -------------------------------------------------------------------------------- /bin/sha1sum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Get sha1 hash of a file or string. 4 | 5 | usage: sha1sum.py [-h] [-c] [file [file ...]] 6 | 7 | positional arguments: 8 | file String or file to hash. 9 | 10 | optional arguments: 11 | -h, --help show this help message and exit 12 | -c, --check Check a file with sha1 hashes and file names for a match. 13 | format: 14 | sha1_hash filename 15 | sha1_hash filename 16 | etc. 17 | """ 18 | 19 | from __future__ import print_function 20 | 21 | import argparse 22 | import os 23 | import re 24 | import sys 25 | 26 | import six 27 | 28 | from Crypto.Hash import SHA 29 | 30 | 31 | def get_hash(fileobj): 32 | h = SHA.new() 33 | chunk_size = 8192 34 | while True: 35 | chunk = fileobj.read(chunk_size) 36 | if len(chunk) == 0: 37 | break 38 | h.update(chunk) 39 | return h.hexdigest() 40 | 41 | 42 | def check_list(fileobj): 43 | correct = True 44 | for line in fileobj: 45 | match = re.match(r"(\w+)[ \t]+(.+)", line) 46 | try: 47 | with open(match.group(2), "rb") as f1: 48 | if match.group(1) == get_hash(f1): 49 | print(match.group(2) + ": Pass") 50 | else: 51 | print(match.group(2) + ": Fail") 52 | correct = False 53 | except Exception: 54 | print("Invalid format.") 55 | correct = False 56 | return correct 57 | 58 | 59 | def make_file(txt): 60 | f = six.BytesIO() 61 | if isinstance(txt, six.binary_type): 62 | f.write(txt) 63 | else: 64 | f.write(txt.encode("utf-8")) 65 | f.seek(0) 66 | return f 67 | 68 | 69 | ap = argparse.ArgumentParser() 70 | ap.add_argument( 71 | "-c", 72 | "--check", 73 | action="store_true", 74 | default=False, 75 | help="""Check a file with sha1 hashes and file names for a match. format: hash filename""", 76 | ) 77 | ap.add_argument("file", action="store", nargs="*", help="String or file to hash.") 78 | args = ap.parse_args(sys.argv[1:]) 79 | 80 | if args.check: 81 | if args.file: 82 | s = True 83 | for arg in args.file: 84 | if os.path.isfile(arg): 85 | s = s and check_list(open(arg)) 86 | else: 87 | s = check_list(make_file(sys.stdin.read())) 88 | if s: 89 | sys.exit(0) 90 | else: 91 | sys.exit(1) 92 | 93 | else: 94 | if args.file: 95 | for arg in args.file: 96 | if os.path.isfile(arg): 97 | with open(arg, "rb") as f: 98 | print(get_hash(f) + " " + arg) 99 | elif arg == "-": 100 | print(get_hash(make_file(sys.stdin.read()))) 101 | else: 102 | print(get_hash(make_file(arg))) 103 | else: 104 | print(get_hash(make_file(sys.stdin.read()))) 105 | --------------------------------------------------------------------------------