From 4ee91e60fd4410d6e57485b10c1458a49e4baa0f Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 3 Oct 2020 18:20:25 +0300 Subject: [PATCH 001/116] Get rid of unnecessary bashisms. --- share/cht.sh.txt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 5227f47e..43090bac 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # shellcheck disable=SC1117,SC2001 # # [X] open section @@ -122,8 +122,8 @@ EOF local _exit_code=0 - local dependencies=(python git virtualenv) - for dep in "${dependencies[@]}"; do + local dependencies="python git virtualenv" + for dep in $dependencies; do command -v "$dep" >/dev/null || \ { echo "DEPENDENCY: \"$dep\" is needed to install cheat.sh in the standalone mode" >&2; _exit_code=1; } done @@ -191,15 +191,15 @@ EOF if [[ $PYTHON2 = YES ]]; then python="python2" pip="pip" - virtualenv_python3_option=() + virtualenv_python3_options= else python="python3" pip="pip3" - virtualenv_python3_option=(-p python3) + virtualenv_python3_options="-p python3" fi _say_what_i_do Creating virtual environment - "$python" "$(command -v virtualenv)" "${virtualenv_python3_option[@]}" ve \ + "$python" "$(command -v virtualenv)" $virtualenv_python3_options ve \ || fatal Could not create virtual environment with "python2 $(command -v virtualenv) ve" export CHEATSH_PATH_WORKDIR=$PWD @@ -712,8 +712,7 @@ cmd_update() { if ! cmp "$0" "$TMP2" > /dev/null 2>&1; then if grep -q ^__CHTSH_VERSION= "$TMP2"; then # section was vaildated by us already - args=(--shell "$section") - cp "$TMP2" "$0" && echo "Updated. Restarting..." && rm "$TMP2" && CHEATSH_RESTART=1 exec "$0" "${args[@]}" + cp "$TMP2" "$0" && echo "Updated. Restarting..." && rm "$TMP2" && CHEATSH_RESTART=1 exec "$0" --shell "$section" else echo "Something went wrong. Please update manually" fi From de8bea610fcdc865533ff38d502e117eb80ab20e Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 3 Oct 2020 18:21:14 +0300 Subject: [PATCH 002/116] Unbreak passing arguments to ftp(1)/curl(1) --- share/cht.sh.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 43090bac..524a5c6c 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -422,14 +422,14 @@ elif [ "$(uname -s)" = OpenBSD ] && [ -x /usr/bin/ftp ]; then esac done shift $((OPTIND - 1)) - /usr/bin/ftp "$args" "$@" + /usr/bin/ftp $args "$@" } else command -v curl >/dev/null || { echo 'DEPENDENCY: install "curl" to use cht.sh' >&2; exit 1; } _CURL=$(command -v curl) if [ x"$CHTSH_CURL_OPTIONS" != x ]; then curl() { - $_CURL "${CHTSH_CURL_OPTIONS}" "$@" + $_CURL ${CHTSH_CURL_OPTIONS} "$@" } fi fi From e840c3967f7cba102e012cbbb0d76e53ddac8e19 Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 3 Oct 2020 19:04:03 +0300 Subject: [PATCH 003/116] One more invalid arguments expansion. --- share/cht.sh.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 524a5c6c..31f81d27 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -340,7 +340,7 @@ do_query() b_opts="-b \"\$HOME/.cht.sh/id\"" fi - eval curl "$b_opts" -s "$uri" > "$TMP1" + eval curl $b_opts -s "$uri" > "$TMP1" if [ -z "$lines" ] || [ "$(wc -l "$TMP1" | awk '{print $1}')" -lt "$lines" ]; then cat "$TMP1" From 23c7e84bffca3cc0d4d3231df9f2c3c596e174fd Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 3 Oct 2020 19:04:27 +0300 Subject: [PATCH 004/116] Reset OPTIND before running getopts, since it may happen more than once. --- share/cht.sh.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 31f81d27..40982285 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -402,6 +402,7 @@ if [ "$CHTSH_MODE" = auto ] && [ -d "$CHEATSH_INSTALLATION" ]; then # ignoring all options # currently the standalone.py does not support them anyway local opt + OPTIND=1 while getopts "b:s" opt; do : done @@ -414,6 +415,7 @@ elif [ "$(uname -s)" = OpenBSD ] && [ -x /usr/bin/ftp ]; then # any better test not involving either OS matching or actual query? curl() { local opt args="-o -" + OPTIND=1 while getopts "b:s" opt; do case $opt in b) args="$args -c $OPTARG";; From fbe85feddc7812d3e26e5189936d14a80d823056 Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 3 Oct 2020 19:18:32 +0300 Subject: [PATCH 005/116] Make "local" more portable --- share/cht.sh.txt | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 40982285..6ee7fb58 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -55,8 +55,12 @@ esac # for KSH93 # shellcheck disable=SC2034,SC2039,SC2168 -if echo "$KSH_VERSION" | grep -q ' 93' && ! local foo 2>/dev/null; then - alias local=typeset +if ! local foo 2>/dev/null; then + if typeset foo 2>/dev/null; then + alias local=typeset + else + alias local=eval # XXX avoid "local foo", use "local foo=" instead + fi fi fatal() @@ -77,7 +81,7 @@ cheatsh_standalone_install() { # the function installs cheat.sh with the upstream repositories # in the standalone mode - local installdir; installdir="$1" + local installdir="$1" local default_installdir="$HOME/.cheat.sh" [ -z "$installdir" ] && installdir=${default_installdir} @@ -130,7 +134,7 @@ EOF [ "$_exit_code" -ne 0 ] && return "$_exit_code" while true; do - local _installdir + local _installdir= echo -n "Where should cheat.sh be installed [$installdir]? "; read -r _installdir [ -n "$_installdir" ] && installdir=$_installdir @@ -158,7 +162,7 @@ EOF fi local space_needed=700 - local space_available; space_available=$(($(df -k "$installdir" | awk '{print $4}' | tail -1)/1024)) + local space_available=$(($(df -k "$installdir" | awk '{print $4}' | tail -1)/1024)) if [ "$space_available" -lt "$space_needed" ]; then echo "ERROR: Installation directory has no enough space (needed: ${space_needed}M, available: ${space_available}M" @@ -250,8 +254,8 @@ EOF _say_what_i_do Done - local v1; v1=$(printf "\033[0;1;32m") - local v2; v2=$(printf "\033[0m") + local v1=$(printf "\033[0;1;32m") + local v2=$(printf "\033[0m") cat < Date: Sat, 3 Oct 2020 19:38:35 +0300 Subject: [PATCH 006/116] more shell compatibility goo --- share/cht.sh.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 6ee7fb58..2a17a190 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -58,10 +58,18 @@ esac if ! local foo 2>/dev/null; then if typeset foo 2>/dev/null; then alias local=typeset + elif declare foo 2>/dev/null; then + alias local=declare else alias local=eval # XXX avoid "local foo", use "local foo=" instead fi fi +unset foo + +# for zsh +if [ -n "$ZSH_NAME" ]; then + set -o shwordsplit +fi fatal() { From c14e8d7ccf9d11f334acca24c8494e7994c7499c Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Mon, 5 Oct 2020 22:09:33 +0300 Subject: [PATCH 007/116] drop --color=always, it doesn't exist outside GNU world --- tests/run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index e89d14fe..13f5f566 100644 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -85,7 +85,7 @@ while read -r number test_line; do eval "curl -s $CHTSH_URL/$test_line" > "$TMP" fi - if ! diff -u3 --color=always results/"$number" "$TMP" > "$TMP2"; then + if ! diff -u3 results/"$number" "$TMP" > "$TMP2"; then if [[ $update_tests_results = NO ]]; then if [ "$show_details" = YES ]; then cat "$TMP2" From 2b3dc33c65f620cabf046ce71b1401ba20b8ab40 Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Thu, 8 Oct 2020 00:42:35 +0300 Subject: [PATCH 008/116] Initial version of CI --- .github/workflows/tests-ubuntu.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/tests-ubuntu.yml diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml new file mode 100644 index 00000000..109b600e --- /dev/null +++ b/.github/workflows/tests-ubuntu.yml @@ -0,0 +1,19 @@ +name: Ubuntu Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: install dependencies + run: pip3 install -r requirements.txt + - name: run tests + run: bash tests/run-tests.sh From 4c68eaf3ebf9f0a2da08b2b2be0797a9fda7cc20 Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Thu, 8 Oct 2020 00:59:34 +0300 Subject: [PATCH 009/116] Minor simplification while here. --- tests/run-tests.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 13f5f566..8e3c7941 100644 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -109,8 +109,4 @@ else echo TESTS/OK/UPDATED "$i/$((i-failed))/$failed" fi -if [ "$failed" != 0 ]; then - exit 1 -else - exit 0 -fi +test $failed -eq 0 From c601c61ebd0ce29202401d22533b5e6b4c5aea9f Mon Sep 17 00:00:00 2001 From: WANDEX Date: Sat, 15 Aug 2020 11:31:47 +0300 Subject: [PATCH 010/116] fix: replaced hardcoded paths '$HOME/.cht.sh' on $CHTSH_HOME, to properly handle CHTSH environment variable, which override default CHTSH_HOME path --- share/cht.sh.txt | 18 +++++++++--------- tests/results/8 | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/share/cht.sh.txt b/share/cht.sh.txt index 2a17a190..b06a569d 100755 --- a/share/cht.sh.txt +++ b/share/cht.sh.txt @@ -29,7 +29,7 @@ __CHTSH_DATETIME="2020-08-05 09:30:30 +0200" # cht.sh configuration loading # -# configuration is stored in ~/.cht.sh/ (can be overridden by CHTSH_HOME) +# configuration is stored in ~/.cht.sh/ (can be overridden with CHTSH env var.) # CHTSH_HOME=${CHTSH:-"$HOME"/.cht.sh} [ -z "$CHTSH_CONF" ] && CHTSH_CONF=$CHTSH_HOME/cht.sh.conf @@ -348,8 +348,8 @@ do_query() local b_opts= local uri="${CHTSH_URL}/\"\$(get_query_options $query)\"" - if [ -e "$HOME/.cht.sh/id" ]; then - b_opts="-b \"\$HOME/.cht.sh/id\"" + if [ -e "$CHTSH_HOME/id" ]; then + b_opts="-b \"\$CHTSH_HOME/id\"" fi eval curl $b_opts -s "$uri" > "$TMP1" @@ -532,7 +532,7 @@ if [ "$is_macos" != yes ]; then fi command -v rlwrap >/dev/null || { echo 'DEPENDENCY: install "rlwrap" to use cht.sh in the shell mode' >&2; exit 1; } -mkdir -p "$HOME/.cht.sh/" +mkdir -p "$CHTSH_HOME/" lines=$(tput lines) if command -v less >/dev/null; then @@ -625,11 +625,11 @@ EOF } cmd_hush() { - mkdir -p "$HOME/.cht.sh/" && touch "$HOME/.cht.sh/.hushlogin" && echo "Initial 'use help' message was disabled" + mkdir -p "$CHTSH_HOME/" && touch "$CHTSH_HOME/.hushlogin" && echo "Initial 'use help' message was disabled" } cmd_id() { - id_file="$HOME/.cht.sh/id" + id_file="$CHTSH_HOME/id" if [ id = "$input" ]; then new_id="" @@ -754,7 +754,7 @@ TMP1=$(mktemp /tmp/cht.sh.XXXXXXXXXXXXX) trap 'rm -f $TMP1 $TMP2' EXIT trap 'true' INT -if ! [ -e "$HOME/.cht.sh/.hushlogin" ] && [ -z "$this_query" ]; then +if ! [ -e "$CHTSH_HOME/.hushlogin" ] && [ -z "$this_query" ]; then echo "type 'help' for the cht.sh shell help" fi @@ -766,7 +766,7 @@ while true; do fi input=$( - rlwrap -H "$HOME/.cht.sh/history" -pgreen -C cht.sh -S "$full_prompt" bash "$0" --read | sed 's/ *#.*//' + rlwrap -H "$CHTSH_HOME/history" -pgreen -C cht.sh -S "$full_prompt" bash "$0" --read | sed 's/ *#.*//' ) cmd_name=${input%% *} @@ -785,5 +785,5 @@ while true; do version) cmd_name=version;; *) cmd_name="query"; cmd_args="$input";; esac - "cmd_$cmd_name" $cmd_args + "cmd_$cmd_name" $cmd_args done diff --git a/tests/results/8 b/tests/results/8 index 5227f47e..bebc1af1 100644 --- a/tests/results/8 +++ b/tests/results/8 @@ -29,7 +29,7 @@ __CHTSH_DATETIME="2020-08-05 09:30:30 +0200" # cht.sh configuration loading # -# configuration is stored in ~/.cht.sh/ (can be overridden by CHTSH_HOME) +# configuration is stored in ~/.cht.sh/ (can be overridden with CHTSH env var.) # CHTSH_HOME=${CHTSH:-"$HOME"/.cht.sh} [ -z "$CHTSH_CONF" ] && CHTSH_CONF=$CHTSH_HOME/cht.sh.conf @@ -336,8 +336,8 @@ do_query() local b_opts= local uri="${CHTSH_URL}/\"\$(get_query_options $query)\"" - if [ -e "$HOME/.cht.sh/id" ]; then - b_opts="-b \"\$HOME/.cht.sh/id\"" + if [ -e "$CHTSH_HOME/id" ]; then + b_opts="-b \"\$CHTSH_HOME/id\"" fi eval curl "$b_opts" -s "$uri" > "$TMP1" @@ -518,7 +518,7 @@ if [ "$is_macos" != yes ]; then fi command -v rlwrap >/dev/null || { echo 'DEPENDENCY: install "rlwrap" to use cht.sh in the shell mode' >&2; exit 1; } -mkdir -p "$HOME/.cht.sh/" +mkdir -p "$CHTSH_HOME/" lines=$(tput lines) if command -v less >/dev/null; then @@ -611,11 +611,11 @@ EOF } cmd_hush() { - mkdir -p "$HOME/.cht.sh/" && touch "$HOME/.cht.sh/.hushlogin" && echo "Initial 'use help' message was disabled" + mkdir -p "$CHTSH_HOME/" && touch "$CHTSH_HOME/.hushlogin" && echo "Initial 'use help' message was disabled" } cmd_id() { - id_file="$HOME/.cht.sh/id" + id_file="$CHTSH_HOME/id" if [ id = "$input" ]; then new_id="" @@ -741,7 +741,7 @@ TMP1=$(mktemp /tmp/cht.sh.XXXXXXXXXXXXX) trap 'rm -f $TMP1 $TMP2' EXIT trap 'true' INT -if ! [ -e "$HOME/.cht.sh/.hushlogin" ] && [ -z "$this_query" ]; then +if ! [ -e "$CHTSH_HOME/.hushlogin" ] && [ -z "$this_query" ]; then echo "type 'help' for the cht.sh shell help" fi @@ -753,7 +753,7 @@ while true; do fi input=$( - rlwrap -H "$HOME/.cht.sh/history" -pgreen -C cht.sh -S "$full_prompt" bash "$0" --read | sed 's/ *#.*//' + rlwrap -H "$CHTSH_HOME/history" -pgreen -C cht.sh -S "$full_prompt" bash "$0" --read | sed 's/ *#.*//' ) cmd_name=${input%% *} @@ -772,5 +772,5 @@ while true; do version) cmd_name=version;; *) cmd_name="query"; cmd_args="$input";; esac - "cmd_$cmd_name" $cmd_args + "cmd_$cmd_name" $cmd_args done From 9283d1759730e8cb4893d8af183d893552f7cc23 Mon Sep 17 00:00:00 2001 From: Alexander Gonzalez Date: Fri, 30 Oct 2020 18:25:54 -0400 Subject: [PATCH 011/116] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0f861ac..f7edf81c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Such a thing exists. * Ultrafast, returns answers within 100 ms, as a rule. * Has a convenient command line client, `cht.sh`, that is very advantageous and helpful, though not mandatory. * Can be used directly from code editors, without opening a browser and not switching your mental context. -* Supports a special stealth mode where it can be used fully invisibly without ever touching a key and and making sounds. +* Supports a special stealth mode where it can be used fully invisibly without ever touching a key and making sounds.

From 5714240f947021d24ca1e8832c94026dbfc85a3a Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 4 Nov 2020 12:51:01 +0300 Subject: [PATCH 012/116] .travis.yml Use `curl` with connection timeout This should fix current build failure with timeout https://travis-ci.org/github/chubin/cheat.sh/builds/741308362#L402 --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f64a78ee..21b10e21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,7 @@ before_install: - docker-compose ps script: - - sleep 3 - - curl http://localhost:8002 + - curl --connect-timeout 10 http://localhost:8002 - docker-compose logs --no-color - docker logs chtsh - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From fa8ad4d4a3fb7fb7a6e668a252a173476141a6c5 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 4 Nov 2020 15:14:11 +0300 Subject: [PATCH 013/116] Use `curl` retry flags for waiting for service `--connect-timeout` didn't work, because Docker allows it, but breaks, because nothing is listening on the other side, giving this error. $ curl --connect-timeout 10 http://localhost:8002 curl: (56) Recv failure: Connection reset by peer The command "curl --connect-timeout 10 http://localhost:8002" exited with 56. https://travis-ci.org/github/chubin/cheat.sh/builds/741348053#L401 `--retry-all` makes `curl` retry on all errors, including connection resets. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 21b10e21..88a88054 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_install: - docker-compose ps script: - - curl --connect-timeout 10 http://localhost:8002 + - curl --retry 3 --retry-all http://localhost:8002 - docker-compose logs --no-color - docker logs chtsh - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From 77b436943bdb95a9951441f2d8bfe07548935122 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 4 Nov 2020 16:12:59 +0300 Subject: [PATCH 014/116] Try newer Ubuntu with newer `curl` --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88a88054..bb556c65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: bionic +dist: focal language: - generic @@ -10,7 +10,7 @@ before_install: - docker-compose ps script: - - curl --retry 3 --retry-all http://localhost:8002 + - curl --retry 3 --retry-all-errors http://localhost:8002 - docker-compose logs --no-color - docker logs chtsh - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From 14d6ef0abf410554450f0284bc3127175ac835b2 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 4 Nov 2020 19:33:11 +0300 Subject: [PATCH 015/116] Use `wget` instead of `curl` to wait for container Because even Ubuntu 20.04 version of `curl` is outdated --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb556c65..d3357e46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_install: - docker-compose ps script: - - curl --retry 3 --retry-all-errors http://localhost:8002 + - wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http - docker-compose logs --no-color - docker logs chtsh - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From 4bc39db0542d57653282528c5eec5e554f6ad57d Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 4 Nov 2020 07:48:54 +0300 Subject: [PATCH 016/116] Use `flask run` server for debugging Because at least live reloading doesn't seem to work with `gevent` --- bin/srv.py | 24 +++++++++++++++--------- docker-compose.debug.yml | 21 +++++++++++++++++---- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/bin/srv.py b/bin/srv.py index 52aaff11..fef3ea36 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -275,12 +275,18 @@ def answer(topic=None): return Response(result, mimetype='text/plain') -if '--debug' in sys.argv: - app.debug = True -if 'CHEATSH_PORT' in os.environ: - PORT = int(os.environ.get('CHEATSH_PORT')) -else: - PORT = CONFIG['server.port'] -SRV = WSGIServer((CONFIG['server.bind'], PORT), app) # log=None) -print("Starting server on {}:{}".format(SRV.address[0], SRV.address[1])) -SRV.serve_forever() +if __name__ == '__main__': + # Serving cheat.sh with `gevent` + if '--debug' in sys.argv: + # Not all debug mode features are available under `gevent` + # https://github.com/pallets/flask/issues/3825 + app.debug = True + + if 'CHEATSH_PORT' in os.environ: + PORT = int(os.environ.get('CHEATSH_PORT')) + else: + PORT = CONFIG['server.port'] + + SRV = WSGIServer((CONFIG['server.bind'], PORT), app) # log=None) + print("Starting gevent server on {}:{}".format(SRV.address[0], SRV.address[1])) + SRV.serve_forever() diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index 56c5e9c9..a4107e3f 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -1,8 +1,21 @@ -# Compose override to add --debug option to bin/srv.py -# call to print tracebacks on errors to stdout. +# Compose override, see https://docs.docker.com/compose/extends/ +# +# - Run `flask` standalone server with more debug aids instead of `gevent` +# - Turn on Flask debug mode to print tracebacks and autoreload code +# - Mounts fresh sources from current dir as volume +# +# Usage: +# docker-compose -f docker-compose.yml -f docker-compose.debug.yml up # -# See https://docs.docker.com/compose/extends/ version: '2' services: app: - command: "--debug" + environment: + FLASK_ENV: development + #FLASK_RUN_RELOAD: False + FLASK_APP: "bin/srv.py" + FLASK_RUN_HOST: 0.0.0.0 + FLASK_RUN_PORT: 8002 + entrypoint: ["/usr/bin/flask", "run"] + volumes: + - .:/app:Z From 6b8aeaf711484959d0176fca4c8b2224694475d8 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 12:48:02 +0300 Subject: [PATCH 017/116] Test "production" container on Travis --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d3357e46..48b1c6ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,12 @@ language: before_install: - docker-compose build - docker images - - docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d + - docker-compose -f docker-compose.yml up -d + #- docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d - docker-compose ps script: + # wait for the server to load - wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http - docker-compose logs --no-color - - docker logs chtsh - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From 4401bc3649736d3f37aeca9d6a69b85fbec972a7 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 13:06:20 +0300 Subject: [PATCH 018/116] Move all server init into `before_install:` section --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48b1c6ae..70d308e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ before_install: - docker-compose -f docker-compose.yml up -d #- docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d - docker-compose ps - -script: - # wait for the server to load + # wait until the web server is up - wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http - docker-compose logs --no-color + +script: - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh From 803b8e1e6965407d092cd14756cf1bb4bb954aa9 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 13:08:04 +0300 Subject: [PATCH 019/116] Run GitHub tests on Ubuntu 20.04 --- .github/workflows/tests-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 109b600e..99947a2b 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 From cd6e386e47f9adad9308cd2ccd3fca1ee5b5edbe Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 12 Nov 2020 12:42:42 +0100 Subject: [PATCH 020/116] Create logdir for fetch --- lib/fetch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/fetch.py b/lib/fetch.py index 75a03042..01d249a6 100644 --- a/lib/fetch.py +++ b/lib/fetch.py @@ -200,6 +200,10 @@ def main(args): _show_usage() sys.exit(0) + logdir = os.path.dirname(CONFIG["path.log.fetch"]) + if not os.path.exists(logdir): + os.makedirs() + logging.basicConfig( filename=CONFIG["path.log.fetch"], level=logging.DEBUG, From 286ae8e13b61900fc98ee608faa1b0dfd76d1150 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 12 Nov 2020 12:55:11 +0100 Subject: [PATCH 021/116] Update lib/fetch.py Co-authored-by: Anatoli Babenia --- lib/fetch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fetch.py b/lib/fetch.py index 01d249a6..7d57fd32 100644 --- a/lib/fetch.py +++ b/lib/fetch.py @@ -202,7 +202,7 @@ def main(args): logdir = os.path.dirname(CONFIG["path.log.fetch"]) if not os.path.exists(logdir): - os.makedirs() + os.makedirs(logdir) logging.basicConfig( filename=CONFIG["path.log.fetch"], From 6309536a6a720b27b5b416f33cd6e5dad8bd6972 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 12 Nov 2020 20:18:53 +0000 Subject: [PATCH 022/116] Update tests/results/15 (python/:learn) --- tests/results/15 | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/results/15 b/tests/results/15 index 78b15dc2..6e6a66d1 100644 --- a/tests/results/15 +++ b/tests/results/15 @@ -28,17 +28,19 @@ 10.0 / 3 # => 3.3333333333333335 # Modulo operation -7 % 3 # => 1 +7 % 3 # => 1 +# i % j have the same sign as j, unlike C +-7 % 3 # => 2 # Exponentiation (x**y, x to the yth power) 2**3 # => 8 # Enforce precedence with parentheses -1 + 3 * 2 # => 7 +1 + 3 * 2 # => 7 (1 + 3) * 2 # => 8 # Boolean values are primitives (Note: the capitalization) -True # => True +True # => True False # => False # negate with not @@ -104,7 +106,7 @@ "This is a string." 'This is also a string.' -# Strings can be added too! But try not to do this. +# Strings can be added too "Hello " + "world!" # => "Hello world!" # String literals (but not variables) can be concatenated without using '+' "Hello " "world!" # => "Hello world!" @@ -118,10 +120,9 @@ # You can also format using f-strings or formatted string literals (in Python 3.6+) name = "Reiko" f"She said her name is {name}." # => "She said her name is Reiko" -# You can basically put any Python statement inside the braces and it will be output in the string. +# You can basically put any Python expression inside the braces and it will be output in the string. f"{name} is {len(name)} characters long." # => "Reiko is 5 characters long." - # None is an object None # => None @@ -151,7 +152,6 @@ # Simple way to get input data from console input_string_var = input("Enter some data: ") # Returns the data as a string -# Note: In earlier versions of Python, input() method was named as raw_input() # There are no declarations, only assignments. # Convention is to use lower_case_with_underscores @@ -484,7 +484,7 @@ with open('myfile2.txt', "r+") as file:  contents = json.load(file) # reads a json object from a file -print(contents)  +print(contents) # print: {"aa": 12, "bb": 21} @@ -751,9 +751,8 @@  # Call the static method  print(Human.grunt()) # => "*grunt*" - # Cannot call static method with instance of object - # because i.grunt() will automatically put "self" (the object i) as an argument - print(i.grunt()) # => TypeError: grunt() takes 0 positional arguments but 1 was given + # Static methods can be called by instances too + print(i.grunt()) # => "*grunt*"  # Update the property for this instance  i.age = 42 @@ -898,7 +897,7 @@  def __init__(self, *args, **kwargs):  # Typically to inherit attributes you have to call super: - # super(Batman, self).__init__(*args, **kwargs)  + # super(Batman, self).__init__(*args, **kwargs)  # However we are dealing with multiple inheritance here, and super()  # only works with the next base class in the MRO list.  # So instead we explicitly call __init__ for all ancestors. From 45e7ce027af10436b1b6af1a8560f715fae505d8 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 12 Nov 2020 21:48:11 +0000 Subject: [PATCH 023/116] Add python-Levenshtein to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index af1981cd..d74ff77b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ PyICU pycld2 colorama pyyaml +python-Levenshtein From 260b6725526ca2a805a85e95fe145aa77c50e3e3 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 13:28:34 +0300 Subject: [PATCH 024/116] Fetch cheat sheets in GitHub actions --- .github/workflows/tests-ubuntu.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 99947a2b..6737fb8c 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -15,5 +15,7 @@ jobs: - uses: actions/checkout@v2 - name: install dependencies run: pip3 install -r requirements.txt + - name: fetch upstream cheat sheets + run: python3 lib/fetch.py fetch-all - name: run tests run: bash tests/run-tests.sh From 6a8896fb3956931f5ef1a7c4d8a1850f345c0295 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 13:29:24 +0300 Subject: [PATCH 025/116] Python 3 is default on 20.04+ --- .github/workflows/tests-ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 6737fb8c..298d6656 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -14,8 +14,8 @@ jobs: steps: - uses: actions/checkout@v2 - name: install dependencies - run: pip3 install -r requirements.txt + run: pip install -r requirements.txt - name: fetch upstream cheat sheets - run: python3 lib/fetch.py fetch-all + run: python lib/fetch.py fetch-all - name: run tests run: bash tests/run-tests.sh From 38f3b699ac9e68971e5cd7f54423528d02dee959 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Thu, 12 Nov 2020 14:16:54 +0300 Subject: [PATCH 026/116] Run GitHub build every week at Thursday 9am --- .github/workflows/tests-ubuntu.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 298d6656..dbf95fb4 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -5,6 +5,8 @@ on: branches: [ master ] pull_request: branches: [ master ] + schedule: + - cron: '0 9 * * 4' jobs: build: From 4639d6809f48926f742d46f78dce7b1f5511fce6 Mon Sep 17 00:00:00 2001 From: fedeb Date: Thu, 5 Nov 2020 14:15:18 +0100 Subject: [PATCH 027/116] Added a check on get_answer_dict that identifies random requests --- lib/routing.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 1da0def5..5684c34e 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -9,7 +9,7 @@ from __future__ import print_function import re - +import random import cache import adapter.cheat_sheets import adapter.cmd @@ -106,10 +106,36 @@ def _get_page_dict(self, query, topic_type, request_options=None): """ Return answer_dict for the `query`. """ - return self._adapter[topic_type]\ .get_page_dict(query, request_options=request_options) + + def is_random_request(self,topic): + print("Entrato in is_random_request con {}".format(topic)) + if topic.endswith('/:random') or topic.lstrip('/') == ':random': + #We strip the :random part and see if the query is valid by running a get_topics_list() + topic = topic[:-8] + topic_list = [x[len(topic):] + for x in self.get_topics_list() + if x.startswith(topic + "/")] + if topic_list: + #This is a correct formatted query like /cpp/:random + print("La richiesta random è giusta") + print("Questa è la topic_list della richiesta :random = {}".format(topic_list)) + return True + return False + + def select_random_topic(self,topic): + print("Entrato in select_random_topic con {}".format(topic)) + topic = topic[:-8] + topic_list = [x[len(topic):] + for x in self.get_topics_list() + if x.startswith(topic + "/")] + if ":list" in topic_list: topic_list.remove(":list") + random_topic = topic + random.choice(topic_list) + return random_topic + + def get_answer_dict(self, topic, request_options=None): """ Find cheat sheet for the topic. @@ -120,6 +146,8 @@ def get_answer_dict(self, topic, request_options=None): Returns: answer_dict: the answer dictionary """ + if self.is_random_request(topic): + topic = self.select_random_topic(topic) topic_type = self.get_topic_type(topic) From e01333bfce2b61e1111067c51b23b7f14b3c643d Mon Sep 17 00:00:00 2001 From: fedeb Date: Thu, 5 Nov 2020 14:32:34 +0100 Subject: [PATCH 028/116] Remved /:list from the possible random choice --- lib/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routing.py b/lib/routing.py index 5684c34e..65b17457 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -131,7 +131,7 @@ def select_random_topic(self,topic): topic_list = [x[len(topic):] for x in self.get_topics_list() if x.startswith(topic + "/")] - if ":list" in topic_list: topic_list.remove(":list") + if "/:list" in topic_list: topic_list.remove("/:list") random_topic = topic + random.choice(topic_list) return random_topic From cba1884efb2287007cbf97cbe3a9a7c9df3d35b5 Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 17:50:19 +0100 Subject: [PATCH 029/116] removed rosetta/ and '' from possible random choices --- lib/routing.py | 60 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 65b17457..bd27a3e1 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -109,31 +109,48 @@ def _get_page_dict(self, query, topic_type, request_options=None): return self._adapter[topic_type]\ .get_page_dict(query, request_options=request_options) - - def is_random_request(self,topic): - print("Entrato in is_random_request con {}".format(topic)) + + def handle_if_random_request(self,topic): + """ + Return topic type for `topic` or "unknown" if topic can't be determined. + """ + + def __select_random_topic(stripped_topic,topic_list): + #Here we remove the special pages if present + if ":list" in topic_list: topic_list.remove(":list") + if "rosetta/" in topic_list: topic_list.remove("rosetta/") + random_topic = random.choice(topic_list) + print("Il random_topic è {}".format(stripped_topic + random_topic)) + return stripped_topic + random_topic + + print("Entrato in handle_if_random_request con {}".format(topic)) if topic.endswith('/:random') or topic.lstrip('/') == ':random': #We strip the :random part and see if the query is valid by running a get_topics_list() - topic = topic[:-8] - topic_list = [x[len(topic):] - for x in self.get_topics_list() - if x.startswith(topic + "/")] + + # Here we take the cheat.sh/x/ part + stripped_topic = topic[:-7] + + topic_list = [x[len(stripped_topic):] + for x in self.get_topics_list() + if x.startswith(stripped_topic)] + + if '' in topic_list: topic_list.remove('') if topic_list: #This is a correct formatted query like /cpp/:random print("La richiesta random è giusta") print("Questa è la topic_list della richiesta :random = {}".format(topic_list)) - return True - return False - - def select_random_topic(self,topic): - print("Entrato in select_random_topic con {}".format(topic)) - topic = topic[:-8] - topic_list = [x[len(topic):] - for x in self.get_topics_list() - if x.startswith(topic + "/")] - if "/:list" in topic_list: topic_list.remove("/:list") - random_topic = topic + random.choice(topic_list) - return random_topic + random_topic = __select_random_topic(stripped_topic,topic_list) + return random_topic + else: + #This is wrongly random formatted + # It is not a valid random request, we just strip the /:random + wrongly_formatted_random = topic[:-8] + print("La richiesta random è sbagliata") + print("Eseguo lo stripping = {}".format(wrongly_formatted_random)) + return wrongly_formatted_random + #Here if not a random requst, we just forward the topic + return topic + def get_answer_dict(self, topic, request_options=None): @@ -146,9 +163,8 @@ def get_answer_dict(self, topic, request_options=None): Returns: answer_dict: the answer dictionary """ - if self.is_random_request(topic): - topic = self.select_random_topic(topic) - + + topic = self.handle_if_random_request(topic) topic_type = self.get_topic_type(topic) # 'question' queries are pretty expensive, that's why they should be handled From 5a4d5b61ec313b4dfbfdf37ab2a9b0cf6f068dad Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 20:15:54 +0100 Subject: [PATCH 030/116] Fixed comments regarding :random --- lib/routing.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index bd27a3e1..925f1234 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -114,40 +114,31 @@ def handle_if_random_request(self,topic): """ Return topic type for `topic` or "unknown" if topic can't be determined. """ - - def __select_random_topic(stripped_topic,topic_list): - #Here we remove the special pages if present + + def __select_random_topic(prefix,topic_list): + #Here we remove the special cases if ":list" in topic_list: topic_list.remove(":list") if "rosetta/" in topic_list: topic_list.remove("rosetta/") random_topic = random.choice(topic_list) - print("Il random_topic è {}".format(stripped_topic + random_topic)) - return stripped_topic + random_topic + return prefix + random_topic - print("Entrato in handle_if_random_request con {}".format(topic)) if topic.endswith('/:random') or topic.lstrip('/') == ':random': #We strip the :random part and see if the query is valid by running a get_topics_list() - - # Here we take the cheat.sh/x/ part - stripped_topic = topic[:-7] - - topic_list = [x[len(stripped_topic):] + prefix = topic[:-7] + topic_list = [x[len(prefix):] for x in self.get_topics_list() - if x.startswith(stripped_topic)] - + if x.startswith(prefix)] if '' in topic_list: topic_list.remove('') if topic_list: - #This is a correct formatted query like /cpp/:random - print("La richiesta random è giusta") - print("Questa è la topic_list della richiesta :random = {}".format(topic_list)) - random_topic = __select_random_topic(stripped_topic,topic_list) + # This is a correct formatted random query like /cpp/:random, the topic_list is not empty. + random_topic = __select_random_topic(prefix,topic_list) return random_topic else: - #This is wrongly random formatted - # It is not a valid random request, we just strip the /:random + # This is a wrongly formatted random query like /xyxyxy/:random, the topic_list not empty + # we just strip the /:random and let the already implemented logic handle it. wrongly_formatted_random = topic[:-8] - print("La richiesta random è sbagliata") - print("Eseguo lo stripping = {}".format(wrongly_formatted_random)) return wrongly_formatted_random + #Here if not a random requst, we just forward the topic return topic From 2040c5708cbadba67e7a3381aec20942c4852f72 Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 20:25:23 +0100 Subject: [PATCH 031/116] Fixed typo --- lib/routing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 925f1234..0c220b20 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -112,9 +112,9 @@ def _get_page_dict(self, query, topic_type, request_options=None): def handle_if_random_request(self,topic): """ - Return topic type for `topic` or "unknown" if topic can't be determined. + Check if the `query` if a :random one, if yes we check its correctness and then randomly select a topic, based on the provided prefix. + """ - def __select_random_topic(prefix,topic_list): #Here we remove the special cases if ":list" in topic_list: topic_list.remove(":list") From 0e65fc4fdf3d5acb925e0b744af30536c889f0c2 Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 20:48:38 +0100 Subject: [PATCH 032/116] Minor fix on checking if is a random request --- lib/routing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 0c220b20..d8ac53c1 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -124,17 +124,18 @@ def __select_random_topic(prefix,topic_list): if topic.endswith('/:random') or topic.lstrip('/') == ':random': #We strip the :random part and see if the query is valid by running a get_topics_list() + if topic.lstrip('/') == ':random' : topic = topic.lstrip('/') prefix = topic[:-7] topic_list = [x[len(prefix):] for x in self.get_topics_list() if x.startswith(prefix)] if '' in topic_list: topic_list.remove('') if topic_list: - # This is a correct formatted random query like /cpp/:random, the topic_list is not empty. + # This is a correct formatted random query like /cpp/:random as the topic_list is not empty. random_topic = __select_random_topic(prefix,topic_list) return random_topic else: - # This is a wrongly formatted random query like /xyxyxy/:random, the topic_list not empty + # This is a wrongly formatted random query like /xyxyxy/:random as the topic_list is empty # we just strip the /:random and let the already implemented logic handle it. wrongly_formatted_random = topic[:-8] return wrongly_formatted_random From f20a71fc4c84a4bef45010ced1d76034f162b13c Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 21:00:27 +0100 Subject: [PATCH 033/116] Added a line on the readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7edf81c..81490a5c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Such a thing exists. [![Build Status](https://travis-ci.org/chubin/cheat.sh.svg?branch=master)](https://travis-ci.org/chubin/cheat.sh) ## Features - +notnot **cheat.sh** * Has a simple curl/browser interface. @@ -658,6 +658,7 @@ Other pages: :post how to post new cheat sheet :styles list of color styles :styles-demo show color styles usage examples + :random fetches a random page (can be used in a subsection too: /go/:random) ``` ## Search From fbe956c19300b424d27290cd4de8a8c9e1e63a40 Mon Sep 17 00:00:00 2001 From: fedeb Date: Sat, 7 Nov 2020 21:15:26 +0100 Subject: [PATCH 034/116] Another check if a given topic has just the :list and rosetta/ --- lib/routing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/routing.py b/lib/routing.py index d8ac53c1..6be5269e 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -119,6 +119,9 @@ def __select_random_topic(prefix,topic_list): #Here we remove the special cases if ":list" in topic_list: topic_list.remove(":list") if "rosetta/" in topic_list: topic_list.remove("rosetta/") + #Here we still check that topic_list in not empty + if not topic_list: + return prefix random_topic = random.choice(topic_list) return prefix + random_topic From 6e1c3d8e76d81fda8dd5c97a8295466a6219bc5a Mon Sep 17 00:00:00 2001 From: fedeb Date: Mon, 9 Nov 2020 18:39:52 +0100 Subject: [PATCH 035/116] Fixed readme & style, removed special cases from random pool. --- README.md | 2 +- lib/routing.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 81490a5c..e0aa2885 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Such a thing exists. [![Build Status](https://travis-ci.org/chubin/cheat.sh.svg?branch=master)](https://travis-ci.org/chubin/cheat.sh) ## Features -notnot + **cheat.sh** * Has a simple curl/browser interface. diff --git a/lib/routing.py b/lib/routing.py index 6be5269e..e37c62be 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -109,33 +109,42 @@ def _get_page_dict(self, query, topic_type, request_options=None): return self._adapter[topic_type]\ .get_page_dict(query, request_options=request_options) - - def handle_if_random_request(self,topic): + def handle_if_random_request(self, topic): """ Check if the `query` if a :random one, if yes we check its correctness and then randomly select a topic, based on the provided prefix. """ - def __select_random_topic(prefix,topic_list): + + def __select_random_topic(prefix, topic_list): #Here we remove the special cases - if ":list" in topic_list: topic_list.remove(":list") - if "rosetta/" in topic_list: topic_list.remove("rosetta/") - #Here we still check that topic_list in not empty - if not topic_list: + if "rosetta/" in topic_list: + topic_list.remove("rosetta/") + + cleaned_topic_list = [ x for x in topic_list if ':' not in x] + + #Here we still check that cleaned_topic_list in not empty + if not cleaned_topic_list: return prefix - random_topic = random.choice(topic_list) + + random_topic = random.choice(cleaned_topic_list) return prefix + random_topic if topic.endswith('/:random') or topic.lstrip('/') == ':random': #We strip the :random part and see if the query is valid by running a get_topics_list() - if topic.lstrip('/') == ':random' : topic = topic.lstrip('/') + if topic.lstrip('/') == ':random' : + topic = topic.lstrip('/') prefix = topic[:-7] + topic_list = [x[len(prefix):] for x in self.get_topics_list() if x.startswith(prefix)] - if '' in topic_list: topic_list.remove('') + + if '' in topic_list: + topic_list.remove('') + if topic_list: # This is a correct formatted random query like /cpp/:random as the topic_list is not empty. - random_topic = __select_random_topic(prefix,topic_list) + random_topic = __select_random_topic(prefix, topic_list) return random_topic else: # This is a wrongly formatted random query like /xyxyxy/:random as the topic_list is empty @@ -145,8 +154,6 @@ def __select_random_topic(prefix,topic_list): #Here if not a random requst, we just forward the topic return topic - - def get_answer_dict(self, topic, request_options=None): """ From 8942d41bab30e293551a8633ba8acc7aa240931e Mon Sep 17 00:00:00 2001 From: fedeb Date: Tue, 10 Nov 2020 11:19:43 +0100 Subject: [PATCH 036/116] Further removing of special cases --- lib/routing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index e37c62be..b471373e 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -117,10 +117,7 @@ def handle_if_random_request(self, topic): def __select_random_topic(prefix, topic_list): #Here we remove the special cases - if "rosetta/" in topic_list: - topic_list.remove("rosetta/") - - cleaned_topic_list = [ x for x in topic_list if ':' not in x] + cleaned_topic_list = [ x for x in topic_list if '/' not in x and ':' not in x] #Here we still check that cleaned_topic_list in not empty if not cleaned_topic_list: From 3c88b52a6f505daeabbf68c9e2b7f9197a19b189 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 11:47:22 +0100 Subject: [PATCH 037/116] Show package versions --- .github/workflows/tests-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index dbf95fb4..7ac88985 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: install dependencies - run: pip install -r requirements.txt + run: pip install -r requirements.txt ; pip freeze - name: fetch upstream cheat sheets run: python lib/fetch.py fetch-all - name: run tests From 1056ac96e0776ed0395366309cca7eab64df7297 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 11:57:03 +0100 Subject: [PATCH 038/116] Upgrade packages if they are already installed --- .github/workflows/tests-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 7ac88985..14cea6c3 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: install dependencies - run: pip install -r requirements.txt ; pip freeze + run: pip install --upgrade -r requirements.txt ; pip freeze - name: fetch upstream cheat sheets run: python lib/fetch.py fetch-all - name: run tests From cf892b425611d9ec9b3cd0e91287aa3a51af7dae Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 12:22:26 +0100 Subject: [PATCH 039/116] Remove pip freeze from ci --- .github/workflows/tests-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 14cea6c3..08f718cd 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: install dependencies - run: pip install --upgrade -r requirements.txt ; pip freeze + run: pip install --upgrade -r requirements.txt - name: fetch upstream cheat sheets run: python lib/fetch.py fetch-all - name: run tests From 1f0ce075bb746e78e4430aa03f79aa710e12d41b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 12:49:24 +0100 Subject: [PATCH 040/116] Install python requirements in Dockerfile (ci) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e41aa0a5..da9b5b80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev \ ## copying WORKDIR /app COPY . /app -RUN mkdir -p /root/.cheat.sh/log/ \ +RUN mkdir -p /root/.cheat.sh/log/ && pip3 install --no-cache-dir --update -r requirements.txt \ && python3 lib/fetch.py fetch-all # installing server dependencies From 1ce75de5ea398221a80f11b5eb561c85b1994489 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Fri, 13 Nov 2020 15:31:29 +0300 Subject: [PATCH 041/116] Try to build container with GitHub Actions --- .github/workflows/tests-ubuntu.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 08f718cd..b0360554 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -10,9 +10,7 @@ on: jobs: build: - runs-on: ubuntu-20.04 - steps: - uses: actions/checkout@v2 - name: install dependencies @@ -21,3 +19,9 @@ jobs: run: python lib/fetch.py fetch-all - name: run tests run: bash tests/run-tests.sh + + docker: + runs-on: ubuntu-20.04 + steps: + - run: docker-compose build + From 485614f25c1fff8f7ef4dde5fecd58398d72e817 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Fri, 13 Nov 2020 15:37:41 +0300 Subject: [PATCH 042/116] Port .travis.yml to GitHub Actions --- .github/workflows/tests-ubuntu.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index b0360554..270e64e2 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -23,5 +23,16 @@ jobs: docker: runs-on: ubuntu-20.04 steps: - - run: docker-compose build + - uses: actions/checkout@v2 + - run: docker-compose build + - run: - docker images + - run: | + docker-compose -f docker-compose.yml up -d + # docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d + docker-compose ps + # wait until the web server is up + wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http + docker-compose logs --no-color + - run: CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh + From e8e26bc17eefa208dce15cac6c53965e578b3f4d Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Fri, 13 Nov 2020 15:45:00 +0300 Subject: [PATCH 043/116] Fix invalid .yml for Actions Ref. #253 --- .github/workflows/tests-ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index 270e64e2..d9121126 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v2 - run: docker-compose build - - run: - docker images + - run: docker images - run: | docker-compose -f docker-compose.yml up -d # docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d From e59502b64af69addcfd18886c79b904c3c89bbef Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 18:21:28 +0100 Subject: [PATCH 044/116] Move pip3 to right place --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index da9b5b80..d8d760c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,11 +6,12 @@ RUN apk add --update --no-cache git py3-six py3-pygments py3-yaml py3-gevent \ ## building missing python packages RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev \ && pip3 install --no-cache-dir rapidfuzz colored polyglot pycld2 \ + && pip3 install --no-cache-dir --update -r requirements.txt \ && apk del build-deps ## copying WORKDIR /app COPY . /app -RUN mkdir -p /root/.cheat.sh/log/ && pip3 install --no-cache-dir --update -r requirements.txt \ +RUN mkdir -p /root/.cheat.sh/log/ \ && python3 lib/fetch.py fetch-all # installing server dependencies From a9247746a5667fb838279e5c11a5070eb128ffc8 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 18:31:23 +0100 Subject: [PATCH 045/116] Move pip3 to right place --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d8d760c0..a3f5c8c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ RUN apk add --update --no-cache git py3-six py3-pygments py3-yaml py3-gevent \ libstdc++ py3-colorama py3-requests py3-icu py3-redis ## building missing python packages RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev \ + && pip3 install --no-cache-dir --upgrade -r requirements.txt \ && pip3 install --no-cache-dir rapidfuzz colored polyglot pycld2 \ - && pip3 install --no-cache-dir --update -r requirements.txt \ && apk del build-deps ## copying WORKDIR /app From c097a437dcbc453e26b364f5d1236edde8ee682e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 18:39:50 +0100 Subject: [PATCH 046/116] Copy requirements.txt --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a3f5c8c7..2ae72935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,9 @@ FROM alpine:3.12 RUN apk add --update --no-cache git py3-six py3-pygments py3-yaml py3-gevent \ libstdc++ py3-colorama py3-requests py3-icu py3-redis ## building missing python packages +COPY requirements.txt /tmp RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev \ - && pip3 install --no-cache-dir --upgrade -r requirements.txt \ + && pip3 install --no-cache-dir --upgrade -r /tmp/requirements.txt \ && pip3 install --no-cache-dir rapidfuzz colored polyglot pycld2 \ && apk del build-deps ## copying From 89f04f7555cc0563b955548d251ba87008f2dd7d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 13 Nov 2020 19:02:43 +0000 Subject: [PATCH 047/116] Upgrade pygments --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2ae72935..aa961e98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,9 @@ RUN apk add --update --no-cache git py3-six py3-pygments py3-yaml py3-gevent \ libstdc++ py3-colorama py3-requests py3-icu py3-redis ## building missing python packages COPY requirements.txt /tmp -RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev \ - && pip3 install --no-cache-dir --upgrade -r /tmp/requirements.txt \ - && pip3 install --no-cache-dir rapidfuzz colored polyglot pycld2 \ +RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev libffi-dev \ + && pip3 install --no-cache-dir --upgrade pygments \ + && pip3 install --no-cache-dir -r /tmp/requirements.txt \ && apk del build-deps ## copying WORKDIR /app From 2e61d2bb1000ebe8c22ba3944ec7660bcf9a6d73 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 16 Nov 2020 22:54:18 +0000 Subject: [PATCH 048/116] Disable caching for UpstreamAdapter --- lib/adapter/upstream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/adapter/upstream.py b/lib/adapter/upstream.py index 7622b76e..28fa7ba7 100644 --- a/lib/adapter/upstream.py +++ b/lib/adapter/upstream.py @@ -48,7 +48,7 @@ class UpstreamAdapter(Adapter): _adapter_name = "upstream" _output_format = "ansi" - _cache_needed = True + _cache_needed = False def _get_page(self, topic, request_options=None): From ae25b7ec2ba1f0e754407987f0f48ac0bab14bdc Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 16 Nov 2020 23:03:42 +0000 Subject: [PATCH 049/116] Reorder commands in Dockerfile --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index aa961e98..55c5f8e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,15 +3,15 @@ FROM alpine:3.12 ## installing dependencies RUN apk add --update --no-cache git py3-six py3-pygments py3-yaml py3-gevent \ libstdc++ py3-colorama py3-requests py3-icu py3-redis +## copying +WORKDIR /app +COPY . /app ## building missing python packages -COPY requirements.txt /tmp RUN apk add --no-cache --virtual build-deps py3-pip g++ python3-dev libffi-dev \ && pip3 install --no-cache-dir --upgrade pygments \ - && pip3 install --no-cache-dir -r /tmp/requirements.txt \ + && pip3 install --no-cache-dir -r requirements.txt \ && apk del build-deps -## copying -WORKDIR /app -COPY . /app +# fetching dependencies RUN mkdir -p /root/.cheat.sh/log/ \ && python3 lib/fetch.py fetch-all From 3da4aeafe3c9f06c03cc477cc156473eead65ec6 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 16 Nov 2020 23:58:57 +0000 Subject: [PATCH 050/116] Return 'cache' property in answer_dict --- lib/adapter/adapter.py | 1 + lib/adapter/upstream.py | 2 +- lib/routing.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/adapter/adapter.py b/lib/adapter/adapter.py index f33babb0..ffa55ec4 100644 --- a/lib/adapter/adapter.py +++ b/lib/adapter/adapter.py @@ -145,6 +145,7 @@ def get_page_dict(self, topic, request_options=None): 'topic': topic, 'topic_type': self._adapter_name, 'format': self._get_output_format(topic), + 'cache': self._cache_needed, } answer_dict.update(answer) diff --git a/lib/adapter/upstream.py b/lib/adapter/upstream.py index 28fa7ba7..786d9b22 100644 --- a/lib/adapter/upstream.py +++ b/lib/adapter/upstream.py @@ -58,7 +58,7 @@ def _get_page(self, topic, request_options=None): + "?" + options_string try: response = requests.get(url, timeout=CONFIG["upstream.timeout"]) - answer = response.text + answer = {"cache": False, "answer": response.text} except requests.exceptions.ConnectionError: answer = {"cache": False, "answer":_are_you_offline()} return answer diff --git a/lib/routing.py b/lib/routing.py index b471373e..1247c43d 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -182,7 +182,8 @@ def get_answer_dict(self, topic, request_options=None): } answer = self._get_page_dict(topic, topic_type, request_options=request_options) - cache.put('q:' + topic, answer) + if answer.get("cache", True): + cache.put('q:' + topic, answer) return answer # Try to find cacheable queries in the cache. From 5f425f719dd79545d9f53e492a0310701afe9882 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Fri, 13 Nov 2020 15:31:29 +0300 Subject: [PATCH 051/116] Try to build container with GitHub Actions --- .github/workflows/tests-ubuntu.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests-ubuntu.yml b/.github/workflows/tests-ubuntu.yml index d9121126..87aec310 100644 --- a/.github/workflows/tests-ubuntu.yml +++ b/.github/workflows/tests-ubuntu.yml @@ -34,5 +34,3 @@ jobs: wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http docker-compose logs --no-color - run: CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh - - From 27d3720884ef60d857711d6477bb88e3a6c3bb7b Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Fri, 13 Nov 2020 16:04:06 +0300 Subject: [PATCH 052/116] Clean up Travis files --- .travis.yml | 17 ----------------- README.md | 1 - tests/run-tests.sh | 3 --- 3 files changed, 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 70d308e9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -dist: focal - -language: - - generic - -before_install: - - docker-compose build - - docker images - - docker-compose -f docker-compose.yml up -d - #- docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d - - docker-compose ps - # wait until the web server is up - - wget --timeout 3 --tries=5 --spider localhost:8002 2>&1 | grep -i http - - docker-compose logs --no-color - -script: - - CHEATSH_TEST_STANDALONE=NO bash tests/run-tests.sh diff --git a/README.md b/README.md index e0aa2885..4879dcc8 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ What features should it have? Such a thing exists. -[![Build Status](https://travis-ci.org/chubin/cheat.sh.svg?branch=master)](https://travis-ci.org/chubin/cheat.sh) ## Features diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 8e3c7941..33db7603 100644 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -90,9 +90,6 @@ while read -r number test_line; do if [ "$show_details" = YES ]; then cat "$TMP2" fi - if grep -q "Internal Server Error" "$TMP2"; then - [[ $TRAVIS == true ]] && docker logs chtsh - fi echo "FAILED: [$number] $test_line" else cat "$TMP" > results/"$number" From e2e24f879ee903405aa60f28b0c7ecfec77cabc6 Mon Sep 17 00:00:00 2001 From: fedeb Date: Sun, 15 Nov 2020 19:54:05 +0100 Subject: [PATCH 053/116] Added :random to the help page. --- share/help.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/share/help.txt b/share/help.txt index 40724875..059a0c96 100644 --- a/share/help.txt +++ b/share/help.txt @@ -31,6 +31,7 @@ Special pages: :bash_completion bash function for tab completion :styles list of color styles :styles-demo show color styles usage examples + :random fetches a random cheat sheet Shell client: @@ -75,6 +76,7 @@ each programming language topic has the following subptopics: hello hello world + how to start the program :learn big cheat sheet for learning language from scratch :list list of topics + :random fetches a random cheat sheet belonging to the topic Support programming languages: From 7eee26ab38488369a793697e2cd47ae8912ad190 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Mon, 16 Nov 2020 11:30:39 +0300 Subject: [PATCH 054/116] Decouple Flask `app` from `gevent` monkeypatching --- bin/app.py | 270 ++++++++++++++++++++++++++++++++++++++++++++++++ bin/srv.py | 296 +++-------------------------------------------------- 2 files changed, 286 insertions(+), 280 deletions(-) create mode 100644 bin/app.py diff --git a/bin/app.py b/bin/app.py new file mode 100644 index 00000000..bda993fa --- /dev/null +++ b/bin/app.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# vim: set encoding=utf-8 +# pylint: disable=wrong-import-position,wrong-import-order + +""" +Main server program. + +Configuration parameters: + + path.internal.malformed + path.internal.static + path.internal.templates + path.log.main + path.log.queries +""" + +from __future__ import print_function + +import sys +if sys.version_info[0] < 3: + reload(sys) + sys.setdefaultencoding('utf8') + +import sys +import logging +import os +import requests +import jinja2 +from flask import Flask, request, send_from_directory, redirect, Response + +sys.path.append(os.path.abspath(os.path.join(__file__, "..", "..", "lib"))) +from config import CONFIG +from limits import Limits +from cheat_wrapper import cheat_wrapper +from post import process_post_request +from options import parse_args + +from stateful_queries import save_query, last_query + +if not os.path.exists(os.path.dirname(CONFIG["path.log.main"])): + os.makedirs(os.path.dirname(CONFIG["path.log.main"])) +logging.basicConfig( + filename=CONFIG["path.log.main"], + level=logging.DEBUG, + format='%(asctime)s %(message)s') + +app = Flask(__name__) # pylint: disable=invalid-name +app.jinja_loader = jinja2.ChoiceLoader([ + app.jinja_loader, + jinja2.FileSystemLoader(CONFIG["path.internal.templates"])]) + +LIMITS = Limits() + +def is_html_needed(user_agent): + """ + Basing on `user_agent`, return whether it needs HTML or ANSI + """ + plaintext_clients = [ + 'curl', 'wget', 'fetch', 'httpie', 'lwp-request', 'openbsd ftp', 'python-requests'] + return all([x not in user_agent for x in plaintext_clients]) + +def is_result_a_script(query): + return query in [':cht.sh'] + +@app.route('/files/') +def send_static(path): + """ + Return static file `path`. + Can be served by the HTTP frontend. + """ + return send_from_directory(CONFIG["path.internal.static"], path) + +@app.route('/favicon.ico') +def send_favicon(): + """ + Return static file `favicon.ico`. + Can be served by the HTTP frontend. + """ + return send_from_directory(CONFIG["path.internal.static"], 'favicon.ico') + +@app.route('/malformed-response.html') +def send_malformed(): + """ + Return static file `malformed-response.html`. + Can be served by the HTTP frontend. + """ + dirname, filename = os.path.split(CONFIG["path.internal.malformed"]) + return send_from_directory(dirname, filename) + +def log_query(ip_addr, found, topic, user_agent): + """ + Log processed query and some internal data + """ + log_entry = "%s %s %s %s\n" % (ip_addr, found, topic, user_agent) + with open(CONFIG["path.log.queries"], 'ab') as my_file: + my_file.write(log_entry.encode('utf-8')) + +def get_request_ip(req): + """ + Extract IP address from `request` + """ + + if req.headers.getlist("X-Forwarded-For"): + ip_addr = req.headers.getlist("X-Forwarded-For")[0] + if ip_addr.startswith('::ffff:'): + ip_addr = ip_addr[7:] + else: + ip_addr = req.remote_addr + if req.headers.getlist("X-Forwarded-For"): + ip_addr = req.headers.getlist("X-Forwarded-For")[0] + if ip_addr.startswith('::ffff:'): + ip_addr = ip_addr[7:] + else: + ip_addr = req.remote_addr + + return ip_addr + +def get_answer_language(request): + """ + Return preferred answer language based on + domain name, query arguments and headers + """ + + def _parse_accept_language(accept_language): + languages = accept_language.split(",") + locale_q_pairs = [] + + for language in languages: + try: + if language.split(";")[0] == language: + # no q => q = 1 + locale_q_pairs.append((language.strip(), "1")) + else: + locale = language.split(";")[0].strip() + weight = language.split(";")[1].split("=")[1] + locale_q_pairs.append((locale, weight)) + except IndexError: + pass + + return locale_q_pairs + + def _find_supported_language(accepted_languages): + for lang_tuple in accepted_languages: + lang = lang_tuple[0] + if '-' in lang: + lang = lang.split('-', 1)[0] + return lang + return None + + lang = None + hostname = request.headers['Host'] + if hostname.endswith('.cheat.sh'): + lang = hostname[:-9] + + if 'lang' in request.args: + lang = request.args.get('lang') + + header_accept_language = request.headers.get('Accept-Language', '') + if lang is None and header_accept_language: + lang = _find_supported_language( + _parse_accept_language(header_accept_language)) + + return lang + +def _proxy(*args, **kwargs): + # print "method=", request.method, + # print "url=", request.url.replace('/:shell-x/', ':3000/') + # print "headers=", {key: value for (key, value) in request.headers if key != 'Host'} + # print "data=", request.get_data() + # print "cookies=", request.cookies + # print "allow_redirects=", False + + url_before, url_after = request.url.split('/:shell-x/', 1) + url = url_before + ':3000/' + + if 'q' in request.args: + url_after = '?' + "&".join("arg=%s" % x for x in request.args['q'].split()) + + url += url_after + print(url) + print(request.get_data()) + resp = requests.request( + method=request.method, + url=url, + headers={key: value for (key, value) in request.headers if key != 'Host'}, + data=request.get_data(), + cookies=request.cookies, + allow_redirects=False) + + excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] + headers = [(name, value) for (name, value) in resp.raw.headers.items() + if name.lower() not in excluded_headers] + + response = Response(resp.content, resp.status_code, headers) + return response + + +@app.route("/", methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) +def answer(topic=None): + """ + Main rendering function, it processes incoming weather queries. + Depending on user agent it returns output in HTML or ANSI format. + + Incoming data: + request.args + request.headers + request.remote_addr + request.referrer + request.query_string + """ + + user_agent = request.headers.get('User-Agent', '').lower() + html_needed = is_html_needed(user_agent) + options = parse_args(request.args) + + if topic in ['apple-touch-icon-precomposed.png', 'apple-touch-icon.png', 'apple-touch-icon-120x120-precomposed.png'] \ + or (topic is not None and any(topic.endswith('/'+x) for x in ['favicon.ico'])): + return '' + + request_id = request.cookies.get('id') + if topic is not None and topic.lstrip('/') == ':last': + if request_id: + topic = last_query(request_id) + else: + return "ERROR: you have to set id for your requests to use /:last\n" + else: + if request_id: + save_query(request_id, topic) + + if request.method == 'POST': + process_post_request(request, html_needed) + if html_needed: + return redirect("/") + return "OK\n" + + if 'topic' in request.args: + return redirect("/%s" % request.args.get('topic')) + + if topic is None: + topic = ":firstpage" + + if topic.startswith(':shell-x/'): + return _proxy() + #return requests.get('http://127.0.0.1:3000'+topic[8:]).text + + lang = get_answer_language(request) + if lang: + options['lang'] = lang + + ip_address = get_request_ip(request) + if '+' in topic: + not_allowed = LIMITS.check_ip(ip_address) + if not_allowed: + return "429 %s\n" % not_allowed, 429 + + html_is_needed = is_html_needed(user_agent) and not is_result_a_script(topic) + if html_is_needed: + output_format='html' + else: + output_format='ansi' + result, found = cheat_wrapper(topic, request_options=options, output_format=output_format) + if 'Please come back in several hours' in result and html_is_needed: + malformed_response = open(os.path.join(CONFIG["path.internal.malformed"])).read() + return malformed_response + + log_query(ip_address, found, topic, user_agent) + if html_is_needed: + return result + return Response(result, mimetype='text/plain') diff --git a/bin/srv.py b/bin/srv.py index fef3ea36..847375a7 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -1,292 +1,28 @@ #!/usr/bin/env python -# vim: set encoding=utf-8 -# pylint: disable=wrong-import-position,wrong-import-order - -""" -Main server program. - -Configuration parameters: - - path.internal.malformed - path.internal.static - path.internal.templates - path.log.main - path.log.queries -""" - -from __future__ import print_function - -import sys -if sys.version_info[0] < 3: - reload(sys) - sys.setdefaultencoding('utf8') - +# +# Serving cheat.sh with `gevent` +# from gevent.monkey import patch_all from gevent.pywsgi import WSGIServer patch_all() -import sys -import logging import os -import requests -import jinja2 -from flask import Flask, request, send_from_directory, redirect, Response - -sys.path.append(os.path.abspath(os.path.join(__file__, "..", "..", "lib"))) -from config import CONFIG -from limits import Limits -from cheat_wrapper import cheat_wrapper -from post import process_post_request -from options import parse_args - -from stateful_queries import save_query, last_query - -if not os.path.exists(os.path.dirname(CONFIG["path.log.main"])): - os.makedirs(os.path.dirname(CONFIG["path.log.main"])) -logging.basicConfig( - filename=CONFIG["path.log.main"], - level=logging.DEBUG, - format='%(asctime)s %(message)s') - -app = Flask(__name__) # pylint: disable=invalid-name -app.jinja_loader = jinja2.ChoiceLoader([ - app.jinja_loader, - jinja2.FileSystemLoader(CONFIG["path.internal.templates"])]) - -LIMITS = Limits() - -def is_html_needed(user_agent): - """ - Basing on `user_agent`, return whether it needs HTML or ANSI - """ - plaintext_clients = [ - 'curl', 'wget', 'fetch', 'httpie', 'lwp-request', 'openbsd ftp', 'python-requests'] - return all([x not in user_agent for x in plaintext_clients]) - -def is_result_a_script(query): - return query in [':cht.sh'] - -@app.route('/files/') -def send_static(path): - """ - Return static file `path`. - Can be served by the HTTP frontend. - """ - return send_from_directory(CONFIG["path.internal.static"], path) - -@app.route('/favicon.ico') -def send_favicon(): - """ - Return static file `favicon.ico`. - Can be served by the HTTP frontend. - """ - return send_from_directory(CONFIG["path.internal.static"], 'favicon.ico') - -@app.route('/malformed-response.html') -def send_malformed(): - """ - Return static file `malformed-response.html`. - Can be served by the HTTP frontend. - """ - dirname, filename = os.path.split(CONFIG["path.internal.malformed"]) - return send_from_directory(dirname, filename) - -def log_query(ip_addr, found, topic, user_agent): - """ - Log processed query and some internal data - """ - log_entry = "%s %s %s %s\n" % (ip_addr, found, topic, user_agent) - with open(CONFIG["path.log.queries"], 'ab') as my_file: - my_file.write(log_entry.encode('utf-8')) - -def get_request_ip(req): - """ - Extract IP address from `request` - """ - - if req.headers.getlist("X-Forwarded-For"): - ip_addr = req.headers.getlist("X-Forwarded-For")[0] - if ip_addr.startswith('::ffff:'): - ip_addr = ip_addr[7:] - else: - ip_addr = req.remote_addr - if req.headers.getlist("X-Forwarded-For"): - ip_addr = req.headers.getlist("X-Forwarded-For")[0] - if ip_addr.startswith('::ffff:'): - ip_addr = ip_addr[7:] - else: - ip_addr = req.remote_addr - - return ip_addr - -def get_answer_language(request): - """ - Return preferred answer language based on - domain name, query arguments and headers - """ - - def _parse_accept_language(accept_language): - languages = accept_language.split(",") - locale_q_pairs = [] - - for language in languages: - try: - if language.split(";")[0] == language: - # no q => q = 1 - locale_q_pairs.append((language.strip(), "1")) - else: - locale = language.split(";")[0].strip() - weight = language.split(";")[1].split("=")[1] - locale_q_pairs.append((locale, weight)) - except IndexError: - pass - - return locale_q_pairs - - def _find_supported_language(accepted_languages): - for lang_tuple in accepted_languages: - lang = lang_tuple[0] - if '-' in lang: - lang = lang.split('-', 1)[0] - return lang - return None - - lang = None - hostname = request.headers['Host'] - if hostname.endswith('.cheat.sh'): - lang = hostname[:-9] - - if 'lang' in request.args: - lang = request.args.get('lang') - - header_accept_language = request.headers.get('Accept-Language', '') - if lang is None and header_accept_language: - lang = _find_supported_language( - _parse_accept_language(header_accept_language)) - - return lang - -def _proxy(*args, **kwargs): - # print "method=", request.method, - # print "url=", request.url.replace('/:shell-x/', ':3000/') - # print "headers=", {key: value for (key, value) in request.headers if key != 'Host'} - # print "data=", request.get_data() - # print "cookies=", request.cookies - # print "allow_redirects=", False - - url_before, url_after = request.url.split('/:shell-x/', 1) - url = url_before + ':3000/' - - if 'q' in request.args: - url_after = '?' + "&".join("arg=%s" % x for x in request.args['q'].split()) - - url += url_after - print(url) - print(request.get_data()) - resp = requests.request( - method=request.method, - url=url, - headers={key: value for (key, value) in request.headers if key != 'Host'}, - data=request.get_data(), - cookies=request.cookies, - allow_redirects=False) - - excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] - headers = [(name, value) for (name, value) in resp.raw.headers.items() - if name.lower() not in excluded_headers] - - response = Response(resp.content, resp.status_code, headers) - return response - - -@app.route("/", methods=['GET', 'POST']) -@app.route("/", methods=["GET", "POST"]) -def answer(topic=None): - """ - Main rendering function, it processes incoming weather queries. - Depending on user agent it returns output in HTML or ANSI format. - - Incoming data: - request.args - request.headers - request.remote_addr - request.referrer - request.query_string - """ - - user_agent = request.headers.get('User-Agent', '').lower() - html_needed = is_html_needed(user_agent) - options = parse_args(request.args) - - if topic in ['apple-touch-icon-precomposed.png', 'apple-touch-icon.png', 'apple-touch-icon-120x120-precomposed.png'] \ - or (topic is not None and any(topic.endswith('/'+x) for x in ['favicon.ico'])): - return '' - - request_id = request.cookies.get('id') - if topic is not None and topic.lstrip('/') == ':last': - if request_id: - topic = last_query(request_id) - else: - return "ERROR: you have to set id for your requests to use /:last\n" - else: - if request_id: - save_query(request_id, topic) - - if request.method == 'POST': - process_post_request(request, html_needed) - if html_needed: - return redirect("/") - return "OK\n" - - if 'topic' in request.args: - return redirect("/%s" % request.args.get('topic')) - - if topic is None: - topic = ":firstpage" - - if topic.startswith(':shell-x/'): - return _proxy() - #return requests.get('http://127.0.0.1:3000'+topic[8:]).text - - lang = get_answer_language(request) - if lang: - options['lang'] = lang - - ip_address = get_request_ip(request) - if '+' in topic: - not_allowed = LIMITS.check_ip(ip_address) - if not_allowed: - return "429 %s\n" % not_allowed, 429 - - html_is_needed = is_html_needed(user_agent) and not is_result_a_script(topic) - if html_is_needed: - output_format='html' - else: - output_format='ansi' - result, found = cheat_wrapper(topic, request_options=options, output_format=output_format) - if 'Please come back in several hours' in result and html_is_needed: - malformed_response = open(os.path.join(CONFIG["path.internal.malformed"])).read() - return malformed_response +import sys - log_query(ip_address, found, topic, user_agent) - if html_is_needed: - return result - return Response(result, mimetype='text/plain') +from app import app, CONFIG -if __name__ == '__main__': - # Serving cheat.sh with `gevent` - if '--debug' in sys.argv: - # Not all debug mode features are available under `gevent` - # https://github.com/pallets/flask/issues/3825 - app.debug = True +if '--debug' in sys.argv: + # Not all debug mode features are available under `gevent` + # https://github.com/pallets/flask/issues/3825 + app.debug = True - if 'CHEATSH_PORT' in os.environ: - PORT = int(os.environ.get('CHEATSH_PORT')) - else: - PORT = CONFIG['server.port'] +if 'CHEATSH_PORT' in os.environ: + port = int(os.environ.get('CHEATSH_PORT')) +else: + port = CONFIG['server.port'] - SRV = WSGIServer((CONFIG['server.bind'], PORT), app) # log=None) - print("Starting gevent server on {}:{}".format(SRV.address[0], SRV.address[1])) - SRV.serve_forever() +srv = WSGIServer((CONFIG['server.bind'], port), app) +print("Starting gevent server on {}:{}".format(srv.address[0], srv.address[1])) +srv.serve_forever() From 0593f32d321ca603eb48569a29d078a973f04b67 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 16 Nov 2020 23:59:42 +0000 Subject: [PATCH 055/116] Update tests/results/7 (:random) --- tests/results/7 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/results/7 b/tests/results/7 index 40724875..059a0c96 100644 --- a/tests/results/7 +++ b/tests/results/7 @@ -31,6 +31,7 @@ Special pages: :bash_completion bash function for tab completion :styles list of color styles :styles-demo show color styles usage examples + :random fetches a random cheat sheet Shell client: @@ -75,6 +76,7 @@ each programming language topic has the following subptopics: hello hello world + how to start the program :learn big cheat sheet for learning language from scratch :list list of topics + :random fetches a random cheat sheet belonging to the topic Support programming languages: From 9970ee323d5fba7677f3d714cc72d7e734c8989f Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Tue, 17 Nov 2020 17:23:33 +0300 Subject: [PATCH 056/116] Remove explicit `gevent` imports It is enough to monkey patch one time at the top level, which is done in `bin/srv.py` web server script. --- lib/adapter/cmd.py | 9 ++++----- lib/adapter/question.py | 6 ++---- lib/fmt/comments.py | 6 +----- lib/frontend/html.py | 7 ++----- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/adapter/cmd.py b/lib/adapter/cmd.py index 1e909910..edcf923a 100644 --- a/lib/adapter/cmd.py +++ b/lib/adapter/cmd.py @@ -1,15 +1,14 @@ """ """ -# pylint: disable=relative-import,wrong-import-position,unused-argument,abstract-method +# pylint: disable=unused-argument,abstract-method -from gevent.monkey import patch_all -from gevent.subprocess import Popen, PIPE +import os.path +import re +from subprocess import Popen, PIPE from .adapter import Adapter -import os.path -import re def _get_abspath(path): """Find absolute path of the specified `path` diff --git a/lib/adapter/question.py b/lib/adapter/question.py index 0e22d859..54b97ecb 100644 --- a/lib/adapter/question.py +++ b/lib/adapter/question.py @@ -4,15 +4,13 @@ path.internal.bin.upstream """ -# pylint: disable=relative-import,wrong-import-position,wrong-import-order +# pylint: disable=relative-import from __future__ import print_function -from gevent.monkey import patch_all -from gevent.subprocess import Popen, PIPE - import os import re +from subprocess import Popen, PIPE from polyglot.detect import Detector from polyglot.detect.base import UnknownLanguage diff --git a/lib/fmt/comments.py b/lib/fmt/comments.py index 824140fd..5b7625bb 100644 --- a/lib/fmt/comments.py +++ b/lib/fmt/comments.py @@ -18,19 +18,15 @@ Configuration parameters: """ -# pylint: disable=wrong-import-position,wrong-import-order - from __future__ import print_function -from gevent.monkey import patch_all -from gevent.subprocess import Popen - import sys import os import textwrap import hashlib import re from itertools import groupby, chain +from subprocess import Popen from tempfile import NamedTemporaryFile from config import CONFIG diff --git a/lib/frontend/html.py b/lib/frontend/html.py index c73e96c9..a130e69c 100644 --- a/lib/frontend/html.py +++ b/lib/frontend/html.py @@ -5,22 +5,19 @@ path.internal.ansi2html """ -from gevent.monkey import patch_all -from gevent.subprocess import Popen, PIPE - -# pylint: disable=wrong-import-position,wrong-import-order import sys import os import re +from subprocess import Popen, PIPE MYDIR = os.path.abspath(os.path.join(__file__, '..', '..')) sys.path.append("%s/lib/" % MYDIR) +# pylint: disable=wrong-import-position from config import CONFIG from globals import error from buttons import TWITTER_BUTTON, GITHUB_BUTTON, GITHUB_BUTTON_FOOTER import frontend.ansi -# pylint: disable=wrong-import-position,wrong-import-order # temporary having it here, but actually we have the same data # in the adapter module From 6f7c87b8d46332e12fb505e190c22f65e49d8cc1 Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Tue, 17 Nov 2020 13:38:16 +0300 Subject: [PATCH 057/116] Properly setup Flask logging to stderr Sometimes there is nothing, sometimes lines are duplicated. Duplicated lines problem appears, because Python root logger handler and Flask's `werkzeug` logger handler both write the same message. `cheat.sh` is hit, because it sets this root logging handler. Normally `werkzeug` doesn't setup its own handler if it sees that there are some handlers that process its messages. This in case of `cheat.sh` resulted in no stderr logging at all. No Exceptions either. Because `werkzeug` catches the exceptions and logs them. Hovewer, sometimes `werkzeug` starts too early, befora app is imported (https://github.com/pallets/werkzeug/issues/1969) and sets its logger anyway, before the app itself, resulting in duplicating lines. In that case app root handler needs to skip lines from `werkzeug`. Kudos to @brandon-rhodes for `logging_tree` awesomeness --- bin/app.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bin/app.py b/bin/app.py index bda993fa..638bfbea 100644 --- a/bin/app.py +++ b/bin/app.py @@ -37,12 +37,34 @@ from stateful_queries import save_query, last_query + if not os.path.exists(os.path.dirname(CONFIG["path.log.main"])): os.makedirs(os.path.dirname(CONFIG["path.log.main"])) logging.basicConfig( filename=CONFIG["path.log.main"], level=logging.DEBUG, format='%(asctime)s %(message)s') +# Fix Flask "exception and request logging" to `stderr`. +# +# When Flask's werkzeug detects that logging is already set, it +# doesn't add its own logger that prints exceptions. +stderr_handler = logging.StreamHandler() +logging.getLogger().addHandler(stderr_handler) +# +# Alter log format to disting log lines from everything else +stderr_handler.setFormatter(logging.Formatter('%(filename)s:%(lineno)s: %(message)s')) +# +# Sometimes werkzeug starts logging before an app is imported +# (https://github.com/pallets/werkzeug/issues/1969) +# resulting in duplicating lines. In that case we need root +# stderr handler to skip lines from werkzeug. +class SkipFlaskLogger(object): + def filter(self, record): + if record.name != 'werkzeug': + return True +if logging.getLogger('werkzeug').handlers: + stderr_handler.addFilter(SkipFlaskLogger()) + app = Flask(__name__) # pylint: disable=invalid-name app.jinja_loader = jinja2.ChoiceLoader([ From 9e4a5fe704a8be3e0898d6d5ad7d9a5e50f93070 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 22 Nov 2020 19:00:44 +0000 Subject: [PATCH 058/116] Show all cheat sheets when several found --- lib/cheat_wrapper.py | 4 +- lib/frontend/ansi.py | 18 +++++--- lib/routing.py | 100 ++++++++++++++++++++++++++++--------------- lib/search.py | 11 ++--- 4 files changed, 85 insertions(+), 48 deletions(-) diff --git a/lib/cheat_wrapper.py b/lib/cheat_wrapper.py index 73fc74a4..5a5fa624 100644 --- a/lib/cheat_wrapper.py +++ b/lib/cheat_wrapper.py @@ -11,7 +11,7 @@ import re import json -from routing import get_answer_dict, get_topics_list +from routing import get_answers, get_topics_list from search import find_answers_by_keyword from languages_data import LANGUAGE_ALIAS, rewrite_editor_section_name import postprocessing @@ -98,7 +98,7 @@ def _parse_query(query): answers = find_answers_by_keyword( topic, keyword, options=search_options, request_options=request_options) else: - answers = [get_answer_dict(topic, request_options=request_options)] + answers = get_answers(topic, request_options=request_options) answers = [ postprocessing.postprocess( diff --git a/lib/frontend/ansi.py b/lib/frontend/ansi.py index 3880af50..276d58ba 100644 --- a/lib/frontend/ansi.py +++ b/lib/frontend/ansi.py @@ -101,6 +101,10 @@ def _visualize(answers, request_options, search_mode=False): if color_style not in CONFIG['frontend.styles']: color_style = '' + # if there is more than one answer, + # show the source of the answer + multiple_answers = len(answers) > 1 + found = True result = "" for answer_dict in answers: @@ -109,14 +113,16 @@ def _visualize(answers, request_options, search_mode=False): answer = answer_dict['answer'] found = found and not topic_type == 'unknown' - if search_mode and topic != 'LIMITED': + if multiple_answers and topic != 'LIMITED': + section_name = f"{topic_type}:{topic}" + if not highlight: - result += "\n[%s]\n" % topic + result += f"#[{section_name}]\n" else: - result += "\n%s%s %s %s%s\n" % ( - colored.bg('dark_gray'), colored.attr("res_underlined"), - topic, - colored.attr("res_underlined"), colored.attr('reset')) + result += "".join([ + "\n", colored.bg('dark_gray'), colored.attr("res_underlined"), + f" {section_name} ", + colored.attr("res_underlined"), colored.attr('reset'), "\n"]) if answer_dict['format'] in ['ansi', 'text']: result += answer diff --git a/lib/routing.py b/lib/routing.py index 1247c43d..c23f5fc7 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -4,12 +4,13 @@ Exports: get_topics_list() - get_answer_dict() + get_answers() """ -from __future__ import print_function -import re import random +import re +from typing import Any, Dict, List + import cache import adapter.cheat_sheets import adapter.cmd @@ -83,20 +84,28 @@ def get_topics_list(self, skip_dirs=False, skip_internal=False): self._cached_topics_list = answer return answer - def get_topic_type(self, topic): + def get_topic_type(self, topic: str) -> List[str]: """ - Return topic type for `topic` or "unknown" if topic can't be determined. + Return list of topic types for `topic` + or ["unknown"] if topic can't be determined. """ - def __get_topic_type(topic): + def __get_topic_type(topic: str) -> List[str]: + result = [] for regexp, route in self.routing_table: if re.search(regexp, topic): if route in self._adapter: if self._adapter[route].is_found(topic): - return route + result.append(route) else: - return route - return CONFIG["routing.default"] + result.append(route) + if not result: + return [CONFIG["routing.default"]] + + # cut the default route off, if there are more than one route found + if len(result) > 1: + return result[:-1] + return result if topic not in self._cached_topic_type: self._cached_topic_type[topic] = __get_topic_type(topic) @@ -111,7 +120,9 @@ def _get_page_dict(self, query, topic_type, request_options=None): def handle_if_random_request(self, topic): """ - Check if the `query` if a :random one, if yes we check its correctness and then randomly select a topic, based on the provided prefix. + Check if the `query` if a :random one, + if yes we check its correctness and then randomly select a topic, + based on the provided prefix. """ @@ -152,60 +163,79 @@ def __select_random_topic(prefix, topic_list): #Here if not a random requst, we just forward the topic return topic - def get_answer_dict(self, topic, request_options=None): + def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List[Dict[str, Any]]: """ - Find cheat sheet for the topic. + Find cheat sheets for the topic. Args: `topic` (str): the name of the topic of the cheat sheet Returns: - answer_dict: the answer dictionary + [answer_dict]: list of answers (dictionaries) """ + # if topic specified as :, + # cut off + topic_type = "" + if re.match("[^/]+:", topic): + topic_type, topic = topic.split(":", 1) + topic = self.handle_if_random_request(topic) - topic_type = self.get_topic_type(topic) + topic_types = self.get_topic_type(topic) + + # if topic_type is specified explicitly, + # show pages only of that type + if topic_type and topic_type in topic_types: + topic_types = [topic_type] # 'question' queries are pretty expensive, that's why they should be handled # in a special way: # we do not drop the old style cache entries and try to reuse them if possible - if topic_type == 'question': + if topic_types == ['question']: answer = cache.get('q:' + topic) if answer: if isinstance(answer, dict): - return answer - return { + return [answer] + return [{ 'topic': topic, 'topic_type': 'question', 'answer': answer, 'format': 'text+code', - } + }] - answer = self._get_page_dict(topic, topic_type, request_options=request_options) + answer = self._get_page_dict(topic, topic_types[0], request_options=request_options) if answer.get("cache", True): cache.put('q:' + topic, answer) - return answer + return [answer] # Try to find cacheable queries in the cache. # If answer was not found in the cache, resolve it in a normal way and save in the cache - cache_needed = self._adapter[topic_type].is_cache_needed() - if cache_needed: - answer = cache.get(topic) - if not isinstance(answer, dict): - answer = None - if answer: - return answer + answers = [] + for topic_type in topic_types: - answer = self._get_page_dict(topic, topic_type, request_options=request_options) - if isinstance(answer, dict): - if "cache" in answer: - cache_needed = answer["cache"] + cache_entry_name = f"{topic_type}:{topic}" + cache_needed = self._adapter[topic_type].is_cache_needed() - if cache_needed and answer: - cache.put(topic, answer) - return answer + if cache_needed: + answer = cache.get(cache_entry_name) + if not isinstance(answer, dict): + answer = None + if answer: + answers.append(answer) + + answer = self._get_page_dict(topic, topic_type, request_options=request_options) + if isinstance(answer, dict): + if "cache" in answer: + cache_needed = answer["cache"] + + if cache_needed and answer: + cache.put(cache_entry_name, answer) + + answers.append(answer) + + return answers # pylint: disable=invalid-name _ROUTER = Router() get_topics_list = _ROUTER.get_topics_list -get_answer_dict = _ROUTER.get_answer_dict +get_answers = _ROUTER.get_answers diff --git a/lib/search.py b/lib/search.py index dc95aea4..e4beaa38 100644 --- a/lib/search.py +++ b/lib/search.py @@ -22,7 +22,7 @@ import re from config import CONFIG -from routing import get_answer_dict, get_topics_list +from routing import get_answers, get_topics_list def _limited_entry(): return { @@ -100,10 +100,11 @@ def find_answers_by_keyword(directory, keyword, options="", request_options=None if not options_dict["recursive"] and '/' in subtopic: continue - answer_dict = get_answer_dict(topic, request_options=request_options) - answer_text = answer_dict.get('answer', '') - if match(answer_text, keyword, options_dict=options_dict): - answers_found.append(answer_dict) + answer_dicts = get_answers(topic, request_options=request_options) + for answer_dict in answer_dicts: + answer_text = answer_dict.get('answer', '') + if match(answer_text, keyword, options_dict=options_dict): + answers_found.append(answer_dict) if len(answers_found) > CONFIG['search.limit']: answers_found.append( From 667edabebbdcf3b0364e6ec1c449ea40d5106475 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 22 Nov 2020 19:00:56 +0000 Subject: [PATCH 059/116] Update tests --- tests/results/17 | 24 ++++++++++++++++++++++++ tests/results/2 | 26 ++++++++++++++++++++++++++ tests/results/3 | 25 +++++++++++++++++++++++++ tests/results/4 | 20 ++++++++++++++++++++ tests/results/5 | 15 ++++++++++++++- 5 files changed, 109 insertions(+), 1 deletion(-) diff --git a/tests/results/17 b/tests/results/17 index a1cc24ff..a00b7dea 100644 --- a/tests/results/17 +++ b/tests/results/17 @@ -1,3 +1,4 @@ + cheat.sheets:az  # Microsoft Azure CLI 2.0 # Command-line tools for Azure @@ -88,3 +89,26 @@ # detach disk az vm disk detach --vm-name vm1 -g RESOURCE_GROUP --name DISK1_ID + + tldr:az  +# az +# The official CLI tool for Microsoft Azure. +# More information: . + +# Log in to Azure: +az login + +# Manage azure subscription information: +az account + +# List all Azure Managed Disks: +az disk list + +# List all Azure virtual machines: +az vm list + +# Manage Azure Kubernetes Services: +az aks + +# Manage Azure Network resources: +az network diff --git a/tests/results/2 b/tests/results/2 index 3e3ad19d..2f1e0982 100644 --- a/tests/results/2 +++ b/tests/results/2 @@ -1,3 +1,4 @@ + cheat:ls  # To display everything in

, excluding hidden files: ls <dir> @@ -15,3 +16,28 @@ # To display directories only, include hidden: ls -d .*/ */ <dir> + + tldr:ls  +# ls +# List directory contents. + +# List files one per line: +ls -1 + +# List all files, including hidden files: +ls -a + +# List all files, with trailing `/` added to directory names: +ls -F + +# Long format list (permissions, ownership, size and modification date) of all files: +ls -la + +# Long format list with size displayed using human readable units (KB, MB, GB): +ls -lh + +# Long format list sorted by size (descending): +ls -lS + +# Long format list of all files, sorted by modification date (oldest first): +ls -ltr diff --git a/tests/results/3 b/tests/results/3 index cfff5cc9..65b6bffd 100644 --- a/tests/results/3 +++ b/tests/results/3 @@ -1,3 +1,4 @@ +#[cheat:ls] # To display everything in , excluding hidden files: ls @@ -15,3 +16,27 @@ ls -d */ # To display directories only, include hidden: ls -d .*/ */ +#[tldr:ls] +# ls +# List directory contents. + +# List files one per line: +ls -1 + +# List all files, including hidden files: +ls -a + +# List all files, with trailing `/` added to directory names: +ls -F + +# Long format list (permissions, ownership, size and modification date) of all files: +ls -la + +# Long format list with size displayed using human readable units (KB, MB, GB): +ls -lh + +# Long format list sorted by size (descending): +ls -lS + +# Long format list of all files, sorted by modification date (oldest first): +ls -ltr diff --git a/tests/results/4 b/tests/results/4 index b028fe79..f8f97c5d 100644 --- a/tests/results/4 +++ b/tests/results/4 @@ -1,3 +1,4 @@ + cheat.sheets:btrfs  # Create a btrfs file system on /dev/sdb, /dev/sdc, and /dev/sdd mkfs.btrfs /dev/sdb /dev/sdc /dev/sdd @@ -52,3 +53,22 @@ # convert btrfs to ext3/ext4 btrfs-convert -r /dev/sdb1 + + tldr:btrfs  +# btrfs +# A filesystem based on the copy-on-write (COW) principle for Linux. + +# Create subvolume: +sudo btrfs subvolume create path/to/subvolume + +# List subvolumes: +sudo btrfs subvolume list path/to/mount_point + +# Show space usage information: +sudo btrfs filesystem df path/to/mount_point + +# Enable quota: +sudo btrfs quota enable path/to/subvolume + +# Show quota: +sudo btrfs qgroup show path/to/subvolume diff --git a/tests/results/5 b/tests/results/5 index 50d71659..0e7ea410 100644 --- a/tests/results/5 +++ b/tests/results/5 @@ -1,4 +1,4 @@ - btrfs  + cheat.sheets:btrfs  # create the subvolume /mnt/sv1 in the /mnt volume btrfs subvolume create /mnt/sv1 @@ -13,3 +13,16 @@ # taking snapshot of a subvolume btrfs subvolume snapshot /mnt/sv1 /mnt/sv1_snapshot + + tldr:btrfs  +# Create subvolume: +sudo btrfs subvolume create path/to/subvolume + +# List subvolumes: +sudo btrfs subvolume list path/to/mount_point + +# Enable quota: +sudo btrfs quota enable path/to/subvolume + +# Show quota: +sudo btrfs qgroup show path/to/subvolume From efc830810869a1bd129739da34a0579aa53acfa5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 22 Nov 2020 20:09:51 +0000 Subject: [PATCH 060/116] Continue if response found in cache --- lib/routing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/routing.py b/lib/routing.py index c23f5fc7..38af02de 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -222,6 +222,7 @@ def get_answers(self, topic: str, request_options:Dict[str, str] = None) -> List answer = None if answer: answers.append(answer) + continue answer = self._get_page_dict(topic, topic_type, request_options=request_options) if isinstance(answer, dict): From 880ef6f5dcb3fd1ae6e35f8d0cbd9bc52c208c5b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 22 Nov 2020 20:17:21 +0000 Subject: [PATCH 061/116] Update results/17 (az) --- tests/results/17 | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/tests/results/17 b/tests/results/17 index a00b7dea..abdd880e 100644 --- a/tests/results/17 +++ b/tests/results/17 @@ -5,11 +5,14 @@ # Install Azure CLI 2.0 with one curl command. curl -L https://aka.ms/InstallAzureCli | bash -# create a resource group named "MyResourceGroup" in the westus2 region of Azure -az group create -n MyResourceGroup -l westus2 +# create a resource group named "MyRG" in the 'westus2' region +az group create -n MyRG -l westus2 -# create a Linux VM using the UbuntuTLS image, with two attached storage disks of 10 GB and 20 GB -az vm create -n MyLinuxVM -g MyResourceGroup --ssh-key-value $HOME/.ssh/id_rsa.pub --image UbuntuLTS --data-disk-sizes-gb 10 20 +# create a Linux VM using the UbuntuTLS image, +# with two attached storage disks of 10 GB and 20 GB +az vm create -n MyLinuxVM -g MyRG \ + --ssh-key-value $HOME/.ssh/id_rsa.pub --image UbuntuLTS \ + --data-disk-sizes-gb 10 20 # list VMs az vm list --output table @@ -17,28 +20,28 @@ # list only VMs having distinct state az vm list -d --query "[?powerState=='VM running']" --output table -# delete VM (with the name MyLinuxVM in the group MyResourceGroup) -az vm delete -g MyResourceGroup -n MyLinuxVM --yes +# delete VM (with the name MyLinuxVM in the group MyRG) +az vm delete -g MyRG -n MyLinuxVM --yes # Delete all VMs in a resource group -az vm delete --ids $(az vm list -g MyResourceGroup --query "[].id" -o tsv) +az vm delete --ids $(az vm list -g MyRG --query "[].id" -o tsv) # Create an Image based on a running VM -az vm deallocate -g MyResourceGroup -n MyLinuxVM -az vm generalize -g MyResourceGroup -n MyLinuxVM -az image create --resource-group MyResourceGroup --name MyTestImage --source MyLinuxVM +az vm deallocate -g MyRG -n MyLinuxVM +az vm generalize -g MyRG -n MyLinuxVM +az image create --resource-group MyRG --name MyTestImage --source MyLinuxVM # Running VM based on a VHD az storage blob upload --account-name "${account_name}" \ - --account-key "${account_key}" --container-name "${container_name}" --type page \ - --file "${file}" --name "${vhd_name}" + --account-key "${account_key}" --container-name "${container}" --type page \ + --file "${file}" --name "${vhd}" az disk create \ - --resource-group ${resource_group} \ + --resource-group "${resource_group}" \  --name myManagedDisk \ - --source https://${account_name}.blob.core.windows.net/${container_name}/${vhd_name} + --source "https://${account_name}.blob.core.windows.net/${container}/${vhd}" # open port -az vm open-port --resource-group MyResourceGroup --name MyLinuxVM --port 443 --priority 899 +az vm open-port --resource-group MyRG --name MyLinuxVM --port 443 --priority 899 # Show storage accounts az storage account list --output table @@ -47,7 +50,8 @@ az storage container list --account-name mystorageaccount --output table # Show blobs in a container -az storage blob list --account-name mystorageaccount --container-name mycontainer --output table +az storage blob list --account-name mystorageaccount \ + --container-name mycontainer --output table # list account keys az storage account keys list --account-name STORAGE_NAME --resource-group RESOURCE_GROUP @@ -63,8 +67,8 @@ # Copy blob az storage blob copy start \ - --source-uri 'https://md-ldh5nknx2rkz.blob.core.windows.net/jzwuuuzzapn0/abcd?sv=2017-04-17&sr=b&si=68041718-6828-4f5e-9e6e-a1b719975062&sig=XXX' \ - --account-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== \ + --source-uri 'https://xxx.blob.core.windows.net/jzwuuuzzapn0/abcd?...' \ + --account-key XXXXXXXXXX== \  --account-name destaccount \  --destination-container vms \  --destination-blob DESTINATION-blob.vhd @@ -82,7 +86,8 @@ az snapshot create --resource-group IC-EXASOL-001 --source vm1-disk1 -n vm1-snap1 # create SAS url for a snapshot -az snapshot grant-access --resource-group IC-EXASOL-001 --name vm1-snap1 --duration-in-seconds 36000 --query '[accessSas]' -o tsv +az snapshot grant-access --resource-group IC-EXASOL-001 --name vm1-snap1\ + --duration-in-seconds 36000 --query '[accessSas]' -o tsv # attach disk az vm disk attach --vm-name vm1 -g RESOURCE_GROUP --disk DISK1_ID From 382f0eeff5b91f4a05f08c5bda4abffa6a2b0d1d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 23 Nov 2020 08:21:05 +0100 Subject: [PATCH 062/116] Update lib/routing.py Co-authored-by: Anatoli Babenia --- lib/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routing.py b/lib/routing.py index 38af02de..e45002e6 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -120,7 +120,7 @@ def _get_page_dict(self, query, topic_type, request_options=None): def handle_if_random_request(self, topic): """ - Check if the `query` if a :random one, + Check if the `query` is a :random one, if yes we check its correctness and then randomly select a topic, based on the provided prefix. From 3313b06655224b31c3eeaa209ec125d38ac4cd08 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 29 Nov 2020 21:37:30 +0000 Subject: [PATCH 063/116] Hotfix unicode processing for Python 3 (fixes #265) This fix is temprorary, and must be implemented properly, after Python 2 support will be fully dropped. --- lib/fmt/comments.py | 5 +++-- lib/postprocessing.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/fmt/comments.py b/lib/fmt/comments.py index 5b7625bb..8bd122d7 100644 --- a/lib/fmt/comments.py +++ b/lib/fmt/comments.py @@ -230,7 +230,7 @@ def _beautify(text, filetype, add_comments=False, remove_text=False): # or remove the text completely. Otherwise the code has to remain aligned unindent_code = add_comments or remove_text - lines = [x.rstrip('\n') for x in text.splitlines()] + lines = [x.decode("utf-8").rstrip('\n') for x in text.splitlines()] lines = _cleanup_lines(lines) lines_classes = zip(_classify_lines(lines), lines) lines_classes = _wrap_lines(lines_classes, unindent_code=unindent_code) @@ -292,7 +292,8 @@ def beautify(text, lang, options): # if mode is unknown, just don't transform the text at all return text - text = text.encode('utf-8') + if isinstance(text, str): + text = text.encode('utf-8') digest = "t:%s:%s:%s" % (hashlib.md5(text).hexdigest(), lang, mode) # temporary added line that removes invalid cache entries diff --git a/lib/postprocessing.py b/lib/postprocessing.py index bc82c1e9..649c4a6e 100644 --- a/lib/postprocessing.py +++ b/lib/postprocessing.py @@ -40,6 +40,8 @@ def _join_paragraphs(paragraphs): def _split_paragraphs(text): answer = [] paragraph = "" + if isinstance(text, bytes): + text = text.decode("utf-8") for line in text.splitlines(): if line == "": answer.append(paragraph) From d5ae13363178fd02a34fc0df5f555c3c8b096c98 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 10 Dec 2020 14:58:37 +0100 Subject: [PATCH 064/116] Remove broken fonts stylesheet --- share/ansi2html.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/share/ansi2html.sh b/share/ansi2html.sh index 964ffddd..0b1e78a0 100644 --- a/share/ansi2html.sh +++ b/share/ansi2html.sh @@ -104,7 +104,6 @@ printf '%s' " -