├── tests ├── colon-in-header-arg-value │ ├── hello.nim │ └── colon-in-header-arg-value.org ├── mkdirp_yes │ ├── foo │ │ └── bar │ │ │ └── mkdirp_test.sh │ └── mkdirp_yes.org ├── shebang │ ├── echo.sh │ └── shebang.org ├── begin_src │ ├── begin_src.el │ └── begin_src.org ├── dmacs │ ├── early-init.el │ └── README ├── key_without_args_mkdir │ ├── do │ │ └── not │ │ │ └── exist │ │ │ └── test.sage │ └── key_without_args_mkdir.org ├── global_tangle │ ├── header_arg_inheritance.el │ ├── global_tangle.el │ ├── specified_file.nim │ ├── header_arg_inheritance2.el │ ├── global_tangle_lang_specific.nim │ ├── global_tangle.nim │ ├── header_arg_inheritance2.org │ ├── header_arg_inheritance.org │ ├── global_tangle_lang_specific.org │ └── global_tangle.org ├── src_blocks_with_extra_indentation │ ├── hi2.nim │ ├── hello2.nim │ └── src_blocks_with_extra_indentation.org ├── include │ ├── file1.el │ ├── file2.org │ ├── file3.org │ └── file1.org ├── multiple_src_blocks_tangled_to_same_file │ ├── hi.nim │ ├── hello.nim │ └── multiple_src_blocks_tangled_to_same_file.org ├── tangle_mode │ ├── file_permissions_644 │ ├── file_permissions_755 │ ├── file_permissions_600_override_shebang │ └── tangle_mode.org ├── tangle_no_yes │ ├── specified_file.nim │ ├── tangle_no_yes.nim │ └── tangle_no_yes.org ├── property_drawer │ ├── specified_file.nim │ ├── foo.el │ ├── foo │ │ └── bar.el │ ├── property_drawer.nim │ ├── property_drawer2.org │ └── property_drawer.org ├── key_without_args │ ├── key_without_args.sage │ └── key_without_args.org ├── wyag │ ├── wyag │ ├── wyag-tests │ └── libwyag.py ├── org_tangle_rs │ ├── README │ ├── org-tangle-engine │ │ ├── lib_mini.rs │ │ └── lib.rs │ ├── engine_mini.org │ ├── org-parser │ │ └── lib.rs │ ├── lib.org │ └── engine.org ├── mkdirp_no │ └── mkdirp_no.org ├── missing_arg_value │ └── missing_arg_value.org ├── test1 │ ├── test_passing.nim │ ├── test_failing.nim │ └── test1.ORG ├── invalid_arg_no_colon │ └── invalid_arg_no_colon.org ├── nested_src │ ├── tangled.org │ ├── nested_src.nim │ └── nested_src.org ├── quick_test.sh ├── test.sh └── eless │ ├── eless │ └── eless.org ├── .gitignore ├── doc └── img │ └── Screenshot_ntangle_v0.4.2.png ├── .dir-locals.el ├── config.nims ├── ntangle.nimble ├── .gitattributes ├── LICENSE ├── .github └── workflows │ └── test.yml ├── README.org └── src └── ntangle.nim /tests/colon-in-header-arg-value/hello.nim: -------------------------------------------------------------------------------- 1 | echo "hello" 2 | -------------------------------------------------------------------------------- /tests/mkdirp_yes/foo/bar/mkdirp_test.sh: -------------------------------------------------------------------------------- 1 | echo "Hello" 2 | -------------------------------------------------------------------------------- /tests/shebang/echo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hello" 3 | -------------------------------------------------------------------------------- /tests/begin_src/begin_src.el: -------------------------------------------------------------------------------- 1 | (message "this will be tangled") 2 | -------------------------------------------------------------------------------- /tests/dmacs/early-init.el: -------------------------------------------------------------------------------- 1 | (setq package-enable-at-startup nil) 2 | -------------------------------------------------------------------------------- /tests/key_without_args_mkdir/do/not/exist/test.sage: -------------------------------------------------------------------------------- 1 | φ = var('φ') 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ntangle 2 | src/nimcache/ 3 | /src/ntangle 4 | other/ 5 | -------------------------------------------------------------------------------- /tests/global_tangle/header_arg_inheritance.el: -------------------------------------------------------------------------------- 1 | (message "this will be tangled") 2 | -------------------------------------------------------------------------------- /tests/src_blocks_with_extra_indentation/hi2.nim: -------------------------------------------------------------------------------- 1 | echo "hi 1" 2 | echo "hi 2" 3 | -------------------------------------------------------------------------------- /tests/include/file1.el: -------------------------------------------------------------------------------- 1 | (message "1") 2 | 3 | (message "2") 4 | 5 | (message "3") 6 | -------------------------------------------------------------------------------- /tests/multiple_src_blocks_tangled_to_same_file/hi.nim: -------------------------------------------------------------------------------- 1 | echo "hi 1" 2 | echo "hi 2" 3 | -------------------------------------------------------------------------------- /tests/tangle_mode/file_permissions_644: -------------------------------------------------------------------------------- 1 | echo "permissions of this file will be 644" 2 | -------------------------------------------------------------------------------- /tests/tangle_mode/file_permissions_755: -------------------------------------------------------------------------------- 1 | echo "permissions of this file will be 755" 2 | -------------------------------------------------------------------------------- /tests/global_tangle/global_tangle.el: -------------------------------------------------------------------------------- 1 | (message "this will be tangled to global_tangle.el") 2 | -------------------------------------------------------------------------------- /tests/global_tangle/specified_file.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to specified_file.nim" 2 | -------------------------------------------------------------------------------- /tests/tangle_no_yes/specified_file.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to specified_file.nim" 2 | -------------------------------------------------------------------------------- /tests/tangle_no_yes/tangle_no_yes.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to tangle_no_yes.nim" 2 | -------------------------------------------------------------------------------- /tests/multiple_src_blocks_tangled_to_same_file/hello.nim: -------------------------------------------------------------------------------- 1 | echo "hello 1" 2 | echo "hello 2" 3 | -------------------------------------------------------------------------------- /tests/property_drawer/specified_file.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to specified_file.nim" 2 | -------------------------------------------------------------------------------- /tests/global_tangle/header_arg_inheritance2.el: -------------------------------------------------------------------------------- 1 | (message "hello 1") 2 | 3 | (message "hello 2") 4 | -------------------------------------------------------------------------------- /tests/include/file2.org: -------------------------------------------------------------------------------- 1 | #+begin_src emacs-lisp -n :tangle msg2.el 2 | (message "2") 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/key_without_args/key_without_args.sage: -------------------------------------------------------------------------------- 1 | φ = var('φ') 2 | 3 | f(φ) = cos(φ) 4 | 5 | f(123) 6 | -------------------------------------------------------------------------------- /tests/wyag/wyag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # <> 3 | import libwyag 4 | libwyag.main() 5 | -------------------------------------------------------------------------------- /tests/global_tangle/global_tangle_lang_specific.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to global_tangle.nim" 2 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/README: -------------------------------------------------------------------------------- 1 | Source: https://github.com/OrgTangle/org-rs/blob/master/org-parser/src/lib.org 2 | -------------------------------------------------------------------------------- /tests/mkdirp_no/mkdirp_no.org: -------------------------------------------------------------------------------- 1 | #+begin_src shell :tangle "~/foo/bar/mkdirp_test.sh" 2 | echo "Hello" 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/src_blocks_with_extra_indentation/hello2.nim: -------------------------------------------------------------------------------- 1 | echo "hello 1" 2 | echo "hello 2" 3 | let 4 | foo = "bar" 5 | -------------------------------------------------------------------------------- /doc/img/Screenshot_ntangle_v0.4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrgTangle/ntangle/HEAD/doc/img/Screenshot_ntangle_v0.4.2.png -------------------------------------------------------------------------------- /tests/shebang/shebang.org: -------------------------------------------------------------------------------- 1 | #+begin_src shell :shebang "#!/usr/bin/env bash" :tangle "echo.sh" 2 | echo "Hello" 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/include/file3.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | #+begin_src emacs-lisp -n 4 | (message "3") 5 | #+end_src 6 | -------------------------------------------------------------------------------- /tests/mkdirp_yes/mkdirp_yes.org: -------------------------------------------------------------------------------- 1 | #+begin_src shell :tangle "./foo/bar/mkdirp_test.sh" :mkdirp yes 2 | echo "Hello" 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/property_drawer/foo.el: -------------------------------------------------------------------------------- 1 | (message "this will be tangled to foo.el") 2 | 3 | echo "this block will be tangled to foo.el too!" 4 | -------------------------------------------------------------------------------- /tests/property_drawer/foo/bar.el: -------------------------------------------------------------------------------- 1 | (message "this will be tangled to foo/bar.el") 2 | 3 | (message "second line tangled to foo/bar.el") 4 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | (nil 2 | . ((nil . ((indent-tabs-mode . nil) 3 | (fill-column . 70))) 4 | (org-mode . ((mode . auto-fill))))) 5 | -------------------------------------------------------------------------------- /tests/colon-in-header-arg-value/colon-in-header-arg-value.org: -------------------------------------------------------------------------------- 1 | #+begin_src nim :tangle hello.nim :flags -d:release 2 | echo "hello" 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/dmacs/README: -------------------------------------------------------------------------------- 1 | Source: 2 | - https://github.com/dakra/dmacs/blob/master/init.org 3 | - https://raw.githubusercontent.com/dakra/dmacs/master/init.org 4 | -------------------------------------------------------------------------------- /tests/tangle_mode/file_permissions_600_override_shebang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "permissions of this file will be 600 even though :shebang is used" 3 | -------------------------------------------------------------------------------- /tests/key_without_args_mkdir/key_without_args_mkdir.org: -------------------------------------------------------------------------------- 1 | #+begin_src sage :tangle do/not/exist/test.sage :session :mkdirp yes 2 | φ = var('φ') 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/missing_arg_value/missing_arg_value.org: -------------------------------------------------------------------------------- 1 | #+begin_src shell :tangle 2 | echo "This should error out as the :tangle header arg is missing a value." 3 | #+end_src 4 | -------------------------------------------------------------------------------- /tests/global_tangle/global_tangle.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to global_tangle.nim" 2 | 3 | echo "this will be tangled and appended to the first block tangled above" 4 | -------------------------------------------------------------------------------- /tests/property_drawer/property_drawer.nim: -------------------------------------------------------------------------------- 1 | echo "this will be tangled to property_drawer.nim" 2 | 3 | echo "this will the second block tangled to property_drawer.nim" 4 | -------------------------------------------------------------------------------- /tests/test1/test_passing.nim: -------------------------------------------------------------------------------- 1 | when isMainModule: 2 | import unittest 3 | suite "check number equality": 4 | 5 | test "1 == 1": 6 | check: 7 | 1 == 1 8 | -------------------------------------------------------------------------------- /tests/test1/test_failing.nim: -------------------------------------------------------------------------------- 1 | when isMainModule: 2 | import unittest 3 | suite "check number equality (failing)": 4 | 5 | test "1 == 0": 6 | check: 7 | 1 == 0 8 | -------------------------------------------------------------------------------- /tests/include/file1.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | #+begin_src emacs-lisp -n 4 | (message "1") 5 | #+end_src 6 | 7 | #+include: "./file2.org" 8 | #+include: "file3.org" 9 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/org-tangle-engine/lib_mini.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | 4 | use std::str::Lines; 5 | 6 | use std::fs; 7 | use std::io; 8 | use std::env; 9 | -------------------------------------------------------------------------------- /tests/invalid_arg_no_colon/invalid_arg_no_colon.org: -------------------------------------------------------------------------------- 1 | Below block should not be tangled. 2 | #+begin_src nim tangle yes 3 | echo "this source block has an invalid tangle arg, no colon" 4 | #+end_src 5 | -------------------------------------------------------------------------------- /tests/global_tangle/header_arg_inheritance2.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | #+begin_src emacs-lisp 4 | (message "hello 1") 5 | #+end_src 6 | 7 | #+begin_src emacs-lisp 8 | (message "hello 2") 9 | #+end_src 10 | -------------------------------------------------------------------------------- /tests/begin_src/begin_src.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | #+begin_src emacs-lisp -n 4 | (message "this will be tangled") 5 | #+end_src 6 | 7 | #+begin_src emacs-lisp -n :tangle no 8 | (message "this will not be tangled") 9 | #+end_src 10 | -------------------------------------------------------------------------------- /tests/global_tangle/header_arg_inheritance.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | #+begin_src emacs-lisp :tangle no 4 | (message "this will not be tangled") 5 | #+end_src 6 | 7 | #+begin_src emacs-lisp 8 | (message "this will be tangled") 9 | #+end_src 10 | -------------------------------------------------------------------------------- /tests/nested_src/tangled.org: -------------------------------------------------------------------------------- 1 | Nested Emacs-Lisp code block in an Org snippet: 2 | * Escaped Org heading 3 | #+begin_src emacs-lisp 4 | (message "hello") 5 | #+end_src 6 | *this comma will go away 7 | ,this comma remains as it's now followed by "#+" or "*" 8 | ,# and so this comma remains too 9 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | task pullConfig, "Fetch my global config.nims": 2 | exec("git submodule add -f -b master https://github.com/kaushalmodi/nim_config") 3 | when fileExists("nim_config/config.nims"): 4 | include "nim_config/config.nims" # This gives "nim musl", "nim test" and "nim docs" that's run on CI 5 | -------------------------------------------------------------------------------- /tests/global_tangle/global_tangle_lang_specific.org: -------------------------------------------------------------------------------- 1 | #+property: header-args:nim :tangle yes 2 | #+property: header-args :tangle no 3 | 4 | #+begin_src nim 5 | echo "this will be tangled to global_tangle.nim" 6 | #+end_src 7 | 8 | #+begin_src shell 9 | echo "this will not be tangled" 10 | #+end_src 11 | -------------------------------------------------------------------------------- /tests/nested_src/nested_src.nim: -------------------------------------------------------------------------------- 1 | let escapedOrgText = """ 2 | * Escaped Org heading 3 | #+begin_src emacs-lisp 4 | (message "hello") 5 | #+end_src 6 | """ 7 | 8 | let doubleEscapedOrgText = """ 9 | ,* Double Escaped Org heading 10 | ,#+begin_src emacs-lisp 11 | (message "hello") 12 | ,#+end_src 13 | """ 14 | -------------------------------------------------------------------------------- /ntangle.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.7.0" 4 | author = "Kaushal Modi" 5 | description = "Command-line utility for Tangling of Org mode documents" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["ntangle"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 0.19.6", "cligen >= 0.9.31" 13 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/engine_mini.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle org-tangle-engine/lib_mini.rs :mkdirp yes 2 | #+title: org-tangle-engine 3 | 4 | * use 5 | 6 | #+begin_src rust 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | 10 | use std::str::Lines; 11 | 12 | use std::fs; 13 | use std::io; 14 | use std::env; 15 | #+end_src 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # -*- conf -*- 2 | # https://github.com/github/linguist#using-gitattributes 3 | # Syntax highlight the .nims files using Nim syntax highlighter. 4 | *.nims linguist-language=Nim 5 | # https://github.com/github/linguist#vendored-code 6 | # Do not count the "vendored" files in the repo's language statistics. 7 | .dir-locals.el linguist-vendored 8 | tests/* linguist-vendored 9 | -------------------------------------------------------------------------------- /tests/tangle_no_yes/tangle_no_yes.org: -------------------------------------------------------------------------------- 1 | #+begin_src nim :tangle no 2 | echo "this block won't get tangled" 3 | #+end_src 4 | 5 | Below has extra spaces before ":tangle". But that should not matter. 6 | #+begin_src nim :tangle yes 7 | echo "this will be tangled to tangle_no_yes.nim" 8 | #+end_src 9 | 10 | #+begin_src nim :tangle specified_file.nim 11 | echo "this will be tangled to specified_file.nim" 12 | #+end_src 13 | -------------------------------------------------------------------------------- /tests/multiple_src_blocks_tangled_to_same_file/multiple_src_blocks_tangled_to_same_file.org: -------------------------------------------------------------------------------- 1 | #+begin_src nim :tangle hi.nim :padline no 2 | echo "hi 1" 3 | #+end_src 4 | 5 | #+begin_src nim :tangle hello.nim :padline no 6 | echo "hello 1" 7 | #+end_src 8 | 9 | #+begin_src nim :tangle hi.nim :padline no 10 | echo "hi 2" 11 | #+end_src 12 | 13 | #+begin_src nim :tangle hello.nim :padline no 14 | echo "hello 2" 15 | #+end_src 16 | -------------------------------------------------------------------------------- /tests/src_blocks_with_extra_indentation/src_blocks_with_extra_indentation.org: -------------------------------------------------------------------------------- 1 | Code blocks have extra indentation of 2 spaces. 2 | 3 | #+begin_src nim :tangle hi2.nim :padline no 4 | echo "hi 1" 5 | #+end_src 6 | 7 | #+begin_src nim :tangle hello2.nim :padline no 8 | echo "hello 1" 9 | #+end_src 10 | 11 | #+begin_src nim :tangle hi2.nim :padline no 12 | echo "hi 2" 13 | #+end_src 14 | 15 | #+begin_src nim :tangle hello2.nim :padline no 16 | echo "hello 2" 17 | let 18 | foo = "bar" 19 | #+end_src 20 | -------------------------------------------------------------------------------- /tests/tangle_mode/tangle_mode.org: -------------------------------------------------------------------------------- 1 | #+begin_src shell :tangle "file_permissions_755" :tangle-mode (identity #o755) 2 | echo "permissions of this file will be 755" 3 | #+end_src 4 | 5 | #+begin_src shell :tangle "file_permissions_644" :tangle-mode (identity #o644) 6 | echo "permissions of this file will be 644" 7 | #+end_src 8 | 9 | #+begin_src shell :tangle "file_permissions_600_override_shebang" :tangle-mode (identity #o600) :shebang "#!/usr/bin/env bash" 10 | echo "permissions of this file will be 600 even though :shebang is used" 11 | #+end_src 12 | -------------------------------------------------------------------------------- /tests/key_without_args/key_without_args.org: -------------------------------------------------------------------------------- 1 | The first one won't be tangled. 2 | #+begin_src sage :session 3 | x = var('x') 4 | #+end_src 5 | This one will be tangled. 6 | #+begin_src sage :session :tangle yes 7 | φ = var('φ') 8 | #+end_src 9 | This one will also be tangled. 10 | #+begin_src sage :tangle yes :session 11 | f(φ) = cos(φ) 12 | #+end_src 13 | And this again will be tangled. The =mkdirp= is a no-op here, as we 14 | do not give a directory. There is a separate test, which checks that 15 | the option works correctly. 16 | #+begin_src sage :tangle yes :session :mkdirp yes 17 | f(123) 18 | #+end_src 19 | 20 | -------------------------------------------------------------------------------- /tests/global_tangle/global_tangle.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | At Org level 0. 4 | 5 | #+begin_src nim 6 | echo "this will be tangled to global_tangle.nim" 7 | #+end_src 8 | 9 | #+begin_src nim :tangle no 10 | echo "this block won't get tangled" 11 | #+end_src 12 | * Heading 1 13 | Now at Org level 1. 14 | #+begin_src nim :tangle yes 15 | echo "this will be tangled and appended to the first block tangled above" 16 | #+end_src 17 | ** Heading 1.1 18 | Now at Org level 2. 19 | #+begin_src nim :tangle specified_file.nim 20 | echo "this will be tangled to specified_file.nim" 21 | #+end_src 22 | * Heading 2 23 | Back at Org level 1. 24 | #+begin_src emacs-lisp 25 | (message "this will be tangled to global_tangle.el") 26 | #+end_src 27 | -------------------------------------------------------------------------------- /tests/test1/test1.ORG: -------------------------------------------------------------------------------- 1 | #+title: No ob-nim output when unittest fails 2 | #+date: <2018-05-25 Fri> 3 | 4 | #+property: header-args :eval never-export 5 | #+property: header-args:nim :exports both :results output replace verbatim 6 | * Passing test 7 | #+begin_src nim :tangle test_passing.nim 8 | when isMainModule: 9 | import unittest 10 | suite "check number equality": 11 | 12 | test "1 == 1": 13 | check: 14 | 1 == 1 15 | #+end_src 16 | 17 | #+RESULTS: 18 | : 19 | : [Suite] check number equality 20 | : [OK] 1 == 1 21 | 22 | * Failing test 23 | #+begin_src nim :tangle test_failing.nim 24 | when isMainModule: 25 | import unittest 26 | suite "check number equality (failing)": 27 | 28 | test "1 == 0": 29 | check: 30 | 1 == 0 31 | #+end_src 32 | 33 | #+RESULTS: 34 | -------------------------------------------------------------------------------- /tests/nested_src/nested_src.org: -------------------------------------------------------------------------------- 1 | #+property: header-args:org :tangle tangled.org 2 | 3 | #+begin_src org 4 | Nested Emacs-Lisp code block in an Org snippet: 5 | ,* Escaped Org heading 6 | ,#+begin_src emacs-lisp 7 | (message "hello") 8 | ,#+end_src 9 | ,*this comma will go away 10 | ,this comma remains as it's now followed by "#+" or "*" 11 | ,# and so this comma remains too 12 | #+end_src 13 | 14 | The commas in below src block must also be removed: 15 | #+begin_src nim :tangle yes 16 | let escapedOrgText = """ 17 | ,* Escaped Org heading 18 | ,#+begin_src emacs-lisp 19 | (message "hello") 20 | ,#+end_src 21 | """ 22 | #+end_src 23 | 24 | Only one leading comma on each line in the below src block is removed: 25 | #+begin_src nim :tangle yes 26 | let doubleEscapedOrgText = """ 27 | ,,* Double Escaped Org heading 28 | ,,#+begin_src emacs-lisp 29 | (message "hello") 30 | ,,#+end_src 31 | """ 32 | #+end_src 33 | -------------------------------------------------------------------------------- /tests/quick_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Usage: ./quick_test.sh 3 | 4 | owner="OrgTangle" 5 | repo="ntangle" 6 | latest_release_url="$(curl -s "https://api.github.com/repos/${owner}/${repo}/releases/latest" | grep 'browser_download_url.*\.xz' | cut -d: -f2,3 | sed 's/[" ]//g')" 7 | archive_name="$(echo "${latest_release_url}" | rev | cut -d/ -f1 | rev)" 8 | temp_dir="/tmp/${USER}/${repo}/quick" 9 | test="tangle_no_yes" 10 | 11 | if [[ -d "${temp_dir}" ]] 12 | then 13 | rm -rf "${temp_dir}" 14 | fi 15 | mkdir -p "${temp_dir}" 16 | 17 | cd "${temp_dir}" || exit 18 | ls -la 19 | 20 | echo "" 21 | echo "Downloading ${latest_release_url} .." 22 | wget -nd "${latest_release_url}" 23 | tar xf "${archive_name}" 24 | 25 | echo "" 26 | echo "Downloading test ${test} .." 27 | wget -nd "https://raw.githubusercontent.com/${owner}/${repo}/master/tests/${test}/${test}.org" 28 | 29 | echo "" 30 | cmd="./${repo} ${test}.org" 31 | echo "Running ${cmd} .." 32 | eval "${cmd}" 33 | 34 | echo "" 35 | ls -la 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kaushal Modi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: RunTests 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - '*' 7 | push: 8 | schedule: 9 | # Every week: https://crontab.guru/#0_0_*_*_0 10 | - cron: '0 0 * * 0' 11 | 12 | jobs: 13 | run_tests: 14 | # runs-on: ubuntu-latest 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | nim: ['devel', 'version-1-4'] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: alaviss/setup-nim@0.1.1 23 | with: 24 | path: 'nim' 25 | version: ${{ matrix.nim }} 26 | - name: Pull kaushalmodi's global config.nims 27 | run: nim pullConfig 28 | - name: Run tests 29 | run: | 30 | # --accept to say Yes to prompts like "Prompt: No local packages.json found, download it from internet? [y/N]" 31 | nimble install --depsOnly --accept 32 | # Test installing PROGNAME using nimble. 33 | # --reject denies the offer to install from PROGNAME@#head if "nimble install PROGNAME" fails. 34 | nimble install --reject ntangle 35 | -------------------------------------------------------------------------------- /tests/property_drawer/property_drawer2.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | At Org level 0. 4 | 5 | * Heading 1 6 | :PROPERTIES: 7 | :header-args: :tangle foo.el 8 | :END: 9 | At Org level 1. 10 | #+name: block1 11 | #+begin_src emacs-lisp 12 | (message "this will be tangled to foo.el") 13 | #+end_src 14 | ** Heading 1.1 15 | :PROPERTIES: 16 | :header-args:emacs-lisp: :tangle no 17 | :END: 18 | At Org level 2. 19 | 20 | Only the emacs-lisp block will *not* be tangled from this subtree. 21 | #+name: block2 22 | #+begin_src emacs-lisp 23 | (message "this block will *not* be tangled") 24 | #+end_src 25 | 26 | But the below Nim block will tangle fine (though incorrectly to the 27 | foo.el file!). 28 | #+name: block3 29 | #+begin_src nim 30 | echo "this block will be tangled to foo.el too!" 31 | #+end_src 32 | 33 | - Note :: ~ntangle~ creates /block3/ *below* /block1/ in the tangled 34 | ~foo.el~, while ~org-bable-tangle~ reverses that order. It's 35 | not clear why that's the case with ~org-babel-tangle~ if 36 | /block3/ is appearing *after* /block1/ in this Org 37 | file. Assuming that that's a bug in Org mode, ~ntangle~ is 38 | not copying that behavior right now. <2018-10-07 Sun> 39 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/org-parser/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(uniform_paths)] 2 | #![allow(unused_parens)] 3 | #![allow(dead_code)] 4 | #![allow(unused_macros)] 5 | 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | // use std::sync::Arc; 9 | // use std::collections::HashMap; 10 | 11 | pub type Line = String; 12 | pub type LineVec = Vec ; 13 | pub type ElementVec = Vec ; 14 | pub type PropertyVec = Vec ; 15 | 16 | pub enum Element { 17 | N (Node), 18 | B (Block), 19 | T (Text), 20 | L (List), 21 | } 22 | 23 | pub struct Root { 24 | path: Option , 25 | property_vec: PropertyVec, 26 | body: ElementVec, 27 | } 28 | 29 | pub struct Node { 30 | headline: Line, 31 | property_vec: PropertyVec, 32 | body: ElementVec, 33 | } 34 | 35 | pub struct List { 36 | list_type: ListType, 37 | content: Vec <(Text, Option )>, 38 | } 39 | 40 | pub enum ListType { 41 | DashMark, 42 | PlusMark, 43 | Numbered, 44 | } 45 | 46 | pub struct Text { 47 | line_vec: LineVec, 48 | } 49 | 50 | pub struct Block { 51 | block_type: BlockType, 52 | property_vec: PropertyVec, 53 | line_vec: LineVec, 54 | } 55 | 56 | pub enum BlockType { 57 | CodeBlock, 58 | } 59 | 60 | pub struct Property { 61 | name: String, 62 | value: String, 63 | } 64 | -------------------------------------------------------------------------------- /tests/property_drawer/property_drawer.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle yes 2 | 3 | At Org level 0. 4 | 5 | #+begin_src nim 6 | echo "this will be tangled to property_drawer.nim" 7 | #+end_src 8 | 9 | #+begin_src nim :tangle no 10 | echo "this block won't get tangled" 11 | #+end_src 12 | 13 | * Heading 1 14 | :PROPERTIES: 15 | :HEADER-ARGS: :tangle no 16 | :END: 17 | Below block will also *not* be tangled. This subtree also tests the 18 | ~header-args~ property in all-caps. 19 | #+begin_src nim 20 | echo "hello" 21 | #+end_src 22 | ** Heading 1.1 23 | Now at Org level 2. 24 | #+begin_src nim :tangle specified_file.nim 25 | echo "this will be tangled to specified_file.nim" 26 | #+end_src 27 | * Heading 2 28 | :PROPERTIES: 29 | :header-args: :tangle foo/bar.el 30 | :header-args+: :mkdirp yes 31 | :END: 32 | Back at Org level 1. 33 | #+begin_src emacs-lisp 34 | (message "this will be tangled to foo/bar.el") 35 | #+end_src 36 | ** Heading 2.1 37 | :PROPERTIES: 38 | :header-args:nim: :tangle no 39 | :END: 40 | Now again at Org level 2. 41 | 42 | Only the nim blocks will *not* be tangled from this subtree. 43 | #+begin_src nim 44 | echo "this will *not* be tangled" 45 | #+end_src 46 | 47 | But the below block will tangle fine. 48 | #+begin_src emacs-lisp 49 | (message "second line tangled to foo/bar.el") 50 | #+end_src 51 | *** Heading 2.1.1 52 | :PROPERTIES: 53 | :header-args:nim: :tangle yes 54 | :END: 55 | Now at Org level 3. 56 | 57 | Below nim block will be tangled. 58 | #+begin_src nim 59 | echo "this will the second block tangled to property_drawer.nim" 60 | #+end_src 61 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Time-stamp: <2018-10-13 12:16:45 kmodi> 3 | 4 | set -euo pipefail # http://redsymbol.net/articles/unofficial-bash-strict-mode 5 | IFS=$'\n\t' 6 | 7 | repo_root="$(git rev-parse --show-toplevel)" 8 | 9 | run_test () { 10 | bin="$1" 11 | "${bin}" tests/test1/test1.ORG tests/tangle_no_yes/tangle_no_yes.org # Test multiple Org files as arguments 12 | "${bin}" tests/src_blocks_with_extra_indentation/ tests/multiple_src_blocks_tangled_to_same_file 13 | "${bin}" tests/wyag/write-yourself-a-git.org 14 | "${bin}" tests/shebang/shebang.org 15 | "${bin}" tests/global_tangle/ 16 | "${bin}" tests/org_tangle_rs/ 17 | "${bin}" tests/nested_src/ 18 | "${bin}" tests/property_drawer/ 19 | "${bin}" tests/dmacs/ 20 | "${bin}" tests/begin_src/ 21 | "${bin}" tests/key_without_args/ 22 | 23 | "${bin}" tests/missing_arg_value/missing_arg_value.org || true 24 | "${bin}" tests/invalid_arg_no_colon/ || true 25 | 26 | TEMP_HOME="${repo_root}/tests/mkdirp_no" 27 | rm -rf "${TEMP_HOME}/foo/" 28 | HOME="${TEMP_HOME}" "${bin}" tests/mkdirp_no/mkdirp_no.org || true 29 | 30 | rm -rf "${repo_root}/tests/mkdirp_yes/foo/" 31 | "${bin}" tests/mkdirp_yes/mkdirp_yes.org 32 | 33 | rm -rf "${repo_root}/tests/key_without_args_mkdir/do/" 34 | "${bin}" tests/key_without_args_mkdir/ 35 | 36 | # Test tangling an Org file in the same dir. 37 | cd tests/tangle_mode || exit 38 | "../../${bin}" tangle_mode.org 39 | 40 | # "${bin}" tests/eless/eless.org 41 | } 42 | 43 | # Regular build 44 | cd "${repo_root}" 45 | nimble build -d:release 46 | run_test "./ntangle" 47 | 48 | # musl build 49 | cd "${repo_root}" 50 | nim musl src/ntangle.nim 51 | run_test "./src/ntangle" 52 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/lib.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle org-parser/lib.rs :mkdirp yes 2 | #+title: org-parser 3 | 4 | * [todo-stack] 5 | 6 | - learn basic parsing-techniques 7 | - learn combine 8 | https://github.com/Marwes/combine 9 | 10 | * attribute 11 | 12 | #+begin_src rust 13 | #![feature(uniform_paths)] 14 | #![allow(unused_parens)] 15 | #![allow(dead_code)] 16 | #![allow(unused_macros)] 17 | #+end_src 18 | 19 | * use 20 | 21 | #+begin_src rust 22 | use std::path::Path; 23 | use std::path::PathBuf; 24 | // use std::sync::Arc; 25 | // use std::collections::HashMap; 26 | #+end_src 27 | 28 | * datatype 29 | 30 | #+begin_src rust 31 | pub type Line = String; 32 | pub type LineVec = Vec ; 33 | pub type ElementVec = Vec ; 34 | pub type PropertyVec = Vec ; 35 | #+end_src 36 | 37 | * Element 38 | 39 | #+begin_src rust 40 | pub enum Element { 41 | N (Node), 42 | B (Block), 43 | T (Text), 44 | L (List), 45 | } 46 | #+end_src 47 | 48 | * Root 49 | 50 | #+begin_src rust 51 | pub struct Root { 52 | path: Option , 53 | property_vec: PropertyVec, 54 | body: ElementVec, 55 | } 56 | #+end_src 57 | 58 | * Node 59 | 60 | #+begin_src rust 61 | pub struct Node { 62 | headline: Line, 63 | property_vec: PropertyVec, 64 | body: ElementVec, 65 | } 66 | #+end_src 67 | 68 | * List 69 | 70 | #+begin_src rust 71 | pub struct List { 72 | list_type: ListType, 73 | content: Vec <(Text, Option )>, 74 | } 75 | 76 | pub enum ListType { 77 | DashMark, 78 | PlusMark, 79 | Numbered, 80 | } 81 | #+end_src 82 | 83 | * Text 84 | 85 | #+begin_src rust 86 | pub struct Text { 87 | line_vec: LineVec, 88 | } 89 | #+end_src 90 | 91 | * Block 92 | 93 | #+begin_src rust 94 | pub struct Block { 95 | block_type: BlockType, 96 | property_vec: PropertyVec, 97 | line_vec: LineVec, 98 | } 99 | 100 | pub enum BlockType { 101 | CodeBlock, 102 | } 103 | #+end_src 104 | 105 | * Property 106 | 107 | #+begin_src rust 108 | pub struct Property { 109 | name: String, 110 | value: String, 111 | } 112 | #+end_src 113 | -------------------------------------------------------------------------------- /tests/wyag/wyag-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #<> 3 | LEFT=$(pwd)/wyag 4 | RIGHT=git 5 | 6 | LEFT_CWD="" 7 | RIGHT_CWD="" 8 | 9 | RUN=0 10 | ERROR=0 11 | LOGFILE=$(mktemp) 12 | 13 | function test_setup() { 14 | true; 15 | } 16 | 17 | function test_begin() { 18 | let RUN+=1 19 | 20 | echo -n "" > $LOGFILE 21 | test_log "begin #$RUN: $1..." 22 | 23 | LEFT_CWD=$(mktemp -d --tmpdir ${RUN}_LEFT.XXXX) 24 | RIGHT_CWD=$(mktemp -d --tmpdir ${RUN}_RIGHT.XXXX) 25 | 26 | test_setup 27 | } 28 | 29 | function test_log() { 30 | echo -e "\t$@" >> $LOGFILE 31 | } 32 | 33 | function test_run() { 34 | # Run $@ 35 | 36 | test_log "run $@" 37 | 38 | cd $LEFT_CWD 39 | $LEFT $@ > /dev/null 2> /dev/null 40 | LEFT_ERR=$? 41 | 42 | cd $RIGHT_CWD 43 | $RIGHT $@ > /dev/null 2> /dev/null 44 | RIGHT_ERR=$? 45 | 46 | DIFF=$(diff -arq $LEFT_CWD $RIGHT_CWD) 47 | DIFF_ERR=$? 48 | 49 | if [[ $LEFT_ERR != $RIGHT_ERR ]]; then 50 | let ERROR+=1 51 | test_log "Return codes don't match: ($LEFT_ERR != $RIGHT_ERR)."; 52 | fi 53 | } 54 | 55 | function assert_equivalent() { 56 | # Run $@ on LEFT_CWD and RIGHT_CWD and check that error code and 57 | # stdout are identical 58 | 59 | test_log assert_equivalent $@ 60 | 61 | cd $LEFT_CWD 62 | LEFT_LOG=$($@) > /dev/null 2>/dev/null 63 | LEFT_ERR=$? 64 | 65 | cd $RIGHT_CWD 66 | RIGHT_LOG=$($@) 2>/dev/null 67 | RIGHT_ERR=$? 68 | 69 | if [[ $LEFT_ERR != $RIGHT_ERR ]]; then 70 | let FAILED+=1 71 | test_log "Return codes don't match: ($LEFT_ERR != $RIGHT_ERR)." 72 | fi 73 | if [[ $LEFT_LOG != $RIGHT_LOG ]]; then 74 | let FAILED+=1 75 | test_log "Stdouts differ." 76 | fi 77 | } 78 | 79 | function test_done() { 80 | if [[ $ERROR != 0 ]]; then 81 | echo -e "#${RUN}\tFAILED" 82 | cat $LOGFILE 83 | else 84 | echo -e "#${RUN} ok" 85 | fi 86 | 87 | rm -rf $LEFT_CWD 88 | rm -rf $RIGHT_CWD 89 | } 90 | 91 | 92 | 93 | # Test #1: git init [vs] wyag init 94 | test_begin "Create a new repo" 95 | test_run init 96 | assert_equivalent git status --porcelain=v2 --branch 97 | test_done 98 | 99 | # Test #2: git init test [vs] wyag init test 100 | test_begin "Create a new repo in a different directory" 101 | test_run init test 102 | assert_equivalent git -C test status --porcelain=v2 --branch 103 | test_done 104 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/org-tangle-engine/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | 4 | use std::str::Lines; 5 | 6 | use std::fs; 7 | use std::io; 8 | use std::env; 9 | 10 | #[derive(Debug)] 11 | struct TangleError { 12 | report: String, 13 | } 14 | 15 | impl TangleError { 16 | fn new (report: &str) -> Self { 17 | TangleError { 18 | report: report.to_string (), 19 | } 20 | } 21 | } 22 | 23 | const DESTINATION_PREFIX: &'static str = "#+property: tangle "; 24 | 25 | fn destination_line_p (line: &str) -> bool { 26 | line .trim_start () .starts_with (DESTINATION_PREFIX) 27 | } 28 | 29 | fn find_destination (string: &str) -> Option { 30 | for line in string.lines () { 31 | if destination_line_p (line) { 32 | let destination = &line [DESTINATION_PREFIX.len () ..]; 33 | let destination = destination.trim (); 34 | return Some (destination.to_string ()); 35 | } 36 | } 37 | None 38 | } 39 | 40 | #[test] 41 | fn test_find_destination () { 42 | let example = "#+property: tangle core.rs"; 43 | let destination = find_destination (example) .unwrap (); 44 | assert_eq! (destination, "core.rs"); 45 | } 46 | 47 | const BLOCK_BEGIN: &'static str = "#+begin_src "; 48 | const BLOCK_END: &'static str = "#+end_src"; 49 | 50 | fn block_begin_line_p (line: &str) -> bool { 51 | line .trim_start () .starts_with (BLOCK_BEGIN) 52 | } 53 | 54 | fn block_end_line_p (line: &str) -> bool { 55 | line .trim_start () .starts_with (BLOCK_END) 56 | } 57 | 58 | fn tangle_collect ( 59 | result: &mut String, 60 | lines: &mut Lines, 61 | ) -> Result <(), TangleError> { 62 | for line in lines { 63 | if block_end_line_p (line) { 64 | return Ok (()); 65 | } else { 66 | result.push_str (line); 67 | result.push ('\n'); 68 | } 69 | } 70 | let error = TangleError::new ("block_end mismatch"); 71 | Err (error) 72 | } 73 | 74 | fn tangle (string: &str) -> Result { 75 | let mut result = String::new (); 76 | let mut lines = string.lines (); 77 | while let Some (line) = lines.next () { 78 | if block_begin_line_p (line) { 79 | tangle_collect (&mut result, &mut lines)?; 80 | } 81 | } 82 | Ok (result) 83 | } 84 | 85 | #[test] 86 | fn test_tangle () { 87 | let example = format! ( 88 | "{}\n{}\n{}\n{}\n", 89 | "#+begin_src rust", 90 | "hi", 91 | "hi", 92 | "#+end_src", 93 | ); 94 | let expect = format! ( 95 | "{}\n{}\n", 96 | "hi", 97 | "hi", 98 | ); 99 | let result = tangle (&example) .unwrap (); 100 | assert_eq! (expect, result); 101 | let example = format! ( 102 | "{}\n{}\n{}\n{}\n", 103 | " #+begin_src rust", 104 | " hi", 105 | " hi", 106 | " #+end_src", 107 | ); 108 | let expect = format! ( 109 | "{}\n{}\n", 110 | " hi", 111 | " hi", 112 | ); 113 | let result = tangle (&example) .unwrap (); 114 | assert_eq! (expect, result); 115 | } 116 | 117 | fn good_path_p (path: &Path) -> bool { 118 | for component in path.iter () { 119 | if let Some (string) = component.to_str () { 120 | if string.starts_with ('.') { 121 | if ! string .chars () .all (|x| x == '.') { 122 | return false; 123 | } 124 | } 125 | } else { 126 | return false; 127 | } 128 | } 129 | true 130 | } 131 | 132 | pub fn org_file_p (file: &Path) -> bool { 133 | if let Some (os_string) = file.extension () { 134 | if let Some (string) = os_string.to_str () { 135 | string == "org" 136 | } else { 137 | false 138 | } 139 | } else { 140 | false 141 | } 142 | } 143 | 144 | pub fn file_tangle (file: &Path) -> io::Result <()> { 145 | if ! org_file_p (file) { 146 | return Ok (()); 147 | } 148 | println! ("- tangle : {:?}", file); 149 | let string = fs::read_to_string (file)?; 150 | if let Some (destination) = find_destination (&string) { 151 | let result = tangle (&string) .unwrap (); 152 | let mut destination_path = PathBuf::new (); 153 | destination_path.push (file); 154 | destination_path.pop (); 155 | destination_path.push (destination); 156 | fs::write (&destination_path, result) 157 | } else { 158 | Ok (()) 159 | } 160 | } 161 | 162 | pub fn dir_tangle (dir: &Path) -> io::Result <()> { 163 | for entry in dir.read_dir ()? { 164 | if let Ok (entry) = entry { 165 | if good_path_p (&entry.path ()) { 166 | if entry.file_type ()? .is_file () { 167 | file_tangle (&entry.path ())? 168 | } 169 | } 170 | } 171 | } 172 | Ok (()) 173 | } 174 | 175 | pub fn dir_tangle_rec (dir: &Path) -> io::Result <()> { 176 | for entry in dir.read_dir ()? { 177 | if let Ok (entry) = entry { 178 | if good_path_p (&entry.path ()) { 179 | if entry.file_type ()? .is_file () { 180 | file_tangle (&entry.path ())? 181 | } else if entry.file_type ()? .is_dir () { 182 | dir_tangle_rec (&entry.path ())? 183 | } 184 | } 185 | } 186 | } 187 | Ok (()) 188 | } 189 | 190 | pub fn absolute_lize (path: &Path) -> PathBuf { 191 | if path.is_relative () { 192 | let mut absolute_path = env::current_dir () .unwrap (); 193 | absolute_path.push (path); 194 | absolute_path 195 | } else { 196 | path.to_path_buf () 197 | } 198 | } 199 | 200 | pub fn tangle_all_before_build () -> io::Result <()> { 201 | let path = Path::new ("."); 202 | let current_dir = env::current_dir () .unwrap (); 203 | println! ("- org_tangle_engine"); 204 | println! (" tangle_all_before_build"); 205 | println! (" current_dir : {:?}", current_dir); 206 | let path = absolute_lize (&path); 207 | dir_tangle_rec (&path) 208 | } 209 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+title: NTangle 2 | 3 | /Command-line utility for Tangling of Org documents — programmed in 4 | Nim./ 5 | 6 | [[https://github.com/OrgTangle/ntangle/actions/workflows/test.yml][https://github.com/OrgTangle/ntangle/actions/workflows/test.yml/badge.svg]] 7 | 8 | * What is Tangling 9 | From [[https://orgmode.org/manual/Extracting-source-code.html][Org Manual -- Extracting source code]]: 10 | 11 | #+begin_quote 12 | Extracting source code from code blocks is a basic task in /literate 13 | programming/. Org has features to make this easy. In literate 14 | programming parlance, documents on creation are /woven/ with code and 15 | documentation, and on export, the code is *tangled* for execution by a 16 | computer. 17 | 18 | Org facilitates weaving and tangling for producing, maintaining, 19 | sharing, and exporting literate programming documents. Org provides 20 | extensive customization options for extracting source code. 21 | 22 | When Org tangles ~src~ code blocks, it expands, merges, and transforms 23 | them. Then Org recomposes them into one or more separate files, as 24 | configured through the options. During this /tangling/ process, Org 25 | expands variables in the source code, and resolves any Noweb style 26 | references (see [[https://orgmode.org/manual/Noweb-reference-syntax.html][Noweb reference syntax]]). 27 | #+end_quote 28 | 29 | /You can visit the same Org manual section from within Emacs by going 30 | to this Info manual node: ~(org) Extracting Source Code~./ 31 | * Why ~ntangle~? 32 | While the tangling of Org documents works great from within Emacs, it 33 | can be quite a bit slow for large Org documents. This project aims to 34 | provide /almost/ the same tangling functionality at a much higher 35 | speed. 36 | 37 | You do *not* need Emacs or Org mode installed to use ~ntangle~. 38 | * Installation 39 | 40 | [[https://github.com/nim-lang/nimble][~nimble~]] can be used to install ~ntangle~. This utility ships with Nim 41 | installations. Think of it as the equivalent of the Python ~pip~. 42 | 43 | /See [[https://github.com/dom96/choosenim#installation][*choosenim* installation]] to install *nim* and *nimble*./ 44 | 45 | To install ~ntangle~: 46 | #+begin_example 47 | nimble install ntangle 48 | #+end_example 49 | 50 | Above installs the binary in the *~/.nimble/bin/* directory. 51 | * Features 52 | ** Tangling /minus Noweb/ [7/8] 53 | - [X] Understands global tangle header-args in ~#+property~ keywords 54 | - [X] Understands tangle header-args directly above Org source blocks 55 | - [-] Understands tangle subtree property drawer header-args 56 | - [X] Rudimentary parsing of the property drawer 57 | header-args. ~ntangle~ does not check the validity of the property 58 | drawer (i.e. ~:PROPERTIES:~ appears immediately after the heading, 59 | and it ends with ~:END:~, and that the properties are only between 60 | them, etc.). Also ~header-args~ is treated the same as 61 | ~header-args+~ for simplicity for now. 62 | - [ ] Treat ~header-args~ vs ~header-args+~ values correctly. 63 | - [X] Understands the precendence order of the tangle header-args when 64 | a mix of global and source block header-args are used 65 | - [X] Language-specific header-args (e.g. ~#+property: header-args:nim 66 | :tangle yes~) 67 | - [X] Concatenating multiple source blocks to the same tangled file 68 | - [X] Respects comma-escaping in source blocks 69 | - [X] Removes common indentation, but also retains the indentation 70 | where needed 71 | *** Supported ~header-args~ switches [5/7] 72 | - [X] ~:tangle~ 73 | - [X] ~:padline~ 74 | - [X] ~:shebang~ 75 | - [X] ~:mkdirp~ 76 | - [X] ~:tangle-mode~ 77 | - [ ] ~:comments~ 78 | - [ ] ~:no-expand~ 79 | ** Noweb [0/1] 80 | - [ ] Noweb support. I sorely miss the lack of ~noweb~ support.. I use 81 | it heavily in [[https://github.com/kaushalmodi/eless][~eless~]]. 82 | 83 | /There is *no* plan to support Org Babel functions with Noweb, like 84 | evaluating code blocks during tangling, etc. (just use Emacs for those 85 | :D)./ 86 | *** Supported ~header-args~ switches for Noweb [0/3] 87 | - [ ] ~:noweb~ 88 | - [ ] ~:noweb-ref~ 89 | - [ ] ~:noweb-sep~ 90 | * Screenshot 91 | [[https://raw.githubusercontent.com/OrgTangle/ntangle/master/doc/img/Screenshot_ntangle_v0.4.2.png][https://raw.githubusercontent.com/OrgTangle/ntangle/master/doc/img/Screenshot_ntangle_v0.4.2.png]] 92 | * Usage 93 | Add one or more Org files (files with names ending in ".org") or 94 | directory names after the ~ntangle~ command. If directory names are 95 | added, only the files in there with names ending with ".org" will be 96 | parsed. 97 | #+begin_example 98 | ntangle 99 | #+end_example 100 | 101 | or a list of files: 102 | 103 | #+begin_example 104 | ntangle .. 105 | #+end_example 106 | 107 | or a list of directories: 108 | 109 | #+begin_example 110 | ntangle .. 111 | #+end_example 112 | 113 | or a mix of lists of files and directories: 114 | 115 | #+begin_example 116 | ntangle .. 117 | #+end_example 118 | 119 | The tangled files will be created in paths relative to the source Org 120 | file. 121 | * Org mode file samples for tangling 122 | You can find samples of the supported Org mode tangling in the [[https://github.com/OrgTangle/ntangle/tree/master/tests][*test* 123 | directory]] of this project. 124 | * Org Tangle Syntax 125 | ~ntangle~ expects the Org files to use the ~header-args~ property 126 | syntax used in Org mode 9.0 and newer. There was a minor syntax change 127 | with *header-args* property in Org 9.0 ([[https://code.orgmode.org/bzg/org-mode/src/a85ba9fb9bc7518bc0b654c79812f5606be84c58/etc/ORG-NEWS#L1042][see ORG-NEWS]]). 128 | 129 | So if you used the below in Org 8.x and older: 130 | #+begin_src org 131 | # Deprecated syntax 132 | ,#+property: tangle yes 133 | #+end_src 134 | 135 | Refactor that to: 136 | #+begin_src org 137 | # Org 9.0 syntax 138 | ,#+property: header-args :tangle yes 139 | #+end_src 140 | 141 | Similarly, refactor a property drawer from: 142 | #+begin_src org 143 | # Deprecated syntax 144 | ,* Some heading 145 | :PROPERTIES: 146 | :tangle: yes 147 | :END: 148 | #+end_src 149 | 150 | To: 151 | #+begin_src org 152 | # Org 9.0 syntax 153 | ,* Some heading 154 | :PROPERTIES: 155 | :header-args: :tangle yes 156 | :END: 157 | #+end_src 158 | * Development 159 | Below assumes that you have ~nim~ and ~nimble~ installed. 160 | ** Building 161 | #+begin_example 162 | git clone https://github.com/OrgTangle/ntangle 163 | cd ntangle 164 | nimble build # creates the ntangle binary in the same directory 165 | # nimble build -d:release # same as above but creates an optimized binary 166 | #+end_example 167 | ** Testing 168 | #+begin_src shell :results output verbatim 169 | # cd to the git repo dir 170 | ./tests/test.sh 171 | #+end_src 172 | * History 173 | I was [[https://www.reddit.com/r/emacs/comments/8m5wuf/a_python_version_of_orgbabeltangle_for_literate/dzl3ooo/][motivated]] to start this project after reading about the 174 | [[https://github.com/OrgTangle/org-babel-tangle.py][~org-babel-tangle.py~]] Python project by @thblt. 175 | 176 | I wanted to just see how easy it was to translate a Python script to 177 | Nim (it was very easy!), and from there, this project started 178 | snowballing, gathering features of its own :). 179 | * Other Org tangling implementations 180 | See [[https://github.com/OrgTangle]]. 181 | -------------------------------------------------------------------------------- /tests/org_tangle_rs/engine.org: -------------------------------------------------------------------------------- 1 | #+property: header-args :tangle org-tangle-engine/lib.rs :mkdirp yes 2 | #+title: org-tangle-engine 3 | 4 | * use 5 | 6 | #+begin_src rust 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | 10 | use std::str::Lines; 11 | 12 | use std::fs; 13 | use std::io; 14 | use std::env; 15 | #+end_src 16 | 17 | * TangleError 18 | 19 | #+begin_src rust 20 | #[derive(Debug)] 21 | struct TangleError { 22 | report: String, 23 | } 24 | 25 | impl TangleError { 26 | fn new (report: &str) -> Self { 27 | TangleError { 28 | report: report.to_string (), 29 | } 30 | } 31 | } 32 | #+end_src 33 | 34 | * find_destination 35 | 36 | #+begin_src rust 37 | const DESTINATION_PREFIX: &'static str = "#+property: tangle "; 38 | 39 | fn destination_line_p (line: &str) -> bool { 40 | line .trim_start () .starts_with (DESTINATION_PREFIX) 41 | } 42 | 43 | fn find_destination (string: &str) -> Option { 44 | for line in string.lines () { 45 | if destination_line_p (line) { 46 | let destination = &line [DESTINATION_PREFIX.len () ..]; 47 | let destination = destination.trim (); 48 | return Some (destination.to_string ()); 49 | } 50 | } 51 | None 52 | } 53 | 54 | #[test] 55 | fn test_find_destination () { 56 | let example = "#+property: tangle core.rs"; 57 | let destination = find_destination (example) .unwrap (); 58 | assert_eq! (destination, "core.rs"); 59 | } 60 | #+end_src 61 | 62 | * tangle 63 | 64 | *** line predicates 65 | 66 | #+begin_src rust 67 | const BLOCK_BEGIN: &'static str = "#+begin_src "; 68 | const BLOCK_END: &'static str = "#+end_src"; 69 | 70 | fn block_begin_line_p (line: &str) -> bool { 71 | line .trim_start () .starts_with (BLOCK_BEGIN) 72 | } 73 | 74 | fn block_end_line_p (line: &str) -> bool { 75 | line .trim_start () .starts_with (BLOCK_END) 76 | } 77 | #+end_src 78 | 79 | *** tangle_collect 80 | 81 | #+begin_src rust 82 | fn tangle_collect ( 83 | result: &mut String, 84 | lines: &mut Lines, 85 | ) -> Result <(), TangleError> { 86 | for line in lines { 87 | if block_end_line_p (line) { 88 | return Ok (()); 89 | } else { 90 | result.push_str (line); 91 | result.push ('\n'); 92 | } 93 | } 94 | let error = TangleError::new ("block_end mismatch"); 95 | Err (error) 96 | } 97 | #+end_src 98 | 99 | *** tangle 100 | 101 | #+begin_src rust 102 | fn tangle (string: &str) -> Result { 103 | let mut result = String::new (); 104 | let mut lines = string.lines (); 105 | while let Some (line) = lines.next () { 106 | if block_begin_line_p (line) { 107 | tangle_collect (&mut result, &mut lines)?; 108 | } 109 | } 110 | Ok (result) 111 | } 112 | #+end_src 113 | 114 | *** test_tangle 115 | 116 | #+begin_src rust 117 | #[test] 118 | fn test_tangle () { 119 | let example = format! ( 120 | "{}\n{}\n{}\n{}\n", 121 | "#+begin_src rust", 122 | "hi", 123 | "hi", 124 | "#+end_src", 125 | ); 126 | let expect = format! ( 127 | "{}\n{}\n", 128 | "hi", 129 | "hi", 130 | ); 131 | let result = tangle (&example) .unwrap (); 132 | assert_eq! (expect, result); 133 | let example = format! ( 134 | "{}\n{}\n{}\n{}\n", 135 | " #+begin_src rust", 136 | " hi", 137 | " hi", 138 | " #+end_src", 139 | ); 140 | let expect = format! ( 141 | "{}\n{}\n", 142 | " hi", 143 | " hi", 144 | ); 145 | let result = tangle (&example) .unwrap (); 146 | assert_eq! (expect, result); 147 | } 148 | #+end_src 149 | 150 | * interface 151 | 152 | *** good_path_p 153 | 154 | #+begin_src rust 155 | fn good_path_p (path: &Path) -> bool { 156 | for component in path.iter () { 157 | if let Some (string) = component.to_str () { 158 | if string.starts_with ('.') { 159 | if ! string .chars () .all (|x| x == '.') { 160 | return false; 161 | } 162 | } 163 | } else { 164 | return false; 165 | } 166 | } 167 | true 168 | } 169 | #+end_src 170 | 171 | *** org_file_p 172 | 173 | #+begin_src rust 174 | pub fn org_file_p (file: &Path) -> bool { 175 | if let Some (os_string) = file.extension () { 176 | if let Some (string) = os_string.to_str () { 177 | string == "org" 178 | } else { 179 | false 180 | } 181 | } else { 182 | false 183 | } 184 | } 185 | #+end_src 186 | 187 | *** file_tangle 188 | 189 | #+begin_src rust 190 | pub fn file_tangle (file: &Path) -> io::Result <()> { 191 | if ! org_file_p (file) { 192 | return Ok (()); 193 | } 194 | println! ("- tangle : {:?}", file); 195 | let string = fs::read_to_string (file)?; 196 | if let Some (destination) = find_destination (&string) { 197 | let result = tangle (&string) .unwrap (); 198 | let mut destination_path = PathBuf::new (); 199 | destination_path.push (file); 200 | destination_path.pop (); 201 | destination_path.push (destination); 202 | fs::write (&destination_path, result) 203 | } else { 204 | Ok (()) 205 | } 206 | } 207 | #+end_src 208 | 209 | *** dir_tangle 210 | 211 | #+begin_src rust 212 | pub fn dir_tangle (dir: &Path) -> io::Result <()> { 213 | for entry in dir.read_dir ()? { 214 | if let Ok (entry) = entry { 215 | if good_path_p (&entry.path ()) { 216 | if entry.file_type ()? .is_file () { 217 | file_tangle (&entry.path ())? 218 | } 219 | } 220 | } 221 | } 222 | Ok (()) 223 | } 224 | #+end_src 225 | 226 | *** dir_tangle_rec 227 | 228 | #+begin_src rust 229 | pub fn dir_tangle_rec (dir: &Path) -> io::Result <()> { 230 | for entry in dir.read_dir ()? { 231 | if let Ok (entry) = entry { 232 | if good_path_p (&entry.path ()) { 233 | if entry.file_type ()? .is_file () { 234 | file_tangle (&entry.path ())? 235 | } else if entry.file_type ()? .is_dir () { 236 | dir_tangle_rec (&entry.path ())? 237 | } 238 | } 239 | } 240 | } 241 | Ok (()) 242 | } 243 | #+end_src 244 | 245 | *** absolute_lize 246 | 247 | #+begin_src rust 248 | pub fn absolute_lize (path: &Path) -> PathBuf { 249 | if path.is_relative () { 250 | let mut absolute_path = env::current_dir () .unwrap (); 251 | absolute_path.push (path); 252 | absolute_path 253 | } else { 254 | path.to_path_buf () 255 | } 256 | } 257 | #+end_src 258 | 259 | *** tangle_all_before_build 260 | 261 | #+begin_src rust 262 | pub fn tangle_all_before_build () -> io::Result <()> { 263 | let path = Path::new ("."); 264 | let current_dir = env::current_dir () .unwrap (); 265 | println! ("- org_tangle_engine"); 266 | println! (" tangle_all_before_build"); 267 | println! (" current_dir : {:?}", current_dir); 268 | let path = absolute_lize (&path); 269 | dir_tangle_rec (&path) 270 | } 271 | #+end_src 272 | -------------------------------------------------------------------------------- /tests/wyag/libwyag.py: -------------------------------------------------------------------------------- 1 | #<> 2 | import argparse 3 | 4 | import collections 5 | 6 | import configparser 7 | 8 | import hashlib 9 | 10 | import os 11 | 12 | import re 13 | 14 | import sys 15 | 16 | import zlib 17 | 18 | argparser = argparse.ArgumentParser(description="The stupid content tracker") 19 | 20 | argsubparsers = argparser.add_subparsers(title="Commands", dest="command") 21 | argsubparsers.required = True 22 | 23 | def main(argv=sys.argv[1:]): 24 | args = argparser.parse_args(argv) 25 | 26 | if args.command == "add" : cmd_add(args) 27 | elif args.command == "cat-file" : cmd_cat_file(args) 28 | elif args.command == "checkout" : cmd_checkout(args) 29 | elif args.command == "commit" : cmd_commit(args) 30 | elif args.command == "hash-object" : cmd_hash_object(args) 31 | elif args.command == "init" : cmd_init(args) 32 | elif args.command == "log" : cmd_log(args) 33 | elif args.command == "ls-tree" : cmd_ls_tree(args) 34 | elif args.command == "merge" : cmd_merge(args) 35 | elif args.command == "rebase" : cmd_rebase(args) 36 | elif args.command == "rm" : cmd_rm(args) 37 | elif args.command == "show-ref" : cmd_show_ref(args) 38 | elif args.command == "tag" : cmd_tag(args) 39 | 40 | class repository(object): 41 | """A git repository""" 42 | 43 | worktree = None 44 | gitdir = None 45 | conf = None 46 | 47 | def __init__(self, path, force=False): 48 | self.worktree = path 49 | self.gitdir = os.path.join(path, ".git") 50 | 51 | if not (force or os.path.isdir(self.gitdir)): 52 | raise Exception("Not a Git repository %s" % path) 53 | 54 | # Read configuration file in .git/config 55 | self.conf = configparser.ConfigParser() 56 | cf = repo_file(self, "config") 57 | 58 | if cf and os.path.exists(cf): 59 | self.conf.read([cf]) 60 | elif not force: 61 | raise Exception("Configuration file missing") 62 | 63 | if not force: 64 | vers = int(self.conf.get("core", "repositoryformatversion")) 65 | if vers != 0 and not force: 66 | raise Exception("Unsupported repositoryformatversion %s" % vers) 67 | 68 | def repo_path(repo, *path): 69 | """Compute path under repo's gitdir.""" 70 | return os.path.join(repo.gitdir, *path) 71 | 72 | def repo_file(repo, *path, mkdir=False): 73 | """Same as repo_path, but create dirname(*path) if absent. For 74 | example, repo_file(r, \"refs\" \"remotes\", \"origin\", \"HEAD\") will create 75 | .git/refs/remotes/origin.""" 76 | 77 | if repo_dir(repo, *path[:-1], mkdir=mkdir): 78 | return repo_path(repo, *path) 79 | 80 | def repo_dir(repo, *path, mkdir=False): 81 | """Same as repo_path, but mkdir *path if absent if mkdir.""" 82 | 83 | path = repo_path(repo, *path) 84 | 85 | if os.path.exists(path): 86 | if (os.path.isdir(path)): 87 | return path 88 | else: 89 | raise Exception("Not a directory %s" % path) 90 | 91 | if mkdir: 92 | print("mkdir -p %s" % path) 93 | os.makedirs(path) 94 | return path 95 | else: 96 | return None 97 | 98 | def repo_create(path): 99 | """Create a new repository at path.""" 100 | 101 | repo = repository(path, True) 102 | 103 | # First, we make sure the path either doesn't exist or is an 104 | # empty dir. 105 | 106 | if os.path.exists(repo.worktree): 107 | if not os.path.isdir(repo.worktree): 108 | raise Exception ("%s is not a directory!" % path) 109 | if os.listdir(repo.worktree): 110 | raise Exception("%s is not empty!" % path) 111 | else: 112 | os.makedirs(repo.worktree) 113 | 114 | assert(repo_dir(repo, "branches", mkdir=True)) 115 | assert(repo_dir(repo, "objects", mkdir=True)) 116 | assert(repo_dir(repo, "refs", "tags", mkdir=True)) 117 | assert(repo_dir(repo, "refs", "heads", mkdir=True)) 118 | 119 | # .git/description 120 | with open(repo_file(repo, "description"), "w") as f: 121 | f.write("Unnamed repository; edit this file 'description' to name the repository.\n") 122 | 123 | # .git/HEAD 124 | with open(repo_file(repo, "HEAD"), "w") as f: 125 | f.write("ref: refs/heads/master\n") 126 | 127 | with open(repo_file(repo, "config"), "w") as f: 128 | config = repo_default_config() 129 | config.write(f) 130 | 131 | return repo 132 | 133 | def repo_default_config(): 134 | ret = configparser.ConfigParser() 135 | 136 | ret.add_section("core") 137 | ret.set("core", "repositoryformatversion", "0") 138 | ret.set("core", "filemode", "false") 139 | ret.set("core", "bare", "false") 140 | 141 | return ret 142 | 143 | argsp = argsubparsers.add_parser("init", help="Initialize a new, empty repository.") 144 | argsp.add_argument("path", 145 | metavar="directory", 146 | nargs="?", 147 | default=".", 148 | help="Where to create the repository.") 149 | 150 | def cmd_init(args): 151 | repo_create(args.path) 152 | 153 | def repo_find(path=".", required=True): 154 | path = os.path.realpath(path) 155 | 156 | if os.path.isdir(os.path.join(path, ".git")): 157 | return repository(path) 158 | 159 | # If we haven't returned, recurse in parent, if w 160 | parent = os.path.realpath(os.path.join(path, "..")) 161 | 162 | if parent == path: 163 | # Bottom case 164 | # os.path.join("/", "..") == "/": 165 | # If parent==path, then path is root. 166 | if required: 167 | raise Exception("No git directory.") 168 | else: 169 | return None 170 | 171 | # Recursive case 172 | return repo_find(parent, required) 173 | 174 | class GitObject (object): 175 | 176 | repo = None 177 | 178 | def __init__(self, repo, data=None): 179 | self.repo=repo 180 | 181 | if data: 182 | self.deserialize(data) 183 | 184 | def serialize(self): 185 | """This function MUST be implemented by subclasses. 186 | 187 | It must read the object's contents from self.data, a byte string, and do 188 | whatever it takes to convert it into a meaningful representation. What exactly that means depend on each subclass.""" 189 | raise Exception("Unimplemented!") 190 | 191 | def deserialize(self, data): 192 | raise Exception("Unimplemented!") 193 | 194 | # @TODO Replace the placeholder classes 195 | class GitCommit(GitObject): pass 196 | class GitTag(GitObject): pass 197 | class GitTree(GitObject): pass 198 | 199 | def object_read(repo, object_id): 200 | """Read object object_id from Git repository repo. Return a 201 | GitObject whose exact type depends on the object.""" 202 | 203 | hash = object_find(repo, object_id) 204 | 205 | path = repo_file(repo, "objects", hash[0:2], hash[2:]) 206 | 207 | with open (path, "rb") as f: 208 | raw = zlib.decompress(f.read()) 209 | 210 | # Read object type 211 | x = raw.find(b' ') 212 | fmt = raw[0:x] 213 | 214 | # Read and validate object size 215 | y = raw.find(b'\x00', x) 216 | size = int(raw[x:y].decode("ascii")) 217 | if size != len(raw)-y-1: 218 | raise Exception("Malformed object %s: bad length" % hash) 219 | 220 | # Pick constructor 221 | if fmt==b'commit' : c = GitCommit 222 | elif fmt==b'tree' : c = GitTree 223 | elif fmt==b'tag' : c = GitTag 224 | elif fmt==b'blob' : c = GitBlob 225 | else: 226 | raise Exception("Unknown type %s for object %s" % (fmt.decode("ascii"), hash)) 227 | 228 | # Call constructor and return object 229 | return c(repo, raw[y+1:]) 230 | 231 | def object_find(repo, name, fmt=None, follow=True): 232 | return name 233 | 234 | def object_write(obj, actually_write=True): 235 | # Serialize object data 236 | data = obj.serialize() 237 | # Add header 238 | result = obj.fmt + b' ' + str(len(data)).encode() + b'\x00' + data 239 | # Compute hash 240 | sha = hashlib.sha1(result).hexdigest() 241 | 242 | if actually_write: 243 | # Compute path 244 | path=repo_file(obj.repo, "objects", sha[0:2], sha[2:], mkdir=actually_write) 245 | 246 | with open(path, 'wb') as f: 247 | # Compress and write 248 | f.write(zlib.compress(result)) 249 | 250 | return sha 251 | 252 | class GitBlob(GitObject): 253 | fmt=b"blob" 254 | 255 | def serialize(self): 256 | return self.blobData 257 | 258 | def deserialize(self, data): 259 | self.blobData = data 260 | 261 | argsp = argsubparsers.add_parser("cat-file", 262 | help="Provide content of repository objects") 263 | 264 | argsp.add_argument("type", 265 | metavar="type", 266 | choices=["blob", "commit", "tag", "tree"], 267 | help="Specify the type") 268 | 269 | argsp.add_argument("object", 270 | metavar="object", 271 | help="The object to display") 272 | 273 | def cmd_cat_file(args): 274 | repo = repo_find() 275 | cat_file(repo, args.type, args.object) 276 | 277 | def cat_file(repo, fmt, obj): 278 | obj = object_read(repo, obj) 279 | 280 | if obj.fmt != fmt.encode("ascii"): 281 | raise Exception("Object is a %s but I wanted a %s." 282 | % (obj.fmt.decode("ascii"), fmt)) 283 | 284 | # I use Utf-8 because it's ASCII-compatible. 285 | sys.stdout.buffer.write(obj.serialize()) 286 | 287 | argsp = argsubparsers.add_parser( 288 | "hash-object", 289 | help="Compute object ID and optionally creates a blob from a file") 290 | 291 | argsp.add_argument("-t", 292 | metavar="type", 293 | dest="type", 294 | choices=["blob", "commit", "tag", "tree"], 295 | default="blob", 296 | help="Specify the type") 297 | 298 | argsp.add_argument("-w", 299 | dest="write", 300 | action="store_true", 301 | help="Actually write the object into the database") 302 | 303 | mutex0 = argsp.add_mutually_exclusive_group(required=True) 304 | 305 | mutex0.add_argument("--stdin", 306 | action="store_true", 307 | required=False, 308 | help="Read from stdin instead of from a file") 309 | 310 | mutex0.add_argument("path", 311 | nargs="?", 312 | help="Read object from ") 313 | 314 | def cmd_hash_object(args): 315 | repo = repository(".") 316 | 317 | with sys.stdin.buffer if args.stdin else open(args.path, "rb") as i: 318 | sha = object_hash(repo, args.type, i, args.write) 319 | print(sha) 320 | 321 | def object_hash(repo, fmt, f, write_to_repo): 322 | data = f.read() 323 | 324 | # Choose constructor depending on 325 | # object type found in header. 326 | if fmt=='commit' : obj = GitCommit(repo, data) 327 | elif fmt=='tree' : obj = GitTree(repo, data) 328 | elif fmt=='tag' : obj = GitTag(repo, data) 329 | elif fmt=='blob' : obj = GitBlob(repo, data) 330 | else: 331 | raise Exception("Unknown type %s!" % fmt) 332 | 333 | return object_write(obj, write_to_repo) 334 | 335 | def kvlm_parse(raw, start=0, dct=None): 336 | if not dct: 337 | dct = collections.OrderedDict() 338 | # You CANNOT declare the argument as dct=OrderedDict() or all 339 | # call to the functions will endlessly grow the same dict. 340 | 341 | # We search for the next space and the next newline. 342 | spc = raw.find(b' ', start) 343 | nl = raw.find(b'\n', start) 344 | 345 | # If space appears before newline, we have a keyword. 346 | 347 | # Base case 348 | # ========= 349 | # If newline appears first (or there's no space at all, in which 350 | # case find returns -1), we assume a blank line. A blank line 351 | # means the remainder of the data is the message. 352 | if (spc < 0) or (nl < spc): 353 | assert(nl == start) 354 | dct[b''] = raw[start+1:] 355 | return dct 356 | 357 | # Recursive case 358 | # ============== 359 | # we read a key-value pair and recurse for the next. 360 | key = raw[start:spc] 361 | 362 | # Find the end of the value. Continuation lines begin with a 363 | # space, so we loop until we find a "\n" not followed by a space. 364 | end = start 365 | while True: 366 | end = raw.find(b'\n', end+1) 367 | if raw[end+1] != ord(' '): break 368 | 369 | # Grab the value 370 | # Also, drop the leading space on continuation lines 371 | value = raw[spc+1:end].replace(b'\n ', b'\n') 372 | 373 | # Don't overwrite existing data contents 374 | if key in dct: 375 | if type(dct[key]) == list: 376 | dct[key].append(value) 377 | else: 378 | dct[key] = [ dct[key], value ] 379 | else: 380 | dct[key]=value 381 | 382 | return kvlm_parse(raw, start=end+1, dct=dct) 383 | 384 | def kvlm_serialize(kvlm): 385 | ret = b'' 386 | 387 | # Output fields 388 | for k in kvlm.keys(): 389 | # Skip the message itself 390 | if k == b'': continue 391 | val = kvlm[k] 392 | # Normalize to a list 393 | if type(val) != list: 394 | val = [ val ] 395 | 396 | for v in val: 397 | ret += k + b' ' + (v.replace(b'\n', b'\n ')) + b'\n' 398 | 399 | # Append message 400 | ret += b'\n' + kvlm[b''] 401 | 402 | return ret 403 | 404 | class GitCommit(GitObject): 405 | fmt=b'commit' 406 | 407 | def deserialize(self, data): 408 | self.kvlm = kvlm_parse(data) 409 | 410 | def serialize(self): 411 | return kvlm_serialize(self.kvlm) 412 | 413 | argsp = argsubparsers.add_parser("log", help="Display history of a given commit.") 414 | argsp.add_argument("commit", 415 | default="HEAD", 416 | nargs="?", 417 | help="Commit to start at.") 418 | 419 | def cmd_log(args): 420 | repo = repo_find() 421 | 422 | print("digraph wyaglog{") 423 | log_graphviz(repo, object_find(repo, args.commit), set()) 424 | print("}") 425 | 426 | def log_graphviz(repo, sha, seen): 427 | 428 | if sha in seen: 429 | return 430 | seen.add(sha) 431 | 432 | commit = object_read(repo, sha) 433 | assert (commit.fmt==b'commit') 434 | 435 | if not b'parent' in commit.kvlm.keys(): 436 | # Base case: the initial commit. 437 | return 438 | 439 | parents = commit.kvlm[b'parent'] 440 | 441 | if type(parents) != list: 442 | parents = [ parents ] 443 | 444 | for p in parents: 445 | p = p.decode("ascii") 446 | print ("c_{0} -> c_{1};".format(sha, p)) 447 | log_graphviz(repo, p, seen) 448 | 449 | class GitTreeLeaf(object): 450 | def __init__(self, mode, path, sha): 451 | self.mode = mode 452 | self.path = path 453 | self.sha = sha 454 | 455 | def tree_parse_one(raw, start=0): 456 | # Read the mode 457 | mode = raw[start:start+6].decode("ascii") 458 | 459 | # Check format 460 | assert(raw[start+6] == ord(' ')) 461 | 462 | # Find the NULL terminator of the path 463 | end = raw.find(b'\x00', start+6) 464 | # and read the path 465 | path = raw[start+7:end] 466 | 467 | # Read the SHA and convert to an hex string 468 | sha = hex( 469 | int.from_bytes( 470 | raw[end+1:end+21], "big"))[2:] # hex() adds 0x in front, 471 | # we don't want that. 472 | return end+21, GitTreeLeaf(mode, path, sha) 473 | 474 | def tree_parse(raw): 475 | pos = 0 476 | max = len(raw) 477 | ret = list() 478 | while pos < max: 479 | pos, data = tree_parse_one(raw, pos) 480 | ret.append(data) 481 | 482 | return ret 483 | 484 | class GitTree(GitObject): 485 | fmt=b'tree' 486 | 487 | def deserialize(self, data): 488 | self.items = tree_parse(data) 489 | 490 | def serialize(self): 491 | #@FIXME Add serializer! 492 | pass 493 | 494 | argsp = argsubparsers.add_parser("ls-tree", help="Pretty-print a tree object.") 495 | argsp.add_argument("object", 496 | help="The object to show.") 497 | 498 | def cmd_ls_tree(args): 499 | repo = repo_find() 500 | obj = object_read(repo, object_find(repo, args.object)) 501 | 502 | assert(obj.fmt == b'tree') 503 | 504 | for item in obj.items: 505 | print("{0} {1} {2}\t{3}".format( 506 | item.mode, 507 | # Git's ls-tree displays the type 508 | # of the object pointed to. We can do that too :) 509 | object_read(repo, item.sha).fmt.decode("ascii"), 510 | item.sha, 511 | item.path.decode("ascii"))) 512 | 513 | argsp = argsubparsers.add_parser("checkout", help="Checkout a commit inside of a directory.") 514 | 515 | argsp.add_argument("commit", 516 | help="The commit or tree to checkout.") 517 | 518 | argsp.add_argument("path", 519 | help="The EMPTY directory to checkout on.") 520 | 521 | def cmd_checkout(args): 522 | repo = repo_find() 523 | 524 | obj = object_read(repo, object_find(repo, args.commit)) 525 | 526 | # If the object is a commit, we grab its tree 527 | if obj.fmt == b'commit': 528 | obj = object_read(repo, obj.kvlm[b'tree'].decode("ascii")) 529 | 530 | # Verify that path is an empty directory 531 | if os.path.exists(args.path): 532 | if not os.path.isdir(args.path): 533 | raise Exception("Not a directory {0}!".format(args.path)) 534 | if os.listdir(args.path): 535 | raise Exception("Not empty {0}!".format(args.path)) 536 | else: 537 | os.makedirs(args.path) 538 | 539 | tree_checkout(repo, obj, os.path.realpath(args.path).encode()) 540 | 541 | def tree_checkout(repo, tree, path): 542 | for item in tree.items: 543 | obj = object_read(repo, item.sha) 544 | print(path, item.path) 545 | dest = os.path.join(path, item.path) 546 | 547 | print("Checkout %s" % dest) 548 | 549 | if obj.fmt == b'tree': 550 | os.mkdir(dest) 551 | checkout(repo, obj, dest) 552 | elif obj.fmt == b'blob': 553 | with open(dest, 'wb') as f: 554 | f.write(obj.blobData) 555 | 556 | def ref_resolve(repo, ref): 557 | with open(repo_file(repo, ref), 'r') as fp: 558 | data = fp.read()[:-1] 559 | # Drop final \n ^^^^^ 560 | if data.startswith("ref: s"): 561 | return ref_resolve(repo, data[5:]) 562 | else: 563 | return data 564 | 565 | def ref_list(repo, path=None): 566 | if not path: 567 | path = repo_dir(repo, "refs") 568 | ret = collections.OrderedDict() 569 | # Git shows refs sorted. To do the same, we use 570 | # an OrderedDict and sort the output of listdir 571 | for f in sorted(os.listdir(path)): 572 | can = os.path.join(path, f) 573 | if os.path.isdir(can): 574 | ret[f] = ref_list(repo, can) 575 | else: 576 | ret[f] = ref_resolve(repo, can) 577 | 578 | return ret 579 | 580 | argsp = argsubparsers.add_parser("show-ref", help="List references.") 581 | 582 | def cmd_show_ref(args): 583 | repo = repo_find() 584 | refs = ref_list(repo) 585 | show_ref(repo, refs, prefix="refs") 586 | 587 | def show_ref(repo, refs, with_hash=True, prefix=""): 588 | for k, v in refs.items(): 589 | if type(v) == str: 590 | print ("{0}{1}{2}".format( 591 | v + " " if with_hash else "", 592 | prefix + "/" if prefix else "", 593 | k)) 594 | else: 595 | show_ref(repo, v, with_hash=with_hash, prefix="{0}{1}{2}".format(prefix, "/" if prefix else "", k)) 596 | 597 | class GitTag(GitCommit): 598 | fmt = b'tag' 599 | 600 | argsp = argsubparsers.add_parser( 601 | "tag", 602 | help="List and create tags") 603 | 604 | argsp.add_argument("-a", 605 | action="store_true", 606 | dest="create_tag_object", 607 | help="Whether to create a tag object") 608 | 609 | argsp.add_argument("name", 610 | nargs="?", 611 | help="The new tag's name") 612 | 613 | argsp.add_argument("object", 614 | default="HEAD", 615 | nargs="?", 616 | help="The object the new tag will point to") 617 | 618 | def cmd_tag(args): 619 | repo = repo_find() 620 | 621 | if args.name: 622 | tag_create(args.name, 623 | args.object, 624 | type="object" if args.create_tag_object else "ref") 625 | else: 626 | refs = ref_list(repo) 627 | show_ref(repo, refs["tags"], with_hash=False) 628 | 629 | def object_resolve(repo, name): 630 | """Resolve name to an object hash in repo. 631 | 632 | This function is aware of: 633 | 634 | - the HEAD literal 635 | - short and long hashes 636 | - tags 637 | - branches 638 | - remote branches""" 639 | candidates = list() 640 | hashRE = re.compile(r"^[0-9A-Fa-f]{1,16}$") 641 | smallHashRE = re.compile(r"^[0-9A-Fa-f]{1,16}$") 642 | 643 | if not name.strip(): 644 | return None 645 | 646 | if name == "HEAD": 647 | return [ ref_resolve(repo, "..", "HEAD") ] 648 | 649 | # A full hash 650 | if fullHashRE.matches(name): 651 | return [ name.lower() ] 652 | 653 | def object_find(repo, name, fmt=None, follow=None): 654 | ## Part 1: identify object 655 | 656 | class GitIndexEntry(object): 657 | ctime = None 658 | """The last time a file's metadata changed. This is a tuple (seconds, nanoseconds)""" 659 | 660 | mtime = None 661 | """The last time a file's data changed. This is a tuple (seconds, nanoseconds)""" 662 | 663 | dev = None 664 | """The ID of device containing this file""" 665 | ino = None 666 | """The file's inode number""" 667 | mode_type = None 668 | """The object type, either b1000 (regular), b1010 (symlink), b1110 (gitlink). """ 669 | mode_perms = None 670 | """The object permissions, an integer.""" 671 | uid = None 672 | """User ID of owner""" 673 | gid = None 674 | """Group ID of ownner (according to stat 2. Isn'th)""" 675 | size = None 676 | """Size of this object, in bytes""" 677 | obj = None 678 | """The object's hash as a hex string""" 679 | flag_assume_valid = None 680 | flag_extended = None 681 | flag_stage = None 682 | flag_name_length = None 683 | """Length of the name if < 0xFFF (yes, three Fs), -1 otherwise""" 684 | 685 | name = None 686 | -------------------------------------------------------------------------------- /src/ntangle.nim: -------------------------------------------------------------------------------- 1 | # NTangle - Basic tangling of Org documents 2 | # https://github.com/OrgTangle/ntangle 3 | 4 | import os, strformat, strutils, tables, terminal, sequtils, times 5 | 6 | type 7 | DebugVerbosity = enum dvNone, dvLow, dvHigh 8 | 9 | const 10 | # DebugVerbosityLevel = dvLow 11 | DebugVerbosityLevel = dvNone 12 | template dbg(msg: string, verbosity = dvLow, prefix = "[DBG] ") = 13 | when DebugVerbosityLevel >= dvLow: 14 | case DebugVerbosityLevel 15 | of dvHigh: 16 | echo prefix & fmt(msg) 17 | of dvLow: 18 | if verbosity == dvLow: 19 | echo prefix & fmt(msg) 20 | else: # This case is never reached 21 | discard 22 | 23 | const 24 | tangledExt = { 25 | "emacs-lisp": "el", 26 | "shell": "sh", 27 | "bash": "sh", 28 | "tcsh": "csh", 29 | "rust": "rs", 30 | "python": "py", 31 | "python3": "py", 32 | "ipython": "py", 33 | "ipython3": "py" 34 | }.toTable 35 | dbg "{tangledExt}" 36 | 37 | type 38 | HeaderArgType = enum 39 | haPropertyKwd # #+property: header-args .. 40 | haPropertyDrawer # :header-args: .. 41 | haPropertyDrawerAppend # :header-args+: .. 42 | haBeginSrc # #+begin_src foo .. 43 | haNone # none of the above 44 | UserError = object of Exception 45 | OrgError = object of Exception 46 | HeaderArgs = object 47 | tangle: string 48 | padline: bool 49 | shebang: string 50 | mkdirp: bool 51 | permissions: set[FilePermission] 52 | LangAndArgs = tuple 53 | argType: HeaderArgType 54 | lang: string 55 | args: seq[tuple[key, val: string]] # args are key / value pairs. 56 | LevelLangIndex = tuple 57 | orgLevel: Natural 58 | lang: string 59 | TangleHeaderArgs = Table[LevelLangIndex, HeaderArgs] # orgLevel, lang, header args 60 | 61 | func initTangleHeaderArgs(): TangleHeaderArgs = initTable[LevelLangIndex, HeaderArgs]() 62 | 63 | var 64 | prevOrgLevel = -1 65 | orgLevel = 0.Natural 66 | fileData = initTable[string, string]() # file, data 67 | headerArgsDefaults = initTangleHeaderArgs() 68 | outFileName: string 69 | fileHeaderArgs = initTable[string, HeaderArgs]() # file, header args 70 | bufEnabled: bool 71 | firstLineSrcBlock = false 72 | blockIndent = 0 73 | startTime: float 74 | 75 | proc resetStateVars() = 76 | ## Reset all the state variables. 77 | ## This is called before reading each new Org file. 78 | prevOrgLevel = -1 79 | orgLevel = 0.Natural 80 | outFileName = "" 81 | firstLineSrcBlock = false 82 | blockIndent = 0 83 | 84 | fileData.clear() 85 | fileHeaderArgs.clear() 86 | headerArgsDefaults.clear() 87 | # Default tangle header args for all Org levels and languages. 88 | headerArgsDefaults[(0.Natural, "")] = HeaderArgs(tangle: "no", 89 | padline: true, 90 | shebang: "", 91 | mkdirp: false, 92 | permissions: {}) 93 | 94 | proc parseFilePermissions(octals: string): set[FilePermission] = 95 | ## Converts the input permissions octal string to a Nim set for FilePermission type. 96 | # https://devdocs.io/nim/os#FilePermission 97 | const 98 | readPerms = [fpUserRead, fpGroupRead, fpOthersRead] 99 | writePerms = [fpUserWrite, fpGroupWrite, fpOthersWrite] 100 | execPerms = [fpUserExec, fpGroupExec, fpOthersExec] 101 | for idx, o in octals: 102 | if o != '0': 103 | if o in {'4', '5', '6', '7'}: 104 | result.incl(readPerms[idx]) 105 | if o in {'2', '3', '6', '7'}: 106 | result.incl(writePerms[idx]) 107 | if o in {'1', '3', '5', '7'}: 108 | result.incl(execPerms[idx]) 109 | dbg "permissions = {result}" 110 | 111 | proc parseTangleHeaderProperties(file: string, lnum: int, haObj: LangAndArgs) = 112 | ## Org header arguments related to tangling. See (org) Extracting Source Code. 113 | let 114 | (dir, basename, _) = splitFile(file) 115 | dbg "Org file = {file}, dir={dir}, base name={basename}", dvHigh 116 | dbg "Line {lnum}, Lang {haObj.lang} - hdrArgs: {haObj.args}" 117 | var 118 | hArgs: HeaderArgs 119 | outfile = "" 120 | if haObj.lang != "": 121 | let 122 | langLower = haObj.lang.toLowerAscii() 123 | ext = if tangledExt.hasKey(langLower): 124 | tangledExt[langLower] 125 | else: 126 | haObj.lang 127 | outfile = dir / basename & "." & ext 128 | 129 | if headerArgsDefaults.hasKey((orgLevel, haObj.lang)): 130 | hArgs = headerArgsDefaults[(orgLevel, haObj.lang)] 131 | dbg "Line {lnum} - Using Org level {orgLevel} + lang {haObj.lang} scope, now hArgs = {hArgs}" 132 | else: 133 | hArgs = headerArgsDefaults[(orgLevel, "")] 134 | dbg "Line {lnum} - Using only Org level {orgLevel} scope, now hArgs = {hArgs}" 135 | 136 | # If hArgs already specifies the tangled file path, use that! 137 | if hArgs.tangle != "yes" and 138 | hArgs.tangle != "no": 139 | dbg "** Line {lnum} - Old outfile={outfile}, overriding it to {hArgs.tangle}" 140 | if (not hArgs.tangle.startsWith "/"): # if relative path 141 | outfile = dir / hArgs.tangle 142 | else: 143 | outfile = hArgs.tangle 144 | 145 | for hdrArg in haObj.args: 146 | let 147 | (argKey, argVal) = hdrArg 148 | dbg "argkey={argkey}, argval={argval}, onBeginSrc={haObj.argType == haBeginSrc}, outfile={outfile}" 149 | case argkey 150 | of "tangle": 151 | hArgs.tangle = argval 152 | case argval 153 | of "yes": 154 | discard 155 | of "no": 156 | bufEnabled = false 157 | of "": # empty `:tangle` without argument is invalid! 158 | raise newException(OrgError, fmt("A `:tangle` key without yes/no/filename argument is invalid!")) 159 | else: #filename 160 | outfile = argval.expandTilde 161 | if (not outfile.startsWith "/"): # if relative path 162 | outfile = dir / outfile 163 | of "padline": 164 | case argval 165 | of "yes": 166 | hArgs.padline = true 167 | of "no": 168 | hArgs.padline = false 169 | else: 170 | raise newException(OrgError, fmt("The '{argval}' value for ':{argkey}' is invalid. The only valid values are 'yes' and 'no'.")) 171 | of "shebang": 172 | hArgs.shebang = argval 173 | of "mkdirp": 174 | case argval 175 | of "yes": 176 | hArgs.mkdirp = true 177 | of "no": 178 | hArgs.mkdirp = false 179 | else: 180 | raise newException(OrgError, fmt("The '{argval}' value for ':{argkey}' is invalid. The only valid values are 'yes' and 'no'.")) 181 | of "tangle-mode": 182 | let 183 | octalPerm = argval.split("#o", maxsplit=1) 184 | if octalPerm.len != 2: 185 | raise newException(OrgError, fmt("Line {lnum} - The header argkey ':{argkey}' has invalid file permissions syntax: {argval}")) 186 | if octalPerm[1].len < 3: 187 | raise newException(OrgError, fmt("Line {lnum} - The header argkey ':{argkey}' has invalid file permissions syntax: {argval}")) 188 | let 189 | octalPermOwner = octalPerm[1][0] 190 | octalPermGroup = octalPerm[1][1] 191 | octalPermOther = octalPerm[1][2] 192 | hArgs.permissions = parseFilePermissions(octalPermOwner & octalPermGroup & octalPermOther) 193 | # of "comments": 194 | # case argval 195 | # of "yes": 196 | # of "no": 197 | # of "link": 198 | # of "org": 199 | # of "both": 200 | # of "noweb": 201 | # else: 202 | # # error message 203 | # of "no-expand": 204 | # case argval 205 | # of "yes": 206 | # hArgs.no-expand = true 207 | # of "no": 208 | # hArgs.no-expand = false 209 | # else: 210 | # raise newException(OrgError, fmt("The '{argval}' value for ':{argkey}' is invalid. The only valid values are 'yes' and 'no'.")) 211 | # of "noweb": 212 | # case argval 213 | # of "yes": 214 | # of "no": 215 | # of "tangle": 216 | # of "no-export": 217 | # of "strip-export": 218 | # of "eval": 219 | # else: 220 | # # error message 221 | # of "noweb-ref": 222 | # # use argval 223 | # of "noweb-sep": 224 | # # use argval 225 | of "comments", "no-expand", "noweb", "noweb-ref", "noweb-sep": 226 | styledEcho(fgYellow, " [WARN] ", 227 | fgDefault, "Line ", 228 | styleBright, $lnum, 229 | resetStyle, fmt" - ':{argkey}' header argument is not supported at the moment.") 230 | else: # Ignore all other header args 231 | discard 232 | 233 | # Update the default HeaderArgs for the current orgLevel+lang 234 | # scope, but only using the header args set using property keyword 235 | # or the drawer property. 236 | if haObj.argType != haBeginSrc: 237 | dbg "** Line {lnum}: Updating headerArgsDefaults[({orgLevel}, {haObj.lang})] to {hArgs}" 238 | headerArgsDefaults[(orgLevel, haObj.lang)] = hArgs 239 | 240 | dbg "[after] Line {lnum} - hArgs = {hArgs}" 241 | if outfile != "": 242 | # Save the updated hArgs to the file-specific HeaderArgs global 243 | # value. 244 | outFileName = outfile 245 | 246 | dbg "line={lnum}, onBeginSrc={haObj.argType == haBeginSrc}, hArgs.tangle={hArgs.tangle}, outFileName={outFileName}" 247 | if haObj.argType == haBeginSrc: 248 | if hArgs.tangle != "no": 249 | doAssert outFileName != "" 250 | dbg "line {lnum}: buffering enabled for `{outFileName}'" 251 | bufEnabled = true 252 | firstLineSrcBlock = true 253 | 254 | dbg "** Line {lnum}: Updating fileHeaderArgs[{outFileName}] to {hArgs}" 255 | fileHeaderArgs[outFileName] = hArgs 256 | 257 | proc orgRemoveEscapeCommas(line: string): string = 258 | ## Remove only single leading comma if it's followed by "#+" or "*". 259 | ## The leading comma can have preceeding spaces too, but it still 260 | ## should be removed. 261 | ## 262 | ## Examples: 263 | ## ",#+foo" -> "#+foo" 264 | ## " ,#+foo" -> " #+foo" 265 | ## ",* foo" -> "* foo" 266 | ## ",,* foo" -> ",* foo" 267 | ## ",abc" -> ",abc" This comma remains 268 | ## ",# abc" -> ",# abc" This comma remains too 269 | let 270 | lineParts = line.split(",", maxSplit = 1) 271 | if (lineParts.len == 2) and 272 | (lineParts[1].startsWith("#+") or 273 | lineParts[1].startsWith("*") or 274 | lineParts[1].startsWith(",")): 275 | return lineParts[0] & lineParts[1] 276 | else: 277 | return line 278 | 279 | proc lineAdjust(line: string, indent: int): string = 280 | ## Remove extra indentation from ``line``, and append it with newline. 281 | dbg "[lineAdjust] line={line}", dvHigh 282 | result = 283 | if indent == 0: 284 | line & "\n" 285 | elif line.len <= 2: 286 | line & "\n" 287 | else: 288 | var 289 | truncSafe = true 290 | for i, c in line[0 ..< indent]: 291 | dbg "line[{i}] = {c}" 292 | if c != ' ': # Don't truncate if the to-be-truncated portion is not all spaces 293 | truncSafe = false 294 | break 295 | if truncSafe: 296 | line[indent .. line.high] & "\n" 297 | else: 298 | line & "\n" 299 | result = result.orgRemoveEscapeCommas() 300 | 301 | proc getOrgLevel(line: string): Natural = 302 | ## Return the current Org level if ``line`` is an Org heading. 303 | ## If not on an Org heading, return 0. 304 | ## 305 | ## An Org heading has no leading space, and begins with one or more 306 | ## ``*`` chars, followed by a space, and heading text. 307 | ## 308 | ## Examples: "* Heading Level 1", "** Heading Level 2". 309 | let 310 | lastStarLocation = line.find("* ") 311 | if (lastStarLocation >= 0) and 312 | line[0 .. lastStarLocation].allCharsInSet({'*'}): 313 | return lastStarLocation + 1 314 | 315 | proc updateHeaderArgsDefault() = 316 | ## Update the default header args for the current orgLevel scope. 317 | # Switch to sibling heading. Example: from "** Heading 4.2" to "** Heading 4.3". 318 | if prevOrgLevel == orgLevel.int: 319 | assert orgLevel != 0 320 | for i in countDown(orgLevel-1, 0): 321 | if headerArgsDefaults.hasKey((i.Natural, "")): 322 | headerArgsDefaults[(orgLevel, "")] = headerArgsDefaults[(i.Natural, "")] 323 | break 324 | # Switch to child heading. Example: from "** Heading 4.2" to "*** Heading 4.2.1". 325 | elif prevOrgLevel < orgLevel.int: 326 | if prevOrgLevel < 0: 327 | headerArgsDefaults[(orgLevel, "")] = headerArgsDefaults[(0.Natural, "")] 328 | else: 329 | headerArgsDefaults[(orgLevel, "")] = headerArgsDefaults[(prevOrgLevel.Natural, "")] 330 | # Switch to parent heading. Example: from "** Heading 4.2" to "* Heading 5". 331 | else: 332 | # Do nothing in this case, because with orgLevel < prevOrgLevel, 333 | # headerArgsDefaults[(orgLevel.Natural, "")] should already have 334 | # been populated earlier. 335 | discard 336 | prevOrgLevel = orgLevel 337 | 338 | proc getHeaderArgs(file: string, line: string, lnum: int): LangAndArgs = 339 | ## Get well-formatted header args. 340 | ## 341 | ## Examples: 342 | ## 343 | ## " #+BEGIN_SRC nim :tangle \"hello.nim\" :flags -d:release " 344 | ## "#+property: header-args:nim :tangle hello.nim :flags -d:release" 345 | ## "#+property: HEADER-ARGS :tangle hello.nim :flags -d:release" 346 | ## " :header-args: :tangle hello.nim :flags -d:release" 347 | ## 348 | ## All of the above inputs will result in the below string sequence 349 | ## for the ``args`` field of ``LandAndArgs``: 350 | ## -> @["tangle hello.nim", "flags -d:release"] 351 | ## The ``lang`` field will be an empty string or a language string 352 | ## like ``"nim"``. 353 | let 354 | spaceSepParts = line.strip.split(" ").filterIt(it != "") 355 | var 356 | haType: HeaderArgType = haNone 357 | headerArgsRaw: seq[string] 358 | headerArgs: seq[tuple[key, val: string]] 359 | headerArgPair: string 360 | lang: string 361 | dbg "spaceSepParts: {spaceSepParts}", dvHigh 362 | if spaceSepParts.len >= 3 and 363 | spaceSepParts[0].toLowerAscii() == "#+property:" and 364 | spaceSepParts[1].toLowerAscii().startsWith("header-args"): 365 | doAssert spaceSepParts[2][0] == ':', 366 | fmt"{file}:{lnum} :: {line}" & "\n" & 367 | " : The first switch in 'header-args' property must be a key with ':' prefix." 368 | headerArgsRaw = spaceSepParts[2 .. spaceSepParts.high] 369 | let 370 | kwdParts = spaceSepParts[1].split(":") 371 | if kwdParts.len == 2: 372 | lang = kwdParts[1].strip() 373 | haType = haPropertyKwd 374 | # ":header-args:", ":header-args+:", ":header-args:nim:" 375 | elif spaceSepParts.len >= 3 and 376 | spaceSepParts[0].toLowerAscii().startsWith(":header-args"): 377 | doAssert spaceSepParts[1][0] == ':', 378 | fmt"{file}:{lnum} :: {line}" & "\n" & 379 | " : The first switch in 'header-args' drawer property must be a key with ':' prefix." 380 | headerArgsRaw = spaceSepParts[1 .. spaceSepParts.high] 381 | let 382 | kwdParts = spaceSepParts[0].split(":") 383 | doAssert kwdParts.len >= 3 384 | if kwdParts.len == 4: 385 | lang = kwdParts[2].strip(chars = {' ', '+'}) 386 | if kwdParts[1].strip().endsWith("+"): 387 | haType = haPropertyDrawerAppend 388 | else: 389 | haType = haPropertyDrawer 390 | elif spaceSepParts.len >= 2 and 391 | spaceSepParts[0].toLowerAscii() == "#+begin_src": 392 | lang = spaceSepParts[1].strip() 393 | haType = haBeginSrc 394 | var 395 | startHeaderArgs = 0 396 | for i in 2 .. spaceSepParts.high: 397 | if spaceSepParts[i][0] == ':': 398 | startHeaderArgs = i 399 | break 400 | if startHeaderArgs >= 2: 401 | headerArgsRaw = spaceSepParts[startHeaderArgs .. spaceSepParts.high] 402 | if haType != haNone: 403 | var lastKey = false 404 | var keyVal: tuple[key, val: string] 405 | template notEmpty(kv: untyped): untyped = kv.key.len > 0 or kv.val.len > 0 406 | for i, h in headerArgsRaw: 407 | if h.len >= 2 and h[0] == ':': # this is a key 408 | if notEmpty(keyVal): # add current `keyVal` if anything 409 | headerArgs.add keyVal 410 | keyVal = (key: "", val: "") 411 | keyVal.key = h[1 .. h.high] 412 | lastKey = true 413 | else: 414 | let 415 | # If value of h is "\"some file.txt\"", change it to "some file.txt" 416 | valWithoutLiteralQuotes = h.strip(chars = {'"'}) 417 | if not lastKey: # if the last was `not` a key, append `h` to last 418 | keyVal.val &= " " & valWithoutLiteralQuotes 419 | else: 420 | keyVal.val = valWithoutLiteralQuotes 421 | lastKey = false 422 | if notEmpty(keyVal): # add current `keyVal` if anything 423 | headerArgs.add keyVal 424 | return (haType, lang, headerArgs) 425 | 426 | proc parseLine(file: string, line: string, lnum: int) = 427 | ## On detection of "#+begin_src" with ":tangle foo", enable 428 | ## recording of LINE, next line onwards to global table ``fileData``. 429 | ## On detection of "#+end_src", stop that recording. 430 | if line.getOrgLevel() > 0: 431 | orgLevel = line.getOrgLevel() 432 | dbg "orgLevel = {orgLevel}" 433 | updateHeaderArgsDefault() 434 | let 435 | haObj = getHeaderArgs(file, line, lnum) 436 | dbg "[line {lnum}] {line}", dvHigh 437 | if haObj.argType != haNone: 438 | dbg "getHeaderArgs: line {lnum}:: {haObj}" 439 | if haObj.argType in {haPropertyKwd, haPropertyDrawer, haPropertyDrawerAppend}: 440 | dbg "Property header-args found [Lang={haObj.lang}]: {haObj.args}" 441 | parseTangleHeaderProperties(file, lnum, haObj) 442 | else: 443 | let 444 | lineParts = line.strip.split(":") 445 | linePartsLower = lineParts.mapIt(it.toLowerAscii.strip()) 446 | if firstLineSrcBlock: 447 | dbg " first line of src block" 448 | dbg "line {lnum}: bufEnabled: {bufEnabled} linePartsLower: {linePartsLower}", dvHigh 449 | if bufEnabled: 450 | assert outFileName != "" 451 | if (linePartsLower[0] == "#+end_src"): 452 | bufEnabled = false 453 | dbg "line {lnum}: buffering disabled for `{outFileName}'" 454 | else: 455 | dbg " {lineParts.len} parts: {lineParts}", dvHigh 456 | 457 | # Assume that the first line of every src block has zero 458 | # indentation. 459 | if firstLineSrcBlock: 460 | blockIndent = (line.len - line.strip(trailing=false).len) 461 | 462 | try: 463 | if firstLineSrcBlock and fileHeaderArgs[outFileName].padline: 464 | fileData[outFileName].add("\n") 465 | fileData[outFileName].add(lineAdjust(line, blockIndent)) 466 | except KeyError: # If outFileName key is not yet set in fileData 467 | fileData[outFileName] = lineAdjust(line, blockIndent) 468 | dbg " extra indentation: {blockIndent}" 469 | firstLineSrcBlock = false 470 | elif haObj.argType == haBeginSrc: 471 | parseTangleHeaderProperties(file, lnum, haObj) 472 | 473 | proc writeFiles() = 474 | ## Write the files from ``fileData``. 475 | dbg "fileData elements: {fileData.len}" 476 | dbg "fileData: {fileData}" 477 | if fileData.len == 0: 478 | echo fmt" No tangle blocks found" 479 | return 480 | 481 | for file, data in fileData: 482 | dbg " Tangling to `{file}' .." 483 | let 484 | (outDir, _, _) = splitFile(file) 485 | var 486 | dataUpdated = data 487 | dbg " outDir: `{outDir}'" 488 | if outDir != "": 489 | if (not dirExists(outDir)): 490 | if fileHeaderArgs[file].mkdirp: 491 | echo fmt" Creating {outDir}/ .." 492 | createDir(outDir) 493 | else: 494 | raise newException(UserError, fmt"Unable to write to `{file}' as `{outDir}/' directory does not exist. Set ':mkdirp yes' header arg to auto-create it.") 495 | 496 | if fileHeaderArgs[file].shebang != "": 497 | dataUpdated = fileHeaderArgs[file].shebang & "\n" & data 498 | dbg "{file}: <<{dataUpdated}>>" 499 | styledEcho(" Writing ", fgGreen, file, fgDefault, fmt" ({dataUpdated.countLines} lines) ..") 500 | writeFile(file, dataUpdated) 501 | if fileHeaderArgs[file].permissions != {}: 502 | file.setFilePermissions(fileHeaderArgs[file].permissions) 503 | elif fileHeaderArgs[file].shebang != "": 504 | # If a tangled file has a shebang, auto-add user executable 505 | # permissions (as Org does too). 506 | file.inclFilePermissions({fpUserExec}) 507 | 508 | echo "" 509 | styledEcho("Total tangling time: ", fgGreen, fmt"{(cpuTime() - startTime):.2f}", fgDefault, " seconds") 510 | 511 | proc doOrgTangle(file: string) = 512 | ## Tangle Org file ``file``. 513 | if file.toLowerAscii.endsWith(".org"): # Ignore files with names not ending in ".org" 514 | resetStateVars() 515 | styledEcho("Parsing ", styleBright, file, resetStyle, " ..") 516 | var 517 | lnum = 1 518 | for line in lines(file): 519 | dbg("", prefix=" ") # blank line 520 | dbg "{lnum}: {line}", dvHigh 521 | parseLine(file, line, lnum) 522 | inc lnum 523 | writeFiles() 524 | echo "" 525 | 526 | proc ntangle(orgFilesOrDirs: seq[string]) = 527 | ## Command-line utility for Tangling of Org mode documents 528 | startTime = cpuTime() 529 | try: 530 | for f1 in orgFilesOrDirs: 531 | let 532 | f1IsFile = f1.fileExists and (not f1.dirExists) 533 | f1IsDir = f1.dirExists and (not f1.fileExists) 534 | dbg "is {f1} a directory? {f1IsDir}" 535 | dbg "is {f1} a file? {f1IsFile}" 536 | if f1IsFile: 537 | doOrgTangle(f1) 538 | elif f1IsDir: 539 | styledEcho("Entering directory ", styleBright, f1 / "", resetStyle, " ..") 540 | for f2 in f1.walkDirRec: 541 | doOrgTangle(f2) 542 | else: 543 | raise newException(UserError, fmt("{f1} is neither a valid file nor a directory")) 544 | except: 545 | stderr.styledWriteLine(fgRed, fmt" [ERROR] {getCurrentException().name}: ", 546 | fgDefault, getCurrentExceptionMsg() & "\n") 547 | quit QuitFailure 548 | 549 | when isMainModule: 550 | import cligen 551 | const 552 | url = "https://github.com/OrgTangle/ntangle" 553 | 554 | # https://github.com/c-blake/cligen/issues/83#issuecomment-444951772 555 | proc mergeParams(cmdNames: seq[string], cmdLine=commandLineParams()): seq[string] = 556 | result = cmdLine 557 | if cmdLine.len == 0: 558 | result = @["--help"] 559 | 560 | const 561 | version = staticExec("git describe --tags HEAD") 562 | nimbleData = staticRead("../ntangle.nimble") 563 | uri = "https://github.com/OrgTangle/ntangle" 564 | myUsage = "\nNAME\n ntangle - ${doc}" & 565 | "\nUSAGE\n ${command} ${args}" & 566 | "\n\nOPTIONS\n$options" & 567 | "\nURI\n " & uri & 568 | "\n\nAUTHOR\n " & nimbleData.fromNimble("author") & 569 | "\n\nVERSION\n " & version 570 | 571 | # https://github.com/c-blake/cligen/blob/master/RELEASE-NOTES.md#version-0928 572 | clCfg.version = version 573 | 574 | dispatch(ntangle, usage=myUsage) 575 | -------------------------------------------------------------------------------- /tests/eless/eless: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Version: v0.5 3 | 4 | # This script uses the unofficial strict mode as explained in 5 | # http://redsymbol.net/articles/unofficial-bash-strict-mode 6 | # 7 | # Also checks have been done with www.shellcheck.net to have a level of 8 | # confidence that this script will be free of loopholes.. or is it? :) 9 | # 10 | # This file is tangled from https://github.com/kaushalmodi/eless/blob/v0.5/eless.org 11 | # Do NOT edit this manually. 12 | 13 | eless_version='v0.5' 14 | eless_git_hash='6115f73' 15 | 16 | h=" 17 | Script to run emacs in view-mode with some sane defaults in attempt to replace 18 | less, diff, man, (probably ls too). 19 | 20 | * Options to this script 21 | |--------+--------------------------| 22 | | Option | Description | 23 | |--------+--------------------------| 24 | | -h | Show this help and quit | 25 | | --gui | Run eless in GUI mode | 26 | | -V | Print version and quit | 27 | | -D | Run with debug messages | 28 | |--------+--------------------------| 29 | 30 | * Common bindings in 'view-mode' 31 | |--------------+------------------------------------------------------------------------------| 32 | | Binding | Description | 33 | |--------------+------------------------------------------------------------------------------| 34 | | SPC | Scroll forward 'page size' lines. With prefix scroll forward prefix lines. | 35 | | DEL or S-SPC | Scroll backward 'page size' lines. With prefix scroll backward prefix lines. | 36 | | | (If your terminal does not support this, use xterm instead or using C-h.) | 37 | | RET | Scroll forward one line. With prefix scroll forward prefix line(s). | 38 | | y | Scroll backward one line. With prefix scroll backward prefix line(s). | 39 | | s | Do forward incremental search. | 40 | | r | Do reverse incremental search. | 41 | | e | Quit the 'view-mode' and use that emacs session as usual to modify | 42 | | | the opened file if needed. | 43 | |--------------+------------------------------------------------------------------------------| 44 | 45 | ** Custom bindings 46 | |--------------+------------------------------------------------------------| 47 | | Binding | Description | 48 | |--------------+------------------------------------------------------------| 49 | | ! or K | Delete lines matching regexp | 50 | | & or k | Keep lines matching regexp | 51 | | 0 | Delete this window | 52 | | 1 | Keep only this window | 53 | | A | Auto-revert Tail Mode (like tail -f on current buffer) | 54 | | D | Dired | 55 | | N | Next error (next line in *occur*) | 56 | | P | Previous error (previous line in *occur*) | 57 | | a | Auto-revert Mode | 58 | | g or F5 | Revert buffer (probably after keep/delete lines) | 59 | | n | Next line | 60 | | o | Occur | 61 | | p | Previous line | 62 | | q | Quit emacs if at most one buffer is open, else kill buffer | 63 | | t | Toggle line truncation | 64 | | = or + or - | Adjust font size (in GUI mode) | 65 | | C-down/up | Inc/Dec frame height (in GUI mode) | 66 | | C-right/left | Inc/Dec frame width (in GUI mode) | 67 | |--------------+------------------------------------------------------------| 68 | 69 | ** Do 'C-h b' and search for 'view-mode' to see more bindings in this mode. 70 | 71 | * For GNU/Linux systems, set the environment variable PAGER to 'eless' to use it 72 | for viewing man pages. 'man grep' will then show the grep man page in eless. 73 | 74 | For macOS systems, 'PAGER=less man -P \"eless --gui\" grep' will work instead. 75 | 76 | * Usage Examples 77 | 78 | eless foo.txt # Open foo.txt in eless in terminal (-nw) mode by default. 79 | eless foo.txt --gui # Open foo.txt in eless in GUI mode. 80 | echo 'foo' | eless # 81 | echo 'foo' | eless - # Same as above. The hyphen after eless does not matter; is anyways discarded. 82 | grep 'bar' foo.txt | eless # 83 | diff foo bar | eless # Colored diff! 84 | diff -u foo bar | eless # Colored diff for unified diff format 85 | eless . # Open dired in the current directory (enhanced 'ls') 86 | ls --color=always | eless # Auto-detect ANSI color codes and convert those to colors 87 | PAGER=eless git diff # Show git diff with ANSI coded colors 88 | eless -h | eless # See eless help ;-) 89 | info emacs | eless # Read emacs Info manual in eless 90 | eless foo.tar.xz # Read the contents of archives; emacs does the unarchiving automatically 91 | PAGER=eless python3; help('def') # Read (I)Python keyword help pages (example: help for 'def' keyword) 92 | PAGER=eless python3; help('shlex') # Read (I)Python module help pages (example: help for 'shlex' module) 93 | PAGER=eless python3; help('TYPES') # Read (I)Python topic help pages (example: help for 'TYPES' topic) 94 | PAGER=eless man grep # Launches man pages in eless (terminal mode), if the env var PAGER is set to eless (does not work on macOS). 95 | PAGER=less man -P eless grep # Launches man pages in eless (terminal mode), if the env var PAGER is *not* set to eless (works on macOS). 96 | PAGER=\"eless --gui\" man grep # Launches man pages in eless (GUI mode), if the env var PAGER is set to \"eless --gui\" (does not work on macOS). 97 | PAGER=less man -P \"eless --gui\" grep # Launches man pages in eless (GUI mode), if the env var PAGER is *not* set to \"eless --gui\" (works on macOS). 98 | " 99 | 100 | set -o pipefail 101 | set -e # Error out and exit the script when any line in this script returns an error 102 | set -u # Error out when unbound variables are found 103 | 104 | # IFS=$'\n\t' # Separate fields in a sequence only at newlines and tab characters 105 | IFS=$' ' # Separate each field in a sequence at space characters 106 | 107 | help=0 108 | debug=0 109 | no_window_arg="-nw" 110 | emacs_args=("${no_window_arg}") # Run emacs with -nw by default 111 | piped_data_file='' 112 | cmd='' 113 | 114 | input_from_pipe_flag=0 115 | output_to_pipe_flag=0 116 | 117 | # Use the emacs binary if set by the environment variable EMACS, else set that 118 | # variable to emacs. 119 | EMACS="${EMACS:-emacs}" 120 | 121 | # http://redsymbol.net/articles/bash-exit-traps/ 122 | function cleanup { 123 | if [[ ! -z "${piped_data_file}" ]] && [[ ${debug} -eq 0 ]] 124 | then 125 | # Remove /tmp/foo.XXXXXX, /tmp/foo.XXXXXX.noblank 126 | rm -f "${piped_data_file}" "${piped_data_file}.noblank" 127 | fi 128 | } 129 | trap cleanup EXIT 130 | 131 | function debug { 132 | if [[ $debug -eq 1 ]] 133 | then 134 | function debug { 135 | echo -e "DEBUG: $*" >&2 136 | } 137 | debug "$@" 138 | else 139 | function debug { 140 | true 141 | } 142 | fi 143 | } 144 | 145 | function eless_print_version { 146 | if [[ "${eless_version}" == "master" ]] 147 | then 148 | echo "Eless Version ${eless_git_hash} (commit hash of current master~1)" 149 | echo "https://github.com/kaushalmodi/eless/tree/${eless_version}" 150 | else 151 | echo "Eless Version ${eless_version}" 152 | echo "https://github.com/kaushalmodi/eless/tree/${eless_version}" 153 | fi 154 | } 155 | 156 | for var in "$@" 157 | do 158 | if [[ "${var}" == '-D' ]] 159 | then 160 | eless_print_version 161 | export ELESS_DEBUG=1 162 | debug=1 163 | fi 164 | done 165 | 166 | # https://gist.github.com/davejamesmiller/1966557 167 | if [[ -t 0 ]] # Script is called normally - Terminal input (keyboard) - interactive 168 | then 169 | # eless foo 170 | # eless foo | cat - 171 | debug "--> Input from terminal" 172 | input_from_pipe_flag=0 173 | else # Script is getting input from pipe or file - non-interactive 174 | # echo bar | eless foo 175 | # echo bar | eless foo | cat - 176 | piped_data_file="$(mktemp -t emacs-stdin-"$USER".XXXXXXX)" # https://github.com/koalaman/shellcheck/wiki/SC2086 177 | debug "Piped data file : $piped_data_file" 178 | # https://github.com/kaushalmodi/eless/issues/21#issuecomment-366141999 179 | cat > "${piped_data_file}" 180 | debug "--> Input from pipe/file" 181 | input_from_pipe_flag=1 182 | fi 183 | 184 | # http://stackoverflow.com/a/911213/1219634 185 | if [[ -t 1 ]] # Output is going to the terminal 186 | then 187 | # eless foo 188 | # echo bar | eless foo 189 | debug " Output to terminal -->" 190 | output_to_pipe_flag=0 191 | else # Output is going to a pipe, file? 192 | # eless foo | cat - 193 | # echo bar | eless foo | cat - 194 | debug " Output to a pipe -->" 195 | output_to_pipe_flag=1 196 | fi 197 | 198 | for var in "$@" 199 | do 200 | debug "var : $var" 201 | 202 | if [[ "${var}" == '-D' ]] 203 | then 204 | : # Put just a colon to represent null operation # https://unix.stackexchange.com/a/133976/57923 205 | # Do not pass -D option to emacs. 206 | elif [[ "${var}" == '-V' ]] 207 | then 208 | eless_print_version 209 | exit 0 210 | elif [[ "${var}" == '-' ]] 211 | then 212 | : # Discard the '-'; it does nothing. (for the cases where a user might do "echo foo | eless -") 213 | elif [[ "${var}" == '-nw' ]] 214 | then 215 | : # Ignore the user-passed "-nw" option; we are adding it by default. 216 | elif [[ "${var}" == '-h' ]] # Do not hijack --help; use that to show emacs help 217 | then 218 | help=1 219 | elif [[ "${var}" == '--gui' ]] 220 | then 221 | # Delete the ${no_window_arg} from ${emacs_args[@]} array if user passed "--gui" option 222 | # http://stackoverflow.com/a/16861932/1219634 223 | emacs_args=("${emacs_args[@]/${no_window_arg}}") 224 | else 225 | # Collect all other arguments passed to eless and forward them to emacs. 226 | emacs_args=("${emacs_args[@]}" "${var}") 227 | fi 228 | done 229 | 230 | if [[ ${help} -eq 1 ]] 231 | then 232 | eless_print_version 233 | echo "${h}" 234 | exit 0 235 | fi 236 | 237 | debug "Raw Args : $*" # https://github.com/koalaman/shellcheck/wiki/SC2145 238 | debug "Emacs Args : ${emacs_args[*]}" 239 | 240 | function emacs_Q_view_mode { 241 | 242 | # Here $@ is the list of arguments passed specifically to emacs_Q_view_mode, 243 | # not to eless. 244 | debug "Args passed to emacs_Q_view_mode : $*" 245 | 246 | ${EMACS} -Q "$@" \ 247 | --eval '(progn 248 | (when (getenv "ELESS_DEBUG") 249 | (setq debug-on-error t)) 250 | 251 | ;; Keep the default-directory to be the same from where 252 | ;; this script was launched from; useful during C-x C-f 253 | (setq default-directory "'"$(pwd)"'/") 254 | 255 | ;; No clutter 256 | (menu-bar-mode -1) 257 | (if (fboundp (function tool-bar-mode)) (tool-bar-mode -1)) 258 | 259 | ;; Show line and column numbers in the mode-line 260 | (line-number-mode 1) 261 | (column-number-mode 1) 262 | 263 | (setq-default indent-tabs-mode nil) ;Use spaces instead of tabs for indentation 264 | (setq x-select-enable-clipboard t) 265 | (setq x-select-enable-primary t) 266 | (setq save-interprogram-paste-before-kill t) 267 | (setq require-final-newline t) 268 | (setq visible-bell t) 269 | (setq load-prefer-newer t) 270 | (setq ediff-window-setup-function (function ediff-setup-windows-plain)) 271 | 272 | (setq org-src-fontify-natively t) ;Syntax-highlight source blocks in org 273 | 274 | (fset (quote yes-or-no-p) (quote y-or-n-p)) ;Use y or n instead of yes or no 275 | 276 | (setq ido-save-directory-list-file nil) ;Do not save ido history 277 | (ido-mode 1) 278 | (setq ido-enable-flex-matching t) ;Enable fuzzy search 279 | (setq ido-everywhere t) 280 | (setq ido-create-new-buffer (quote always)) ;Create a new buffer if no buffer matches substringv 281 | (setq ido-use-filename-at-point (quote guess)) ;Find file at point using ido 282 | (add-to-list (quote ido-ignore-buffers) "*Messages*") 283 | 284 | (setq isearch-allow-scroll t) ;Allow scrolling using isearch 285 | ;; DEL during isearch should edit the search string, not jump back to the previous result. 286 | (define-key isearch-mode-map [remap isearch-delete-char] (function isearch-del-char)) 287 | 288 | ;; Truncate long lines by default 289 | (setq truncate-partial-width-windows nil) ;Respect the value of truncate-lines 290 | (toggle-truncate-lines +1) 291 | 292 | (global-hl-line-mode 1) 293 | 294 | (defun eless/keep-lines () 295 | (interactive) 296 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 297 | (save-excursion 298 | (goto-char (point-min)) 299 | (call-interactively (function keep-lines))))) 300 | 301 | (defun eless/delete-matching-lines () 302 | (interactive) 303 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 304 | (save-excursion 305 | (goto-char (point-min)) 306 | (call-interactively (function delete-matching-lines))))) 307 | 308 | (defun eless/frame-width-half (double) 309 | (interactive "P") 310 | (let ((frame-resize-pixelwise t) ;Do not round frame sizes to character h/w 311 | (factor (if double 2 0.5))) 312 | (set-frame-size nil (round (* factor (frame-text-width))) (frame-text-height) :pixelwise))) 313 | (defun eless/frame-width-double () 314 | (interactive) 315 | (eless/frame-width-half :double)) 316 | 317 | (defun eless/frame-height-half (double) 318 | (interactive "P") 319 | (let ((frame-resize-pixelwise t) ;Do not round frame sizes to character h/w 320 | (factor (if double 2 0.5))) 321 | (set-frame-size nil (frame-text-width) (round (* factor (frame-text-height))) :pixelwise))) 322 | (defun eless/frame-height-double () 323 | (interactive) 324 | (eless/frame-height-half :double)) 325 | 326 | (defun eless/revert-buffer-retain-view-mode () 327 | (interactive) 328 | (let ((view-mode-state view-mode)) ;save the current state of view-mode 329 | (revert-buffer) 330 | (when view-mode-state 331 | (view-mode 1)))) 332 | 333 | (defun eless/enable-diff-mode-maybe () 334 | (let* ((max-line 10) ;Search first MAX-LINE lines of the buffer 335 | (bound (save-excursion 336 | (goto-char (point-min)) 337 | (forward-line max-line) 338 | (point)))) 339 | (save-excursion 340 | (let ((diff-mode-enable)) 341 | (goto-char (point-min)) 342 | (when (and ;First header line of unified/context diff begins with "--- "/"*** " 343 | (thing-at-point (quote line)) ;Prevent error in string-match if the buffer is empty 344 | (string-match "^\\(---\\|\\*\\*\\*\\) " (thing-at-point (quote line))) 345 | ;; Second header line of unified/context diff begins with "+++ "/"--- " 346 | (progn 347 | (forward-line 1) 348 | (string-match "^\\(\\+\\+\\+\\|---\\) " (thing-at-point (quote line))))) 349 | (setq diff-mode-enable t)) 350 | ;; Check if the diff format is neither context nor unified 351 | (unless diff-mode-enable 352 | (goto-char (point-min)) 353 | (when (re-search-forward "^\\(?:[0-9]+,\\)?[0-9]+\\([adc]\\)\\(?:[0-9]+,\\)?[0-9]+$" bound :noerror) 354 | (forward-line 1) 355 | (let ((diff-type (match-string-no-properties 1))) 356 | (cond 357 | ;; Line(s) added 358 | ((string= diff-type "a") 359 | (when (re-search-forward "^> " nil :noerror) 360 | (setq diff-mode-enable t))) 361 | ;; Line(s) deleted or changed 362 | (t 363 | (when (re-search-forward "^< " nil :noerror) 364 | (setq diff-mode-enable t))))))) 365 | (when diff-mode-enable 366 | (message "Auto-enabling diff-mode") 367 | (diff-mode) 368 | (rename-buffer "*Diff*" :unique) 369 | (view-mode 1)))))) ;Re-enable view-mode 370 | 371 | (setq whitespace-style 372 | (quote (face ;Enable all visualization via faces 373 | trailing ;Show white space at end of lines 374 | tabs ;Show tabs using faces 375 | spaces space-mark ;space-mark shows spaces as dots 376 | space-before-tab space-after-tab ;mix of tabs and spaces 377 | indentation))) ;Highlight spaces/tabs at BOL depending on indent-tabs-mode 378 | (add-hook (quote diff-mode-hook) (function whitespace-mode)) 379 | 380 | (defun eless/enable-ansi-color-maybe () 381 | (save-excursion 382 | (let* ((max-line 100) ;Search first MAX-LINE lines of the buffer 383 | (bound (progn 384 | (goto-char (point-min)) 385 | (forward-line max-line) 386 | (point))) 387 | (ESC "\u001b") 388 | ;; Example ANSI codes: ^[[0;36m, or ^[[0m where ^[ is the ESC char 389 | (ansi-regexp (concat ESC "\\[" "[0-9]+\\(;[0-9]+\\)*m"))) 390 | (goto-char (point-min)) 391 | (when (re-search-forward ansi-regexp bound :noerror) 392 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 393 | (message "Auto-converting ANSI codes to colors") 394 | (require (quote ansi-color)) 395 | (ansi-color-apply-on-region (point-min) (point-max))))))) 396 | 397 | (defun eless/kill-emacs-or-buffer (&optional kill-emacs) 398 | (interactive "P") 399 | (let ((num-non-special-buffers 0)) 400 | (dolist (buf (buffer-list)) 401 | (unless (string-match "\\`[ *]" (buffer-name buf)) ;Do not count buffers with names starting with space or * 402 | (setq num-non-special-buffers (+ 1 num-non-special-buffers))) 403 | (with-current-buffer buf 404 | ;; Mark all view-mode buffers as "not modified" to prevent save prompt on 405 | ;; quitting. 406 | (when view-mode 407 | (set-buffer-modified-p nil) 408 | (when (local-variable-p (quote kill-buffer-hook)) 409 | (setq kill-buffer-hook nil))))) 410 | (if (or kill-emacs 411 | (<= num-non-special-buffers 1)) 412 | (save-buffers-kill-emacs) 413 | (kill-buffer (current-buffer))))) ;Else only kill the current buffer 414 | 415 | (defun eless/save-buffers-maybe-and-kill-emacs () 416 | (interactive) 417 | (eless/kill-emacs-or-buffer :kill-emacs)) 418 | 419 | (defun eless/dired-mode-customization () 420 | ;; dired-find-file is bound to "f" and "RET" by default 421 | ;; So changing the "RET" binding to dired-view-file so that the file opens 422 | ;; in view-mode in the spirit of eless. 423 | (define-key dired-mode-map (kbd "RET") (function dired-view-file)) 424 | (define-key dired-mode-map (kbd "E") (function wdired-change-to-wdired-mode)) 425 | (define-key dired-mode-map (kbd "Q") (function quit-window)) 426 | (define-key dired-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 427 | (add-hook (quote dired-mode-hook) (function eless/dired-mode-customization)) 428 | 429 | (defun eless/Man-mode-customization () 430 | (define-key Man-mode-map (kbd "Q") (function quit-window)) 431 | (define-key Man-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 432 | (add-hook (quote Man-mode-hook) (function eless/Man-mode-customization)) 433 | 434 | (defun eless/Info-mode-customization () 435 | (define-key Info-mode-map (kbd "Q") (function quit-window)) 436 | (define-key Info-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 437 | (add-hook (quote Info-mode-hook) (function eless/Info-mode-customization)) 438 | 439 | (defun eless/tar-mode-customization () 440 | (define-key tar-mode-map (kbd "RET") (function tar-view)) 441 | (define-key tar-mode-map (kbd "Q") (function quit-window)) 442 | (define-key tar-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 443 | (add-hook (quote tar-mode-hook) (function eless/tar-mode-customization)) 444 | 445 | (cond 446 | ((derived-mode-p (quote dired-mode)) (eless/dired-mode-customization)) 447 | ((derived-mode-p (quote Man-mode)) (eless/Man-mode-customization)) 448 | ((derived-mode-p (quote Info-mode)) (eless/Info-mode-customization)) 449 | ((derived-mode-p (quote tar-mode)) (eless/tar-mode-customization)) 450 | (t ;Enable view-mode if none of the above major-modes are active 451 | ;; Auto-enable diff-mode. For example, when doing "diff foo bar | eless" 452 | (eless/enable-diff-mode-maybe) 453 | ;; Auto-convert ANSI codes to colors. For example, when doing "ls --color=always | eless" 454 | (eless/enable-ansi-color-maybe) 455 | (view-mode 1))) 456 | 457 | (eval-after-load (quote view) 458 | (quote 459 | (progn 460 | (define-key view-mode-map (kbd "!") (function eless/delete-matching-lines)) 461 | (define-key view-mode-map (kbd "&") (function eless/keep-lines)) 462 | (define-key view-mode-map (kbd "0") (function delete-window)) 463 | (define-key view-mode-map (kbd "1") (function delete-other-windows)) 464 | (define-key view-mode-map (kbd "A") (function auto-revert-tail-mode)) 465 | (define-key view-mode-map (kbd "D") (function dired)) 466 | (define-key view-mode-map (kbd "N") (function next-error)) ;Next line in *occur* 467 | (define-key view-mode-map (kbd "P") (function previous-error)) ;Previous line in *occur* 468 | (define-key view-mode-map (kbd "K") (function eless/delete-matching-lines)) 469 | (define-key view-mode-map (kbd "a") (function auto-revert-mode)) 470 | (define-key view-mode-map (kbd "g") (function eless/revert-buffer-retain-view-mode)) 471 | (define-key view-mode-map (kbd "k") (function eless/keep-lines)) 472 | (define-key view-mode-map (kbd "n") (function next-line)) 473 | (define-key view-mode-map (kbd "o") (function occur)) 474 | (define-key view-mode-map (kbd "p") (function previous-line)) 475 | (define-key view-mode-map (kbd "q") (function eless/kill-emacs-or-buffer)) 476 | (define-key view-mode-map (kbd "t") (function toggle-truncate-lines))))) 477 | 478 | ;; Global custom bindings 479 | (global-set-key (kbd "M-/") (function hippie-expand)) 480 | (global-set-key (kbd "C-x C-b") (function ibuffer)) 481 | (global-set-key (kbd "C-x C-c") (function eless/save-buffers-maybe-and-kill-emacs)) 482 | (global-set-key (kbd "C-x C-f") (function view-file)) 483 | (global-set-key (kbd "C-c q") (function query-replace-regexp)) 484 | (global-set-key (kbd "") (function eless/revert-buffer-retain-view-mode)) 485 | 486 | (when (display-graphic-p) 487 | (eval-after-load (quote view) 488 | (quote 489 | (progn 490 | (define-key view-mode-map (kbd "+") (function text-scale-adjust)) 491 | (define-key view-mode-map (kbd "-") (function text-scale-adjust)) 492 | (define-key view-mode-map (kbd "=") (function text-scale-adjust))))) 493 | (global-set-key (kbd "C-") (function eless/frame-width-double)) 494 | (global-set-key (kbd "C-") (function eless/frame-width-half)) 495 | (global-set-key (kbd "C-") (function eless/frame-height-double)) 496 | (global-set-key (kbd "C-") (function eless/frame-height-half))) 497 | 498 | (let* ((cfg-file "elesscfg") 499 | (cfg-path (if (fboundp (quote locate-user-emacs-file)) 500 | (locate-user-emacs-file cfg-file) 501 | ;; For emacs older than 23.1. 502 | (let ((home (file-name-as-directory (getenv "HOME")))) 503 | (or (expand-file-name cfg-file (concat home ".emacs.d")) 504 | (expand-file-name cfg-file home)))))) 505 | (unless (load cfg-path :noerror) 506 | (load-theme (quote tango-dark) :no-confirm) 507 | ;; The tango-dark theme is good except for the bright yellow hl-line face 508 | (custom-theme-set-faces 509 | (quote user) 510 | (quote (hl-line ((t (:background "color-238"))))) 511 | (quote (Man-overstrike ((t (:foreground "#f3dc55" :weight normal)))))))) ;gold yellow 512 | )' 2>/dev/null "${piped_data_file}.noblank" 538 | 539 | # Now parse only the first line of that ${piped_data_file}.noblank file. 540 | first_line_piped_data=$(head -n 1 "${piped_data_file}.noblank") 541 | debug "first_line_piped_data = ${first_line_piped_data}" 542 | 543 | # It is not mandatory for the below perl regex to always match. So OR it with 544 | # "true" so that "set -e" does not kill the script at this point. 545 | 546 | # The first line of man pages is assumed to be 547 | # FOO(1) optional something something FOO(1) 548 | # For some odd reason, the "BASH_BUILTINS" man page is named just 549 | # "builtins"; below deals with that corner case. 550 | # .. faced this problem when trying to do "man read | eless". 551 | # If the man page name is completely in upper-case, convert it 552 | # to lower-case. 553 | man_page=$(echo "${first_line_piped_data}" \ 554 | | perl -ne '/^([A-Za-z0-9-_]+\([a-z0-9]+\))(?=\s+.*?\1$)/ and print $1' \ 555 | | perl -pe 's/bash_builtins/builtins/i' \ 556 | | perl -pe 's/xsel\(1x\)/xsel/i' \ 557 | | perl -pe 's/^[A-Z0-9-_()]+$/\L$_/' \ 558 | || true) 559 | # Using perl expression above instead of below grep (which requires 560 | # GNU grep -- not available by default on macOS): 561 | # grep -Po '^([A-Za-z-_]+\([0-9]+\))(?=\s+.*?\1$)' 562 | 563 | # If it's not a regular man page, check if it's a Perl man page. 564 | if [[ -z ${man_page} ]] 565 | then 566 | # The first line of Perl man pages is assumed to be 567 | # Foo::Bar(1zoo) something something Foo::Bar(1zoo) 568 | # Example: PAGER=eless man Net::FTP or PAGER=less man Net::FTP | eless 569 | # If the man page name is completely in upper-case, convert it 570 | # to lower-case. 571 | # Example: PAGER=eless man error::pass1 or PAGER=less man error::pass1 | eless 572 | man_page=$(echo "${first_line_piped_data}" \ 573 | | perl -ne '/^([A-Za-z0-9-_]+::[A-Za-z0-9-_]+)(\([a-z0-9]+\))(?=\s+.*?\1\2$)/ and print $1' \ 574 | | perl -pe 's/^[A-Z0-9-_]+::[A-Z0-9-_]+$/\L$_/' \ 575 | || true) 576 | fi 577 | 578 | # The first line of Python package MODULE help is assumed to be 579 | # "Help on package MODULE:" OR "Help on module MODULE:" OR "Help on SOMETHING in module MODULE:" 580 | # Examples: PAGER=eless python3; help('shlex') -> "Help on module shlex:" 581 | # PAGER=eless python3; help('iter') -> "Help on built-in function iter in module builtins:" 582 | # PAGER=eless python3; help('exit') -> "Help on Quitter in module _sitebuiltins object:" 583 | python_module_help=$(echo "${first_line_piped_data}" \ 584 | | perl -ne '/^Help on (?:.+ in )*(?:module|package) (.*)(?=:$)/ and print $1' \ 585 | || true) 586 | # Using perl expression above instead of below grep (which requires 587 | # GNU grep -- not available by default on macOS): 588 | # grep -Po '^Help on (.+ in )*(module|package) \K(.*)(?=:$)' 589 | 590 | # The first line of info manuals is assumed to be 591 | # /path/to/some.info or /path/to/some.info.gz 592 | # Example: "/home/kmodi/usr_local/apps/6/emacs/26/share/info/emacs.info.gz" -> "emacs" 593 | info_man=$(echo "${first_line_piped_data}" \ 594 | | perl -ne '/^(?:.*\/)*([^\/]+)(?=\.info(?:\-[0-9]+)*(?:\.gz)*$)/ and print $1' \ 595 | || true) 596 | # Using perl expression above instead of below grep (which requires 597 | # GNU grep -- not available by default on macOS): 598 | # grep -Po '^(.*/)*\K[^/]+(?=\.info(\-[0-9]+)*(\.gz)*$)' 599 | 600 | if [[ ! -z ${man_page} ]] 601 | then 602 | # After setting PAGER variable globally to eless (example, using export on bash, 603 | # setenv on (t)csh, try something like `man grep'. That will launch the man 604 | # page in eless. 605 | debug "Man Page = ${man_page}" 606 | 607 | cmd="emacs_Q_view_mode \ 608 | ${emacs_args[*]} \ 609 | --eval '(progn 610 | (man \"${man_page}\") 611 | ;; Below workaround is only for emacs 24.5.x and older releases 612 | ;; where the man page takes some time to load. 613 | ;; 1-second delay before killing the *scratch* window 614 | ;; seems to be sufficient 615 | (when (version<= emacs-version \"24.5.99\") 616 | (sit-for 1)) 617 | (delete-window))'" 618 | elif [[ ! -z ${python_module_help} ]] 619 | then 620 | debug "Python Module = ${python_module_help}" 621 | 622 | cmd="emacs_Q_view_mode \ 623 | ${emacs_args[*]} \ 624 | --eval '(progn 625 | (man \"${piped_data_file}\") 626 | ;; Below workaround is only for emacs 24.5.x and older releases 627 | ;; where the man page takes some time to load. 628 | ;; 1-second delay before killing the *scratch* window 629 | ;; seems to be sufficient 630 | (when (version<= emacs-version \"24.5.99\") 631 | (sit-for 1)) 632 | (delete-window) 633 | (rename-buffer \"${python_module_help}\"))'" 634 | elif [[ ! -z ${info_man} ]] 635 | then 636 | # Try something like `info emacs | eless'. 637 | # That will launch the Info manual in eless. 638 | debug "Info Manual = ${info_man}" 639 | 640 | cmd="emacs_Q_view_mode \ 641 | ${emacs_args[*]} \ 642 | --eval '(progn 643 | (info (downcase \"${info_man}\")))'" 644 | else # No man page or info manual detected 645 | debug "No man page or info manual detected" 646 | 647 | cmd="emacs_Q_view_mode ${piped_data_file} \ 648 | ${emacs_args[*]} \ 649 | --eval '(progn 650 | (set-visited-file-name nil) 651 | (rename-buffer \"*Stdin*\" :unique))'" 652 | fi 653 | # Below else condition is reached when you do this: 654 | # eless foo.txt 655 | else 656 | cmd="emacs_Q_view_mode ${emacs_args[*]}" 657 | fi 658 | fi 659 | 660 | debug "Eless Command : $cmd" 661 | 662 | eval "$cmd" 663 | 664 | # References: 665 | # http://superuser.com/a/843744/209371 666 | # http://stackoverflow.com/a/15330784/1219634 - /dev/stdin (Kept just for 667 | # reference, not using this in this script any more.) 668 | # https://github.com/dj08/utils-generic/blob/master/eless 669 | -------------------------------------------------------------------------------- /tests/eless/eless.org: -------------------------------------------------------------------------------- 1 | # Time-stamp: <2018-05-29 12:11:15 kmodi> 2 | #+title: eless -- A Better less 3 | #+author: Kaushal Modi 4 | 5 | #+startup: shrink 6 | 7 | #+texinfo_dir_category: Emacs 8 | #+texinfo_dir_title: Eless: (eless). 9 | #+texinfo_dir_desc: Use emacs view-mode as less 10 | 11 | # https://raw.githubusercontent.com/magit/magit/master/Documentation/magit.org 12 | # #+texinfo_deffn: t 13 | # #+texinfo_class: info+ 14 | 15 | #+html_head: 16 | #+html_head: 17 | #+html_head: 18 | #+html_head: 19 | 20 | # No list bullets in task/checkbox lists 21 | #+html_head: 22 | 23 | # Make the tangled shell scripts executables 24 | #+property: header-args:shell :shebang "#!/usr/bin/env bash" 25 | 26 | #+macro: issue =eless= issue #[[https://github.com/kaushalmodi/eless/issues/$1][$1]] 27 | #+macro: user [[https://github.com/$1][$2]] 28 | 29 | # http://lists.gnu.org/r/emacs-orgmode/2017-04/msg00181.html 30 | # You need to have set `org-export-allow-bind-keywords' to t for below 31 | # to work. 32 | #+bind: org-html-inline-image-rules (("file" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'") ("http" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'") ("https" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'") ("https" . "svg\\?branch=")) 33 | 34 | # Github ribbon 35 | #+begin_export html 36 | 48 | 49 | Fork me on GitHub 50 | 51 | #+end_export 52 | 53 | * Readme :readme: 54 | :PROPERTIES: 55 | :EXPORT_FILE_NAME: README 56 | :EXPORT_TITLE: Eless - A Better Less 57 | :END: 58 | [[https://travis-ci.org/kaushalmodi/eless][https://travis-ci.org/kaushalmodi/eless.svg?branch=master]] | [[https://eless.scripter.co][*Documentation*]] 59 | 60 | ----- 61 | 62 | [[https://github.com/kaushalmodi/eless][*eless*]] is a combination of Bash script and a minimal emacs 63 | =view-mode= config. 64 | 65 | This script is designed to: 66 | 67 | - Be portable -- Just one bash script to download to run 68 | - Be independent of a user's emacs config 69 | - /You can still [[https://eless.scripter.co/#user-config-override][customize]] the =eless= config if you like./ 70 | - Not require an emacs server to be already running 71 | 72 | It was created out of a need to have something /like/ =less= (in the 73 | sense of /launch quickly, do, and quit/), but /better/ in these ways: 74 | 75 | - Syntax highlighting 76 | - Org-mode file rendering 77 | - A better navigable man page viewer 78 | - A better Info viewer 79 | - Dired, especially =wdired= (batch edit symbolic links, for 80 | example?) 81 | - Colored diffs, =git diff=, =git log=, =ls=, etc. (auto ANSI 82 | detection) 83 | - Filter log files to only show (or not show) lines matching a regexp 84 | - Auto-revert log files when I want (like =tail -f=) 85 | - Quickly change frame and font sizes 86 | - .. and more; basically everything that emacs has to offer! 87 | 88 | I call it =eless= and here's a little taste of what it looks like: 89 | 90 | #+attr_html: :width 1000px 91 | [[https://raw.githubusercontent.com/kaushalmodi/eless/master/docs/images/eless-examples.png][https://raw.githubusercontent.com/kaushalmodi/eless/master/docs/images/eless-examples.png]] 92 | 93 | /Shown above, starting from top left image and proceeding clock-wise../ 94 | - =eless eless.org= 95 | - =rg --color=ansi 'man pages' | eless= (rg[[https://github.com/BurntSushi/ripgrep][?]]) 96 | - =man grep= (I have set my =PAGER= env var to =eless=.) 97 | - =info eless= (I have aliased =info= to ='\info \!* | eless'= in my 98 | tcsh shell.) 99 | - =eless .= (Shows the current directory contents in =dired=.) 100 | - =diff= of =eless.org= with an older saved version and piping the 101 | result to =eless= 102 | 103 | *Meta Features* 104 | 105 | - [X] This script passes [[http://www.shellcheck.net][ShellCheck]], and 106 | - [X] Unofficial Bash [[http://redsymbol.net/articles/unofficial-bash-strict-mode][strict mode]] is enabled. 107 | - [X] Always-in-sync documentation as the =eless= script and 108 | documentation are generated using Org Babel from [[https://github.com/kaushalmodi/eless/blob/master/eless.org][one file]] (even this 109 | README). 110 | - [X] The [[https://eless.scripter.co][documentation site]] is generated on-the-fly on Netlify using 111 | that same /one file/. 112 | - [X] This bash script has tests too! 113 | ** Requirements 114 | :PROPERTIES: 115 | :CUSTOM_ID: requirements 116 | :END: 117 | |----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 118 | | | <70> | 119 | | Software | Details | 120 | |----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 121 | | =emacs= | If only *running* the =eless= script, the mininum required emacs version is 22.1 (manually tested). If *developing* (running =make all html test=), the minimum required version is *24.4* (24.4 onwards versions are auto-tested on Travis). | 122 | | =bash= | This is a =bash= script. So even if you don't use =bash= shell, you need to have the =bash= binary discoverable through your environment variable =PATH=. /Tested to work in =tcsh= shell on RHEL 6.6./ | 123 | | =perl= | Perl is used to replace =grep -Po= and case-insensitive =sed= based replacements (using =/I=) as those features are available only in GNU versions of =grep= and =sed= (which are not present by default on /macOS/ systems). /Tested with Perl v5.16.3 on RHEL 6.6./ | 124 | |----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 125 | 126 | - NOTE :: If the environment variable =EMACS= is set, =eless= uses that 127 | as the emacs binary, else it defaults to using =emacs= as 128 | emacs binary. 129 | ** Installation 130 | :PROPERTIES: 131 | :CUSTOM_ID: installation 132 | :END: 133 | I do not have an installation script for this project, but you can 134 | install it quickly with few manual steps. 135 | *** Clone this rep 136 | :PROPERTIES: 137 | :CUSTOM_ID: clone-this-rep 138 | :END: 139 | For the following instructions, let's assume that you clone this repo 140 | to =~/downloads/eless=. 141 | #+begin_src shell 142 | git clone https://github.com/kaushalmodi/eless ~/downloads/eless 143 | #+end_src 144 | *** Get the =eless= executable 145 | :PROPERTIES: 146 | :CUSTOM_ID: get-the-eless-executable 147 | :END: 148 | Assuming that you have =~/.local/bin/= directory added to your 149 | environment variable =PATH=: 150 | #+begin_src shell 151 | cp -f ~/downloads/eless/eless ~/.local/bin/. 152 | # Just making sure that the script has executable permissions. 153 | chmod +x ~/.local/bin/eless 154 | #+end_src 155 | *** Get =eless= documentation 156 | :PROPERTIES: 157 | :CUSTOM_ID: get-eless-documentation 158 | :END: 159 | And finally, let's say have a =~/.local/share/= directory. Then copy 160 | the documentation to an =eless/= sub-directory in that. 161 | #+begin_src shell 162 | mkdir -p ~/.local/share/eless/info 163 | # Org source - One source for script + documentation 164 | cp -f ~/downloads/eless/eless.org ~/.local/share/eless/eless.org 165 | cp -f ~/downloads/eless/docs/eless.info ~/.local/share/eless/info/eless.info 166 | cp -f ~/downloads/eless/docs/dir ~/.local/share/eless/info/dir 167 | #+end_src 168 | Make sure that you add =~/.local/share/eless/info= to your environment 169 | variable =INFOPATH=. 170 | *** Installation Directory Structure 171 | :PROPERTIES: 172 | :CUSTOM_ID: installation-directory-structure 173 | :END: 174 | In the end, the file structure for the newly copied files should look 175 | like this: 176 | #+begin_example 177 | ~/.local 178 | ├── bin/ 179 | │ └── eless 180 | └── share/ 181 | └── eless/ 182 | ├── eless.org 183 | └── info/ 184 | ├── eless.info 185 | └── dir 186 | #+end_example 187 | *** Note 188 | If you plan to keep the cloned =eless= repo updated to the latest 189 | master all the time (which I recommend), then you may even create 190 | symlinks to those instead of copying them in the above steps. 191 | ** Try it out 192 | :PROPERTIES: 193 | :CUSTOM_ID: try-it-out 194 | :END: 195 | Here are some usage examples: 196 | #+begin_src shell :noweb yes 197 | <> 198 | <> 199 | #+end_src 200 | - NOTE :: Above examples are tested to work in a *=bash= 201 | shell*. Specifically, examples like ~PAGER=eless man grep~ 202 | might need to be adapted for the shell you are using, 203 | [[#example-eless-config-in-bash][and also the OS]]. 204 | ** Contributors 205 | :PROPERTIES: 206 | :CUSTOM_ID: contributors 207 | :END: 208 | - Thanks to {{{user(sshaw,Skye Shaw)}}} for helping improving =eless= 209 | so that it can run on /macOS/ and emacs 22.1, and suggesting Bash 210 | =trap=. 211 | - Thanks to {{{user(iqbalansari,Iqbal Ansari)}}} for adding support to 212 | read piped data in =emacs -Q -nw=. 213 | - Thanks to {{{user(alphapapa,Adam Porter)}}} for adding a =bash= 214 | /collapsing function/ for debug statements, and testing out and 215 | providing suggestions on improving the =eless= build flow. 216 | * Eless Options 217 | :PROPERTIES: 218 | :EXPORT_FILE_NAME: eless-options 219 | :CUSTOM_ID: eless-options 220 | :END: 221 | # Do "C-c '" in the below block to edit the org table 222 | #+begin_src org :noweb-ref noweb-eless-options :exports results :results output replace 223 | |--------+--------------------------| 224 | | Option | Description | 225 | |--------+--------------------------| 226 | | -h | Show this help and quit | 227 | | --gui | Run eless in GUI mode | 228 | | -V | Print version and quit | 229 | | -D | Run with debug messages | 230 | |--------+--------------------------| 231 | #+end_src 232 | * =view-mode= Common Bindings 233 | :PROPERTIES: 234 | :EXPORT_FILE_NAME: view-mode-common-bindings 235 | :CUSTOM_ID: view-mode-common-bindings 236 | :END: 237 | #+begin_src org :noweb-ref noweb-view-mode-common-bindings :exports results :results output replace 238 | |--------------+------------------------------------------------------------------------------| 239 | | Binding | Description | 240 | |--------------+------------------------------------------------------------------------------| 241 | | SPC | Scroll forward 'page size' lines. With prefix scroll forward prefix lines. | 242 | | DEL or S-SPC | Scroll backward 'page size' lines. With prefix scroll backward prefix lines. | 243 | | | (If your terminal does not support this, use xterm instead or using C-h.) | 244 | | RET | Scroll forward one line. With prefix scroll forward prefix line(s). | 245 | | y | Scroll backward one line. With prefix scroll backward prefix line(s). | 246 | | s | Do forward incremental search. | 247 | | r | Do reverse incremental search. | 248 | | e | Quit the 'view-mode' and use that emacs session as usual to modify | 249 | | | the opened file if needed. | 250 | |--------------+------------------------------------------------------------------------------| 251 | #+end_src 252 | * Custom Bindings 253 | :PROPERTIES: 254 | :EXPORT_FILE_NAME: eless-custom-bindings 255 | :CUSTOM_ID: eless-custom-bindings 256 | :END: 257 | #+begin_src org :noweb-ref noweb-custom-bindings :exports results :results output replace 258 | |--------------+------------------------------------------------------------| 259 | | Binding | Description | 260 | |--------------+------------------------------------------------------------| 261 | | ! or K | Delete lines matching regexp | 262 | | & or k | Keep lines matching regexp | 263 | | 0 | Delete this window | 264 | | 1 | Keep only this window | 265 | | A | Auto-revert Tail Mode (like tail -f on current buffer) | 266 | | D | Dired | 267 | | N | Next error (next line in *occur*) | 268 | | P | Previous error (previous line in *occur*) | 269 | | a | Auto-revert Mode | 270 | | g or F5 | Revert buffer (probably after keep/delete lines) | 271 | | n | Next line | 272 | | o | Occur | 273 | | p | Previous line | 274 | | q | Quit emacs if at most one buffer is open, else kill buffer | 275 | | t | Toggle line truncation | 276 | | = or + or - | Adjust font size (in GUI mode) | 277 | | C-down/up | Inc/Dec frame height (in GUI mode) | 278 | | C-right/left | Inc/Dec frame width (in GUI mode) | 279 | |--------------+------------------------------------------------------------| 280 | #+end_src 281 | * Usage Examples 282 | :PROPERTIES: 283 | :EXPORT_FILE_NAME: usage-examples 284 | :CUSTOM_ID: usage-examples 285 | :END: 286 | #+begin_src shell :noweb-ref noweb-usage-examples 287 | eless foo.txt # Open foo.txt in eless in terminal (-nw) mode by default. 288 | eless foo.txt --gui # Open foo.txt in eless in GUI mode. 289 | echo 'foo' | eless # 290 | echo 'foo' | eless - # Same as above. The hyphen after eless does not matter; is anyways discarded. 291 | grep 'bar' foo.txt | eless # 292 | diff foo bar | eless # Colored diff! 293 | diff -u foo bar | eless # Colored diff for unified diff format 294 | eless . # Open dired in the current directory (enhanced 'ls') 295 | ls --color=always | eless # Auto-detect ANSI color codes and convert those to colors 296 | PAGER=eless git diff # Show git diff with ANSI coded colors 297 | eless -h | eless # See eless help ;-) 298 | info emacs | eless # Read emacs Info manual in eless 299 | eless foo.tar.xz # Read the contents of archives; emacs does the unarchiving automatically 300 | PAGER=eless python3; help('def') # Read (I)Python keyword help pages (example: help for 'def' keyword) 301 | PAGER=eless python3; help('shlex') # Read (I)Python module help pages (example: help for 'shlex' module) 302 | PAGER=eless python3; help('TYPES') # Read (I)Python topic help pages (example: help for 'TYPES' topic) 303 | PAGER=eless man grep # Launches man pages in eless (terminal mode), if the env var PAGER is set to eless (does not work on macOS). 304 | PAGER=less man -P eless grep # Launches man pages in eless (terminal mode), if the env var PAGER is *not* set to eless (works on macOS). 305 | #+end_src 306 | #+begin_src shell :noweb-ref noweb-usage-examples-eless-gui 307 | PAGER="eless --gui" man grep # Launches man pages in eless (GUI mode), if the env var PAGER is set to "eless --gui" (does not work on macOS). 308 | PAGER=less man -P "eless --gui" grep # Launches man pages in eless (GUI mode), if the env var PAGER is *not* set to eless (works on macOS). 309 | #+end_src 310 | - NOTE :: Above examples are tested to work in a *=bash= 311 | shell*. Specifically, examples like ~PAGER=eless man grep~ 312 | might need to be adapted for the shell you are using, [[#example-eless-config-in-bash][or the 313 | OS]]. 314 | * Current =eless= Version 315 | :PROPERTIES: 316 | :CUSTOM_ID: current-version 317 | :END: 318 | # Using noweb is a nifty way to do sort of search/replace in all code blocks. 319 | #+begin_src text :noweb-ref version 320 | v0.5 321 | #+end_src 322 | #+begin_src text :exports none :noweb-ref git-repo 323 | https://github.com/kaushalmodi/eless 324 | #+end_src 325 | 326 | # Get the current commit hash 327 | # To update manually , put the point in the below source block 328 | # and hit "C-c C-c" to update the git-hash source block - 329 | # https://emacs.stackexchange.com/a/13352/115 330 | #+begin_src shell :eval no-export :exports results :results output code :results_switches ":noweb-ref git-hash" 331 | git rev-parse HEAD | head -c 7 332 | #+end_src 333 | #+results: 334 | #+BEGIN_SRC shell :noweb-ref git-hash 335 | 6115f73 336 | #+END_SRC 337 | This commit hash was retrieved before (obviously) the commit was made 338 | where you see this. So if you see a commit hash when checking =eless= 339 | version, it would always refer to the one-earlier commit. 340 | * Code 341 | :PROPERTIES: 342 | :EXPORT_FILE_NAME: code 343 | :CUSTOM_ID: code 344 | :HEADER-ARGS: :tangle eless 345 | :END: 346 | ** Script Header :noexport: 347 | #+begin_src shell :noweb yes :exports none 348 | 349 | # Version: <> 350 | 351 | # This script uses the unofficial strict mode as explained in 352 | # http://redsymbol.net/articles/unofficial-bash-strict-mode 353 | # 354 | # Also checks have been done with www.shellcheck.net to have a level of 355 | # confidence that this script will be free of loopholes.. or is it? :) 356 | # 357 | # This file is tangled from <>/blob/<>/eless.org 358 | # Do NOT edit this manually. 359 | #+end_src 360 | 361 | #+begin_src shell :noweb yes :exports none 362 | eless_version='<>' 363 | eless_git_hash='<>' 364 | #+end_src 365 | ** Help String :noexport: 366 | #+begin_src shell :noweb yes :exports none 367 | h=" 368 | Script to run emacs in view-mode with some sane defaults in attempt to replace 369 | less, diff, man, (probably ls too). 370 | 371 | ,* Options to this script 372 | <> 373 | 374 | ,* Common bindings in 'view-mode' 375 | <> 376 | 377 | ,** Custom bindings 378 | <> 379 | 380 | ,** Do 'C-h b' and search for 'view-mode' to see more bindings in this mode. 381 | 382 | ,* For GNU/Linux systems, set the environment variable PAGER to 'eless' to use it 383 | for viewing man pages. 'man grep' will then show the grep man page in eless. 384 | 385 | For macOS systems, 'PAGER=less man -P \"eless --gui\" grep' will work instead. 386 | 387 | ,* Usage Examples 388 | 389 | <> 390 | PAGER=\"eless --gui\" man grep # Launches man pages in eless (GUI mode), if the env var PAGER is set to \"eless --gui\" (does not work on macOS). 391 | PAGER=less man -P \"eless --gui\" grep # Launches man pages in eless (GUI mode), if the env var PAGER is *not* set to \"eless --gui\" (works on macOS). 392 | " 393 | #+end_src 394 | ** Unofficial Bash Strict Mode 395 | :PROPERTIES: 396 | :CUSTOM_ID: unofficial-bash-strict-mode 397 | :END: 398 | The [[http://redsymbol.net/articles/unofficial-bash-strict-mode/][/Unofficial Bash Strict Mode/]] is enabled to make this script more 399 | robust and reliable. 400 | 401 | The script will error out immediately when, 402 | 1. Any command in a pipeline in this code fails. 403 | #+begin_src shell 404 | set -o pipefail 405 | #+end_src 406 | 2. Any line in this script returns an error 407 | #+begin_src shell :padline no 408 | set -e # Error out and exit the script when any line in this script returns an error 409 | #+end_src 410 | 3. Any undefined variable is referenced. 411 | #+begin_src shell :padline no 412 | set -u # Error out when unbound variables are found 413 | #+end_src 414 | 415 | #+begin_src shell :exports none 416 | # IFS=$'\n\t' # Separate fields in a sequence only at newlines and tab characters 417 | IFS=$' ' # Separate each field in a sequence at space characters 418 | #+end_src 419 | ** Initialize variables 420 | :PROPERTIES: 421 | :CUSTOM_ID: initialize-variables 422 | :END: 423 | #+begin_src shell 424 | help=0 425 | debug=0 426 | no_window_arg="-nw" 427 | emacs_args=("${no_window_arg}") # Run emacs with -nw by default 428 | piped_data_file='' 429 | cmd='' 430 | 431 | input_from_pipe_flag=0 432 | output_to_pipe_flag=0 433 | 434 | # Use the emacs binary if set by the environment variable EMACS, else set that 435 | # variable to emacs. 436 | EMACS="${EMACS:-emacs}" 437 | #+end_src 438 | ** Cleanup using =trap= 439 | :PROPERTIES: 440 | :CUSTOM_ID: cleanup-using-trap 441 | :END: 442 | The below =cleanup= function is auto-executed via Bash =trap= when the 443 | script exits /for any reason/. Read [[http://redsymbol.net/articles/bash-exit-traps/][this post on /redsymbol.net/]] for 444 | more information. 445 | #+begin_src shell 446 | # http://redsymbol.net/articles/bash-exit-traps/ 447 | function cleanup { 448 | if [[ ! -z "${piped_data_file}" ]] && [[ ${debug} -eq 0 ]] 449 | then 450 | # Remove /tmp/foo.XXXXXX, /tmp/foo.XXXXXX.noblank 451 | rm -f "${piped_data_file}" "${piped_data_file}.noblank" 452 | fi 453 | } 454 | trap cleanup EXIT 455 | #+end_src 456 | ** Debug function 457 | :PROPERTIES: 458 | :CUSTOM_ID: debug-function 459 | :END: 460 | This function redefines itself the first time it is called. When 461 | debugging is enabled, it defines itself as a function which outputs to 462 | STDERR, then calls itself to do the first output. When debugging is 463 | disabled, it defines itself as a function that does nothing, so 464 | subsequent calls do not output. 465 | #+begin_src shell 466 | function debug { 467 | if [[ $debug -eq 1 ]] 468 | then 469 | function debug { 470 | echo -e "DEBUG: $*" >&2 471 | } 472 | debug "$@" 473 | else 474 | function debug { 475 | true 476 | } 477 | fi 478 | } 479 | #+end_src 480 | Above is a =bash= /collapsing function/. See [[http://wiki.bash-hackers.org/howto/collapsing_functions][here]] and [[https://github.com/kaushalmodi/eless/issues/13][here]] for more info. 481 | #+begin_src shell :exports none :noweb yes 482 | function eless_print_version { 483 | if [[ "${eless_version}" == "master" ]] 484 | then 485 | echo "Eless Version ${eless_git_hash} (commit hash of current master~1)" 486 | echo "<>/tree/${eless_version}" 487 | else 488 | echo "Eless Version ${eless_version}" 489 | echo "<>/tree/${eless_version}" 490 | fi 491 | } 492 | #+end_src 493 | 494 | If user has passed the =-D= option, run the script in debug mode. 495 | 496 | #+begin_src shell 497 | for var in "$@" 498 | do 499 | if [[ "${var}" == '-D' ]] 500 | then 501 | eless_print_version 502 | export ELESS_DEBUG=1 503 | debug=1 504 | fi 505 | done 506 | #+end_src 507 | ** Input/Output Detection 508 | :PROPERTIES: 509 | :CUSTOM_ID: input-output-detection 510 | :END: 511 | We need this script to know: 512 | - Where it is getting the input from: 513 | - From the terminal? 514 | #+begin_src shell :tangle no 515 | eless foo 516 | #+end_src 517 | - From a pipe? 518 | #+begin_src shell :tangle no 519 | diff a b | eless 520 | #+end_src 521 | - Where the output is going to: 522 | - To the terminal? 523 | #+begin_src shell :tangle no 524 | eless foo 525 | #+end_src 526 | - To a pipe? 527 | #+begin_src shell :tangle no 528 | eless | grep foo 529 | #+end_src 530 | In this case, we do not do anything at the moment. See [[https://github.com/kaushalmodi/eless/issues/4][here]]. 531 | 532 | Below code determines that using =[[ -t 0 ]]= and =[[ -t 1]]=. 533 | #+begin_src shell 534 | # https://gist.github.com/davejamesmiller/1966557 535 | if [[ -t 0 ]] # Script is called normally - Terminal input (keyboard) - interactive 536 | then 537 | # eless foo 538 | # eless foo | cat - 539 | debug "--> Input from terminal" 540 | input_from_pipe_flag=0 541 | else # Script is getting input from pipe or file - non-interactive 542 | # echo bar | eless foo 543 | # echo bar | eless foo | cat - 544 | piped_data_file="$(mktemp -t emacs-stdin-"$USER".XXXXXXX)" # https://github.com/koalaman/shellcheck/wiki/SC2086 545 | debug "Piped data file : $piped_data_file" 546 | # https://github.com/kaushalmodi/eless/issues/21#issuecomment-366141999 547 | cat > "${piped_data_file}" 548 | debug "--> Input from pipe/file" 549 | input_from_pipe_flag=1 550 | fi 551 | 552 | # http://stackoverflow.com/a/911213/1219634 553 | if [[ -t 1 ]] # Output is going to the terminal 554 | then 555 | # eless foo 556 | # echo bar | eless foo 557 | debug " Output to terminal -->" 558 | output_to_pipe_flag=0 559 | else # Output is going to a pipe, file? 560 | # eless foo | cat - 561 | # echo bar | eless foo | cat - 562 | debug " Output to a pipe -->" 563 | output_to_pipe_flag=1 564 | fi 565 | #+end_src 566 | ** Parse options 567 | :PROPERTIES: 568 | :CUSTOM_ID: parse-options 569 | :END: 570 | We need to parse the arguments such that arguments specific to this 571 | script like =-D= and =--gui= get consumed here, and the ones not known 572 | to this script get passed to =emacs=. 573 | 574 | =getopt= does not support ignoring undefined options. So the below 575 | basic approach of looping through all the arguments ="$@"= is used. 576 | #+begin_src shell :noweb yes 577 | for var in "$@" 578 | do 579 | debug "var : $var" 580 | 581 | if [[ "${var}" == '-D' ]] 582 | then 583 | : # Put just a colon to represent null operation # https://unix.stackexchange.com/a/133976/57923 584 | # Do not pass -D option to emacs. 585 | elif [[ "${var}" == '-V' ]] 586 | then 587 | eless_print_version 588 | exit 0 589 | elif [[ "${var}" == '-' ]] 590 | then 591 | : # Discard the '-'; it does nothing. (for the cases where a user might do "echo foo | eless -") 592 | elif [[ "${var}" == '-nw' ]] 593 | then 594 | : # Ignore the user-passed "-nw" option; we are adding it by default. 595 | elif [[ "${var}" == '-h' ]] # Do not hijack --help; use that to show emacs help 596 | then 597 | help=1 598 | elif [[ "${var}" == '--gui' ]] 599 | then 600 | # Delete the ${no_window_arg} from ${emacs_args[@]} array if user passed "--gui" option 601 | # http://stackoverflow.com/a/16861932/1219634 602 | emacs_args=("${emacs_args[@]/${no_window_arg}}") 603 | else 604 | # Collect all other arguments passed to eless and forward them to emacs. 605 | emacs_args=("${emacs_args[@]}" "${var}") 606 | fi 607 | done 608 | #+end_src 609 | ** Print Help 610 | :PROPERTIES: 611 | :CUSTOM_ID: print-help 612 | :END: 613 | If user asked for this script's help, just print it and exit with 614 | success code. 615 | #+begin_src shell 616 | if [[ ${help} -eq 1 ]] 617 | then 618 | eless_print_version 619 | echo "${h}" 620 | exit 0 621 | fi 622 | #+end_src 623 | 624 | #+begin_src shell :exports none 625 | debug "Raw Args : $*" # https://github.com/koalaman/shellcheck/wiki/SC2145 626 | debug "Emacs Args : ${emacs_args[*]}" 627 | #+end_src 628 | ** Emacs with =-Q= in =view-mode= 629 | :PROPERTIES: 630 | :CUSTOM_ID: emacs-q-view-mode 631 | :END: 632 | The =emacs_Q_view_mode= function is defined to launch emacs with a 633 | customized =view-mode=. 634 | 635 | /Refer to further sections below to see the elisp code referenced by 636 | the =<>= *noweb* placeholder in section [[*Emacs 637 | Configuration]]./ 638 | # :noweb no-export will prevent expansion of the <> when 639 | # exporting 640 | #+begin_src shell :noweb no-export 641 | function emacs_Q_view_mode { 642 | 643 | # Here $@ is the list of arguments passed specifically to emacs_Q_view_mode, 644 | # not to eless. 645 | debug "Args passed to emacs_Q_view_mode : $*" 646 | 647 | ${EMACS} -Q "$@" \ 648 | --eval '(progn 649 | <> 650 | )' 2>/dev/null > 667 | else 668 | # Below if condition is reached when you do this: 669 | # grep 'foo' bar.txt | eless, or 670 | # grep 'foo' bar.txt | eless - 671 | # i.e. Input to eless is coming through a pipe (from grep, in above example) 672 | if [[ ${input_from_pipe_flag} -eq 1 ]] 673 | then 674 | <> 675 | # Below else condition is reached when you do this: 676 | # eless foo.txt 677 | else 678 | <> 679 | fi 680 | fi 681 | #+end_src 682 | *** Output is going to a pipe 683 | :PROPERTIES: 684 | :CUSTOM_ID: output-is-going-to-a-pipe 685 | :END: 686 | This scenario is not supported at the moment. There 687 | [[https://github.com/kaushalmodi/eless/issues/4][is a plan to support it in future]] though. 688 | 689 | For now, the =eless= script will exit with an error code if the output 690 | is being piped to something else. 691 | #+begin_src shell :noweb-ref output-pipe :tangle no 692 | echo "This script is not supposed to send output to a pipe" 693 | exit 1 694 | #+end_src 695 | *** Output is going to /stdout/, Input is coming from a pipe 696 | :PROPERTIES: 697 | :CUSTOM_ID: output-is-going-to-stdout-input-is-coming-from-a-pipe 698 | :END: 699 | =mktemp= requires the =-t= argument to specify the temporary file name 700 | template on Mac OS (See {{{issue(18)}}}.) 701 | #+begin_src shell :noweb no-export :noweb-ref output-stdout--input-pipe :tangle no 702 | debug "Pipe Contents (up to 10 lines) : $(head -n 10 "${piped_data_file}")" 703 | # Remove blank lines from $piped_data_file. Some or all of BSD man 704 | # pages would have a blank line at the top. 705 | # -- https://github.com/kaushalmodi/eless/issues/27#issuecomment-365992910. 706 | # GNU ls man page begins with: 707 | # l1: LS(1) User Commands LS(1) 708 | # BSD ls man page begins with: 709 | # l1: 710 | # l2: LS(1) BSD General Commands Manual LS(1) 711 | perl -ne 'print unless /^\s*$/' "${piped_data_file}" > "${piped_data_file}.noblank" 712 | 713 | # Now parse only the first line of that ${piped_data_file}.noblank file. 714 | first_line_piped_data=$(head -n 1 "${piped_data_file}.noblank") 715 | debug "first_line_piped_data = ${first_line_piped_data}" 716 | 717 | # It is not mandatory for the below perl regex to always match. So OR it with 718 | # "true" so that "set -e" does not kill the script at this point. 719 | 720 | # The first line of man pages is assumed to be 721 | # FOO(1) optional something something FOO(1) 722 | # For some odd reason, the "BASH_BUILTINS" man page is named just 723 | # "builtins"; below deals with that corner case. 724 | # .. faced this problem when trying to do "man read | eless". 725 | # If the man page name is completely in upper-case, convert it 726 | # to lower-case. 727 | man_page=$(echo "${first_line_piped_data}" \ 728 | | perl -ne '/^([A-Za-z0-9-_]+\([a-z0-9]+\))(?=\s+.*?\1$)/ and print $1' \ 729 | | perl -pe 's/bash_builtins/builtins/i' \ 730 | | perl -pe 's/xsel\(1x\)/xsel/i' \ 731 | | perl -pe 's/^[A-Z0-9-_()]+$/\L$_/' \ 732 | || true) 733 | # Using perl expression above instead of below grep (which requires 734 | # GNU grep -- not available by default on macOS): 735 | # grep -Po '^([A-Za-z-_]+\([0-9]+\))(?=\s+.*?\1$)' 736 | 737 | # If it's not a regular man page, check if it's a Perl man page. 738 | if [[ -z ${man_page} ]] 739 | then 740 | # The first line of Perl man pages is assumed to be 741 | # Foo::Bar(1zoo) something something Foo::Bar(1zoo) 742 | # Example: PAGER=eless man Net::FTP or PAGER=less man Net::FTP | eless 743 | # If the man page name is completely in upper-case, convert it 744 | # to lower-case. 745 | # Example: PAGER=eless man error::pass1 or PAGER=less man error::pass1 | eless 746 | man_page=$(echo "${first_line_piped_data}" \ 747 | | perl -ne '/^([A-Za-z0-9-_]+::[A-Za-z0-9-_]+)(\([a-z0-9]+\))(?=\s+.*?\1\2$)/ and print $1' \ 748 | | perl -pe 's/^[A-Z0-9-_]+::[A-Z0-9-_]+$/\L$_/' \ 749 | || true) 750 | fi 751 | 752 | # The first line of Python package MODULE help is assumed to be 753 | # "Help on package MODULE:" OR "Help on module MODULE:" OR "Help on SOMETHING in module MODULE:" 754 | # Examples: PAGER=eless python3; help('shlex') -> "Help on module shlex:" 755 | # PAGER=eless python3; help('iter') -> "Help on built-in function iter in module builtins:" 756 | # PAGER=eless python3; help('exit') -> "Help on Quitter in module _sitebuiltins object:" 757 | python_module_help=$(echo "${first_line_piped_data}" \ 758 | | perl -ne '/^Help on (?:.+ in )*(?:module|package) (.*)(?=:$)/ and print $1' \ 759 | || true) 760 | # Using perl expression above instead of below grep (which requires 761 | # GNU grep -- not available by default on macOS): 762 | # grep -Po '^Help on (.+ in )*(module|package) \K(.*)(?=:$)' 763 | 764 | # The first line of info manuals is assumed to be 765 | # /path/to/some.info or /path/to/some.info.gz 766 | # Example: "/home/kmodi/usr_local/apps/6/emacs/26/share/info/emacs.info.gz" -> "emacs" 767 | info_man=$(echo "${first_line_piped_data}" \ 768 | | perl -ne '/^(?:.*\/)*([^\/]+)(?=\.info(?:\-[0-9]+)*(?:\.gz)*$)/ and print $1' \ 769 | || true) 770 | # Using perl expression above instead of below grep (which requires 771 | # GNU grep -- not available by default on macOS): 772 | # grep -Po '^(.*/)*\K[^/]+(?=\.info(\-[0-9]+)*(\.gz)*$)' 773 | 774 | if [[ ! -z ${man_page} ]] 775 | then 776 | <> 777 | elif [[ ! -z ${python_module_help} ]] 778 | then 779 | <> 780 | elif [[ ! -z ${info_man} ]] 781 | then 782 | <> 783 | else # No man page or info manual detected 784 | <> 785 | fi 786 | #+end_src 787 | **** Input is piped from =man= command 788 | :PROPERTIES: 789 | :CUSTOM_ID: input-is-piped-from-man-command 790 | :END: 791 | #+begin_src shell :noweb-ref man-page :tangle no 792 | # After setting PAGER variable globally to eless (example, using export on bash, 793 | # setenv on (t)csh, try something like `man grep'. That will launch the man 794 | # page in eless. 795 | debug "Man Page = ${man_page}" 796 | 797 | cmd="emacs_Q_view_mode \ 798 | ${emacs_args[*]} \ 799 | --eval '(progn 800 | (man \"${man_page}\") 801 | ;; Below workaround is only for emacs 24.5.x and older releases 802 | ;; where the man page takes some time to load. 803 | ;; 1-second delay before killing the *scratch* window 804 | ;; seems to be sufficient 805 | (when (version<= emacs-version \"24.5.99\") 806 | (sit-for 1)) 807 | (delete-window))'" 808 | #+end_src 809 | The =sit-for= hack is needed for emacs versions older than 25.x. It 810 | was reported in [[https://github.com/kaushalmodi/eless/issues/3][this issue]]. 811 | **** Input is piped from a =modules= help in /IPython/ 812 | :PROPERTIES: 813 | :CUSTOM_ID: input-is-piped-from-a-modules-help-in-ipython 814 | :END: 815 | #+begin_src shell :noweb-ref python-module-help :tangle no 816 | debug "Python Module = ${python_module_help}" 817 | 818 | cmd="emacs_Q_view_mode \ 819 | ${emacs_args[*]} \ 820 | --eval '(progn 821 | (man \"${piped_data_file}\") 822 | ;; Below workaround is only for emacs 24.5.x and older releases 823 | ;; where the man page takes some time to load. 824 | ;; 1-second delay before killing the *scratch* window 825 | ;; seems to be sufficient 826 | (when (version<= emacs-version \"24.5.99\") 827 | (sit-for 1)) 828 | (delete-window) 829 | (rename-buffer \"${python_module_help}\"))'" 830 | #+end_src 831 | The =sit-for= hack is needed for emacs versions older than 25.x. It 832 | was reported in [[https://github.com/kaushalmodi/eless/issues/3][this issue]]. 833 | **** Input is piped from =info= command 834 | :PROPERTIES: 835 | :CUSTOM_ID: input-is-piped-from-info-command 836 | :END: 837 | #+begin_src shell :noweb-ref info-manual :tangle no 838 | # Try something like `info emacs | eless'. 839 | # That will launch the Info manual in eless. 840 | debug "Info Manual = ${info_man}" 841 | 842 | cmd="emacs_Q_view_mode \ 843 | ${emacs_args[*]} \ 844 | --eval '(progn 845 | (info (downcase \"${info_man}\")))'" 846 | #+end_src 847 | **** Input is piped from something else 848 | :PROPERTIES: 849 | :CUSTOM_ID: input-is-piped-from-something-else 850 | :END: 851 | This scenario could be anything, like: 852 | #+begin_src shell :tangle no 853 | diff a b | eless 854 | grep 'foo' bar | eless 855 | ls --color=always | eless 856 | #+end_src 857 | 858 | In that case, just open the =${piped_data_file}= saved from the =STDIN= 859 | stream using =emacs_Q_view_mode=. 860 | #+begin_src shell :noweb-ref neither-man-nor-info :tangle no 861 | debug "No man page or info manual detected" 862 | 863 | cmd="emacs_Q_view_mode ${piped_data_file} \ 864 | ${emacs_args[*]} \ 865 | --eval '(progn 866 | (set-visited-file-name nil) 867 | (rename-buffer \"*Stdin*\" :unique))'" 868 | #+end_src 869 | *** Output is going to /stdout/, Input is an argument to the script 870 | :PROPERTIES: 871 | :CUSTOM_ID: output-is-going-to-stdout-input-is-an-argument-to-the-script 872 | :END: 873 | #+begin_src shell :noweb-ref output-stdout--input-stdin :tangle no 874 | cmd="emacs_Q_view_mode ${emacs_args[*]}" 875 | #+end_src 876 | ** Eval 877 | :PROPERTIES: 878 | :CUSTOM_ID: eval 879 | :END: 880 | Finally we =eval= the constructed =${cmd}= variable. 881 | #+begin_src shell 882 | debug "Eless Command : $cmd" 883 | 884 | eval "$cmd" 885 | #+end_src 886 | #+begin_src shell :exports none 887 | 888 | # References: 889 | # http://superuser.com/a/843744/209371 890 | # http://stackoverflow.com/a/15330784/1219634 - /dev/stdin (Kept just for 891 | # reference, not using this in this script any more.) 892 | # https://github.com/dj08/utils-generic/blob/master/eless 893 | #+end_src 894 | ** Emacs Configuration 895 | :PROPERTIES: 896 | :HEADER-ARGS: :noweb-ref emacs-config :noweb-sep "\n\n" 897 | :CUSTOM_ID: emacs-configuration 898 | :END: 899 | # :noweb-sep "\n\n" <- Inserts one empty line between noweb ref 900 | # source blocks 901 | Here is a "Do The Right Thing" config for =view-mode= that gets 902 | loaded in the emacs instance launched in the [[#emacs-q-view-mode][=emacs_Q_view_mode= function]]. 903 | *** Enable debug on error (in debug mode [=-D=]) 904 | :PROPERTIES: 905 | :CUSTOM_ID: debug-on-error 906 | :END: 907 | #+begin_src emacs-lisp 908 | (when (getenv "ELESS_DEBUG") 909 | (setq debug-on-error t)) 910 | #+end_src 911 | *** General setup 912 | :PROPERTIES: 913 | :CUSTOM_ID: general-setup 914 | :END: 915 | #+begin_src emacs-lisp 916 | ;; Keep the default-directory to be the same from where 917 | ;; this script was launched from; useful during C-x C-f 918 | (setq default-directory "'"$(pwd)"'/") 919 | 920 | ;; No clutter 921 | (menu-bar-mode -1) 922 | (if (fboundp (function tool-bar-mode)) (tool-bar-mode -1)) 923 | 924 | ;; Show line and column numbers in the mode-line 925 | (line-number-mode 1) 926 | (column-number-mode 1) 927 | 928 | (setq-default indent-tabs-mode nil) ;Use spaces instead of tabs for indentation 929 | (setq x-select-enable-clipboard t) 930 | (setq x-select-enable-primary t) 931 | (setq save-interprogram-paste-before-kill t) 932 | (setq require-final-newline t) 933 | (setq visible-bell t) 934 | (setq load-prefer-newer t) 935 | (setq ediff-window-setup-function (function ediff-setup-windows-plain)) 936 | 937 | (setq org-src-fontify-natively t) ;Syntax-highlight source blocks in org 938 | 939 | (fset (quote yes-or-no-p) (quote y-or-n-p)) ;Use y or n instead of yes or no 940 | #+end_src 941 | *** Ido setup 942 | :PROPERTIES: 943 | :CUSTOM_ID: ido-setup 944 | :END: 945 | #+begin_src emacs-lisp 946 | (setq ido-save-directory-list-file nil) ;Do not save ido history 947 | (ido-mode 1) 948 | (setq ido-enable-flex-matching t) ;Enable fuzzy search 949 | (setq ido-everywhere t) 950 | (setq ido-create-new-buffer (quote always)) ;Create a new buffer if no buffer matches substringv 951 | (setq ido-use-filename-at-point (quote guess)) ;Find file at point using ido 952 | (add-to-list (quote ido-ignore-buffers) "*Messages*") 953 | #+end_src 954 | *** Isearch setup 955 | :PROPERTIES: 956 | :CUSTOM_ID: isearch-setup 957 | :END: 958 | #+begin_src emacs-lisp 959 | (setq isearch-allow-scroll t) ;Allow scrolling using isearch 960 | ;; DEL during isearch should edit the search string, not jump back to the previous result. 961 | (define-key isearch-mode-map [remap isearch-delete-char] (function isearch-del-char)) 962 | #+end_src 963 | *** Enable line truncation 964 | :PROPERTIES: 965 | :CUSTOM_ID: enable-line-truncation 966 | :END: 967 | #+begin_src emacs-lisp 968 | ;; Truncate long lines by default 969 | (setq truncate-partial-width-windows nil) ;Respect the value of truncate-lines 970 | (toggle-truncate-lines +1) 971 | #+end_src 972 | *** Highlight the current line 973 | :PROPERTIES: 974 | :CUSTOM_ID: highlight-the-current-line 975 | :END: 976 | #+begin_src emacs-lisp 977 | (global-hl-line-mode 1) 978 | #+end_src 979 | *** Custom functions 980 | :PROPERTIES: 981 | :CUSTOM_ID: custom-functions 982 | :END: 983 | **** Keep/delete matching lines 984 | :PROPERTIES: 985 | :CUSTOM_ID: keep-delete-matching-lines 986 | :END: 987 | #+begin_src emacs-lisp 988 | (defun eless/keep-lines () 989 | (interactive) 990 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 991 | (save-excursion 992 | (goto-char (point-min)) 993 | (call-interactively (function keep-lines))))) 994 | 995 | (defun eless/delete-matching-lines () 996 | (interactive) 997 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 998 | (save-excursion 999 | (goto-char (point-min)) 1000 | (call-interactively (function delete-matching-lines))))) 1001 | #+end_src 1002 | **** Frame and font re-sizing 1003 | :PROPERTIES: 1004 | :CUSTOM_ID: frame-and-font-re-sizing 1005 | :END: 1006 | #+begin_src emacs-lisp 1007 | (defun eless/frame-width-half (double) 1008 | (interactive "P") 1009 | (let ((frame-resize-pixelwise t) ;Do not round frame sizes to character h/w 1010 | (factor (if double 2 0.5))) 1011 | (set-frame-size nil (round (* factor (frame-text-width))) (frame-text-height) :pixelwise))) 1012 | (defun eless/frame-width-double () 1013 | (interactive) 1014 | (eless/frame-width-half :double)) 1015 | 1016 | (defun eless/frame-height-half (double) 1017 | (interactive "P") 1018 | (let ((frame-resize-pixelwise t) ;Do not round frame sizes to character h/w 1019 | (factor (if double 2 0.5))) 1020 | (set-frame-size nil (frame-text-width) (round (* factor (frame-text-height))) :pixelwise))) 1021 | (defun eless/frame-height-double () 1022 | (interactive) 1023 | (eless/frame-height-half :double)) 1024 | #+end_src 1025 | **** Revert buffer in =view-mode= 1026 | :PROPERTIES: 1027 | :CUSTOM_ID: revert-buffer-in-view-mode 1028 | :END: 1029 | #+begin_src emacs-lisp 1030 | (defun eless/revert-buffer-retain-view-mode () 1031 | (interactive) 1032 | (let ((view-mode-state view-mode)) ;save the current state of view-mode 1033 | (revert-buffer) 1034 | (when view-mode-state 1035 | (view-mode 1)))) 1036 | #+end_src 1037 | **** Detect if =diff-mode= should be enabled 1038 | :PROPERTIES: 1039 | :CUSTOM_ID: detect-if-diff-mode-should-be-enabled 1040 | :END: 1041 | #+begin_src emacs-lisp 1042 | (defun eless/enable-diff-mode-maybe () 1043 | (let* ((max-line 10) ;Search first MAX-LINE lines of the buffer 1044 | (bound (save-excursion 1045 | (goto-char (point-min)) 1046 | (forward-line max-line) 1047 | (point)))) 1048 | (save-excursion 1049 | (let ((diff-mode-enable)) 1050 | (goto-char (point-min)) 1051 | (when (and ;First header line of unified/context diff begins with "--- "/"*** " 1052 | (thing-at-point (quote line)) ;Prevent error in string-match if the buffer is empty 1053 | (string-match "^\\(---\\|\\*\\*\\*\\) " (thing-at-point (quote line))) 1054 | ;; Second header line of unified/context diff begins with "+++ "/"--- " 1055 | (progn 1056 | (forward-line 1) 1057 | (string-match "^\\(\\+\\+\\+\\|---\\) " (thing-at-point (quote line))))) 1058 | (setq diff-mode-enable t)) 1059 | ;; Check if the diff format is neither context nor unified 1060 | (unless diff-mode-enable 1061 | (goto-char (point-min)) 1062 | (when (re-search-forward "^\\(?:[0-9]+,\\)?[0-9]+\\([adc]\\)\\(?:[0-9]+,\\)?[0-9]+$" bound :noerror) 1063 | (forward-line 1) 1064 | (let ((diff-type (match-string-no-properties 1))) 1065 | (cond 1066 | ;; Line(s) added 1067 | ((string= diff-type "a") 1068 | (when (re-search-forward "^> " nil :noerror) 1069 | (setq diff-mode-enable t))) 1070 | ;; Line(s) deleted or changed 1071 | (t 1072 | (when (re-search-forward "^< " nil :noerror) 1073 | (setq diff-mode-enable t))))))) 1074 | (when diff-mode-enable 1075 | (message "Auto-enabling diff-mode") 1076 | (diff-mode) 1077 | (rename-buffer "*Diff*" :unique) 1078 | (view-mode 1)))))) ;Re-enable view-mode 1079 | #+end_src 1080 | ***** Enable =whitespace-mode= in =diff-mode= 1081 | :PROPERTIES: 1082 | :CUSTOM_ID: enable-whitespace-mode-in-diff-mode 1083 | :END: 1084 | Enable =whitespace-mode= to easily detect presence of tabs and 1085 | trailing spaces in diffs. 1086 | #+begin_src emacs-lisp 1087 | (setq whitespace-style 1088 | (quote (face ;Enable all visualization via faces 1089 | trailing ;Show white space at end of lines 1090 | tabs ;Show tabs using faces 1091 | spaces space-mark ;space-mark shows spaces as dots 1092 | space-before-tab space-after-tab ;mix of tabs and spaces 1093 | indentation))) ;Highlight spaces/tabs at BOL depending on indent-tabs-mode 1094 | (add-hook (quote diff-mode-hook) (function whitespace-mode)) 1095 | #+end_src 1096 | **** Detect if ANSI codes need to be converted to colors 1097 | :PROPERTIES: 1098 | :CUSTOM_ID: detect-if-ansi-codes-need-to-be-converted-to-colors 1099 | :END: 1100 | #+begin_src emacs-lisp 1101 | (defun eless/enable-ansi-color-maybe () 1102 | (save-excursion 1103 | (let* ((max-line 100) ;Search first MAX-LINE lines of the buffer 1104 | (bound (progn 1105 | (goto-char (point-min)) 1106 | (forward-line max-line) 1107 | (point))) 1108 | (ESC "\u001b") 1109 | ;; Example ANSI codes: ^[[0;36m, or ^[[0m where ^[ is the ESC char 1110 | (ansi-regexp (concat ESC "\\[" "[0-9]+\\(;[0-9]+\\)*m"))) 1111 | (goto-char (point-min)) 1112 | (when (re-search-forward ansi-regexp bound :noerror) 1113 | (let ((inhibit-read-only t)) ;Ignore read-only status of buffer 1114 | (message "Auto-converting ANSI codes to colors") 1115 | (require (quote ansi-color)) 1116 | (ansi-color-apply-on-region (point-min) (point-max))))))) 1117 | #+end_src 1118 | **** "Do The Right Thing" Kill 1119 | :PROPERTIES: 1120 | :CUSTOM_ID: do-the-right-thing-kill 1121 | :END: 1122 | Before killing emacs, loop through all the buffers and mark all 1123 | the =view-mode= buffers as being unmodified (regardless of if they 1124 | actually were). The =view-mode= buffers would have been auto-marked 1125 | as modified if filtering commands like =eless/delete-matching-lines=, 1126 | =eless/keep-lines=, etc. were used. 1127 | 1128 | By overriding the state of these buffers as being unmodified, we are 1129 | saved from emacs prompting to save those modified =view-mode= buffers 1130 | at the time of quitting. 1131 | #+begin_src emacs-lisp 1132 | (defun eless/kill-emacs-or-buffer (&optional kill-emacs) 1133 | (interactive "P") 1134 | (let ((num-non-special-buffers 0)) 1135 | (dolist (buf (buffer-list)) 1136 | (unless (string-match "\\`[ *]" (buffer-name buf)) ;Do not count buffers with names starting with space or * 1137 | (setq num-non-special-buffers (+ 1 num-non-special-buffers))) 1138 | (with-current-buffer buf 1139 | ;; Mark all view-mode buffers as "not modified" to prevent save prompt on 1140 | ;; quitting. 1141 | (when view-mode 1142 | (set-buffer-modified-p nil) 1143 | (when (local-variable-p (quote kill-buffer-hook)) 1144 | (setq kill-buffer-hook nil))))) 1145 | (if (or kill-emacs 1146 | (<= num-non-special-buffers 1)) 1147 | (save-buffers-kill-emacs) 1148 | (kill-buffer (current-buffer))))) ;Else only kill the current buffer 1149 | 1150 | (defun eless/save-buffers-maybe-and-kill-emacs () 1151 | (interactive) 1152 | (eless/kill-emacs-or-buffer :kill-emacs)) 1153 | #+end_src 1154 | **** =dired-mode= setup 1155 | :PROPERTIES: 1156 | :CUSTOM_ID: dired-mode-setup 1157 | :END: 1158 | #+begin_src emacs-lisp 1159 | (defun eless/dired-mode-customization () 1160 | ;; dired-find-file is bound to "f" and "RET" by default 1161 | ;; So changing the "RET" binding to dired-view-file so that the file opens 1162 | ;; in view-mode in the spirit of eless. 1163 | (define-key dired-mode-map (kbd "RET") (function dired-view-file)) 1164 | (define-key dired-mode-map (kbd "E") (function wdired-change-to-wdired-mode)) 1165 | (define-key dired-mode-map (kbd "Q") (function quit-window)) 1166 | (define-key dired-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 1167 | (add-hook (quote dired-mode-hook) (function eless/dired-mode-customization)) 1168 | #+end_src 1169 | **** =Man-mode= setup 1170 | :PROPERTIES: 1171 | :CUSTOM_ID: man-mode-setup 1172 | :END: 1173 | #+begin_src emacs-lisp 1174 | (defun eless/Man-mode-customization () 1175 | (define-key Man-mode-map (kbd "Q") (function quit-window)) 1176 | (define-key Man-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 1177 | (add-hook (quote Man-mode-hook) (function eless/Man-mode-customization)) 1178 | #+end_src 1179 | **** =Info-mode= setup 1180 | :PROPERTIES: 1181 | :CUSTOM_ID: info-mode-setup 1182 | :END: 1183 | #+begin_src emacs-lisp 1184 | (defun eless/Info-mode-customization () 1185 | (define-key Info-mode-map (kbd "Q") (function quit-window)) 1186 | (define-key Info-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 1187 | (add-hook (quote Info-mode-hook) (function eless/Info-mode-customization)) 1188 | #+end_src 1189 | **** =tar-mode= setup 1190 | :PROPERTIES: 1191 | :CUSTOM_ID: tar-mode-setup 1192 | :END: 1193 | When =eless= is passed an archive file as an argument, the =tar-mode= 1194 | is enabled automatically that will do the job of showing the archive 1195 | contents, extracting and viewing them. 1196 | #+begin_src shell :noweb-ref dont-tangle 1197 | eless foo.tar.xz 1198 | eless bar.tar.gz 1199 | #+end_src 1200 | #+begin_src emacs-lisp 1201 | (defun eless/tar-mode-customization () 1202 | (define-key tar-mode-map (kbd "RET") (function tar-view)) 1203 | (define-key tar-mode-map (kbd "Q") (function quit-window)) 1204 | (define-key tar-mode-map (kbd "q") (function eless/kill-emacs-or-buffer))) 1205 | (add-hook (quote tar-mode-hook) (function eless/tar-mode-customization)) 1206 | #+end_src 1207 | *** Auto-setting of major modes 1208 | :PROPERTIES: 1209 | :CUSTOM_ID: auto-setting-of-major-modes 1210 | :END: 1211 | #+begin_src emacs-lisp 1212 | (cond 1213 | ((derived-mode-p (quote dired-mode)) (eless/dired-mode-customization)) 1214 | ((derived-mode-p (quote Man-mode)) (eless/Man-mode-customization)) 1215 | ((derived-mode-p (quote Info-mode)) (eless/Info-mode-customization)) 1216 | ((derived-mode-p (quote tar-mode)) (eless/tar-mode-customization)) 1217 | (t ;Enable view-mode if none of the above major-modes are active 1218 | ;; Auto-enable diff-mode. For example, when doing "diff foo bar | eless" 1219 | (eless/enable-diff-mode-maybe) 1220 | ;; Auto-convert ANSI codes to colors. For example, when doing "ls --color=always | eless" 1221 | (eless/enable-ansi-color-maybe) 1222 | (view-mode 1))) 1223 | #+end_src 1224 | *** Key bindings 1225 | :PROPERTIES: 1226 | :CUSTOM_ID: key-bindings 1227 | :END: 1228 | #+begin_src emacs-lisp 1229 | (eval-after-load (quote view) 1230 | (quote 1231 | (progn 1232 | (define-key view-mode-map (kbd "!") (function eless/delete-matching-lines)) 1233 | (define-key view-mode-map (kbd "&") (function eless/keep-lines)) 1234 | (define-key view-mode-map (kbd "0") (function delete-window)) 1235 | (define-key view-mode-map (kbd "1") (function delete-other-windows)) 1236 | (define-key view-mode-map (kbd "A") (function auto-revert-tail-mode)) 1237 | (define-key view-mode-map (kbd "D") (function dired)) 1238 | (define-key view-mode-map (kbd "N") (function next-error)) ;Next line in *occur* 1239 | (define-key view-mode-map (kbd "P") (function previous-error)) ;Previous line in *occur* 1240 | (define-key view-mode-map (kbd "K") (function eless/delete-matching-lines)) 1241 | (define-key view-mode-map (kbd "a") (function auto-revert-mode)) 1242 | (define-key view-mode-map (kbd "g") (function eless/revert-buffer-retain-view-mode)) 1243 | (define-key view-mode-map (kbd "k") (function eless/keep-lines)) 1244 | (define-key view-mode-map (kbd "n") (function next-line)) 1245 | (define-key view-mode-map (kbd "o") (function occur)) 1246 | (define-key view-mode-map (kbd "p") (function previous-line)) 1247 | (define-key view-mode-map (kbd "q") (function eless/kill-emacs-or-buffer)) 1248 | (define-key view-mode-map (kbd "t") (function toggle-truncate-lines))))) 1249 | 1250 | ;; Global custom bindings 1251 | (global-set-key (kbd "M-/") (function hippie-expand)) 1252 | (global-set-key (kbd "C-x C-b") (function ibuffer)) 1253 | (global-set-key (kbd "C-x C-c") (function eless/save-buffers-maybe-and-kill-emacs)) 1254 | (global-set-key (kbd "C-x C-f") (function view-file)) 1255 | (global-set-key (kbd "C-c q") (function query-replace-regexp)) 1256 | (global-set-key (kbd "") (function eless/revert-buffer-retain-view-mode)) 1257 | 1258 | (when (display-graphic-p) 1259 | (eval-after-load (quote view) 1260 | (quote 1261 | (progn 1262 | (define-key view-mode-map (kbd "+") (function text-scale-adjust)) 1263 | (define-key view-mode-map (kbd "-") (function text-scale-adjust)) 1264 | (define-key view-mode-map (kbd "=") (function text-scale-adjust))))) 1265 | (global-set-key (kbd "C-") (function eless/frame-width-double)) 1266 | (global-set-key (kbd "C-") (function eless/frame-width-half)) 1267 | (global-set-key (kbd "C-") (function eless/frame-height-double)) 1268 | (global-set-key (kbd "C-") (function eless/frame-height-half))) 1269 | #+end_src 1270 | *** User config override 1271 | :PROPERTIES: 1272 | :CUSTOM_ID: user-config-override 1273 | :END: 1274 | If an =elesscfg= file is present in the =user-emacs-directory= 1275 | (default value is =~/.emacs.d/=), load that. As the user can be using 1276 | that file to set their favorite theme (or not set one), the 1277 | =eless= default theme is not loaded if that file is present. 1278 | 1279 | User can further choose to re-define any of the above functions or 1280 | key-bindings in this file. 1281 | #+begin_src emacs-lisp 1282 | (let* ((cfg-file "elesscfg") 1283 | (cfg-path (if (fboundp (quote locate-user-emacs-file)) 1284 | (locate-user-emacs-file cfg-file) 1285 | ;; For emacs older than 23.1. 1286 | (let ((home (file-name-as-directory (getenv "HOME")))) 1287 | (or (expand-file-name cfg-file (concat home ".emacs.d")) 1288 | (expand-file-name cfg-file home)))))) 1289 | (unless (load cfg-path :noerror) 1290 | (load-theme (quote tango-dark) :no-confirm) 1291 | ;; The tango-dark theme is good except for the bright yellow hl-line face 1292 | (custom-theme-set-faces 1293 | (quote user) 1294 | (quote (hl-line ((t (:background "color-238"))))) 1295 | (quote (Man-overstrike ((t (:foreground "#f3dc55" :weight normal)))))))) ;gold yellow 1296 | #+end_src 1297 | * Contributing :contributing: 1298 | :PROPERTIES: 1299 | :EXPORT_FILE_NAME: CONTRIBUTING 1300 | :EXPORT_TITLE: Contributing Guide 1301 | :CUSTOM_ID: contributing 1302 | :END: 1303 | This guide is for you if you'd like to do any of the below: 1304 | - Open an issue (plus provide debug information). 1305 | - Simply clone this repo and build =eless= locally. 1306 | - Do above + Provide a PR. 1307 | ** How to help debug 1308 | :PROPERTIES: 1309 | :CUSTOM_ID: how-to-help-debug 1310 | :END: 1311 | - If you find =eless= not working as expected, file an [[https://github.com/kaushalmodi/eless/issues][issue]]. 1312 | - Include the following debug information: 1313 | 1. =emacs --version= 1314 | 2. =eless= debug info: 1315 | - Append the =-D= option to your =eless= use case. Examples: 1316 | - =eless foo -D= 1317 | - =info org | eless -D= 1318 | - If you are providing debug info for something like =man foo=, do 1319 | - ~PAGER="eless -D" man foo~ or ~man foo | eless -D~. 1320 | ** Development 1321 | :PROPERTIES: 1322 | :CUSTOM_ID: development 1323 | :END: 1324 | *** Preparation 1325 | :PROPERTIES: 1326 | :CUSTOM_ID: preparation 1327 | :END: 1328 | #+begin_src shell 1329 | git clone https://github.com/kaushalmodi/eless 1330 | #+end_src 1331 | Also see the [[*Requirements][*Requirements*]] section if you'd like to build the =eless= 1332 | script + documentation locally. 1333 | *** Building =eless= 1334 | :PROPERTIES: 1335 | :CUSTOM_ID: building-eless 1336 | :END: 1337 | #+begin_src shell 1338 | make eless 1339 | #+end_src 1340 | **** Sanity check of the tangled =eless= 1341 | :PROPERTIES: 1342 | :CUSTOM_ID: sanity-check-of-the-tangled-eless 1343 | :END: 1344 | 1. Run the tangled =eless= through [[http://www.shellcheck.net/][shellcheck]] to ensure that there are 1345 | no errors. 1346 | 2. Ensure that =make test= passes. Add/update tests as needed. 1347 | *** Building documentation 1348 | :PROPERTIES: 1349 | :CUSTOM_ID: building-documentation 1350 | :END: 1351 | Below will generate/update the Info manual and =README.org= and 1352 | =CONTRIBUTING.org= for Github. 1353 | #+begin_src shell 1354 | make doc 1355 | #+end_src 1356 | **** Understand the changes 1357 | :PROPERTIES: 1358 | :CUSTOM_ID: understand-the-changes 1359 | :END: 1360 | - The randomly generated hyperlinks and section numbers in the Info 1361 | document and HTML will be different. 1362 | - Other than that, you shouldn't see any unexpected changes. 1363 | *** Build everything 1364 | :PROPERTIES: 1365 | :CUSTOM_ID: build-everything 1366 | :END: 1367 | If you'd like to build the script as well the documentation together, 1368 | you can do: 1369 | #+begin_src shell 1370 | make all 1371 | #+end_src 1372 | *** Submitting PR 1373 | :PROPERTIES: 1374 | :CUSTOM_ID: submitting-pr 1375 | :END: 1376 | - You can submit a PR once you have reviewed all the changes in the 1377 | tangled =eless= script and documentation. 1378 | - =make test= has to pass before a PR is merged. 1379 | * Miscellaneous 1380 | :PROPERTIES: 1381 | :CUSTOM_ID: miscellaneous 1382 | :END: 1383 | ** Example =eless= config in =tcsh= 1384 | :PROPERTIES: 1385 | :EXPORT_FILE_NAME: example-eless-config-in-tcsh 1386 | :CUSTOM_ID: example-eless-config-in-tcsh 1387 | :END: 1388 | #+begin_src shell 1389 | setenv PAGER eless # Show man pages using eless (on non-macOS systems) 1390 | 1391 | alias info '\info \!* | eless' 1392 | 1393 | alias diff '\diff \!* | eless' 1394 | alias diffg '\diff \!* | eless --gui' 1395 | 1396 | # (MAN)pages in eless (G)UI mode. Note that will not work on macOS 1397 | # systems. 1398 | alias mang '(setenv PAGER "eless --gui"; man \!*)' 1399 | 1400 | # For macOS systems, set PAGER to less and instead use the -P switch to set the 1401 | # man pager to eless. 1402 | alias eman '(setenv PAGER less; man -P eless \!*)' 1403 | alias emang '(setenv PAGER less; man -P "eless --gui" \!*)' 1404 | 1405 | alias ev eless 1406 | #+end_src 1407 | ** Example =eless= config in =bash= 1408 | :PROPERTIES: 1409 | :EXPORT_FILE_NAME: example-eless-config-in-bash 1410 | :CUSTOM_ID: example-eless-config-in-bash 1411 | :END: 1412 | #+begin_src shell 1413 | export PAGER=eless 1414 | 1415 | # Note for macOS users using man: 1416 | # "PAGER=eless man ls", for example, would not work because 1417 | # of the way how man handles the stream of man pages on those 1418 | # systems. But with the below alias, "eman ls" will work instead. 1419 | # (Ref: https://github.com/kaushalmodi/eless/issues/27) 1420 | alias eman='PAGER=less man -P eless' 1421 | #+end_src 1422 | ** Example =eless= config in =zsh= 1423 | :PROPERTIES: 1424 | :EXPORT_FILE_NAME: example-eless-config-in-zsh 1425 | :CUSTOM_ID: example-eless-config-in-zsh 1426 | :END: 1427 | #+begin_src shell 1428 | export PAGER=eless 1429 | 1430 | # Note for macOS users using man: 1431 | # "PAGER=eless man ls", for example, would not work because 1432 | # of the way how man handles the stream of man pages on those 1433 | # systems. But with the below alias, "eman ls" will work instead. 1434 | # (Ref: https://github.com/kaushalmodi/eless/issues/27) 1435 | alias eman='PAGER=less man -P eless' 1436 | #+end_src 1437 | * COMMENT Local Variables :ARCHIVE: 1438 | # Local Variables: 1439 | # fill-column: 70 1440 | # eval: (auto-fill-mode 1) 1441 | # End: 1442 | --------------------------------------------------------------------------------