[
  {
    "path": ".github/workflows/macos.yml",
    "content": "name: macos-latest\n\non:\n  push:\n    branches: [ main, alpha, beta, sd/* ]\n    paths: ['src/**', 'test/**', '!README.md' ]\n  pull_request:\n    branches: [ main ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n\n    runs-on: macos-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Bash tests\n      run: |\n        dir=$PWD\n        git clone https://github.com/shellgei/rusty_bash_test -b v1.2.7 --depth 1\n        #git clone https://github.com/shellgei/rusty_bash_test --depth 1\n        cd rusty_bash_test\n        ./test.bash $dir\n"
  },
  {
    "path": ".github/workflows/ubuntu.yml",
    "content": "name: ubuntu-latest\n\non:\n  push:\n    branches: [ main, alpha, beta, sd/* ]\n    paths: ['src/**', 'test/**', '!README.md' ]\n  pull_request:\n    branches: [ main ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Bash tests\n      run: |\n        dir=$PWD\n        git clone https://github.com/shellgei/rusty_bash_test -b v1.2.7 --depth 1\n        cd rusty_bash_test\n        ./test.bash $dir\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\nCargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\ntest/ok\ntest/error\n"
  },
  {
    "path": ".sushrc",
    "content": "case $- in\n    *i*) ;;\n      *) return;;\nesac\n\ncase \"$TERM\" in\n    xterm-color|*-256color) color_prompt=yes;;\nesac\n\nbuild_profile=$([[ \"$SUSH_VERSION\" == *-release ]] || echo \"(${SUSH_VERSION##*-})\")\nif [ \"$color_prompt\" = yes ]; then\n\tPS1='\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;36m\\]\\b\\[\\033[00m\\]\\[\\033[01;35m\\]\\w\\[\\033[00m\\]'$build_profile'🍣 '\nelse\n\tPS1='\\u@\\h:\\w'$build_profile'🍣 '\nfi \n\n\ncase \"$TERM\" in\nxterm*|rxvt*)\n    PS1=\"\\[\\033]2;\\u@\\h: \\w\\007\\]$PS1\"\n    ;;\n*)\n    ;;\nesac\n\n\nPS2='> '\nPS4='+ '\nalias ll='ls -l'\nalias git-writing='git add -A ; git commit -m Writing ; git push'\n\n\ncommand_not_found_handle() { #command_not_found should be loaded before bash-completion in this stage\n\tif [ -e /usr/lib/command-not-found ] ; then\n\t\t/usr/lib/command-not-found -- \"$1\"\n\tfi\n}\n\n\nexport BASH_COMPLETION=/opt/homebrew/Cellar/bash-completion/1.3_3/etc/bash_completion\nif [ \"$(uname)\" = \"Darwin\" -a -f /opt/homebrew/Cellar/bash-completion/1.3_3/etc/bash_completion ]; then\n\tsource /opt/homebrew/Cellar/bash-completion/1.3_3/etc/bash_completion\n\tcomplete -d cd\nelif [ -f /usr/share/bash-completion/bash_completion ]; then\n    . /usr/share/bash-completion/bash_completion\n    _comp_complete_load scp #for completion of rsync\n    # . /usr/share/bash-completion/completions/git # for git-completion on WSL\n    complete -d cd\nelif [ -f /etc/bash_completion ]; then\n    . /etc/bash_completion\n    complete -d cd\nfi\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"sush\"\nversion = \"1.2.7\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[package.metadata.compat]\ntarget_bash_version = \"5.2.37\" # Ubuntu 25.04\n\n[dependencies]\nnix = { version = \"0.30.1\", features = [\"fs\", \"process\", \"signal\", \"term\", \"user\", \"time\", \"hostname\", \"resource\"]}\ntermion = \"4.0.5\"\nunicode-width = \"0.1.11\"\nsignal-hook = \"0.3.17\"\nrev_lines = \"0.3.0\"\nfaccess = \"0.2.4\"\nio-streams = \"0.16.3\"\nregex = \"1.11.1\"\nrand = \"0.9\"\nrand_chacha = { version = \"0.9.0\", features = [ \"os_rng\" ]}\ntime = \"0.3\"\nsprintf = \"0.4\"\nlibc = \"0.2.178\"\n\n# Internationalization\nfluent-bundle = \"0.16\"\nunic-langid = \"0.9\"\nonce_cell = \"1\"\nlocale_config = \"0.3\"\n\n\n# Compile-time feature\n[features]\nlang_ar = []\nlang_da = []\nlang_de = []\nlang_el = []\nlang_en = []\nlang_es = []\nlang_fi = []\nlang_fr = []\nlang_hi = []\nlang_it = []\nlang_ja = []\nlang_ko = []\nlang_nl = []\nlang_no = []\nlang_pl = []\nlang_pt = []\nlang_ru = []\nlang_sl = []\nlang_sv = []\nlang_sw = []\nlang_uk = []\nlang_zh = []\n\ndefault = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]               # All\n0 = [\"lang_en\", \"lang_fr\", \"lang_ja\"]                       # Dev Pack\n1 = [\"0\", \"lang_es\", \"lang_fr\", \"lang_it\", \"lang_pt\"]       # Latin\n2 = [\"0\", \"lang_nl\", \"lang_de\", \"lang_sl\", \"lang_el\"]       # Central Europe\n3 = [\"0\", \"lang_da\", \"lang_fi\", \"lang_no\", \"lang_sv\"]       # Northern Europe\n4 = [\"0\", \"lang_ru\", \"lang_uk\", \"lang_pl\"]                  # Eastern Europe\n5 = [\"0\", \"lang_hi\", \"lang_ja\", \"lang_ko\", \"lang_zh\"]       # Asia\n6 = [\"0\", \"lang_ar\", \"lang_sw\"]                             # Africa / Middle East\n\n[build-dependencies]\ncargo_toml = \"0.22\"\n\n[profile.release]\nopt-level = 3\ncodegen-units = 1\nlto = true\npanic = \"abort\"\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2024, Ryuichi Ueda\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# Sushi shell (a.k.a. 🍣 Sush): a Bash clone shell implemented in Rust\n\nformer name: Rusty Bash\n\n[![ubuntu-latest](https://github.com/shellgei/rusty_bash/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/shellgei/rusty_bash/actions/workflows/ubuntu.yml)\n[![macos-latest](https://github.com/shellgei/rusty_bash/actions/workflows/macos.yml/badge.svg)](https://github.com/shellgei/rusty_bash/actions/workflows/macos.yml)\n![](https://img.shields.io/github/license/shellgei/rusty_bash)\n\n## NEWS\n\nBash-completion starts working on our shell! ([how to use](docs/SETUP_BASH_COMPLETION.md))\n\n![completion](https://github.com/user-attachments/assets/e4af177c-3fdd-4f59-a70b-9c97df96b4bc)\n\n## What's this?\n\nA clone of Bash, which is developed as a hobby of our group and for monthly articles on SoftwareDesign magazine published by Gijutsu-Hyohron Co., Ltd.\n\n## Quick Start\n\n```bash\n$ git clone https://github.com/shellgei/rusty_bash.git\n$ cd rusty_bash\n$ cargo run\n・・・\n    Finished dev [unoptimized + debuginfo] target(s) in 0.04s\n     Running `target/debug/sush`\nueda@uedaP1g6:main🌵~/GIT/rusty_bash(debug)🍣\n```\n\n## Install \n\n```bash\n$ git clone https://github.com/shellgei/rusty_bash.git\n$ cd rusty_bash\n$ cargo build --release\n### ↓  Change /bin/ to /usr/local/bin/ or another path in $PATH if you are using Mac or BSD ###\n$ sudo cp target/release/sush /bin/\n$ cp .sushrc ~/.sushrc # edit if some errors occur\n$ sush\nueda@uedaP1g6:main🌵~/GIT/rusty_bash🍣\n```\n\n## Comparison with Bash 5.2\n\nThis graph shows the test result with the script in `./sush_test/bash_genuine_test` of [this test repository](https://github.com/ryuichiueda/bash_for_sush_test). Currently, the binary built from alpha repo has passed 27 of 84 test scripts.\n\n![](https://github.com/ryuichiueda/bash_for_sush_test/blob/master/sush_test/graph.png)\n\n### Bash behavior that we don't follow\n\nThe following behavior of Bash will not be imitated by `sush`. So we alter the right output file (e.g `globstar.right`) for comparision. \n\n* the output order of associative array\n* output duplication of globstar\n    * Bash outputs the same path repeatedly in some situations of globstar. It may be for compatibility of ksh. \n* overflow at calculations\n    * Bash outputs overflow calculation results at the border of 64 bit intergers and `arith5.sub` tells that this behavior should be reproduced. But we don't follow it. \n    ```bash\n    ### Bash example ###\n    $ echo $(( -9223372036854775808 * -1 )) \n    -9223372036854775808                    #IT'S WRONG. \n    $ echo $(( -9223372036854775807 * -1 )) #IT'S OK.\n    9223372036854775807\n    ### Sushi shell ###\n    🍣 echo $(( -9223372036854775808 * -1 ))\n    9223372036854775808\n    🍣 echo $(( -9223372036854775807 * -1 ))\n    9223372036854775807\n    ```\n* spaces of error log\n    * Bash adds spaces to each token and displays them in error messages. These spaces are elliminated in our shell.\n    ```bash\n    ### Bash ###\n    $ (( 1++     ))\n    bash: ((: 1++     : syntax error: operand expected (error token is \"+     \")\n    ### Sush ###\n    🍣 (( 1++     ))\n    sush: ((: 1++     : syntax error: operand expected (error token is \"+\")\n    ```\n* error messages of `readonly`\n    * We added `readonly: ` to the messages in `attr.right`.\n        * e.g.: `./attr.tests: line 17: a: readonly variable` -> `./attr.tests: line 17: readonly: a: readonly variable`\n\n## Contribution\n\nBecause the shell in this repository can be a standard one in the next generation, it may a good idea to leave your name as a contributor. Give us pull requests with what you think as contribution. As our community is not big, rules have not been fixed yet. \n\nFollowings are not difficult but very important tasks.\n\n* To fix the code based on Clippy. (There are many warnings by Clippy in the current codes. )\n* To develop builtin commands. (Especially `echo` may be easy. )\n* To add test cases.\n* To fix the test methodology, especially for the parts related to human input.\n\n### Important branch\n\n* alpha: checkout this branch if you want to develop.\n* beta: we are using the head version of this branch on a day-to-day basis.\n* main: the beta version is merged to this branch if fatal problems are not found for a week.\n* alpha-sushiline: a version with [sushiline](https://github.com/t-koba/sushline), which is a clone of GNU Readline\n\n## List of Features\n\n* :heavy_check_mark: :available\n* :construction: :partially available (or having known bugs) \n* :no_good: : not implemented\n\n### compound commands\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| if | :heavy_check_mark: | while | :heavy_check_mark: | () | :heavy_check_mark: |\n| {} | :heavy_check_mark: | case | :heavy_check_mark: | until | :no_good: | select | :no_good: |\n| for | :heavy_check_mark: | [[ ]] | :heavy_check_mark: |\n\n### special parameters\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| $ | :heavy_check_mark: | ? | :heavy_check_mark: | * | :heavy_check_mark: |\n| @ | :heavy_check_mark: | # | :heavy_check_mark: | - | :heavy_check_mark: |\n| ! | :no_good: | _ | :heavy_check_mark: |\n\n### builtin commands\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| cd | :heavy_check_mark: | pwd | :heavy_check_mark: | read | :construction: |\n| exit | :heavy_check_mark: | source | :heavy_check_mark: | set | :construction: |\n| shopt | :construction: | : | :heavy_check_mark: | . | :heavy_check_mark: | [ | :no_good: |\n| alias | :heavy_check_mark: | bg | :construction: | bind | :no_good: |\n| break | :heavy_check_mark: | builtin | :heavy_check_mark: | caller | :under_construction: |\n| command | :heavy_check_mark: | compgen | :construction: | complete | :construction: |\n| compopt | :no_good: | continue | :heavy_check_mark: | declare | :no_good: |\n| dirs | :no_good: | disown | :heavy_check_mark: | echo | :no_good: |\n| enable | :no_good: | eval | :heavy_check_mark: | exec | :no_good: |\n| fc | :no_good: | fg | :construction: | getopts | :construction: |\n| hash | :no_good: | help | :no_good: | history | :construction: |\n| jobs | :construction: | kill | :under_construction: | let | :no_good: |\n| local | :heavy_check_mark: | logout | :no_good: | mapfile | :no_good: |\n| popd | :no_good: | printf | :heavy_check_mark: | pushd | :no_good: |\n| readonly | :no_good: | return | :heavy_check_mark: | false | :heavy_check_mark: |\n| shift | :heavy_check_mark: | suspend | :no_good: | test | :heavy_check_mark: |\n| times | :no_good: | trap | :no_good: | true | :heavy_check_mark: |\n| type | :no_good: | typeset | :no_good: | ulimit | :heavy_check_mark: |\n| umask | :no_good: | unalias | :heavy_check_mark: | unset | :construction: |\n| wait | :construction: | export | :heavy_check_mark: |\n\n### options\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| -c | :heavy_check_mark: | -i | :heavy_check_mark: | -l, --login | :no_good: |\n| -r | :no_good: | -s | :no_good: | -D | :no_good: |\n| [-+]O | :no_good: | -- | :no_good: | --debugger | :no_good: |\n| --dimp-po-strings | :no_good: | --help | :heavy_check_mark: | --init-file | :no_good: |\n| --rcfile | :no_good: | --noediting | :no_good: | --noprofile | :no_good: |\n| --norc | :no_good: | --posix | :under_construction: | --restricted | :heavy_check_mark: |\n| -v, --verbose | :no_good: | --version | :heavy_check_mark: | -e | :heavy_check_mark: |\n| --pipefail | :heavy_check_mark: | -B | :heavy_check_mark: |  |  |\n\n\n### shopt \n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| autocd | :no_good: | cdable_vars | :no_good: | cdspell | :no_good: |\n| checkhash | :no_good: | checkjobs | :no_good: | checkwinsize | :no_good: |\n| cmdhist | :no_good: | compat31 | :no_good: | compat32 | :no_good: |\n| compat40 | :no_good: | compat41 | :no_good: | dirspell | :no_good: |\n| dotglob | :heavy_check_mark: | execfail | :no_good: | expand_aliases | :no_good: |\n| extdebug | :no_good: | extglob | :heavy_check_mark: | extquote | :no_good: |\n| failglob | :no_good: | force_fignore | :no_good: | globstar | :heavy_check_mark: |\n| gnu_errfmt | :no_good: | histappend | :no_good: | histreedit | :no_good: |\n| histverify | :no_good: | hostcomplete | :no_good: | huponexit | :no_good: |\n| interactive_comments | :no_good: | lastpipe | :no_good: | lithist | :no_good: |\n| login_shell | :no_good: | mailwarn | :no_good: | no_empty_cmd_completion | :no_good: |\n| nocaseglob | :no_good: | nocasematch | :no_good: | nullglob | :heavy_check_mark: |\n| progcomp | :heavy_check_mark: | promptvars | :no_good: | restricted_shell | :heavy_check_mark: |\n| shift_verbose | :no_good: | sourcepath | :no_good: | xpg_echo | :no_good: |\n\n### variables\n\nBorn Shell Variables\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| CDPATH | :no_good: | HOME | :heavy_check_mark: | IFS | :heavy_check_mark: |\n| MAIL | :no_good: | MAILPATH | :no_good: | OPTARG | :heavy_check_mark: |\n| OPTIND | :heavy_check_mark: | PATH | :heavy_check_mark: | PS1 | :heavy_check_mark: |\n| PS2 | :heavy_check_mark: | | | | |\n\nBash Variables\n\n|features | status |features | status |features | status |\n|-------------------|----|-------------------|----|-------------------|----|\n| _ | :heavy_check_mark: | BASH | :no_good: | BASHOPTS | :no_good: |\n| BASHPID | :heavy_check_mark: | BASH_ALIASES | :no_good: | BASH_ARGC | :no_good: |\n| BASH_ARGV | :no_good: | BASH_ARGV0 | :no_good: | BASH_CMDS | :no_good: |\n| BASH_COMMAND | :no_good: | BASH_COMPAT | :no_good: | BASH_ENV | :no_good: |\n| BASH_EXECUTION_STRING | :no_good: | BASH_LINENO | :no_good: | BASH_LOADABLES_PATH | :no_good: |\n| BASH_REMATCH | :heavy_check_mark: | BASH_SOURCE | :no_good: | BASH_SUBSHELL | :heavy_check_mark: |\n| BASH_VERSINFO | :heavy_check_mark: | BASH_VERSION | :heavy_check_mark: | BASH_XTRACEFD | :no_good: |\n| CHILD_MAX | :no_good: | COLUMNS | :no_good: | COMP_CWORD | :no_good: |\n| COMP_LINE | :no_good: | COMP_POINT | :no_good: | COMP_TYPE | :no_good: |\n| COMP_KEY | :no_good: | COMP_WORDBREAKS | :no_good: | COMP_WORDS | :no_good: |\n| COMPREPLY | :no_good: | COPROC | :no_good: | DIRSTACK | :no_good: |\n| EMACS | :no_good: | ENV | :no_good: | EPOCHREALTIME | :heavy_check_mark: |\n| EPOCHSECONDS | :heavy_check_mark: | EUID | :no_good: | EXECIGNORE | :no_good: |\n| FCEDIT | :no_good: | FIGNORE | :no_good: | FUNCNAME | :heavy_check_mark: |\n| FUNCNEST | :no_good: | GLOBIGNORE | :no_good: | GROUPS | :no_good: |\n| histchars | :no_good: | HISTCMD | :no_good: | HISTCONTROL | :no_good: |\n| HISTFILE | :heavy_check_mark: | HISTFILESIZE | :heavy_check_mark: | HISTIGNORE | :no_good: |\n| HISTSIZE | :no_good: | HISTTIMEFORMAT | :no_good: | HOSTFILE | :no_good: |\n| HOSTNAME | :no_good: | HOSTTYPE | :heavy_check_mark: | IGNOREEOF | :no_good: |\n| INPUTRC | :no_good: | INSIDE_EMACS | :no_good: | LANG | :heavy_check_mark: |\n| LC_ALL | :no_good: | LC_COLLATE | :no_good: | LC_CTYPE | :no_good: |\n| LC_MESSAGES | :no_good: | LC_NUMERIC | :no_good: | LC_TIME | :no_good: |\n| LINENO | :heavy_check_mark: | LINES | :no_good: | MACHTYPE | :heavy_check_mark: |\n| MAILCHECK | :no_good: | MAPFILE | :no_good: | OLDPWD | :heavy_check_mark: |\n| OPTERR | :no_good: | OSTYPE | :heavy_check_mark: | PIPESTATUS | :heavy_check_mark: |\n| POSIXLY_CORRECT | :no_good: | PPID | :no_good: | PROMPT_COMMAND | :no_good: |\n| PROMPT_DIRTRIM | :no_good: | PS0 | :no_good: | PS3 | :no_good: |\n| PS4 | :heavy_check_mark: | PWD | :heavy_check_mark: | RANDOM | :heavy_check_mark: |\n| READLINE_ARGUMENT | :no_good: | READLINE_LINE | :no_good: | READLINE_MARK | :no_good: |\n| READLINE_POINT | :no_good: | REPLY | :no_good: | SECONDS | :heavy_check_mark: |\n| SHELL | :heavy_check_mark: | SHELLOPTS | :no_good: | SHLVL | :heavy_check_mark: |\n| SRANDOM | :heavy_check_mark: | TIMEFORMAT | :no_good: | TMOUT | :no_good: |\n| TMPDIR | :no_good: | UID | :no_good: | | |\n\n### beyond Bash\n\n|features | status |\n|-------------------|----|\n| repeat command | :heavy_check_mark: |\n| branch display in prompt | :heavy_check_mark: |\n\n## Thanks to\n\nPartially in Japanese.\n\n* blog articles\n    * [Rustでシェル作った | κeenのHappy Hacκing Blog](https://keens.github.io/blog/2016/09/04/rustdeshierutsukutta/)\n    * [Rustで始める自作シェル その1 | ぶていのログでぶログ](https://tech.buty4649.net/entry/2021/12/19/235124)\n    * [Rustのターミナル操作crateいろいろ | meganehouser](https://meganehouser.github.io/2019-12-11_rust-terminal-crates.html)\n    * [原理原則で理解するフォアグラウンドプロセスとバックグラウンドプロセスの違い | @tajima_taso](https://qiita.com/tajima_taso/items/c5553762af5e1a599fed)\n    * [Bashタブ補完自作入門 | Cybouzu Inside Out](https://blog.cybozu.io/entry/2016/09/26/080000)\n\n## Attempts by other groups\n\n- [reubeno/brush](https://github.com/reubeno/brush)\n\n## Copyright\n\n© 2022-2025 shellgei group\n\n- Ryuichi Ueda: [@ry@mi.shellgei.org](https://mi.shellgei.org/@ru), @ueda.tech (https://bsky.app/profile/ueda.tech)\n- [@caro@mi.shellgei.org](https://mi.shellgei.org/@caro)\n"
  },
  {
    "path": "RELEASE.md",
    "content": "See https://github.com/shellgei/rusty_bash/releases \n"
  },
  {
    "path": "build.rs",
    "content": "//SPDX-FileCopyrightText: 2024 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse cargo_toml::Manifest;\nuse std::{env, path::PathBuf};\n\nfn main() {\n    // SUSH_VERSION, SUSH_VERSINFO[4]\n    let profile = env::var(\"PROFILE\").unwrap_or(\"\".to_string());\n    println!(\"cargo:rustc-env=CARGO_BUILD_PROFILE={profile}\");\n    // HOSTTYPE, MACHTYPE, BASH_VERSINFO[5], SUSH_VERSINFO[5]\n    let target_arch = env::var(\"CARGO_CFG_TARGET_ARCH\").unwrap_or(\"unknown\".to_string());\n    println!(\"cargo:rustc-env=CARGO_CFG_TARGET_ARCH={target_arch}\");\n    // MACHTYPE, BASH_VERSINFO[5], SUSH_VERSINFO[5]\n    let target_vendor = env::var(\"CARGO_CFG_TARGET_VENDOR\").unwrap_or(\"unknown\".to_string());\n    println!(\"cargo:rustc-env=CARGO_CFG_TARGET_VENDOR={target_vendor}\");\n    // OSTYPE, MACHTYPE, BASH_VERSINFO[5], SUSH_VERSINFO[5]\n    let target_os = env::var(\"CARGO_CFG_TARGET_OS\").unwrap_or(\"unknown\".to_string());\n    println!(\"cargo:rustc-env=CARGO_CFG_TARGET_OS={target_os}\");\n\n    // metadata\n    let manifest_path = PathBuf::from(env::var(\"CARGO_MANIFEST_DIR\").unwrap()).join(\"Cargo.toml\");\n    let manifest = Manifest::from_path(&manifest_path).expect(\"failed to parse Cargo.toml\");\n    // compat\n    let compat = manifest\n        .package\n        .as_ref()\n        .and_then(|p| p.metadata.as_ref()?.get(\"compat\"))\n        .and_then(|v| v.as_table())\n        .expect(\"Missing [package.metadata.compat]\");\n    for (k, v) in compat {\n        let env_key: String = k\n            .chars()\n            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })\n            .collect::<String>()\n            .to_ascii_uppercase();\n        let val = v\n            .as_str()\n            .unwrap_or_else(|| panic!(\"Non-string value for key {k} in [package.metadata.compat]\"));\n        println!(\"cargo:rustc-env=COMPAT_{env_key}={val}\");\n    }\n}\n"
  },
  {
    "path": "docs/SETUP_BASH_COMPLETION.md",
    "content": "# setup of bash-completion\n\n- 20250427 Ryuichi Ueda\n    - bluesky: @ueda.tech (https://bsky.app/profile/ueda.tech)\n\n## contents\n\n- Linux\n    - with command_not_found\n- macOS\n\n## Linux (Ubuntu 24.04 or some versions near 24.04)\n\n  The following three lines are minimally required for bash-completion\nat the bottom of `~/.sushrc` if you are using v1.1.4 or a version near v1.1.4. \n\n```bash\nsource /usr/share/bash-completion/bash_completion\n_comp_complete_load scp #for completion of rsync\ncomplete -d cd\n```\n\nWith this three lines, you can use completion for various commands\nincluding `git`. \n\n  The first line calls the main script of Bash-completion, which usually\nexists in `/usr/share/bash-completion/`. If you can't find the path,\nplease search it by `find` and change the path.\n\n  The second line is required since a warning disturbs completion for `rsync`.\nThis problem is solved if the completion function for `scp` is load. \nSince this happens owing to a bug of Rusty Bash, it should be removed someday. \nSomeday...\n\n  The third line is also necessary to avoid a problem. This line resets\nthe completion method for `cd` to directory completion since it was set \nto function completion after `bash_completion` for some reason.\n\n### with `command_not_found`\n\n  You can also use `command_not_found`.  Before the three lines for\nbash-completion, please add the following function. The path should be\nchanged if `command-not-found` script exists in the different directory. \n\n```bash\ncommand_not_found_handle() {\n        if [ -e /usr/lib/command-not-found ] ; then\n                /usr/lib/command-not-found -- \"$1\"\n        fi\n}\n```\n\nFor some reason, this definition has to be written BEFORE the call of\nbash-completion. \n\n## macOS (Sequoia 15.3.1 or some versions near it)\n\n  In macOS, the three lines for bash-completion are required at the bottom\nof `~/.sushrc`. Moreover, we may have to install bash-completion by ourselves. \n\n  When you are using homebrew, you can do it with the following command.\n\n```bash\n🍣 brew install bash-completion\n```\n\nThen you can find `bash_completion` script in a directory under `/opt/homebrew/Cellar`\nlike this. \n\n```bash\n🍣 find /opt/homebrew/Cellar/ | grep bash_completion$\n/opt/homebrew/Cellar//bash-completion/1.3_3/etc/bash_completion\n```\n\nPlease use the path found as above instead of the path used in the Linux example.\n\n"
  },
  {
    "path": "error",
    "content": "./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_builtins.bash\n./test_builtins.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_job.bash\n./test_job.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_builtins.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_redirects.bash\n./test_others.bash\n./test_fixed_v1.2.1.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n./test_fixed_v1.2.4.bash\n"
  },
  {
    "path": "i18n/ar.ftl",
    "content": "license = ترخيص\nversion = إصدار\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    هذا برنامج مفتوح المصدر.\n    يمكنك استخدامه وتعديله وإعادة توزيعه بحرية، سواء بالشيفرة المصدرية أو\n    بشكل ثنائي، مع أو بدون تعديل، بشرط الحفاظ على إشعار حقوق النشر الأصلي\n    وقائمة الشروط وإخلاء المسؤولية.\n\n    يتم توفير هذا البرنامج \"كما هو\"، دون أي ضمان من أي نوع،\n    سواء كان صريحًا أو ضمنيًا، في حدود ما يسمح به القانون.\n"
  },
  {
    "path": "i18n/da.ftl",
    "content": "license = Licens\nversion = version\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nbuiltins =\n    Builtin commands:\n        cd                        Change the current directory\n        pwd                       Print the current working directory\n        exit                      Exit the shell\n        source                    Read and execute commands from a file\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        command                   Execute a command, ignoring shell functions\n        continue                  Resume the next iteration of a loop\n        eval                      Evaluate arguments as a shell command\n        local                     Declare local variables inside functions\n        return                    Return from a shell function\n        false                     Do nothing, unsuccessfully\n        true                      Do nothing, successfully\n        shift                     Shift positional parameters\n        unalias                   Remove aliases\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Dette er open source-software.\n    Du er fri til at bruge, ændre og redistribuere denne software i kilde-\n    eller binær form, med eller uden ændringer, forudsat at den originale\n    copyrightmeddelelse, betingelser og ansvarsfraskrivelse bevares.\n\n    DENNE SOFTWARE LEVERES \"SOM DEN ER\", UDEN NOGEN FORM FOR GARANTI,\n    UDTRYKKELIG ELLER UNDERFORSTÅET, I DET OMFANG LOVEN TILLADER.\n"
  },
  {
    "path": "i18n/de.ftl",
    "content": "license = Lizenz\nversion = Version\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Dies ist Open-Source-Software.\n    Sie dürfen diese Software in Quell- oder Binärform frei verwenden,\n    modifizieren und weiterverbreiten, mit oder ohne Änderungen,\n    vorausgesetzt, dass der ursprüngliche Urheberrechtshinweis,\n    die Liste der Bedingungen und der Haftungsausschluss erhalten bleiben.\n\n    DIESE SOFTWARE WIRD \"WIE BESEHEN\" BEREITGESTELLT, OHNE JEGLICHE GARANTIE,\n    AUSDRÜCKLICH ODER STILLSCHWEIGEND, SOWEIT GESETZLICH ZULÄSSIG.\n"
  },
  {
    "path": "i18n/el.ftl",
    "content": "license = Άδεια\nversion = έκδοση\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Αυτό είναι λογισμικό ανοιχτού κώδικα.\n    Είστε ελεύθεροι να χρησιμοποιείτε, να τροποποιείτε και να διανέμετε αυτό το λογισμικό\n    σε μορφή πηγαίου ή δυαδικού κώδικα, με ή χωρίς τροποποιήσεις, υπό την προϋπόθεση\n    ότι διατηρείται η αρχική ειδοποίηση πνευματικών δικαιωμάτων, ο κατάλογος όρων και η αποποίηση ευθυνών.\n\n    ΑΥΤΟ ΤΟ ΛΟΓΙΣΜΙΚΟ ΠΑΡΕΧΕΤΑΙ \"ΩΣ ΕΧΕΙ\", ΧΩΡΙΣ ΚΑΜΙΑ ΕΓΓΥΗΣΗ,\n    ΡΗΤΗ Ή ΣΙΩΠΗΡΗ, ΣΤΟΝ ΒΑΘΜΟ ΠΟΥ ΕΠΙΤΡΕΠΕΤΑΙ ΑΠΟ ΤΟΝ ΝΟΜΟ.\n"
  },
  {
    "path": "i18n/en.ftl",
    "content": "license = License\nversion = version\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    This is open source software.\n    You are free to use, modify, and redistribute this software in source\n    or binary form, with or without modification, provided that the original\n    copyright notice, list of conditions, and disclaimer are retained.\n\n    THIS SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n    EXPRESS OR IMPLIED, TO THE EXTENT PERMITTED BY LAW.\n"
  },
  {
    "path": "i18n/es.ftl",
    "content": "license = Licencia\nversion = versión\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Este es un software de código abierto.\n    Usted es libre de usar, modificar y redistribuir este software en forma\n    fuente o binaria, con o sin modificaciones, siempre que el aviso de\n    copyright original, la lista de condiciones y la cláusula de\n    exención de responsabilidad sean conservados.\n\n    ESTE SOFTWARE SE PROPORCIONA \"TAL CUAL\", SIN GARANTÍA DE NINGÚN TIPO,\n    EXPRESA O IMPLÍCITA, EN LA MEDIDA EN QUE LO PERMITA LA LEY.\n"
  },
  {
    "path": "i18n/fi.ftl",
    "content": "license = Lisenssi\nversion = versio\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Tämä on avoimen lähdekoodin ohjelmisto.\n    Voit vapaasti käyttää, muokata ja levittää tätä ohjelmistoa\n    lähde- tai binäärimuodossa, muutoksilla tai ilman, kunhan alkuperäinen\n    tekijänoikeusilmoitus, ehdot ja vastuuvapauslauseke säilyvät.\n\n    TÄMÄ OHJELMISTO TOIMITETAAN \"SELLAISENAAN\" ILMAN MITÄÄN TAKUITA,\n    ILMAISTUJA TAI OLETETTUJA, LAIN SALLIMISSA RAJOISSA.\n"
  },
  {
    "path": "i18n/fr.ftl",
    "content": "license = Licence\nversion = version\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Ceci est un logiciel open source.\n    Vous êtes libre d'utiliser, de modifier et de redistribuer ce logiciel\n    sous forme source ou binaire, avec ou sans modification, à condition que\n    l'avis de droit d’auteur original, la liste des conditions et la clause\n    de non-responsabilité soient conservés.\n\n    CE LOGICIEL EST FOURNI \"TEL QUEL\", SANS GARANTIE D'AUCUNE SORTE,\n    EXPRESSE OU IMPLICITE, DANS LA LIMITE AUTORISÉE PAR LA LOI.\n"
  },
  {
    "path": "i18n/hi.ftl",
    "content": "license = लाइसेंस\nversion = संस्करण\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    यह एक ओपन सोर्स सॉफ़्टवेयर है।\n    आप इस सॉफ़्टवेयर का उपयोग, संशोधन और पुनर्वितरण स्वतंत्र रूप से कर सकते हैं,\n    स्रोत या बाइनरी रूप में, संशोधन के साथ या बिना, बशर्ते कि मूल\n    कॉपीराइट सूचना, शर्तों की सूची, और अस्वीकरण बनाए रखें।\n\n    यह सॉफ़्टवेयर \"जैसा है\" प्रदान किया गया है, किसी भी प्रकार की वारंटी के बिना,\n    स्पष्ट या निहित, कानून द्वारा अनुमत सीमा तक।\n"
  },
  {
    "path": "i18n/it.ftl",
    "content": "License = Licenza\nversion = versione\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Questo è un software open source.\n    Sei libero di usare, modificare e ridistribuire questo software in forma\n    sorgente o binaria, con o senza modifiche, a condizione che\n    l'avviso di copyright originale, l'elenco delle condizioni e la clausola\n    di esclusione di responsabilità siano mantenuti.\n\n    QUESTO SOFTWARE È FORNITO \"COSÌ COM'È\", SENZA ALCUNA GARANZIA,\n    ESPRESSA O IMPLICITA, NEI LIMITI CONSENTITI DALLA LEGGE.\n"
  },
  {
    "path": "i18n/ja.ftl",
    "content": "license = ライセンス\nversion = バージョン\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    これはオープンソースソフトウェアです。\n    このソフトウェアは、オリジナルの著作権表示、条件の一覧、\n    免責事項が保持されている限り、ソースまたはバイナリ形式で、\n    修正の有無にかかわらず、自由に使用、変更、再配布できます。\n\n    本ソフトウェアは、法律で許される範囲において、\n    明示的または黙示的ないかなる保証もなく「現状のまま」提供されます。\n"
  },
  {
    "path": "i18n/ko.ftl",
    "content": "License = 라이선스\nversion = 버전\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    이것은 오픈 소스 소프트웨어입니다.\n    원본 저작권 고지, 조건 목록 및 면책 조항이 유지되는 한,\n    이 소프트웨어를 소스 또는 바이너리 형태로 수정하거나 하지 않고도\n    자유롭게 사용, 수정 및 재배포할 수 있습니다.\n\n    이 소프트웨어는 법이 허용하는 범위 내에서\n    명시적이거나 묵시적인 어떠한 보증 없이 \"있는 그대로\" 제공됩니다.\n"
  },
  {
    "path": "i18n/nl.ftl",
    "content": "license = Licentie\nversion = versie\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Dit is open-sourcesoftware.\n    U bent vrij om deze software te gebruiken, aan te passen en opnieuw te verspreiden,\n    in bron- of binaire vorm, met of zonder wijzigingen, op voorwaarde dat de originele\n    copyrightvermelding, lijst van voorwaarden en disclaimer behouden blijven.\n\n    DEZE SOFTWARE WORDT GELEVERD \"AS IS\", ZONDER ENIGE GARANTIE,\n    EXPLICIET OF IMPLICIET, VOOR ZOVER TOEGESTAAN DOOR DE WET.\n"
  },
  {
    "path": "i18n/no.ftl",
    "content": "license = Lisens\nversion = versjon\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Dette er åpen kildekode-programvare.\n    Du står fritt til å bruke, endre og redistribuere denne programvaren,\n    i kildekode eller binær form, med eller uten modifikasjoner,\n    forutsatt at den opprinnelige opphavsrettsmerknaden, vilkårene\n    og ansvarsfraskrivelsen beholdes.\n\n    DENNE PROGRAMVAREN LEVERES \"SOM DEN ER\", UTEN GARANTIER AV NOE SLAG,\n    VERKEN UTTRYKTE ELLER UNDERFORSTÅTTE, I DEN UTSTREKNING LOVEN TILLATER.\n"
  },
  {
    "path": "i18n/pl.ftl",
    "content": "license = Licencja\nversion = wersja\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    To jest oprogramowanie open source.\n    Możesz swobodnie używać, modyfikować i rozpowszechniać to oprogramowanie\n    w formie źródłowej lub binarnej, z modyfikacjami lub bez, pod warunkiem\n    zachowania oryginalnej noty copyright, listy warunków i zrzeczenia się odpowiedzialności.\n\n    TO OPROGRAMOWANIE JEST DOSTARCZANE \"TAKIE, JAKIE JEST\", BEZ ŻADNEJ GWARANCJI,\n    WYRAŹNEJ LUB DOROZUMIANEJ, W ZAKRESIE DOZWOLONYM PRZEZ PRAWO.\n"
  },
  {
    "path": "i18n/pt.ftl",
    "content": "license = Licença\nversion = versão\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Este é um software de código aberto.\n    Você é livre para usar, modificar e redistribuir este software\n    em forma de código-fonte ou binário, com ou sem modificações,\n    desde que o aviso de direitos autorais, a lista de condições\n    e a isenção de responsabilidade originais sejam mantidos.\n\n    ESTE SOFTWARE É FORNECIDO \"NO ESTADO EM QUE SE ENCONTRA\", SEM QUALQUER GARANTIA,\n    EXPRESSA OU IMPLÍCITA, NA MEDIDA PERMITIDA PELA LEI.\n"
  },
  {
    "path": "i18n/ru.ftl",
    "content": "license = Лицензия\nversion = версия\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Это программное обеспечение с открытым исходным кодом.\n    Вы можете свободно использовать, изменять и распространять это программное обеспечение\n    в исходной или двоичной форме, с изменениями или без, при условии сохранения\n    оригинального уведомления об авторских правах, списка условий и отказа от ответственности.\n\n    ЭТО ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ \"КАК ЕСТЬ\", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ,\n    ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, В ПРЕДЕЛАХ, ДОПУСКАЕМЫХ ЗАКОНОМ.\n"
  },
  {
    "path": "i18n/sl.ftl",
    "content": "license = Licenca\nversion = različica\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    To je programska oprema z odprto kodo.\n    Prosto jo lahko uporabljate, spreminjate in razširjate v izvorni\n    ali binarni obliki, s spremembami ali brez, pod pogojem, da se ohranijo\n    izvirno obvestilo o avtorskih pravicah, seznam pogojev in izjava o omejitvi odgovornosti.\n\n    TA PROGRAMSKA OPREMA SE PONUDI \"TAKŠNA, KOT JE\", BREZ KAKRŠNE KOLI GARANCIJE,\n    IZRECNE ALI IMPLICITNE, V OBSEGU, KI GA DOVOLJUJE ZAKON.\n"
  },
  {
    "path": "i18n/sv.ftl",
    "content": "license = Licens\nversion = version\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Detta är öppen källkodsprogramvara.\n    Du är fri att använda, modifiera och distribuera denna programvara i källkod\n    eller binär form, med eller utan ändringar, förutsatt att det ursprungliga\n    upphovsrättsmeddelandet, villkoren och ansvarsfriskrivningen bevaras.\n\n    DENNA PROGRAMVARA TILLHANDAHÅLLS \"I BEFINTLIGT SKICK\", UTAN NÅGON GARANTI,\n    VARE SIG UTTRYCKLIG ELLER UNDERFÖRSTÅDD, I DEN UTSTRÄCKNING LAGEN MEDGER.\n"
  },
  {
    "path": "i18n/sw.ftl",
    "content": "license = Leseni\nversion = toleo\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Hii ni programu ya chanzo huria.\n    Unaweza kuitumia, kuibadilisha, na kuisambaza tena kwa uhuru\n    katika hali ya msimbo wa chanzo au iliyobainishwa, ukiwa umeifanyia\n    mabadiliko au la, mradi taarifa ya hakimiliki, masharti na\n    kanusho asilia zimedumishwa.\n\n    PROGRAMU HII HUTOLEWA \"KAMA ILIVYO\", BILA DHAMANA YOYOTE,\n    IWE IKO WAZI AU IMEFICHWA, KADRI INAVYORUHUSIWA KISHERIA.\n"
  },
  {
    "path": "i18n/uk.ftl",
    "content": "license = Ліцензія\nversion = версія\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    Це програмне забезпечення з відкритим кодом.\n    Ви можете вільно використовувати, змінювати та розповсюджувати це програмне забезпечення\n    у вихідному або бінарному вигляді, з модифікаціями або без, за умови збереження\n    оригінального повідомлення про авторські права, списку умов і відмови від відповідальності.\n\n    ЦЕ ПРОГРАМНЕ ЗАБЕЗПЕЧЕННЯ НАДАЄТЬСЯ \"ЯК Є\", БЕЗ ЖОДНИХ ГАРАНТІЙ,\n    ЯВНИХ ЧИ НЕЯВНИХ, У МЕЖАХ, ДОЗВОЛЕНИХ ЗАКОНОМ.\n"
  },
  {
    "path": "i18n/zh.ftl",
    "content": "license = 授權\nversion = 版本\n\nusage = Usage: sushi [LONG OPTIONS] [OPTIONS] [SCRIPT] [ARGS]\n\noptions =\n    Options:\n        -c                        Execute COMMAND and exit\n        -i                        Force interactive mode\n        -l, --login               unsuported\n        -r                        unsuported\n        -s                        unsuported\n        -D                        unsuported\n        -O, +O                    unsuported\n        --                        unsuported\n        --debugger                unsuported\n        --dimp-po-strings         unsuported\n        --help                    Display this help message and exit\n        --init-file FILE          unsuported\n        --rcfile FILE             unsuported\n        --noediting               unsuported\n        --noprofile               unsuported\n        --norc                    unsuported\n        --posix                   unsuported\n        --restricted              unsuported\n        -v, --verbose             unsuported\n        --version                 Display version information and exit\n        -e                        Exit immediately if a command returns non‑zero\n        --pipefail                Return status of first failing command in pipeline\n        -B                        Enable brace expansion (equivalent to `set -B`)\n\ncomp-commands =\n    Compound commands:\n        if                        Conditional execution\n        while                     Loop while a condition is true\n        ()                        Run commands in a subshell\n        case                      Match patterns against a word\n        until                     unsupported\n        for                       Iterate over a list of items\n\nbuiltins =\n    Builtin commands:\n        :                         No-op (does nothing)\n        \".\"                       Source a file in the current shell\n        alias                     Define or display aliases\n        bg                        Resume a job in the background\n        bind                      Unsupported\n        break                     Exit from a loop\n        builtin                   Execute a shell builtin, bypassing functions\n        caller                    Unsupported\n        cd                        Change the current directory\n        command                   Execute a command, ignoring shell functions\n        compgen                   Generate possible completion matches\n        complete                  Specify how arguments are completed\n        compopt                   Unsupported\n        continue                  Resume the next iteration of a loop\n        declare                   Unsupported\n        dirs                      Unsupported\n        disown                    Unsupported\n        echo                      Unsupported\n        enable                    Unsupported\n        eval                      Evaluate arguments as a shell command\n        exec                      Unsupported\n        exit                      Exit the shell\n        export                    Unsupported\n        false                     Do nothing, unsuccessfully\n        fc                        Unsupported\n        fg                        Resume a job in the foreground\n        getopts                   Parse positional parameters\n        hash                      Unsupported\n        help                      Unsupported\n        history                   Show or manipulate the command history\n        jobs                      Display status of jobs\n        kill                      Unsupported\n        let                       Unsupported\n        local                     Declare local variables inside functions\n        logout                    Unsupported\n        mapfile                   Unsupported\n        popd                      Unsupported\n        printf                    Unsupported\n        pushd                     Unsupported\n        pwd                       Print the current working directory\n        read                      Read a line from standard input\n        readonly                  Unsupported\n        return                    Return from a shell function\n        set                       Modify shell options\n        shift                     Shift positional parameters\n        shopt                     Change shell optional behavior\n        source                    Read and execute commands from a file\n        suspend                   Unsupported\n        test                      Unsupported\n        times                     Unsupported\n        trap                      Unsupported\n        true                      Do nothing, successfully\n        type                      Unsupported\n        typeset                   Unsupported\n        ulimit                    Unsupported\n        umask                     Unsupported\n        unalias                   Remove aliases\n        unset                     Unset variables or functions\n        wait                      Wait for jobs to complete\n\nparameters =\n    Special parameters:\n        \"$\"                       Process ID of the shell or script\n        ?                         Exit status of the last command\n        @                         All positional parameters (as separate words)\n        #                         Number of positional parameters\n        -                         Current shell options\n        _                         Last argument of the previous command\n        !                         unsupported\n\nshopt =\n    Shell options:\n        dotglob                   Include hidden files (starting with .) in pathname expansions\n        extglob                   Enable extended pattern matching operators\n        progcomp                  Enable programmable command completion\n        nullglob                  Allow patterns which match nothing to expand to null string\n\nvariables-born =\n    Born Shell Variables:\n        CDPATH                    unsuported\n        HOME                      User’s home directory\n        IFS                       Internal Field Separator (partial support)\n        MAIL                      unsuported\n        MAILPATH                  unsuported\n        OPTARG                    Argument value for the current option (getopts)\n        OPTIND                    Index of the next argument to be processed by getopts\n        PATH                      Search path for commands\n        PS1                       Primary prompt string\n        PS2                       Secondary prompt string\n\nvariables-bash =\n    Bash Variables:\n        _                         Last argument of the previous command\n        BASH                      unsuported\n        BASHOPTS                  unsuported\n        BASHPID                   PID of the current Bash process\n        BASH_ALIASES              unsuported\n        BASH_ARGC                 unsuported\n        BASH_ARGV                 unsuported\n        BASH_ARGV0                unsuported\n        BASH_CMDS                 unsuported\n        BASH_COMMAND              unsuported\n        BASH_COMPAT               unsuported\n        BASH_ENV                  unsuported\n        BASH_EXECUTION_STRING     unsuported\n        BASH_LINENO               unsuported\n        BASH_LOADABLES_PATH       unsuported\n        BASH_REMATCH              Array of regex capture groups\n        BASH_SOURCE               unsuported\n        BASH_SUBSHELL             Current subshell level\n        BASH_VERSINFO             Array with Bash version fields\n        BASH_VERSION              Human‑readable Bash version\n        BASH_XTRACEFD             unsuported\n        CHILD_MAX                 unsuported\n        COLUMNS                   unsuported\n        COMP_CWORD                unsuported\n        COMP_LINE                 unsuported\n        COMP_POINT                unsuported\n        COMP_TYPE                 unsuported\n        COMP_KEY                  unsuported\n        COMP_WORDBREAKS           unsuported\n        COMP_WORDS                unsuported\n        COMPREPLY                 unsuported\n        COPROC                    unsuported\n        DIRSTACK                  unsuported\n        EMACS                     unsuported\n        ENV                       unsuported\n        EPOCHREALTIME             Epoch seconds with microseconds\n        EPOCHSECONDS              Epoch seconds (integer)\n        EUID                      unsuported\n        EXECIGNORE                unsuported\n        FCEDIT                    unsuported\n        FIGNORE                   unsuported\n        FUNCNAME                  unsuported\n        FUNCNEST                  unsuported\n        GLOBIGNORE                unsuported\n        GROUPS                    unsuported\n        histchars                 unsuported\n        HISTCMD                   unsuported\n        HISTCONTROL               unsuported\n        HISTFILE                  Path to the history file\n        HISTFILESIZE              Max lines kept in history file\n        HISTIGNORE                unsuported\n        HISTSIZE                  unsuported\n        HISTTIMEFORMAT            unsuported\n        HOSTFILE                  unsuported\n        HOSTNAME                  unsuported\n        HOSTTYPE                  Hardware platform string\n        IGNOREEOF                 unsuported\n        INPUTRC                   unsuported\n        INSIDE_EMACS              unsuported\n        LANG                      Current locale\n        LC_ALL                    unsuported\n        LC_COLLATE                unsuported\n        LC_CTYPE                  unsuported\n        LC_MESSAGES               unsuported\n        LC_NUMERIC                unsuported\n        LC_TIME                   unsuported\n        LINENO                    Current script line number\n        LINES                     unsuported\n        MACHTYPE                  Machine type triple\n        MAILCHECK                 unsuported\n        MAPFILE                   unsuported\n        OLDPWD                    Previous working directory\n        OPTERR                    unsuported\n        OSTYPE                    Operating‑system type\n        PIPESTATUS                Exit statuses of the last pipeline\n        POSIXLY_CORRECT           unsuported\n        PPID                      unsuported\n        PROMPT_COMMAND            unsuported\n        PROMPT_DIRTRIM            unsuported\n        PS0                       unsuported\n        PS3                       unsuported\n        PS4                       Debug prompt (used with set -x)\n        PWD                       Current working directory\n        RANDOM                    Pseudo‑random integer (0‑32767)\n        READLINE_ARGUMENT         unsuported\n        READLINE_LINE             unsuported\n        READLINE_MARK             unsuported\n        READLINE_POINT            unsuported\n        REPLY                     unsuported\n        SECONDS                   Seconds since the shell started\n        SHELL                     Path to the user’s default shell\n        SHELLOPTS                 unsuported\n        SHLVL                     Shell nesting level\n        SRANDOM                   64-bit cryptographic random\n        TIMEFORMAT                unsuported\n        TMOUT                     unsuported\n        TMPDIR                    unsuported\n        UID                       unsuported\n    Beyond Bash feature: \n        branch display in prompt\n           \ntext-help = Project homepage: https://github.com/shellgei/rusty_bash\n\ntext-version =\n    本軟體為開放原始碼軟體。\n    您可自由使用、修改及再散佈本軟體，\n    不論是原始碼或二進位形式，無論是否修改，前提是保留\n    原始著作權聲明、條款清單及免責聲明。\n\n    本軟體按「原樣」提供，無任何明示或默示保證，\n    在法律允許的範圍內適用。\n"
  },
  {
    "path": "src/core/builtins/alias.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\n\npub fn alias(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() == 1 || args.len() == 2 && args[1] == \"-p\" {\n        if !core.shopts.query(\"expand_aliases\") {\n            return 0;\n        }\n        for k in &core.db.get_indexes_all(\"BASH_ALIASES\") {\n            let v = core.db.get_elem(\"BASH_ALIASES\", k).unwrap();\n            println!(\"alias {k}='{v}'\");\n        }\n        return 0;\n    }\n\n    if args.len() == 2 && args[1].contains(\"=\") {\n        let kv: Vec<String> = args[1].split('=').map(|t| t.to_string()).collect();\n        let _ = core\n            .db\n            .set_assoc_elem(\"BASH_ALIASES\", &kv[0], &kv[1..].join(\"=\"), None);\n        return 0;\n    }\n\n    if args.len() == 2 && core.db.has_array_value(\"BASH_ALIASES\", &args[1]) {\n        let alias = core.db.get_elem(\"BASH_ALIASES\", &args[1]).unwrap();\n        println!(\"alias {}='{}'\", &args[1], &alias);\n        return 0;\n    }\n\n    0\n}\n\npub fn unalias(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() <= 1 {\n        println!(\"unalias: usage: unalias [-a] name [name ...]\");\n    }\n\n    if args.iter().any(|s| s == \"-a\") {\n        let _ = core.db.unset(\"BASH_ALIASES\", None, false);\n        let _ = core.db.init_assoc(\"BASH_ALIASES\", None, true, false);\n        return 0;\n    }\n\n    args[1..].iter().for_each(|e| {\n        let _ = core.db.unset_array_elem(\"BASH_ALIASES\", e);\n    });\n\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/caller.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\n\nfn caller_no_arg(core: &mut ShellCore) -> i32 {\n    let linenos_len = core.db.index_based_len(\"BASH_LINENO\");\n    if linenos_len == 0 {\n        return 1;\n    }\n\n    let lineno = core.db.get_elem(\"BASH_LINENO\", \"0\").unwrap();\n    let mut funcname = core.db.get_elem(\"FUNCNAME\", \"1\").unwrap();\n\n    if funcname == \"\" {\n        funcname = \"NULL\".to_string();\n    }else {\n        funcname = \"main\".to_string();\n    }\n\n    println!(\"{} {}\", &lineno, funcname);\n    0\n}\n\nfn caller_arg(core: &mut ShellCore, args: &[String]) -> i32 {\n    let pos = match args[1].parse::<usize>() {\n        Ok(n) => n,\n        _ => return 1,\n    };\n\n    let linenos_len = core.db.index_based_len(\"BASH_LINENO\");\n    if linenos_len == 0 {\n        return 1;\n    }\n    //let functions_len = core.db.index_based_len(\"FUNCNAME\");\n\n\n    let lineno = core.db.get_elem(\"BASH_LINENO\", &pos.to_string()).unwrap();\n    let funcname = core.db.get_elem(\"FUNCNAME\", &(pos+1).to_string()).unwrap();\n\n    let mut script_name = core.script_name.clone();\n    if script_name == \"-\" {\n        script_name = \"main\".to_string();\n    }\n\n    println!(\"{} {} {}\", &lineno, funcname, script_name);\n    0\n}\n\npub fn caller(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() == 1 {\n        caller_no_arg(core)\n    }else{\n        caller_arg(core, args)\n    }\n}\n"
  },
  {
    "path": "src/core/builtins/cd.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::utils::{arg, file};\nuse crate::{error, ShellCore};\n\npub fn cd(core: &mut ShellCore, args: &[String]) -> i32 {\n    if core.db.flags.contains('r') {\n        return super::error_(1, &args[0], \"restricted\", core);\n    }\n\n    let mut args = args.to_owned();\n    arg::consume_arg(\"--\", &mut args);\n\n    if args.len() > 2 {\n        eprintln!(\"sush: cd: too many arguments\");\n        return 1;\n    }\n\n    // only \"cd\"\n    if args.len() == 1 {\n        set_oldpwd(core);\n        let home = core.db.get_param(\"HOME\").unwrap_or_default();\n        return change_directory(core, &home);\n    }\n\n    // cd -\n    if args[1] == \"-\" {\n        return cd_oldpwd(core);\n    }\n\n    // cd /some/dir\n    set_oldpwd(core);\n    change_directory(core, &args[1])\n}\n\nfn cd_oldpwd(core: &mut ShellCore) -> i32 {\n    match core.db.get_param(\"OLDPWD\") {\n        Ok(old) => {\n            println!(\"{}\", &old);\n            set_oldpwd(core);\n            change_directory(core, &old)\n        }\n        Err(_) => {\n            error::print(\"cd: OLDPWD not set\", core);\n            1\n        }\n    }\n}\n\nfn set_oldpwd(core: &mut ShellCore) {\n    if let Some(old) = core.get_current_directory() {\n        let _ = core\n            .db\n            .set_param(\"OLDPWD\", &old.display().to_string(), Some(0));\n    };\n}\n\nfn change_directory(core: &mut ShellCore, target: &str) -> i32 {\n    let path = file::make_canonical_path(core, target);\n    if core.set_current_directory(&path).is_ok() {\n        let _ = core\n            .db\n            .set_param(\"PWD\", &path.display().to_string(), Some(0));\n        0\n    } else {\n        eprintln!(\"sush: cd: {:?}: No such file or directory\", &path);\n        1\n    }\n}\n"
  },
  {
    "path": "src/core/builtins/command.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::command::simple::SimpleCommand;\nuse crate::elements::io::pipe::Pipe;\nuse crate::utils::{arg, file};\nuse crate::{error, file_check, proc_ctrl, utils, ShellCore};\n\npub fn builtin(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() <= 1 {\n        return 0;\n    }\n\n    if !core.builtins.contains_key(&args[1]) {\n        let msg = format!(\"{}: not a shell builtin\", &args[1]);\n        return super::error_(1, &args[0], &msg, core);\n    }\n\n    core.builtins[&args[1]](core, &args[1..])\n}\n\nfn command_v(words: &[String], core: &mut ShellCore, large_v: bool) -> i32 {\n    if words.is_empty() {\n        return 0;\n    }\n\n    let mut return_value = 1;\n\n    for com in words.iter() {\n        if utils::reserved(com) {\n            return_value = 0;\n            match large_v {\n                true => println!(\"{} is a shell keyword\", &com),\n                false => println!(\"{}\", &com),\n            }\n        } else if core.db.has_array_value(\"BASH_ALIASES\", com) {\n            return_value = 0;\n            let alias = core.db.get_elem(\"BASH_ALIASES\", com).unwrap();\n            match large_v {\n                true => println!(\"{} is aliased to `{}'\", &com, &alias),\n                false => println!(\"alias {}='{}'\", &com, &alias),\n            }\n        } else if core.builtins.contains_key(com) {\n            return_value = 0;\n\n            match large_v {\n                true => println!(\"{} is a shell builtin\", &com),\n                false => println!(\"{}\", &com),\n            }\n        } else if core.db.functions.contains_key(com) {\n            return_value = 0;\n            match large_v {\n                true => {\n                    println!(\"{} is a function\", &com);\n                    core.db.functions.get_mut(com).unwrap().pretty_print(0);\n                }\n                false => println!(\"{}\", &com),\n            }\n        } else if let Some(path) = file::search_command(com) {\n            return_value = 0;\n            match large_v {\n                true => println!(\"{} is {}\", &com, &path),\n                false => println!(\"{}\", &com),\n            }\n        } else if file_check::is_executable(com) {\n            return_value = 0;\n            match large_v {\n                true => println!(\"{} is {}\", &com, &com),\n                false => println!(\"{}\", &com),\n            }\n        } else if large_v {\n            let msg = format!(\"command: {com}: not found\");\n            error::print(&msg, core);\n        }\n    }\n\n    return_value\n}\n\npub fn command(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() > 1 {\n        if core.subst_builtins.contains_key(&args[1]) {\n            //TODO\n            return super::error_(1, &args[0], \"substitution command are not supported\", core);\n        }\n    }\n\n    let mut args = arg::dissolve_options(args);\n    if core.db.flags.contains('r') && arg::consume_arg(\"-p\", &mut args) {\n        return super::error_(1, &args[0], \"-p: restricted\", core);\n    }\n\n    if args.len() <= 1 {\n        return 0;\n    }\n\n    let mut pos = 1;\n    while args.len() > pos {\n        match args[pos].starts_with(\"-\") {\n            true => pos += 1,\n            false => break,\n        }\n    }\n\n    let words = args[pos..].to_vec();\n    if words.is_empty() {\n        return 0;\n    }\n\n    let mut args = args[..pos].to_vec();\n    args = arg::dissolve_options(&args);\n\n    let last_option = args.last().unwrap();\n    if last_option == \"-V\" || last_option == \"-v\" {\n        return command_v(&words, core, last_option == \"-V\");\n    } else if core.builtins.contains_key(&words[0]) {\n        return core.builtins[&words[0]](core, &words[..]);\n    }\n\n    let mut command = SimpleCommand::default();\n    let mut pipe = Pipe::new(\"\".to_string());\n    command.args = words;\n    if let Ok(pid) = command.exec_command(core, &mut pipe) {\n        proc_ctrl::wait_pipeline(core, vec![pid], false, false);\n    }\n\n    core.db.exit_status\n}\n"
  },
  {
    "path": "src/core/builtins/compgen.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::word::{path_expansion, tilde_expansion};\nuse crate::elements::word::{Word, WordMode};\nuse crate::utils;\nuse crate::utils::{arg, directory, glob};\nuse crate::{file_check, Feeder, ShellCore};\nuse faccess;\nuse faccess::PathExt;\nuse rev_lines::RevLines;\nuse std::env;\nuse std::collections::HashSet;\nuse std::fs::File;\nuse std::io::{BufRead, BufReader};\nuse std::path::Path;\n\npub fn compgen_f(core: &mut ShellCore, args: &[String], dir_only: bool) -> Vec<String> {\n    let mut arg_index = 2;\n    if args.len() > 2 && args[2] == \"--\" {\n        arg_index = 3;\n    }\n\n    let path = if arg_index < args.len() {\n        args[arg_index].to_string()\n    } else {\n        \"\".to_string()\n    }\n    .replace(\"\\\\\", \"\");\n\n    let mut split: Vec<String> = path.split(\"/\").map(|s| s.to_string()).collect();\n    let key = match split.pop() {\n        Some(g) => g,\n        _ => return vec![],\n    };\n\n    split.push(\"\".to_string());\n    let org_dir = split.join(\"/\");\n    let mut dir = org_dir.clone();\n    if dir.starts_with(\"~\") {\n        let mut feeder = Feeder::new(&dir);\n        if let Ok(Some(mut w)) = Word::parse(&mut feeder, core, Some(WordMode::CompgenF)) {\n            tilde_expansion::eval(&mut w, core);\n            dir = w.text + &feeder.consume(feeder.len());\n        }\n    }\n\n    if key.is_empty() {\n        let mut files = directory::files(&dir);\n        if dir_only {\n            files.retain(|p| file_check::is_dir(&(dir.clone() + p)));\n        }\n        files.sort();\n        return files.iter().map(|f| org_dir.clone() + f).collect();\n    }\n\n    let mut ans = directory::glob(&dir, &(key.clone() + \"*\"), &core.shopts);\n    if key == \".\" {\n        ans.append(&mut directory::glob(&dir, \".\", &core.shopts));\n        ans.append(&mut directory::glob(&dir, \"..\", &core.shopts));\n    }\n    ans.iter_mut().for_each(|a| {\n        a.pop();\n    });\n    if dir_only {\n        ans.retain(|p| file_check::is_dir(p));\n    }\n    ans.sort();\n    ans.iter_mut().for_each(|e| {\n        *e = e.replacen(&dir, &org_dir, 1);\n    });\n    ans\n}\n\nfn normalize_compgen_args(args: &[String]) -> Vec<String> {\n    if args.len() < 3 || args[1] != \"-A\" {\n        return args.to_vec();\n    }\n\n    let mut result = args.to_vec();\n    result.remove(1);\n    let replace = match result[1].as_str() {\n        \"command\" => \"-c\",\n        \"directory\" => \"-d\",\n        \"file\" => \"-f\",\n        \"user\" => \"-u\",\n        \"setopt\" => \"-o\",\n        \"function\" => \"-A function\",\n        \"hostname\" => \"-A hostname\",\n        \"shopt\" => \"-A shopt\",\n        \"stopped\" => \"-A stopped\",\n        \"job\" => \"-j\",\n        \"variable\" => \"-v\",\n        a => a,\n    };\n\n    result[1] = replace.to_string();\n    result\n}\n\nfn command_list(target: &String, core: &mut ShellCore) -> Vec<String> {\n    let mut comlist = HashSet::new();\n    for path in core.db.get_param(\"PATH\").unwrap_or_default().split(':') {\n        /* /mnt is ellimimated from PATH on WSL because it contains all files in Windows.*/\n        let explorer_path = path.ends_with(\"Windows\") || path.ends_with(\"WINDOWS\");\n        if utils::is_wsl() && path.starts_with(\"/mnt\") && !explorer_path {\n            // We want to use explorer.exe\n            continue;\n        }\n\n        for command in directory::files(path).iter() {\n            if !Path::new(&(path.to_owned() + \"/\" + command)).executable() {\n                continue;\n            }\n\n            if command.starts_with(target) {\n                comlist.insert(command.clone());\n            }\n        }\n    }\n    let mut ans: Vec<String> = comlist.iter().map(|c| c.to_string()).collect();\n    ans.sort();\n    ans\n}\n\npub fn compgen(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if args.len() <= 1 {\n        eprintln!(\"sush: {}: still unsupported\", &args[0]);\n        return 1;\n    }\n    let mut args = arg::dissolve_options(&args);\n    let exclude = arg::consume_with_next_arg(\"-X\", &mut args); //TODO: implement X pattern\n    let prefix = arg::consume_with_next_arg(\"-P\", &mut args);\n    let suffix = arg::consume_with_next_arg(\"-S\", &mut args);\n\n    let args = normalize_compgen_args(&args);\n\n    let mut ans = match args[1].as_str() {\n        \"-a\" => compgen_a(core, &args),\n        \"-b\" => compgen_b(core, &args),\n        \"-c\" => compgen_c(core, &args),\n        \"-d\" => compgen_d(core, &args),\n        \"-e\" => compgen_e(&args),\n        \"-f\" => compgen_f(core, &args, false),\n        \"-h\" => compgen_h(core, &args), //history (sush original)\n        \"-j\" => compgen_j(core, &args),\n        \"-o\" => compgen_o(core, &args),\n        \"-u\" => compgen_u(core, &args),\n        \"-v\" => compgen_v(core, &args),\n        \"-A function\" => compgen_function(core, &args),\n        \"-A hostname\" => compgen_hostname(core, &args),\n        \"-A shopt\" => compgen_shopt(core, &args),\n        \"-A stopped\" => compgen_stopped(core, &args),\n        \"-W\" => {\n            if args.len() < 2 {\n                eprintln!(\"sush: compgen: -W: option requires an argument\");\n                return 2;\n            }\n            compgen_large_w(core, &args)\n        }\n        \"-G\" => {\n            if args.len() < 2 {\n                eprintln!(\"sush: compgen: -G: option requires an argument\");\n                return 2;\n            }\n            compgen_large_g(core, &args)\n        }\n        _ => {\n            eprintln!(\"sush: compgen: {}: invalid option\", &args[1]);\n            return 2;\n        }\n    };\n\n    if let Some(pattern) = exclude {\n        let extglob = core.shopts.query(\"extglob\");\n        ans.retain(|a| !glob::parse_and_compare(a, &pattern, extglob));\n    }\n\n    if let Some(p) = prefix {\n        for a in ans.iter_mut() {\n            *a = p.clone() + a;\n        }\n    }\n    if let Some(s) = suffix {\n        for a in ans.iter_mut() {\n            *a = a.to_owned() + &s.clone();\n        }\n    }\n\n    ans.iter().for_each(|a| println!(\"{}\", &a));\n    match ans.is_empty() {\n        true => 1,\n        false => 0,\n    }\n}\n\nfn get_head(args: &[String], pos: usize) -> String {\n    if args.len() > pos && args[pos] != \"--\" {\n        args[pos].clone()\n    } else if args.len() > pos + 1 {\n        args[pos + 1].clone()\n    } else {\n        \"\".to_string()\n    }\n}\n\nfn drop_unmatch(args: &[String], pos: usize, list: &mut Vec<String>) {\n    let head = get_head(args, pos);\n    if !head.is_empty() {\n        list.retain(|s| s.starts_with(&head));\n    }\n}\n\npub fn compgen_a(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut commands = vec![];\n\n    let mut aliases: Vec<String> = core.db.get_indexes_all(\"BASH_ALIASES\").to_vec();\n    commands.append(&mut aliases);\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        commands.retain(|a: &String| a.starts_with(&head));\n    }\n    commands\n}\n\npub fn compgen_b(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut commands = vec![];\n    let mut builtins: Vec<String> = core.builtins.keys().cloned().collect();\n    commands.append(&mut builtins);\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        commands.retain(|a| a.starts_with(&head));\n    }\n    commands\n}\n\npub fn compgen_c(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut commands = vec![];\n    if args.len() > 2 {\n        commands.extend(compgen_f(core, args, false));\n    }\n    commands.retain(|p| Path::new(p).executable() || file_check::is_dir(p));\n\n    let mut aliases: Vec<String> = core.db.get_indexes_all(\"BASH_ALIASES\").to_vec();\n    commands.append(&mut aliases);\n    let mut builtins: Vec<String> = core.builtins.keys().cloned().collect();\n    commands.append(&mut builtins);\n    let mut functions: Vec<String> = core.db.functions.keys().cloned().collect();\n    commands.append(&mut functions);\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        commands.retain(|a| a.starts_with(&head));\n    }\n    let mut command_in_paths = command_list(&head, core);\n    commands.append(&mut command_in_paths);\n    commands\n}\n\nfn compgen_d(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    compgen_f(core, args, true)\n}\n\npub fn compgen_e(args: &[String]) -> Vec<String> {\n    let mut envs = env::vars().map(|e| e.0).collect::<Vec<String>>();\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        envs.retain(|a| a.starts_with(&head));\n    }\n\n    envs\n}\n\npub fn compgen_h(core: &mut ShellCore, _: &[String]) -> Vec<String> {\n    let len = core.history.len();\n    if len >= 10 {\n        return core.history[0..10].to_vec();\n    }\n\n    let mut ans = core.history.to_vec();\n\n    if let Ok(hist_file) = File::open(core.db.get_param(\"HISTFILE\").unwrap_or_default()) {\n        for h in RevLines::new(BufReader::new(hist_file)) {\n            if let Ok(s) = h {\n                ans.push(s)\n            }\n\n            if ans.len() >= 10 {\n                return ans;\n            }\n        }\n    }\n\n    while ans.len() < 10 {\n        ans.push(\"echo Hello World\".to_string());\n    }\n    ans\n}\n\npub fn compgen_v(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut commands = vec![];\n\n    let mut aliases: Vec<String> = core.db.get_indexes_all(\"BASH_ALIASES\").to_vec();\n    commands.append(&mut aliases);\n    let mut functions: Vec<String> = core.db.functions.keys().cloned().collect();\n    commands.append(&mut functions);\n    let mut vars: Vec<String> = core.db.get_param_keys();\n    commands.append(&mut vars);\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        commands.retain(|a| a.starts_with(&head));\n    }\n    commands\n}\n\npub fn compgen_o(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut commands = vec![];\n\n    let mut options: Vec<String> = core.options.get_keys();\n    commands.append(&mut options);\n\n    let head = get_head(args, 2);\n    if !head.is_empty() {\n        commands.retain(|a| a.starts_with(&head));\n    }\n    commands\n}\n\nfn compgen_large_g(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let glob = args[2].to_string();\n    path_expansion::expand(&glob, &core.shopts)\n}\n\nfn compgen_large_w(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans: Vec<String> = vec![];\n    let mut words = args[2].to_string();\n\n    if words.starts_with(\"$\") {\n        if let Ok(value) = core.db.get_param(&args[2][1..]) {\n            words = value;\n        }\n    }\n\n    let mut feeder = Feeder::new(&words);\n    while !feeder.is_empty() {\n        match Word::parse(&mut feeder, core, None) {\n            Ok(Some(mut w)) => {\n                if let Ok(mut v) = w.eval(core) {\n                    ans.append(&mut v);\n                }\n            }\n            _ => {\n                let len = feeder.scanner_multiline_blank(core);\n                feeder.consume(len);\n            }\n        }\n    }\n\n    drop_unmatch(args, 3, &mut ans);\n    ans\n}\n\npub fn compgen_u(_: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans = vec![];\n\n    if let Ok(f) = File::open(\"/etc/passwd\") {\n        for line in BufReader::new(f).lines() {\n            match line {\n                Ok(line) => {\n                    let splits: Vec<&str> = line.split(':').collect();\n                    ans.push(splits[0].to_string());\n                }\n                _ => return vec![],\n            }\n        }\n    }\n\n    drop_unmatch(args, 2, &mut ans);\n    ans\n}\n\npub fn compgen_shopt(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans = core.shopts.get_keys();\n    drop_unmatch(args, 2, &mut ans);\n    ans\n}\n\npub fn compgen_function(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans = core.db.functions.keys().cloned().collect();\n    drop_unmatch(args, 2, &mut ans);\n    ans\n}\n\npub fn compgen_hostname(_: &mut ShellCore, _: &[String]) -> Vec<String> {\n    //TODO: Implement!\n    vec![]\n}\n\npub fn compgen_stopped(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans = vec![];\n\n    for job in &core.job_table {\n        if job.display_status == \"Stopped\" {\n            ans.push(job.text.split(' ').next().unwrap().to_string());\n        }\n    }\n\n    drop_unmatch(args, 2, &mut ans);\n    ans\n}\n\npub fn compgen_j(core: &mut ShellCore, args: &[String]) -> Vec<String> {\n    let mut ans = vec![];\n\n    for job in &core.job_table {\n        ans.push(job.text.split(' ').next().unwrap().to_string());\n    }\n\n    drop_unmatch(args, 2, &mut ans);\n    ans\n}\n"
  },
  {
    "path": "src/core/builtins/complete.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::{CompletionEntry, HashMap};\nuse crate::utils::arg;\nuse crate::{builtins, ShellCore};\n\nfn action_to_reduce_symbol(arg: &str) -> String {\n    match arg {\n        \"file\" => \"f\",\n        \"directory\" => \"d\",\n        \"command\" => \"c\",\n        \"alias\" => \"a\",\n        \"builtin\" => \"b\",\n        \"export\" => \"e\",\n        \"group\" => \"g\",\n        \"keyword\" => \"k\",\n        \"variable\" => \"v\",\n        \"setopt\" => \"o\",\n        \"job\" => \"j\",\n        \"service\" => \"s\",\n        \"user\" => \"u\",\n        _ => \"\",\n    }\n    .to_string()\n}\n\nfn opt_to_action(arg: &str) -> String {\n    match arg {\n        \"-a\" => \"alias\",\n        \"-b\" => \"builtin\",\n        \"-c\" => \"command\",\n        \"-d\" => \"directory\",\n        \"-e\" => \"export\",\n        \"-f\" => \"file\",\n        \"-g\" => \"group\",\n        \"-k\" => \"keyword\",\n        \"-j\" => \"job\",\n        \"-o\" => \"setopt\",\n        \"-u\" => \"user\",\n        \"-v\" => \"variable\",\n        _ => \"\",\n    }\n    .to_string()\n}\n\nfn print_complete(core: &mut ShellCore) -> i32 {\n    if !core.completion.default_function.is_empty() {\n        println!(\"complete -F {} -D\", &core.completion.default_function);\n    }\n\n    for (name, info) in &core.completion.entries {\n        if !info.large_w_cands.is_empty() {\n            print!(\"complete -W {} \", &info.large_w_cands);\n        } else if !info.function.is_empty() {\n            print!(\"complete -F {} \", &info.function);\n        } else if !info.action.is_empty() {\n            let symbol = action_to_reduce_symbol(&info.action);\n\n            if symbol.is_empty() {\n                print!(\"complete -A {} \", &info.action);\n            } else {\n                print!(\"complete -{} \", &symbol);\n            }\n\n            if info.options.contains_key(\"-P\") {\n                print!(\"-P '{}' \", &info.options[\"-P\"]);\n            }\n            if info.options.contains_key(\"-S\") {\n                print!(\"-S '{}' \", &info.options[\"-S\"]);\n            }\n        } else {\n            print!(\"complete \");\n        }\n        println!(\"{}\", &name);\n    }\n    0\n}\n\nfn complete_f(core: &mut ShellCore, args: &[String], o_options: &[String]) -> i32 {\n    let d_option = arg::has_option(\"-D\", args);\n    let mut arg_index = 1;\n    if d_option {\n        arg_index = 2;\n    }\n\n    if args.len() <= arg_index {\n        return builtins::error_(2, &args[0], \"-F: option requires an argument\", core);\n    }\n\n    if d_option {\n        core.completion.default_function = args[arg_index].clone();\n        0\n    } else {\n        let func = args[arg_index].clone();\n        for command in &args[arg_index + 1..] {\n            if !core.completion.entries.contains_key(command) {\n                core.completion\n                    .entries\n                    .insert(command.clone(), CompletionEntry::default());\n            }\n\n            let info = &mut core.completion.entries.get_mut(command).unwrap();\n            info.function = func.clone();\n            info.o_options = o_options.to_owned();\n        }\n\n        0\n    }\n}\n\nfn complete_large_w(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_vec();\n    let com = args.pop().unwrap();\n\n    core.completion\n        .entries\n        .insert(com.clone(), CompletionEntry::default());\n\n    let info = &mut core.completion.entries.get_mut(&com).unwrap();\n    info.large_w_cands = args[2].clone();\n    0\n}\n\nfn complete_r(core: &mut ShellCore, args: &[String]) -> i32 {\n    for command in &args[1..] {\n        core.completion.entries.remove(command);\n    }\n\n    0\n}\n\npub fn complete(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if args.len() <= 1 || args[1] == \"-p\" {\n        return print_complete(core);\n    }\n\n    let mut o_options = vec![];\n    let mut args = arg::dissolve_options(&args);\n\n    if args[1] == \"-W\" { return complete_large_w(core, &args);\n    }\n\n    if arg::consume_arg(\"-r\", &mut args) {\n        return complete_r(core, &args);\n    }\n\n    while let Some(v) = arg::consume_with_next_arg(\"-o\", &mut args) {\n        o_options.push(v);\n    }\n\n    let mut options = HashMap::new();\n    let prefix = arg::consume_with_next_arg(\"-P\", &mut args);\n    if let Some(prefix) = prefix {\n        options.insert(\"-P\".to_string(), prefix.clone());\n    }\n    let suffix = arg::consume_with_next_arg(\"-S\", &mut args);\n    if let Some(suffix) = suffix {\n        options.insert(\"-S\".to_string(), suffix.clone());\n    }\n\n    let action = opt_to_action(&args[1]);\n    if !action.is_empty() {\n        for command in &args[2..] {\n            if !core.completion.entries.contains_key(command) {\n                core.completion\n                    .entries\n                    .insert(command.clone(), CompletionEntry::default());\n            }\n\n            let info = &mut core.completion.entries.get_mut(command).unwrap();\n            info.action = action.clone();\n            info.options = options.clone();\n        }\n        return 0;\n    }\n\n    if args.len() > 3 && args[1] == \"-A\" {\n        for command in &args[3..] {\n            if !core.completion.entries.contains_key(command) {\n                core.completion\n                    .entries\n                    .insert(command.clone(), CompletionEntry::default());\n            }\n\n            let info = &mut core.completion.entries.get_mut(command).unwrap();\n            info.action = args[2].clone();\n            info.options = options.clone();\n        }\n\n        return 0;\n    }\n\n    if arg::consume_arg(\"-F\", &mut args) {\n        complete_f(core, &args, &o_options)\n    } else {\n        let msg = format!(\"{}: still unsupported\", &args[1]);\n        builtins::error_(1, &args[0], &msg, core)\n    }\n}\n"
  },
  {
    "path": "src/core/builtins/compopt.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::CompletionEntry;\nuse crate::utils::arg;\nuse crate::ShellCore;\n\nfn compopt_set(info: &mut CompletionEntry, plus: &[String], minus: &[String]) -> i32 {\n    for opt in minus {\n        //add\n        if !info.o_options.contains(opt) {\n            info.o_options.push(opt.to_string());\n        }\n    }\n\n    for opt in plus {\n        //remove\n        info.o_options.retain(|e| e != opt);\n    }\n\n    0\n}\n\nfn compopt_print(core: &mut ShellCore, args: &[String]) -> i32 {\n    let optlist = [\n        \"bashdefault\",\n        \"default\",\n        \"dirnames\",\n        \"filenames\",\n        \"noquote\",\n        \"nosort\",\n        \"nospace\",\n        \"plusdirs\",\n    ];\n    let optlist: Vec<String> = optlist.iter().map(|s| s.to_string()).collect();\n\n    let com = args[1].clone();\n    if core.completion.entries.contains_key(&com) {\n        let info = &core.completion.entries.get_mut(&com).unwrap();\n\n        print!(\"compopt \");\n        for opt in &optlist {\n            match info.o_options.contains(opt) {\n                true => print!(\"-o {opt} \"),\n                false => print!(\"+o {opt} \"),\n            }\n        }\n        println!(\"{}\", &com);\n    } else {\n        eprintln!(\"sush: compopt: {}: no completion specification\", &args[1]);\n        return 1;\n    }\n\n    0\n}\n\npub fn compopt(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    if args.len() < 2 {\n        dbg!(\"{:?}\", &core.completion.entries);\n        return 1;\n    }\n\n    if !args[1].starts_with(\"-\") && !args[1].starts_with(\"+\") {\n        return compopt_print(core, &args);\n    }\n\n    let mut flag = \"\".to_string();\n    let mut minus = vec![];\n    let mut plus = vec![];\n    let mut minus_d = vec![];\n    let mut plus_d = vec![];\n    let mut minus_e = vec![];\n    let mut plus_e = vec![];\n\n    while args.len() > 1 {\n        if args[1] == \"-D\" || args[1] == \"-E\" {\n            flag = args[1].clone();\n            args.remove(1);\n            continue;\n        }\n\n        if args[1] == \"-o\" {\n            let opt = arg::consume_with_next_arg(\"-o\", &mut args);\n            if opt.is_none() {\n                return 1;\n            }\n\n            match flag.as_str() {\n                \"\" => minus.push(opt.unwrap()),\n                \"-D\" => minus_d.push(opt.unwrap()),\n                \"-E\" => minus_e.push(opt.unwrap()),\n                _ => return 1,\n            }\n            continue;\n        }\n\n        if args[1] == \"+o\" {\n            let opt = arg::consume_with_next_arg(\"+o\", &mut args);\n            if opt.is_none() {\n                return 1;\n            }\n\n            match flag.as_str() {\n                \"\" => plus.push(opt.unwrap()),\n                \"-D\" => plus_d.push(opt.unwrap()),\n                \"-E\" => plus_e.push(opt.unwrap()),\n                _ => return 1,\n            }\n            continue;\n        }\n\n        break;\n    }\n\n    let info = if args.len() == 1 {\n        &mut core.completion.current\n    } else if args.len() == 2 {\n        match core.completion.entries.get_mut(&args[1]) {\n            Some(i) => i,\n            None => return 1,\n        }\n    } else {\n        return 1;\n    };\n    compopt_set(info, &plus, &minus)\n\n    //TODO: support of -D -E\n}\n"
  },
  {
    "path": "src/core/builtins/echo.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::ansi_c_str::AnsiCString;\nuse crate::error::exec::ExecError;\nuse crate::utils::c_string;\nuse crate::{Feeder, ShellCore};\nuse std::ffi::CString;\nuse std::io;\nuse std::io::{stdout, Write};\n\nfn arg_to_c_str(arg: &str, core: &mut ShellCore) -> Result<CString, ExecError> {\n    let mut f = Feeder::new(arg);\n    let ans = match AnsiCString::parse(&mut f, core, true) {\n        Ok(Some(mut ansi_c_str)) => c_string::to_carg(&ansi_c_str.eval()),\n        Ok(None) => c_string::to_carg(arg),\n        Err(e) => return Err(ExecError::ParseError(e)),\n    };\n\n    Ok(ans)\n}\n\npub fn echo(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    let mut first = true;\n    let mut e_opt = false;\n    let mut n_opt = false;\n\n    if args.len() == 1 {\n        println!();\n        return 0;\n    }\n\n    match args[1].as_ref() {\n        \"-ne\" | \"-en\" => {\n            e_opt = true;\n            n_opt = true;\n            args.remove(1);\n        }\n        \"-e\" => {\n            e_opt = true;\n            args.remove(1);\n        }\n        \"-n\" => {\n            n_opt = true;\n            args.remove(1);\n        }\n        _ => {}\n    }\n\n    for a in &args[1..] {\n        if !first {\n            let _ = io::stdout().write(b\" \");\n        }\n        first = false;\n\n        let bytes = match e_opt {\n            false => c_string::to_carg(a).into_bytes(),\n            true => match arg_to_c_str(a, core) {\n                Ok(v) => v.into_bytes(),\n                Err(e) => {\n                    e.print(core);\n                    return 1;\n                }\n            },\n        };\n\n        io::stdout().write_all(&bytes).unwrap();\n    }\n\n    if !n_opt {\n        let _ = io::stdout().write(b\"\\n\");\n    }\n\n    stdout().flush().unwrap();\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/exec.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{proc_ctrl, ShellCore};\nuse nix::errno::Errno;\nuse nix::unistd;\nuse std::ffi::CString;\n\npub fn exec(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.db.flags.contains('r') {\n        return super::error_(1, &args[0], \"restricted\", core);\n    }\n\n    if args.len() == 1 {\n        return 0;\n    }\n\n    if core.db.flags.contains('i') || core.shopts.query(\"execfail\") {\n        exec_command(&args[1..], core, \"\")\n    } else {\n        proc_ctrl::exec_command(&args[1..], core, \"\")\n    }\n}\n\nfn exec_command(args: &[String], core: &mut ShellCore, fullpath: &str) -> i32 {\n    let cargs: Vec<CString> = args\n        .iter()\n        .map(|a| CString::new(a.to_string()).unwrap())\n        .collect();\n    let cfullpath = CString::new(fullpath).unwrap();\n\n    if !fullpath.is_empty() {\n        let _ = unistd::execv(&cfullpath, &cargs);\n    }\n    let result = unistd::execvp(&cargs[0], &cargs);\n\n    match result {\n        Err(Errno::E2BIG) => super::error_(126, &args[0], \"Arg list too long\", core),\n        Err(Errno::EACCES) => {\n            super::error_(126, &args[0], \"cannot execute: Permission denied\", core)\n        }\n        Err(Errno::ENOENT) => super::error_(127, &args[0], \"No such file or directory\", core),\n        Err(e) => {\n            let msg = format!(\"{:?}\", &e);\n            super::error_(127, &args[0], &msg, core)\n        }\n        _ => 127,\n    }\n}\n"
  },
  {
    "path": "src/core/builtins/getopts.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{arg, error, ShellCore};\n\n#[derive(Debug)]\nenum Opt {\n    Single(String),\n    WithArg(String),\n}\n\nstruct NoArgOpt<'a>{\n    name: &'a str,\n    arg: &'a str,\n    index: usize,\n    subindex: usize,\n    subarg: bool,\n    exp_args_len: usize,\n    silence: bool,\n    scope: Option<usize>,\n}\n\nimpl Opt {\n    fn is_single(&self, opt: &str) -> bool {\n        match self {\n            Self::Single(s) => s == opt,\n            _ => false,\n        }\n    }\n\n    fn is_witharg(&self, opt: &str) -> bool {\n        match self {\n            Self::WithArg(s) => s == opt,\n            _ => false,\n        }\n    }\n}\n\nfn parse(optstring: &str) -> (Vec<Opt>, bool) {\n    let mut optstring = optstring.to_string();\n    let mut ans = vec![];\n    let mut silence = false;\n\n    if optstring.starts_with(\":\") {\n        optstring.remove(0);\n        silence = true;\n    }\n\n    for c in optstring.chars() {\n        if c == ':' {\n            match ans.pop() {\n                Some(Opt::Single(opt)) => ans.push(Opt::WithArg(opt)),\n                _ => return (vec![], silence),\n            }\n        } else {\n            let opt = format!(\"-{c}\");\n            ans.push(Opt::Single(opt));\n        }\n    }\n\n    (ans, silence)\n}\n\npub fn get_index(core: &mut ShellCore) -> (usize, usize) {\n    //index, subindex\n    let mut index = 1;\n    let mut subindex = 0;\n\n    if let Ok(s) = core.db.get_param(\"OPTIND\") {\n        if let Ok(n) = s.parse::<usize>() {\n            index = n;\n        }\n\n        if let Ok(p) = core.db.get_param(\"OPTIND_PREV\") {\n            if let Ok(prev) = p.parse::<usize>() {\n                if index != prev {\n                    let _ = core.db.set_param(\"OPTIND_SUB\", \"0\", None);\n                }\n            }\n        }\n    }\n\n    if let Ok(s) = core.db.get_param(\"OPTIND_SUB\") {\n        if let Ok(n) = s.parse::<usize>() {\n            subindex = n;\n        }\n    }\n\n    (index, subindex)\n}\n\nfn set_no_arg_option(no_arg_opt: &NoArgOpt, core: &mut ShellCore,) -> i32 {\n    let result = core.db.set_param(no_arg_opt.name, &no_arg_opt.arg[1..], no_arg_opt.scope);\n    core.db.set_param(\"OPTARG\", \"\", no_arg_opt.scope).ok();\n\n    if !no_arg_opt.subarg || no_arg_opt.subindex + 1 == no_arg_opt.exp_args_len {\n        let _ = core.db.set_param(\"OPTIND\", &(no_arg_opt.index + 1).to_string(), no_arg_opt.scope);\n        let _ = core.db.set_param(\"OPTIND_SUB\", \"0\", no_arg_opt.scope);\n        let _ = core.db.set_param(\"OPTIND_PREV\", &(no_arg_opt.index + 1).to_string(), no_arg_opt.scope);\n    } else {\n        let _ = core.db.set_param(\"OPTIND\", &no_arg_opt.index.to_string(), no_arg_opt.scope);\n        let _ = core.db.set_param(\"OPTIND_SUB\", &(no_arg_opt.subindex + 1).to_string(), no_arg_opt.scope);\n        let _ = core.db.set_param(\"OPTIND_PREV\", &no_arg_opt.index.to_string(), no_arg_opt.scope);\n    }\n\n    if let Err(e) = result {\n        if !no_arg_opt.silence {\n            let msg = format!(\"getopts: {:?}\", &e);\n            error::print(&msg, core);\n        }\n        return 1;\n    }\n    0\n}\n\nfn set_option_with_arg(\n    name: &str,\n    arg: &str,\n    index: usize,\n    optarg: &str,\n    silence: bool,\n    core: &mut ShellCore,\n    scope: Option<usize>,\n) -> i32 {\n    let result = core.db.set_param(name, &arg[1..], scope);\n\n    let _ = core.db.set_param(\"OPTARG\", optarg, scope);\n    let _ = core.db.set_param(\"OPTIND\", &(index + 2).to_string(), scope);\n    let _ = core\n        .db\n        .set_param(\"OPTIND_PREV\", &(index + 2).to_string(), scope);\n    let _ = core.db.set_param(\"OPTIND_SUB\", \"0\", scope);\n\n    if let Err(e) = result {\n        let _ = core\n            .db\n            .set_param(\"OPTIND_PREV\", &(index + 10).to_string(), scope);\n        if !silence {\n            let msg = format!(\"getopts: {:?}\", &e);\n            error::print(&msg, core);\n        }\n        return 1;\n    }\n\n    0\n}\n\npub fn getopts(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    let scope = core.db.get_scope_pos(\"OPTIND\").unwrap_or(0);\n    let scope_sub = core.db.get_scope_pos(\"OPTIND_SUB\").unwrap_or(0);\n    let scope_prev = core.db.get_scope_pos(\"OPTIND_PREV\").unwrap_or(0);\n\n    if scope_sub != scope {\n        let _ = core.db.set_param(\"OPTIND_SUB\", \"0\", Some(scope));\n    }\n    if scope_prev != scope {\n        let _ = core.db.set_param(\"OPTIND_PREV\", \"0\", Some(scope));\n    }\n\n    let _ = core.db.set_param(\"OPTARG\", \"\", Some(scope));\n\n    if args.len() < 3 {\n        error::print(\"getopts: usage: getopts optstring name [arg ...]\", core);\n        return 2;\n    }\n\n    let (targets, silence) = parse(&args[1]);\n    if targets.is_empty() {\n        return 1;\n    }\n\n    let (index, subindex) = get_index(core);\n    let name = args[2].clone();\n    let _ = core.db.set_param(&name, \"?\", Some(scope));\n\n    let args = match args.len() > 3 {\n        true => &args[2..],\n        false => &core.db.position_parameters.last().unwrap().clone(),\n    };\n\n    if args.len() <= index {\n        return 1;\n    }\n\n    let mut arg = args[index].clone();\n    let exp_args = arg::dissolve_options(&[arg.clone()]);\n    let subarg = exp_args.len() > 1;\n    if subarg {\n        if exp_args.len() <= subindex {\n            return 1;\n        }\n        arg = exp_args[subindex].clone();\n    }\n\n    if !arg.starts_with(\"-\") {\n        return 1;\n    }\n\n    if arg.starts_with(\"--\") {\n        let _ = core\n            .db\n            .set_param(\"OPTIND\", &(index + 1).to_string(), Some(scope));\n        let _ = core\n            .db\n            .set_param(\"OPTIND_PREV\", &(index + 1).to_string(), Some(scope));\n        return 1;\n    }\n\n    if targets.iter().any(|t| t.is_single(&arg)) {\n\n        let no_arg_opt = NoArgOpt {\n            name: &name,\n            arg: &arg,\n            index,\n            subindex,\n            subarg,\n            exp_args_len: exp_args.len(),\n            silence,\n            scope: Some(scope),\n        };\n        return set_no_arg_option(&no_arg_opt, core);\n    }\n\n    if targets.iter().any(|t| t.is_witharg(&arg)) {\n        let optarg = match args.len() > index + 1 {\n            true => args[index + 1].clone(),\n            false => return 1,\n        };\n\n        return set_option_with_arg(&name, &arg, index, &optarg, silence, core, Some(scope));\n    }\n\n    1\n}\n"
  },
  {
    "path": "src/core/builtins/hash.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::utils::arg;\nuse crate::ShellCore;\n\nfn print_all(core: &mut ShellCore) -> i32 {\n    println!(\"hits\tcommand\");\n\n    for com in core.db.get_indexes_all(\"BASH_CMDS\") {\n        if let Ok(path) = core.db.get_elem(\"BASH_CMDS\", &com) {\n            if let Some(n) = core.db.hash_counter.get(&com) {\n                println!(\"{:4}\\t{}\", &n, &path);\n            } else {\n                core.db.hash_counter.insert(com, 0);\n                println!(\"   0\\t{}\", &path);\n            }\n        }\n    }\n\n    0\n}\n\npub fn hash(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    let mut args = arg::dissolve_options(&args);\n\n    if args.len() == 1 {\n        return print_all(core);\n    }\n\n    if arg::consume_arg(\"-p\", &mut args) {\n        if args.len() == 1 {\n            return super::error_(1, \"hash\", \"-p: option requires an argument\", core);\n        }\n        if args.len() == 2 {\n            return super::error_(1, \"hash\", \"still not implemented\", core);\n        }\n\n        if let Err(e) = core\n            .db\n            .set_assoc_elem(\"BASH_CMDS\", &args[2], &args[1], Some(0))\n        {\n            let msg = String::from(&e);\n            return super::error_(1, \"hash\", &msg, core);\n        }\n    }\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/history.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::utils::arg;\nuse crate::ShellCore;\nuse std::fs::File;\nuse std::io::{BufRead, BufReader};\n\npub fn history_c(core: &mut ShellCore) -> i32 {\n    core.rewritten_history.clear();\n    core.history.clear();\n    0\n}\n\npub fn history(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    let mut args = arg::dissolve_options(&args);\n    if arg::consume_arg(\"-c\", &mut args) {\n        return history_c(core);\n    }\n\n    if args.len() > 1 {\n        let msg = format!(\"{}: invalid option\", &args[1]);\n        return super::error_(1, \"history\", &msg, core);\n    }\n\n    let mut number = 1;\n\n    let filename = core.db.get_param(\"HISTFILE\").unwrap_or_default();\n    if filename.is_empty() {\n        return 0;\n    }\n\n    let file = match File::open(&filename) {\n        Ok(f) => f,\n        _ => return 0,\n    };\n\n    let f = BufReader::new(file);\n    for line in f.lines() {\n        println!(\"{:5} {}\", number, &line.unwrap());\n        number += 1;\n    }\n\n    for h in core.history.iter().rev() {\n        println!(\"{:5} {}\", number, &h);\n        number += 1;\n    }\n\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/job_commands.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse libc;\nuse crate::core::JobEntry;\nuse crate::utils::arg;\nuse crate::ShellCore;\nuse crate::{signal, utils};\nuse nix::sys::signal::Signal;\nuse nix::unistd;\nuse nix::unistd::Pid;\nuse std::{thread, time};\n//use std::sync::atomic::Ordering::Relaxed;\n\nfn pid_to_array_pos(pid: i32, jobs: &[JobEntry]) -> Option<usize> {\n    (0..jobs.len()).find(|&i| jobs[i].pids[0].as_raw() == pid)\n}\n\nfn jobid_to_pos(id: usize, jobs: &mut [JobEntry]) -> Option<usize> {\n    for (i, job) in jobs.iter_mut().enumerate() {\n        if job.id == id {\n            return Some(i);\n        }\n    }\n    None\n}\n\npub fn bg(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.job_table.is_empty() {\n        return 1;\n    }\n\n    let mut args = arg::dissolve_options(&args);\n    if !core.db.flags.contains('m') {\n        return super::error_(1, &args[0], \"no job control\", core);\n    }\n\n    if arg::consume_arg(\"-s\", &mut args) {\n        return super::error_(1, &args[0], \"-s: invalid option\", core);\n    }\n\n    let pos = match args.len() {\n        1 => {\n            let id = core.job_table_priority[0];\n            jobid_to_pos(id, &mut core.job_table)\n        }\n        2 => jobspec_to_array_pos(core, &args[0], &args[1]),\n        _ => None,\n    };\n\n    match pos {\n        Some(p) => {\n            let id = core.job_table[p].id;\n\n            if core.job_table[p].no_control {\n                let msg = format!(\"job {} started without job control\", &id);\n                return super::error_(1, &args[0], &msg, core);\n            }\n\n            if core.job_table[p].display_status == \"Running\" {\n                let msg = format!(\"job {} already in background\", &id);\n                return super::error_(0, &args[0], &msg, core);\n            }\n\n            let priority = get_priority(core, p);\n            let symbol = match priority {\n                0 => \"+\",\n                1 => \"-\",\n                _ => \" \",\n            };\n            println!(\"[{}]{} {} &\", &id, &symbol, &core.job_table[p].text);\n            core.job_table[p].send_cont()\n        }\n        None => return 1,\n    }\n    0\n}\n\npub fn fg(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    let mut args = arg::dissolve_options(&args);\n    if !core.db.flags.contains('m') {\n        return super::error_(1, &args[0], \"no job control\", core);\n    }\n\n    if arg::consume_arg(\"-s\", &mut args) {\n        return super::error_(1, &args[0], \"-s: invalid option\", core);\n    }\n\n    let id = if args.len() == 1 {\n        if core.job_table_priority.is_empty() {\n            return 1;\n        }\n        core.job_table_priority[0]\n    } else if args.len() == 2 {\n        match jobspec_to_array_pos(core, &args[0], &args[1]) {\n            Some(pos) => core.job_table[pos].id,\n            None => return 1,\n        }\n    } else {\n        return 1;\n    };\n\n    let pos = match jobid_to_pos(id, &mut core.job_table) {\n        Some(i) => i,\n        _ => return 1,\n    };\n\n    if core.job_table[pos].no_control {\n        let id = core.job_table[pos].id;\n        let msg = format!(\"job {} started without job control\", &id);\n        return super::error_(1, &args[0], &msg, core);\n    }\n\n    let pgid = core.job_table[pos].solve_pgid();\n    if pgid.as_raw() == 0 {\n        return 1;\n    }\n\n    signal::ignore(Signal::SIGTTOU);\n\n    let mut exit_status = 1;\n    if let Some(fd) = core.tty_fd.as_ref() {\n        //if unistd::tcsetpgrp(fd, pgid).is_ok() {\n        if core.fds.tcsetpgrp(*fd, pgid).is_ok() {\n            println!(\"{}\", &core.job_table[pos].text);\n            core.job_table[pos].send_cont();\n            exit_status = core.job_table[pos].update_status(true, false).unwrap_or(1);\n\n            if let Ok(mypgid) = unistd::getpgid(Some(Pid::from_raw(0))) {\n                let _ = core.fds.tcsetpgrp(*fd, mypgid);\n            }\n        }\n    } else {\n        println!(\"{}\", &core.job_table[pos].text);\n        core.job_table[pos].send_cont();\n        exit_status = core.job_table[pos].update_status(true, false).unwrap_or(1);\n    }\n\n    remove(core, pos);\n    signal::restore(Signal::SIGTTOU);\n    exit_status\n}\n\nfn jobspec_to_array_pos(core: &mut ShellCore, com: &str, jobspec: &str) -> Option<usize> {\n    let poss = jobspec_to_array_poss(core, jobspec);\n    if poss.is_empty() {\n        let msg = format!(\"{}: no such job\", &jobspec);\n        super::error_(127, com, &msg, core);\n        return None;\n    } else if poss.len() > 1 {\n        let msg = format!(\n            \"{}: ambiguous job spec\",\n            jobspec.strip_prefix('%').unwrap_or(jobspec)\n        );\n        super::error_(127, com, &msg, core);\n        return None;\n    }\n\n    Some(poss[0])\n}\n\nfn jobspec_to_array_poss(core: &mut ShellCore, jobspec: &str) -> Vec<usize> {\n    if jobspec.is_empty() {\n        return (0..core.job_table.len()).collect();\n    }\n\n    if core.job_table.is_empty() {\n        return vec![];\n    }\n\n    let s = &jobspec[1..];\n    let mut ans = vec![];\n\n    if let Ok(n) = s.parse::<usize>() {\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if n == job.id {\n                ans.push(i);\n            }\n        }\n    } else if s == \"%\" || s == \"+\" {\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if job.id == core.job_table_priority[0] {\n                ans.push(i);\n            }\n        }\n    } else if s == \"-\" {\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if core.job_table_priority.len() < 2 {\n                if job.id == core.job_table_priority[0] {\n                    ans.push(i);\n                }\n            } else if job.id == core.job_table_priority[1] {\n                ans.push(i);\n            }\n        }\n    } else if let Some(stripped) = s.strip_prefix('?') {\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if job.text.contains(stripped) {\n                ans.push(i);\n            }\n        }\n    } else {\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if job.text.starts_with(s) {\n                ans.push(i);\n            }\n        }\n    }\n\n    ans\n}\n\npub fn jobs(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n    if arg::consume_arg(\"-n\", &mut args) {\n        core.jobtable_print_status_change();\n        return 0;\n    }\n\n    let jobspecs = arg::consume_starts_with(\"%\", &mut args);\n    let jobspec = match jobspecs.last() {\n        Some(s) => s.clone(),\n        None => String::new(),\n    };\n\n    if core.job_table.is_empty() && jobspec.is_empty() {\n        return 0;\n    }\n\n    let poss = jobspec_to_array_poss(core, &jobspec);\n\n    if poss.is_empty() {\n        let msg = format!(\"{}: no such job\", &jobspec);\n        return super::error_(127, \"jobs\", &msg, core);\n    }\n    if poss.len() > 1 && !jobspec.is_empty() {\n        let msg = format!(\n            \"{}: ambiguous job spec\",\n            jobspec.strip_prefix('%').unwrap_or(&jobspec)\n        );\n        super::error_(127, \"jobs\", &msg, core);\n        let msg = format!(\"{}: no such job\", &jobspec);\n        return super::error_(127, \"jobs\", &msg, core);\n    }\n\n    if arg::consume_arg(\"-p\", &mut args) {\n        for id in poss {\n            core.job_table[id].print_p();\n        }\n        return 0;\n    }\n\n    if !jobspec.is_empty() {\n        let l_opt = arg::consume_arg(\"-l\", &mut args);\n        let r_opt = arg::consume_arg(\"-r\", &mut args);\n        let s_opt = arg::consume_arg(\"-s\", &mut args);\n        if core.job_table[poss[0]].print(&core.job_table_priority, l_opt, r_opt, s_opt, true) {\n            remove(core, poss[0]);\n        }\n        return 0;\n    }\n\n    print(core, &args);\n    0\n}\n\nfn get_priority(core: &mut ShellCore, pos: usize) -> usize {\n    let id = core.job_table[pos].id;\n    for i in 0..core.job_table_priority.len() {\n        if core.job_table_priority[i] == id {\n            return i;\n        }\n    }\n\n    core.job_table.len()\n}\n\nfn print(core: &mut ShellCore, args: &[String]) {\n    let l_opt = arg::has_option(\"-l\", args);\n    let r_opt = arg::has_option(\"-r\", args);\n    let s_opt = arg::has_option(\"-s\", args);\n\n    let mut rem = vec![];\n    for id in 0..core.job_table.len() {\n        if core.job_table[id].print(&core.job_table_priority, l_opt, r_opt, s_opt, true) {\n            rem.push(id);\n        }\n    }\n\n    for pos in rem.into_iter().rev() {\n        remove(core, pos);\n    }\n}\n\nfn remove_coproc(core: &mut ShellCore, pos: usize) {\n    if let Some(name) = &core.job_table[pos].coproc_name {\n        let _ = core.db.unset(&name, None, false);\n        let _ = core.db.unset(&(name.to_owned() + \"_PID\"), None, false);\n\n        if let Ok(fd0) = core.db.get_elem(&name, \"0\") {\n            if let Ok(n) = fd0.parse::<i32>() {\n                let _ = unsafe{libc::close(n)};\n            }\n        }\n        if let Ok(fd1) = core.db.get_elem(&name, \"1\") {\n            if let Ok(n) = fd1.parse::<i32>() {\n                let _ = unsafe{libc::close(n)};\n            }\n        }\n\n        let _ = core.db.unset(&(name), None, false);\n    }\n}\n\nfn remove(core: &mut ShellCore, pos: usize) {\n    let job_id = core.job_table[pos].id;\n    remove_coproc(core, pos);\n    core.job_table.remove(pos);\n    core.job_table_priority.retain(|id| *id != job_id);\n}\n\nfn wait_jobspec(\n    core: &mut ShellCore,\n    com: &str,\n    jobspec: &str,\n    var_name: &Option<String>,\n    f_opt: bool,\n) -> (i32, bool) {\n    match jobspec_to_array_pos(core, com, jobspec) {\n        Some(pos) => wait_a_job(core, pos, var_name, f_opt),\n        None => (127, false),\n    }\n}\n\nfn wait_next(\n    core: &mut ShellCore,\n    ids: &[usize],\n    var_name: &Option<String>,\n    f_opt: bool,\n) -> (i32, bool) {\n    if core.job_table_priority.is_empty() {\n        return (127, false);\n    }\n\n    let mut exit_status = 0;\n    let mut drop = 0;\n    let mut end = false;\n    let mut pid = String::new();\n    let mut remove_job = false;\n\n    loop {\n        /*\n        dbg!(\"H\");\n        if core.sigint.load(Relaxed) {\n            dbg!(\"!!!\");\n        }*/\n\n        thread::sleep(time::Duration::from_millis(10)); //0.1秒周期に変更\n        for (i, job) in core.job_table.iter_mut().enumerate() {\n            if !ids.contains(&i) && !ids.is_empty() {\n                continue;\n            }\n\n            if let Ok(es) = job.update_status(false, true) {\n                if job.display_status == \"Done\"\n                    || job.display_status == \"Killed\"\n                    || (job.display_status == \"Stopped\" && !f_opt)\n                {\n                    exit_status = es;\n                    drop = i;\n                    end = true;\n                    remove_job = job.display_status == \"Done\" || job.display_status == \"Killed\";\n                    pid = job.pids[0].to_string();\n                    break;\n                }\n            }\n        }\n\n        if end {\n            break;\n        }\n    }\n\n    if let Some(var) = var_name {\n        let _ = core.db.unset(var, None, false);\n        if let Err(e) = core.db.set_param(var, &pid, None) {\n            e.print(core);\n        }\n    }\n\n    if remove_job {\n        remove(core, drop);\n    }\n    (exit_status, true)\n}\n\nfn wait_pid(core: &mut ShellCore, pid: i32, var_name: &Option<String>, f_opt: bool) -> (i32, bool) {\n    match pid_to_array_pos(pid, &core.job_table) {\n        Some(i) => wait_a_job(core, i, var_name, f_opt),\n        None => (127, false),\n    }\n}\n\nfn wait_a_job(\n    core: &mut ShellCore,\n    pos: usize,\n    var_name: &Option<String>,\n    f_opt: bool,\n) -> (i32, bool) {\n    if core.job_table.len() < pos {\n        return (\n            super::error_(127, \"wait\", \"invalpos jobpos\", core),\n            false,\n        );\n    }\n\n    let pid = core.job_table[pos].pids[0].to_string();\n\n    let ans = match core.job_table[pos].update_status(true, false) {\n        Ok(n) => {\n            if let Some(var) = var_name {\n                let _ = core.db.unset(var, None, false);\n                if let Err(e) = core.db.set_param(var, &pid, None) {\n                    e.print(core);\n                }\n            }\n            (n, true)\n        }\n        Err(e) => {\n            e.print(core);\n            return (1, false);\n        }\n    };\n\n    if f_opt && core.job_table[pos].display_status == \"Stopped\" {\n        wait_a_job(core, pos, var_name, f_opt)\n    } else {\n        remove(core, pos);\n        ans\n    }\n}\n\nfn wait_arg_job(\n    core: &mut ShellCore,\n    com: &str,\n    arg: &str,\n    var_name: &Option<String>,\n    f_opt: bool,\n) -> (i32, bool) {\n    if arg.starts_with(\"%\") {\n        return wait_jobspec(core, com, arg, var_name, f_opt);\n    }\n\n    if let Ok(pid) = arg.parse::<i32>() {\n        return wait_pid(core, pid, var_name, f_opt);\n    }\n\n    (127, false)\n}\n\nfn wait_all(core: &mut ShellCore) -> i32 {\n    let mut exit_status = 0;\n    let mut remove_list = vec![];\n    for pos in 0..core.job_table.len() {\n        match core.job_table[pos].update_status(true, false) {\n            Ok(n) => {\n                if core.job_table[pos].display_status == \"Done\"\n                    || core.job_table[pos].display_status == \"Killed\"\n                {\n                    remove_list.push(pos);\n                }\n                exit_status = n;\n            }\n            Err(e) => {\n                e.print(core);\n                exit_status = 1;\n                break;\n            }\n        }\n    }\n\n    for pos in remove_list.into_iter().rev() {\n        remove(core, pos);\n    }\n\n    exit_status\n}\n\nfn wait_n(\n    core: &mut ShellCore,\n    args: &mut Vec<String>,\n    var_name: &Option<String>,\n    f_opt: bool,\n) -> i32 {\n    let mut jobs = arg::consume_with_subsequents(\"-n\", args);\n    jobs.remove(0);\n    if jobs.is_empty() {\n        return wait_next(core, &[], var_name, f_opt).0;\n    }\n\n    let mut ids = vec![];\n    for j in &jobs {\n        if j.starts_with(\"%\") {\n            ids.append(&mut jobspec_to_array_poss(core, j));\n        } else {\n            for (i, job) in core.job_table.iter_mut().enumerate() {\n                if job.pids[0].to_string() == *j {\n                    ids.push(i);\n                }\n            }\n        }\n    }\n    ids.sort();\n    ids.dedup();\n    let mut ans = -1;\n\n    for _ in 0..ids.len() {\n        let tmp = match ans {\n            -1 => wait_next(core, &ids, var_name, f_opt),\n            _ => wait_next(core, &ids, &None, f_opt),\n        };\n\n        if tmp.1 && ans == -1 {\n            ans = tmp.0;\n        }\n    }\n    ans\n}\n\npub fn wait(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.is_subshell {\n        super::error_(127, &args[0], \"called from subshell\", core);\n    }\n\n    if args.len() <= 1 {\n        return wait_all(core);\n    }\n\n    let mut args = arg::dissolve_options(&args);\n    let var_name = arg::consume_with_next_arg(\"-p\", &mut args);\n    let f_opt = arg::consume_arg(\"-f\", &mut args);\n\n    if args.len() > 1 && args[1] == \"-n\" {\n        return wait_n(core, &mut args, &var_name, f_opt);\n    }\n\n    if args.len() > 1 {\n        return wait_arg_job(core, &args[0], &args[1], &var_name, f_opt).0;\n    }\n    1\n}\n\n/* TODO: implement original kill */\npub fn kill(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    //let mut args = arg::dissolve_options(args);\n    let path = utils::get_command_path(&args[0], core);\n\n    match path.is_empty() {\n        true => return 1,\n        false => args[0] = path,\n    }\n\n    if args.len() >= 3 && args[2].starts_with(\"%\") {\n        match jobspec_to_array_pos(core, &args[0], &args[2]) {\n            Some(id) => args[2] = core.job_table[id].pids[0].to_string(),\n            None => return 1,\n        }\n    }\n\n    let com = args[0].to_string();\n    for arg in args.iter_mut() {\n        if arg == \"-n\" {\n            *arg = \"-s\".to_string();\n        }\n        if arg.starts_with(\"%\") {\n            if let Some(pos) = jobspec_to_array_pos(core, &com, arg) {\n                *arg = core.job_table[pos].pids[0].to_string();\n            } else {\n                let msg = format!(\"{}: no such job\", &arg);\n                return super::error_(127, \"jobs\", &msg, core);\n            }\n        }\n    }\n\n    /*\n    args.insert(0, \"eval\".to_string());\n    super::eval(core, &args)\n    */\n    super::run_external(core, &args, |es| es > 0)\n}\n\npub fn disown(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n    let h_opt = arg::consume_arg(\"-h\", &mut args);\n    let _r_opt = arg::consume_arg(\"-r\", &mut args); //TODO: implement\n\n    if args.len() == 1 {\n        let ids = jobspec_to_array_poss(core, \"%%\");\n\n        if ids.len() == 1 {\n            remove_coproc(core, ids[0]);\n            core.job_table.remove(ids[0]);\n            core.job_table_priority.remove(0);\n            return 0;\n        }\n\n        return 1;\n    }\n\n    if args.len() == 2 && args[1] == \"-a\" {\n        core.job_table.clear();\n        core.job_table_priority.clear();\n        return 0;\n    }\n\n    for a in &args[1..] {\n        if a.starts_with(\"-\") {\n            let msg = format!(\"{}: invalid option\", &a);\n            super::error_(127, &args[0], &msg, core);\n            eprintln!(\"disown: usage: disown [-h] [-ar] [jobspec ... | pid ...]\");\n            return 127;\n        }\n    }\n\n    for a in &args[1..] {\n        if let Some(pos) = jobspec_to_array_pos(core, &args[0], a) {\n            if h_opt {\n                //TODO: to make each job doesn't stop by SIGHUP\n            } else {\n                remove(core, pos);\n            }\n        }\n    }\n\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/loop_control.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\n\npub fn return_(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.source_function_level <= 0 {\n        eprintln!(\"sush: return: can only `return' from a function or sourced script\");\n        return 2;\n    }\n    core.return_flag = true;\n\n    if args.len() < 2 {\n        return 0;\n    } else if let Ok(n) = args[1].parse::<i32>() {\n        return n % 256;\n    }\n\n    eprintln!(\"sush: return: {}: numeric argument required\", args[1]);\n    2\n}\n\npub fn break_(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.loop_level <= 0 {\n        eprintln!(\"sush: break: only meaningful in a `for', `while', or `until' loop\");\n        return 0;\n    }\n\n    core.break_counter += 1;\n    if args.len() < 2 {\n        return 0;\n    }\n\n    match args[1].parse::<i32>() {\n        Ok(n) => {\n            if n > 0 {\n                core.break_counter += n - 1;\n            } else {\n                eprintln!(\"sush: break: {}: loop count out of range\", args[1]);\n                return 1;\n            }\n        }\n        Err(_) => {\n            eprintln!(\"sush: break: {}: numeric argument required\", args[1]);\n            return 128;\n        }\n    };\n    0\n}\n\npub fn continue_(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if core.loop_level <= 0 {\n        eprintln!(\"sush: continue: only meaningful in a `for', `while', or `until' loop\");\n        return 0;\n    }\n\n    //core.continue_counter += 1;\n    core.continue_counter = 1;\n    if args.len() < 2 {\n        return 0;\n    }\n\n    match args[1].parse::<i32>() {\n        Ok(n) => {\n            if n > 0 {\n                //core.continue_counter += n - 1;\n                core.continue_counter = n;\n            } else {\n                eprintln!(\"sush: continue: {}: loop count out of range\", args[1]);\n                return 1;\n            }\n        }\n        Err(_) => {\n            eprintln!(\"sush: continue: {}: numeric argument required\", args[1]);\n            return 128;\n        }\n    };\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/option.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse crate::utils::arg;\nuse crate::{error, ShellCore};\n\npub fn set_positions(core: &mut ShellCore, args: &[String]) -> Result<(), ExecError> {\n    let com = match core.db.position_parameters.pop() {\n        Some(scope) => {\n            if scope.is_empty() {\n                \"\".to_string()\n            } else {\n                scope[0].clone()\n            }\n        }\n        None => return Err(ExecError::Other(\"empty param stack\".to_string())),\n    };\n\n    let mut tmp = args.to_vec();\n    if !tmp.is_empty() {\n        tmp[0] = com;\n    } else {\n        tmp.push(com);\n    }\n\n    core.db.position_parameters.push(tmp);\n    Ok(())\n}\n\npub fn set_positions_c(core: &mut ShellCore, args: &[String]) -> Result<(), ExecError> {\n    if core.db.position_parameters.pop().is_none() {\n        return Err(ExecError::Other(\"empty param stack\".to_string()));\n    }\n\n    core.db.position_parameters.push(args.to_vec());\n    Ok(())\n}\n\nfn check_invalid_options(args: &[String]) -> Result<(), ExecError> {\n    for a in args {\n        if a.starts_with(\"-\") {\n            return Err(ExecError::InvalidOption(\n                \"set: \".to_owned() + &a.to_string(),\n            ));\n        }\n    }\n    Ok(())\n}\n\npub fn set_options(core: &mut ShellCore, args: &mut Vec<String>) -> Result<(), ExecError> {\n    set_short_options(core, args);\n    check_invalid_options(args)\n}\n\npub fn set_short_options(core: &mut ShellCore, args: &mut Vec<String>) {\n    for (short, long) in [\n        ('a', \"allexport\"),\n        ('t', \"onecmd\"),\n        ('m', \"monitor\"),\n        ('C', \"noclobber\"),\n        ('a', \"allexport\"),\n        ('B', \"braceexpand\"),\n        ('f', \"\"),\n        ('u', \"\"),\n        ('e', \"\"),\n        ('r', \"\"),\n        ('H', \"\"),\n        ('x', \"\"),\n        ('v', \"\"),\n    ] {\n        let minus_opt = format!(\"-{short}\");\n        let plus_opt = format!(\"+{short}\");\n\n        if arg::consume_option(&minus_opt, args) {\n            if !core.db.flags.contains(short) {\n                core.db.flags += &minus_opt[1..];\n            }\n            if !long.is_empty() {\n                let _ = core.options.set(long, true);\n            }\n        }\n\n        if arg::consume_option(&plus_opt, args) {\n            core.db.flags.retain(|f| f != short);\n            if !long.is_empty() {\n                let _ = core.options.set(long, false);\n            }\n        }\n    }\n}\n\nfn set_bash_flags(core: &mut ShellCore, args: &[String]) {\n    let positive = args[1] == \"-o\";\n\n    if args[2] == \"monitor\" {\n        if positive && !core.db.flags.contains('m') {\n            core.db.flags.push('m');\n        } else if !positive {\n            core.db.flags.retain(|f| f != 'm');\n        }\n    }else if args[2] == \"allexport\" {\n        if positive && !core.db.flags.contains('a') {\n            core.db.flags.push('a');\n        } else if !positive {\n            core.db.flags.retain(|f| f != 'a');\n        }\n    }\n}\n\npub fn set(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n\n    if core.db.flags.contains('r') && arg::consume_arg(\"+r\", &mut args) {\n        let _ = super::error_(1, &args[0], \"+r: invalid option\", core);\n        eprintln!(\"set: usage: set [-abefhkmnptuvxBCEHPT] [-o option-name] [--] [-] [arg ...]\");\n        return 1;\n    }\n\n    if args.len() <= 1 {\n        core.db.print_params_and_funcs();\n        return 0;\n    }\n\n    set_short_options(core, &mut args);\n\n    if args.len() < 2 {\n        return 0;\n    }\n\n    if args[1] == \"--\" || args[1] == \"-\" {\n        args[1] = core.db.position_parameters[0][0].clone();\n        args.remove(0);\n        match set_positions(core, &args) {\n            Ok(()) => return 0,\n            Err(e) => {\n                return super::error_(1, &args[0], &String::from(&e), core);\n            }\n        }\n    }\n\n    if args[1] == \"-o\" || args[1] == \"+o\" {\n        let positive = args[1] == \"-o\";\n\n        if args.len() == 2 {\n            core.options.print_all(positive);\n            return 0;\n        } else {\n            set_bash_flags(core, &args);\n            /*\n            if args[2] == \"monitor\" {\n                if positive && !core.db.flags.contains('m') {\n                    core.db.flags.push('m');\n                } else if !positive {\n                    core.db.flags.retain(|f| f != 'm');\n                }\n            }else if args[2] == \"allexport\" {\n                if positive && !core.db.flags.contains('a') {\n                    core.db.flags.push('a');\n                } else if !positive {\n                    core.db.flags.retain(|f| f != 'a');\n                }\n            }*/\n\n            return match core.options.set(&args[2], positive) {\n                Ok(()) => 0,\n                Err(e) => {\n                    return super::error_(2, &args[0], &String::from(&e), core);\n                }\n            };\n        }\n    }\n\n    if !args[1].starts_with(\"-\") && !args[1].starts_with(\"+\") {\n        if let Err(e) = set_positions(core, &args) {\n            e.print(core);\n            return 2;\n        } else {\n            return 0;\n        }\n    }\n\n    if let Err(e) = check_invalid_options(&args) {\n        e.print(core);\n        return 2;\n    }\n    0\n}\n\npub fn shift(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if args.len() == 1 {\n        let mut last = core.db.position_parameters.pop().unwrap();\n        if last.len() > 1 {\n            last.remove(1);\n        }\n        core.db.position_parameters.push(last);\n        return 0;\n    }\n\n    if args.len() == 2 {\n        let n = match args[1].parse::<i32>() {\n            Ok(n) => n,\n            Err(_) => {\n                let err = format!(\"shift: {}: numeric argument required\", &args[1]);\n                error::print(&err, core);\n                return 1;\n            }\n        };\n\n        if n < 0 {\n            let err = format!(\"shift: {}: shift count out of range\", &args[1]);\n            error::print(&err, core);\n            return 1;\n        }\n\n        let mut last = core.db.position_parameters.pop().unwrap();\n        for _ in 0..n {\n            if last.len() == 1 {\n                break;\n            }\n            last.remove(1);\n        }\n        core.db.position_parameters.push(last);\n        return 0;\n    }\n\n    error::print(\"shift: too many arguments\", core);\n    1\n}\n\npub fn shopt_print(core: &mut ShellCore, args: &[String], all: bool) -> i32 {\n    if all {\n        core.shopts.print_all(true);\n        return 0;\n    }\n\n    let mut res = true;\n    match args[1].as_str() {\n        \"-s\" => core.shopts.print_if(true),\n        \"-u\" => core.shopts.print_if(false),\n        \"-q\" => return 0,\n        opt => res = core.shopts.print_opt(opt, false),\n    }\n\n    match res {\n        true => 0,\n        false => 1,\n    }\n}\n\npub fn shopt(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n    let print = arg::consume_arg(\"-p\", &mut args);\n    let o_opt = arg::consume_arg(\"-o\", &mut args);\n    let q_opt = arg::consume_arg(\"-q\", &mut args);\n\n    /* print section */\n    if print && o_opt {\n        if args.len() >= 2 && !q_opt {\n            core.options.print_opt(&args[1], true);\n        } else if !q_opt {\n            core.options.print_all(false);\n        }\n        return 0;\n    }\n\n    /* q option */\n    if q_opt {\n        for a in &args[1..] {\n            if ! core.shopts.query(a) {\n                return 1;\n            }\n        }\n    }\n\n    if args.len() < 3 {\n        // \"shopt\" or \"shopt option\"\n        if !q_opt {\n            let len = args.len();\n            return shopt_print(core, &args, len < 2);\n        }\n        return 0;\n    }\n    /* end of print section */\n\n    if o_opt {\n        let opt = match args[1].as_str() {\n            \"-s\" => \"-o\",\n            \"-u\" => \"+o\",\n            other => other,\n        }\n        .to_string();\n        let mut args_for_set = vec![\"set\".to_string(), opt];\n        args_for_set.append(&mut args[2..].to_vec());\n\n        return set(core, &args_for_set);\n    }\n\n    match args[1].as_str() {\n        //TODO: args[3..] must to be set\n        \"-s\" => {\n            if core.shopts.implemented.contains(&args[2]) {\n                match core.shopts.set(&args[2], true) {\n                    Ok(()) => 0,\n                    Err(e) => {\n                        e.print(core);\n                        1\n                    }\n                }\n            } else {\n                let msg = format!(\"shopt: {}: not supported yet\", &args[2]);\n                error::print(&msg, core);\n                1\n            }\n        }\n        \"-q\" => {\n            for arg in &args[2..] {\n                if !core.shopts.exist(arg) {\n                    let msg = format!(\"shopt: {}: invalid shell option name\", &arg);\n                    error::print(&msg, core);\n                    return 1;\n                }\n                if !core.shopts.query(arg) {\n                    return 1;\n                }\n            }\n            0\n        }\n        \"-u\" => match core.shopts.set(&args[2], false) {\n            Ok(()) => 0,\n            Err(e) => {\n                e.print(core);\n                1\n            }\n        },\n        arg => {\n            eprintln!(\"sush: shopt: {arg}: invalid shell option name\");\n            eprintln!(\"shopt: usage: shopt [-su] [optname ...]\");\n            1\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/builtins/printf.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::substitution::Substitution;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::{error, Feeder, ShellCore};\nuse std::io::{stdout, Write};\n\n#[derive(Debug, Clone)]\nenum PrintfToken {\n    B(String),\n    DI(String),\n    F(String),\n    O(String, bool),\n    S(String),\n    U(String),\n    X(String, bool),\n    LargeX(String, bool),\n    Q,\n    Other(String),\n    Normal(String),\n    EscapedChar(char),\n}\n\nimpl PrintfToken {\n    fn continue_(&self) -> bool {\n        !matches!(self, Self::Normal(_) | Self::EscapedChar(_))\n    }\n\n    fn to_int(s: &String) -> Result<isize, ExecError> {\n        if s.starts_with(\"'\") && s.len() > 1 {\n            let ch = match s.chars().nth(1) {\n                Some(n) => n,\n                None => return Err(ExecError::Other(\"Invalid char: \".to_owned() + s)),\n            };\n            return Ok(ch as isize);\n        }\n\n        match s.parse::<isize>() {\n            Ok(n) => Ok(n),\n            Err(_) => Err(ArithError::InvalidNumber(s.to_string()).into()),\n        }\n    }\n\n    fn to_float(s: &String) -> Result<f64, ExecError> {\n        match s.parse::<f64>() {\n            Ok(n) => Ok(n),\n            Err(_) => Err(ArithError::InvalidNumber(s.to_string()).into()),\n        }\n    }\n\n    //TODO: implement!\n    fn padding_float(_: &mut String, mut _fmt: String) {\n        if _fmt.is_empty() {}\n    }\n\n    fn padding(s: &mut String, mut fmt: String, is_int: bool) {\n        if fmt.is_empty() {\n            return;\n        }\n\n        let mut right = false;\n        let mut padding = ' ';\n\n        if fmt.starts_with(\"-\") {\n            fmt.remove(0);\n            right = true;\n        }\n        if fmt.starts_with(\"0\") {\n            padding = fmt.remove(0);\n        }\n\n        if !is_int {\n            padding = ' ';\n        }\n\n        let len = fmt.parse::<usize>().unwrap_or(0);\n        if right {\n            while s.len() < len {\n                s.push(' ');\n            }\n            return;\n        }\n\n        if is_int && s.starts_with(\"-\") && padding == '0' {\n            while s.len() < len {\n                s.insert(1, '0');\n            }\n            return;\n        }\n\n        while s.len() < len {\n            s.insert(0, padding);\n        }\n    }\n\n    fn render_value(&mut self, args: &mut Vec<String>) -> Result<String, ExecError> {\n        match self {\n            Self::DI(fmt) => {\n                let mut a = pop(args);\n                Self::padding(&mut a, fmt.clone(), true);\n                Ok(a)\n            }\n            Self::U(fmt) => {\n                let mut a = pop(args);\n                if a.starts_with(\"-\") {\n                    a.remove(0);\n                    let mut num = a.parse::<u64>()?;\n                    num = u64::MAX - num + 1;\n                    let mut a = num.to_string();\n                    Self::padding(&mut a, fmt.clone(), true);\n                    Ok(a)\n                } else {\n                    Self::padding(&mut a, fmt.clone(), true);\n                    Ok(a)\n                }\n            }\n            Self::F(fmt) => {\n                let mut a = format!(\"{:.6}\", Self::to_float(&pop(args))?);\n                Self::padding_float(&mut a, fmt.clone());\n                Ok(a)\n            }\n            Self::S(fmt) => {\n                let mut a = pop(args);\n                Self::padding(&mut a, fmt.clone(), false);\n                Ok(a)\n            }\n            Self::B(fmt) => {\n                let mut a = replace_escape(&pop(args));\n                Self::padding(&mut a, fmt.clone(), false);\n                Ok(a)\n            }\n            Self::X(fmt, hash) => {\n                let mut a = format!(\"{:x}\", Self::to_int(&pop(args))?);\n                if *hash {\n                    a = \"0x\".to_owned() + &a;\n                }\n\n                Self::padding(&mut a, fmt.clone(), true);\n                Ok(a)\n            }\n            Self::O(fmt, hash) => {\n                let mut a = format!(\"{:o}\", Self::to_int(&pop(args))?);\n                if *hash {\n                    a.insert(0, '0');\n                }\n\n                Self::padding(&mut a, fmt.clone(), true);\n                Ok(a)\n            }\n            Self::LargeX(fmt, hash) => {\n                let mut a = format!(\"{:X}\", Self::to_int(&pop(args))?);\n                if *hash {\n                    a = \"0X\".to_owned() + &a;\n                }\n\n                Self::padding(&mut a, fmt.clone(), true);\n                Ok(a)\n            }\n            Self::Q => {\n                let a = pop(args);\n                let mut q = a\n                    .replace(\"\\\\\", \"\\\\\\\\\")\n                    .replace(\"$\", \"\\\\$\")\n                    .replace(\"|\", \"\\\\|\")\n                    .replace(\"\\\"\", \"\\\\\\\"\")\n                    .replace(\"'\", \"\\\\\\'\")\n                    .replace(\"~\", \"\\\\~\")\n                    .replace(\"(\", \"\\\\(\")\n                    .replace(\")\", \"\\\\)\")\n                    .replace(\"{\", \"\\\\{\")\n                    .replace(\"}\", \"\\\\}\")\n                    .replace(\"!\", \"\\\\!\")\n                    .replace(\"&\", \"\\\\&\");\n\n                if q == \"\" {\n                    q = \"''\".to_string();\n                }\n\n                Ok(q)\n            }\n            Self::Other(s) => {\n                let a = pop(args);\n                let formatted = match sprintf::sprintf!(&s, a) {\n                    Ok(res) => res,\n                    Err(e) => {\n                        let msg = format!(\"{} {} {}\", &e, &s, &a);\n                        return Err(ExecError::Other(msg));\n                    }\n                };\n\n                Ok(formatted)\n            }\n            Self::EscapedChar(c) => Ok(esc_to_str(*c)),\n            Self::Normal(s) => Ok(s.clone()),\n        }\n    }\n}\n\nfn pop(args: &mut Vec<String>) -> String {\n    match args.is_empty() {\n        true => \"\".to_string(),\n        false => args.remove(0),\n    }\n}\n\nfn esc_to_str(ch: char) -> String {\n    match ch {\n        'a' => char::from(7).to_string(),\n        'b' => char::from(8).to_string(),\n        'e' | 'E' => char::from(27).to_string(),\n        'f' => char::from(12).to_string(),\n        'n' => \"\\n\".to_string(),\n        'r' => \"\\r\".to_string(),\n        't' => \"\\t\".to_string(),\n        'v' => char::from(11).to_string(),\n        '\\\\' => \"\\\\\".to_string(),\n        '\\'' => \"'\".to_string(),\n        '\"' => \"\\\"\".to_string(),\n        _ => (\"\\\\\".to_owned() + &ch.to_string()).to_string(),\n    }\n}\n\nfn replace_escape(s: &str) -> String {\n    let mut ans = String::new();\n    let mut esc = false;\n\n    for ch in s.chars() {\n        if esc || ch == '\\\\' {\n            if esc {\n                ans.push_str(&esc_to_str(ch));\n            }\n            esc = !esc;\n            continue;\n        }\n\n        ans.push(ch);\n    }\n\n    ans\n}\n\nfn scanner_normal(remaining: &str) -> usize {\n    let mut pos = 0;\n\n    for c in remaining.chars() {\n        if c == '%' || c == '\\\\' {\n            break;\n        }\n        pos += c.len_utf8();\n    }\n    pos\n}\n\nfn scanner_escaped_char(remaining: &str) -> usize {\n    if !remaining.starts_with(\"\\\\\") {\n        return 0;\n    }\n\n    match remaining.chars().nth(1) {\n        Some(ch) => 1 + ch.len_utf8(),\n        _ => 0,\n    }\n}\n\nfn scanner_hash(remaining: &str) -> usize {\n    let mut ans = 0;\n    for c in remaining.chars() {\n        if c != '#' {\n            break;\n        }\n        ans += 1;\n    }\n    ans\n}\n\nfn scanner_format_num(remaining: &str) -> usize {\n    let mut ans = 0;\n    for c in remaining.chars() {\n        if !\"-.\".contains(c) && !c.is_ascii_digit() {\n            break;\n        }\n\n        ans += 1;\n    }\n    ans\n}\n\nfn parse(pattern: &str) -> Vec<PrintfToken> {\n    let mut remaining = pattern.to_string();\n    let mut ans = vec![];\n\n    while !remaining.is_empty() {\n        let len = scanner_normal(&remaining);\n        if len > 0 {\n            let tail = remaining.split_off(len);\n            ans.push(PrintfToken::Normal(remaining));\n            remaining = tail;\n            continue;\n        }\n\n        let len = scanner_escaped_char(&remaining);\n        if len > 0 {\n            remaining.remove(0);\n            ans.push(PrintfToken::EscapedChar(remaining.remove(0)));\n            continue;\n        }\n\n        if remaining.starts_with(\"%\") {\n            remaining.remove(0); // %\n\n            let mut has_hash = false;\n            let mut num_part = String::new();\n            let len = scanner_hash(&remaining);\n            if len > 0 {\n                let tail = remaining.split_off(len);\n                has_hash = true;\n                remaining = tail;\n            }\n\n            let len = scanner_format_num(&remaining);\n            if len > 0 {\n                let tail = remaining.split_off(len);\n                num_part = remaining.clone();\n                remaining = tail;\n            }\n\n            let token = match remaining.chars().next() {\n                Some('b') => PrintfToken::B(num_part),\n                Some('d') => PrintfToken::DI(num_part),\n                Some('i') => PrintfToken::DI(num_part),\n                Some('f') => PrintfToken::F(num_part),\n                Some('o') => PrintfToken::O(num_part, has_hash),\n                Some('s') => PrintfToken::S(num_part),\n                Some('u') => PrintfToken::U(num_part),\n                Some('x') => PrintfToken::X(num_part, has_hash),\n                Some('X') => PrintfToken::LargeX(num_part, has_hash),\n                Some('q') => PrintfToken::Q,\n                Some(c) => PrintfToken::Other(\"%\".to_owned() + &num_part + &c.to_string()),\n                None => PrintfToken::Normal(\"%\".to_string()),\n            };\n\n            remaining.remove(0);\n            ans.push(token);\n        }\n    }\n\n    ans\n}\n\nfn format(pattern: &str, args: &mut Vec<String>) -> Result<String, ExecError> {\n    let mut ans = String::new();\n\n    let mut tokens = parse(pattern);\n    let mut fin = true;\n\n    for tok in tokens.iter_mut() {\n        if tok.continue_() {\n            fin = false;\n        }\n        ans += &tok.render_value(args)?;\n    }\n\n    if !args.is_empty() && !fin {\n        if let Ok(s) = format(pattern, args) {\n            ans += &s;\n        }\n    }\n    Ok(ans)\n}\n\nfn arg_check(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() < 2 || args[1] == \"--help\" || args[1] == \"-v\" && args.len() == 3 {\n        let msg = \"printf: usage: printf [-v var] format [arguments]\".to_string();\n        error::print(&msg, core);\n        return 2;\n    }\n\n    if args[1] == \"-v\" && args.len() == 2 {\n        let msg = \"printf: -v: option requires an argument\".to_string();\n        error::print(&msg, core);\n        let msg = \"printf: usage: printf [-v var] format [arguments]\".to_string();\n        error::print(&msg, core);\n        return 2;\n    }\n\n    0\n}\n\nfn printf_v(core: &mut ShellCore, args: &mut Vec<String>) -> i32 {\n    if args[3] == \"--\" {\n        args.remove(3);\n    }\n\n    let s = match format(&args[3], &mut args[4..].to_vec()) {\n        Ok(ans) => ans,\n        Err(e) => {\n            let msg = String::from(&e);\n            return super::error_(1, \"printf\", &msg, core);\n        }\n    };\n\n    if args[2].contains(\"[\") {\n        let mut f = Feeder::new(&(args[2].clone() + \"=\" + &s));\n        if let Ok(Some(mut a)) = Substitution::parse(&mut f, core, false, false) {\n            if let Err(e) = a.eval(core, None, false) {\n                let msg = String::from(&e);\n                return super::error_(2, \"printf\", &msg, core);\n            }\n        } else {\n            return 1;\n        }\n        return 0;\n    }\n    if let Err(e) = core.db.set_param(&args[2], &s, None) {\n        let msg = String::from(&e);\n        return super::error_(2, \"printf\", &msg, core);\n    }\n\n    0\n}\n\npub fn printf(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    match arg_check(core, &args) {\n        0 => {}\n        n => return n,\n    }\n\n    if args[1] == \"-v\" {\n        return printf_v(core, &mut args);\n    }\n\n    let s = match format(&args[1], &mut args[2..].to_vec()) {\n        Ok(ans) => ans,\n        Err(e) => {\n            let msg = format!(\"printf: {e:?}\");\n            error::print(&msg, core);\n            return 1;\n        }\n    };\n    print!(\"{}\", &s);\n    stdout().flush().unwrap();\n    0\n}\n"
  },
  {
    "path": "src/core/builtins/pwd.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\n\npub fn pwd(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() == 1 || &args[1][..1] != \"-\" {\n        // $ pwd, $ pwd aaa\n        return show_pwd(core, false);\n    }\n\n    match args[1].as_str() {\n        //$pwd -L, pwd -P, pwd -aaaa\n        \"-P\" => show_pwd(core, true), // シンボリックリンク名を解決して表示\n        \"-L\" => show_pwd(core, false), // シンボリックリンク名をそのまま表示（bash default）\n        _ => {\n            eprintln!(\"sush: pwd: {}: invalid option\", &args[1]);\n            eprintln!(\"pwd: usage: pwd [-LP]\");\n            1\n        }\n    }\n}\n\nfn show_pwd(core: &mut ShellCore, physical: bool) -> i32 {\n    if let Some(mut path) = core.get_current_directory() {\n        if physical && path.is_symlink() {\n            if let Ok(c) = path.canonicalize() {\n                path = c;\n            }\n        }\n        println!(\"{}\", path.display());\n        return 0;\n    }\n    1\n}\n"
  },
  {
    "path": "src/core/builtins/read.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::error_;\nuse crate::elements::substitution::variable::Variable;\nuse crate::{arg, error, utils, ShellCore};\n\nfn check_word_limit(word: &mut String, limit: &mut usize) -> bool {\n    let mut pos = 0;\n    for c in word.chars() {\n        if *limit == 0 {\n            let _ = word.split_off(pos);\n            return true;\n        }\n        *limit -= 1;\n\n        pos += c.len_utf8();\n    }\n    false\n}\n\npub fn read_(\n    core: &mut ShellCore,\n    args: &mut Vec<String>,\n    ignore_escape: bool,\n    limit: &mut usize,\n    delim: &String,\n) -> i32 {\n    let mut remaining = utils::read_line_stdin_unbuffered(delim).unwrap_or(\"\".to_string());\n    if remaining.is_empty() {\n        return 1;\n    }\n\n    let ifs = match core.db.exist(\"IFS\") {\n        true => core.db.get_param(\"IFS\").unwrap(),\n        false => \" \\t\\n\".to_string(),\n    };\n\n    let mut tail_space = ifs\n        .chars()\n        .filter(|i| \" \\t\\n\".contains(*i))\n        .collect::<String>();\n    tail_space += delim;\n\n    args.remove(0);\n    if args.is_empty() {\n        args.push(\"REPLY\".to_string());\n    }\n\n    consume_ifs(&mut remaining, \" \\t\", limit);\n\n    while !args.is_empty() && !remaining.is_empty() && *limit != 0 {\n        let mut word = match eat_word(core, &mut remaining, &ifs, ignore_escape, delim) {\n            Some(w) => w,\n            None => break,\n        };\n\n        check_word_limit(&mut word, limit);\n\n        if args.len() == 1 && *limit != 0 {\n            let bkup = remaining.clone();\n            consume_ifs(&mut remaining, &ifs, limit);\n\n            if remaining.is_empty() || remaining == \"\\n\" {\n            } else {\n                word += &bkup;\n            }\n        }\n\n        consume_tail_ifs(&mut word, &tail_space);\n\n        if let Err(e) = Variable::parse_and_set(&args[0], &word, core) {\n            return super::error_(1, \"read\", &String::from(&e), core);\n        }\n\n        args.remove(0);\n        consume_ifs(&mut remaining, &ifs, limit);\n    }\n\n    0\n}\n\npub fn read_a(\n    core: &mut ShellCore,\n    name: &str,\n    ignore_escape: bool,\n    limit: &mut usize,\n    delim: &String,\n) -> i32 {\n    let mut remaining = utils::read_line_stdin_unbuffered(delim).unwrap_or(\"\".to_string());\n    if remaining.is_empty() {\n        return 1;\n    }\n\n    let ifs = match core.db.exist(\"IFS\") {\n        true => core.db.get_param(\"IFS\").unwrap(),\n        false => \" \\t\\n\".to_string(),\n    };\n\n    let mut tail_space = ifs\n        .chars()\n        .filter(|i| \" \\t\\n\".contains(*i))\n        .collect::<String>();\n    tail_space += delim;\n\n    consume_ifs(&mut remaining, \" \\t\", limit);\n\n    let mut pos = 0;\n    while !remaining.is_empty() {\n        let mut word = match eat_word(core, &mut remaining, &ifs, ignore_escape, delim) {\n            Some(w) => w,\n            None => break,\n        };\n        check_word_limit(&mut word, limit);\n        consume_tail_ifs(&mut word, &tail_space);\n\n        if let Err(e) = core.db.set_array_elem(name, &word, pos, None, false) {\n            let msg = format!(\"{:?}\", &e);\n            error::print(&msg, core);\n            return 1;\n        }\n        pos += 1;\n        consume_ifs(&mut remaining, &ifs, limit);\n    }\n\n    0\n}\n\npub fn read(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.is_empty() {\n        return 0;\n    }\n\n    let mut args = arg::dissolve_options(args);\n    let r_opt = arg::consume_arg(\"-r\", &mut args);\n    let mut limit = usize::MAX;\n    let limit_str = arg::consume_with_next_arg(\"-n\", &mut args);\n    let delim = match arg::consume_with_next_arg(\"-d\", &mut args) {\n        Some(c) => c,\n        None => \"\\n\".to_string(),\n    };\n\n    if let Some(limit_str) = limit_str {\n        match limit_str.parse::<usize>() {\n            Ok(n) => limit = n,\n            Err(_) => {\n                let err = format!(\"{}: invalid number\", &limit_str);\n                return error_(1, \"read\", &err, core);\n            }\n        };\n    }\n\n    if let Some(a) = arg::consume_with_next_arg(\"-a\", &mut args) {\n        return read_a(core, &a, r_opt, &mut limit, &delim);\n    }\n\n    read_(core, &mut args, r_opt, &mut limit, &delim)\n}\n\npub fn eat_word(\n    _core: &mut ShellCore,\n    remaining: &mut String,\n    ifs: &str,\n    ignore_escape: bool,\n    delim: &String,\n) -> Option<String> {\n    let mut esc = false;\n    let mut pos = 0;\n    let mut escape_pos = vec![];\n\n    for c in remaining.chars() {\n        if (esc || c == '\\\\') && !ignore_escape {\n            esc = !esc;\n            if esc {\n                escape_pos.push(pos);\n            }\n            pos += c.len_utf8();\n            continue;\n        }\n\n        if ifs.contains(c) {\n            break;\n        }\n        pos += c.len_utf8();\n    }\n\n    if let Some(p) = escape_pos.last() {\n        if p + 2 == remaining.len() && remaining.ends_with('\\n') {\n            remaining.pop();\n            remaining.pop();\n\n            let line = utils::read_line_stdin_unbuffered(delim).unwrap_or(\"\".to_string());\n            if !line.is_empty() {\n                *remaining += &line;\n                return eat_word(_core, remaining, ifs, ignore_escape, delim);\n            }\n        }\n    }\n\n    let tail = remaining.split_off(pos);\n    let mut ans = remaining.clone();\n    *remaining = tail;\n\n    for p in escape_pos {\n        ans.remove(p);\n    }\n\n    Some(ans)\n}\n\npub fn consume_tail_ifs(remaining: &mut String, ifs: &str) {\n    loop {\n        if let Some(c) = remaining.chars().last() {\n            if ifs.contains(c) {\n                remaining.pop();\n                continue;\n            }\n        }\n        break;\n    }\n}\n\npub fn consume_ifs(remaining: &mut String, ifs: &str, limit: &mut usize) {\n    let special_ifs: Vec<char> = ifs.chars().filter(|s| !\" \\t\\n\".contains(*s)).collect();\n    let mut pos = 0;\n    let mut special_ifs_exist = false;\n\n    for ch in remaining.chars() {\n        if !ifs.contains(ch) || *limit == 0 {\n            break;\n        }\n\n        if special_ifs.contains(&ch) {\n            if special_ifs_exist {\n                break;\n            }\n\n            special_ifs_exist = true;\n        }\n        pos += ch.len_utf8();\n        *limit -= 1;\n    }\n\n    let tail = remaining.split_off(pos);\n    *remaining = tail;\n}\n"
  },
  {
    "path": "src/core/builtins/source.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::parse::ParseError;\nuse crate::{file_check, Feeder, Script, ShellCore};\n\nfn check_error(core: &mut ShellCore, args: &[String]) -> i32 {\n    if core.db.flags.contains('r') && args[1].contains('/') {\n        let msg = format!(\"{}: restricted\", &args[1]);\n        return super::error_(1, &args[0], &msg, core);\n    }\n\n    if args.len() < 2 {\n        eprintln!(\"sush: source: filename argument required\");\n        eprintln!(\"source: usage: source filename [arguments]\");\n        return 2;\n    }\n\n    if file_check::is_dir(&args[1]) {\n        eprintln!(\"sush: source: {}: is a directory\", &args[1]);\n        return 1;\n    }\n    0\n}\n\npub fn source(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    let check = check_error(core, &args);\n    if check != 0 {\n        return check;\n    }\n\n    let mut feeder = Feeder::new(\"\");\n    if let Err(e) = feeder.set_file(&args[1]) {\n        ParseError::Input(e).print(core);\n        return 1;\n    }\n\n    let mut source = match core.db.get_vec(\"BASH_SOURCE\", false) {\n        Ok(s) => s,\n        Err(e) => {\n            e.print(core);\n            return 1;\n        }\n    };\n\n    core.source_function_level += 1;\n    core.source_files.push(args[1].to_string());\n    core.db.position_parameters.push(args[1..].to_vec());\n    source.insert(0, args[1].clone());\n    let _ = core.db.init_array(\"BASH_SOURCE\", Some(source.clone()), None, false);\n\n    feeder.main_feeder = true;\n    while let Ok(()) = feeder.feed_line(core) {\n        if core.return_flag {\n            feeder.consume(feeder.len());\n        }\n\n        match Script::parse(&mut feeder, core, false) {\n            Ok(Some(mut s)) => {\n                let _ = s.exec(core);\n            }\n            Err(e) => e.print(core),\n            _ => {}\n        }\n    }\n\n    source.remove(0);\n    let _ = core.db.init_array(\"BASH_SOURCE\", Some(source), None, false);\n    core.db.position_parameters.pop();\n    core.source_function_level -= 1;\n    core.source_files.pop();\n    core.return_flag = false;\n    core.db.exit_status\n}\n"
  },
  {
    "path": "src/core/builtins/trap.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse crate::signal;\nuse crate::ShellCore;\nuse nix::sys::signal::Signal;\nuse signal_hook::iterator::Signals;\nuse std::str::FromStr;\nuse std::sync::atomic::AtomicBool;\nuse std::sync::atomic::Ordering::Relaxed;\nuse std::sync::Arc;\nuse std::{thread, time};\n\npub fn trap(core: &mut ShellCore, args: &[String]) -> i32 {\n    let args = args.to_owned();\n    if args.len() == 1 {\n        for e in &core.traplist {\n            if e.0 == 0 {\n                println!(\"trap -- '{}' EXIT\", &e.1);\n            } else if let Ok(s) = Signal::try_from(e.0) {\n                println!(\"trap -- '{}' {}\", &e.1, &s);\n            }\n        }\n        return 0;\n    }\n\n    if args.len() < 3 {\n        // TODO: print the list of trap entries if args.len() == 1\n        eprintln!(\"trap: usage: trap arg signal_spec ...\");\n        return 2;\n    }\n\n    let forbiddens = Vec::from(signal_hook::consts::FORBIDDEN);\n    let signals = match args_to_nums(&args[2..], &forbiddens) {\n        Ok(v) => v,\n        Err(e) => {\n            e.print(core);\n            return 1;\n        }\n    };\n\n    let mut exit = false;\n    let mut valid_signals = vec![];\n    for n in &signals {\n        if *n == 0 {\n            exit = true;\n            continue;\n        }\n\n        if let Ok(s) = TryFrom::try_from(*n) {\n            signal::ignore(s);\n            valid_signals.push(*n);\n            continue;\n        };\n\n        let msg = format!(\"trap: {n}: invalid signal specification\");\n        return super::error_(1, &args[0], &msg, core);\n    }\n\n    if !valid_signals.is_empty() {\n        for n in &valid_signals {\n            core.traplist.push((*n, args[1].to_string()));\n        }\n        run_thread(valid_signals, &args[1], core);\n    }\n\n    if exit {\n        core.traplist.push((0, args[1].to_string()));\n        core.exit_script = args[1].clone();\n    }\n\n    0\n}\n\nfn run_thread(signal_nums: Vec<i32>, script: &str, core: &mut ShellCore) {\n    core.trapped\n        .push((Arc::new(AtomicBool::new(false)), script.to_string()));\n\n    let trap = Arc::clone(&core.trapped.last().unwrap().0);\n\n    thread::spawn(move || {\n        let mut signals =\n            Signals::new(signal_nums.clone()).expect(\"sush(fatal): cannot prepare signal data\");\n\n        loop {\n            thread::sleep(time::Duration::from_millis(5));\n            for signal in signals.pending() {\n                if signal_nums.contains(&signal) {\n                    trap.store(true, Relaxed);\n                }\n            }\n        }\n    });\n}\n\nfn arg_to_num(arg: &str, forbiddens: &[i32]) -> Result<i32, ExecError> {\n    if arg == \"EXIT\" || arg == \"0\" {\n        return Ok(0);\n    }\n\n    if let Ok(n) = Signal::from_str(arg) {\n        return Ok(n as i32);\n    }\n\n    if let Ok(n) = Signal::from_str(&(\"SIG\".to_owned() + arg)) {\n        return Ok(n as i32);\n    }\n\n    if let Ok(n) = arg.parse::<i32>() {\n        if forbiddens.contains(&n) {\n            return Err(ExecError::Other(format!(\n                \"trap: {arg}: forbidden signal for trap\"\n            )));\n        }\n        return Ok(n);\n    }\n\n    Err(ExecError::Other(format!(\n        \"trap: {arg}: invalid signal specification\"\n    )))\n}\n\nfn args_to_nums(args: &[String], forbiddens: &[i32]) -> Result<Vec<i32>, ExecError> {\n    let mut ans = vec![];\n    for a in args {\n        let n = arg_to_num(a, forbiddens)?;\n        ans.push(n);\n    }\n    Ok(ans)\n}\n"
  },
  {
    "path": "src/core/builtins/type_.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::utils::{arg, file};\nuse crate::{file_check, utils, ShellCore};\n\nfn type_no_opt_sub(core: &mut ShellCore, com: &String) -> i32 {\n    if core.shopts.query(\"expand_aliases\") && core.db.has_array_value(\"BASH_ALIASES\", com) {\n        let alias = core.db.get_elem(\"BASH_ALIASES\", com).unwrap();\n        println!(\"{} is aliased to `{}'\", &com, &alias);\n        return 0;\n    }\n    if utils::reserved(com) {\n        println!(\"{} is a shell keyword\", &com);\n        return 0;\n    }\n    if core.db.functions.contains_key(com) {\n        println!(\"{} is a function\", &com);\n        if let Some(val) = core.db.functions.get_mut(com) {\n            val.pretty_print(0);\n        };\n        return 0;\n    }\n    if core.builtins.contains_key(com) {\n        println!(\"{com} is a shell builtin\");\n        return 0;\n    }\n    if let Some(path) = file::search_command(com) {\n        println!(\"{} is {}\", com, &path);\n        return 0;\n    }\n    if file_check::is_executable(com) {\n        println!(\"{com} is {com}\");\n        return 0;\n    }\n\n    let s = format!(\"{}: not found\", &com);\n    super::error_(1, \"type\", &s, core)\n}\n\nfn type_no_opt(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut exit_status = 0;\n    for a in args {\n        exit_status += type_no_opt_sub(core, a);\n    }\n    if exit_status > 1 {\n        exit_status = 1;\n    }\n    exit_status\n}\n\nfn type_t(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut exit_status = 0;\n    for a in args {\n        exit_status += type_t_sub(core, a);\n    }\n    if exit_status > 1 {\n        exit_status = 1;\n    }\n    exit_status\n}\n\nfn type_t_sub(core: &mut ShellCore, com: &String) -> i32 {\n    if core.shopts.query(\"expand_aliases\") && core.db.has_array_value(\"BASH_ALIASES\", com) {\n        println!(\"alias\");\n        return 0;\n    }\n    if utils::reserved(com) {\n        println!(\"keyword\");\n        return 0;\n    }\n    if core.db.functions.contains_key(com) {\n        println!(\"function\");\n        return 0;\n    }\n    if core.builtins.contains_key(com) {\n        println!(\"builtin\");\n        return 0;\n    }\n    if file::search_command(com).is_some() || file_check::is_executable(com) {\n        println!(\"file\");\n        return 0;\n    }\n\n    1\n}\n\nfn type_p(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut exit_status = 0;\n    for a in args {\n        exit_status += type_p_sub(core, a);\n    }\n    if exit_status > 1 {\n        exit_status = 1;\n    }\n    exit_status\n}\n\nfn type_large_p(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut exit_status = 0;\n    for a in args {\n        exit_status += type_large_p_sub(core, a);\n    }\n    if exit_status > 1 {\n        exit_status = 1;\n    }\n    exit_status\n}\n\nfn type_p_sub(core: &mut ShellCore, com: &String) -> i32 {\n    if core.db.has_array_value(\"BASH_ALIASES\", com)\n        || core.db.functions.contains_key(com)\n        || utils::reserved(com)\n        || core.builtins.contains_key(com)\n    {\n        return 0;\n    }\n\n    if let Ok(path) = core.db.get_elem(\"BASH_CMDS\", com) {\n        if !path.is_empty() {\n            println!(\"{}\", &path);\n            return 0;\n        }\n    }\n\n    if let Some(path) = file::search_command(com) {\n        println!(\"{}\", &path);\n        return 0;\n    }\n    if file_check::is_executable(com) {\n        println!(\"{com}\");\n        return 0;\n    }\n    1\n}\n\nfn type_large_p_sub(core: &mut ShellCore, com: &String) -> i32 {\n    let mut es = 1;\n    if core.db.has_array_value(\"BASH_ALIASES\", com)\n        || core.db.functions.contains_key(com)\n        || utils::reserved(com)\n        || core.builtins.contains_key(com)\n    {\n        es = 0;\n    }\n\n    if let Some(path) = file::search_command(com) {\n        println!(\"{}\", &path);\n        return 0;\n    }\n    if file_check::is_executable(com) {\n        println!(\"{com}\");\n        return 0;\n    }\n\n    es\n}\n\npub fn type_(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() < 2 {\n        return 0;\n    }\n\n    let mut args = arg::dissolve_options(args);\n\n    let t_option = arg::consume_arg(\"-t\", &mut args);\n    if t_option {\n        if args.len() > 1 && args[1] == \"--\" {\n            args.remove(1);\n        }\n        return type_t(core, &args[1..]);\n    }\n    let p_option = arg::consume_arg(\"-p\", &mut args);\n    if p_option {\n        if args.len() > 1 && args[1] == \"--\" {\n            args.remove(1);\n        }\n        return type_p(core, &args[1..]);\n    }\n    let large_p_option = arg::consume_arg(\"-P\", &mut args);\n    if large_p_option {\n        if args[1] == \"--\" {\n            args.remove(1);\n        }\n        return type_large_p(core, &args[1..]);\n    }\n\n    type_no_opt(core, &args[1..])\n}\n"
  },
  {
    "path": "src/core/builtins/ulimit.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{arg, ShellCore};\nuse nix::libc;\nuse nix::sys::resource;\nuse nix::sys::resource::{Resource, rlim_t};\n\nfn items() -> &'static [(&'static str, &'static str, &'static str, Resource)] {\n    &[\n        #[cfg(any(target_os = \"linux\"))]\n        (\n            \"real-time non-blocking time  \",\n            \"microseconds\",\n            \"-R\",\n            Resource::RLIMIT_RTTIME,\n        ),\n        (\n            \"core file size              \",\n            \"blocks\",\n            \"-c\",\n            Resource::RLIMIT_CORE,\n        ),\n        (\n            \"data seg size               \",\n            \"kbytes\",\n            \"-d\",\n            Resource::RLIMIT_DATA,\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"scheduling priority                 \",\n            \"\",\n            \"-e\",\n            Resource::RLIMIT_NICE,\n        ),\n        (\n            \"file size                   \",\n            \"blocks\",\n            \"-f\",\n            Resource::RLIMIT_FSIZE,\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"pending signals                     \",\n            \"\",\n            \"-i\",\n            Resource::RLIMIT_SIGPENDING,\n        ),\n        (\n            \"max locked memory           \",\n            \"kbytes\",\n            \"-l\",\n            Resource::RLIMIT_MEMLOCK,\n        ),\n        (\n            \"max memory size             \",\n            \"kbytes\",\n            \"-m\",\n            Resource::RLIMIT_RSS,\n        ),\n        (\n            \"open files                          \",\n            \"\",\n            \"-n\",\n            Resource::RLIMIT_NOFILE,\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"pipe size                \",\n            \"512 bytes\",\n            \"-p\",\n            Resource::RLIMIT_SIGPENDING,\n        ), //dummy\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"POSIX message queues         \",\n            \"bytes\",\n            \"-q\",\n            Resource::RLIMIT_MSGQUEUE,\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"real-time priority                  \",\n            \"\",\n            \"-r\",\n            Resource::RLIMIT_RTPRIO,\n        ),\n        (\n            \"stack size                  \",\n            \"kbytes\",\n            \"-s\",\n            Resource::RLIMIT_STACK,\n        ),\n        (\n            \"cpu time                   \",\n            \"seconds\",\n            \"-t\",\n            Resource::RLIMIT_CPU,\n        ),\n        (\n            \"max user processes                  \",\n            \"\",\n            \"-u\",\n            Resource::RLIMIT_NPROC,\n        ),\n        #[cfg(not(any(target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\")))]\n        (\n            \"virtual memory              \",\n            \"kbytes\",\n            \"-v\",\n            Resource::RLIMIT_AS,\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"android\"))]\n        (\n            \"file locks                          \",\n            \"\",\n            \"-x\",\n            Resource::RLIMIT_LOCKS,\n        ),\n    ]\n}\n\nfn print_items(args: &[String], soft: bool) -> i32 {\n    for a in args {\n        for (item, unit, opt, key) in items() {\n            if a == opt {\n                print_item(item, unit, opt, *key, soft);\n            }\n        }\n    }\n\n    0\n}\n\nfn print_item(item: &str, unit: &str, opt: &str, key: Resource, soft: bool) -> i32 {\n    let (soft_limit, hard_limit) = resource::getrlimit(key).unwrap();\n    let mut v = if soft { soft_limit } else { hard_limit };\n    let mut infty = nix::sys::resource::RLIM_INFINITY;\n\n    if item.starts_with(\"pipe size\") {\n        v = ((libc::PIPE_BUF as rlim_t) / 512) as rlim_t;\n    }\n\n    if unit.starts_with(\"kbytes\") {\n        v /= 1024;\n        infty /= 1024;\n    }\n\n    let s = if v == infty {\n        \"unlimited\"\n    } else {\n        &v.to_string()\n    };\n\n    if unit == \"\" {\n        println!(\"{}({}) {}\", &item, &opt, &s);\n    } else {\n        println!(\"{}({}, {}) {}\", &item, &unit, &opt, &s);\n    }\n    0\n}\n\nfn print_all(soft: bool) -> i32 {\n    for (item, unit, opt, key) in items() {\n        print_item(item, unit, opt, *key, soft);\n    }\n    0\n}\n\nfn set_limit(opt: &String, num: &String, soft: bool, hard: bool) -> i32 {\n    let mut limit = match num.as_str() {\n        \"unlimited\" => nix::sys::resource::RLIM_INFINITY,\n        numstr => match numstr.parse::<rlim_t>() {\n            Ok(n) => n,\n            Err(e) => {\n                dbg!(\"{:?}\", &e);\n                return 1;\n            }\n        },\n    };\n\n    for (_, unit, opt2, key) in items() {\n        if opt == opt2 {\n            let (mut soft_limit, mut hard_limit) = resource::getrlimit(*key).unwrap();\n\n            if unit.starts_with(\"kbytes\") {\n                limit *= 1024;\n            }\n\n            if soft {\n                soft_limit = limit;\n            }\n            if hard {\n                hard_limit = limit;\n            }\n\n            match resource::setrlimit(*key, soft_limit, hard_limit) {\n                Err(e) => {\n                    dbg!(\"{:?}\", &e);\n                    return 1;\n                }\n                _ => return 0,\n            }\n        }\n    }\n\n    0\n}\n\npub fn ulimit(_: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n    let mut soft = arg::consume_arg(\"-S\", &mut args);\n    let mut hard = arg::consume_arg(\"-H\", &mut args);\n\n    if args.iter().any(|a| a == \"-a\") {\n        return print_all(!hard);\n    }\n\n    if args.len() > 2 && args[2].parse::<usize>().is_ok() {\n        if !soft && !hard {\n            soft = true;\n            hard = true;\n        }\n        return set_limit(&args[1], &args[2], soft, hard);\n    }\n\n    print_items(&args, !hard)\n}\n"
  },
  {
    "path": "src/core/builtins/unset.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{Feeder, ShellCore};\nuse crate::error::exec::ExecError;\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\n\nfn unset_all(core: &mut ShellCore, name: &str) -> Result<i32, ExecError> {\n    if ! core.shopts.query(\"localvar_unset\") {\n        core.db.unset(name, None, false)?;\n        return Ok(0);\n    }\n\n    let mut scope = core.db.get_scope_num()-1;\n    if scope <= 1 {\n        core.db.unset(name, None, true)?;\n    }else{\n        scope -= 1;\n        core.db.unset(name, Some(scope), true)?;\n    }\n    Ok(0)\n}\n\nfn unset_var(core: &mut ShellCore, name: &str) -> Result<i32, ExecError> {\n    if ! core.shopts.query(\"localvar_unset\") {\n        core.db.unset_var(name, None, false)?;\n        return Ok(0);\n    }\n\n    let mut scope = core.db.get_scope_num()-1;\n    if scope <= 1 {\n        core.db.unset_var(name, None, true)?;\n    }else{\n        scope -= 1;\n        core.db.unset_var(name, Some(scope), true)?;\n    }\n\n    Ok(0)\n}\n\nfn unset_nameref(core: &mut ShellCore, name: &str) -> i32 {\n    if ! core.shopts.query(\"localvar_unset\") {\n        let _ = core.db.unset_nameref(name, None);\n        return 0;\n    }\n\n    let mut scope = core.db.get_scope_num()-1;\n    if scope <= 1 {\n        let _ = core.db.unset_nameref(name, None);\n    }else{\n        scope -= 1;\n        let _ = core.db.unset_nameref(name, Some(scope));\n    }\n\n    0\n}\n\nfn unset_function(core: &mut ShellCore, name: &str) -> i32 {\n    core.db.unset_function(name);\n    0\n}\n\nfn unset_one(core: &mut ShellCore, args: &mut Vec<String>) -> i32 {\n    match args[1].as_ref() {\n        \"-f\" => {\n            if args.len() > 2 {\n                let name = args.remove(2);\n                return unset_function(core, &name);\n            }\n        }\n        \"-v\" => {\n            if args.len() > 2 {\n                let name = args.remove(2);\n                if let Err(e) = unset_var(core, &name) {\n                    return super::error(1, &args[0], &e, core);\n                }else{\n                    return 0;\n                }\n            }\n        }\n        \"-n\" => {\n            if args.len() > 2 {\n                let name = args.remove(2);\n                return unset_nameref(core, &name);\n            }\n        }\n        name => {\n            let name = name.to_string();\n            args.remove(1);\n            if !name.contains(\"[\") {\n                if let Err(e) = unset_all(core, &name) {\n                    return super::error(1, &args[0], &e, core);\n                }else{\n                    return 0;\n                }\n            }\n\n            let pos = name.find(\"[\").unwrap();\n            let mut name = name.clone();\n            let mut index = name.split_off(pos);\n\n            if !index.ends_with(\"]\") {\n                let msg = format!(\"{}: invalid variable\", &name);\n                return super::error_(1, &args[0], &msg, core);\n            }\n\n            index.remove(0);\n            index.pop();\n            let mut index = index;\n\n            if core.db.is_array(&name) {\n                if let Err(_) = index.parse::<isize>() {\n                    let mut f = Feeder::new(&index);\n                    match ArithmeticExpr::parse(&mut f, core, false, \"[\") {\n                        Ok(Some(mut v)) => {\n                            if !f.is_empty() {\n                                let e = ExecError::ArrayIndexInvalid(index.to_string());\n                                return super::error(1, &args[0], &e, core);\n                            }\n                            if let Ok(n) = v.eval(core) {\n                                index = n;\n                            }\n                        },\n                        _ => {\n                            let e = ExecError::ArrayIndexInvalid(index.to_string());\n                            return super::error(1, &args[0], &e, core);\n                        },\n                    }\n                }\n            }\n\n            if let Err(e) = core.db.unset_array_elem(&name, &index) {\n                return super::error_(1, &args[0], &String::from(&e), core);\n            }\n            return 0;\n        }\n    }\n    0\n}\n\npub fn unset(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    let mut exit_status = 0;\n\n    loop {\n        if args.len() < 2 {\n            break;\n        }\n\n        if (args[1] == \"-v\" || args[1] == \"-f\" || args[1] == \"-n\")\n        && args.len() == 2 {\n            break;\n        }\n\n        exit_status = unset_one(core, &mut args);\n    }\n    exit_status\n}\n"
  },
  {
    "path": "src/core/builtins/variable/print.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{builtins, ShellCore};\nuse crate::elements::substitution::Substitution;\nuse crate::utils::arg;\n\nfn format_options(name: &String, core: &mut ShellCore) -> String {\n    let mut opts: Vec<char> = core.db.get_flags(name).chars().collect();\n    opts.sort();\n\n    let ans: String = opts.into_iter().collect();\n    match ans.len() {\n        0 => \"--\".to_string(),\n        _ =>  \"-\".to_owned() + &ans,\n    }\n}\n\nfn drop_by_args(core: &mut ShellCore, names: &mut Vec<String>, args: &[String]) {\n    for flag in ['i', 'a', 'A', 'r', 'x', 'u', 'n', 'l'] {\n        let opt = \"-\".to_owned() + &flag.to_string();\n        if arg::has_option(&opt, args) {\n            names.retain(|n| core.db.has_flag(n, flag));\n        }\n    }\n}\n\nfn output(core: &mut ShellCore, name: &String, args: &[String]) {\n    let mut options = format_options(name, core);\n    if core.options.query(\"posix\") {\n        options.retain(|e| e != 'r');\n    }\n\n    match core.options.query(\"posix\") {\n        false => print!(\"declare {options} \"),\n        true => print!(\"{} {} \", &args[0], options),\n    };\n    core.db.print_for_declare(name);\n}\n\nfn all_params(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut names = core.db.get_param_keys();\n    drop_by_args(core, &mut names, args);\n    names.iter().for_each(|n| {output(core, n, args); });\n    0\n}\n\nfn all_functions(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() != 2 {\n        return 1;\n    }\n\n    let mut names = core.db.get_func_keys();\n    names.sort();\n\n    if names.iter().all(|n| core.db.print_func(n)) {\n        return 0;\n    }\n    1\n}\n\npub(super) fn names_match(core: &mut ShellCore, names: &mut Vec<String>,\n                          args: &[String]) -> i32 {\n    drop_by_args(core, names, args);\n\n    for n in names {\n        if ! core.db.exist(n) && ! core.db.exist_nameref(n) {\n            return builtins::error_(1, n, \"not found\", core);\n        }\n\n        output(core, n, args);\n    }\n    0\n}\n\npub(super) fn f_option(core: &mut ShellCore, args: &[String],\n                        subs: &mut [Substitution]) -> i32 {\n    if subs.is_empty() {\n        return all_functions(core, &args);\n    }\n\n    if args.len() != 2 {\n        return 1;\n    }\n\n    let mut names: Vec<String> = subs.\n        iter()\n        .map(|s| s.left_hand.name.clone())\n        .collect();\n    names.sort();\n\n    if names.iter().all(|n| core.db.print_func(n)) {\n        return 0;\n    }\n    1\n}\n\npub(super) fn args_match(core: &mut ShellCore, args: &[String]) -> i32 {\n    if args.len() <= 1 {\n        core.db.print_params_and_funcs();\n        return 0;\n    }\n    all_params(core, &args)\n}\n"
  },
  {
    "path": "src/core/builtins/variable/set_value.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\nuse crate::elements::substitution::Substitution;\nuse crate::error::exec::ExecError;\nuse crate::utils::arg;\n\nfn set_options_pre(core: &mut ShellCore, name: &String,\n                       scope: usize, args: &[String]) {\n    if arg::has_option(\"-x\", args) {\n        core.db.set_flag(name, 'x', scope);\n    }else if arg::has_option(\"+x\", args) {\n        core.db.unset_flag(name, 'x', scope);\n    }\n\n    if arg::has_option(\"-n\", args) {\n        core.db.set_flag_nameref(name, 'n', scope);\n    }else if arg::has_option(\"+n\", args) {\n        core.db.unset_flag_nameref(name, 'n', scope);\n    }\n\n    if arg::has_option(\"-i\", args) {\n        core.db.set_flag(name, 'i', scope);\n    }else if arg::has_option(\"+i\", args) {\n        core.db.unset_flag(name, 'i', scope);\n    }\n\n    if arg::has_option(\"-l\", args) {\n        core.db.unset_flag(name, 'u', scope);\n        core.db.set_flag(name, 'l', scope);\n    }else if arg::has_option(\"+l\", args) {\n        core.db.unset_flag(name, 'l', scope);\n    }\n\n    if arg::has_option(\"-u\", args) {\n        core.db.unset_flag(name, 'l', scope);\n        core.db.set_flag(name, 'u', scope);\n    }else if arg::has_option(\"+u\", args) {\n        core.db.unset_flag(name, 'u', scope);\n    }\n}\n\nfn set_options_post(core: &mut ShellCore, name: &String,\n                       scope: usize, args: &[String]) {\n    if arg::has_option(\"-r\", args) {\n        core.db.set_flag(&name, 'r', scope);\n    }\n}\n\nfn readonly_check(core: &mut ShellCore, name: &str) -> Result<(), ExecError> {\n    if core.db.is_readonly(&name) {\n        return Err(ExecError::VariableReadOnly(name.to_string()));\n    }\n    Ok(())\n}\n\nfn array_to_element_check(sub: &mut Substitution) -> Result<(), ExecError> {\n    if let Some(r) = sub.right_hand.as_mut() {\n        if sub.left_hand.index.is_some()\n        && r.text.starts_with(\"(\") {\n            let msg = format!(\"{}: cannot assign list to array member\", sub.left_hand.text);\n            return Err(ExecError::Other(msg));\n        }\n    }\n    Ok(())\n}\n\nfn check_global_option(core: &mut ShellCore, args: &[String],\n                       name: &str, scope: usize) -> usize {\n    if arg::has_option(\"-g\", args) && scope != 0 {\n        let _ = core.db.unset(&name, None, false);\n        return 0;\n    }\n    scope\n}\n\nfn eval(core: &mut ShellCore, args: &[String], sub: &mut Substitution,\n        name: &str, scope: usize) -> Result<(), ExecError> {\n    if arg::has_option(\"-n\", args) {\n        sub.reset_nameref = true;\n    }\n\n    if sub.right_hand.is_some() {\n        return sub.eval(core, Some(scope), true);\n    }\n\n    let change_type = (!core.db.is_array(&name) && arg::has_option(\"-a\", args))\n                    || (!core.db.is_assoc(&name) && arg::has_option(\"-A\", args));\n\n    if !core.db.exist_l(&name, scope) || change_type {\n        sub.left_hand.init_variable(core, Some(scope), &mut args.to_vec())?;\n    }\n\n    Ok(())\n}\n\npub(super) fn exec(core: &mut ShellCore, sub: &mut Substitution, args: &[String],\n               scope: usize) -> Result<(), ExecError> {\n    let name = sub.left_hand.name.clone();\n    readonly_check(core, &name)?;\n\n    if arg::has_option(\"-n\", args) {\n        if sub.left_hand.index.is_some() \n        || core.db.is_array(&sub.left_hand.name)\n        || core.db.is_assoc(&sub.left_hand.name) {\n            return Err(ExecError::RefCannotBeArray(sub.left_hand.text.clone()));\n        }\n    }\n\n    array_to_element_check(sub)?;\n    let scope = check_global_option(core, args, &name, scope);\n\n    if ( arg::has_option(\"+i\", args) || arg::has_option(\"-n\", args) )\n    && core.db.has_flag_scope(&name, 'i', scope) {\n        core.db.int_to_str_type(&name, scope)?;\n    }\n\n    let arg_indicate_array = arg::has_option(\"-A\", args) || arg::has_option(\"-a\", args);\n\n    if arg_indicate_array && !core.db.exist(&name) && !core.db.exist_nameref(&name) {\n        sub.left_hand.init_variable(core, Some(scope), &mut args.to_vec())?;\n    }\n\n    if let Some(r) = sub.right_hand.as_mut() {\n        let right_is_array = [\"(\", \"'(\", \"\\\"(\"].iter().any(|e| r.text.starts_with(e));\n        if arg_indicate_array && right_is_array {\n            sub.left_hand.index = None;\n        }\n    }\n\n    let already_array = core.db.is_array(&name) || core.db.is_assoc(&name);\n    let subs_elem_quoted_string = match sub.right_hand.as_mut() {\n        Some(r) => sub.left_hand.index.is_some()\n                   && (r.text.starts_with(\"'\") || r.text.starts_with(\"\\\"\")),\n        _ => false,\n    };\n\n    if arg_indicate_array || (already_array && args[0] == \"declare\") {\n        sub.quoted = false;   //^ Bash bug???\n    }\n\n    let treat_as_export = core.db.has_flag(&name, 'x') || arg::has_option(\"-x\", args);\n    if sub.right_hand.is_some()\n        && already_array\n        && !subs_elem_quoted_string\n        && (!treat_as_export || arg_indicate_array) {\n        sub.reparse(core)?;\n    }\n\n    set_options_pre(core, &name, scope, args);\n    let res = eval(core, args, sub, &name, scope);\n    set_options_post(core, &name, scope, args);\n\n    if arg::has_option(\"-n\", args) {\n        if res.is_err() {\n            core.db.unset_nameref(&name, Some(scope))?;\n        }\n    }\n\n    res\n}\n"
  },
  {
    "path": "src/core/builtins/variable.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod print;\nmod set_value;\n\nuse crate::elements::substitution::Substitution;\nuse crate::error::exec::ExecError;\nuse crate::utils::arg;\nuse crate::{env, ShellCore};\n\npub fn local(core: &mut ShellCore, args: &[String], subs: &mut [Substitution]) -> i32 {\n    let args = args.to_owned();\n    let scope = if core.db.get_scope_num() > 2 {\n        core.db.get_scope_num() - 2 //The last element of data.parameters is for local itself.\n    } else {\n        let e = &ExecError::ValidOnlyInFunction;\n        return super::error(1, &args[0], e, core);\n    };\n\n    if core.shopts.query(\"localvar_inherit\") {\n        subs.into_iter().for_each(|e| e.localvar_inherit(core) );\n    }\n\n    for sub in subs.iter_mut() {\n        if let Err(e) = set_value::exec(core, sub, &args, scope) {\n            e.print(core);\n            return 1;\n        }\n    }\n\n    0\n}\n\npub fn declare(core: &mut ShellCore, args: &[String], subs: &mut [Substitution]) -> i32 {\n    let mut args = arg::dissolve_options(args);\n    arg::consume_arg(\"--\", &mut args);\n\n    if arg::has_option(\"-f\", &args) {\n        return print::f_option(core, &args, subs);\n    }else if subs.is_empty() {\n        return print::args_match(core, &mut args);\n    }else if arg::consume_arg(\"-p\", &mut args) { //declare -p hoge\n        let mut names = subs.iter().map(|s| s.text.clone()).collect();\n        return print::names_match(core, &mut names, &args);\n    }\n\n    let scope = core.db.get_scope_num() - 2;\n    for sub in subs {\n        if let Err(e) = set_value::exec(core, sub, &args, scope) {\n            return super::error(1, &args[0], &e, core);\n        }\n    }\n    0\n}\n\npub fn export(core: &mut ShellCore, args: &[String], subs: &mut [Substitution]) -> i32 {\n    let mut args = args.to_owned();\n    for sub in subs.iter_mut() {\n        let scope = core.db.get_scope_pos(&sub.left_hand.name).unwrap_or(0);\n        args.push(\"-x\".to_string());\n        if let Err(e) = set_value::exec(core, sub, &args, scope) {\n            e.print(core);\n            return 1;\n        }\n        match core.db.get_param(&sub.left_hand.name) {\n            Ok(v) => unsafe{env::set_var(&sub.left_hand.name, v)},\n            Err(e) => {\n                e.print(core);\n                return 1;\n            }\n        }\n    }\n    0\n}\n\npub fn readonly(core: &mut ShellCore, args: &[String], subs: &mut [Substitution]) -> i32 {\n    let args = arg::dissolve_options(args);\n\n    if subs.is_empty() {\n        let mut args = args.to_vec();\n        args.push(\"-r\".to_string()); \n        return print::args_match(core, &mut args);\n    }\n\n    for sub in subs {\n        if sub.left_hand.index.is_some() {\n            let e = ExecError::VariableInvalid(sub.left_hand.text.clone());\n            return super::error(1, &args[0], &e, core);\n        }\n\n        let scope = core.db.get_scope_pos(&sub.left_hand.name).unwrap_or(0);\n\n        if let Err(e) = set_value::exec(core, sub, &args, scope) {\n            return super::error(1, &args[0], &e, core);\n        }\n        core.db.set_flag(&sub.left_hand.name, 'r', scope);\n    }\n    0\n}\n"
  },
  {
    "path": "src/core/builtins.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod alias;\nmod caller;\nmod cd;\nmod command;\npub mod compgen;\npub mod complete;\nmod compopt;\nmod echo;\nmod exec;\nmod getopts;\nmod hash;\nmod history;\nmod job_commands;\nmod loop_control;\npub mod option;\npub mod variable;\nmod printf;\nmod pwd;\nmod read;\npub mod source;\nmod trap;\nmod type_;\n#[cfg(not(target_os = \"macos\"))]\nmod ulimit;\nmod unset;\n\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{exit, Feeder, Script, ShellCore};\nuse std::io::Write;\nuse std::process::Command;\n\npub fn error_(exit_status: i32, name: &str, msg: &str, core: &mut ShellCore) -> i32 {\n    let shellname = core.db.get_param(\"0\").unwrap();\n    if core.db.flags.contains('i') {\n        eprintln!(\"{}: {}: {}\", &shellname, name, msg);\n    } else {\n        let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n        eprintln!(\"{}: line {}: {}: {}\", &shellname, &lineno, name, msg);\n    }\n    exit_status\n}\n\npub fn error(exit_status: i32, name: &str, err: &ExecError, core: &mut ShellCore) -> i32 {\n    error_(exit_status, name, &String::from(err), core)\n}\n\npub fn run_external(core: &mut ShellCore, args: &[String], err_msg_cond: fn(i32) -> bool) -> i32 {\n    match Command::new(&args[0]).args(args[1..].to_vec()).output() {\n        Ok(com) => {\n            let exit_status = com.status.code().unwrap_or(127);\n            if ! com.stdout.is_empty() {\n                let _ = std::io::stdout().write_all(&com.stdout);\n            }\n            if ! err_msg_cond(exit_status) {\n                return exit_status;\n            }\n\n            let shellname = core.db.get_param(\"0\").unwrap();\n            eprint!(\"{}: \", &shellname);\n            if ! core.db.flags.contains('i') {\n                let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n                eprint!(\"line {}: \", &lineno);\n            }\n            let _ = std::io::stderr().write_all(&com.stderr);\n            exit_status\n        },\n        _ => 127\n    }\n}\n\nimpl ShellCore {\n    pub fn set_builtins(&mut self) {\n        self.builtins.insert(\":\".to_string(), true_);\n        self.builtins.insert(\"alias\".to_string(), alias::alias);\n        self.builtins.insert(\"bg\".to_string(), job_commands::bg);\n        self.builtins.insert(\"bind\".to_string(), bind);\n        self.builtins\n            .insert(\"break\".to_string(), loop_control::break_);\n        self.builtins\n            .insert(\"builtin\".to_string(), command::builtin);\n        self.builtins.insert(\"cd\".to_string(), cd::cd);\n        self.builtins.insert(\"caller\".to_string(), caller::caller);\n        self.builtins\n            .insert(\"command\".to_string(), command::command);\n        self.builtins\n            .insert(\"compgen\".to_string(), compgen::compgen);\n        self.builtins\n            .insert(\"complete\".to_string(), complete::complete);\n        self.builtins\n            .insert(\"compopt\".to_string(), compopt::compopt);\n        self.builtins\n            .insert(\"continue\".to_string(), loop_control::continue_);\n        self.builtins.insert(\"debug\".to_string(), debug);\n        self.builtins\n            .insert(\"disown\".to_string(), job_commands::disown);\n        self.builtins.insert(\"echo\".to_string(), echo::echo);\n        self.builtins.insert(\"eval\".to_string(), eval);\n        self.builtins.insert(\"exec\".to_string(), exec::exec);\n        self.builtins.insert(\"exit\".to_string(), exit);\n        self.builtins.insert(\"false\".to_string(), false_);\n        self.builtins.insert(\"fg\".to_string(), job_commands::fg);\n        self.builtins\n            .insert(\"getopts\".to_string(), getopts::getopts);\n        self.builtins.insert(\"hash\".to_string(), hash::hash);\n        self.builtins\n            .insert(\"history\".to_string(), history::history);\n        self.builtins.insert(\"jobs\".to_string(), job_commands::jobs);\n        self.builtins.insert(\"kill\".to_string(), job_commands::kill);\n        self.builtins.insert(\"let\".to_string(), let_);\n        self.builtins.insert(\"printf\".to_string(), printf::printf);\n        self.builtins.insert(\"pwd\".to_string(), pwd::pwd);\n        self.builtins.insert(\"read\".to_string(), read::read);\n        self.builtins\n            .insert(\"return\".to_string(), loop_control::return_);\n        self.builtins.insert(\"set\".to_string(), option::set);\n        self.builtins.insert(\"trap\".to_string(), trap::trap);\n        self.builtins.insert(\"type\".to_string(), type_::type_);\n        self.builtins.insert(\"shift\".to_string(), option::shift);\n        self.builtins.insert(\"shopt\".to_string(), option::shopt);\n\n        //if file::search_command(\"ulimit\").is_none() {\n        #[cfg(not(target_os = \"macos\"))]\n        self.builtins.insert(\"ulimit\".to_string(), ulimit::ulimit);\n        /*\n        #[cfg(target_os = \"macos\")]\n                    self.builtins.insert(\"ulimit\".to_string(), ulimit_mac::ulimit);\n                }*/\n\n        self.builtins.insert(\"unalias\".to_string(), alias::unalias);\n        self.builtins.insert(\"unset\".to_string(), unset::unset);\n        self.builtins.insert(\"source\".to_string(), source::source);\n        self.builtins.insert(\".\".to_string(), source::source);\n        self.builtins.insert(\"true\".to_string(), true_);\n        self.builtins.insert(\"test\".to_string(), test);\n        self.builtins.insert(\"[\".to_string(), test);\n        self.builtins.insert(\"wait\".to_string(), job_commands::wait);\n\n        self.subst_builtins\n            .insert(\"export\".to_string(), variable::export);\n        self.subst_builtins\n            .insert(\"readonly\".to_string(), variable::readonly);\n        self.subst_builtins\n            .insert(\"typeset\".to_string(), variable::declare);\n        self.subst_builtins\n            .insert(\"declare\".to_string(), variable::declare);\n        self.subst_builtins\n            .insert(\"local\".to_string(), variable::local);\n    }\n}\n\npub fn eval(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut args = args.to_owned();\n    args.remove(0);\n    if !args.is_empty() && args[0] == \"--\" {\n        args.remove(0);\n    }\n\n    let script = args.join(\" \");\n    let mut feeder = Feeder::new(&script);\n    let lineno = match core.db.get_param(\"LINENO\") {\n        Ok(s) => s.parse::<usize>().unwrap_or_default(),\n        _ => 0,\n    };\n    feeder.lineno += lineno - 1;\n\n    match Script::parse(&mut feeder, core, false) {\n        Ok(Some(mut s)) => {\n            core.eval_level += 1;\n            let _ = s.exec(core);\n            core.eval_level -= 1;\n        }\n        Err(ParseError::UnexpectedSymbol(t)) => {\n            let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"0\".to_string());\n            let com = &core.db.position_parameters[0][0];\n            eprintln!(\n                \"{}: eval: line {}: syntax error near unexpected token `{}'\",\n                com, &lineno, &t\n            );\n            eprintln!(\"{}: eval: line {}: `{}'\", com, &lineno, &script);\n            return 2;\n        }\n        Err(e) => e.print(core),\n        _ => {}\n    }\n\n    core.db.exit_status\n}\n\npub fn exit(core: &mut ShellCore, args: &[String]) -> i32 {\n    if core.db.flags.contains('i') {\n        eprintln!(\"exit\");\n    }\n    if args.len() > 1 {\n        match &args[1].parse::<i32>() {\n            Ok(n) => core.db.exit_status = *n,\n            _ => core.db.exit_status = 1,\n        }\n    }\n    exit::normal(core)\n}\n\npub fn false_(_: &mut ShellCore, _: &[String]) -> i32 {\n    1\n}\n\npub fn true_(_: &mut ShellCore, _: &[String]) -> i32 {\n    0\n}\n\npub fn bind(_: &mut ShellCore, _: &[String]) -> i32 {\n    0\n}\n\npub fn debug(_: &mut ShellCore, _: &[String]) -> i32 {\n//    let pos = core.db.get_scope_pos(\"words\").unwrap();\n//\n//    dbg!(\"{:?}\", &core.db.params.len());\n//    dbg!(\"{:?}\", &pos);\n//    dbg!(\"{:?}\", &core.db.params[pos].get(\"words\"));\n    0\n}\n\npub fn let_(core: &mut ShellCore, args: &[String]) -> i32 {\n    let mut last_result = 0;\n    core.valid_assoc_expand_once = true;\n\n    for a in &args[1..] {\n        match ArithmeticExpr::parse(&mut Feeder::new(&a.replace(\"$\", \"\\\\$\")), core, false, \"\") {\n            Ok(Some(mut a)) => match a.eval(core) {\n                Ok(s) => last_result = if s == \"0\" { 1 } else { 0 },\n                Err(e) => {\n                    core.valid_assoc_expand_once = false;\n                    return error(1, &args[0], &e, core);\n                }\n            },\n            Ok(None) => {\n                core.valid_assoc_expand_once = false;\n                return error_(1, &args[0], \"expression expected\", core);\n            }\n            Err(e) => {\n                core.valid_assoc_expand_once = false;\n                return error(1, &args[0], &From::from(e), core);\n            }\n        }\n    }\n\n    core.valid_assoc_expand_once = false;\n    last_result\n}\n\npub fn test(core: &mut ShellCore, args: &[String]) -> i32 {\n    /* difference between the builtin test and the external command */\n    if (args.len() == 5 && args[0] == \"[\" && args[4] == \"]\")\n    || (args.len() == 4 && args[0] == \"test\") {\n        if args[2] == \"=\" {\n            if args[1] == args[3] {\n                return 0;\n            }else if args[1] != args[3] {\n                return 1;\n            }\n        }\n    }\n\n    run_external(core, args, |es| es > 1)\n    /*\n    /* call the external test command */\n    match Command::new(&args[0]).args(args[1..].to_vec()).output() {\n        Ok(com) => {\n            let exit_status = com.status.code().unwrap_or(127);\n            if exit_status > 1 {\n                let msg = String::from_utf8(com.stderr).unwrap_or(\"\".to_string());\n                let shellname = core.db.get_param(\"0\").unwrap();\n                if core.db.flags.contains('i') {\n                    eprintln!(\"{}: {}\", &shellname, msg);\n                } else {\n                    let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n                    eprintln!(\"{}: line {}: {}\", &shellname, &lineno, msg);\n                }\n            }\n            exit_status\n        },\n        _ => 127\n    }*/\n}\n"
  },
  {
    "path": "src/core/completion.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse std::collections::HashMap;\n\n#[derive(Debug, Clone, Default)]\npub struct Completion {\n    pub entries: HashMap<String, CompletionEntry>,\n    pub current: CompletionEntry,\n    pub default_function: String,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct CompletionEntry {\n    pub function: String,\n    pub o_options: Vec<String>,\n    pub action: String,\n    pub options: HashMap<String, String>,\n    pub large_w_cands: String,\n}\n"
  },
  {
    "path": "src/core/database/data/array.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::{case_change, Data};\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse std::collections::HashMap;\n\n#[derive(Debug, Clone)]\npub struct ArrayData {\n    pub body: HashMap<usize, String>,\n    pub flags: String,\n}\n\nimpl From<Option<Vec<String>>> for ArrayData {\n    fn from(v: Option<Vec<String>>) -> Self {\n        let mut ans = Self::new();\n        v.unwrap().into_iter().enumerate().for_each(|(i, e)| {\n            ans.body.insert(i, e);\n        });\n        ans\n    }\n}\n\nimpl Data for ArrayData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        let mut formatted = \"(\".to_string();\n        for i in self.keys() {\n            let ansi = utils::to_ansi_c(&self.body[&i]);\n            if ansi == self.body[&i] {\n                formatted += &format!(\"[{}]=\\\"{}\\\" \", i, &ansi.replace(\"$\", \"\\\\$\"));\n            } else {\n                formatted += &format!(\"[{}]={} \", i, &ansi);\n            }\n        }\n        if formatted.ends_with(\" \") {\n            formatted.pop();\n        }\n        formatted += \")\";\n        formatted\n    }\n\n    fn clear(&mut self) {\n        self.body.clear();\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let mut value = value.to_string();\n        case_change(&self.flags, &mut value);\n        self.body.insert(0, value);\n        Ok(())\n    }\n\n    fn append_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let mut value = if let Some(v) = self.body.get(&0) {\n            v.to_owned() + value\n        } else {\n            value.to_string()\n        };\n\n        case_change(&self.flags, &mut value);\n        self.body.insert(0, value);\n        Ok(())\n    }\n\n    fn set_as_array(&mut self, name: &str, key: &str,\n                    value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let n = self.index_of(key)?;\n\n        let mut value = value.to_string();\n        case_change(&self.flags, &mut value);\n\n        self.body.insert(n, value);\n        Ok(())\n    }\n\n    fn append_to_array_elem(&mut self, name: &str, key: &str,\n                            value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let n = self.index_of(key)?;\n        let mut value = if let Some(v) = self.body.get(&n) {\n            v.to_owned() + value\n        } else {\n            value.to_string()\n        };\n\n        case_change(&self.flags, &mut value);\n        self.body.insert(n, value);\n        Ok(())\n    }\n\n    fn get_as_array(&mut self, key: &str, ifs: &str) -> Result<String, ExecError> {\n        if key == \"@\" {\n            return Ok(self.values().join(\" \"));\n        }\n        if key == \"*\" {\n            return Ok(self.values().join(ifs));\n        }\n\n        let n = self.index_of(key)?;\n        Ok(self.body.get(&n).unwrap_or(&\"\".to_string()).clone())\n    }\n\n    fn get_vec_from(&mut self, pos: usize, skip_non: bool) -> Result<Vec<String>, ExecError> {\n        if self.body.is_empty() {\n            return Ok(vec![]);\n        }\n\n        let keys = self.keys();\n        let max = *keys.iter().max().unwrap();\n        let mut ans = vec![];\n        for i in pos..(max + 1) {\n            match self.body.get(&i) {\n                Some(s) => ans.push(s.clone()),\n                None => {\n                    if !skip_non {\n                        ans.push(\"\".to_string());\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Ok(self.keys().iter().map(|k| k.to_string()).collect())\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        self.body\n            .get(&0)\n            .map(|v| Ok(v.clone()))\n            .ok_or(ExecError::Other(\"No entry\".to_string()))?\n    }\n\n    fn is_array(&self) -> bool {\n        true\n    }\n    fn len(&mut self) -> usize {\n        self.body.len()\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        let n = self.index_of(key)?;\n        Ok(self.body.contains_key(&n))\n    }\n\n    fn index_based_len(&mut self) -> usize {\n        match self.body.iter().map(|e| e.0).max() {\n            Some(n) => *n + 1,\n            None => 0,\n        }\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.len());\n        }\n\n        let n = self.index_of(key)?;\n        let s = self.body.get(&n).unwrap_or(&\"\".to_string()).clone();\n\n        Ok(s.chars().count())\n    }\n\n    fn remove_elem(&mut self, key: &str) -> Result<(), ExecError> {\n        if key == \"*\" || key == \"@\" {\n            self.body.clear();\n            return Ok(());\n        }\n\n        let index = self.index_of(key)?;\n        self.body.remove(&index);\n        Ok(())\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl ArrayData {\n    pub fn new() -> Self {\n        Self {\n            body: HashMap::new(),\n            flags: \"a\".to_string(),\n        }\n    }\n\n    pub fn values(&self) -> Vec<String> {\n        let mut keys: Vec<usize> = self.body.iter().map(|e| *e.0).collect();\n        keys.sort();\n        keys.iter().map(|i| self.body[i].clone()).collect()\n    }\n\n    pub fn keys(&self) -> Vec<usize> {\n        let mut keys: Vec<usize> = self.body.iter().map(|e| *e.0).collect();\n        keys.sort();\n        keys\n    }\n\n    fn index_of(&mut self, key: &str) -> Result<usize, ExecError> {\n        let mut index = match key.parse::<isize>() {\n            Ok(i) => i,\n            _ => return Err(ExecError::ArrayIndexInvalid(key.to_string())),\n        };\n\n        if index >= 0 {\n            return Ok(index as usize);\n        }\n\n        let keys = self.keys();\n        let max = match keys.iter().max() {\n            Some(n) => *n as isize,\n            None => -1,\n        };\n        index += max + 1;\n\n        if index < 0 {\n            return Err(ExecError::ArrayIndexInvalid(key.to_string()));\n        }\n\n        Ok(index as usize)\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/array_int.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::array::ArrayData;\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse std::collections::HashMap;\n\n#[derive(Debug, Clone)]\npub struct IntArrayData {\n    body: HashMap<usize, isize>,\n    pub flags: String,\n}\n\nimpl Data for IntArrayData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        let mut formatted = \"(\".to_string();\n        for i in self.keys() {\n            formatted += &format!(\"[{}]=\\\"{}\\\" \", i, &self.body[&i]);\n        }\n        if formatted.ends_with(\" \") {\n            formatted.pop();\n        }\n        formatted += \")\";\n        formatted\n    }\n\n    fn clear(&mut self) {\n        self.body.clear();\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        let n = self.index_of(key)?;\n        Ok(self.body.contains_key(&n))\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let n = super::to_int(value)?;\n        self.body.insert(0, n);\n        Ok(())\n    }\n\n    fn append_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let n = match value.parse::<isize>() {\n            Ok(n) => n,\n            Err(e) => return Err(ExecError::Other(e.to_string())),\n        };\n\n        if let Some(v) = self.body.get(&0) {\n            self.body.insert(0, v + n);\n        } else {\n            self.body.insert(0, n);\n        }\n        Ok(())\n    }\n\n    fn set_as_array(&mut self, name: &str, key: &str, value: &str)\n    -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let key = self.index_of(key)?;\n        let n = super::to_int(value)?;\n        self.body.insert(key, n);\n        Ok(())\n    }\n\n    fn append_to_array_elem(&mut self, name: &str, key: &str,\n                            value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let key = self.index_of(key)?;\n        let n = super::to_int(value)?;\n\n        if let Some(prev) = self.body.get(&key) {\n            self.body.insert(key, prev + n);\n        } else {\n            self.body.insert(key, n);\n        }\n        Ok(())\n    }\n\n    fn get_as_array(&mut self, key: &str, _: &str) -> Result<String, ExecError> {\n        if key == \"@\" {\n            return Ok(self.values().join(\" \"));\n        }\n        /*\n        if key == \"@\" {\n            return Ok(self.values().join(ifs));\n        }*/\n\n        let n = key\n            .parse::<usize>()\n            .map_err(|_| ExecError::ArrayIndexInvalid(key.to_string()))?;\n\n        Ok(self.body.get(&n).unwrap_or(&0).to_string())\n    }\n\n    fn get_all_as_array(&mut self, skip_none: bool) -> Result<Vec<String>, ExecError> {\n        if self.body.is_empty() {\n            return Ok(vec![]);\n        }\n\n        let keys = self.keys();\n        let max = *keys.iter().max().unwrap();\n        let mut ans = vec![];\n        for i in 0..(max + 1) {\n            match self.body.get(&i) {\n                Some(s) => ans.push(s.to_string()),\n                None => {\n                    if !skip_none {\n                        ans.push(\"\".to_string());\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn get_vec_from(&mut self, pos: usize, skip_non: bool) -> Result<Vec<String>, ExecError> {\n        if self.body.is_empty() {\n            return Ok(vec![]);\n        }\n\n        let keys = self.keys();\n        let max = *keys.iter().max().unwrap();\n        let mut ans = vec![];\n        for i in pos..(max + 1) {\n            match self.body.get(&i) {\n                Some(s) => ans.push(s.to_string()),\n                None => {\n                    if !skip_non {\n                        ans.push(\"\".to_string());\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Ok(self.keys().iter().map(|k| k.to_string()).collect())\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        self.body\n            .get(&0)\n            .map(|v| Ok(v.to_string()))\n            .ok_or(ExecError::Other(\"No entry\".to_string()))?\n    }\n\n    fn get_str_type(&self) -> Box<dyn Data> {\n        let mut hash = HashMap::new();\n        for d in &self.body {\n            hash.insert(*d.0, d.1.to_string());\n        }\n\n        let mut str_d = ArrayData {\n            body: hash,\n            flags: self.flags.clone(),\n        };\n        str_d.unset_flag('i');\n\n        Box::new(str_d)\n    }\n\n    fn is_array(&self) -> bool {\n        true\n    }\n    fn len(&mut self) -> usize {\n        self.body.len()\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.len());\n        }\n\n        let n = key\n            .parse::<usize>()\n            .map_err(|_| ExecError::ArrayIndexInvalid(key.to_string()))?;\n        let s = self.body.get(&n).unwrap_or(&0).to_string();\n\n        Ok(s.chars().count())\n    }\n\n    fn remove_elem(&mut self, key: &str) -> Result<(), ExecError> {\n        if key == \"*\" || key == \"@\" {\n            self.body.clear();\n            return Ok(());\n        }\n\n        if let Ok(n) = key.parse::<usize>() {\n            self.body.remove(&n);\n            return Ok(());\n        }\n        Err(ExecError::Other(\"invalid index\".to_string()))\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        if flag == 'i' {\n            return true;\n        }\n        self.flags.contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl IntArrayData {\n    pub fn new() -> Self {\n        Self {\n            body: HashMap::new(),\n            flags: \"ai\".to_string(),\n        }\n    }\n\n    pub fn values(&self) -> Vec<String> {\n        let mut keys: Vec<usize> = self.body.iter().map(|e| *e.0).collect();\n        keys.sort();\n        keys.iter().map(|i| self.body[i].to_string()).collect()\n    }\n\n    pub fn keys(&self) -> Vec<usize> {\n        let mut keys: Vec<usize> = self.body.iter().map(|e| *e.0).collect();\n        keys.sort();\n        keys\n    }\n\n    fn index_of(&mut self, key: &str) -> Result<usize, ExecError> {\n        let mut index = match key.parse::<isize>() {\n            Ok(i) => i,\n            _ => return Err(ExecError::ArrayIndexInvalid(key.to_string())),\n        };\n\n        if index >= 0 {\n            return Ok(index as usize);\n        }\n\n        let keys = self.keys();\n        let max = match keys.iter().max() {\n            Some(n) => *n as isize,\n            None => -1,\n        };\n        index += max + 1;\n\n        if index < 0 {\n            return Err(ExecError::ArrayIndexInvalid(key.to_string()));\n        }\n\n        Ok(index as usize)\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/array_ondemand.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::utils;\nuse super::{Data, ExecError};\n\n#[derive(Debug, Clone)]\npub struct OnDemandArray {\n    pub values: fn() -> Vec<String>,\n    pub flags: String,\n}\n\nimpl Data for OnDemandArray {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        return \"*********\".to_string()\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        let mut formatted = \"(\".to_string();\n        for (i, v) in (self.values)().into_iter().enumerate() {\n            let ansi = utils::to_ansi_c(&v);\n            if ansi == v {\n                formatted += &format!(\"[{}]=\\\"{}\\\" \", i, &ansi.replace(\"$\", \"\\\\$\"));\n            } else {\n                formatted += &format!(\"[{}]={} \", i, &ansi);\n            }\n        }\n        if formatted.ends_with(\" \") {\n            formatted.pop();\n        }\n        formatted += \")\";\n        formatted\n    }\n\n    fn get_as_array(&mut self, key: &str, ifs: &str) -> Result<String, ExecError> {\n        if key == \"@\" {\n            return Ok((self.values)().join(\" \"));\n        }\n        if key == \"*\" {\n            return Ok((self.values)().join(ifs));\n        }\n\n        let index = self.index_of(key)?;\n        let vs = (self.values)();\n        if index < vs.len() {\n            return Ok(vs[index].clone());\n        }\n\n        Ok(\"\".to_string())\n    }\n\n    fn get_vec_from(&mut self, pos: usize, _: bool) -> Result<Vec<String>, ExecError> {\n        let vs = (self.values)();\n        if pos < vs.len() {\n            return Ok(vs[pos..].to_vec());\n        }\n\n        Ok(vec![])\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        let num = (self.values)().len();\n        Ok((0..num).map(|k| k.to_string()).collect())\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        let vs = (self.values)();\n        if vs.is_empty() {\n            Ok(\"\".to_string())\n        }else{\n            Ok(vs[0].clone())\n        }\n    }\n\n    fn is_array(&self) -> bool {\n        true\n    }\n    fn len(&mut self) -> usize {\n        (self.values)().len()\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n\n        let n = self.index_of(key)?;\n        Ok(n < (self.values)().len())\n    }\n\n    fn index_based_len(&mut self) -> usize {\n        self.len()\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.len());\n        }\n\n        let n = self.index_of(key)?;\n        let vs = (self.values)();\n\n        if n < vs.len() {\n            return Ok(vs[n].to_string().len());\n        }\n\n        Ok(0)\n    }\n\n    fn remove_elem(&mut self, _: &str) -> Result<(), ExecError> {\n        Ok(())\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        self.flags.contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl OnDemandArray {\n    pub fn new(values: fn() -> Vec<String>) -> Self {\n        Self {\n            values: values,\n            flags: \"a\".to_string(),\n        }\n    }\n\n    fn index_of(&mut self, key: &str) -> Result<usize, ExecError> {\n        let index = match key.parse::<isize>() {\n            Ok(i) => i,\n            _ => return Err(ExecError::ArrayIndexInvalid(key.to_string())),\n        };\n\n        Ok(index as usize)\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/assoc.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::{case_change, Data};\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse std::collections::HashMap;\n\n#[derive(Debug, Clone)]\npub struct AssocData {\n    body: HashMap<String, String>,\n    last: Option<String>,\n    pub flags: String,\n}\n\nimpl From<HashMap<String, String>> for AssocData {\n    fn from(hm: HashMap<String, String>) -> Self {\n        Self {\n            body: hm,\n            last: None,\n            flags: \"A\".to_string(),\n        }\n    }\n}\n\nimpl Data for AssocData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        let mut formatted = String::new();\n        formatted += \"(\";\n        for k in self.keys() {\n            let v = &self.get(&k).unwrap_or(\"\".to_string());\n            let mut ansi = utils::to_ansi_c(v);\n            if ansi == *v {\n                ansi = format!(\"\\\"{}\\\"\", &ansi);\n            }\n\n            let k = utils::to_ansi_c(&k);\n            formatted += &format!(\"[{}]={} \", k, &ansi);\n        }\n        formatted += \")\";\n        formatted\n    }\n\n    fn clear(&mut self) {\n        self.body.clear();\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let mut value = value.to_string();\n        case_change(&self.flags, &mut value);\n\n        self.body.insert(\"0\".to_string(), value);\n        Ok(())\n    }\n\n    fn set_as_assoc(&mut self, name: &str, key: &str,\n                    value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let mut value = value.to_string();\n        case_change(&self.flags, &mut value);\n\n        self.body.insert(key.to_string(), value.clone());\n        self.last = Some(value);\n        Ok(())\n    }\n\n    fn append_to_assoc_elem(&mut self, name: &str, key: &str,\n                            value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let mut value = if let Some(v) = self.body.get(key) {\n            v.to_owned() + value\n        } else {\n            value.to_string()\n        };\n\n        case_change(&self.flags, &mut value);\n        self.body.insert(key.to_string(), value.clone());\n        self.last = Some(value);\n        Ok(())\n    }\n\n    fn get_as_assoc(&mut self, key: &str) -> Result<String, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.values().join(\" \"));\n        }\n\n        match self.body.get(key) {\n            Some(s) => Ok(s.to_string()),\n            None => Err(ExecError::ArrayIndexInvalid(key.to_string())),\n        }\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        if let Some(s) = self.body.get(\"0\") {\n            return Ok(s.to_string());\n        }\n\n        self.last\n            .clone()\n            .ok_or(ExecError::Other(\"No last input\".to_string()))\n    }\n\n    fn is_assoc(&self) -> bool {\n        true\n    }\n    fn len(&mut self) -> usize {\n        self.body.len()\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        Ok(self.body.contains_key(key))\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.len());\n        }\n\n        let s = self.body.get(key).unwrap_or(&\"\".to_string()).clone();\n\n        Ok(s.chars().count())\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Ok(self.keys().clone())\n    }\n\n    fn get_all_as_array(&mut self, skip_none: bool) -> Result<Vec<String>, ExecError> {\n        if self.body.is_empty() {\n            return Ok(vec![]);\n        }\n\n        let mut keys = self.keys();\n        keys.sort();\n        let mut ans = vec![];\n        for i in keys {\n            match self.body.get(&i) {\n                Some(s) => ans.push(s.clone()),\n                None => {\n                    if !skip_none {\n                        ans.push(\"\".to_string());\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn get_vec_from(&mut self, _: usize, skip_non: bool) -> Result<Vec<String>, ExecError> {\n        self.get_all_as_array(skip_non)\n    }\n\n    fn remove_elem(&mut self, key: &str) -> Result<(), ExecError> {\n        if key == \"*\" || key == \"@\" {\n            //     self.body.clear();\n            return Ok(());\n        }\n\n        self.body.remove(key);\n        Ok(())\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        self.flags.contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl AssocData {\n    pub fn new() -> Self {\n        Self {\n            body: HashMap::new(),\n            last: None,\n            flags: \"A\".to_string(),\n        }\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        self.body.get(key).cloned()\n    }\n\n    pub fn keys(&self) -> Vec<String> {\n        let mut keys: Vec<String> = self.body.iter().map(|e| e.0.clone()).collect();\n        keys.sort();\n        keys\n    }\n\n    pub fn values(&self) -> Vec<String> {\n        self.body.iter().map(|e| e.1.clone()).collect()\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/assoc_int.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::assoc::AssocData;\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse std::collections::HashMap;\n\n#[derive(Debug, Clone, Default)]\npub struct IntAssocData {\n    body: HashMap<String, isize>,\n    last: Option<String>,\n    pub flags: String,\n}\n\nimpl Data for IntAssocData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        let mut formatted = String::new();\n        formatted += \"(\";\n        for k in self.keys() {\n            let v = &self.get(&k).unwrap_or(\"\".to_string());\n            let mut ansi = utils::to_ansi_c(v);\n            if ansi == *v {\n                ansi = format!(\"\\\"{}\\\"\", &ansi);\n            }\n\n            let k = utils::to_ansi_c(&k);\n            formatted += &format!(\"[{}]={} \", k, &ansi);\n        }\n\n        formatted += \")\";\n        formatted\n    }\n\n    fn clear(&mut self) {\n        self.body.clear();\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let n = super::to_int(value)?;\n        self.body.insert(\"0\".to_string(), n);\n        Ok(())\n    }\n\n    fn set_as_assoc(&mut self, name: &str, key: &str,\n                    value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let n = super::to_int(value)?;\n        self.body.insert(key.to_string(), n);\n        self.last = Some(value.to_string());\n        Ok(())\n    }\n\n    fn append_to_assoc_elem(&mut self, name: &str, key: &str,\n                            value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        let n = super::to_int(value)?;\n\n        if let Some(v) = self.body.get(key) {\n            self.body.insert(key.to_string(), v + n);\n        } else {\n            self.body.insert(key.to_string(), n);\n        }\n        self.last = Some(value.to_string());\n        Ok(())\n    }\n\n    fn get_as_assoc(&mut self, key: &str) -> Result<String, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.values().join(\" \"));\n        }\n\n        match self.body.get(key) {\n            Some(s) => Ok(s.to_string()),\n            None => Err(ExecError::ArrayIndexInvalid(key.to_string())),\n        }\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        if let Some(s) = self.body.get(\"0\") {\n            return Ok(s.to_string());\n        }\n\n        self.last\n            .clone()\n            .ok_or(ExecError::Other(\"No last input\".to_string()))\n    }\n\n    fn get_str_type(&self) -> Box<dyn Data> {\n        let mut hash = HashMap::new();\n        for d in &self.body {\n            hash.insert(d.0.to_string(), d.1.to_string());\n        }\n\n        let mut new_d = AssocData::from(hash);\n        new_d.flags = self.flags.clone();\n        new_d.unset_flag('i');\n        Box::new(new_d)\n    }\n\n    fn is_assoc(&self) -> bool {\n        true\n    }\n    fn len(&mut self) -> usize {\n        self.body.len()\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        Ok(self.body.contains_key(key))\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(self.len());\n        }\n\n        let s = *self.body.get(key).unwrap_or(&0);\n\n        Ok(s.to_string().len())\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Ok(self.keys().clone())\n    }\n\n    fn get_all_as_array(&mut self, skip_none: bool) -> Result<Vec<String>, ExecError> {\n        if self.body.is_empty() {\n            return Ok(vec![]);\n        }\n\n        let mut keys = self.keys();\n        keys.sort();\n        let mut ans = vec![];\n        for i in keys {\n            match self.body.get(&i) {\n                Some(s) => ans.push(s.to_string()),\n                None => {\n                    if !skip_none {\n                        ans.push(\"\".to_string());\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn get_vec_from(&mut self, _: usize, skip_non: bool) -> Result<Vec<String>, ExecError> {\n        self.get_all_as_array(skip_non)\n    }\n\n    fn remove_elem(&mut self, key: &str) -> Result<(), ExecError> {\n        if key == \"*\" || key == \"@\" {\n            //     self.body.clear();\n            return Ok(());\n        }\n\n        self.body.remove(key);\n        Ok(())\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        if flag == 'i' {\n            return true;\n        }\n        self.flags.contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl IntAssocData {\n    pub fn new() -> Self {\n        Self { body: HashMap::new(), last: None, flags: \"i\".to_string() }\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        Some(self.body.get(key).unwrap_or(&0).to_string())\n    }\n\n    pub fn keys(&self) -> Vec<String> {\n        let mut keys: Vec<String> = self.body.iter().map(|e| e.0.clone()).collect();\n        keys.sort();\n        keys\n        //self.body.iter().map(|e| e.0.clone()).collect()\n    }\n\n    pub fn values(&self) -> Vec<String> {\n        self.body.iter().map(|e| e.1.to_string()).collect()\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/random.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse rand_chacha::rand_core::RngCore;\nuse rand_chacha::rand_core::SeedableRng;\nuse rand_chacha::ChaCha20Rng;\n\n#[derive(Debug, Clone)]\npub struct RandomVar {\n    rng: ChaCha20Rng,\n    flags: String, \n}\n\nimpl Data for RandomVar {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        \"\".to_string()\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self.get_as_single().unwrap()\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        let rand = self.rng.next_u32() & 0x7FFF;\n        Ok(rand.to_string())\n    }\n\n    fn len(&mut self) -> usize {\n        self.get_as_single().unwrap().len()\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        let seed = value.parse::<u64>().unwrap_or(0);\n        self.rng = ChaCha20Rng::seed_from_u64(seed + 4011); //4011: for bash test\n        Ok(())\n    }\n\n    fn is_special(&self) -> bool {\n        true\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl RandomVar {\n    pub fn new() -> Self {\n        Self {\n            rng: ChaCha20Rng::seed_from_u64(0),\n            flags: \"i\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/seconds.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse crate::utils::clock;\nuse std::time::Duration;\n\n#[derive(Debug, Clone)]\npub struct Seconds {\n    origin: Duration,\n    shift: isize,\n    flags: String,\n}\n\nimpl Data for Seconds {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        \"\".to_string() //TODO\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        let elapsed = clock::monotonic_time() - self.origin;\n        let ans = format!(\"{}\", elapsed.as_secs() as isize + self.shift);\n\n        Ok(ans)\n    }\n\n    fn len(&mut self) -> usize {\n        0\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        self.shift = value.parse::<isize>().unwrap_or(0);\n        self.origin = clock::monotonic_time();\n        Ok(())\n    }\n\n    fn is_special(&self) -> bool {\n        true\n    }\n    fn is_single_num(&self) -> bool {\n        true\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl Seconds {\n    pub fn new() -> Self {\n        Self {\n            origin: clock::monotonic_time(),\n            shift: 0,\n            flags: \"i\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/single.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::{case_change, Data};\nuse crate::error::exec::ExecError;\nuse crate::utils;\n\n#[derive(Debug, Clone)]\npub struct SingleData {\n    pub body: String,\n    pub flags: String,\n}\n\nimpl From<&str> for SingleData {\n    fn from(s: &str) -> Self {\n        Self {\n            body: s.to_string(),\n            flags: String::new(),\n        }\n    }\n}\n\nimpl Data for SingleData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        let mut s = self.body.replace(\"'\", \"\\\\'\");\n        if s.contains('~') || s.starts_with('#') {\n            s = \"'\".to_owned() + &s + \"'\";\n        }\n        let ansi = utils::to_ansi_c(&s);\n        if ansi == s {\n            ansi.replace(\"$\", \"\\\\$\")\n        } else {\n            ansi\n        }\n    }\n\n    fn clear(&mut self) {\n        self.body.clear();\n    }\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        self.nameref_check(name, value)?;\n\n        /*\n        if self.has_flag('n') {\n            if value.contains('[') {\n                let splits: Vec<&str> = value.split('[').collect();\n                if ! utils::is_var(&splits[0]) || ! splits[1].ends_with(']') {\n                        return Err(ExecError::InvalidNameRef(value.to_string()));\n                }\n\n                if name == splits[0] {\n                        return Err(ExecError::SelfRef(name.to_string()));\n                }\n            }else if value == \"\" {\n            }else if ! utils::is_var(value) {\n                return Err(ExecError::InvalidNameRef(value.to_string()));\n            }else if name == value {\n                return Err(ExecError::SelfRef(name.to_string()));\n            }\n        }*/\n\n        self.body = value.to_string();\n        case_change(&self.flags, &mut self.body);\n        Ok(())\n    }\n\n    fn append_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        self.body += value;\n        case_change(&self.flags, &mut self.body);\n        Ok(())\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        Ok(self.body.to_string())\n    }\n\n    fn len(&mut self) -> usize {\n        self.body.chars().count()\n    }\n\n    fn is_single(&self) -> bool {\n        true\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        Ok(key == \"0\")\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl SingleData {\n    pub fn new(flags: &str) -> Self {\n        Self {\n            body: \"\".to_string(),\n            flags: flags.to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/single_int.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::single::SingleData;\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse crate::utils;\n\n#[derive(Debug, Clone)]\npub struct IntData {\n    pub body: isize,\n    pub flags: String,\n}\n\nimpl Data for IntData {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n    fn _get_fmt_string(&self) -> String {\n        utils::to_ansi_c(&self.body.to_string())\n    }\n\n    fn clear(&mut self) {}\n\n    fn set_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        match value.parse::<isize>() {\n            Ok(n) => self.body = n,\n            Err(e) => {\n                return Err(ExecError::Other(e.to_string()));\n            }\n        }\n        Ok(())\n    }\n\n    fn append_as_single(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n\n        match value.parse::<isize>() {\n            Ok(n) => self.body += n,\n            Err(e) => {\n                return Err(ExecError::Other(e.to_string()));\n            }\n        }\n        Ok(())\n    }\n\n    fn init_as_num(&mut self) -> Result<(), ExecError> {\n        self.body = 0;\n        Ok(())\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        Ok(self.body.to_string())\n    }\n    fn get_as_single_num(&mut self) -> Result<isize, ExecError> {\n        Ok(self.body)\n    }\n\n    fn get_str_type(&self) -> Box<dyn Data> {\n        let mut d = SingleData::from(self.body.to_string().as_ref());\n        d.flags = self.flags.clone();\n        let _ = d.unset_flag('i');\n        Box::new(d)\n    }\n\n    fn len(&mut self) -> usize {\n        self.body.to_string().len()\n    }\n    fn is_single(&self) -> bool {\n        true\n    }\n    fn is_single_num(&self) -> bool {\n        true\n    }\n\n    fn has_key(&mut self, key: &str) -> Result<bool, ExecError> {\n        if key == \"@\" || key == \"*\" {\n            return Ok(true);\n        }\n        Ok(key == \"0\")\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        if flag == 'i' {\n            return true;\n        }\n        self.flags.contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl IntData {\n    pub fn new() -> Self {\n        Self {\n            body: 0,\n            flags: \"i\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/single_ondemand.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::{Data, ExecError};\n\n#[derive(Debug, Clone)]\npub struct OnDemandSingle {\n    value: fn() -> String,\n    flags: String,\n}\n\nimpl Data for OnDemandSingle {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n    \n    fn get_fmt_string(&mut self) -> String {\n        self.get_as_single().unwrap()\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        Ok((self.value)())\n    }\n\n    fn set_as_single(&mut self, name: &str, _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)\n    }\n\n    fn len(&mut self) -> usize {\n        (self.value)().chars().count()\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl OnDemandSingle {\n    pub fn new(timefn: fn() -> String) -> Self {\n        Self {\n            value: timefn,\n            flags: \"\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/srandom.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse rand_chacha::rand_core::{RngCore, SeedableRng};\nuse rand_chacha::ChaCha20Rng;\n\n#[derive(Debug, Clone)]\npub struct SRandomVar {\n    rng: ChaCha20Rng,\n    prev: String,\n    flags: String,\n}\n\nimpl Data for SRandomVar {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n\n    fn _get_fmt_string(&self) -> String {\n        self.prev.clone()\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        let rand = self.rng.next_u32();\n        self.prev = rand.to_string();\n        Ok(self.prev.clone())\n    }\n\n    fn len(&mut self) -> usize {\n        self.prev.len()\n    }\n\n    fn is_special(&self) -> bool {\n        true\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl SRandomVar {\n    pub fn new() -> Self {\n        Self {\n            rng: ChaCha20Rng::from_os_rng(),\n            prev: \"\".to_string(),\n            flags: \"i\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data/uninit.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::Data;\nuse crate::error::exec::ExecError;\nuse super::super::{\n    ArrayData, AssocData, IntArrayData, IntAssocData, IntData, SingleData\n};\n\n#[derive(Debug, Clone)]\npub struct Uninit {\n    flags: String,\n}\n\nimpl Data for Uninit {\n    fn boxed_clone(&self) -> Box<dyn Data> {\n        Box::new(self.clone())\n    }\n    fn _get_fmt_string(&self) -> String {\n        \"\".to_string()\n    }\n\n    fn initialize(&mut self) -> Option<Box<dyn Data>> {\n        let num = self.has_flag('i');\n\n        if self.has_flag('a') {\n            match num {\n                true => {\n                    let mut d = IntArrayData::new();\n                    d.flags = self.flags.clone();\n                    return Some(Box::new(d));\n                },\n                false => {\n                    let mut d = ArrayData::new();\n                    d.flags = self.flags.clone();\n                    return Some(Box::new(d));\n                },\n            }\n        }\n\n        if self.has_flag('A') {\n            match num {\n                true => {\n                    let mut d = IntAssocData::new();\n                    d.flags = self.flags.clone();\n                    return Some(Box::new(d));\n                },\n                false => {\n                    let mut d = AssocData::new();\n                    d.flags = self.flags.clone();\n                    return Some(Box::new(d));\n                },\n            }\n        }\n\n        match num {\n            true => {\n                let mut d = IntData::new();\n                d.flags = self.flags.clone();\n                Some(Box::new(d))\n            },\n            false => {\n                let d = SingleData::new(&self.flags);\n                Some(Box::new(d))\n            },\n        }\n    }\n\n    fn clear(&mut self) {}\n    fn is_initialized(&self) -> bool {\n        false\n    }\n    fn get_as_array(&mut self, _: &str, _: &str) -> Result<String, ExecError> {\n        Ok(\"\".to_string())\n    }\n    fn get_all_as_array(&mut self, _: bool) -> Result<Vec<String>, ExecError> {\n        Ok(vec![])\n    }\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Ok(vec![])\n    }\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        Ok(\"\".to_string())\n    }\n    fn len(&mut self) -> usize {\n        0\n    }\n    fn elem_len(&mut self, _: &str) -> Result<usize, ExecError> {\n        Ok(0)\n    }\n    fn remove_elem(&mut self, _: &str) -> Result<(), ExecError> {\n        Ok(())\n    }\n\n    fn set_flag(&mut self, flag: char) {\n        if ! self.flags.contains(flag) {\n            self.flags.push(flag);\n        }\n    }\n\n    fn unset_flag(&mut self, flag: char) {\n        self.flags.retain(|e| e != flag);\n    }\n\n    fn is_assoc(&self) -> bool {\n        self.flags.contains('A')\n    }\n\n    fn is_array(&self) -> bool {\n        self.flags.contains('a')\n    }\n\n    fn get_flags(&mut self) -> &str {\n        &self.flags\n    }\n}\n\nimpl Uninit {\n    pub fn new(flags: &str) -> Self {\n        Self { flags: flags.to_string() }\n    }\n}\n"
  },
  {
    "path": "src/core/database/data.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\npub mod array;\npub mod array_int;\npub mod assoc;\npub mod assoc_int;\npub mod array_ondemand;\npub mod random;\npub mod seconds;\npub mod single;\npub mod single_int;\npub mod single_ondemand;\npub mod srandom;\npub mod uninit;\n\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse std::fmt;\nuse std::fmt::Debug;\nuse crate::utils;\n\nfn to_int(s: &str) -> Result<isize, ExecError> {\n    match s.parse::<isize>() {\n        Ok(n) => Ok(n),\n        Err(e) => Err(ArithError::OperandExpected(e.to_string()).into()),\n    }\n}\n\nfn case_change(flags: &str, text: &mut String) {\n    if flags.contains('l') {\n        *text = text.to_lowercase();\n    }else if flags.contains('u') {\n        *text = text.to_uppercase();\n    }\n}\n\nimpl Debug for dyn Data {\n    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {\n        fmt.debug_struct(&self._get_fmt_string()).finish()\n    }\n}\n\nimpl Clone for Box<dyn Data> {\n    fn clone(&self) -> Box<dyn Data> {\n        self.boxed_clone()\n    }\n}\n\npub trait Data {\n    fn boxed_clone(&self) -> Box<dyn Data>;\n\n    fn _get_fmt_string(&self) -> String {\n        \"********\".to_string()\n    }\n\n    fn get_fmt_string(&mut self) -> String {\n        self._get_fmt_string()\n    }\n\n    fn get_str_type(&self) -> Box<dyn Data> {\n        self.boxed_clone()\n    }\n\n    fn is_initialized(&self) -> bool {\n        true\n    }\n\n    fn initialize(&mut self) -> Option<Box<dyn Data>> {\n        None\n    }\n\n    fn has_key(&mut self, _: &str) -> Result<bool, ExecError> {\n        Ok(false)\n    }\n\n    fn clear(&mut self) {}\n\n    fn set_as_single(&mut self, name: &str,  _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)\n    }\n\n    fn append_as_single(&mut self, name: &str, _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)\n    }\n    fn get_as_single_num(&mut self) -> Result<isize, ExecError> {\n        Err(ExecError::Other(\"not a single variable\".to_string()))\n    }\n\n    fn set_as_array(&mut self, name: &str, _: &str, _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        Err(ExecError::Other(\"not an array\".to_string()))\n    }\n    fn append_to_array_elem(&mut self, name: &str, _: &str,\n                            _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        Err(ExecError::Other(\"not an array\".to_string()))\n    }\n\n    fn set_as_assoc(&mut self, name: &str, _: &str, _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        Err(ExecError::Other(\"not an associative table\".to_string()))\n    }\n    fn append_to_assoc_elem(&mut self, name: &str, _: &str,\n                            _: &str) -> Result<(), ExecError> {\n        self.readonly_check(name)?;\n        Err(ExecError::Other(\"not an associative table\".to_string()))\n    }\n\n    fn get_as_single(&mut self) -> Result<String, ExecError> {\n        Err(ExecError::Other(\"not a single variable\".to_string()))\n    }\n    fn get_as_array(&mut self, key: &str, _: &str) -> Result<String, ExecError> {\n        //TODO: change to ArithError\n        let err = ArithError::OperandExpected(key.to_string());\n        Err(ExecError::ArithError(key.to_string(), err))\n    }\n    fn get_as_assoc(&mut self, key: &str) -> Result<String, ExecError> {\n        let err = ArithError::OperandExpected(key.to_string());\n        Err(ExecError::ArithError(key.to_string(), err))\n    }\n\n    fn get_as_array_or_assoc(&mut self, pos: &str, ifs: &str) -> Result<String, ExecError> {\n        if self.is_assoc() {\n            return match self.get_as_assoc(pos) {\n                Ok(d) => Ok(d),\n                Err(_) => Ok(\"\".to_string()),\n            };\n        }\n        if self.is_array() {\n            return match self.get_as_array(pos, ifs) {\n                Ok(d) => Ok(d),\n                Err(_) => Ok(\"\".to_string()),\n            };\n        }\n\n        if pos == \"0\" || pos == \"*\" || pos == \"@\" {\n            match self.get_as_single() {\n                Ok(d) => Ok(d),\n                Err(_) => Ok(\"\".to_string()),\n            }\n        } else {\n            Ok(\"\".to_string())\n        }\n    }\n\n    fn get_all_as_array(&mut self, flatten: bool) -> Result<Vec<String>, ExecError> {\n        self.get_vec_from(0, flatten)\n    }\n\n    fn get_vec_from(&mut self, _: usize, _: bool) -> Result<Vec<String>, ExecError> {\n        Err(ExecError::Other(\"not an array\".to_string()))\n    }\n\n    fn get_all_indexes_as_array(&mut self) -> Result<Vec<String>, ExecError> {\n        Err(ExecError::Other(\"not an array\".to_string()))\n    }\n\n    fn is_special(&self) -> bool {\n        false\n    }\n    fn is_single(&self) -> bool {\n        false\n    }\n    fn is_single_num(&self) -> bool {\n        false\n    }\n    fn is_assoc(&self) -> bool {\n        false\n    }\n    fn is_array(&self) -> bool {\n        false\n    }\n    fn len(&mut self) -> usize;\n    fn index_based_len(&mut self) -> usize {\n        self.len()\n    }\n\n    fn elem_len(&mut self, key: &str) -> Result<usize, ExecError> {\n        match key {\n            \"0\" => Ok(self.len()),\n            _ => Ok(0),\n        }\n    }\n\n    fn init_as_num(&mut self) -> Result<(), ExecError> {\n        Err(ExecError::Other(\"Undefined call init_as_num\".to_string()))\n    }\n\n    fn remove_elem(&mut self, _: &str) -> Result<(), ExecError> {\n        Err(ExecError::Other(\"Undefined call remove_elem\".to_string()))\n    }\n\n    fn readonly_check(&mut self, name: &str) -> Result<(), ExecError> {\n        if self.has_flag('r') {\n            return Err(ExecError::VariableReadOnly(name.to_string()));\n        }\n        Ok(())\n    }\n\n    fn set_flag(&mut self, _: char) {}\n\n    fn unset_flag(&mut self, _: char) {}\n\n    fn has_flag(&mut self, flag: char) -> bool {\n        self.get_flags().contains(flag)\n    }\n\n    fn get_flags(&mut self) -> &str;\n\n    fn nameref_check(&mut self, name: &str, value: &str) -> Result<(), ExecError> {\n        if ! self.has_flag('n') {\n            return Ok(());\n        }\n\n        if value.contains('[') {\n            let splits: Vec<&str> = value.split('[').collect();\n            if ! utils::is_var(&splits[0]) || ! splits[1].ends_with(']') {\n                    return Err(ExecError::InvalidNameRef(value.to_string()));\n            }\n\n            if name == splits[0] {\n                    return Err(ExecError::SelfRef(name.to_string()));\n            }\n        }else if value == \"\" {\n        }else if ! utils::is_var(value) {\n            return Err(ExecError::InvalidNameRef(value.to_string()));\n        }else if name == value {\n            return Err(ExecError::SelfRef(name.to_string()));\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_appenders.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse std::env;\nuse super::SingleData;\nuse crate::core::DataBase;\nuse crate::error::exec::ExecError;\n\nimpl DataBase {\n    pub fn append_param(\n        &mut self,\n        name: &str,\n        val: &str,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        /*\n        if let Some(nameref) = self.get_nameref(name)? {\n            return self.set_param(&nameref, val, scope);\n        }*/\n\n        if name == \"BASH_ARGV0\" {\n            let n = scope.unwrap_or(self.get_scope_num() - 1);\n            self.position_parameters[n][0] += val;\n        }\n\n        if !self.flags.contains('r')\n            && (self.flags.contains('a') || self.has_flag(name, 'x'))\n            && env::var(name).is_err()\n        {\n            unsafe{env::set_var(name, \"\")};\n        }\n\n        let scope = self.get_target_scope(name, scope);\n\n        if self.params[scope].get(name).is_none() {\n            self.set_entry(scope, name, Box::new(SingleData::from(\"\")))?;\n        }\n\n        let d = self.params[scope].get_mut(name).unwrap();\n        if d.is_array() {\n            return d.append_to_array_elem(name, \"0\", val);\n        }\n\n        d.append_as_single(name, val)?;\n\n        if env::var(name).is_ok() {\n            let v = d.get_as_single()?;\n            unsafe{env::set_var(name, v)};\n        }\n\n        Ok(())\n    }\n\n    pub fn append_param2(\n        &mut self,\n        name: &str,\n        index: &str,\n        val: &str,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        if index.is_empty() {\n            return self.append_param(name, val, scope);\n        }\n\n        if self.is_array(name) {\n            if let Ok(n) = index.parse::<isize>() {\n                self.set_array_elem(name, val, n, scope, true)?;\n            }\n        } else if self.is_assoc(name) {\n            self.append_to_assoc_elem(name, index, val, scope)?;\n        } else {\n            match index.parse::<isize>() {\n                Ok(n) => self.set_array_elem(name, val, n, scope, true)?,\n                _ => self.append_to_assoc_elem(name, index, val, scope)?,\n            }\n        }\n        Ok(())\n    }\n\n    pub fn append_to_assoc_elem(&mut self, name: &str, key: &str,\n        val: &str, scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        let scope = self.get_target_scope(name, scope);\n        match self.params[scope].get_mut(name) {\n            Some(v) => v.append_to_assoc_elem(name, key, val),\n            _ => Err(ExecError::Other(\"TODO\".to_string())),\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_checkers.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::core::DataBase;\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse crate::utils::restricted_shell;\n\nimpl DataBase {\n    pub(super) fn check_on_write(&mut self, name: &str, values: &Option<Vec<String>>\n    ) -> Result<(), ExecError> {\n        Self::name_check(name)?;\n        restricted_shell::check(self, name, values)?;\n        Ok(())\n    }\n\n    pub fn has_array_value(&mut self, name: &str, index: &str) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.has_key(index).unwrap_or(false),\n            None => false,\n        }\n    }\n\n    pub fn has_flag_scope(&mut self, name: &str, flag: char, scope: usize) -> bool {\n        if let Some(e) = self.params[scope].get_mut(name) {\n            return e.has_flag(flag);\n        }\n        false\n    }\n\n    pub fn has_flag(&mut self, name: &str, flag: char) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.has_flag(flag),\n            None => false,\n        }\n    }\n\n    pub fn exist(&mut self, name: &str) -> bool {\n        if let Ok(Some(nameref)) = self.get_nameref(name) {\n            return self.exist(&nameref);\n        }\n\n        if let Ok(n) = name.parse::<usize>() {\n            let scope = self.position_parameters.len() - 1;\n            return n < self.position_parameters[scope].len();\n        }\n\n        let num = self.params.len();\n        for scope in (0..num).rev() {\n            if self.params[scope].contains_key(name) {\n                return true;\n            }\n        }\n        false\n    }\n\n    pub fn exist_nameref(&mut self, name: &str) -> bool {\n        if let Some(d) = self.get_ref(name) {\n            return d.has_flag('n');\n        }\n\n        false\n    }\n\n    pub fn exist_l(&mut self, name: &str, scope: usize) -> bool {\n        if scope >= self.params.len() {\n            return false;\n        }\n\n        self.params[scope].contains_key(name)\n    }\n\n    pub fn exist_nameref_l(&mut self, name: &str, scope: usize) -> bool {\n        if let Some(d) = self.params[scope].get_mut(name) {\n            return d.has_flag('n');\n        }\n\n        false\n    }\n\n    pub fn has_key(&mut self, name: &str, key: &str) -> Result<bool, ExecError> {\n        let num = self.params.len();\n        for scope in (0..num).rev() {\n            if let Some(e) = self.params[scope].get_mut(name) {\n                return e.has_key(key);\n            }\n        }\n        Ok(false)\n    }\n\n    pub fn name_check(name: &str) -> Result<(), ExecError> {\n        if !utils::is_param(name) {\n            return Err(ExecError::VariableInvalid(name.to_string()));\n        }\n        Ok(())\n    }\n\n    pub fn is_readonly(&mut self, name: &str) -> bool {\n        self.has_flag(name, 'r')\n    }\n\n    pub fn is_int(&mut self, name: &str) -> bool {\n        self.has_flag(name, 'i')\n    }\n\n    pub fn is_assoc(&mut self, name: &str) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.is_assoc(),\n            None => false,\n        }\n    }\n\n    pub fn is_single(&mut self, name: &str) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.is_single(),\n            _ => false,\n        }\n    }\n\n    pub fn is_single_num(&mut self, name: &str) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.is_single_num(),\n            _ => false,\n        }\n    }\n\n    pub fn is_array(&mut self, name: &str) -> bool {\n        match self.get_ref(name) {\n            Some(d) => d.is_array(),\n            _ => false,\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_getters.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::data::Data;\nuse super::DataBase;\nuse crate::error::exec::ExecError;\nuse std::collections::HashSet;\nuse std::env;\n\nimpl DataBase {\n    pub fn get_ref(&mut self, name: &str) -> Option<&mut Box<dyn Data>> {\n        let scope = self.get_scope_pos(name)?;\n        self.params[scope].get_mut(name)\n    }\n\n    pub fn get_ifs_head(&mut self) -> String {\n        let ifs = self.get_param(\"IFS\").unwrap_or(\" \".to_string());\n        match ifs.as_str() {\n            \"\" => \"\".to_string(),\n            s => s.chars().next().unwrap().to_string(),\n        }\n    }\n\n    pub fn get_scope_num(&mut self) -> usize {\n        self.params.len()\n    }\n\n    pub fn get_param_keys(&mut self) -> Vec<String> {\n        let mut keys = HashSet::new();\n        for scope in &self.params {\n            scope.keys()\n                 .for_each(|k| { keys.insert(k); });\n        }\n        let mut ans = keys.iter()\n                          .map(|c| c.to_string())\n                          .collect::<Vec<String>>();\n        ans.sort();\n        ans\n    }\n\n    pub fn get_func_keys(&mut self) -> Vec<String> {\n        let mut keys = self.functions\n                           .keys()\n                           .map(|c| c.to_string())\n                           .collect::<Vec<String>>();\n        keys.sort();\n        keys \n    }\n\n    pub fn get_scope_pos(&mut self, name: &str) -> Option<usize> {\n        let num = self.params.len();\n        (0..num)\n            .rev()\n            .find(|&scope| self.params[scope].contains_key(name))\n    }\n\n    pub fn get_position_params(&self) -> Vec<String> {\n        match self.position_parameters.last() {\n            Some(v) => v[1..].to_vec(),\n            _ => vec![],\n        }\n    }\n\n    pub fn get_indexes_all(&mut self, name: &str) -> Vec<String> {\n        let scope = self.position_parameters.len() - 1;\n        if name == \"@\" {\n            return self.position_parameters[scope].clone();\n        }\n\n        match self.get_ref(name) {\n            Some(d) => d.get_all_indexes_as_array().unwrap_or_default(),\n            None => vec![],\n        }\n    }\n\n    pub fn get_vec_from(\n        &mut self,\n        name: &str,\n        pos: usize,\n        flatten: bool,\n    ) -> Result<Vec<String>, ExecError> {\n        let scope = self.position_parameters.len() - 1;\n        if name == \"@\" {\n            return Ok(self.position_parameters[scope].clone());\n        }\n\n        match self.get_ref(name) {\n            Some(d) => {\n                if let Ok(v) = d.get_vec_from(pos, flatten) {\n                    return Ok(v);\n                }\n                Ok(vec![])\n            }\n            None => {\n                if self.flags.contains('u') {\n                    return Err(ExecError::UnboundVariable(name.to_string()));\n                }\n                Ok(vec![])\n            }\n        }\n    }\n\n    pub fn get_var_len(&mut self, name: &str) -> usize {\n        if let Some(d) = self.get_ref(name) {\n            return d.len();\n        }\n        0\n    }\n\n    pub fn index_based_len(&mut self, name: &str) -> usize {\n        if let Some(d) = self.get_ref(name) {\n            return d.index_based_len();\n        }\n        0\n    }\n\n    pub fn get_vec(&mut self, name: &str, flatten: bool) -> Result<Vec<String>, ExecError> {\n        self.get_vec_from(name, 0, flatten)\n    }\n\n    pub fn get_elem(&mut self, name: &str, pos: &str) -> Result<String, ExecError> {\n        Self::name_check(name)?;\n\n        let scope = self.get_scope_pos(name);\n\n        if scope.is_none() {\n            return match self.flags.contains('u') {\n                true => Err(ExecError::UnboundVariable(name.to_string())),\n                false => Ok(\"\".to_string()),\n            };\n        }\n\n        let ifs = self.get_ifs_head();\n        self.params[scope.unwrap()]\n            .get_mut(name)\n            .unwrap()\n            .get_as_array_or_assoc(pos, &ifs)\n    }\n\n    pub fn get_elem_or_param(&mut self, name: &str, index: &str) -> Result<String, ExecError> {\n        match index.is_empty() {\n            true => self.get_param(name),\n            false => self.get_elem(name, index),\n        }\n    }\n\n    pub fn get_elem_len(&mut self, name: &str, key: &str) -> Result<usize, ExecError> {\n        Self::name_check(name)?;\n\n        if let Some(v) = self.get_ref(name) {\n            if key == \"@\" || key == \"*\" {\n                if ! v.is_initialized() {\n                    return Ok(0);\n                }\n                if ! v.is_array() && ! v.is_assoc() {\n                    return Ok(1);\n                }\n            }\n\n            return v.elem_len(key);\n        }\n\n        if self.flags.contains('u') {\n            return Err(ExecError::UnboundVariable(name.to_string()));\n        }\n\n        Ok(0)\n    }\n\n    pub fn get_braced_param_hash_length(&mut self, name: &str) -> Result<usize, ExecError> {\n        Self::name_check(name)?;\n\n        if name == \"@\" || name == \"*\" {\n            let scope = self.position_parameters.len();\n            return Ok(self.position_parameters[scope - 1].len() - 1);\n        }\n\n        if let Ok(n) = name.parse::<usize>() {\n            return Ok(position_param(self, n)?.chars().count());\n        }\n\n        if let Some(v) = self.get_ref(name) {\n            return v.elem_len(\"0\");\n        }\n\n        if let Ok(v) = env::var(name) {\n            return Ok(v.chars().count());\n        }\n\n        if self.flags.contains('u') {\n            return Err(ExecError::UnboundVariable(name.to_string()));\n        }\n\n        Ok(0)\n    }\n\n    pub fn get_param(&mut self, name: &str) -> Result<String, ExecError> {\n        Self::name_check(name)?;\n\n        if let Some(nameref) = self.get_nameref(name)? {\n            return self.get_param(&nameref);\n        }\n\n        if let Some(val) = special_param(self, name) {\n            return Ok(val);\n        }\n\n        if name == \"@\" || name == \"*\" {\n            return connected_position_params(self, name == \"*\");\n        } //in double quoted subword, this method should not be used\n\n        if let Ok(n) = name.parse::<usize>() {\n            return position_param(self, n);\n        }\n\n        if let Some(v) = self.get_ref(name) {\n            if v.is_special() {\n                return v.get_as_single();\n            } else if v.is_single_num() {\n                let val = v.get_as_single_num()?; //.unwrap_or_default();\n                return Ok(val.to_string());\n            } else {\n                let val = v.get_as_single().unwrap_or_default();\n                return Ok(val);\n            }\n        }\n\n        if let Ok(v) = env::var(name) {\n            let _ = self.set_param(name, &v, Some(0));\n            return Ok(v);\n        }\n\n        if self.flags.contains('u') {\n            return Err(ExecError::UnboundVariable(name.to_string()));\n        }\n\n        Ok(\"\".to_string())\n    }\n\n    pub fn get_nameref(&mut self, name: &str) -> Result<Option<String>, ExecError> {\n        let d = match self.get_ref(name) {\n            Some(d) => d,\n            None => return Ok(None),\n        };\n\n        if ! d.has_flag('n') {\n            return Ok(None);\n        }\n\n        if ! d.is_initialized() {\n            return Ok(None);\n        }\n\n        match d.get_as_single() {\n            Ok(nameref) => Ok(Some(nameref)),\n            Err(e) => Err(e), \n        }\n    }\n\n    pub fn get_flags(&mut self, name: &str) -> &str {\n        if let Some(v) = self.get_ref(name) {\n            v.get_flags()\n        }else{\n            \"\"\n        }\n    }\n}\n\nfn special_param(db: &DataBase, name: &str) -> Option<String> {\n    let val = match name {\n        \"-\" => db.flags.clone(),\n        \"?\" => db.exit_status.to_string(),\n        \"_\" => db.last_arg.clone(),\n        \"#\" => {\n            let pos = db.position_parameters.len() - 1;\n            (db.position_parameters[pos].len() - 1).to_string()\n        }\n        _ => return None,\n    };\n\n    Some(val)\n}\n\nfn connected_position_params(db: &mut DataBase, aster: bool) -> Result<String, ExecError> {\n    let mut joint = \" \".to_string();\n    if aster {\n        joint = db.get_ifs_head();\n    }\n\n    match db.position_parameters.last() {\n        Some(a) => Ok(a[1..].join(&joint)),\n        _ => Ok(\"\".to_string()),\n    }\n}\n\nfn position_param(db: &DataBase, pos: usize) -> Result<String, ExecError> {\n    let scope = db.position_parameters.len();\n    match db.position_parameters[scope - 1].len() > pos {\n        true => Ok(db.position_parameters[scope - 1][pos].to_string()),\n        false => Ok(String::new()),\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_initializers.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::core::DataBase;\nuse crate::core::database::{Data, IntData, Uninit, AssocData, IntAssocData, ArrayData, IntArrayData, OnDemandArray};\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse crate::utils::clock;\nuse super::data::single_ondemand::OnDemandSingle;\nuse super::data::random::RandomVar;\nuse super::data::seconds::Seconds;\nuse super::data::srandom::SRandomVar;\nuse std::{env, process};\nuse nix::unistd;\n\nimpl DataBase {\n    pub(super) fn initialize(&mut self) -> Result<(), String> {\n        self.exit_status = 0;\n    \n        self.set_param(\"$\", &process::id().to_string(), None)?;\n        //self.set_param(\"BASHPID\", &process::id().to_string(), None)?;\n        let bashpid = || process::id().to_string();\n        self.params[0].insert(\"BASHPID\".to_string(), Box::new(OnDemandSingle::new(bashpid)));\n        self.set_flag(\"BASHPID\", 'i', 0);\n        self.set_param(\"BASH_SUBSHELL\", \"0\", None)?;\n        self.set_param(\"HOME\", &env::var(\"HOME\").unwrap_or(\"/\".to_string()), None)?;\n        self.set_param(\"OPTIND\", \"1\", None)?;\n        self.set_param(\"IFS\", \" \\t\\n\", None)?;\n    \n        self.init_as_num(\"UID\", &unistd::getuid().to_string(), None)?;\n        self.set_flag(\"UID\", 'i', 0);\n        self.set_flag(\"UID\", 'r', 0);\n    \n        self.params[0].insert(\"RANDOM\".to_string(), Box::new(RandomVar::new()));\n        self.params[0].insert(\"SRANDOM\".to_string(), Box::new(SRandomVar::new()));\n        self.params[0].insert(\"SECONDS\".to_string(), Box::new(Seconds::new()));\n        self.params[0].insert(\"EPOCHSECONDS\".to_string(), Box::new(OnDemandSingle::new(clock::get_epochseconds)));\n        self.params[0].insert(\"EPOCHREALTIME\".to_string(), Box::new(OnDemandSingle::new(clock::get_epochrealtime)));\n        self.params[0].insert(\"GROUPS\".to_string(), Box::new(OnDemandArray::new(utils::groups)));\n\n        self.init_array(\"BASH_SOURCE\", Some(vec![]), None, false)?;\n        self.init_array(\"BASH_ARGC\", Some(vec![]), None, false)?;\n        self.init_array(\"BASH_ARGV\", Some(vec![]), None, false)?;\n        self.init_array(\"BASH_LINENO\", Some(vec![]), None, false)?;\n        self.init_array(\"DIRSTACK\", Some(vec![]), None, false)?;\n\n        self.init_assoc(\"BASH_ALIASES\", None, true, false)?;\n        self.init_assoc(\"BASH_CMDS\", None, true, false)?;\n\n        Ok(())\n    }\n\n    pub fn init_as_num(&mut self, name: &str, value: &str, scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![]))?;\n\n        let mut data = IntData::new();\n\n        if !value.is_empty() {\n            match value.parse::<isize>() {\n                Ok(n) => data.body = n,\n                Err(e) => return Err(ExecError::Other(e.to_string())),\n            }\n        }\n\n        let scope = self.get_target_scope(name, scope);\n        self.set_entry(scope, name, Box::new(data))\n    }\n\n    pub fn init_array(\n        &mut self,\n        name: &str,\n        v: Option<Vec<String>>,\n        scope: Option<usize>,\n        i_flag: bool,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &v)?;\n\n        let scope = self.get_target_scope(name, scope);\n        if i_flag {\n            let mut obj = IntArrayData::new();\n            if let Some(v) = v {\n                for (i, e) in v.into_iter().enumerate() {\n                    obj.set_as_array(name, &i.to_string(), &e)?;\n                }\n            }\n            return self.set_entry(scope, name, Box::new(obj));\n        }\n\n        if v.is_none() {\n            return self.set_entry(scope, name, Box::new(Uninit::new(\"a\")));\n        }\n\n        let mut obj = ArrayData::new();\n        for (i, e) in v.unwrap().into_iter().enumerate() {\n            obj.set_as_array(name, &i.to_string(), &e)?;\n        }\n        self.set_entry(scope, name, Box::new(obj))\n    }\n\n    pub fn init_assoc(\n        &mut self,\n        name: &str,\n        scope: Option<usize>,\n        set_array: bool,\n        i_flag: bool,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &None)?;\n\n        let obj = if i_flag {\n            Box::new(IntAssocData::new()) as Box::<dyn Data>\n        } else if set_array {\n            Box::new(AssocData::new()) as Box::<dyn Data>\n        } else {\n            Box::new(Uninit::new(\"A\")) as Box::<dyn Data>\n        };\n\n        let scope = self.get_target_scope(name, scope);\n        self.set_entry(scope, name, obj)\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_print.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse super::{Data, DataBase};\n\nimpl DataBase {\n    pub fn print_params_and_funcs(&mut self) {\n        self.get_param_keys()\n            .into_iter()\n            .for_each(|k| self.print_param(&k));\n        self.get_func_keys()\n            .into_iter()\n            .for_each(|k| { self.print_func(&k); });\n    }\n\n    pub fn print_param(&mut self, name: &str) {\n        if let Some(d) = self.get_ref(name) {\n            Self::print_with_name(d, name, false);\n        }\n    }\n\n    pub fn print_func(&mut self, name: &str) -> bool {\n        if let Some(f) = self.functions.get_mut(name) {\n            f.pretty_print(0);\n            return true;\n        }\n        false\n    }\n\n    pub fn print_for_declare(&mut self, name: &str) {\n        if let Some(d) = self.get_ref(name) {\n            Self::print_with_name(d, name, true);\n        }\n    }\n\n    fn print_with_name(d: &mut Box::<dyn Data>, name: &str, declare_print: bool) {\n        let body = d.get_fmt_string();\n        if !d.is_initialized() {\n            println!(\"{name}\");\n        } else if declare_print\n            && (d.is_single() || d.is_special() )\n            && !body.starts_with(\"\\\"\")\n            && !body.ends_with(\"\\\"\")\n        {\n            println!(\"{name}=\\\"{body}\\\"\");\n        } else {\n            println!(\"{name}={body}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_setters/database_setter_backend.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse std::env;\nuse super::{ArrayData, Data, Uninit};\nuse crate::core::DataBase;\nuse crate::error::exec::ExecError;\n\nimpl DataBase {\n    pub(super) fn set_elem(&mut self, scope: usize, name: &str,\n        pos: isize, val: &String, i_flag: bool\n        ) -> Result<(), ExecError> {\n        if self.is_readonly(name) {\n            return Err(ExecError::VariableReadOnly(name.to_string()));\n        }\n        if ! self.params[scope].contains_key(name) {\n            self.set_uninit_array(scope, name, i_flag)?;\n            return self.set_elem(scope, name, pos, val, i_flag);\n        }\n\n\n        let d = self.params[scope].get_mut(name).unwrap();\n        if let Some(init_d) = d.initialize() {\n            *d = init_d;\n        }\n\n        if d.is_array() {\n            d.set_as_array(name, &pos.to_string(), val)\n        } else if d.is_assoc() {\n            d.set_as_assoc(name, &pos.to_string(), val)\n        } else {\n            let data = d.get_as_single()?;\n            self.set_uninit_array(scope, name, i_flag)?;\n\n            if !data.is_empty() {\n                self.set_elem(scope, name, 0, &data, i_flag)?;\n            }\n            self.set_elem(scope, name, pos, val, i_flag)\n        } \n    }\n\n    pub(super) fn append_elem(&mut self, scope: usize, \n        name: &str, pos: isize, val: &String,\n    ) -> Result<(), ExecError> {\n        if ! self.params[scope].contains_key(name) {\n            self.set_uninit_array(scope, name, false)?;\n            return self.set_elem(scope, name, pos, val, false);\n        }\n\n        let d = self.params[scope].get_mut(name).unwrap();\n        if let Some(init_d) = d.initialize() {\n            *d = init_d;\n        }\n\n        if d.is_array() {\n            d.append_to_array_elem(name, &pos.to_string(), val)\n        } else if d.is_assoc() {\n            d.append_to_assoc_elem(name, &pos.to_string(), val)\n        } else {\n            let data = d.get_as_single()?;\n            self.set_uninit_array(scope, name, false)?;\n            self.append_elem(scope, name, 0, &data)?;\n            self.append_elem(scope, name, pos, val)\n        }\n    }\n\n    pub(super) fn set_uninit_array(&mut self, scope: usize,\n        name: &str, i_flag: bool,\n    ) -> Result<(), ExecError> {\n        let obj = if i_flag {\n            Box::new(Uninit::new(\"ai\")) as Box::<dyn Data>\n        }else {\n            Box::new(ArrayData::from(Some(vec![]))) as Box::<dyn Data>\n        };\n\n        self.set_entry(scope, name, obj)\n    }\n\n    pub fn set_entry(&mut self, scope: usize, name: &str,\n                     data: Box::<dyn Data>) -> Result<(), ExecError> {\n        if self.has_flag(name, 'r') {\n            return Err(ExecError::VariableReadOnly(name.to_string()));\n        }\n\n        self.params[scope].insert(name.to_string(), data);\n        Ok(())\n    }\n\n    pub fn remove_entry(&mut self, scope: usize, name: &str) -> Result<bool, ExecError> {\n        if self.has_flag(name, 'r') {\n            return Err(ExecError::VariableReadOnly(name.to_string()));\n        }\n        \n        if self.params[scope].contains_key(name) {\n            self.params[scope].remove(name);\n            if scope == 0 {\n                unsafe{env::remove_var(name)};\n            }\n\n            return Ok(true);\n        }\n        Ok(false)\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_setters.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nmod database_setter_backend;\n\nuse std::env;\nuse super::{ArrayData, Data, IntData, SingleData, Uninit};\nuse crate::core::DataBase;\nuse crate::error::exec::ExecError;\n\nimpl DataBase {\n    pub fn set_param(\n        &mut self,\n        name: &str,\n        val: &str,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        if let Ok(Some(nameref)) = self.get_nameref(name) {\n            return self.set_param(&nameref, val, scope);\n        }\n\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        if name == \"BASH_ARGV0\" {\n            let n = scope.unwrap_or(self.get_scope_num() - 1);\n            self.position_parameters[n][0] = val.to_string();\n        }\n\n        if !self.flags.contains('r')\n        && (self.flags.contains('a')\n       || self.has_flag(name, 'x')) {\n            unsafe{env::set_var(name, \"\")};\n        }\n\n        let scope = self.get_target_scope(name, scope);\n\n        if self.params[scope].get(name).is_none() {\n            self.set_entry(scope, name, Box::new(SingleData::from(\"\")))?;\n        }\n\n        let d = self.params[scope].get_mut(name).unwrap();\n        if let Some(init_d) = d.initialize() {\n            *d = init_d;\n        }\n\n        d.set_as_single(name, val)?;\n\n        if env::var(name).is_ok() || self.flags.contains('a') {\n            let v = d.get_as_single()?;\n            unsafe{env::set_var(name, &v)};\n        }\n        Ok(())\n    }\n\n    pub fn set_nameref(\n        &mut self,\n        name: &str,\n        val: &str,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        if name == \"BASH_ARGV0\" {\n            let n = scope.unwrap_or(self.get_scope_num() - 1);\n            self.position_parameters[n][0] = val.to_string();\n        }\n\n        let scope = self.get_target_scope(name, scope);\n\n        if self.params[scope].get(name).is_none() {\n            self.set_entry(scope, name, Box::new(SingleData::from(\"\")))?;\n        }\n\n        let d = self.params[scope].get_mut(name).unwrap();\n        if let Some(init_d) = d.initialize() {\n            *d = init_d;\n        }\n\n        d.set_as_single(name, val)?;\n        Ok(())\n    }\n\n    pub fn set_param2(\n        &mut self,\n        name: &str,\n        index: &str,\n        val: &str,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        if let Ok(Some(nameref)) = self.get_nameref(name) {\n            return self.set_param2(&nameref, index, val, scope);\n        }\n\n        if index.is_empty() {\n            return self.set_param(name, val, scope);\n        }\n\n        if self.is_array(name) {\n            if let Ok(n) = index.parse::<isize>() {\n                self.set_array_elem(name, val, n, scope, false)?;\n            }\n        } else if self.is_assoc(name) {\n            self.set_assoc_elem(name, index, val, scope)?;\n        } else {\n            match index.parse::<isize>() {\n                Ok(n) => self.set_array_elem(name, val, n, scope, false)?,\n                _ => self.set_assoc_elem(name, index, val, scope)?,\n            }\n        }\n        Ok(())\n    }\n\n    pub fn set_array_elem(&mut self, name: &str, val: &str,\n        pos: isize, scope: Option<usize>, append: bool,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        let scope = self.get_target_scope(name, scope);\n        let i_flag = self.has_flag(name, 'i');\n        match append {\n            false => self.set_elem(scope, name, pos, &val.to_string(), i_flag),\n            true  => self.append_elem(scope, name, pos, &val.to_string()),\n        }\n    }\n\n    pub fn set_assoc_elem(&mut self, name: &str, key: &str,\n        val: &str, scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        self.check_on_write(name, &Some(vec![val.to_string()]))?;\n\n        let scope = self.get_target_scope(name, scope);\n        match self.params[scope].get_mut(name) {\n            Some(v) => {\n                if let Some(init_v) = v.initialize() {\n                    *v = init_v;\n                }\n                v.set_as_assoc(name, key, val)\n            }\n            _ => Err(ExecError::Other(\"TODO\".to_string())),\n        }\n    }\n\n    pub fn set_flag(&mut self, name: &str, flag: char, scope: usize) {\n        if let Ok(Some(nameref)) = self.get_nameref(name) {\n            return self.set_flag(&nameref, flag, scope);\n        }\n\n        match self.params[scope].get_mut(name) {\n            Some(d) => { d.set_flag(flag); },\n            None => {\n                let obj = match flag {\n                    'i' => Box::new(IntData::new()) as Box::<dyn Data>,\n                    _ => Box::new(Uninit::new(&flag.to_string())) as Box::<dyn Data>,\n                };\n                let _ = self.set_entry(scope, name, obj);\n            }\n        }\n    }\n\n    pub fn set_flag_nameref(&mut self, name: &str, flag: char, scope: usize) {\n        match self.params[scope].get_mut(name) {\n            Some(d) => { d.set_flag(flag); },\n            None => {\n                let obj = match flag {\n                    'i' => Box::new(IntData::new()) as Box::<dyn Data>,\n                    _ => Box::new(Uninit::new(&flag.to_string())) as Box::<dyn Data>,\n                };\n                let _ = self.set_entry(scope, name, obj);\n            }\n        }\n    }\n\n    pub fn set_scope_to_env(&mut self, scope: usize) {\n        for (k, v) in &mut self.params[scope] {\n            unsafe{env::set_var(k, v.get_as_single().unwrap_or(\"\".to_string()))};\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/database/database_unsetters.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::core::DataBase;\nuse crate::core::database::Uninit;\nuse crate::error::exec::ExecError;\nuse std::env;\n\nimpl DataBase {\n    pub fn unset_flag(&mut self, name: &str, flag: char, scope: usize) {\n        if flag != 'n' {\n            if let Ok(Some(nameref)) = self.get_nameref(name) {\n                return self.unset_flag(&nameref, flag, scope);\n            }\n        }\n\n        let rf = &mut self.params[scope];\n        if let Some(d) = rf.get_mut(name) {\n            d.unset_flag(flag);\n        }\n    }\n\n    pub fn unset_flag_nameref(&mut self, name: &str, flag: char, scope: usize) {\n        let rf = &mut self.params[scope];\n        if let Some(d) = rf.get_mut(name) {\n            d.unset_flag(flag);\n        }\n    }\n\n    pub fn unset_nameref(&mut self, name: &str,\n                         called_scope: Option<usize>) -> Result<(), ExecError> {\n        if let Some(scope) = called_scope {\n            if let Some(d) = self.params[scope].get_mut(name) {\n                if d.has_flag('n') {\n                    self.remove_entry(scope, name)?;\n                }\n            }\n            return Ok(());\n        }\n\n        let num = self.params.len();\n        for scope in 0..num {\n            if let Some(d) = self.params[scope].get_mut(name) {\n                if d.has_flag('n') {\n                    self.remove_entry(scope, name)?;\n                }\n            }\n        }\n        Ok(())\n    }\n\n    pub fn unset_var(&mut self, name: &str,\n                     called_scope: Option<usize>,\n                     localvar_unset: bool) -> Result<bool, ExecError> {\n        if let Ok(Some(nameref)) = self.get_nameref(name) {\n            if nameref != \"\" {\n                 return self.unset_var(&nameref, called_scope, localvar_unset);\n            }\n            return Ok(false);\n        }\n\n        let mut res = false;\n\n        if let Some(scope) = called_scope {\n            if scope == 0 {\n                return Ok(false);\n            }\n            res = self.remove_entry(scope, name)?;\n\n            unsafe{env::set_var(name, \"\")};\n            for scope in self.params.iter_mut() {\n                if let Some(d) = scope.get_mut(name) {\n                    res = true;\n                    if localvar_unset {\n                        *d = Box::new( Uninit::new(\"\") );\n                    }else {\n                        scope.remove(name);\n                    }\n                }\n            }\n\n            return Ok(res);\n        }\n\n        let num = self.params.len();\n        for scope in (0..num).rev() {\n            if self.remove_entry(scope, name)? {\n                return Ok(true);\n            }\n        }\n        Ok(res)\n    }\n\n    pub fn unset_function(&mut self, name: &str) {\n        self.functions.remove(name);\n    }\n\n    pub fn unset(&mut self, name: &str, called_scope: Option<usize>,\n                 localvar_unset: bool) -> Result<(), ExecError> {\n        if self.unset_var(name, called_scope, localvar_unset)? {\n            return Ok(());\n        }\n        self.unset_function(name);\n        Ok(())\n    }\n\n    pub fn unset_array_elem(&mut self, name: &str, key: &str) -> Result<(), ExecError> {\n        if self.is_single(name) && (key == \"0\" || key == \"@\" || key == \"*\") {\n            self.unset_var(name, None, false)?;\n            return Ok(());\n        }\n\n        for scope in &mut self.params {\n            if let Some(d) = scope.get_mut(name) {\n                d.remove_elem(key)?;\n            }\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/core/database.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\n//The methods of DataBase are distributed in database/database_*.rs files.\npub mod data;\nmod database_appenders;\nmod database_checkers;\nmod database_getters;\nmod database_print;\nmod database_setters;\nmod database_unsetters;\nmod database_initializers;\n\nuse self::data::array::ArrayData;\nuse self::data::array_int::IntArrayData;\nuse self::data::uninit::Uninit;\nuse self::data::assoc::AssocData;\nuse self::data::assoc_int::IntAssocData;\nuse self::data::array_ondemand::OnDemandArray;\nuse self::data::single::SingleData;\nuse self::data::single_int::IntData;\nuse self::data::Data;\nuse crate::elements::command::function_def::FunctionDefinition;\nuse crate::error::exec::ExecError;\nuse std::collections::HashMap;\n\n#[derive(Debug, Default)]\npub struct DataBase {\n    pub flags: String,\n    pub params: Vec<HashMap<String, Box<dyn Data>>>,\n    pub position_parameters: Vec<Vec<String>>,\n    pub functions: HashMap<String, FunctionDefinition>,\n    pub exit_status: i32,\n    pub last_arg: String,\n    pub hash_counter: HashMap<String, usize>,\n}\n\nimpl DataBase {\n    pub fn new() -> DataBase {\n        let mut data = DataBase {\n            params: vec![HashMap::new()],\n            position_parameters: vec![vec![]],\n            flags: \"B\".to_string(),\n            ..Default::default()\n        };\n\n        data.initialize().unwrap();\n        data\n    }\n\n    pub fn get_target_scope(&mut self, name: &str, scope: Option<usize>) -> usize {\n        match scope {\n            Some(n) => n,\n            None => self.solve_scope(name),\n        }\n    }\n\n    fn solve_scope(&mut self, name: &str) -> usize {\n        self.get_scope_pos(name).unwrap_or(0)\n    }\n\n    pub fn push_local(&mut self) {\n        self.params.push(HashMap::new());\n    }\n\n    pub fn pop_local(&mut self) {\n        self.params.pop();\n    }\n\n    pub fn init(&mut self, name: &str, scope: usize) {\n        if let Some(d) = self.params[scope].get_mut(name) {\n            d.clear();\n        }\n    }\n\n    pub fn int_to_str_type(&mut self, name: &str, scope: usize) -> Result<(), ExecError> {\n        let scope_len = self.params.len();\n        for ly in scope..scope_len {\n            if let Some(v) = self.params[ly].get_mut(name) {\n                let _ = v.unset_flag('i');\n            }\n        }\n\n        if let Some(d) = self.params[scope].get_mut(name) {\n            let new_d = d.get_str_type();\n            self.params[scope].insert(name.to_string(), new_d);\n        }\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/core/file_descs.rs",
    "content": "//SPDXFileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nextern crate libc;\nuse libc::dup2;\nuse libc::fcntl;\nuse libc::{F_GETFD, F_DUPFD_CLOEXEC};\n\nuse crate::error::exec::ExecError;\nuse nix::unistd::Pid;\nuse std::os::fd::{OwnedFd, FromRawFd, RawFd};\nuse nix::unistd;\nuse std::os::fd::AsRawFd;\nuse std::fs::File;\n\n/* We want to control all opening FDs with this. \n * However, I have no idea how to handle\n * files and std{in, out, err}. */\n#[derive(Default, Debug)]\npub struct FileDescriptors {\n    fds: Vec<Option<OwnedFd>>,\n}\n\nimpl FileDescriptors {\n    pub(super) fn new() -> Self {\n        let mut data = Self::default();\n        for _ in 0..256 {\n            data.fds.push(None);\n        }\n\n        data\n    }\n\n    pub fn dupfd_cloexec(&mut self, from: RawFd,\n                                hereafter: RawFd) -> Result<RawFd, ExecError> {\n        let fd = unsafe{fcntl(from, F_DUPFD_CLOEXEC, hereafter)};\n        self.fds[fd as usize] = Some(unsafe { OwnedFd::from_raw_fd(fd) });\n\n        Ok(fd)\n    }\n\n    pub fn tcsetpgrp(&mut self, fd: RawFd, pgid: Pid) -> Result<(), ExecError> {\n        if let Some(fd) = self.fds[fd as usize].as_mut() {\n            return Ok(unistd::tcsetpgrp(fd, pgid)?);\n        }\n        Ok(())\n    }\n\n    pub fn tcgetpgrp(&mut self, fd: RawFd) -> Result<Pid, ExecError> {\n        if let Some(fd) = self.fds[fd as usize].as_mut() {\n            return Ok(unistd::tcgetpgrp(fd)?);\n        }\n        Err(ExecError::Other(\"cannot get process group\".to_string()))\n    }\n\n    pub fn close(&mut self, fd: RawFd) {\n        if fd < 0 || fd >= 256 {\n            return;\n        }\n        self.fds[fd as usize] = None;\n        let _ = unistd::close(fd);\n    }\n\n    pub fn pipe(&mut self) -> (RawFd, RawFd) {\n        let (recv, send) = unistd::pipe().expect(\"Cannot open pipe\");\n        let fd_recv = recv.as_raw_fd();\n        let fd_send = send.as_raw_fd();\n\n        self.fds[fd_recv as usize] = Some(recv);\n        self.fds[fd_send as usize] = Some(send);\n\n        (fd_recv, fd_send)\n    }\n\n    pub fn backup(&mut self, from: RawFd) -> RawFd {\n        //if fcntl::fcntl(from, fcntl::F_GETFD).is_err() {\n        if unsafe{fcntl(from, F_GETFD)} == -1 {\n            return from;\n        }\n        self.dupfd_cloexec(from, 10).unwrap()\n    }\n\n    pub fn replace(&mut self, from: RawFd, to: RawFd) -> Result<(), ExecError> {\n        if from < 0 || to < 0 {\n            return Ok(());\n        }\n\n        if unsafe{dup2(from, to)} < 0 {\n            return Err(ExecError::Other(\"dup2 error\".to_string()));\n        }\n\n        //unistd::dup2(from, to)?;\n        self.close(from);\n        Ok(())\n    }\n\n    pub fn share(&mut self, from: RawFd, to: RawFd) -> Result<(), ExecError> {\n        if from < 0 || to < 0 {\n            return Err(ExecError::Other(\"minus fd number\".to_string()));\n        }\n\n        if unsafe{dup2(from, to)} < 0 {\n            //return Err(ExecError::Other(\"dup2 error\".to_string()));\n            return Err(ExecError:: BadFd(from));\n        }\n\n        Ok(())\n    }\n\n    pub fn get_file(&mut self, fd: RawFd) -> File {\n        let f = self.fds[fd as usize].as_mut().unwrap().try_clone().unwrap();\n        self.fds[fd as usize] = None;\n        File::from(f)\n    }\n}\n"
  },
  {
    "path": "src/core/history.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::ShellCore;\nuse rev_lines::RevLines;\nuse std::fs::File;\nuse std::fs::OpenOptions;\nuse std::io::{BufReader, BufWriter, Write};\n\nimpl ShellCore {\n    pub fn fetch_history(&mut self, pos: usize, prev: usize, prev_str: String) -> String {\n        if prev < self.history.len() {\n            self.history[prev] = prev_str;\n        } else {\n            self.rewritten_history\n                .insert(prev + 1 - self.history.len(), prev_str);\n        }\n\n        if pos < self.history.len() {\n            self.history[pos].clone()\n        } else {\n            self.fetch_history_file(pos + 1 - self.history.len())\n        }\n    }\n\n    pub fn fetch_history_file(&mut self, pos: usize) -> String {\n        if let Some(s) = self.rewritten_history.get(&pos) {\n            return s.to_string();\n        }\n        if pos == 0 {\n            return String::new();\n        }\n\n        let mut file_line = pos - 1;\n        if let Ok(n) = self\n            .db\n            .get_param(\"HISTFILESIZE\")\n            .unwrap_or_default()\n            .parse::<usize>()\n        {\n            file_line %= n;\n        }\n\n        if let Ok(hist_file) = File::open(self.db.get_param(\"HISTFILE\").unwrap_or_default()) {\n            let mut rev_lines = RevLines::new(BufReader::new(hist_file));\n            if let Some(Ok(s)) = rev_lines.nth(file_line) {\n                return s;\n            }\n        }\n\n        String::new()\n    }\n\n    pub fn write_history_to_file(&mut self) {\n        if !self.db.flags.contains('i') || self.is_subshell {\n            return;\n        }\n        let filename = self.db.get_param(\"HISTFILE\").unwrap_or_default();\n        if filename.is_empty() {\n            eprintln!(\"sush: HISTFILE is not set\");\n            return;\n        }\n\n        let file = match OpenOptions::new().create(true).append(true).open(&filename) {\n            Ok(f) => f,\n            _ => {\n                eprintln!(\"sush: invalid history file\");\n                return;\n            }\n        };\n\n        let mut f = BufWriter::new(file);\n        for h in self.history.iter().rev() {\n            if h.is_empty() {\n                continue;\n            }\n            let _ = f.write(h.as_bytes());\n            let _ = f.write(&[0x0A]);\n        }\n        let _ = f.flush();\n    }\n}\n"
  },
  {
    "path": "src/core/jobtable.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\nuse nix::sys::signal;\nuse nix::sys::wait;\nuse nix::sys::wait::{WaitPidFlag, WaitStatus};\nuse nix::unistd;\nuse nix::unistd::Pid;\n\n#[derive(Debug, Default)]\npub struct JobEntry {\n    pub id: usize,\n    pub pids: Vec<Pid>,\n    proc_statuses: Vec<WaitStatus>,\n    pub display_status: String,\n    pub text: String,\n    change: bool,\n    pub no_control: bool,\n    pub coproc_name: Option<String>,\n    pub coproc_fds: Vec<i32>,\n}\n\nfn wait_nonblock(pid: &Pid, status: &mut WaitStatus, coproc: bool) -> Result<(), ExecError> {\n    let waitflags = WaitPidFlag::WNOHANG | WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED;\n\n    match wait::waitpid(*pid, Some(waitflags)) {\n        Ok(s) => {\n            if s != WaitStatus::StillAlive || !still(status) {\n                *status = s;\n            }\n        },\n        Err(e) => {\n            if ! coproc {\n                return Err(ExecError::Errno(e));\n            }\n            *status = WaitStatus::Exited(*pid, 0);\n        },\n    }\n\n    Ok(())\n}\n\nfn wait_block(pid: &Pid, status: &mut WaitStatus) -> Result<i32, ExecError> {\n    *status = wait::waitpid(*pid, Some(WaitPidFlag::WUNTRACED))?;\n    let exit_status = match status {\n        WaitStatus::Exited(_, es) => *es,\n        WaitStatus::Stopped(_, _) => 148,\n        WaitStatus::Signaled(_, sig, _) => *sig as i32 + 128,\n        _ => 1,\n    };\n\n    Ok(exit_status)\n}\n\nfn still(status: &WaitStatus) -> bool {\n    matches!(\n        status,\n        WaitStatus::StillAlive | WaitStatus::Stopped(_, _) | WaitStatus::Continued(_)\n    )\n}\n\nimpl JobEntry {\n    pub fn new(\n        pids: Vec<Option<Pid>>,\n        statuses: &[WaitStatus],\n        text: &str,\n        status: &str,\n        id: usize,\n    ) -> JobEntry {\n        JobEntry {\n            id,\n            pids: pids.into_iter().flatten().collect(),\n            proc_statuses: statuses.to_vec(),\n            display_status: status.to_string(),\n            text: text.to_string(),\n            ..Default::default()\n        }\n    }\n\n    pub fn update_status(&mut self, wait: bool, check_done: bool) -> Result<i32, ExecError> {\n        let mut exit_status = 0;\n        let before = self.proc_statuses[0];\n        for (status, pid) in self.proc_statuses.iter_mut().zip(&self.pids) {\n            if still(status) {\n                match wait {\n                    true => exit_status = wait_block(pid, status)?,\n                    false => {\n                        wait_nonblock(pid, status, self.coproc_name.is_some())?;\n                    }\n                }\n            }\n        }\n        self.change |= before != self.proc_statuses[0];\n\n        /* check stopped processes */\n        let mut stopped = false;\n        for s in &self.proc_statuses {\n            match s {\n                WaitStatus::Stopped(_, _) => {\n                    stopped = true;\n                    break;\n                }\n                WaitStatus::Exited(_, es) => {\n                    if check_done {\n                        exit_status = *es;\n                        break;\n                    }\n                }\n                _ => {}\n            }\n        }\n\n        if stopped {\n            self.display_status = \"Stopped\".to_string();\n            return Ok(148);\n        }\n\n        if !stopped && self.display_status == \"Stopped\" || self.change {\n            self.change_display_status(self.proc_statuses[0]);\n        }\n\n        Ok(exit_status)\n    }\n\n    pub fn print_p(&self) {\n        println!(\"{}\", self.pids[0]);\n    }\n\n    pub fn print(\n        &self,\n        priority: &[usize],\n        l_opt: bool,\n        r_opt: bool,\n        s_opt: bool,\n        add_amp: bool,\n    ) -> bool {\n        if r_opt && self.display_status != \"Running\" || s_opt && self.display_status != \"Stopped\" {\n            return false;\n        }\n\n        let symbol = if priority[0] == self.id {\n            \"+\"\n        } else if priority.len() > 1 && priority[1] == self.id {\n            \"-\"\n        } else {\n            \" \"\n        };\n\n        let pid = match l_opt {\n            true => &self.pids[0].to_string(),\n            false => \"\",\n        };\n\n        let tmp = self.text.clone();\n        let text = tmp.trim_end();\n\n        if self.display_status == \"Stopped\" {\n            println!(\n                \"[{}]{} {} {}                 {}\",\n                self.id, &symbol, &pid, &self.display_status, &text\n            );\n        } else if add_amp && self.display_status != \"Done\" {\n            println!(\n                \"[{}]{} {} {}                 {} &\",\n                self.id, &symbol, &pid, &self.display_status, &text\n            );\n        } else {\n            println!(\n                \"[{}]{} {} {}                    {}\",\n                self.id, &symbol, &pid, &self.display_status, &text\n            );\n        }\n\n        self.display_status == \"Done\" || self.display_status == \"Killed\"\n    }\n\n    fn display_status_on_signal(signal: &signal::Signal, coredump: bool) -> String {\n        let coredump_msg = match coredump {\n            true => \"    (core dumped)\",\n            false => \"\",\n        };\n\n        let msg = match signal {\n            signal::SIGHUP => \"Hangup\",\n            signal::SIGINT => \"Interrupt\",\n            signal::SIGQUIT => \"Quit\",\n            signal::SIGILL => \"Illeagal instruction\",\n            signal::SIGTRAP => \"Trace/breakpoint trap\",\n            signal::SIGABRT => \"Aborted\",\n            signal::SIGBUS => \"Bus error\",\n            signal::SIGFPE => \"Floating point exception\",\n            signal::SIGKILL => \"Killed\",\n            signal::SIGUSR1 => \"User defined signal 1\",\n            signal::SIGSEGV => \"Segmentation fault\",\n            signal::SIGUSR2 => \"User defined signal 2\",\n            signal::SIGPIPE => \"Broken pipe\",\n            signal::SIGALRM => \"Alarm clock\",\n            signal::SIGTERM => \"Terminated\",\n            //  signal::SIGSTKFLT => \"Stack fault\",           not in macOS\n            signal::SIGXCPU => \"CPU time limit exceeded\",\n            signal::SIGXFSZ => \"File size limit exceeded\",\n            signal::SIGVTALRM => \"Virtual timer expired\",\n            signal::SIGPROF => \"Profiling timer expired\",\n            //  signal::SIGPWR    => \"Power failure\",         not in macOS\n            signal::SIGSYS => \"Bad system call\",\n            _ => \"\",\n        };\n\n        (msg.to_owned() + coredump_msg).to_string()\n    }\n\n    fn change_display_status(&mut self, after: WaitStatus) {\n        self.display_status = match after {\n            WaitStatus::Exited(_, _) => \"Done\".to_string(),\n            WaitStatus::Stopped(_, _) => \"Stopped\".to_string(),\n            WaitStatus::Continued(_) => \"Running\".to_string(),\n            WaitStatus::Signaled(_, signal, coredump) => {\n                Self::display_status_on_signal(&signal, coredump)\n            }\n            _ => return,\n        }\n    }\n\n    pub fn send_cont(&mut self) {\n        for pid in &self.pids {\n            let _ = signal::kill(Pid::from_raw(-i32::from(*pid)), signal::SIGCONT);\n        }\n    }\n\n    pub fn solve_pgid(&self) -> Pid {\n        for pid in &self.pids {\n            if let Ok(pgid) = unistd::getpgid(Some(*pid)) {\n                return pgid;\n            }\n        }\n        Pid::from_raw(0)\n    }\n}\n\nimpl ShellCore {\n    fn close_coproc(&mut self, pos: usize) {\n        let name = self.job_table[pos].coproc_name.clone().unwrap();\n        let _ = self.db.unset(&name, None, false);\n        let _ = self.db.unset(&(name.clone() + \"_PID\"), None, false);\n\n        if let Ok(fd0) = self.db.get_elem(&name, \"0\") {\n            if let Ok(n) = fd0.parse::<i32>() {\n                let _ = unsafe{libc::close(n)};\n            }\n        }\n        if let Ok(fd1) = self.db.get_elem(&name, \"1\") {\n            if let Ok(n) = fd1.parse::<i32>() {\n                let _ = unsafe{libc::close(n)};\n            }\n        }\n\n        let _ = self.db.unset(&(name), None, false);\n    }\n\n    pub fn jobtable_check_status(&mut self) -> Result<(), ExecError> {\n        if self.is_subshell {\n            return Ok(());\n        }\n\n        let mut stopped = vec![];\n        for i in 0..self.job_table.len() {\n            let table = &mut self.job_table[i];\n\n            if table.update_status(false, false)? == 148 {\n                stopped.push(i);\n            }\n\n            if  table.coproc_name.is_some() \n            && (table.display_status == \"Done\" \n                || table.display_status == \"Killed\") {\n                self.close_coproc(i);\n            }\n        }\n\n        for i in stopped {\n            let job_id = self.job_table[i].id;\n            self.job_table_priority.retain(|id| *id != job_id);\n            self.job_table_priority.insert(0, job_id);\n        }\n\n        Ok(())\n    }\n\n    pub fn jobtable_print_status_change(&mut self) {\n        if self.is_subshell {\n            return;\n        }\n\n        for e in self.job_table.iter_mut() {\n            if e.change {\n                e.print(&self.job_table_priority, false, false, false, false);\n                e.change = false;\n            }\n        }\n\n        self.job_table\n            .retain(|e| still(&e.proc_statuses[0]) || e.display_status == \"Stopped\");\n\n        let ids = self.job_table.iter().map(|j| j.id).collect::<Vec<usize>>();\n        self.job_table_priority.retain(|id| ids.contains(id));\n    }\n\n    pub fn generate_new_job_id(&self) -> usize {\n        match self.job_table.last() {\n            None => 1,\n            Some(job) => job.id + 1,\n        }\n    }\n\n    pub fn get_jobentry_pid_by_coproc_name(&mut self, name: &str) -> Option<Pid> {\n        let ans = self.job_table.iter_mut().find(|e| e.coproc_name.is_some() \n                                                 && e.coproc_name.as_ref().unwrap() == name)?;\n\n        Some(ans.pids[0])\n    }\n\n    pub fn get_stopped_job_commands(&self) -> Vec<String> {\n        self.job_table\n            .iter()\n            .map(|j| j.text.split(' ').next().unwrap().to_string())\n            .collect()\n    }\n}\n"
  },
  {
    "path": "src/core/options.rs",
    "content": "//SPDXFileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDXLicense-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse std::collections::HashMap;\n\n#[derive(Debug, Default)]\npub struct Options {\n    opts: HashMap<String, bool>,\n    pub implemented: Vec<String>,\n}\n\nimpl Options {\n    pub fn new_as_basic_opts() -> Options {\n        let mut options = Options::default();\n        options.opts.insert(\"allexport\".to_string(), false);\n        options.opts.insert(\"pipefail\".to_string(), false);\n        options.opts.insert(\"monitor\".to_string(), true);\n        options.opts.insert(\"noclobber\".to_string(), false);\n        options.opts.insert(\"noglob\".to_string(), false);\n        options.opts.insert(\"onecmd\".to_string(), false);\n        options.opts.insert(\"posix\".to_string(), false);\n        options.opts.insert(\"history\".to_string(), false); //TODO: still dummy\n        options\n    }\n\n    pub fn new_as_shopts() -> Options {\n        let mut options = Options::default();\n        let opt_strs = vec![\n            \"autocd\",\n            \"cdable_vars\",\n            \"cdspell\",\n            \"checkhash\",\n            \"checkjobs\",\n            \"checkwinsize\",\n            \"cmdhist\",\n            \"compat31\",\n            \"compat32\",\n            \"compat40\",\n            \"compat41\",\n            \"dirspell\",\n            \"dotglob\",\n            \"execfail\",\n            \"expand_aliases\",\n            \"extdebug\",\n            \"extglob\",\n            \"extquote\",\n            \"failglob\",\n            \"force_fignore\",\n            \"globstar\",\n            \"globskipdots\",\n            \"gnu_errfmt\",\n            \"histappend\",\n            \"histreedit\",\n            \"histverify\",\n            \"hostcomplete\",\n            \"huponexit\",\n            \"interactive_comments\",\n            \"lastpipe\",\n            \"lithist\",\n            \"login_shell\",\n            \"mailwarn\",\n            \"no_empty_cmd_completion\",\n            \"nocaseglob\",\n            \"nocasematch\",\n            \"nullglob\",\n            \"promptvars\",\n            \"restricted_shell\",\n            \"shift_verbose\",\n            \"sourcepath\",\n            \"xpg_echo\",\n            \"assoc_expand_once\",\n            \"localvar_inherit\",\n            \"localvar_unset\",\n        ];\n\n        for opt in opt_strs {\n            options.opts.insert(opt.to_string(), false);\n        }\n\n        let true_list = [\"extglob\", \"progcomp\", \"globskipdots\"];\n        for opt in true_list {\n            options.opts.insert(opt.to_string(), true);\n        }\n\n        options.implemented = [\n            \"extglob\",\n            \"progcomp\",\n            \"nullglob\",\n            \"dotglob\",\n            \"globstar\",\n            \"globskipdots\",\n            \"nocasematch\",\n            \"expand_aliases\",\n            \"xpg_echo\",\n            \"lastpipe\",\n            \"execfail\",\n            \"assoc_expand_once\",\n            \"localvar_inherit\",\n            \"localvar_unset\",\n        ]\n        .iter()\n        .map(|s| s.to_string())\n        .collect();\n        //TODO: nocasematch and xpg_echo are dummy\n\n        options\n    }\n\n    pub fn format(opt: &str, onoff: bool) -> String {\n        let onoff_str = match onoff {\n            true => \"on\",\n            false => \"off\",\n        };\n\n        match opt.len() < 16 {\n            true => format!(\"{opt:16}{onoff_str}\"),\n            false => format!(\"{opt}\\t{onoff_str}\"),\n        }\n    }\n\n    pub fn format2(opt: &str, onoff: bool) -> String {\n        let onoff_str = match onoff {\n            true => \"-\",\n            false => \"+\",\n        };\n\n        format!(\"set {onoff_str}o {opt}\")\n    }\n\n    pub fn print_opt(&self, opt: &str, set_format: bool) -> bool {\n        match self.opts.get_key_value(opt) {\n            None => {\n                eprintln!(\"sush: shopt: {opt}: invalid shell option name\");\n                false\n            }\n            Some(kv) => {\n                match set_format {\n                    false => println!(\"{}\", Self::format(kv.0, *kv.1)),\n                    true => println!(\"{}\", Self::format2(kv.0, *kv.1)),\n                }\n                true\n            }\n        }\n    }\n\n    pub fn print_all(&self, positive: bool) {\n        let f = match positive {\n            true => Self::format,\n            false => Self::format2,\n        };\n\n        let mut list = self\n            .opts\n            .iter()\n            .map(|opt| f(opt.0, *opt.1))\n            .collect::<Vec<String>>();\n\n        list.sort();\n        list.iter().for_each(|e| println!(\"{e}\"));\n    }\n\n    pub fn print_if(&self, onoff: bool) {\n        let mut list = self\n            .opts\n            .iter()\n            .filter(|opt| *opt.1 == onoff)\n            .map(|opt| Self::format(opt.0, *opt.1))\n            .collect::<Vec<String>>();\n\n        list.sort();\n        list.iter().for_each(|e| println!(\"{e}\"));\n    }\n\n    pub fn exist(&self, opt: &str) -> bool {\n        self.opts.contains_key(opt)\n    }\n\n    pub fn query(&self, opt: &str) -> bool {\n        self.exist(opt) && self.opts[opt]\n    }\n\n    pub fn set(&mut self, opt: &str, onoff: bool) -> Result<(), ExecError> {\n        if !self.opts.contains_key(opt) {\n            let msg = format!(\"{opt}: invalid option name\");\n            return Err(ExecError::Other(msg));\n        }\n\n        self.opts.insert(opt.to_string(), onoff);\n\n        Ok(())\n    }\n\n    pub fn get_keys(&self) -> Vec<String> {\n        self.opts.clone().into_keys().collect()\n    }\n}\n"
  },
  {
    "path": "src/core.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-FileCopyrightText: 2024 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod builtins;\npub mod completion;\npub mod database;\npub mod history;\npub mod jobtable;\npub mod options;\nmod file_descs;\n\nuse self::completion::{Completion, CompletionEntry};\nuse self::database::DataBase;\nuse self::options::Options;\nuse self::file_descs::FileDescriptors;\nuse crate::core::jobtable::JobEntry;\nuse crate::elements::substitution::Substitution;\nuse crate::{error, proc_ctrl, signal};\nuse nix::sys::signal::Signal;\nuse nix::sys::time::{TimeSpec, TimeVal};\nuse nix::unistd::Pid;\nuse std::collections::HashMap;\nuse std::os::fd::RawFd;\n//use std::os::fd::{FromRawFd, OwnedFd};\nuse std::sync::atomic::AtomicBool;\nuse std::sync::Arc;\nuse std::{env, io, path};\nuse crate::error::exec::ExecError;\nuse crate::file_check;\n\npub struct MeasuredTime {\n    pub real: TimeSpec,\n    pub user: TimeVal,\n    pub sys: TimeVal,\n}\n\nimpl Default for MeasuredTime {\n    fn default() -> Self {\n        Self {\n            real: TimeSpec::new(0, 0),\n            user: TimeVal::new(0, 0),\n            sys: TimeVal::new(0, 0),\n        }\n    }\n}\n\ntype BuiltinFn = fn(&mut ShellCore, &[String]) -> i32;\ntype SubstBuiltinFn = fn(&mut ShellCore, &[String], &mut [Substitution]) -> i32;\n\n#[derive(Default)]\npub struct ShellCore {\n    pub db: DataBase,\n    pub alias_memo: Vec<(String, String)>,\n    pub rewritten_history: HashMap<usize, String>,\n    pub history: Vec<String>,\n    pub builtins: HashMap<String, BuiltinFn>,\n    pub subst_builtins: HashMap<String, SubstBuiltinFn>,\n    pub sigint: Arc<AtomicBool>,\n    pub trapped: Vec<(Arc<AtomicBool>, String)>,\n    pub traplist: Vec<(i32, String)>,\n    pub is_subshell: bool,\n    pub source_function_level: i32,\n    pub source_files: Vec<String>,\n    pub eval_level: i32,\n    pub loop_level: i32,\n    pub break_counter: i32,\n    pub continue_counter: i32,\n    pub return_flag: bool,\n    pub compat_bash: bool,\n    pub fds: FileDescriptors,\n    pub tty_fd: Option<RawFd>,\n    //pub tty_fd: Option<OwnedFd>,\n    pub job_table: Vec<JobEntry>,\n    pub job_table_priority: Vec<usize>,\n    current_dir: Option<path::PathBuf>, // the_current_working_directory\n    pub completion: Completion,\n    pub measured_time: MeasuredTime,\n    pub options: Options,\n    pub shopts: Options,\n    pub suspend_e_option: bool,\n    pub script_name: String,\n    pub exit_script: String,\n    pub exit_script_run: bool,\n    pub valid_assoc_expand_once: bool,\n    //pub process_sub: Vec<(Pid, RawFd)>,\n    pub proc_sub_pid: Vec<Pid>,\n    pub proc_sub_fd: Vec<RawFd>,\n}\n\nimpl ShellCore {\n    pub fn configure(&mut self) -> Result<(), ExecError> {\n        self.init_current_directory();\n        self.set_initial_parameters();\n        self.set_builtins();\n        signal::ignore(Signal::SIGPIPE);\n        signal::ignore(Signal::SIGTSTP);\n\n        let _ = self.db.set_param(\"PS4\", \"+ \", None);\n\n        if file_check::is_tty(0) && self.script_name == \"-\" {\n            self.db.flags += \"himH\";\n            let _ = self.db.set_param(\"PS1\", \"🍣 \", None);\n            let _ = self.db.set_param(\"PS2\", \"> \", None);\n            self.tty_fd = Some(self.fds.dupfd_cloexec(0, 255)?);\n        } else {\n            self.db.flags += \"h\";\n        }\n\n        let home = self.db.get_param(\"HOME\").unwrap_or_default().to_string();\n        let _ = self\n            .db\n            .set_param(\"HISTFILE\", &(home + \"/.sush_history\"), None);\n        let _ = self.db.set_param(\"HISTFILESIZE\", \"2000\", None);\n\n        if let Ok(\"1\") = env::var(\"SUSH_COMPAT_TEST_MODE\").as_deref() {\n            if self.db.flags.contains('i') {\n                eprintln!(\"THIS IS BASH COMPATIBILITY TEST MODE\");\n            }\n            self.compat_bash = true;\n        };\n\n        if self.script_name != \"-\" {\n            let zero = \"0\".to_string();\n            let _ = self.db.set_param2(\"BASH_LINENO\", &zero, &zero, None);\n            let _ = self\n                .db\n                .set_param2(\"BASH_SOURCE\", &zero, &self.script_name, None);\n        }\n        Ok(())\n    }\n\n    pub fn new() -> Self {\n        ShellCore {\n            db: DataBase::new(),\n            sigint: Arc::new(AtomicBool::new(false)),\n            options: Options::new_as_basic_opts(),\n            shopts: Options::new_as_shopts(),\n            script_name: \"-\".to_string(),\n            fds: FileDescriptors::new(),\n            ..Default::default()\n        }\n    }\n\n    pub fn configure_c_mode(&mut self) -> Result<(), ExecError> {\n        if file_check::is_tty(0) {\n            self.tty_fd = Some(self.fds.dupfd_cloexec(0, 255)?);\n        }\n\n        self.init_current_directory();\n        self.set_initial_parameters();\n        self.set_builtins();\n        signal::ignore(Signal::SIGPIPE);\n        signal::ignore(Signal::SIGTSTP);\n\n        if let Ok(\"1\") = env::var(\"SUSH_COMPAT_TEST_MODE\").as_deref() {\n            self.compat_bash = true\n        };\n        Ok(())\n    }\n\n    fn set_initial_parameters(&mut self) {\n        let version = env!(\"CARGO_PKG_VERSION\");\n        let profile = env!(\"CARGO_BUILD_PROFILE\");\n        let t_arch = env!(\"CARGO_CFG_TARGET_ARCH\");\n        let t_vendor = env!(\"CARGO_CFG_TARGET_VENDOR\");\n        let t_os = env!(\"CARGO_CFG_TARGET_OS\");\n        let machtype = format!(\"{t_arch}-{t_vendor}-{t_os}\");\n        let symbol = \"rusty_bash\";\n        let t_bash_symbol = \"bash_compatible\";\n        let t_bash_ver = env!(\"COMPAT_TARGET_BASH_VERSION\");\n        let vparts = version.split('.').collect();\n        let tbvparts = t_bash_ver.split('.').collect();\n        let sush_versinfo = [vparts, vec![symbol, profile, &machtype]]\n            .concat()\n            .iter()\n            .map(|e| e.to_string())\n            .collect();\n        let bash_versinfo = [tbvparts, vec![t_bash_symbol, profile, &machtype]]\n            .concat()\n            .iter()\n            .map(|e| e.to_string())\n            .collect();\n\n\n        let _ = self.db.set_param(\n            \"BASH_VERSION\",\n            &format!(\"{t_bash_ver}({t_bash_symbol})-{profile}\"),\n            None,\n        );\n\n        let _ = self.db.set_param(\"MACHTYPE\", &machtype, None);\n        let _ = self.db.set_param(\"HOSTTYPE\", t_arch, None);\n        let _ = self.db.set_param(\"OSTYPE\", t_os, None);\n\n        if let Ok(\"1\") = env::var(\"SUSH_COMPAT_TEST_MODE\").as_deref() {\n        }else{\n            let _ = self\n                .db\n                .init_array(\"SUSH_VERSINFO\", Some(sush_versinfo), None, false);\n            let _ = self.db.set_param(\n                \"SUSH_VERSION\",\n                &format!(\"{version}({symbol})-{profile}\"),\n                None,\n            );\n            self.db.set_flag(\"SUSH_VERSINFO\", 'r', 0);\n        }\n\n        let _ = self\n            .db\n            .init_array(\"BASH_VERSINFO\", Some(bash_versinfo), None, false);\n        self.db.set_flag(\"BASH_VERSINFO\", 'r', 0);\n    }\n\n    pub fn flip_exit_status(&mut self) {\n        self.db.exit_status = if self.db.exit_status == 0 { 1 } else { 0 };\n    }\n\n    fn set_subshell_parameters(&mut self) -> Result<(), String> {\n        let pid = nix::unistd::getpid();\n        self.db.init_as_num(\"BASHPID\", &pid.to_string(), Some(0))?;\n        match self.db.get_param(\"BASH_SUBSHELL\").unwrap().parse::<usize>() {\n            Ok(num) => self\n                .db\n                .set_param(\"BASH_SUBSHELL\", &(num + 1).to_string(), Some(0))?,\n            Err(_) => self.db.set_param(\"BASH_SUBSHELL\", \"0\", Some(0))?,\n        }\n        Ok(())\n    }\n\n    pub fn initialize_as_subshell(&mut self, pid: Pid, pgid: Pid) {\n        signal::restore(Signal::SIGINT);\n        signal::restore(Signal::SIGTSTP);\n        signal::restore(Signal::SIGPIPE);\n\n        self.is_subshell = true;\n        proc_ctrl::set_pgid(self, pid, pgid);\n        let _ = self.set_subshell_parameters();\n        //self.job_table.clear();\n\n        self.exit_script.clear();\n    }\n\n    pub fn init_current_directory(&mut self) {\n        match env::current_dir() {\n            Ok(path) => self.current_dir = Some(path),\n            Err(err) => {\n                let msg = format!(\"pwd: error retrieving current directory: {err:?}\");\n                error::print(&msg, self);\n            }\n        }\n    }\n\n    pub fn get_current_directory(&mut self) -> Option<path::PathBuf> {\n        if self.current_dir.is_none() {\n            self.init_current_directory();\n        }\n        self.current_dir.clone()\n    }\n\n    pub fn set_current_directory(&mut self, path: &path::PathBuf) -> Result<(), io::Error> {\n        env::set_current_dir(path)?;\n        self.current_dir = Some(path.clone());\n        Ok(())\n    }\n\n    pub fn get_ps4(&mut self) -> String {\n        let ps4 = self\n            .db\n            .get_param(\"PS4\")\n            .unwrap_or_default()\n            .trim_end()\n            .to_string();\n        let mut multi_ps4 = ps4.to_string();\n        for _ in 0..(self.source_files.len() as i32 + self.eval_level) {\n            multi_ps4 += &ps4;\n        }\n\n        multi_ps4\n    }\n\n    pub fn replace_alias(&mut self, word: &mut String) -> bool {\n        let before = word.clone();\n        match self.replace_alias_core(word) {\n            true => {\n                self.alias_memo.push((before, word.clone()));\n                true\n            }\n            false => false,\n        }\n    }\n\n    fn replace_alias_core(&mut self, word: &mut String) -> bool {\n        if !self.shopts.query(\"expand_aliases\") && !self.db.flags.contains('i') {\n            return false;\n        }\n\n        let mut ans = false;\n        let mut prev_head = \"\".to_string();\n        let history = [word.clone()];\n\n        loop {\n            let head = match word.replace(\"\\n\", \" \").split(' ').nth(0) {\n                Some(h) => h.to_string(),\n                _ => return ans,\n            };\n\n            if prev_head == head {\n                return ans;\n            }\n\n            //if let Some(value) = self.aliases.get(&head) {\n            if self.db.has_array_value(\"BASH_ALIASES\", &head) {\n                let value = self.db.get_elem(\"BASH_ALIASES\", &head).unwrap_or_default();\n                *word = word.replacen(&head, &value, 1);\n                if history.contains(word) {\n                    return false;\n                }\n                ans = true;\n            }\n            prev_head = head;\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/ansi_c_str.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::escaped_char::EscapedChar;\nuse crate::elements::subword::simple::SimpleSubword;\nuse crate::elements::subword::Subword;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Clone, Debug)]\npub enum AnsiCToken {\n    Normal(String),\n    Oct(String),\n    Hex(String),\n    EmptyHex,\n    Unicode4(String),\n    Unicode8(String),\n    Control(char),\n    OtherEscaped(String),\n}\n\nimpl AnsiCToken {\n    pub fn render(&mut self) -> String {\n        match &self {\n            AnsiCToken::EmptyHex => String::new(),\n            AnsiCToken::Normal(s) => s.clone(),\n            AnsiCToken::Oct(s) => {\n                let mut num = u32::from_str_radix(s, 8).unwrap();\n                if num >= 256 {\n                    num -= 256;\n                }\n\n                if num >= 128 {\n                    num += 0xE000;\n                    char::from_u32(num).unwrap().to_string()\n                } else {\n                    char::from(num as u8).to_string()\n                }\n            }\n            AnsiCToken::Hex(s) => {\n                let hex = match s.len() > 2 {\n                    true => s[s.len() - 2..].to_string(),\n                    false => s.to_string(),\n                };\n\n                let mut num = match u32::from_str_radix(&hex, 16) {\n                    Ok(n) => n,\n                    _ => return String::new(),\n                };\n                if num >= 256 {\n                    num -= 256;\n                }\n\n                if num >= 128 {\n                    num += 0xE000;\n                    char::from_u32(num).unwrap().to_string()\n                } else {\n                    char::from(num as u8).to_string()\n                }\n            }\n            AnsiCToken::Unicode4(s) => {\n                let num = u32::from_str_radix(s, 16).unwrap();\n                match char::from_u32(num) {\n                    Some(c) => c.to_string(),\n                    _ => \"U+\".to_owned() + s,\n                }\n            }\n            AnsiCToken::Unicode8(s) => {\n                let num = u64::from_str_radix(s, 16).unwrap();\n                let mut ans = String::new();\n                for i in (0..4).rev() {\n                    let n = (((num >> (i * 8)) & 0xFF) + 0xE000 + ((i + 1) << 8)) as u32;\n                    ans.push(char::from_u32(n).unwrap());\n                }\n                //unsafe { char::from_u32_unchecked(num as u32) }.to_string()\n                ans\n            }\n            AnsiCToken::Control(c) => {\n                let num = if *c == '@' {\n                    0\n                } else if *c == '[' {\n                    27\n                } else if *c == '\\\\' {\n                    28\n                } else if *c == ']' {\n                    29\n                } else if *c == '^' {\n                    30\n                } else if *c == '_' {\n                    31\n                } else if *c == '?' {\n                    127\n                } else if c.is_ascii_digit() {\n                    *c as u32 - 32\n                } else if c.is_ascii_lowercase() {\n                    *c as u32 - 96\n                } else if c.is_ascii_uppercase() {\n                    *c as u32 - 64\n                } else if *c as u32 >= 32 {\n                    *c as u32 - 32\n                } else {\n                    *c as u32\n                };\n\n                char::from_u32(num).unwrap().to_string()\n            }\n            AnsiCToken::OtherEscaped(s) => match s.as_ref() {\n                \"a\" => char::from(7).to_string(),\n                \"b\" => char::from(8).to_string(),\n                \"e\" | \"E\" => char::from(27).to_string(),\n                \"f\" => char::from(12).to_string(),\n                \"n\" => \"\\n\".to_string(),\n                \"r\" => \"\\r\".to_string(),\n                \"t\" => \"\\t\".to_string(),\n                \"v\" => char::from(11).to_string(),\n                \"\\\\\" => \"\\\\\".to_string(),\n                \"'\" => \"'\".to_string(),\n                \"\\\"\" => \"\\\"\".to_string(),\n                _ => (\"\\\\\".to_owned() + s).to_string(),\n            },\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct AnsiCString {\n    pub text: String,\n    pub tokens: Vec<AnsiCToken>,\n}\n\nimpl AnsiCString {\n    pub fn eval(&mut self) -> String {\n        let mut ans = String::new();\n        for t in &mut self.tokens {\n            if let AnsiCToken::EmptyHex = t {\n                break;\n            }\n            ans += &t.render();\n        }\n        ans\n    }\n\n    fn eat_simple_subword(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        if let Some(a) = SimpleSubword::parse(feeder) {\n            ans.text += a.get_text();\n            ans.tokens\n                .push(AnsiCToken::Normal(a.get_text().to_string()));\n            true\n        } else {\n            false\n        }\n    }\n\n    fn eat_oct(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let mut len = feeder.scanner_ansi_c_oct(core);\n        if len < 2 {\n            return false;\n        }\n\n        if len > 4 {\n            len = 4;\n        }\n\n        let token = feeder.consume(len);\n        ans.text += &token.clone();\n        ans.tokens.push(AnsiCToken::Oct(token[1..].to_string()));\n        true\n    }\n\n    fn eat_hex_braced(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        if !feeder.starts_with(\"\\\\x{\") {\n            return false;\n        }\n\n        if feeder.starts_with(\"\\\\x{}\") {\n            ans.text += &feeder.consume(4);\n            ans.tokens.push(AnsiCToken::EmptyHex);\n            return true;\n        }\n\n        let mut len = feeder.scanner_ansi_c_hex(core);\n        if len < 2 {\n            return false;\n        }\n\n        if len < 4 {\n            if feeder.len() > len {\n                len += 1;\n            } else {\n                return false;\n            }\n        }\n\n        let mut token = feeder.consume(len);\n        ans.text += &token.clone();\n        token.remove(2);\n        token.retain(|c| c != '}');\n        let ln = token.len();\n        if ln == 2 {\n        } else if ln == 3 {\n            ans.tokens\n                .push(AnsiCToken::Hex(token[ln - 1..ln].to_string()));\n        } else {\n            ans.tokens\n                .push(AnsiCToken::Hex(token[ln - 2..ln].to_string()));\n        }\n        true\n    }\n\n    fn eat_hex(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let mut len = feeder.scanner_ansi_c_hex(core);\n        if len < 3 {\n            return false;\n        }\n\n        if len > 4 {\n            len = 4;\n        }\n\n        let token = feeder.consume(len);\n        ans.text += &token.clone();\n        ans.tokens.push(AnsiCToken::Hex(token[2..].to_string()));\n        true\n    }\n\n    fn eat_unicode4(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let mut len = feeder.scanner_ansi_unicode4(core);\n        if len < 3 {\n            return false;\n        }\n\n        if len > 6 {\n            len = 6;\n        }\n\n        let token = feeder.consume(len);\n        ans.text += &token.clone();\n        ans.tokens\n            .push(AnsiCToken::Unicode4(token[2..].to_string()));\n        true\n    }\n\n    fn eat_unicode8(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let mut len = feeder.scanner_ansi_unicode8(core);\n        if len < 3 {\n            return false;\n        }\n\n        if len > 10 {\n            len = 10;\n        }\n\n        let token = feeder.consume(len);\n        ans.text += &token.clone();\n        ans.tokens\n            .push(AnsiCToken::Unicode8(token[2..].to_string()));\n        true\n    }\n\n    fn eat_escaped_char(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n        for_echo: bool,\n    ) -> bool {\n        if let Some(a) = EscapedChar::parse(feeder, core) {\n            let txt = a.get_text().to_string();\n            ans.text += &txt.clone();\n\n            if for_echo && txt == \"\\\\'\" {\n                ans.tokens.push(AnsiCToken::Normal(txt));\n            } else if txt != \"\\\\c\" || feeder.is_empty() {\n                ans.tokens\n                    .push(AnsiCToken::OtherEscaped(txt[1..].to_string()));\n            } else if let Some(a) = EscapedChar::parse(feeder, core) {\n                let mut text_after = a.get_text().to_string();\n                ans.text += &text_after.clone();\n                text_after.remove(0);\n                let ctrl_c = text_after.chars().next().unwrap();\n                ans.tokens.push(AnsiCToken::Control(ctrl_c));\n            } else if feeder.starts_with(\"'\") {\n                ans.tokens.push(AnsiCToken::Normal(\"\\\\c\".to_string()));\n            } else {\n                let ctrl_c = feeder.consume(1).chars().next().unwrap();\n                ans.text += &ctrl_c.to_string();\n                ans.tokens.push(AnsiCToken::Control(ctrl_c));\n            }\n            true\n        } else {\n            false\n        }\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        for_echo: bool,\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n\n        loop {\n            if !for_echo && feeder.starts_with(\"'\") {\n                break;\n            }\n\n            if Self::eat_simple_subword(feeder, &mut ans)\n                || Self::eat_hex_braced(feeder, &mut ans, core)\n                || Self::eat_hex(feeder, &mut ans, core)\n                || Self::eat_oct(feeder, &mut ans, core)\n                || Self::eat_unicode4(feeder, &mut ans, core)\n                || Self::eat_unicode8(feeder, &mut ans, core)\n                || Self::eat_escaped_char(feeder, &mut ans, core, for_echo)\n            {\n                continue;\n            }\n\n            if feeder.is_empty() {\n                if for_echo {\n                    break;\n                }\n                feeder.feed_additional_line(core)?;\n                continue;\n            }\n\n            let other = feeder.consume(1);\n            ans.text += &other.clone();\n            ans.tokens.push(AnsiCToken::Normal(other));\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/command/arithmetic.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct ArithmeticCommand {\n    pub text: String,\n    expressions: Vec<ArithmeticExpr>,\n    redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for ArithmeticCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        let mut err = None;\n\n        let exit_status = match self.eval(core) {\n            Ok(n) => {\n                if n == \"0\" {\n                    1\n                } else {\n                    0\n                }\n            }\n            Err(e) => {\n                err = Some(e);\n                1\n            }\n        };\n\n        core.db.exit_status = exit_status;\n\n        match err {\n            Some(ExecError::ArithError(s, e)) => {\n                if s.starts_with(\"$(\") {\n                    ExecError::ArithError(s.clone(), e.clone()).print(core);\n                    Err(ExecError::ArithError(s, e))\n                } else {\n                    let err_with_com = ExecError::ArithError(\"((: \".to_owned() + s.trim_start(), e);\n                    err_with_com.print(core);\n                    Err(err_with_com)\n                }\n            }\n            Some(e) => {\n                e.print(core);\n                Err(e.clone())\n            }\n            _ => Ok(()),\n        }\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl ArithmeticCommand {\n    pub fn eval(&mut self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let mut ans = String::new();\n        for a in &mut self.expressions {\n            ans = a.eval(core)?;\n        }\n\n        Ok(ans)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"((\") {\n            return Ok(None);\n        }\n        feeder.set_backup();\n\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            text: feeder.consume(2),\n            ..Default::default()\n        };\n\n        if let Some(c) = ArithmeticExpr::parse(feeder, core, true, \"((\")? {\n            if feeder.starts_with(\"))\") {\n                ans.text += &c.text;\n                ans.text += &feeder.consume(2);\n                ans.expressions.push(c);\n                feeder.pop_backup();\n                return Ok(Some(ans));\n            }\n        }\n        feeder.rewind();\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/command/brace.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::exit;\nuse crate::{Feeder, Script, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct BraceCommand {\n    text: String,\n    script: Option<Script>,\n    redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for BraceCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        match self.script {\n            Some(ref mut s) => s.exec(core)?,\n            _ => exit::internal(\" (ParenCommand::exec)\"),\n        }\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n\n    fn pretty_print(&mut self, indent_num: usize) {\n        println!(\"{{ \");\n        for s in self.script.iter_mut() {\n            s.pretty_print(indent_num + 1);\n        }\n        println!(\"}}\");\n    }\n}\n\nimpl BraceCommand {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            ..Default::default()\n        };\n        if command::eat_inner_script(feeder, core, \"{\", vec![\"}\"], &mut ans.script, false)? {\n            ans.text.push('{');\n            ans.text.push_str(&ans.script.as_ref().unwrap().get_text());\n            ans.text.push_str(&feeder.consume(1));\n\n            command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n            Ok(Some(ans))\n        } else {\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/case.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::glob;\nuse crate::{Feeder, Script, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct CaseCommand {\n    pub text: String,\n    pub word: Option<Word>,\n    pub patterns_script_end: Vec<(Vec<Word>, Script, String)>,\n    pub redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for CaseCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        core.db\n            .set_param(\"LINENO\", &self.lineno.to_string(), None)?;\n        let mut next = false;\n        let word = self.word.clone().unwrap();\n\n        if core.db.flags.contains('x') {\n            let ps4 = core.get_ps4();\n            eprintln!(\"\\r{} case {} in\\r\", ps4, word.text);\n        }\n\n        let w = match word.eval_for_case_word(core) {\n            Some(w) => w,\n            _ => \"\".to_string(),\n        };\n\n        let extglob = core.shopts.query(\"extglob\");\n\n        for e in &mut self.patterns_script_end {\n            for pattern in &mut e.0 {\n                let mut exec_script = false;\n                if !next {\n                    let p = match pattern.eval_for_case_pattern(core) {\n                        Ok(p) => p,\n                        Err(e) => {\n                            core.db.exit_status = 1;\n                            e.print(core); //TODO: it should be output at a higher level\n                            return Err(e);\n                        }\n                    };\n                    exec_script = glob::parse_and_compare(&w, &p, extglob);\n                }\n\n                if next || exec_script {\n                    let _ = e.1.exec(core);\n\n                    if e.2 == \";;\" {\n                        return Ok(());\n                    }\n                    next = e.2 == \";&\";\n                }\n            }\n        }\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl CaseCommand {\n    /*\n    fn new() -> Self {\n        CaseCommand {\n            text: String::new(),\n            word: None,\n            patterns_script_end: vec![],\n            redirects: vec![],\n            force_fork: false,\n        }\n    }*/\n\n    fn eat_word(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n        let w = match Word::parse(feeder, core, None)? {\n            Some(w) => w,\n            _ => return Ok(false),\n        };\n\n        ans.text += &w.text;\n        ans.word = Some(w);\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n        Ok(true)\n    }\n\n    fn eat_patterns(\n        feeder: &mut Feeder,\n        ans: &mut Vec<Word>,\n        text: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if feeder.starts_with(\"(\") {\n            *text += &feeder.consume(1);\n        }\n\n        command::eat_blank_with_comment(feeder, core, text);\n        loop {\n            if let Some(w) = Word::parse(feeder, core, None)? {\n                *text += &w.text;\n                ans.push(w)\n            } else {\n                return Ok(false);\n            }\n\n            command::eat_blank_with_comment(feeder, core, text);\n\n            if feeder.starts_with(\")\") {\n                break;\n            }\n\n            if feeder.starts_with(\"|\") {\n                *text += &feeder.consume(1);\n                command::eat_blank_with_comment(feeder, core, text);\n            }\n        }\n\n        Ok(!ans.is_empty())\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<Option<CaseCommand>, ParseError> {\n        if !feeder.starts_with(\"case\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            text: feeder.consume(4),\n            ..Default::default()\n        };\n\n        command::eat_blank_lines(feeder, core, &mut ans.text)?;\n        if !Self::eat_word(feeder, &mut ans, core)? {\n            return Ok(None);\n        }\n\n        command::eat_blank_lines(feeder, core, &mut ans.text)?;\n        if !feeder.starts_with(\"in\") {\n            return Ok(None);\n        }\n        ans.text += &feeder.consume(2);\n\n        loop {\n            command::eat_blank_lines(feeder, core, &mut ans.text)?;\n            if feeder.starts_with(\"esac\") {\n                break;\n            }\n\n            let mut patterns = vec![];\n            if !Self::eat_patterns(feeder, &mut patterns, &mut ans.text, core)? {\n                break;\n            }\n\n            let mut script = None;\n            if command::eat_inner_script(\n                feeder,\n                core,\n                \")\",\n                vec![\";;&\", \";;\", \";&\", \"esac\"],\n                &mut script,\n                true,\n            )? {\n                ans.text.push(')');\n                ans.text.push_str(&script.as_ref().unwrap().get_text());\n\n                if feeder.starts_with(\"esac\") {\n                    ans.patterns_script_end\n                        .push((patterns, script.unwrap(), \"\".to_string()));\n                    break;\n                }\n\n                let end_len = if feeder.starts_with(\";;&\") { 3 } else { 2 };\n                let end = feeder.consume(end_len);\n                ans.text.push_str(&end);\n                ans.patterns_script_end\n                    .push((patterns, script.unwrap(), end));\n            } else {\n                return Ok(None);\n            }\n        }\n\n        if feeder.starts_with(\"esac\") {\n            ans.text += &feeder.consume(4);\n            if !ans.patterns_script_end.is_empty() {\n                command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n                ans.lineno = feeder.lineno;\n                return Ok(Some(ans));\n            }\n        }\n\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/command/coproc.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse libc;\nuse super::{Command, Pipe, Redirect};\nuse crate::elements::command;\nuse crate::elements::command::{\n    BraceCommand, IfCommand, ParenCommand, SimpleCommand, WhileCommand\n};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils;\nuse crate::{Feeder, ShellCore};\n//use nix::unistd;\nuse nix::unistd::Pid;\nuse nix::sys::wait;\nuse nix::sys::wait::WaitStatus;\nuse std::thread;\nuse crate::core::jobtable::JobEntry;\n\n#[derive(Debug, Clone, Default)]\npub struct Coprocess {\n    pub text: String,\n    name: String,\n    command: Option<Box<dyn Command>>,\n    force_fork: bool,\n    _dummy: Vec<Redirect>,\n    lineno: usize,\n}\n\nimpl Command for Coprocess {\n    fn exec(&mut self, core: &mut ShellCore, _: &mut Pipe) -> Result<Option<Pid>, ExecError> {\n        if core.break_counter > 0 || core.continue_counter > 0 {\n            return Ok(None);\n        }\n        core.jobtable_check_status()?;\n\n        let backup = core.tty_fd.clone();\n        core.tty_fd = None;\n        let pgid = Pid::from_raw(0);\n        let mut com = self.command.clone().unwrap().clone();\n        com.set_force_fork();\n\n        let mut prevp = Pipe::new(\"|\".to_string());\n        prevp.set(-1, pgid, core);\n\n        prevp.send = unsafe{libc::fcntl(prevp.send, libc::F_DUPFD_CLOEXEC, 60)};\n        prevp.recv = unsafe{libc::fcntl(prevp.recv, libc::F_DUPFD_CLOEXEC, 60)};\n\n        let mut lastp = Pipe::new(\"|\".to_string());\n        lastp.set(prevp.recv, pgid, core);\n\n        lastp.send = unsafe{libc::fcntl(lastp.send, libc::F_DUPFD_CLOEXEC, 60)};\n        lastp.recv = unsafe{libc::fcntl(lastp.recv, libc::F_DUPFD_CLOEXEC, 60)};\n\n        let pid = com.exec(core, &mut lastp)?.unwrap();\n        let fds = vec![lastp.recv.clone(), prevp.send.clone()];\n        let fds_str = Some(vec![lastp.recv.to_string(), prevp.send.to_string()]);\n\n        let _ = core.db.init_array(&self.name, fds_str, Some(0), true);\n        let pid_name = format!(\"{}_PID\", &self.name);\n        let _ = core.db.set_param(&pid_name, &pid.to_string(), Some(0));\n        let _ = core.db.set_flag(&pid_name, 'i', 0);\n\n        core.fds.close(lastp.send);\n        core.fds.close(prevp.recv);\n\n        let _ = core.db.set_param(\"!\", &pid.to_string(), None);\n        let new_job_id = core.generate_new_job_id();\n\n        if core.db.flags.contains('i') {\n            eprintln!(\"[{}] {}\", &new_job_id, &pid);\n        }\n        core.job_table_priority.insert(0, new_job_id);\n        let mut entry = JobEntry::new(\n            vec![Some(pid)],\n            &vec![WaitStatus::StillAlive; 1],\n            &self.get_one_line_text(),\n            \"Running\",\n            new_job_id,\n        );\n        entry.coproc_name = Some(self.name.clone());\n        entry.coproc_fds = fds.clone();\n\n        if let Some(pid) = core.get_jobentry_pid_by_coproc_name(&self.name) {\n            let msg = format!(\"warning: execute_coproc: coproc [{}:{}] still exists\",\n                              &pid, &self.name);\n            let err = ExecError::Other(msg);\n            err.print(core);\n        }\n\n        if !core.options.query(\"monitor\") {\n            entry.no_control = true;\n        }\n\n        core.job_table.push(entry);\n        core.tty_fd = backup;\n\n        thread::spawn(move || {\n            match wait::waitpid(pid, None) {\n                Ok(WaitStatus::Exited(_, _)) | Err(_) => {\n                    let _ = unsafe{libc::close(fds[0])};\n                    let _ = unsafe{libc::close(fds[1])};\n                    return;\n                }\n                _ => {},\n            }\n        });\n\n        Ok(None)\n    }\n\n    fn run(&mut self, _: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        Ok(())\n    }\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self._dummy\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n\n    fn pretty_print(&mut self, indent_num: usize) {\n        self.pretty_print(indent_num);\n    }\n}\n\nimpl Coprocess {\n    pub fn pretty_print(&mut self, indent_num: usize) {\n        println!(\"{} () \", self.name);\n        for com in self.command.iter_mut() {\n            com.pretty_print(indent_num);\n        }\n    }\n\n    fn eat_header(&mut self, feeder: &mut Feeder, core: &mut ShellCore) -> Result<(), ParseError> {\n        self.text += &feeder.consume(6);\n        command::eat_blank_with_comment(feeder, core, &mut self.text);\n\n        let len = feeder.scanner_name(core);\n        self.name = feeder.consume(len).to_string();\n        self.text += &self.name;\n\n        if utils::reserved(&self.name) {\n            return Err(ParseError::UnexpectedSymbol(\"coproc\".to_string()));\n        }else if self.name.is_empty() {\n            self.name = \"COPROC\".to_string();\n        }\n\n        command::eat_blank_with_comment(feeder, core, &mut self.text);\n        Ok(())\n    }\n\n    fn eat_body(&mut self, feeder: &mut Feeder, core: &mut ShellCore) -> Result<(), ParseError> {\n        self.command = if let Some(a) = IfCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else if let Some(a) = ParenCommand::parse(feeder, core, false)? {\n            Some(Box::new(a))\n        } else if let Some(a) = BraceCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else if let Some(a) = WhileCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else {\n            None\n        };\n\n        if let Some(c) = &self.command {\n            self.text += &c.get_text();\n        }\n        Ok(())\n    }\n\n    fn parse_simple_command(feeder: &mut Feeder,\n        core: &mut ShellCore\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n        ans.text += &feeder.consume(6);\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n\n        ans.command = if let Some(a) = SimpleCommand::parse(feeder, core)? {\n            ans.text += &a.get_text();\n            ans.name = \"COPROC\".to_string();\n\n            Some(a)\n        }else{\n            return Err(ParseError::UnexpectedSymbol(\"coproc\".to_string()));\n        };\n\n        Ok(Some(ans))\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if ! feeder.starts_with(\"coproc\") {\n            return Ok(None);\n        }\n        let mut ans = Self::default();\n        ans.lineno = feeder.lineno;\n\n        feeder.set_backup();\n\n        ans.eat_header(feeder, core)?;\n        ans.eat_body(feeder, core)?;\n\n        if ans.command.is_some() {\n            feeder.pop_backup();\n            Ok(Some(ans))\n        } else {\n            feeder.rewind();\n            Self::parse_simple_command(feeder, core)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/for.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{Feeder, Script, ShellCore};\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse std::sync::atomic::Ordering::Relaxed;\n\n#[derive(Debug, Clone, Default)]\npub struct ForCommand {\n    text: String,\n    name: String,\n    has_in: bool,\n    has_arithmetic: bool,\n    values: Vec<Word>,\n    arithmetics: Vec<Option<ArithmeticExpr>>,\n    do_script: Option<Script>,\n    redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for ForCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        core.loop_level += 1;\n\n        let ok = match self.has_arithmetic {\n            true => self.run_with_arithmetic(core),\n            false => self.run_with_values(core),\n        };\n\n        if !ok && core.db.exit_status == 0 {\n            core.db.exit_status = 1;\n        }\n\n        core.loop_level -= 1;\n        if core.loop_level == 0 {\n            core.break_counter = 0;\n        }\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl ForCommand {\n    fn eval_values(&mut self, core: &mut ShellCore) -> Option<Vec<String>> {\n        let mut ans = vec![];\n        for w in &mut self.values {\n            match w.eval(core) {\n                Ok(mut ws) => ans.append(&mut ws),\n                Err(e) => {\n                    e.print(core);\n                    return None;\n                }\n            }\n        }\n\n        Some(ans)\n    }\n\n    fn run_with_values(&mut self, core: &mut ShellCore) -> bool {\n        let values = match self.has_in {\n            true => match self.eval_values(core) {\n                Some(vs) => vs,\n                None => return false,\n            },\n            false => core.db.get_position_params(),\n        };\n\n        for p in values {\n            if core.sigint.load(Relaxed) {\n                return false;\n            }\n            if core.return_flag {\n                return false;\n            }\n\n            if core.db.has_flag(&self.name, 'n') {\n                if let Err(e) = core.db.set_nameref(&self.name, &p, None) {\n                    core.db.exit_status = 1;\n                    e.print(core);\n                }\n            }else{\n                if let Err(e) = core.db.set_param(&self.name, &p, None) {\n                    core.db.exit_status = 1;\n                    e.print(core);\n                }\n            }\n\n            if core.continue_counter > 0 {\n                core.continue_counter -= 1;\n            }\n\n            if let Some(mut s) = self.do_script.clone() {\n                let _ = s.exec(core);\n            }\n\n            if core.break_counter > 0 {\n                core.break_counter -= 1;\n                break;\n            }\n            if core.continue_counter > 1 {\n                break;\n            }\n        }\n        if core.continue_counter > 0 {\n            core.continue_counter -= 1;\n        }\n        true\n    }\n\n    fn eval_arithmetic(a: &mut Option<ArithmeticExpr>, core: &mut ShellCore) -> (bool, String) {\n        match a {\n            None => (true, \"1\".to_string()),\n            Some(arith) => match arith.eval(core) {\n                Ok(n) => (true, n),\n                _ => (false, \"0\".to_string()),\n            },\n        }\n    }\n\n    fn run_with_arithmetic(&mut self, core: &mut ShellCore) -> bool {\n        let (ok, _) = Self::eval_arithmetic(&mut self.arithmetics[0], core);\n        if !ok {\n            return false;\n        }\n\n        while !core.return_flag {\n            if core.sigint.load(Relaxed) {\n                return false;\n            }\n\n            let (ok, val) = Self::eval_arithmetic(&mut self.arithmetics[1], core);\n            if val == \"0\" {\n                return ok;\n            }\n\n            if core.continue_counter > 0 {\n                core.continue_counter -= 1;\n            }\n\n            if let Some(mut s) = self.do_script.clone() {\n                let _ = s.exec(core);\n            }\n\n            if core.break_counter > 0 {\n                core.break_counter -= 1;\n                break;\n            }\n            if core.continue_counter > 1 {\n                break;\n            }\n\n            let (ok, _) = Self::eval_arithmetic(&mut self.arithmetics[2], core);\n            if !ok {\n                return false;\n            }\n        }\n        if core.continue_counter > 0 {\n            core.continue_counter -= 1;\n        }\n        true\n    }\n\n    fn eat_name(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n\n        let len = feeder.scanner_name(core);\n        if len == 0 {\n            return false;\n        }\n\n        ans.name = feeder.consume(len);\n        ans.text += &ans.name.clone();\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n        true\n    }\n\n    fn eat_arithmetic(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if !feeder.starts_with(\"((\") {\n            return Ok(false);\n        }\n        ans.text += &feeder.consume(2);\n        ans.has_arithmetic = true;\n\n        loop {\n            command::eat_blank_lines(feeder, core, &mut ans.text)?;\n\n            let a = ArithmeticExpr::parse(feeder, core, true, \"((\")?;\n            if let Some(ref arith) = a {\n                ans.text += &arith.text;\n            }\n            ans.arithmetics.push(a);\n\n            command::eat_blank_with_comment(feeder, core, &mut ans.text);\n            if feeder.starts_with(\";\") {\n                if ans.arithmetics.len() >= 3 {\n                    return Ok(false);\n                }\n                ans.text += &feeder.consume(1);\n            } else if feeder.starts_with(\"))\") {\n                if ans.arithmetics.len() != 3 {\n                    return Ok(false);\n                }\n                ans.text += &feeder.consume(2);\n                return Ok(ans.arithmetics.len() == 3);\n            } else {\n                return Ok(false);\n            }\n        }\n    }\n\n    fn eat_in_part(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        if !feeder.starts_with(\"in\") {\n            return Ok(());\n        }\n\n        ans.text += &feeder.consume(2);\n        ans.has_in = true;\n\n        loop {\n            command::eat_blank_with_comment(feeder, core, &mut ans.text);\n            match Word::parse(feeder, core, None)? {\n                Some(w) => {\n                    ans.text += &w.text.clone();\n                    ans.values.push(w);\n                }\n                _ => return Ok(()),\n            }\n        }\n    }\n\n    fn eat_end(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n        if feeder.starts_with(\";\") || feeder.starts_with(\"\\n\") {\n            ans.text += &feeder.consume(1);\n            command::eat_blank_with_comment(feeder, core, &mut ans.text);\n            true\n        } else {\n            false\n        }\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"for\") {\n            return Ok(None);\n        }\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            text: feeder.consume(3),\n            ..Default::default()\n        };\n\n        if Self::eat_name(feeder, &mut ans, core) {\n            Self::eat_in_part(feeder, &mut ans, core)?;\n        } else if !Self::eat_arithmetic(feeder, &mut ans, core)? {\n            return Ok(None);\n        }\n\n        if !Self::eat_end(feeder, &mut ans, core) {\n            return Ok(None);\n        }\n\n        command::eat_blank_lines(feeder, core, &mut ans.text)?;\n\n        if command::eat_inner_script(feeder, core, \"do\", vec![\"done\"], &mut ans.do_script, false)? {\n            ans.text.push_str(\"do\");\n            if let Some(ref mut s) = ans.do_script {\n                ans.text.push_str(&s.get_text());\n            }\n            ans.text.push_str(&feeder.consume(4)); //done\n\n            command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n            Ok(Some(ans))\n        } else {\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/function_def.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Pipe, Redirect};\nuse crate::elements::command;\nuse crate::elements::command::{BraceCommand, IfCommand, ParenCommand, WhileCommand};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils;\nuse crate::{Feeder, ShellCore};\nuse nix::unistd::Pid;\n\n#[derive(Debug, Clone, Default)]\npub struct FunctionDefinition {\n    pub text: String,\n    pub file: String,\n    name: String,\n    command: Option<Box<dyn Command>>,\n    force_fork: bool,\n    _dummy: Vec<Redirect>,\n    lineno: usize,\n}\n\nimpl Command for FunctionDefinition {\n    fn exec(&mut self, core: &mut ShellCore, _: &mut Pipe) -> Result<Option<Pid>, ExecError> {\n        if core.break_counter > 0 || core.continue_counter > 0 {\n            return Ok(None);\n        }\n\n        core.db\n            .functions\n            .insert(self.name.to_string(), self.clone());\n        Ok(None)\n    }\n\n    fn run(&mut self, _: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        Ok(())\n    }\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self._dummy\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n\n    fn pretty_print(&mut self, indent_num: usize) {\n        self.pretty_print(indent_num);\n    }\n}\n\nimpl FunctionDefinition {\n    pub fn pretty_print(&mut self, indent_num: usize) {\n        println!(\"{} () \", self.name);\n        for com in self.command.iter_mut() {\n            com.pretty_print(indent_num);\n        }\n    }\n\n    pub fn run_as_command(&mut self, args: &mut [String], core: &mut ShellCore) {\n        if ! core.db.exist(\"FUNCNAME\") {\n            if  core.script_name == \"-\" {\n                let _ = core.db.init_array(\"FUNCNAME\", None, Some(0), false);\n            }else {\n                let _ = core.db.init_array(\"FUNCNAME\", Some(vec![\"main\".to_string()]),\n                                           Some(0), false);\n            }\n        }\n\n        let mut array = core.db.get_vec(\"FUNCNAME\", false).unwrap(); //TODO: implement array push\n        array.insert(0, args[0].clone()); //TODO: We must put the name not only in 0 but also 1..\n        let _ = core.db.init_array(\"FUNCNAME\", Some(array.clone()), None, false);\n\n        let mut linenos = core.db.get_vec(\"BASH_LINENO\", false).unwrap();\n        let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"0\".to_string());\n        linenos.insert(0, lineno.to_string());\n        let _ = core.db.init_array(\"BASH_LINENO\", Some(linenos.clone()), None, false);\n\n        let mut source = core.db.get_vec(\"BASH_SOURCE\", false).unwrap();\n        source.insert(0, self.file.clone());\n        let _ = core.db.init_array(\"BASH_SOURCE\", Some(source.clone()), None, false);\n\n        args[0] = core.db.position_parameters[0][0].clone();\n        core.db.position_parameters.push(args.to_vec());\n\n        let mut dummy = Pipe::new(\"|\".to_string());\n\n        core.source_function_level += 1;\n        if let Err(e) = self.command.as_mut().unwrap().exec(core, &mut dummy) {\n            e.print(core);\n        }\n        core.return_flag = false;\n        core.source_function_level -= 1;\n\n        core.db.position_parameters.pop();\n\n        array.remove(0);\n        if array.is_empty() \n        || ( core.script_name != \"-\" && array[0] == \"main\" ) {\n            let _ = core.db.unset(\"FUNCNAME\", Some(0), core.shopts.query(\"localvar_unset\"));\n        }else {\n            let _ = core.db.init_array(\"FUNCNAME\", Some(array), Some(0), false);\n        }\n\n        linenos.remove(0);\n        source.remove(0);\n        let _ = core.db.init_array(\"BASH_LINENO\", Some(linenos), None, false);\n        let _ = core.db.init_array(\"BASH_SOURCE\", Some(source), None, false);\n    }\n\n    fn eat_header(&mut self, feeder: &mut Feeder, core: &mut ShellCore) -> bool {\n        let has_function_keyword = feeder.starts_with(\"function\");\n        if has_function_keyword {\n            self.text += &feeder.consume(8);\n            command::eat_blank_with_comment(feeder, core, &mut self.text);\n        }\n\n        let len = feeder.scanner_name(core);\n        self.name = feeder.consume(len).to_string();\n\n        if self.name.is_empty() && utils::reserved(&self.name) {\n            return false;\n        }\n        self.text += &self.name;\n        command::eat_blank_with_comment(feeder, core, &mut self.text);\n\n        if feeder.starts_with(\"()\") {\n            self.text += &feeder.consume(2);\n        } else if !has_function_keyword {\n            return false;\n        }\n\n        let _ = command::eat_blank_lines(feeder, core, &mut self.text);\n        true\n    }\n\n    fn eat_body(&mut self, feeder: &mut Feeder, core: &mut ShellCore) -> Result<(), ParseError> {\n        self.command = if let Some(a) = IfCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else if let Some(a) = ParenCommand::parse(feeder, core, false)? {\n            Some(Box::new(a))\n        } else if let Some(a) = BraceCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else if let Some(a) = WhileCommand::parse(feeder, core)? {\n            Some(Box::new(a))\n        } else {\n            None\n        };\n\n        if let Some(c) = &self.command {\n            self.text += &c.get_text();\n        }\n        Ok(())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n        feeder.set_backup();\n        ans.lineno = feeder.lineno;\n\n        if !ans.eat_header(feeder, core) {\n            feeder.rewind();\n            return Ok(None);\n        }\n\n        if let Err(e) = ans.eat_body(feeder, core) {\n            feeder.rewind();\n            return Err(e);\n        }\n\n        if ans.command.is_some() {\n            feeder.pop_backup();\n            if let Some(f) = core.source_files.last() {\n                ans.file = f.clone();\n            }\n            Ok(Some(ans))\n        } else {\n            feeder.rewind();\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/if.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::exit;\nuse crate::{Feeder, Script, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct IfCommand {\n    pub text: String,\n    pub if_elif_scripts: Vec<Script>,\n    pub then_scripts: Vec<Script>,\n    pub else_script: Option<Script>,\n    pub redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for IfCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        for i in 0..self.if_elif_scripts.len() {\n            self.if_elif_scripts[i].exec(core)?;\n            if core.db.exit_status == 0 {\n                let _ = self.then_scripts[i].exec(core);\n                return Ok(());\n            } else {\n                core.db.exit_status = 0;\n            }\n        }\n\n        if let Some(s) = self.else_script.as_mut() {\n            s.exec(core)?\n        }\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl IfCommand {\n    fn end_words(word: &str) -> Result<Vec<&str>, ParseError> {\n        match word {\n            \"if\" | \"elif\" => Ok(vec![\"then\"]),\n            \"then\" => Ok(vec![\"fi\", \"else\", \"elif\"]),\n            \"else\" => Ok(vec![\"fi\"]),\n            unknown => Err(ParseError::UnexpectedSymbol(unknown.to_string())),\n        }\n    }\n\n    fn set_script(word: &str, ans: &mut IfCommand, script: Option<Script>) {\n        match word {\n            \"if\" | \"elif\" => ans.if_elif_scripts.push(script.unwrap()),\n            \"then\" => ans.then_scripts.push(script.unwrap()),\n            \"else\" => ans.else_script = script,\n            _ => exit::internal(\" (if parse error)\"),\n        };\n    }\n\n    fn eat_word_and_script(\n        word: &str,\n        feeder: &mut Feeder,\n        ans: &mut IfCommand,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        let mut s = None;\n        let ends = Self::end_words(word)?;\n        if !command::eat_inner_script(feeder, core, word, ends, &mut s, false)? {\n            return Ok(false);\n        }\n\n        ans.text.push_str(word);\n        ans.text.push_str(&s.as_ref().unwrap().get_text());\n        Self::set_script(word, ans, s);\n        Ok(true)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            ..Default::default()\n        };\n\n        let mut if_or_elif = \"if\";\n        while Self::eat_word_and_script(if_or_elif, feeder, &mut ans, core)?\n            && Self::eat_word_and_script(\"then\", feeder, &mut ans, core)?\n        {\n            Self::eat_word_and_script(\"else\", feeder, &mut ans, core)?; //optional\n\n            if feeder.starts_with(\"fi\") {\n                // If \"else\" exists, always it comes here.\n                ans.text.push_str(&feeder.consume(2));\n                break;\n            }\n\n            if_or_elif = \"elif\";\n        }\n\n        if ans.then_scripts.is_empty() {\n            return Ok(None);\n        }\n\n        command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/command/paren.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Pipe, Redirect};\nuse crate::elements::command;\nuse crate::elements::command::SimpleCommand;\nuse crate::elements::pipeline::Pipeline;\nuse crate::elements::job::Job;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::exit;\nuse crate::{Feeder, Script, ShellCore};\nuse nix::unistd::Pid;\n\nimpl From<SimpleCommand> for ParenCommand {\n    fn from(c: SimpleCommand) -> Self {\n        let mut pip = Pipeline::default();\n        pip.text = c.get_text();\n        pip.commands.push(c.boxed_clone());\n\n\n        let job = Job {\n            text: pip.text.clone(),\n            pipelines: vec![pip],\n            pipeline_ends: vec![\"\".to_string()]\n        };\n\n        let script = Script {\n            text: job.text.clone(),\n            jobs: vec![job.clone()],\n            job_ends: vec![\"\".to_string()],\n        };\n\n        ParenCommand {\n            text: script.text.clone(),\n            script: Some(script),\n            lineno: c.lineno,\n            ..Default::default()\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ParenCommand {\n    pub text: String,\n    pub script: Option<Script>,\n    redirects: Vec<Redirect>,\n    pub lineno: usize,\n}\n\nimpl Command for ParenCommand {\n    fn exec(&mut self, core: &mut ShellCore, pipe: &mut Pipe) -> Result<Option<Pid>, ExecError> {\n        if core.break_counter > 0 || core.continue_counter > 0 {\n            return Ok(None);\n        }\n\n        self.fork_exec(core, pipe)\n    }\n\n    fn run(&mut self, core: &mut ShellCore, fork: bool) -> Result<(), ExecError> {\n        if !fork {\n            exit::internal(\" (no fork for subshell)\");\n        }\n\n        match self.script {\n            Some(ref mut s) => s.exec(core)?,\n            _ => exit::internal(\" (ParenCommand::exec)\"),\n        }\n\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {}\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        true\n    }\n\n    fn get_one_line_text(&self) -> String {\n        match &self.script {\n            Some(s) => format!(\"( {} )\", s.get_one_line_text()),\n            None => \"()\".to_string(),\n        }\n    }\n}\n\nimpl ParenCommand {\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        substitution: bool,\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            ..Default::default()\n        };\n\n        let mut start = \"(\";\n        if substitution && feeder.starts_with(\"`\") {\n            start = \"`\";\n        }\n\n        if command::eat_inner_script(feeder, core, start, vec![\")\"], &mut ans.script, substitution)? {\n            ans.text.push_str(start);\n            ans.text.push_str(&ans.script.as_ref().unwrap().get_text());\n            ans.text.push_str(&feeder.consume(1));\n\n            if !substitution {\n                command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n            }\n            Ok(Some(ans))\n        } else {\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/repeat.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::elements::job::Job;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{utils, Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct RepeatCommand {\n    pub text: String,\n    times: Word,\n    job: Job,\n    force_fork: bool,\n    lineno: usize,\n    _dummy: Vec<Redirect>,\n}\n\nimpl Command for RepeatCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        let n = utils::string_to_calculated_string(&self.times.text, core)?.parse::<usize>()?;\n        /*\n        let mut f = Feeder::new(&self.times.text);\n\n        let n = match ArithmeticExpr::parse(&mut f, core, false, \"\")? {\n            Some(mut a) => a.eval(core)?.parse::<usize>()?,\n            None => 0,\n        };*/\n\n        for _ in 0..n {\n            self.job.clone().exec(core, false)?;\n        }\n\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self._dummy\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl RepeatCommand {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"repeat\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            text: feeder.consume(6),\n            ..Default::default()\n        };\n        command::eat_blank_with_comment(feeder, core, &mut ans.text);\n\n        ans.times = match Word::parse(feeder, core, None)? {\n            Some(w) => w,\n            _ => return Err(ParseError::UnexpectedSymbol(\"repeat\".to_string())),\n        };\n        let _ = command::eat_blank_lines(feeder, core, &mut ans.text);\n\n        ans.job = match Job::parse(feeder, core)? {\n            Some(j) => j,\n            _ => return Err(ParseError::UnexpectedSymbol(\"repeat\".to_string())),\n        };\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/command/simple/alias.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::SimpleCommand;\nuse crate::elements::command;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\npub fn set(\n    com: &mut SimpleCommand,\n    word: &Word,\n    core: &mut ShellCore,\n    feeder: &mut Feeder,\n) -> Result<bool, ParseError> {\n    com.continue_alias_check = false;\n    let mut w = word.text.clone();\n    if !core.replace_alias(&mut w) {\n        return Ok(false);\n    }\n\n    com.continue_alias_check = w.ends_with(\" \");\n    let mut feeder_local = Feeder::new(&w);\n\n    while com.eat_substitution(&mut feeder_local, core)? {\n        command::eat_blank_with_comment(&mut feeder_local, core, &mut com.text);\n    }\n\n    loop {\n        match Word::parse(&mut feeder_local, core, Some(WordMode::Alias)) {\n            Ok(Some(w)) => {\n                if w.text.starts_with(\"#\") && com.words.is_empty() {\n                    break;\n                }\n                com.text.push_str(&w.text);\n                com.words.push(w);\n            }\n            _ => break,\n        }\n        command::eat_blank_with_comment(&mut feeder_local, core, &mut com.text);\n    }\n\n    if let Some(lst) = com.words.last() {\n        if lst.text == \"\\\\\" {\n            com.words.pop();\n            feeder_local.replace(0, \"\\\\\");\n        }\n    }\n\n    feeder.replace(0, &feeder_local.consume(feeder_local.len()));\n\n    if com.words.is_empty() && com.substitutions.is_empty() {\n        com.invalid_alias = true;\n        return Ok(false);\n    }\n\n    Ok(true)\n}\n"
  },
  {
    "path": "src/elements/command/simple/hash.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::SimpleCommand;\nuse crate::elements::command::ExecError;\nuse crate::{utils, ShellCore};\n\npub fn get_and_regist(com: &mut SimpleCommand, core: &mut ShellCore) -> Result<String, ExecError> {\n    if [\"/\", \"./\", \"../\"]\n        .iter()\n        .any(|p| com.args[0].starts_with(p))\n    {\n        return Ok(com.args[0].clone());\n    }\n\n    let mut path = core.db.get_elem(\"BASH_CMDS\", &com.args[0])?;\n\n    if path.is_empty() {\n        path = resolve_path(&com.args[0], core)?;\n    }\n\n    count_up(&com.args[0], core);\n    Ok(path)\n}\n\nfn resolve_path(arg: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n    let path = utils::get_command_path(arg, core);\n    if path.is_empty() {\n        return Ok(path);\n    }\n\n    let restricted = core.db.flags.contains('r');\n    core.db.flags.retain(|f| f != 'r');\n    core.db.set_assoc_elem(\"BASH_CMDS\", arg, &path, None)?;\n    if restricted {\n        core.db.flags.push('r');\n    }\n\n    Ok(path)\n}\n\nfn count_up(arg: &str, core: &mut ShellCore) {\n    match core.db.hash_counter.get_mut(arg) {\n        Some(v) => *v += 1,\n        None => {\n            core.db.hash_counter.insert(arg.to_string(), 1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/simple/parser.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{alias, SimpleCommand, SubsArgType};\nuse crate::elements::command;\nuse crate::elements::command::{Command, ParenCommand};\nuse crate::elements::substitution::Substitution;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::parse::ParseError;\nuse crate::{utils, Feeder, ShellCore};\n\nimpl SimpleCommand {\n    pub fn eat_substitution(&mut self, feeder: &mut Feeder, core: &mut ShellCore)\n    -> Result<bool, ParseError> {\n        match Substitution::parse(feeder, core, false, false)? {\n            Some(s) => {\n                self.text += &s.text;\n                self.substitutions.push(s);\n                Ok(true)\n            }\n            None => Ok(false),\n        }\n    }\n\n    fn eat_substitution_as_arg(&mut self, feeder: &mut Feeder,core: &mut ShellCore)\n    -> Result<bool, ParseError> {\n        if let Some(s) = Substitution::parse(feeder, core, false, true)? {\n            self.text += &s.text;\n            self.substitutions_as_args\n                .push(SubsArgType::Subs(Box::new(s)));\n            return Ok(true);\n        }\n\n        if let Some(w) = Word::parse(feeder, core, None)? {\n            self.text += &w.text;\n            self.substitutions_as_args.push(SubsArgType::Other(w));\n            return Ok(true);\n        }\n\n        Ok(false)\n    }\n\n    fn eat_word(&mut self, feeder: &mut Feeder, core: &mut ShellCore)\n    -> Result<bool, ParseError> {\n        let mut mode = None;\n        if self.command_name == \"eval\" || self.command_name == \"let\" {\n            mode = Some(WordMode::EvalLet);\n        }\n\n        let w = match Word::parse(feeder, core, mode)? {\n            Some(w) => w,\n            _ => return Ok(false),\n        };\n\n        if self.words.is_empty() {\n            self.lineno = feeder.lineno;\n            if utils::reserved(&w.text) {\n                return Ok(false);\n            }\n\n            self.command_name = w.text.clone();\n        }else if self.command_name == \"command\" && self.words.len() == 1 {\n            self.lineno = feeder.lineno;\n            self.command_name = w.text.clone();\n        }\n\n        if (self.words.is_empty()\n            || self.continue_alias_check)\n            && alias::set(self, &w, core, feeder)?  {\n            return Ok(true);\n        }\n\n        self.text += &w.text;\n        self.words.push(w);\n\n        Ok(true)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore)\n    -> Result<Option<Box<dyn Command>>, ParseError> {\n        let mut ans = Self::default();\n        feeder.set_backup();\n\n        while command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?\n              || ans.eat_substitution(feeder, core)? {}\n\n        loop {\n            command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n\n            if core.subst_builtins.contains_key(&ans.command_name) && ans.eat_substitution_as_arg(feeder, core)? {\n                continue;\n            }\n\n            command::eat_blank_with_comment(feeder, core, &mut ans.text);\n            if ! ans.eat_word(feeder, core)? {\n                break;\n            }\n        }\n\n        if ans.invalid_alias {\n            feeder.pop_backup();\n            feeder.consume(feeder.len());\n            return Ok(None);\n        }\n\n        if ans.substitutions.len() + ans.words.len() + ans.redirects.len() > 0 {\n            feeder.pop_backup();\n\n            if ans.words.iter_mut().any(|w| w.is_to_proc_sub() ) {\n                return Ok(Some(Box::new(ParenCommand::from(ans))));\n            }\n\n//            ans.read_heredoc(feeder, core)?;//TODO: maybe required for every command\n            Ok(Some(Box::new(ans)))\n        } else {\n            feeder.rewind();\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command/simple/run_internal.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::SimpleCommand;\nuse super::SubsArgType;\nuse crate::elements::substitution::Substitution;\nuse crate::error::exec::ExecError;\nuse crate::{Feeder, ShellCore};\n\npub fn run(com: &mut SimpleCommand, core: &mut ShellCore) -> Result<bool, ExecError> {\n    let ans = run_function(&mut com.args, core)\n        || run_substitution_builtin(com, core)?\n        || run_builtin(com, core)?;\n    Ok(ans)\n}\n\nfn run_function(args: &mut [String], core: &mut ShellCore) -> bool {\n    match core.db.functions.get_mut(&args[0]) {\n        Some(f) => {\n            f.clone().run_as_command(args, core);\n            true\n        }\n        None => false,\n    }\n}\n\npub fn run_builtin(com: &mut SimpleCommand, core: &mut ShellCore) -> Result<bool, ExecError> {\n    if com.args.is_empty() {\n        eprintln!(\"ShellCore::run_builtin\");\n        return Ok(false);\n    }\n\n    if !core.builtins.contains_key(&com.args[0]) {\n        return Ok(false);\n    }\n\n    let func = core.builtins[&com.args[0]];\n    core.db.exit_status = func(core, &com.args[..]);\n    Ok(true)\n}\n\npub fn run_substitution_builtin(\n    com: &mut SimpleCommand,\n    core: &mut ShellCore,\n) -> Result<bool, ExecError> {\n    if !core.subst_builtins.contains_key(&com.args[0]) {\n        return Ok(false);\n    }\n\n    let mut args = vec![com.args[0].clone()];\n    let mut subs = vec![];\n    for sub in com.substitutions_as_args.iter_mut() {\n        match sub {\n            SubsArgType::Subs(s) => subs.push((**s).clone()),\n            SubsArgType::Other(w) => {\n                for arg in w.eval(core)? {\n                    if arg.starts_with(\"-\") || arg.starts_with(\"+\") {\n                        args.push(arg);\n                    }else{\n                        other_to_subst(&arg, core, &mut subs)?;\n                    }\n                }\n            }\n        }\n    }\n\n    let func = core.subst_builtins[&com.args[0]];\n    core.db.exit_status = func(core, &args[..], &mut subs[..]);\n    Ok(true)\n}\n\nfn other_to_subst(arg: &str,\n    core: &mut ShellCore,\n    subs: &mut Vec<Substitution>\n) -> Result<(), ExecError> {\n    let mut f = Feeder::new(&arg.replace(\"$\", \"\\\\$\"));\n\n    if let Some(mut s) = Substitution::parse(&mut f,\n                             core, true, false)? {\n        s.quoted = true;\n        subs.push(s);\n        return Ok(());\n    }\n\n    let mut s = Substitution::default();\n    s.text = arg.to_string();\n    s.left_hand.text = s.text.clone();\n    s.left_hand.name = s.text.clone();\n    s.quoted = true;\n    subs.push(s);\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/elements/command/simple.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod alias;\npub mod hash;\npub mod parser;\npub mod run_internal;\n\nuse crate::{proc_ctrl, ShellCore};\n\nuse super::{Command, Pipe, Redirect};\nuse crate::elements::substitution::Substitution;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::utils::exit;\nuse nix::unistd::Pid;\nuse std::sync::atomic::Ordering::Relaxed;\n\n#[derive(Debug, Clone)]\nenum SubsArgType {\n    Subs(Box<Substitution>),\n    Other(Word),\n}\n\nimpl SubsArgType {\n    pub fn get_text(&self) -> &str {\n        match self {\n            Self::Subs(e) => &e.text,\n            Self::Other(w) => &w.text,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct SimpleCommand {\n    text: String,\n    substitutions: Vec<Substitution>,\n    words: Vec<Word>,\n    pub args: Vec<String>,\n    redirects: Vec<Redirect>,\n    force_fork: bool,\n    substitutions_as_args: Vec<SubsArgType>,\n    command_name: String,\n    pub lineno: usize,\n    continue_alias_check: bool,\n    invalid_alias: bool,\n    command_path: String,\n}\n\nimpl Command for SimpleCommand {\n    fn exec(&mut self, core: &mut ShellCore, pipe: &mut Pipe) -> Result<Option<Pid>, ExecError> {\n        core.db\n            .set_param(\"LINENO\", &self.lineno.to_string(), None)?;\n        if Self::break_continue_or_return(core) {\n            return Ok(None);\n        }\n\n        core.db.set_param(\"BASH_COMMAND\", &self.text, None)?;\n\n        self.args.clear();\n        let mut words = self.words.to_vec();\n        for w in words.iter_mut() {\n            w.set_pipe(core); //for >()\n            self.set_arg(w, core)?;\n        }\n\n        if !self.args.is_empty() && self.args[0].starts_with(\"%\") {\n            self.redirects.clear();\n            self.args.insert(0, \"fg\".to_string());\n        }\n\n        match self.args.len() {\n            0 => self.exec_set_param(core),\n            _ => self.exec_command(core, pipe),\n        }\n    }\n\n    fn run(&mut self, core: &mut ShellCore, fork: bool) -> Result<(), ExecError> {\n        core.db.push_local();\n        let _ = self.set_local_params(core);\n\n        if !run_internal::run(self, core)? {\n            self.set_environment_variables(core)?;\n            proc_ctrl::exec_command(&self.args, core, &self.command_path);\n        };\n\n        core.db.pop_local();\n\n        match fork {\n            true => exit::normal(core),\n            false => Ok(()),\n        }\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n}\n\nimpl SimpleCommand {\n    fn break_continue_or_return(core: &mut ShellCore) -> bool {\n        core.break_counter > 0 || core.continue_counter > 0\n    }\n\n    pub fn exec_command(\n        &mut self,\n        core: &mut ShellCore,\n        pipe: &mut Pipe,\n    ) -> Result<Option<Pid>, ExecError> {\n        Self::check_sigint(core)?;\n\n        core.db.last_arg = self.args.last().unwrap().clone();\n        self.option_x_output(core);\n\n        if core.db.flags.contains('r') && self.args[0].contains('/') {\n            let msg = format!(\n                \"{}: restricted: cannot specify `/' in command names\",\n                &self.args[0]\n            );\n            return Err(ExecError::Other(msg));\n        }\n\n        if self.args[0] == \"command\" && self.args.len() > 1 {\n            if core.subst_builtins.contains_key(&self.args[1])\n            || core.db.functions.contains_key(&self.args[1]) {\n                self.args.remove(0);\n            }\n        }\n\n        let internal = core.builtins.contains_key(&self.args[0])\n                       || core.subst_builtins.contains_key(&self.args[0])\n                       || core.db.functions.contains_key(&self.args[0]);\n\n        if self.force_fork\n            || (!pipe.lastpipe && pipe.is_connected())\n            || !internal {\n            if ! internal {\n                self.command_path = hash::get_and_regist(self, core)?;\n            }\n            self.fork_exec(core, pipe)\n        } else if self.args.len() == 1 && self.args[0] == \"exec\" {\n            for r in self.get_redirects().iter_mut() {\n                if let Err(e) = r.connect(true, core) {\n                    e.print(core);\n                    core.db.exit_status = 1;\n                    break;\n                }\n            }\n            Ok(None)\n        } else {\n            if let Err(e) = pipe.connect_lastpipe(core) {\n                e.print(core);\n                core.db.exit_status = 1;\n            }\n            if let Err(e) = self.nofork_exec(core) {\n                e.print(core);\n                core.db.exit_status = 1;\n            }\n            Ok(None)\n        }\n    }\n\n    fn check_sigint(core: &mut ShellCore) -> Result<(), ExecError> {\n        if core.sigint.load(Relaxed) {\n            core.db.exit_status = 130;\n            return Err(ExecError::Interrupted);\n        }\n        Ok(())\n    }\n\n    fn exec_set_param(&mut self, core: &mut ShellCore) -> Result<Option<Pid>, ExecError> {\n        core.db.last_arg = String::new();\n        self.option_x_output(core);\n\n        for s in self.substitutions.iter_mut() {\n            if let Err(e) = s.eval(core, None, false) {\n                core.db.exit_status = 1;\n                if !core.db.flags.contains('i') {\n                    if let ExecError::SyntaxError(_) = e {\n                        e.print(core);\n                        let msg = \"`\".to_owned() + &s.text.clone() + \"'\";\n                        return Err(ExecError::Other(msg));\n                    }\n                }\n                return Err(e);\n            }\n        }\n\n        Ok(None)\n    }\n\n    fn set_local_params(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let scope = core.db.get_scope_num() - 1;\n        if core.options.query(\"posix\") {\n            for s in self.substitutions.clone().iter_mut() {\n                s.eval(core, None, false)?;\n            }\n        }\n        for s in self.substitutions.iter_mut() {\n            s.eval(core, Some(scope), false)?;\n        }\n\n        Ok(())\n    }\n\n    fn set_environment_variables(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let scope = core.db.get_scope_num() - 1;\n        core.db.set_scope_to_env(scope);\n        Ok(())\n    }\n\n    fn set_arg(&mut self, word: &mut Word, core: &mut ShellCore) -> Result<(), ExecError> {\n        match word.eval(core) {\n            Ok(ws) => {\n                self.args.extend(ws);\n                Ok(())\n            }\n            Err(e) => {\n                //   e.print(core);\n                if !core.sigint.load(Relaxed) {\n                    core.db.exit_status = 1;\n                }\n                Err(e)\n            }\n        }\n    }\n\n    fn option_x_output(&self, core: &mut ShellCore) {\n        if !core.db.flags.contains('x') {\n            return;\n        }\n\n        let ps4 = core.get_ps4();\n        for s in &self.substitutions {\n            eprintln!(\"\\r{} {}\\r\", &ps4, &s.text);\n        }\n\n        if self.args.is_empty() {\n            return;\n        }\n\n        eprint!(\"{}\", &ps4);\n        for a in &self.args {\n            match a.contains(\" \") {\n                false => eprint!(\" {}\", &a),\n                true => eprint!(\" '{}'\", &a),\n            }\n        }\n\n        for a in &self.substitutions_as_args {\n            match a.get_text().contains(\" \") {\n                false => eprint!(\" {}\", &a.get_text()),\n                true => eprint!(\" '{}'\", &a.get_text()),\n            }\n        }\n\n        eprintln!();\n    }\n}\n"
  },
  {
    "path": "src/elements/command/test.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::elements::expr::conditional::elem::CondElem;\nuse crate::elements::expr::conditional::ConditionalExpr;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct TestCommand {\n    text: String,\n    cond: Option<ConditionalExpr>,\n    redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for TestCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        if core.db.flags.contains('x') {\n            let ps4 = core.get_ps4();\n            eprint!(\"\\r{} [[ \", &ps4);\n        }\n\n        match self.cond.clone().unwrap().eval(core) {\n            Ok(CondElem::Ans(true)) => core.db.exit_status = 0,\n            Ok(CondElem::Ans(false)) => core.db.exit_status = 1,\n            Err(err_msg) => {\n                core.db.exit_status = 2;\n                return Err(err_msg);\n            }\n            _ => {\n                core.db.exit_status = 2;\n                return Err(ExecError::Other(\"unknown error\".to_string()));\n            }\n        };\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl TestCommand {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"[[\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            text: feeder.consume(2),\n            ..Default::default()\n        };\n\n        command::eat_blank_lines(feeder, core, &mut ans.text)?;\n\n        match ConditionalExpr::parse(feeder, core)? {\n            Some(e) => {\n                ans.text += &e.text.clone();\n                ans.cond = Some(e);\n            }\n            None => return Ok(None),\n        }\n\n        command::eat_blank_lines(feeder, core, &mut ans.text)?;\n\n        if feeder.starts_with(\"]]\") {\n            ans.text += &feeder.consume(2);\n            command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n            return Ok(Some(ans));\n        }\n\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/command/while.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{Command, Redirect};\nuse crate::elements::command;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, Script, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct WhileCommand {\n    pub text: String,\n    pub while_script: Option<Script>,\n    pub do_script: Option<Script>,\n    pub redirects: Vec<Redirect>,\n    force_fork: bool,\n    lineno: usize,\n}\n\nimpl Command for WhileCommand {\n    fn run(&mut self, core: &mut ShellCore, _: bool) -> Result<(), ExecError> {\n        if core.return_flag {\n            return Ok(());\n        }\n        core.loop_level += 1;\n        while !core.return_flag {\n            if core.continue_counter > 0 {\n                core.continue_counter -= 1;\n            }\n            core.suspend_e_option = true;\n            self.while_script.clone().as_mut().unwrap().exec(core)?;\n\n            core.suspend_e_option = false;\n            if core.db.exit_status != 0 {\n                core.db.exit_status = 0;\n                break;\n            }\n\n            self.do_script.clone().as_mut().unwrap().exec(core)?;\n\n            if core.break_counter > 0 {\n                core.break_counter -= 1;\n                break;\n            }\n            if core.continue_counter > 1 {\n                break;\n            }\n        }\n        core.loop_level -= 1;\n        if core.loop_level == 0 {\n            core.break_counter = 0;\n        }\n\n        if core.continue_counter > 0 {\n            core.continue_counter -= 1;\n        }\n        Ok(())\n    }\n\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect> {\n        &mut self.redirects\n    }\n    fn get_lineno(&mut self) -> usize {\n        self.lineno\n    }\n    fn set_force_fork(&mut self) {\n        self.force_fork = true;\n    }\n    fn boxed_clone(&self) -> Box<dyn Command> {\n        Box::new(self.clone())\n    }\n    fn force_fork(&self) -> bool {\n        self.force_fork\n    }\n}\n\nimpl WhileCommand {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            ..Default::default()\n        };\n\n        if !command::eat_inner_script(\n            feeder,\n            core,\n            \"while\",\n            vec![\"do\"],\n            &mut ans.while_script,\n            false,\n        )? {\n            return Ok(None);\n        }\n        while command::eat_blank_with_comment(feeder, core, &mut ans.text) {}\n\n        if command::eat_inner_script(feeder, core, \"do\", vec![\"done\"], &mut ans.do_script, false)? {\n            ans.text.push_str(\"while\");\n            ans.text\n                .push_str(&ans.while_script.as_mut().unwrap().get_text());\n            ans.text.push_str(\"do\");\n            ans.text\n                .push_str(&ans.do_script.as_mut().unwrap().get_text());\n            ans.text.push_str(&feeder.consume(4)); //done\n\n            command::eat_redirects(feeder, core, &mut ans.redirects, &mut ans.text)?;\n            Ok(Some(ans))\n        } else {\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/command.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod arithmetic;\npub mod brace;\npub mod case;\npub mod coproc;\npub mod r#for;\npub mod function_def;\npub mod r#if;\npub mod paren;\npub mod repeat;\npub mod simple;\npub mod test;\npub mod r#while;\n\nuse self::arithmetic::ArithmeticCommand;\nuse self::brace::BraceCommand;\nuse self::case::CaseCommand;\nuse self::coproc::Coprocess;\nuse self::function_def::FunctionDefinition;\nuse self::paren::ParenCommand;\nuse self::r#for::ForCommand;\nuse self::r#if::IfCommand;\nuse self::r#while::WhileCommand;\nuse self::repeat::RepeatCommand;\nuse self::simple::SimpleCommand;\nuse self::test::TestCommand;\nuse super::io::redirect::Redirect;\nuse super::{io, Pipe};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::exit;\nuse crate::{proc_ctrl, Feeder, Script, ShellCore};\nuse nix::unistd;\nuse nix::unistd::{ForkResult, Pid};\nuse std::fmt;\nuse std::fmt::Debug;\n\nimpl Debug for dyn Command {\n    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {\n        fmt.debug_struct(\"COMMAND\").finish()\n    }\n}\n\nimpl Clone for Box<dyn Command> {\n    fn clone(&self) -> Box<dyn Command> {\n        self.boxed_clone()\n    }\n}\n\npub trait Command {\n    fn exec(&mut self, core: &mut ShellCore, pipe: &mut Pipe) -> Result<Option<Pid>, ExecError> {\n        if core.break_counter > 0 || core.continue_counter > 0 {\n            return Ok(None);\n        }\n\n        core.db\n            .set_param(\"LINENO\", &self.get_lineno().to_string(), None)?;\n        if self.force_fork() || (!pipe.lastpipe && pipe.is_connected()) {\n            self.fork_exec(core, pipe)\n        } else {\n            pipe.connect_lastpipe(core)?;\n            self.nofork_exec(core)\n        }\n    }\n\n    fn fork_exec_child(&mut self, core: &mut ShellCore, pipe: &mut Pipe) -> Result<(), ExecError> {\n        core.initialize_as_subshell(Pid::from_raw(0), pipe.pgid);\n        io::connect(pipe, self.get_redirects(), core)?;\n        self.run(core, true)\n    }\n\n    fn fork_exec(\n        &mut self,\n        core: &mut ShellCore,\n        pipe: &mut Pipe,\n    ) -> Result<Option<Pid>, ExecError> {\n        match unsafe { unistd::fork()? } {\n            ForkResult::Child => {\n                if let Err(e) = self.fork_exec_child(core, pipe) {\n                    e.print(core);\n                    core.db.exit_status = 1;\n                }\n\n                exit::normal(core)\n            }\n            ForkResult::Parent { child } => {\n                proc_ctrl::set_pgid(core, child, pipe.pgid);\n                pipe.parent_close(core);\n                Ok(Some(child))\n            }\n        }\n    }\n\n    fn nofork_exec(&mut self, core: &mut ShellCore) -> Result<Option<Pid>, ExecError> {\n        let mut result = Ok(None);\n        for r in self.get_redirects().iter_mut() {\n            if let Err(e) = r.connect(true, core) {\n                result = Err(e);\n            }\n        }\n\n        if result.is_ok() {\n            let _ = self.run(core, false);\n        } else {\n            core.db.exit_status = 1;\n        }\n        for r in self.get_redirects().iter_mut().rev() {\n            r.restore(core)?;\n        }\n        result\n    }\n\n    fn run(&mut self, _: &mut ShellCore, fork: bool) -> Result<(), ExecError>;\n    fn get_text(&self) -> String;\n    fn get_one_line_text(&self) -> String {\n        self.get_text().replace(\"\\n\", \" \")\n    }\n    fn get_redirects(&mut self) -> &mut Vec<Redirect>;\n    fn get_lineno(&mut self) -> usize;\n    fn set_force_fork(&mut self);\n    fn boxed_clone(&self) -> Box<dyn Command>;\n    fn force_fork(&self) -> bool;\n\n    fn read_heredoc(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        let lineno = self.get_lineno();\n        for r in self.get_redirects().iter_mut() {\n            if r.called_as_heredoc {\n                continue;\n            }\n            if r.symbol == \"<<\" || r.symbol == \"<<-\" {\n                r.called_as_heredoc = true;\n                r.eat_heredoc(feeder, core, lineno)?;\n                let mut tmp = String::new();\n                eat_blank_with_comment(feeder, core, &mut tmp);\n            }\n        }\n        Ok(())\n    }\n\n    fn pretty_print(&mut self, indent_num: usize) {\n        for _ in 0..indent_num {\n            print!(\"    \");\n        }\n        println!(\"{}\", &self.get_text());\n    }\n}\n\npub fn eat_inner_script(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    left: &str,\n    right: Vec<&str>,\n    ans: &mut Option<Script>,\n    permit_empty: bool,\n) -> Result<bool, ParseError> {\n    if !feeder.starts_with(left) {\n        return Ok(false);\n    }\n    feeder.nest.push((\n        left.to_string(),\n        right.iter().map(|e| e.to_string()).collect(),\n    ));\n    feeder.consume(left.len());\n    let result_script = Script::parse(feeder, core, permit_empty);\n    feeder.nest.pop();\n    *ans = result_script?;\n    Ok(ans.is_some())\n}\n\npub fn eat_blank_with_comment(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    ans_text: &mut String,\n) -> bool {\n    let blank_len = feeder.scanner_blank(core);\n    *ans_text += &feeder.consume(blank_len);\n\n    let comment_len = feeder.scanner_comment();\n    if comment_len + blank_len == 0 {\n        return false;\n    }\n    *ans_text += &feeder.consume(comment_len);\n    true\n}\n\npub fn eat_blank_lines(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    ans_text: &mut String,\n) -> Result<(), ParseError> {\n    loop {\n        eat_blank_with_comment(feeder, core, ans_text);\n        if feeder.starts_with(\"\\n\") {\n            *ans_text += &feeder.consume(1);\n            continue;\n        }\n\n        if feeder.is_empty() {\n            feeder.feed_additional_line(core)?;\n            continue;\n        }\n\n        return Ok(());\n    }\n}\n\nfn eat_redirect(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    ans: &mut Vec<Redirect>,\n    ans_text: &mut String,\n) -> bool {\n    if let Some(r) = Redirect::parse(feeder, core) {\n        *ans_text += &r.text.clone();\n        ans.push(r);\n        true\n    } else {\n        false\n    }\n}\n\npub fn eat_redirects(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    ans_redirects: &mut Vec<Redirect>,\n    ans_text: &mut String,\n) -> Result<bool, ParseError> {\n    let mut exist = false;\n    loop {\n        eat_blank_with_comment(feeder, core, ans_text);\n        if !eat_redirect(feeder, core, ans_redirects, ans_text) {\n            break;\n        } else {\n            exist = true;\n        }\n    }\n\n    Ok(exist)\n}\n\npub fn parse(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n) -> Result<Option<Box<dyn Command>>, ParseError> {\n    if let Some(a) = FunctionDefinition::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = SimpleCommand::parse(feeder, core)? {\n        Ok(Some(a))\n    } else if let Some(a) = IfCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = ArithmeticCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = ParenCommand::parse(feeder, core, false)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = BraceCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Coprocess::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = ForCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = WhileCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = RepeatCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = CaseCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = TestCommand::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else {\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/calculator.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::elem::ArithElem;\nuse super::elem::{float, int, ternary, variable};\nuse super::rev_polish;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::utils::exit;\nuse crate::ShellCore;\n\npub fn pop_operand(\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<ArithElem, ExecError> {\n    if let Some(mut e) = stack.pop() {\n        e.change_to_value(0, core)?;\n        return Ok(e);\n    }\n\n    Err(ExecError::Other(\"no operand 2\".to_string()))\n}\n\npub fn pop_operands(\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(ArithElem, ArithElem), ExecError> {\n    let right = stack.pop();\n    let left = stack.pop();\n\n    if let Some(mut left_v) = left {\n        left_v.change_to_value(0, core)?;\n\n        if let Some(mut right_v) = right {\n            right_v.change_to_value(0, core)?;\n            return Ok((left_v, right_v));\n        }\n    }\n    Err(ExecError::Other(\"no operand 2\".to_string()))\n}\n\nfn bin_operation(\n    op: &str,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    match op {\n        \"=\" | \"*=\" | \"/=\" | \"%=\" | \"+=\" | \"-=\" | \"<<=\" | \">>=\" | \"&=\" | \"^=\" | \"|=\" => {\n            variable::substitution(op, stack, core)\n        }\n        \"&&\" | \"||\" => bin_calc_and_or(op, stack, core),\n        _ => bin_calc_operation(op, stack, core),\n    }\n}\n\nfn bin_calc_and_or(\n    op: &str,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    let mut right = match stack.pop() {\n        Some(e) => e,\n        None => return Err(ArithError::OperandExpected(op.to_string()).into()),\n    };\n    let mut left = match stack.pop() {\n        Some(e) => e,\n        None => return Err(ArithError::OperandExpected(op.to_string()).into()),\n    };\n\n    left.change_to_value(0, core)?;\n\n    if let ArithElem::Integer(n) = left {\n        if n == 0 && op == \"&&\" {\n            stack.push(ArithElem::Integer(0));\n            return Ok(());\n        }\n\n        if n != 0 && op == \"||\" {\n            stack.push(ArithElem::Integer(1));\n            return Ok(());\n        }\n    }\n\n    right.change_to_value(0, core)?;\n\n    if let ArithElem::Integer(n) = right {\n        if n == 0 {\n            stack.push(ArithElem::Integer(0));\n        } else {\n            stack.push(ArithElem::Integer(1));\n        }\n    }\n    Ok(())\n}\n\nfn bin_calc_operation(\n    op: &str,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    let (left, right) = pop_operands(stack, core)?;\n\n    if op == \",\" {\n        stack.push(right);\n        return Ok(());\n    }\n\n    match (left, right) {\n        (ArithElem::Float(fl), ArithElem::Float(fr)) => float::bin_calc(op, fl, fr, stack)?,\n        (ArithElem::Float(fl), ArithElem::Integer(nr)) => {\n            float::bin_calc(op, fl, nr as f64, stack)?\n        }\n        (ArithElem::Integer(nl), ArithElem::Float(fr)) => {\n            float::bin_calc(op, nl as f64, fr, stack)?\n        }\n        (ArithElem::Integer(nl), ArithElem::Integer(nr)) => int::bin_calc(op, nl, nr, stack)?,\n        _ => exit::internal(\"invalid operand\"),\n    };\n\n    Ok(())\n}\n\nfn unary_operation(\n    op: &str,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    let operand = pop_operand(stack, core)?;\n\n    match operand {\n        ArithElem::Float(num) => float::unary_calc(op, num, stack),\n        ArithElem::Integer(num) => int::unary_calc(op, num, stack),\n        _ => exit::internal(\"unknown operand\"),\n    }\n}\n\npub fn calculate(elements: &[ArithElem], core: &mut ShellCore) -> Result<ArithElem, ExecError> {\n    if elements.is_empty() {\n        return Ok(ArithElem::Integer(0));\n    }\n\n    let rev_pol = rev_polish::rearrange(elements)?;\n    dry_run(&rev_pol)?;\n\n    let mut stack = vec![];\n    let mut escaped_unaries = vec![];\n\n    for e in rev_pol {\n        match e {\n            ArithElem::BinaryOp(ref op) => bin_operation(op, &mut stack, core)?,\n            ArithElem::UnaryOp(ref op) => match stack.is_empty() {\n                true => escaped_unaries.push(e),\n                false => {\n                    unary_operation(op, &mut stack, core)?;\n                    while !escaped_unaries.is_empty() {\n                        if let ArithElem::UnaryOp(ref op) = escaped_unaries.pop().unwrap() {\n                            () = unary_operation(op, &mut stack, core)?;\n                        }\n                    }\n                }\n            },\n            ArithElem::Increment(n) => inc(n, &mut stack, core)?,\n            ArithElem::Ternary(left, right) => ternary::operation(&left, &right, &mut stack, core)?,\n            _ => stack.push(e.clone()),\n        }\n    }\n\n    if stack.is_empty() {\n        return Err(ArithError::OperandExpected(String::new()).into());\n    }\n    if stack.len() != 1 {\n        return Err(ArithError::OperandExpected(stack.last().unwrap().to_string()).into());\n    }\n    pop_operand(&mut stack, core)\n}\n\nfn dry_run(rev_pol: &[ArithElem]) -> Result<(), ArithError> {\n    let mut stack = vec![];\n    let mut last = None;\n\n    for e in rev_pol {\n        match e {\n            ArithElem::BinaryOp(_) => {\n                stack.pop();\n                if stack.is_empty() {\n                    return Err(ArithError::OperandExpected(e.to_string()));\n                }\n            }\n            ArithElem::UnaryOp(_) | ArithElem::Increment(_) => last = Some(e),\n            ArithElem::Ternary(_, _) => {\n                if stack.is_empty() {\n                    return Err(ArithError::OperandExpected(e.to_string()));\n                }\n            }\n            _ => stack.push(e.clone()),\n        }\n    }\n\n    if stack.is_empty() {\n        match last {\n            Some(e) => return Err(ArithError::OperandExpected(e.to_string())),\n            None => return Err(ArithError::OperandExpected(String::new())),\n        }\n    }\n    if stack.len() != 1 {\n        return Err(ArithError::SyntaxError(stack.last().unwrap().to_string()));\n    }\n    Ok(())\n}\n\nfn inc(inc: i128, stack: &mut Vec<ArithElem>, core: &mut ShellCore) -> Result<(), ExecError> {\n    if let Some(mut op) = stack.pop() {\n        op.change_to_value(inc, core)?;\n        stack.push(op);\n        Ok(())\n    } else {\n        Err(ArithError::OperandExpected(\"\".to_string()).into())\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/elem/float.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::variable;\nuse super::ArithElem;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::{error, ShellCore};\n\npub fn unary_calc(op: &str, num: f64, stack: &mut Vec<ArithElem>) -> Result<(), ExecError> {\n    match op {\n        \"+\" => stack.push(ArithElem::Float(num)),\n        \"-\" => stack.push(ArithElem::Float(-num)),\n        _ => {\n            return Err(ExecError::Other(\n                \"not supported operator for float number\".to_string(),\n            ))\n        }\n    }\n    Ok(())\n}\n\npub fn bin_calc(\n    op: &str,\n    left: f64,\n    right: f64,\n    stack: &mut Vec<ArithElem>,\n) -> Result<(), ExecError> {\n    let bool_to_01 = |b| {\n        if b {\n            ArithElem::Integer(1)\n        } else {\n            ArithElem::Integer(0)\n        }\n    };\n\n    match op {\n        \"+\" => stack.push(ArithElem::Float(left + right)),\n        \"-\" => stack.push(ArithElem::Float(left - right)),\n        \"*\" => stack.push(ArithElem::Float(left * right)),\n        \"<=\" => stack.push(bool_to_01(left <= right)),\n        \">=\" => stack.push(bool_to_01(left >= right)),\n        \"<\" => stack.push(bool_to_01(left < right)),\n        \">\" => stack.push(bool_to_01(left > right)),\n        \"==\" => stack.push(bool_to_01(left == right)),\n        \"!=\" => stack.push(bool_to_01(left != right)),\n        \"/\" => {\n            if right == 0.0 {\n                return Err(ExecError::Other(\"divided by 0\".to_string()));\n            }\n            stack.push(ArithElem::Float(left / right));\n        }\n        \"**\" => {\n            if right >= 0.0 {\n                stack.push(ArithElem::Float(left.powf(right)));\n            } else {\n                return Err(ExecError::Other(error::exponent(&right.to_string())));\n            }\n        }\n        _ => {\n            return Err(ExecError::Other(\n                \"not supported operator for float numbers\".to_string(),\n            ))\n        }\n    }\n\n    Ok(())\n}\n\npub fn substitute(\n    op: &str,\n    name: &str,\n    index: &str,\n    cur: f64,\n    right: f64,\n    core: &mut ShellCore,\n) -> Result<ArithElem, ExecError> {\n    let new_value = match op {\n        \"+=\" => cur + right,\n        \"-=\" => cur - right,\n        \"*=\" => cur * right,\n        \"/=\" => match right == 0.0 {\n            true => return Err(ArithError::DivZero(right.to_string()).into()),\n            false => cur / right,\n        },\n        _ => return Err(ArithError::OperandExpected(op.to_string()).into()),\n    };\n\n    core.db\n        .set_param2(name, index, &new_value.to_string(), None)?;\n    Ok(ArithElem::Float(new_value))\n}\n\npub fn parse(s: &str) -> Result<f64, ArithError> {\n    let mut sw = s.to_string();\n    let sign = variable::get_sign(&mut sw);\n\n    match (sw.parse::<f64>(), sign.as_str()) {\n        (Ok(f), \"-\") => Ok(-f),\n        (Ok(f), _) => Ok(f),\n        (Err(_), _) => Err(ArithError::InvalidNumber(s.to_string())),\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/elem/int.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::variable;\nuse super::ArithElem;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::utils::exit;\nuse crate::ShellCore;\n\npub fn unary_calc(op: &str, num: i128, stack: &mut Vec<ArithElem>) -> Result<(), ExecError> {\n    match op {\n        \"+\" => stack.push(ArithElem::Integer(num)),\n        \"-\" => stack.push(ArithElem::Integer(-num)),\n        \"!\" => stack.push(ArithElem::Integer(if num == 0 { 1 } else { 0 })),\n        \"~\" => stack.push(ArithElem::Integer(!num)),\n        _ => exit::internal(\"unknown unary operator\"),\n    }\n    Ok(())\n}\n\npub fn bin_calc(\n    op: &str,\n    left: i128,\n    right: i128,\n    stack: &mut Vec<ArithElem>,\n) -> Result<(), ArithError> {\n    let bool_to_01 = |b| {\n        if b {\n            1\n        } else {\n            0\n        }\n    };\n\n    let ans = match op {\n        \"+\" => left + right,\n        \"-\" => left - right,\n        \"*\" => left * right,\n        \"&\" => left & right,\n        \"^\" => left ^ right,\n        \"|\" => left | right,\n        \"&&\" => bool_to_01(left != 0 && right != 0),\n        \"||\" => bool_to_01(left != 0 || right != 0),\n        \"<<\" => {\n            if right < 0 {\n                0\n            } else {\n                left << right\n            }\n        }\n        \">>\" => {\n            if right < 0 {\n                0\n            } else {\n                left >> right\n            }\n        }\n        \"<=\" => bool_to_01(left <= right),\n        \">=\" => bool_to_01(left >= right),\n        \"<\" => bool_to_01(left < right),\n        \">\" => bool_to_01(left > right),\n        \"==\" => bool_to_01(left == right),\n        \"!=\" => bool_to_01(left != right),\n        \"%\" | \"/\" => {\n            if right == 0 {\n                return Err(ArithError::DivZero(right.to_string()));\n            }\n            match op {\n                \"%\" => left % right,\n                _ => left / right,\n            }\n        }\n        \"**\" => {\n            if right >= 0 {\n                let r = right.try_into().unwrap();\n                left.pow(r)\n            } else {\n                return Err(ArithError::Exponent(right));\n            }\n        }\n        _ => exit::internal(\"unknown binary operator\"),\n    };\n\n    stack.push(ArithElem::Integer(ans));\n    Ok(())\n}\n\npub fn substitute(\n    op: &str,\n    name: &str,\n    index: &str,\n    cur: i128,\n    right: i128,\n    core: &mut ShellCore,\n) -> Result<ArithElem, ExecError> {\n    let new_value = match op {\n        \"+=\" => cur + right,\n        \"-=\" => cur - right,\n        \"*=\" => cur * right,\n        \"&=\" => cur & right,\n        \"^=\" => cur ^ right,\n        \"|=\" => cur | right,\n        \"<<=\" => {\n            if right < 0 {\n                0\n            } else {\n                cur << right\n            }\n        }\n        \">>=\" => {\n            if right < 0 {\n                0\n            } else {\n                cur >> right\n            }\n        }\n        \"/=\" | \"%=\" => {\n            if right == 0 {\n                return Err(ArithError::DivZero(right.to_string()).into());\n            }\n            match op == \"%=\" {\n                true => cur % right,\n                false => cur / right,\n            }\n        }\n        _ => return Err(ArithError::OperandExpected(op.to_string()).into()),\n    };\n\n    core.db\n        .set_param2(name, index, &new_value.to_string(), None)?;\n    Ok(ArithElem::Integer(new_value))\n}\n\nfn parse_with_base(base: i128, s: &str, org: &str) -> Result<i128, ArithError> {\n    if s.is_empty() {\n        return Err(ArithError::InvalidIntConst(org.to_string()));\n    }\n\n    let mut ans = 0;\n    for ch in s.chars() {\n        ans *= base;\n        let num = if ch.is_ascii_digit() {\n            ch as i128 - '0' as i128\n        } else if ch.is_ascii_lowercase() {\n            ch as i128 - 'a' as i128 + 10\n        } else if ch.is_ascii_uppercase() {\n            match base <= 36 {\n                true => ch as i128 - 'A' as i128 + 10,\n                false => ch as i128 - 'A' as i128 + 36,\n            }\n        } else if ch == '@' {\n            62\n        } else if ch == '_' {\n            63\n        } else {\n            return Err(ArithError::InvalidNumber(org.to_string()));\n        };\n\n        match num < base {\n            true => ans += num,\n            false => return Err(ArithError::ValueTooGreatForBase(org.to_string())),\n        }\n    }\n\n    Ok(ans)\n}\n\nfn get_base(s: &mut String) -> Result<i128, ArithError> {\n    let s_org = s.to_string();\n    if s.starts_with(\"0x\") || s.starts_with(\"0X\") {\n        s.remove(0);\n        s.remove(0);\n        return Ok(16);\n    }\n\n    if s.starts_with(\"0\") && s.len() > 1 {\n        if s.contains('#') {\n            return Err(ArithError::InvalidNumber(s.clone()));\n        }\n        s.remove(0);\n        return Ok(8);\n    }\n\n    if let Some(n) = s.find(\"#\") {\n        let base_str = s[..n].to_string();\n        *s = s[(n + 1)..].to_string();\n        return match base_str.parse::<i128>() {\n            Ok(n) => match n <= 64 {\n                true => Ok(n),\n                false => Err(ArithError::InvalidBase(s_org.to_string())),\n            },\n            _ => Err(ArithError::InvalidBase(s_org.to_string())),\n        };\n    }\n\n    Ok(10)\n}\n\npub fn parse(s: &str) -> Result<i128, ArithError> {\n    if s.find('\\'').is_some() || s.find('.').is_some() {\n        return Err(ArithError::InvalidNumber(s.to_string()));\n    }\n    if s.is_empty() {\n        return Ok(0);\n    }\n\n    let mut sw = s.to_string();\n    let sign = variable::get_sign(&mut sw);\n    let base = get_base(&mut sw)?;\n    let n = parse_with_base(base, &sw, s)?;\n\n    match sign.as_str() {\n        \"-\" => Ok(-n),\n        _ => Ok(n),\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/elem/ternary.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::calculator;\nuse super::super::{ArithElem, ArithmeticExpr};\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\n\npub fn operation(\n    left: &Option<ArithmeticExpr>,\n    right: &Option<ArithmeticExpr>,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    if left.is_none() {\n        return Err(ArithError::ExpressionExpected(\":\".to_string()).into());\n    }\n    let mut left = left.clone().unwrap();\n\n    if right.is_none() {\n        return Err(ArithError::NoColon(left.text.trim().to_string()).into());\n    }\n\n    let mut right = right.clone().unwrap();\n\n    if left.elements.is_empty() {\n        let msg = format!(\":{}\", &right.text.trim_end());\n        return Err(ArithError::ExpressionExpected(msg).into());\n    }\n\n    if right.elements.is_empty() {\n        return Err(ArithError::ExpressionExpected(\":\".to_string()).into());\n    }\n\n    let ans = match calculator::pop_operand(stack, core)? {\n        ArithElem::Integer(0) => right.eval_in_cond(core)?,\n        ArithElem::Float(_) => {\n            return Err(ExecError::Other(\n                \"float condition is not permitted\".to_string(),\n            ))\n        }\n        _ => left.eval_in_cond(core)?,\n    };\n\n    stack.push(ans);\n    Ok(())\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/elem/variable.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::ArithElem;\nuse super::{float, int};\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse crate::utils::exit;\nuse crate::{Feeder, ShellCore};\n\nfn to_num(w: &str, sub: &str, core: &mut ShellCore) -> Result<ArithElem, ExecError> {\n    if w.find('\\'').is_some() {\n        return Err(ArithError::OperandExpected(w.to_string()).into());\n    }\n\n    let name = w.to_string();\n    str_to_num(&name, sub, core)\n}\n\npub fn str_to_num(name: &str, sub: &str, core: &mut ShellCore) -> Result<ArithElem, ExecError> {\n    let mut name = name.to_string();\n\n    const RESOLVE_LIMIT: i32 = 100; //000;\n\n    for i in 0..RESOLVE_LIMIT {\n        if utils::is_name(&name, core) {\n            if i == RESOLVE_LIMIT - 1 {\n                return Err(ArithError::Recursion(name.clone()).into());\n            }\n            name = core.db.get_elem_or_param(&name, sub)?;\n            continue;\n        }\n        break;\n    }\n    /* name is not a name here */\n\n    match try_parse_to_num(&name) {\n        Ok(e) => Ok(e),\n        Err(_) => resolve_arithmetic_op(&name, core),\n    }\n}\n\nfn resolve_arithmetic_op(name: &str, core: &mut ShellCore) -> Result<ArithElem, ExecError> {\n    let mut f = Feeder::new(name);\n    let mut parsed = match ArithmeticExpr::parse_after_eval(&mut f, core, \"\") {\n        Ok(Some(p)) => p,\n        _ => return Err(ArithError::OperandExpected(name.to_string()).into()),\n    };\n\n    parsed.eval_elems(core, true)\n}\n\nfn try_parse_to_num(name: &str) -> Result<ArithElem, ExecError> {\n    if name.contains('.') {\n        let f = float::parse(name)?;\n        Ok(ArithElem::Float(f))\n    } else {\n        let n = int::parse(name)?;\n        Ok(ArithElem::Integer(n))\n    }\n}\n\npub fn set_and_to_value(\n    name: &str,\n    sub: &str,\n    core: &mut ShellCore,\n    inc: i128,\n    pre: bool,\n) -> Result<ArithElem, ExecError> {\n    match str_to_num(name, sub, core) {\n        Ok(ArithElem::Integer(n)) => {\n            if inc != 0 {\n                core.db\n                    .set_param2(name, sub, &(n + inc).to_string(), None)?;\n            }\n            match pre {\n                true => Ok(ArithElem::Integer(n + inc)),\n                false => Ok(ArithElem::Integer(n)),\n            }\n        }\n        Ok(ArithElem::Float(n)) => {\n            if inc != 0 {\n                core.db\n                    .set_param2(name, sub, &(n + inc as f64).to_string(), None)?;\n            }\n            match pre {\n                true => Ok(ArithElem::Float(n + inc as f64)),\n                false => Ok(ArithElem::Float(n)),\n            }\n        }\n        Ok(_) => exit::internal(\"unknown element\"),\n        Err(err_msg) => Err(err_msg),\n    }\n}\n\npub fn get_sign(s: &mut String) -> String {\n    *s = s.trim().to_string();\n    match s.starts_with(\"+\") || s.starts_with(\"-\") {\n        true => {\n            let c = s.remove(0).to_string();\n            *s = s.trim().to_string();\n            c\n        }\n        false => \"+\".to_string(),\n    }\n}\n\npub fn substitution(\n    op: &str,\n    stack: &mut Vec<ArithElem>,\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    let mut right = match stack.pop() {\n        Some(mut e) => {\n            e.change_to_value(0, core)?;\n            e\n        }\n        _ => return Err(ArithError::OperandExpected(op.to_string()).into()),\n    };\n\n    if let Some(ArithElem::Variable(w, s, 0)) = stack.pop() {\n        let index = match s {\n            Some(mut sub) => sub.eval(core, &w)?,\n            None => \"\".to_string(),\n        };\n        stack.push(subs(op, &w, &index, &mut right, core)?);\n        return Ok(());\n    }\n\n    Err(ArithError::AssignmentToNonVariable(op.to_string() + &right.to_string()).into())\n}\n\nfn subs(\n    op: &str,\n    w: &str,\n    sub: &str,\n    right_value: &mut ArithElem,\n    core: &mut ShellCore,\n) -> Result<ArithElem, ExecError> {\n    if w.find('\\'').is_some() {\n        return Err(ArithError::OperandExpected(w.to_string()).into());\n    }\n\n    let name = w.to_string();\n    let right_str = right_value.to_string();\n\n    match op {\n        \"=\" => {\n            core.db.set_param2(&name, sub, &right_str, None)?;\n            return Ok(right_value.clone());\n        }\n        \"+=\" => {\n            let mut val_str = core.db.get_elem_or_param(&name, sub)?;\n            if val_str.is_empty() {\n                val_str = \"0\".to_string();\n            }\n            if let Ok(left) = val_str.parse::<i128>() {\n                if let ArithElem::Integer(n) = right_value {\n                    core.db\n                        .set_param2(&name, sub, &(left + *n).to_string(), None)?;\n                    return Ok(ArithElem::Integer(left + *n));\n                }\n            } else if let Ok(left) = val_str.parse::<f64>() {\n                if let ArithElem::Float(f) = right_value {\n                    core.db\n                        .set_param2(&name, sub, &(left + *f).to_string(), None)?;\n                    return Ok(ArithElem::Float(left + *f));\n                }\n            }\n        }\n        _ => {}\n    }\n\n    match (to_num(w, sub, core)?, right_value) {\n        (ArithElem::Integer(cur), ArithElem::Integer(right)) => {\n            Ok(int::substitute(op, &name, sub, cur, *right, core)?)\n        }\n        (ArithElem::Float(cur), ArithElem::Integer(right)) => {\n            Ok(float::substitute(op, &name, sub, cur, *right as f64, core)?)\n        }\n        (ArithElem::Float(cur), ArithElem::Float(right)) => {\n            Ok(float::substitute(op, &name, sub, cur, *right, core)?)\n        }\n        (ArithElem::Integer(cur), ArithElem::Float(right)) => {\n            Ok(float::substitute(op, &name, sub, cur as f64, *right, core)?)\n        }\n        _ => Err(ExecError::Other(\"not supported yet\".to_string())),\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/elem.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod float;\npub mod int;\npub mod ternary;\npub mod variable;\n\nuse super::ArithmeticExpr;\nuse super::Word;\nuse crate::elements::substitution::subscript::Subscript;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::{utils, ShellCore};\nuse std::fmt;\n\n#[derive(Debug, Clone)]\npub enum ArithElem {\n    UnaryOp(String),\n    BinaryOp(String),\n    Integer(i128),\n    Float(f64),\n    Ternary(Box<Option<ArithmeticExpr>>, Box<Option<ArithmeticExpr>>),\n    Variable(String, Option<Subscript>, i128), // name + subscript + post increment or decrement\n    InParen(ArithmeticExpr),\n    Increment(i128), //pre increment\n    //    Delimiter(String), //delimiter dividing left and right of &&, ||, and ','\n    /* only for parse */\n    Space(String),\n    Symbol(String),\n    Word(Word, i128),                   // Word + post increment or decrement\n    ArrayElem(String, Subscript, i128), // a[1]++\n}\n\nimpl ArithElem {\n    pub fn order(&self) -> u8 {\n        match self {\n            ArithElem::Increment(_) => 20,\n            ArithElem::UnaryOp(s) => match s.as_str() {\n                \"-\" | \"+\" => 19,\n                _ => 19,\n            },\n            ArithElem::BinaryOp(s) => {\n                match s.as_str() {\n                    \"**\" => 17,\n                    \"*\" | \"/\" | \"%\" => 16,\n                    \"+\" | \"-\" => 15,\n                    \"<<\" | \">>\" => 14,\n                    \"<=\" | \">=\" | \">\" | \"<\" => 13,\n                    \"==\" | \"!=\" => 12,\n                    \"&\" => 11,\n                    \"^\" => 10,\n                    \"|\" => 9,\n                    \"&&\" => 8,\n                    \"||\" => 7,\n                    \",\" => 0,\n                    _ => 2, //substitution\n                }\n            }\n            ArithElem::Ternary(_, _) => 3,\n            _ => 1,\n        }\n    }\n}\n\nimpl fmt::Display for ArithElem {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            ArithElem::Space(s) | ArithElem::Symbol(s) => write!(f, \"{s}\"),\n            ArithElem::InParen(a) => write!(f, \"{}\", a.text),\n            ArithElem::Integer(n) => write!(f, \"{n}\"),\n            ArithElem::Float(val) => {\n                let mut s = val.to_string();\n                if !s.contains('.') {\n                    s.push_str(\".0\");\n                }\n                write!(f, \"{s}\")\n            }\n            ArithElem::Word(w, inc) => match inc {\n                1 => write!(f, \"{}++\", w.text),\n                -1 => write!(f, \"{}--\", w.text),\n                _ => write!(f, \"{}\", w.text),\n            },\n            ArithElem::Variable(w, sub, inc) => {\n                if let Some(s) = sub {\n                    match inc {\n                        1 => write!(f, \"{}{}++\", w, s.text),\n                        -1 => write!(f, \"{}{}--\", w, s.text),\n                        _ => write!(f, \"{}{}\", w, s.text),\n                    }\n                } else {\n                    match inc {\n                        1 => write!(f, \"{w}++\"),\n                        -1 => write!(f, \"{w}--\"),\n                        _ => write!(f, \"{w}\"),\n                    }\n                }\n            }\n            ArithElem::Ternary(left, right) => {\n                let mut s = String::from(\"?\");\n                if let Some(e) = *left.clone() {\n                    s += &e.text;\n                }\n                s.push(':');\n                if let Some(e) = *right.clone() {\n                    s += &e.text;\n                }\n                write!(f, \"{s}\")\n            }\n            ArithElem::UnaryOp(s) | ArithElem::BinaryOp(s) => write!(f, \"{s}\"),\n            ArithElem::Increment(n) => {\n                if *n > 0 {\n                    write!(f, \"++\")\n                } else if *n < 0 {\n                    write!(f, \"--\")\n                } else {\n                    write!(f, \"\")\n                }\n            }\n            ArithElem::ArrayElem(name, subs, inc) => match inc {\n                1 => write!(f, \"{}{}++\", name, subs.text),\n                -1 => write!(f, \"{}{}--\", name, subs.text),\n                _ => write!(f, \"{}{}\", name, subs.text),\n            },\n        }\n    }\n}\n\nimpl ArithElem {\n    pub fn change_to_value(&mut self, add: i128, core: &mut ShellCore) -> Result<(), ExecError> {\n        *self = match self {\n            ArithElem::InParen(a) => a.eval_elems(core, false)?,\n            ArithElem::Variable(name, s, inc) => {\n                if add != 0 && *inc != 0 || !utils::is_name(name, core) {\n                    return Err(ArithError::OperandExpected(name.to_string()).into());\n                }\n\n                let index = match s {\n                    Some(sub) => sub.eval(core, name)?,\n                    None => \"\".to_string(),\n                };\n\n                match add {\n                    0 => variable::set_and_to_value(name, &index, core, *inc, false)?,\n                    _ => variable::set_and_to_value(name, &index, core, add, true)?,\n                }\n            }\n            _ => return Ok(()),\n        };\n        Ok(())\n    }\n\n    pub fn is_operand(&self) -> bool {\n        matches!(\n            self,\n            ArithElem::Float(_)\n                | ArithElem::Integer(_)\n                | ArithElem::ArrayElem(_, _, _)\n                | ArithElem::Word(_, _)\n                | ArithElem::Variable(_, _, _)\n                | ArithElem::InParen(_)\n        )\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/parser.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::elem::{float, int};\nuse super::{ArithElem, ArithmeticExpr};\nuse crate::elements::substitution::subscript::Subscript;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\nimpl ArithmeticExpr {\n    fn eat_space(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> Option<ArithElem> {\n        let len = feeder.scanner_multiline_blank(core);\n        if len == 0 {\n            return None;\n        }\n        let sp = feeder.consume(len);\n        ans.text += &sp;\n        Some(ArithElem::Space(sp.clone()))\n    }\n\n    fn eat_suffix(feeder: &mut Feeder, ans: &mut Self) -> i128 {\n        if feeder.starts_with(\"++\") {\n            ans.text += &feeder.consume(2);\n            1\n        } else if feeder.starts_with(\"--\") {\n            ans.text += &feeder.consume(2);\n            -1\n        } else {\n            0\n        }\n    }\n\n    fn eat_incdec(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        if feeder.starts_with(\"++\") && !feeder.starts_with(\"+++\") {\n            ans.text += &feeder.consume(2);\n            ans.elements.push(ArithElem::Increment(1));\n        } else if feeder.starts_with(\"--\") && !feeder.starts_with(\"---\") {\n            ans.text += &feeder.consume(2);\n            ans.elements.push(ArithElem::Increment(-1));\n        } else {\n            return false;\n        };\n        true\n    }\n\n    fn eat_ternary_symbol(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        if feeder.starts_withs(&[\"?\", \":\"]) {\n            let symbol = feeder.consume(1);\n            ans.in_ternary = symbol == \"?\";\n            ans.text += &symbol.clone();\n            ans.elements.push(ArithElem::Symbol(symbol));\n            return true;\n        }\n\n        false\n    }\n\n    fn eat_num(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ExecError> {\n        let len = feeder.scanner_arith_number(core);\n        if len == 0 {\n            return Ok(false);\n        }\n\n        let w = feeder.consume(len);\n        ans.text += &w.clone();\n\n        if !w.contains('.') {\n            match int::parse(&w) {\n                Ok(n) => {\n                    ans.elements.push(ArithElem::Integer(n));\n                    return Ok(true);\n                }\n                Err(e) => {\n                    return Err(ExecError::ArithError(w.to_string(), e));\n                }\n            }\n        }\n\n        if let Ok(f) = float::parse(&w) {\n            ans.elements.push(ArithElem::Float(f));\n            return Ok(true);\n        }\n\n        ans.elements.push(ArithElem::Variable(w.clone(), None, 0));\n        Ok(true)\n    }\n\n    fn eat_conditional_op(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ExecError> {\n        if !feeder.starts_with(\"?\") {\n            return Ok(false);\n        }\n\n        ans.text += &feeder.consume(1);\n        let left = Self::parse_after_eval(feeder, core, \"?\")?;\n        if let Some(ref l) = left {\n            ans.text += &l.text;\n        }\n\n        if !feeder.starts_with(\":\") {\n            ans.elements\n                .push(ArithElem::Ternary(Box::new(left), Box::new(None)));\n            return Ok(true);\n        }\n\n        ans.text += &feeder.consume(1);\n        let right = Self::parse_after_eval(feeder, core, \":\")?;\n        if let Some(ref r) = right {\n            ans.text += &r.text;\n        }\n\n        ans.elements\n            .push(ArithElem::Ternary(Box::new(left), Box::new(right)));\n        Ok(true)\n    }\n\n    fn eat_array_elem(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n        internal: bool,\n    ) -> Result<bool, ParseError> {\n        let len = feeder.scanner_name(core);\n        if len == 0 {\n            return Ok(false);\n        }\n\n        let name = &feeder.consume(len);\n        ans.text += &name.clone();\n\n        if let Some(s) = Subscript::parse(feeder, core)? {\n            ans.text += &s.text.clone();\n            let sp = Self::eat_space(feeder, ans, core);\n            let suffix = Self::eat_suffix(feeder, ans);\n            if internal {\n                ans.elements\n                    .push(ArithElem::Variable(name.clone(), Some(s), suffix));\n            } else {\n                ans.elements\n                    .push(ArithElem::ArrayElem(name.clone(), s, suffix));\n            }\n            if !internal {\n                if let Some(s) = sp {\n                    ans.elements.push(s);\n                }\n            }\n        } else {\n            let sp = Self::eat_space(feeder, ans, core);\n            let suffix = Self::eat_suffix(feeder, ans);\n            if internal {\n                ans.elements\n                    .push(ArithElem::Variable(name.clone(), None, suffix));\n            } else {\n                ans.elements\n                    .push(ArithElem::Word(Word::from(name.as_str()), suffix));\n            }\n            if !internal {\n                if let Some(s) = sp {\n                    ans.elements.push(s);\n                }\n            }\n        };\n\n        Ok(true)\n    }\n\n    fn eat_word(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore, internal: bool) -> bool {\n        if let Ok(Some(w)) = Word::parse(feeder, core, Some(WordMode::Arithmetic)) {\n            ans.text += &w.text.clone();\n            //let sp = Self::eat_space(feeder, ans, core);\n            let suffix = Self::eat_suffix(feeder, ans);\n            if internal {\n                ans.elements\n                    .push(ArithElem::Variable(w.text.clone(), None, suffix));\n            } else {\n                ans.elements.push(ArithElem::Word(w, suffix));\n            }\n            /*\n            if ! internal && sp.is_some() {\n                ans.elements.push(sp.unwrap());\n            }*/\n            return true;\n        }\n\n        false\n    }\n\n    fn eat_output_format(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_math_output_format(core);\n        if len == 0 {\n            return false;\n        }\n\n        let mut s = feeder.consume(len);\n        ans.text += &s.clone();\n        ans.hide_base = s.contains(\"##\");\n        s.retain(|c: char| c.is_ascii_digit());\n        ans.output_base = s;\n        true\n    }\n\n    fn eat_unary_operator(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        if let Some(e) = ans.elements.last() {\n            if e.is_operand() {\n                return false;\n            }\n        }\n\n        let s = match feeder.scanner_unary_operator(core) {\n            0 => return false,\n            len => feeder.consume(len),\n        };\n\n        ans.text += &s.clone();\n        ans.elements.push(ArithElem::UnaryOp(s));\n        true\n    }\n\n    fn eat_paren_internal(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        ans: &mut Self,\n    ) -> Result<bool, ExecError> {\n        if !feeder.starts_with(\"(\") {\n            return Ok(false);\n        }\n\n        ans.text += &feeder.consume(1);\n        let arith = Self::parse_after_eval(feeder, core, \"(\")?;\n        match arith {\n            Some(a) if feeder.starts_with(\")\") => {\n                ans.text += &a.text;\n                ans.elements.push(ArithElem::InParen(a));\n            }\n            _ => return Ok(false),\n        }\n\n        ans.text += &feeder.consume(1);\n        Ok(true)\n    }\n\n    fn eat_paren(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        ans: &mut Self,\n    ) -> Result<bool, ParseError> {\n        if !feeder.starts_with(\"(\") {\n            return Ok(false);\n        }\n\n        let paren = feeder.consume(1);\n        ans.text += &paren.clone();\n        ans.elements.push(ArithElem::Symbol(paren));\n        let arith = Self::parse(feeder, core, true, \"(\")?;\n        match arith {\n            Some(mut a) if feeder.starts_with(\")\") => {\n                ans.text += &a.text;\n                ans.elements.append(&mut a.elements);\n            }\n            _ => return Ok(false),\n        }\n\n        let paren = feeder.consume(1);\n        ans.text += &paren.clone();\n        ans.elements.push(ArithElem::Symbol(paren));\n        Ok(true)\n    }\n\n    fn eat_binary_operator(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_binary_operator(core);\n        if len == 0 {\n            return false;\n        }\n\n        let s = feeder.consume(len);\n        ans.text += &s.clone();\n        ans.elements.push(ArithElem::BinaryOp(s));\n        true\n    }\n\n    pub fn parse_after_eval(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        left: &str,\n    ) -> Result<Option<Self>, ExecError> {\n        let mut ans = ArithmeticExpr::new();\n\n        loop {\n            Self::eat_space(feeder, &mut ans, core);\n\n            if left == \"[\" && feeder.starts_with(\"]\")\n                || left == \"?\" && feeder.starts_with(\":\")\n                || left == \":\" && (feeder.starts_with(\"]\") || feeder.starts_with(\":\"))\n            {\n                break;\n            }\n\n            if left == \":\" {\n                //substitution cannot exist after \":\" of a ternary\n                if !feeder.starts_with(\"==\") && feeder.scanner_substitution(core) > 0 {\n                    break;\n                }\n            }\n\n            if Self::eat_output_format(feeder, &mut ans, core)\n                || Self::eat_conditional_op(feeder, &mut ans, core)?\n                || Self::eat_incdec(feeder, &mut ans)\n                || Self::eat_unary_operator(feeder, &mut ans, core)\n                || Self::eat_paren_internal(feeder, core, &mut ans)?\n                || Self::eat_binary_operator(feeder, &mut ans, core)\n                || Self::eat_array_elem(feeder, &mut ans, core, true)?\n                || Self::eat_num(feeder, &mut ans, core)?\n                || Self::eat_word(feeder, &mut ans, core, true)\n            {\n                continue;\n            }\n\n            break;\n        }\n        Ok(Some(ans))\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        addline: bool,\n        left: &str,\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = ArithmeticExpr::new();\n\n        loop {\n            if let Some(sp) = Self::eat_space(feeder, &mut ans, core) {\n                ans.elements.push(sp);\n            }\n\n            if !ans.in_ternary && feeder.starts_with(\":\") {\n                break;\n            }\n\n            if left == \"[\" && feeder.starts_with(\"]\") {\n                break;\n            }\n\n            if Self::eat_output_format(feeder, &mut ans, core)\n                || Self::eat_ternary_symbol(feeder, &mut ans)\n                || Self::eat_unary_operator(feeder, &mut ans, core)\n                || Self::eat_paren(feeder, core, &mut ans)?\n                || Self::eat_binary_operator(feeder, &mut ans, core)\n                || Self::eat_array_elem(feeder, &mut ans, core, false)?\n                || Self::eat_word(feeder, &mut ans, core, false)\n            {\n                continue;\n            }\n\n            if !addline || !feeder.is_empty() || feeder.feed_additional_line(core).is_err() {\n                break;\n            }\n        }\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic/rev_polish.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::elem::ArithElem;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\n\npub fn rearrange(elements: &[ArithElem]) -> Result<Vec<ArithElem>, ExecError> {\n    let mut ans = vec![];\n    let mut stack: Vec<ArithElem> = vec![];\n    let mut prev_is_op = false;\n\n    for e in elements {\n        let is_op = e.is_operand();\n        if prev_is_op && is_op {\n            return Err(ArithError::SyntaxError(e.to_string()).into());\n        }\n        prev_is_op = is_op;\n\n        match is_op {\n            true => ans.push(e.clone()),\n            false => rev_polish_op(e, &mut stack, &mut ans),\n        };\n    }\n\n    while let Some(element) = stack.pop() {\n        ans.push(element);\n    }\n\n    Ok(ans)\n}\n\nfn rev_polish_op(elem: &ArithElem, stack: &mut Vec<ArithElem>, ans: &mut Vec<ArithElem>) {\n    loop {\n        match stack.last() {\n            None => {\n                stack.push(elem.clone());\n                break;\n            }\n            Some(_) => {\n                let last = stack.last().unwrap();\n                if last.order() < elem.order() || (last.order() == 2 && elem.order() == 2) {\n                    // assignment\n                    stack.push(elem.clone());\n                    break;\n                }\n                ans.push(stack.pop().unwrap());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/arithmetic.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod calculator;\npub mod elem;\nmod parser;\nmod rev_polish;\n\nuse self::calculator::calculate;\nuse self::elem::ArithElem;\nuse crate::elements::word::Word;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::utils::exit;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct ArithmeticExpr {\n    pub text: String,\n    pub elements: Vec<ArithElem>,\n    output_base: String,\n    hide_base: bool,\n    in_ternary: bool,\n}\n\nimpl ArithmeticExpr {\n    pub fn eval_doller(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let mut txt = String::new();\n        for e in &self.elements {\n            match e {\n                ArithElem::Word(w, inc) => {\n                    if w.text.contains(\"'\") {\n                        let err = ArithError::OperandExpected(w.text.clone());\n                        let arith_txt = self.text.trim_start().to_string();\n                        return Err(ExecError::ArithError(arith_txt, err));\n                    }\n                    let text = w.eval_as_value(core)?;\n                    let word = ArithElem::Word(Word::from(text.as_str()), *inc);\n                    txt += &word.to_string();\n                }\n                e => txt += &e.to_string(),\n            }\n        }\n\n        if let Some(a) = Self::parse_after_eval(&mut Feeder::new(&txt), core, \"\")? {\n            self.text = a.text;\n            self.elements = a.elements;\n        }\n\n        if core.db.flags.contains('x') {\n            let ps4 = core.get_ps4();\n            eprintln!(\"\\r{} (( {} ))\\r\", ps4, &self.text);\n        }\n\n        Ok(())\n    }\n\n    pub fn eval(&mut self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let mut cp = self.clone();\n        cp.eval_doller(core)?;\n\n        let ans = match cp.eval_elems(core, true) {\n            Ok(a) => a,\n            Err(ExecError::ArithError(mut s, a)) => {\n                if s.is_empty() {\n                    s = cp.text.trim_start().to_string();\n                }\n                return Err(ExecError::ArithError(s, a));\n            }\n            Err(e) => return Err(e),\n        };\n\n        match ans {\n            ArithElem::Integer(n) => match self.ans_to_string(n) {\n                Ok(ans) => Ok(ans),\n                Err(a) => Err(ExecError::ArithError(cp.text, a)),\n            },\n            ArithElem::Float(f) => Ok(f.to_string()),\n            e => Err(ExecError::ArithError(\n                cp.text,\n                ArithError::OperandExpected(e.to_string()),\n            )),\n        }\n    }\n\n    pub fn eval_as_int(&mut self, core: &mut ShellCore) -> Result<i128, ExecError> {\n        let _ = self.eval_doller(core);\n\n        match self.eval_elems(core, true)? {\n            ArithElem::Integer(n) => Ok(n),\n            ArithElem::Float(f) => {\n                let msg = format!(\"sush: {}: Not integer. {}\", &self.text, f);\n                Err(ExecError::Other(msg))\n            }\n            _ => exit::internal(\"invalid calculation result\"),\n        }\n    }\n\n    pub fn eval_elems(\n        &mut self,\n        core: &mut ShellCore,\n        permit_empty: bool,\n    ) -> Result<ArithElem, ExecError> {\n        if self.elements.is_empty() && !permit_empty {\n            return Err(ArithError::OperandExpected(\"\\\")\\\"\".to_string()).into());\n        }\n        let es = self.decompose_increments()?;\n\n        calculate(&es, core)\n    }\n\n    fn ans_to_string(&self, n: i128) -> Result<String, ArithError> {\n        let base_str = self.output_base.clone();\n\n        if base_str == \"10\" {\n            return Ok(n.to_string());\n        }\n\n        let base = match base_str.parse::<i128>() {\n            Ok(b) => b,\n            _ => {\n                return Err(ArithError::InvalidBase(base_str));\n            }\n        };\n\n        if base <= 1 || base > 64 {\n            return Err(ArithError::InvalidBase(base_str));\n        }\n\n        let mut tmp = n.abs();\n        let mut digits = vec![];\n        while tmp != 0 {\n            digits.insert(0, (tmp % base) as u8);\n            tmp /= base;\n        }\n\n        let mut ans = Self::dec_to_str(&digits, base);\n        if !self.hide_base {\n            ans = base_str + \"#\" + &ans;\n        }\n\n        if n < 0 {\n            ans.insert(0, '-');\n        }\n\n        Ok(ans)\n    }\n\n    fn dec_to_str(nums: &[u8], base: i128) -> String {\n        let shift = if base <= 0 {\n            |n| n + b'0'\n        } else if base <= 36 {\n            |n| {\n                if n < 10 {\n                    n + b'0'\n                } else {\n                    n - 10 + b'A'\n                }\n            }\n        } else {\n            |n| {\n                if n < 10 {\n                    n + b'0'\n                } else if n < 36 {\n                    n - 10 + b'a'\n                } else if n < 62 {\n                    n - 36 + b'A'\n                } else if n == 62 {\n                    b'@'\n                } else {\n                    b'_'\n                }\n            }\n        };\n\n        let ascii = nums.iter().map(|n| shift(*n)).collect::<Vec<u8>>();\n        std::str::from_utf8(&ascii).unwrap().to_string()\n    }\n\n    fn eval_in_cond(&mut self, core: &mut ShellCore) -> Result<ArithElem, ExecError> {\n        let es = self.decompose_increments()?;\n        calculate(&es, core)\n    }\n\n    fn preinc_to_unarys(&mut self, ans: &mut Vec<ArithElem>, pos: usize, inc: i128) -> i128 {\n        let pm = match inc {\n            1 => \"+\",\n            -1 => \"-\",\n            _ => return 0,\n        }\n        .to_string();\n\n        match (&ans.last(), &self.elements.get(pos + 1)) {\n            (&Some(&ArithElem::Variable(_, _, _)), &Some(&ArithElem::Word(_, _))) => {\n                ans.push(ArithElem::BinaryOp(pm.clone()))\n            }\n            (_, &None) | (_, &Some(&ArithElem::Variable(_, _, _))) => return inc,\n            (&Some(&ArithElem::Integer(_)), _) | (&Some(&ArithElem::Float(_)), _) => {\n                ans.push(ArithElem::BinaryOp(pm.clone()))\n            }\n            _ => ans.push(ArithElem::UnaryOp(pm.clone())),\n        }\n        ans.push(ArithElem::UnaryOp(pm));\n        0\n    }\n\n    fn decompose_increments(&mut self) -> Result<Vec<ArithElem>, ExecError> {\n        let mut ans = vec![];\n        let mut pre_increment = 0;\n\n        let len = self.elements.len();\n        for i in 0..len {\n            let e = self.elements[i].clone();\n            pre_increment = match e {\n                ArithElem::Variable(_, _, _) => {\n                    if pre_increment != 0 {\n                        ans.push(ArithElem::Increment(pre_increment));\n                    }\n                    ans.push(e);\n                    0\n                }\n                ArithElem::Increment(n) => self.preinc_to_unarys(&mut ans, i, n),\n                _ => {\n                    ans.push(self.elements[i].clone());\n                    0\n                }\n            };\n        }\n\n        match pre_increment {\n            //↓treated as + or - in error messages\n            1 => Err(ArithError::OperandExpected(\"+\".to_string()).into()),\n            -1 => Err(ArithError::OperandExpected(\"-\".to_string()).into()),\n            _ => Ok(ans),\n        }\n    }\n\n    pub fn new() -> ArithmeticExpr {\n        ArithmeticExpr {\n            output_base: \"10\".to_string(),\n            ..Default::default()\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/conditional/elem.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::ConditionalExpr;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\nuse std::fmt;\n\n#[derive(Debug, Clone)]\npub enum CondElem {\n    UnaryOp(String),\n    BinaryOp(String),\n    Word(Word),\n    Regex(Word),\n    Operand(String),\n    InParen(ConditionalExpr),\n    Not, // !\n    And, // &&\n    Or,  // ||\n    Ans(bool),\n}\n\nimpl CondElem {\n    pub fn order(&self) -> u8 {\n        match self {\n            CondElem::UnaryOp(_) => 14,\n            CondElem::BinaryOp(_) => 13,\n            CondElem::Not => 12,\n            _ => 0,\n        }\n    }\n\n    pub fn eval(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if let CondElem::Word(w) = self {\n            let new_w = w.tilde_and_dollar_expansion(core)?;\n            *w = new_w;\n        }\n        Ok(())\n    }\n}\n\nimpl fmt::Display for CondElem {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            CondElem::UnaryOp(op) | CondElem::BinaryOp(op) | CondElem::Operand(op) => {\n                write!(f, \"{op}\")\n            }\n            CondElem::InParen(expr) => write!(f, \"{}\", expr.text),\n            CondElem::Word(w) | CondElem::Regex(w) => write!(f, \"{}\", w.text),\n            CondElem::Not => write!(f, \"!\"),\n            CondElem::And => write!(f, \"&&\"),\n            CondElem::Or => write!(f, \"||\"),\n            CondElem::Ans(true) => write!(f, \"true\"),\n            CondElem::Ans(false) => write!(f, \"false\"),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/conditional/parser.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{CondElem, ConditionalExpr};\nuse crate::elements::command;\nuse crate::elements::subword;\nuse crate::elements::word::Word;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\nimpl ConditionalExpr {\n    fn eat_word(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        if feeder.starts_with(\"]]\") || feeder.starts_with(\")\") || feeder.starts_with(\"(\") {\n            return false;\n        }\n\n        match Word::parse(feeder, core, None) {\n            Ok(Some(w)) => {\n                ans.text += &w.text.clone();\n                ans.elements.push(CondElem::Word(w));\n\n                true\n            }\n            _ => false,\n        }\n    }\n\n    fn eat_compare_op(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> String {\n        let len = feeder.scanner_test_compare_op(core);\n        if len == 0 {\n            return \"\".to_string();\n        }\n\n        let opt = feeder.consume(len);\n        ans.text += &opt.clone();\n        ans.elements.push(CondElem::BinaryOp(opt.clone()));\n\n        opt\n    }\n\n    fn eat_subwords(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> Word {\n        let mut word = Word::default();\n        while !feeder.starts_with(\" \") {\n            if let Ok(Some(sw)) = subword::parse(feeder, core, &None) {\n                ans.text += sw.get_text();\n                word.text += sw.get_text();\n                word.subwords.push(sw);\n                continue;\n            }\n\n            let len = feeder.scanner_regex_symbol();\n            if len == 0 {\n                break;\n            }\n\n            let symbol = feeder.consume(len);\n            ans.text += &symbol.clone();\n            word.subwords.push(From::from(&symbol));\n        }\n\n        word\n    }\n\n    fn eat_regex(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        if !Self::eat_blank(feeder, ans, core) {\n            return false;\n        }\n\n        let w = Self::eat_subwords(feeder, ans, core);\n        ans.elements.push(CondElem::Regex(w));\n        true\n    }\n\n    fn eat_file_check_option(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_test_check_option(core);\n        if len == 0 {\n            return false;\n        }\n\n        let opt = feeder.consume(len);\n        ans.text += &opt.clone();\n        ans.elements.push(CondElem::UnaryOp(opt));\n\n        true\n    }\n\n    fn eat_not_and_or(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        if feeder.starts_with(\"!\") {\n            ans.text += &feeder.consume(1);\n            ans.elements.push(CondElem::Not);\n            return true;\n        }\n        if feeder.starts_with(\"&&\") {\n            ans.text += &feeder.consume(2);\n            ans.elements.push(CondElem::And);\n            return true;\n        }\n        if feeder.starts_with(\"||\") {\n            ans.text += &feeder.consume(2);\n            ans.elements.push(CondElem::Or);\n            return true;\n        }\n\n        false\n    }\n\n    fn eat_paren(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(CondElem::UnaryOp(_)) = ans.elements.last() {\n            return Ok(false);\n        }\n\n        if !feeder.starts_with(\"(\") {\n            return Ok(false);\n        }\n\n        ans.text += &feeder.consume(1);\n\n        let expr = match Self::parse(feeder, core)? {\n            Some(e) => e,\n            None => return Ok(false),\n        };\n\n        if !feeder.starts_with(\")\") {\n            return Ok(false);\n        }\n\n        ans.text += &expr.text.clone();\n        ans.elements.push(CondElem::InParen(expr));\n        ans.text += &feeder.consume(1);\n        Ok(true)\n    }\n\n    fn eat_blank(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        match feeder.scanner_blank(core) {\n            0 => false,\n            n => {\n                ans.text += &feeder.consume(n);\n                true\n            }\n        }\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n        let mut read_option = true;\n\n        loop {\n            //Self::eat_blank(feeder, &mut ans, core)?;\n            command::eat_blank_lines(feeder, core, &mut ans.text)?;\n            if feeder.starts_with(\"\\n\") {\n                ans.text += &feeder.consume(1);\n                continue;\n            }\n            if feeder.is_empty() {\n                if feeder.feed_additional_line(core).is_err() {\n                    return Ok(None);\n                }\n                continue;\n            }\n\n            if feeder.starts_with(\"]]\") || feeder.starts_with(\")\") {\n                if ans.elements.is_empty() {\n                    return Ok(None);\n                }\n\n                ans.elements.push(CondElem::And);\n                return Ok(Some(ans));\n            }\n\n            if Self::eat_paren(feeder, &mut ans, core)? {\n                continue;\n            }\n\n            match Self::eat_compare_op(feeder, &mut ans, core).as_ref() {\n                \"\" => {}\n                \"=~\" => match Self::eat_regex(feeder, &mut ans, core) {\n                    false => return Ok(None),\n                    true => continue,\n                },\n                _ => continue,\n            }\n\n            if read_option && Self::eat_file_check_option(feeder, &mut ans, core) {\n                continue;\n            }\n\n            if Self::eat_not_and_or(feeder, &mut ans) {\n                read_option = true;\n                continue;\n            }\n\n            if Self::eat_word(feeder, &mut ans, core) {\n                read_option = false;\n                continue;\n            }\n\n            break;\n        }\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/expr/conditional.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod elem;\nmod parser;\n\nuse self::elem::CondElem;\nuse super::arithmetic::elem::ArithElem;\nuse crate::elements::expr::arithmetic::elem::{float, int};\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::elements::substitution::variable::Variable;\nuse crate::elements::word::Word;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::utils::{file_check, glob};\nuse crate::{utils, Feeder, ShellCore};\nuse regex::Regex;\nuse std::env;\n\nfn to_operand(w: &mut Word) -> Result<CondElem, ExecError> {\n    match w.make_unquoted_word() {\n        Some(v) => Ok(CondElem::Operand(v)),\n        None => Ok(CondElem::Operand(\"\".to_string())),\n    }\n}\n\nfn pop_operand(\n    stack: &mut Vec<CondElem>,\n    core: &mut ShellCore,\n    glob: bool,\n) -> Result<CondElem, ExecError> {\n    match stack.pop() {\n        Some(CondElem::InParen(mut expr)) => expr.eval(core),\n        Some(CondElem::Word(mut w)) => {\n            if glob {\n                let p = w.eval_for_case_pattern(core)?;\n                return Ok(CondElem::Operand(p));\n            }\n            to_operand(&mut w)\n        }\n        Some(elem) => Ok(elem),\n        None => Err(ArithError::OperandExpected(\"\".to_string()).into()),\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ConditionalExpr {\n    pub text: String,\n    elements: Vec<CondElem>,\n}\n\nimpl ConditionalExpr {\n    pub fn eval(&mut self, core: &mut ShellCore) -> Result<CondElem, ExecError> {\n        let mut cp = self.clone();\n\n        let mut from = 0;\n        let mut next = true;\n        let mut last = CondElem::Ans(true);\n\n        for e in cp.elements.iter_mut() {\n            e.eval(core)?;\n        }\n\n        if core.db.flags.contains('x') {\n            let mut elems = cp\n                .elements\n                .clone()\n                .into_iter()\n                .map(|e| e.to_string())\n                .collect::<Vec<String>>();\n            elems.pop();\n            eprintln!(\"{} ]]\\r\", &elems.join(\" \"));\n        }\n\n        for i in 0..cp.elements.len() {\n            match cp.elements[i] {\n                CondElem::And | CondElem::Or => {\n                    if next {\n                        last = Self::calculate(&cp.elements[from..i], core)?;\n                    }\n                    from = i + 1;\n\n                    next = match (&cp.elements[i], &last) {\n                        (CondElem::And, CondElem::Ans(ans)) => *ans,\n                        (CondElem::Or, CondElem::Ans(ans)) => !ans,\n                        _ => {\n                            return Err(ExecError::Other(\n                                \"Internal error conditional.rs:55\".to_string(),\n                            ))\n                        }\n                    };\n                }\n                _ => {}\n            }\n        }\n\n        Ok(last)\n    }\n\n    fn calculate(elems: &[CondElem], core: &mut ShellCore) -> Result<CondElem, ExecError> {\n        let rev_pol = Self::rev_polish(elems)?;\n        let mut stack = Self::reduce(&rev_pol, core)?;\n\n        match pop_operand(&mut stack, core, false) {\n            Ok(CondElem::Operand(s)) => Ok(CondElem::Ans(!s.is_empty())), //for [[ string ]]\n            other_ans => other_ans,\n        }\n    }\n\n    fn rev_polish(elems: &[CondElem]) -> Result<Vec<CondElem>, ExecError> {\n        let mut ans = vec![];\n        let mut stack = vec![];\n\n        for e in elems {\n            let ok = match e {\n                CondElem::Word(_) | CondElem::InParen(_) | CondElem::Regex(_) => {\n                    ans.push(e.clone());\n                    true\n                }\n                op => Self::rev_polish_op(op, &mut stack, &mut ans),\n            };\n\n            if !ok {\n                return Err(ExecError::SyntaxError(e.to_string()));\n            }\n        }\n\n        while let Some(element) = stack.pop() {\n            ans.push(element);\n        }\n\n        Ok(ans)\n    }\n\n    fn reduce(rev_pol: &[CondElem], core: &mut ShellCore) -> Result<Vec<CondElem>, ExecError> {\n        let mut stack = vec![];\n\n        for e in rev_pol {\n            let result = match e {\n                CondElem::Word(_) | CondElem::Regex(_) | CondElem::InParen(_) => {\n                    stack.push(e.clone());\n                    Ok(())\n                }\n                CondElem::UnaryOp(op) => Self::unary_operation(op, &mut stack, core),\n                CondElem::BinaryOp(op) => {\n                    if stack.is_empty() {\n                        return Ok(vec![CondElem::Ans(true)]); //for [[ -ot ]] [[ == ]] [[ = ]] ...\n                    }\n                    if op == \"=~\" {\n                        Self::regex_operation(&mut stack, core)\n                    } else {\n                        Self::bin_operation(op, &mut stack, core)\n                    }\n                }\n                CondElem::Not => match pop_operand(&mut stack, core, false) {\n                    Ok(CondElem::Ans(res)) => {\n                        stack.push(CondElem::Ans(!res));\n                        Ok(())\n                    }\n                    Ok(CondElem::Operand(s)) => {\n                        stack.push(CondElem::Ans(s.is_empty()));\n                        Ok(())\n                    }\n                    _ => Err(ExecError::Other(\"no operand to negate\".to_string())),\n                },\n                // _ => Err(ExecError::Other( error::syntax(\"TODO\"))),\n                _ => Err(ArithError::OperandExpected(\"TODO\".to_string()).into()),\n            };\n\n            if let Err(err_msg) = result {\n                core.db.exit_status = 2;\n                return Err(err_msg);\n            }\n        }\n\n        if stack.len() != 1 {\n            let mut err = \"syntax error\".to_string();\n            if stack.len() > 1 {\n                err = format!(\n                    \"syntax error in conditional expression: unexpected token `{}'\",\n                    &stack[0].to_string()\n                );\n                ExecError::Other(err).print(core);\n                err = format!(\"syntax error near `{}'\", &stack[0].to_string());\n            }\n            return Err(ExecError::Other(err));\n        }\n\n        Ok(stack)\n    }\n\n    fn unary_operation(\n        op: &str,\n        stack: &mut Vec<CondElem>,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        let operand = match pop_operand(stack, core, false) {\n            Ok(CondElem::Operand(v)) => v,\n            Ok(_) => return Err(ExecError::Other(\"unknown operand\".to_string())),\n            Err(e) => return Err(e),\n        };\n\n        if op == \"-o\" || op == \"-v\" || op == \"-z\" || op == \"-n\" {\n            let ans = match op {\n                \"-o\" => core.options.query(&operand),\n                \"-v\" => {\n                    if env::var(&operand).is_ok() {\n                        true\n                    } else {\n                        let mut f = Feeder::new(&operand);\n                        if let Some(v) = Variable::parse(&mut f, core)? {\n                            v.exist(core)?\n                        } else {\n                            false\n                        }\n                    }\n                }\n                \"-z\" => operand.is_empty(),\n                \"-n\" => !operand.is_empty(),\n                _ => false,\n            };\n\n            stack.push(CondElem::Ans(ans));\n            return Ok(());\n        }\n\n        Self::unary_file_check(op, &operand, stack)\n    }\n\n    fn regex_operation(stack: &mut Vec<CondElem>, core: &mut ShellCore) -> Result<(), ExecError> {\n        let right = match pop_operand(stack, core, false) {\n            Ok(CondElem::Regex(right)) => right,\n            Ok(_) => return Err(ExecError::Other(\"Invalid operand\".to_string())),\n            Err(e) => return Err(e),\n        };\n\n        let left = match pop_operand(stack, core, false) {\n            Ok(CondElem::Operand(name)) => name,\n            Ok(_) => return Err(ExecError::Other(\"Invalid operand\".to_string())),\n            Err(e) => return Err(e),\n        };\n\n        let right_eval = match right.eval_for_regex(core) {\n            Some(r) => r,\n            None => return Err(ExecError::Other(\"Invalid operand\".to_string())),\n        };\n\n        let re = match Regex::new(&right_eval) {\n            Ok(regex) => regex,\n            Err(e) => return Err(ExecError::Other(e.to_string())),\n        };\n\n        core.db.init_array(\"BASH_REMATCH\", Some(vec![]), None, false)?;\n        if let Some(res) = re.captures(&left) {\n            for i in 0.. {\n                if let Some(e) = res.get(i) {\n                    let s = e.as_str().to_string();\n                    core.db\n                        .set_array_elem(\"BASH_REMATCH\", &s, i as isize, None, false)?;\n                } else {\n                    break;\n                }\n            }\n            stack.push(CondElem::Ans(true));\n        } else {\n            stack.push(CondElem::Ans(false));\n        }\n\n        Ok(())\n    }\n\n    fn resolve_arithmetic_op(name: &str, core: &mut ShellCore) -> Result<ArithElem, ArithError> {\n        let mut f = Feeder::new(name);\n        let mut parsed = match ArithmeticExpr::parse(&mut f, core, false, \"\") {\n            Ok(Some(p)) => p,\n            _ => return Err(ArithError::OperandExpected(name.to_string())),\n        };\n\n        if let Ok(eval) = parsed.eval(core) {\n            return Self::single_str_to_num(&eval, core);\n        }\n\n        Err(ArithError::OperandExpected(name.to_string()))\n    }\n\n    fn single_str_to_num(name: &str, core: &mut ShellCore) -> Result<ArithElem, ArithError> {\n        if name.contains('.') {\n            let f = float::parse(name)?;\n            return Ok(ArithElem::Float(f));\n        }\n\n        if utils::is_name(name, core) {\n            return Ok(ArithElem::Integer(0));\n        }\n\n        let n = int::parse(name)?;\n        Ok(ArithElem::Integer(n))\n    }\n\n    fn bin_operation(\n        op: &str,\n        stack: &mut Vec<CondElem>,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        let right = match pop_operand(stack, core, true) {\n            Ok(CondElem::Operand(name)) => name,\n            Ok(_) => return Err(ExecError::Other(\"Invalid operand\".to_string())),\n            Err(e) => return Err(e),\n        };\n\n        let left = match pop_operand(stack, core, false) {\n            Ok(CondElem::Operand(name)) => name,\n            Ok(_) => return Err(ExecError::Other(\"Invalid operand\".to_string())),\n            Err(e) => return Err(e),\n        };\n\n        let extglob = core.shopts.query(\"extglob\");\n        if op.starts_with(\"=\") || op == \"!=\" || op == \"<\" || op == \">\" {\n            let ans = match op {\n                \"==\" | \"=\" => glob::parse_and_compare(&left, &right, extglob),\n                \"!=\" => !glob::parse_and_compare(&left, &right, extglob),\n                \">\" => left > right,\n                \"<\" => left < right,\n                _ => false,\n            };\n\n            stack.push(CondElem::Ans(ans));\n            return Ok(());\n        }\n\n        if op == \"-eq\" || op == \"-ne\" || op == \"-lt\" || op == \"-le\" || op == \"-gt\" || op == \"-ge\" {\n            let lnum = match Self::resolve_arithmetic_op(&left, core)? {\n                ArithElem::Integer(n) => n,\n                _ => {\n                    return Err(ExecError::Other(\n                        \"non integer number is not supported\".to_string(),\n                    ))\n                }\n            };\n            let rnum = match Self::resolve_arithmetic_op(&right, core)? {\n                ArithElem::Integer(n) => n,\n                _ => {\n                    return Err(ExecError::Other(\n                        \"non integer number is not supported\".to_string(),\n                    ))\n                }\n            };\n\n            let ans = match op {\n                \"-eq\" => lnum == rnum,\n                \"-ne\" => lnum != rnum,\n                \"-lt\" => lnum < rnum,\n                \"-le\" => lnum <= rnum,\n                \"-gt\" => lnum > rnum,\n                \"-ge\" => lnum >= rnum,\n                _ => false,\n            };\n\n            stack.push(CondElem::Ans(ans));\n            return Ok(());\n        }\n\n        let result = file_check::metadata_comp(&left, &right, op);\n        stack.push(CondElem::Ans(result));\n        Ok(())\n    }\n\n    fn unary_file_check(op: &str, s: &str, stack: &mut Vec<CondElem>) -> Result<(), ExecError> {\n        let result = match op {\n            \"-a\" | \"-e\" => file_check::exists(s),\n            \"-d\" => file_check::is_dir(s),\n            \"-f\" => file_check::is_regular_file(s),\n            \"-h\" | \"-L\" => file_check::is_symlink(s),\n            \"-r\" => file_check::is_readable(s),\n            \"-t\" => file_check::is_tty_str(s),\n            \"-w\" => file_check::is_writable(s),\n            \"-x\" => file_check::is_executable(s),\n            \"-b\" | \"-c\" | \"-g\" | \"-k\" | \"-p\" | \"-s\" | \"-u\" | \"-G\" | \"-N\" | \"-O\" | \"-S\" => {\n                file_check::metadata_check(s, op)\n            }\n            _ => return Err(ExecError::Other(\"unsupported option\".to_string())),\n        };\n\n        stack.push(CondElem::Ans(result));\n        Ok(())\n    }\n\n    fn rev_polish_op(elem: &CondElem, stack: &mut Vec<CondElem>, ans: &mut Vec<CondElem>) -> bool {\n        loop {\n            match stack.last() {\n                None => {\n                    stack.push(elem.clone());\n                    break;\n                }\n                Some(_) => {\n                    let last = stack.last().unwrap();\n                    if last.order() <= elem.order() {\n                        stack.push(elem.clone());\n                        break;\n                    }\n                    ans.push(stack.pop().unwrap());\n                }\n            }\n        }\n\n        true\n    }\n}\n"
  },
  {
    "path": "src/elements/expr.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod arithmetic;\npub mod conditional;\n"
  },
  {
    "path": "src/elements/io/pipe.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse crate::{Feeder, ShellCore};\nuse nix::unistd::Pid;\nuse std::os::unix::prelude::RawFd;\n\n#[derive(Debug, Clone)]\npub struct Pipe {\n    pub text: String,\n    pub recv: RawFd,\n    pub send: RawFd,\n    pub prev: RawFd,\n    pub pgid: Pid,\n    pub lastpipe: bool,\n    pub lastpipe_backup: RawFd,\n    pub proc_sub_recv: RawFd,\n    pub proc_sub_send: RawFd,\n}\n\nimpl Pipe {\n    pub fn new(text: String) -> Pipe {\n        Pipe {\n            text: text.clone(),\n            recv: -1,\n            send: -1,\n            prev: -1,\n            pgid: Pid::from_raw(0),\n            lastpipe: false,\n            lastpipe_backup: -1,\n            proc_sub_recv: -1,\n            proc_sub_send: -1,\n        }\n    }\n\n    pub fn end(prev: RawFd, pgid: Pid, lastpipe: bool) -> Pipe {\n        let mut p = Pipe::new(String::new());\n        p.lastpipe = lastpipe;\n        p.prev = prev;\n        p.pgid = pgid;\n        p\n    }\n\n    pub fn connect_lastpipe(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.lastpipe && self.prev != 0 {\n            self.lastpipe_backup = core.fds.backup(0);\n            core.fds.replace(self.prev, 0)?;\n        }\n        Ok(())\n    }\n\n    pub fn restore_lastpipe(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.lastpipe && self.lastpipe_backup != -1 {\n            core.fds.replace(self.lastpipe_backup, 0)?;\n        }\n        Ok(())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Pipe> {\n        let len = feeder.scanner_pipe(core);\n\n        if len > 0 {\n            Some(Self::new(feeder.consume(len)))\n        } else {\n            None\n        }\n    }\n\n    pub fn set(&mut self, prev: RawFd, pgid: Pid, core: &mut ShellCore) {\n        if self.text != \">()\" {\n            (self.recv, self.send) = core.fds.pipe();\n            self.prev = prev;\n        }\n\n        if self.text == \">()\" {\n            (self.proc_sub_recv, self.proc_sub_send) = core.fds.pipe();\n            self.prev = self.proc_sub_recv;\n        }\n\n        self.pgid = pgid;\n    }\n\n    pub fn connect(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.text == \">()\" {\n            core.fds.replace(self.proc_sub_send, 0)?;\n        }\n\n        core.fds.close(self.recv);\n        core.fds.replace(self.send, 1)?;\n        core.fds.replace(self.prev, 0)?;\n\n        if self.text == \"|&\" {\n            core.fds.share(1, 2)?;\n        }\n        Ok(())\n    }\n\n    pub fn parent_close(&mut self, core: &mut ShellCore) {\n        core.fds.close(self.send);\n        core.fds.close(self.prev);\n    }\n\n    pub fn is_connected(&self) -> bool {\n        if self.lastpipe {\n            return false;\n        }\n        self.recv != -1 || self.send != -1 || self.prev != -1\n    }\n}\n"
  },
  {
    "path": "src/elements/io/redirect.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword;\nuse crate::elements::subword::filler::FillerSubword;\nuse crate::elements::word::Word;\nuse crate::elements::word::WordMode;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::{exit, file_check};\nuse crate::{error, Feeder, ShellCore};\nuse nix::unistd;\nuse nix::unistd::ForkResult;\nuse std::fs::{File, OpenOptions};\nuse std::io::Error;\nuse std::io::Write;\nuse std::os::fd::FromRawFd;\nuse std::os::fd::{IntoRawFd, RawFd};\nuse std::process;\n\n#[derive(Debug, Clone, Default)]\npub struct Redirect {\n    pub text: String,\n    pub symbol: String,\n    pub right: Word,\n    pub left: String,\n    left_fd: RawFd,\n    left_backup: RawFd,\n    extra_left_backup: RawFd, // &>, &>>用\n    here_data: Word,\n    pub called_as_heredoc: bool,\n}\n\nimpl Redirect {\n    pub fn connect(&mut self, restore: bool, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.symbol == \"<<\" || self.symbol == \"<<-\" {\n            return self.redirect_heredocument(core, restore);\n        }\n        if self.symbol == \"<<<\" {\n            return self.redirect_herestring(core, restore);\n        }\n\n        let args = self.right.eval(core)?;\n        if args.len() != 1 {\n            return Err(ExecError::AmbiguousRedirect(self.right.text.clone()));\n        }\n\n        if core.db.flags.contains('r') {\n            match self.symbol.as_str() {\n                \">\" | \">|\" | \"<>\" | \">&\" | \"&>\" | \">>\" => {\n                    let msg = format!(\"{}: restricted: cannot redirect output\", &args[0]);\n                    return Err(ExecError::Other(msg));\n                }\n                _ => {}\n            }\n        }\n\n        self.right.text = args[0].clone();\n\n        if core.options.query(\"noclobber\")\n            && (self.symbol.as_str() == \">\" || self.symbol.as_str() == \">>\")\n            && file_check::exists(&self.right.text)\n        {\n            return Err(ExecError::CannotOverwriteExistingFile(\n                self.right.text.clone(),\n            ));\n        }\n\n        match self.symbol.as_str() {\n            \"<\" => self.redirect_simple_input(restore, core),  // <\n            \">\" => self.redirect_simple_output(restore, core), // >\n            \">&\" => self.redirect_output_fd(restore, core),    // >&2\n            \"<&\" => self.redirect_input_fd(restore, core),     // <&2\n            \">>\" => self.redirect_append(restore, core),\n            \"&>\" => self.redirect_both_output(restore, core),\n            _ => exit::internal(\" (Unknown redirect symbol)\"),\n        }\n    }\n\n    fn set_left_fd(&mut self, default_fd: RawFd) {\n        self.left_fd = match self.left.len() {\n            0 => default_fd,\n            _ => self.left.parse().unwrap(),\n        }\n    }\n\n    fn connect_to_file(\n        &mut self,\n        file_open_result: Result<File, Error>,\n        restore: bool,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        if restore {\n            self.left_backup = core.fds.backup(self.left_fd);\n        }\n\n        if self.left_fd < 0 {\n            return Err(ExecError::BadFd(self.left_fd));\n        }\n\n        match file_open_result {\n            Ok(file) => {\n                let fd = file.into_raw_fd();\n                if let Err(e) = core.fds.replace(fd, self.left_fd) {\n                    core.fds.close(fd);\n                    self.left_fd = -1;\n                    return Err(e);\n                }\n                Ok(())\n            }\n            _ => {\n                let msg = format!(\"{}: {}\", &self.right.text, Error::last_os_error().kind());\n                Err(ExecError::Other(msg))\n            }\n        }\n    }\n\n    fn redirect_simple_input(&mut self, restore: bool,\n                             core: &mut ShellCore) -> Result<(), ExecError> {\n        self.set_left_fd(0);\n        self.connect_to_file(File::open(&self.right.text), restore, core)\n    }\n\n    fn redirect_simple_output(&mut self, restore: bool, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.set_left_fd(1);\n        self.connect_to_file(File::create(&self.right.text), restore, core)\n    }\n\n    fn redirect_output_fd(&mut self, restore: bool, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.right.text == \"-\" {\n            self.set_left_fd(1);\n            core.fds.close(self.left_fd);\n            return Ok(());\n        }\n\n        let right_fd = match self.right.text.parse::<RawFd>() {\n            Ok(n) => n,\n            _ => return Err(ExecError::AmbiguousRedirect(self.right.text.clone())),\n        };\n        self.set_left_fd(1);\n\n        if restore {\n            self.left_backup = core.fds.backup(self.left_fd);\n        }\n\n        core.fds.share(right_fd, self.left_fd)\n    }\n\n    fn redirect_input_fd(&mut self, restore: bool,\n                         core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.right.text == \"-\" {\n            self.set_left_fd(0);\n            core.fds.close(self.left_fd);\n            return Ok(());\n        }\n\n        let right_fd = match self.right.text.parse::<RawFd>() {\n            Ok(n) => n,\n            _ => return Err(ExecError::AmbiguousRedirect(self.right.text.clone())),\n        };\n        self.set_left_fd(0);\n\n        if restore {\n            self.left_backup = core.fds.backup(self.left_fd);\n        }\n\n        core.fds.share(right_fd, self.left_fd)\n    }\n\n    fn redirect_append(&mut self, restore: bool, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.set_left_fd(1);\n        self.connect_to_file(\n            OpenOptions::new()\n                .create(true)\n                .append(true)\n                .open(&self.right.text),\n            restore,\n            core,\n        )\n    }\n\n    fn redirect_both_output(&mut self, restore: bool, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.left_fd = 1;\n        self.connect_to_file(File::create(&self.right.text), restore, core)?;\n\n        if restore {\n            self.extra_left_backup = core.fds.backup(2);\n        }\n        core.fds.share(1, 2)\n    }\n\n    fn redirect_heredocument(\n        &mut self,\n        core: &mut ShellCore,\n        restore: bool,\n    ) -> Result<(), ExecError> {\n        self.left_fd = 0;\n        let (recv, send) = core.fds.pipe();\n\n        if restore {\n            self.left_backup = core.fds.backup(0);\n        }\n\n        let right = self.right.make_unquoted_word().unwrap_or(\"\".to_string());\n        let quoted = right != self.right.text;\n\n        let text = match quoted {\n            false => self.here_data.eval_as_alter(core)?, // TODO: make it precise\n            true => self.here_data.text.clone(),\n        };\n\n        match unsafe { unistd::fork()? } {\n            ForkResult::Child => {\n                core.fds.close(recv);\n                let mut f = unsafe { File::from_raw_fd(send) };\n                let _ = write!(&mut f, \"{}\", &text);\n                f.flush().unwrap();\n                core.fds.close(send);\n                process::exit(0);\n            }\n            ForkResult::Parent { child: _ } => {\n                core.fds.close(send);\n                core.fds.replace(recv, 0)?;\n            }\n        }\n        Ok(())\n    }\n\n    fn redirect_herestring(\n        &mut self,\n        core: &mut ShellCore,\n        restore: bool,\n    ) -> Result<(), ExecError> {\n        self.left_fd = 0;\n        let (recv, send) = core.fds.pipe();\n\n        if restore {\n            self.left_backup = core.fds.backup(0);\n        }\n\n        let text = self.right.eval_as_herestring(core)?;\n\n        match unsafe { unistd::fork()? } {\n            ForkResult::Child => {\n                core.fds.close(recv);\n                let mut f = unsafe { File::from_raw_fd(send) };\n                let _ = writeln!(&mut f, \"{}\", &text);\n                f.flush().unwrap();\n                core.fds.close(send);\n                process::exit(0);\n            }\n            ForkResult::Parent { child: _ } => {\n                core.fds.close(send);\n                core.fds.replace(recv, 0)?;\n            }\n        }\n        Ok(())\n    }\n\n    pub fn restore(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.left_backup >= 0 && self.left_fd >= 0 {\n            if self.left_backup == self.left_fd {\n                core.fds.close(self.left_fd);\n            } else {\n                core.fds.replace(self.left_backup, self.left_fd)?;\n            }\n        }\n        if self.extra_left_backup >= 0 {\n            core.fds.replace(self.extra_left_backup, 2)?;\n        }\n        Ok(())\n    }\n\n    pub fn new() -> Redirect {\n        Redirect {\n            left_fd: -1,\n            left_backup: -1,\n            extra_left_backup: -1,\n            ..Default::default()\n        }\n    }\n\n    fn show_heredoc_warning(&self, lineno: usize, feeder_lineno: usize, core: &mut ShellCore) {\n        let msg = format!(\"warning: here-document at line {} delimited by end-of-file (wanted `{}')\", lineno, &self.right.text.replace(\"\\\\\", \"\"));\n        let _ = core.db.set_param(\"LINENO\", &feeder_lineno.to_string(), None);\n        error::print(&msg, core);\n    }\n\n    pub fn eat_heredoc(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        lineno: usize,\n    ) -> Result<(), ParseError> {\n        let remove_tab = self.symbol == \"<<-\";\n        let end = match self.right.eval_as_value(core) {\n            Ok(s) => s,\n            Err(_) => return Err(ParseError::UnexpectedSymbol(self.right.text.clone())),\n        };\n\n        let mut end_nest = end.clone();\n        let mut back_quote = false;\n        let end_return = end.clone() + \"\\n\";\n        match feeder.nest.last().unwrap().0.as_str() {\n            \"(\" => end_nest += \")\",\n            \"`\" => {\n                back_quote = true;\n                end_nest += \")\";\n            },\n            _ => end_nest += \"\\n\",\n        }\n\n        if feeder.starts_with(\"\\n\") {\n            feeder.consume(1);\n        }\n\n        loop {\n            if feeder.is_empty() &&feeder.feed_additional_line(core).is_err() {\n                self.show_heredoc_warning(lineno, feeder.lineno-1, core);\n                break;\n            }\n\n            if remove_tab {\n                let len = feeder.scanner_tabs();\n                feeder.consume(len);\n            }\n\n            if feeder.starts_with(&end_nest) || feeder.starts_with(&end_return) {\n                feeder.consume(end.len());\n                if !back_quote && feeder.starts_with(\")\") {\n                    self.show_heredoc_warning(lineno, feeder.lineno, core);\n                }\n                break;\n            }else if feeder.starts_with(&(end.clone())) {\n                feeder.consume(end.len());\n                self.show_heredoc_warning(lineno, feeder.lineno, core);\n                break;\n            }\n\n            if let Some(mut sw) = subword::parse(feeder, core, &Some(WordMode::Heredoc))? {\n                sw.set_heredoc_flag();\n                self.here_data.text += sw.get_text();\n                self.here_data.subwords.push(sw);\n            } else {\n                let len = feeder.scanner_char();\n                if len > 0 {\n                    let c = feeder.consume(len);\n                    self.here_data.text += &c;\n                    self.here_data\n                        .subwords\n                        .push(Box::new(FillerSubword { text: c }));\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    fn eat_symbol(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_redirect_symbol(core);\n        if len == 0 {\n            return false;\n        }\n\n        ans.symbol = feeder.consume(len);\n        ans.text += &ans.symbol.clone();\n        true\n    }\n\n    fn eat_right(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let blank_len = feeder.scanner_blank(core);\n        ans.text += &feeder.consume(blank_len);\n\n        let w = match Word::parse(feeder, core, None) {\n            Ok(Some(w)) => w,\n            _ => return false,\n        };\n\n        ans.text += &w.text.clone();\n        ans.right = w;\n        true\n    }\n\n    fn eat_left(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_uint(core);\n        if len == 0 {\n            return true; //左側なし（文法上OK）\n        }\n\n        ans.left = feeder.consume(len);\n        ans.text += &ans.left.clone();\n\n        ans.left.parse::<RawFd>().is_ok()\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Redirect> {\n        let mut ans = Self::new();\n        feeder.set_backup(); //追加\n\n        if Self::eat_left(feeder, &mut ans, core)\n            && Self::eat_symbol(feeder, &mut ans, core)\n            && Self::eat_right(feeder, &mut ans, core)\n        {\n            feeder.pop_backup();\n            Some(ans)\n        } else {\n            feeder.rewind(); //追加\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/io.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod pipe;\npub mod redirect;\n\nuse crate::elements::io::redirect::Redirect;\nuse crate::elements::Pipe;\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\n\npub fn connect(\n    pipe: &mut Pipe,\n    rs: &mut [Redirect],\n    core: &mut ShellCore,\n) -> Result<(), ExecError> {\n    pipe.connect(core)?;\n\n    for r in rs.iter_mut() {\n        r.connect(false, core)?;\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "src/elements/job.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::pipeline::Pipeline;\nuse crate::core::jobtable::JobEntry;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::signal;\nuse crate::utils::exit;\nuse crate::{proc_ctrl, Feeder, ShellCore};\nuse nix::sys::wait::WaitStatus;\nuse nix::unistd;\nuse nix::unistd::{ForkResult, Pid};\nuse std::sync::atomic::Ordering::Relaxed;\n\n#[derive(Debug, Clone, Default)]\npub struct Job {\n    pub pipelines: Vec<Pipeline>,\n    pub pipeline_ends: Vec<String>,\n    pub text: String,\n}\n\nimpl Job {\n    pub fn exec(&mut self, core: &mut ShellCore, bg: bool) -> Result<(), ExecError> {\n        let pgid = match core.is_subshell {\n            true => unistd::getpgrp(),\n            false => Pid::from_raw(0),\n        };\n\n        match bg {\n            true => self.exec_bg(core, pgid),\n            false => self.exec_fg(core, pgid)?,\n        }\n        Ok(())\n    }\n\n    fn exec_fg(&mut self, core: &mut ShellCore, pgid: Pid) -> Result<(), ExecError> {\n        let mut do_next = true;\n        let susp_e_option = core.suspend_e_option;\n\n        signal::check_trap(core);\n\n        for (pipeline, end) in self.pipelines.iter_mut().zip(self.pipeline_ends.iter()) {\n            if core.return_flag {\n                break;\n            }\n            if core.sigint.load(Relaxed) {\n                core.db.exit_status = 130;\n                return Err(ExecError::Interrupted);\n            }\n\n            core.suspend_e_option = susp_e_option || end == \"&&\" || end == \"||\";\n            if do_next {\n                core.jobtable_check_status()?;\n                let (pids, exclamation, time, err) = pipeline.exec(core, pgid);\n                let waitstatuses = proc_ctrl::wait_pipeline(core, pids.clone(), exclamation, time);\n\n                Self::check_stop(core, &pipeline.get_one_line_text(), &pids, &waitstatuses);\n\n                if let Some(e) = err {\n                    return Err(e);\n                }\n            }\n\n            do_next = (core.db.exit_status == 0) == (end == \"&&\");\n            signal::check_trap(core);\n        }\n        signal::check_trap(core);\n        Ok(())\n    }\n\n    fn check_stop(\n        core: &mut ShellCore,\n        text: &str,\n        pids: &[Option<Pid>],\n        waitstatuses: &[WaitStatus],\n    ) {\n        if core.is_subshell || pids.is_empty() || pids[0].is_none() {\n            return;\n        }\n\n        for ws in waitstatuses {\n            if let WaitStatus::Stopped(_, _) = ws {\n                let new_job_id = core.generate_new_job_id();\n                let job = JobEntry::new(pids.to_vec(), waitstatuses, text, \"Stopped\", new_job_id);\n                core.job_table_priority.insert(0, new_job_id);\n                core.job_table.push(job);\n                return;\n            }\n        }\n    }\n\n    fn exec_bg(&mut self, core: &mut ShellCore, pgid: Pid) {\n        let backup = core.tty_fd.clone();//core.tty_fd.as_ref().map(|fd| fd.try_clone().unwrap());\n        core.tty_fd = None;\n\n        let pids = if self.pipelines.len() == 1 {\n            if self.pipelines[0].commands.len() == 1 {\n                self.pipelines[0].commands[0].set_force_fork();\n            }\n            self.pipelines[0].exec(core, pgid).0\n        } else {\n            match self.exec_fork_bg(core, pgid) {\n                Ok(pid) => vec![pid],\n                Err(e) => {\n                    e.print(core);\n                    return;\n                }\n            }\n        };\n\n        if core.db.flags.contains('i') {\n            eprintln!(\"{}\", &pids[0].unwrap().as_raw());\n        }\n        let _ = core.db.set_param(\"!\", &pids[0].unwrap().to_string(), None);\n        let len = pids.len();\n        let new_job_id = core.generate_new_job_id();\n        core.job_table_priority.insert(0, new_job_id);\n        let mut entry = JobEntry::new(\n            pids,\n            &vec![WaitStatus::StillAlive; len],\n            &self.get_one_line_text(),\n            \"Running\",\n            new_job_id,\n        );\n\n        if !core.options.query(\"monitor\") {\n            entry.no_control = true;\n        }\n\n        core.job_table.push(entry);\n\n        core.tty_fd = backup;\n    }\n\n    fn exec_fork_bg(&mut self, core: &mut ShellCore, pgid: Pid) -> Result<Option<Pid>, ExecError> {\n        match unsafe { unistd::fork()? } {\n            ForkResult::Child => {\n                core.initialize_as_subshell(Pid::from_raw(0), pgid);\n                if let Err(e) = self.exec(core, false) {\n                    e.print(core);\n                }\n                exit::normal(core)\n            }\n            ForkResult::Parent { child } => {\n                proc_ctrl::set_pgid(core, child, pgid);\n                Ok(Some(child))\n            }\n        }\n    }\n\n    pub fn pretty_print(\n        &mut self,\n        indent_num: usize,\n        semicolon: &mut bool,\n        printed: &mut bool,\n        job_end: &str,\n        end: bool,\n    ) -> bool {\n        let tmp = self.text.clone();\n        let job_text = tmp.trim_ascii();\n\n        if job_text.is_empty() {\n            *semicolon = *printed;\n            return false;\n        }\n\n        if *semicolon && !end {\n            println!(\";\");\n            *semicolon = false;\n        } else if *printed {\n            println!();\n        }\n\n        let tmp = job_end.to_string();\n        let job_end = tmp.trim_ascii_end();\n\n        let text = match end {\n            false => job_text.to_owned() + job_end,\n            true => job_text.to_owned(),\n        };\n\n        for _ in 0..indent_num {\n            print!(\"    \");\n        }\n        print!(\"{}\", &text);\n        *printed = true;\n        true\n    }\n\n    pub fn get_one_line_text(&self) -> String {\n        let mut ans = String::new();\n        for (i, p) in self.pipelines.iter().enumerate() {\n            ans += p.get_one_line_text().trim_end();\n            ans += &self.pipeline_ends[i];\n        }\n        ans\n    }\n\n    fn eat_blank_line(feeder: &mut Feeder, ans: &mut Job, core: &mut ShellCore) -> bool {\n        let num = feeder.scanner_blank(core);\n        ans.text += &feeder.consume(num);\n        let com_num = feeder.scanner_comment();\n        ans.text += &feeder.consume(com_num);\n        if feeder.starts_with(\"\\n\") {\n            ans.text += &feeder.consume(1);\n            true\n        } else {\n            false\n        }\n    }\n\n    fn eat_pipeline(\n        feeder: &mut Feeder,\n        ans: &mut Job,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(pipeline) = Pipeline::parse(feeder, core)? {\n            ans.text += &pipeline.text.clone();\n            ans.pipelines.push(pipeline);\n            return Ok(true);\n        }\n        Ok(false)\n    }\n\n    fn eat_and_or(feeder: &mut Feeder, ans: &mut Job, core: &mut ShellCore) -> bool {\n        let num = feeder.scanner_and_or(core);\n        let end = feeder.consume(num);\n        ans.pipeline_ends.push(end.clone());\n        ans.text += &end;\n        num != 0 //記号なしの場合にfalseが返る\n    }\n\n    pub fn read_heredoc(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        for pipeline in self.pipelines.iter_mut() {\n            pipeline.read_heredoc(feeder, core)?;\n        }\n        Ok(())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Job>, ParseError> {\n        let mut ans = Self::default();\n        while Self::eat_blank_line(feeder, &mut ans, core) {}\n        if !Self::eat_pipeline(feeder, &mut ans, core)? {\n            if ans.text.is_empty() {\n                return Ok(None);\n            } else {\n                return Ok(Some(ans));\n            }\n        }\n\n        while Self::eat_and_or(feeder, &mut ans, core) {\n            loop {\n                while Self::eat_blank_line(feeder, &mut ans, core) {}\n                if Self::eat_pipeline(feeder, &mut ans, core)? {\n                    break;\n                }\n                if feeder.is_empty() {\n                    feeder.feed_additional_line(core)?;\n                }\n            }\n        }\n\n        let com_num = feeder.scanner_comment();\n        ans.text += &feeder.consume(com_num);\n\n        match !ans.pipelines.is_empty() {\n            true => {\n                ans.read_heredoc(feeder, core)?;\n                Ok(Some(ans))\n            },\n            false => Ok(None),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/pipeline.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::command;\nuse super::command::Command;\nuse super::Pipe;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\nuse nix::sys::resource;\nuse nix::time;\nuse nix::time::ClockId;\nuse nix::unistd::Pid;\n\n#[derive(Debug, Clone, Default)]\npub struct Pipeline {\n    pub commands: Vec<Box<dyn Command>>,\n    pub pipes: Vec<Pipe>,\n    pub text: String,\n    exclamation: bool,\n    pub time: bool,\n}\n\nimpl Pipeline {\n    pub fn exec(\n        &mut self,\n        core: &mut ShellCore,\n        pgid: Pid,\n    ) -> (Vec<Option<Pid>>, bool, bool, Option<ExecError>) {\n        if self.commands.is_empty() {\n            // the case of only '!'\n            self.set_time(core);\n            return (vec![], self.exclamation, self.time, None);\n        }\n\n        let mut prev = -1;\n        let mut pids = vec![];\n        let mut pgid = pgid;\n\n        self.set_time(core);\n\n        for (i, p) in self.pipes.iter_mut().enumerate() {\n            p.set(prev, pgid, core);\n\n            match self.commands[i].exec(core, p) {\n                Ok(pid) => pids.push(pid),\n                Err(e) => return (pids, self.exclamation, self.time, Some(e)),\n            }\n\n            if i == 0 && pgid.as_raw() == 0 {\n                // 最初のexecが終わったら、pgidにコマンドのPIDを記録\n                pgid = pids[0].unwrap();\n            }\n            prev = p.recv;\n        }\n\n        let lastpipe = (!core.db.flags.contains('m')) && core.shopts.query(\"lastpipe\");\n        let mut lastp = Pipe::end(prev, pgid, lastpipe);\n        let result = self.commands[self.pipes.len()].exec(core, &mut lastp);\n        let mut err = None;\n        if lastpipe {\n            if let Err(e) = lastp.restore_lastpipe(core) {\n                err = Some(e);\n            }\n        }\n\n        match result {\n            Ok(pid) => pids.push(pid),\n            Err(e) => return (pids, self.exclamation, self.time, Some(e)),\n        }\n\n        (pids, self.exclamation, self.time, err)\n    }\n\n    /*\n    pub fn exec_coproc(\n        &mut self,\n        core: &mut ShellCore,\n        pgid: Pid,\n    ) -> (Vec<Option<Pid>>, bool, bool, Option<ExecError>) {\n        //let mut pids = vec![];\n\n        let mut prevp = Pipe::new(\"|\".to_string());\n        prevp.set(-1, pgid, core);\n        let mut lastp = Pipe::new(\"|\".to_string());\n        lastp.set(prevp.recv, pgid, core);\n        let result = com.exec(core, &mut lastp);\n\n        match result {\n            Ok(pid) => pids.push(pid),\n            Err(e) => return (pids, self.exclamation, self.time, Some(e)),\n        }\n\n        (pids, self.exclamation, self.time, None)\n    }*/\n\n\n    fn set_time(&mut self, core: &mut ShellCore) {\n        if !self.time {\n            return;\n        }\n\n        let self_usage = resource::getrusage(resource::UsageWho::RUSAGE_SELF).unwrap();\n        let children_usage = resource::getrusage(resource::UsageWho::RUSAGE_CHILDREN).unwrap();\n\n        core.measured_time.user = self_usage.user_time() + children_usage.user_time();\n        core.measured_time.sys = self_usage.system_time() + children_usage.system_time();\n        core.measured_time.real = time::clock_gettime(ClockId::CLOCK_MONOTONIC).unwrap();\n    }\n\n    pub fn read_heredoc(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        for command in self.commands.iter_mut() {\n            command.read_heredoc(feeder, core)?;\n        }\n        Ok(())\n    }\n\n    pub fn get_one_line_text(&self) -> String {\n        let mut ans = String::new();\n\n        if self.exclamation {\n            ans += \"! \";\n        }\n\n        for (i, c) in self.commands.iter().enumerate() {\n            ans += &c.get_one_line_text();\n            if i < self.pipes.len() {\n                ans += &self.pipes[i].text;\n            }\n        }\n        ans\n    }\n\n    fn eat_exclamation(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        match feeder.starts_with(\"!\") {\n            true => ans.text += &feeder.consume(1),\n            false => return false,\n        }\n\n        ans.exclamation = !ans.exclamation;\n        let blank_len = feeder.scanner_blank(core);\n        ans.text += &feeder.consume(blank_len);\n        true\n    }\n\n    fn eat_time(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        match feeder.starts_with(\"time \") || feeder.starts_with(\"time\\t\") {\n            true => ans.text += &feeder.consume(4),\n            false => return false,\n        }\n\n        ans.time = true;\n        let blank_len = feeder.scanner_blank(core);\n        ans.text += &feeder.consume(blank_len);\n        true\n    }\n\n    fn eat_command(\n        feeder: &mut Feeder,\n        ans: &mut Pipeline,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(command) = command::parse(feeder, core)? {\n            ans.text += &command.get_text();\n            ans.commands.push(command);\n\n            let blank_len = feeder.scanner_blank(core);\n            ans.text += &feeder.consume(blank_len);\n            return Ok(true);\n        }\n        Ok(false)\n    }\n\n    fn eat_pipe(feeder: &mut Feeder, ans: &mut Pipeline, core: &mut ShellCore) -> bool {\n        if let Some(p) = Pipe::parse(feeder, core) {\n            ans.text += &p.text.clone();\n            ans.pipes.push(p);\n            true\n        } else {\n            false\n        }\n    }\n\n    fn eat_blank_and_comment(feeder: &mut Feeder, ans: &mut Pipeline, core: &mut ShellCore) {\n        loop {\n            let blank_len = feeder.scanner_multiline_blank(core);\n            ans.text += &feeder.consume(blank_len); //空白、空行を削除\n            let comment_len = feeder.scanner_comment();\n            ans.text += &feeder.consume(comment_len); //コメントを削除\n            if blank_len + comment_len == 0 {\n                //空白、空行、コメントがなければ出る\n                break;\n            }\n        }\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<Option<Pipeline>, ParseError> {\n        let mut ans = Pipeline::default();\n\n        while Self::eat_exclamation(feeder, &mut ans, core)\n            || Self::eat_time(feeder, &mut ans, core)\n        {}\n\n        if !Self::eat_command(feeder, &mut ans, core)? {\n            match ans.exclamation || ans.time {\n                true => return Ok(Some(ans)),\n                false => return Ok(None),\n            }\n        }\n\n        while Self::eat_pipe(feeder, &mut ans, core) {\n            loop {\n                Self::eat_blank_and_comment(feeder, &mut ans, core);\n                if Self::eat_command(feeder, &mut ans, core)? {\n                    break;\n                }\n                if !feeder.is_empty() {\n                    return Ok(None);\n                }\n                feeder.feed_additional_line(core)?;\n            }\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/script.rs",
    "content": "//SPDX-FileCopyrightText: 2022-2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::job::Job;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\nenum Status {\n    UnexpectedSymbol(String),\n    NeedMoreLine,\n    NormalEnd,\n}\n\n#[derive(Debug, Clone, Default)]\npub struct Script {\n    pub jobs: Vec<Job>,\n    pub job_ends: Vec<String>,\n    pub text: String,\n}\n\nimpl Script {\n    pub fn exec(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        for (job, end) in self.jobs.iter_mut().zip(self.job_ends.iter()) {\n            job.exec(core, end == \"&\")?;\n        }\n\n        Ok(())\n    }\n\n    pub fn get_text(&self) -> String {\n        self.text.clone()\n    }\n\n    pub fn pretty_print(&mut self, indent_num: usize) {\n        let mut semicolon = false;\n        let mut printed = false;\n        let mut end_pos = self.jobs.len() - 1;\n\n        for job in self.jobs.iter_mut().rev() {\n            if job.pipelines.is_empty() {\n                end_pos -= 1;\n                break;\n            }\n        }\n\n        for (i, job) in self.jobs.iter_mut().enumerate() {\n            job.pretty_print(\n                indent_num,\n                &mut semicolon,\n                &mut printed,\n                &self.job_ends[i],\n                i == end_pos,\n            );\n        }\n        println!();\n    }\n\n    pub fn get_one_line_text(&self) -> String {\n        let mut ans = String::new();\n        for (i, j) in self.jobs.iter().enumerate() {\n            ans += &j.get_one_line_text();\n            ans += &self.job_ends[i];\n            ans += \" \";\n        }\n        ans.pop();\n        ans\n    }\n\n    fn eat_job(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        ans: &mut Script,\n    ) -> Result<bool, ParseError> {\n        if let Some(job) = Job::parse(feeder, core)? {\n            ans.text += &job.text.clone();\n            ans.jobs.push(job);\n            Ok(true)\n        } else {\n            Ok(false)\n        }\n    }\n\n    fn eat_job_end(feeder: &mut Feeder, ans: &mut Script) -> bool {\n        if feeder.starts_with(\";;\") || feeder.starts_with(\";&\") {\n            ans.job_ends.push(\"\".to_string());\n            return true;\n        }\n        let len = feeder.scanner_job_end();\n        let end = &feeder.consume(len);\n        ans.job_ends.push(end.clone());\n        ans.text += end;\n        len != 0\n    }\n\n    fn check_nest(&self, feeder: &mut Feeder, permit_empty: bool) -> Status {\n        let nest = feeder.nest.last().unwrap();\n\n        if nest.0.is_empty() && feeder.is_empty() {\n            return Status::NormalEnd;\n        }\n\n        match (\n            nest.1.iter().find(|e| feeder.starts_with(e)),\n            self.pipeline_num(),\n        ) {\n            (Some(end), 0) => {\n                if permit_empty {\n                    return Status::NormalEnd;\n                }\n                return Status::UnexpectedSymbol(end.to_string());\n            }\n            (Some(_), _) => return Status::NormalEnd,\n            (None, _) => {}\n        }\n\n        if !feeder.is_empty() {\n            let remaining = feeder.consume(feeder.len());\n            let first_token = remaining.split(\" \").nth(0).unwrap().to_string();\n            return Status::UnexpectedSymbol(first_token);\n        }\n\n        Status::NeedMoreLine\n    }\n\n    fn unalias(&mut self, core: &mut ShellCore) {\n        for a in core.alias_memo.iter().rev() {\n            self.text = self.text.replace(&a.1, &a.0);\n        }\n\n        core.alias_memo.clear();\n    }\n\n    fn pipeline_num(&self) -> usize {\n        self.jobs.iter().map(|j| j.pipelines.len()).sum()\n    }\n\n    /*\n    fn read_heredoc(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        for job in self.jobs.iter_mut() {\n            job.read_heredoc(feeder, core)?;\n        }\n        Ok(())\n    }*/\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        permit_empty: bool,\n    ) -> Result<Option<Script>, ParseError> {\n        let mut ans = Self::default();\n        loop {\n            while Self::eat_job(feeder, core, &mut ans)? && Self::eat_job_end(feeder, &mut ans) {}\n\n            match ans.check_nest(feeder, permit_empty) {\n                Status::NormalEnd => {\n                    ans.unalias(core);\n                    //ans.read_heredoc(feeder, core)?;\n                    return Ok(Some(ans));\n                }\n                Status::NeedMoreLine => {\n                    //ans.read_heredoc(feeder, core)?;\n                    feeder.feed_additional_line(core)?\n                }\n                Status::UnexpectedSymbol(s) => {\n                    //unexpected symbol\n                    let _ = core\n                        .db\n                        .set_param(\"LINENO\", &feeder.lineno.to_string(), None);\n                    core.db.exit_status = 2;\n                    return Err(ParseError::UnexpectedSymbol(s));\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/substitution/array.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::subscript::Subscript;\nuse crate::elements::command;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Array {\n    pub text: String,\n    pub words: Vec<(Option<Subscript>, bool, Word)>, //bool: true if append\n    error_strings: Vec<String>,\n}\n\nimpl Array {\n    pub fn eval(\n        &mut self,\n        core: &mut ShellCore,\n        as_int: bool,\n        as_assoc: bool,\n    ) -> Result<Vec<(Option<Subscript>, bool, String)>, ExecError> {\n        if let Some(c) = self.error_strings.last() {\n            return Err(ExecError::SyntaxError(c.to_string()));\n        }\n\n        let mut ans = vec![];\n\n        if as_int {\n            for (s, append, w) in &mut self.words {\n                ans.push((s.clone(), *append, w.eval_as_integer(core)?));\n            }\n        } else {\n            for (s, append, w) in &mut self.words {\n                if as_assoc {\n                    ans.push((s.clone(), *append, w.eval_as_value(core)?));\n                } else {\n                    for e in w.eval(core)? {\n                        ans.push((s.clone(), *append, e));\n                    }\n                }\n            }\n        }\n        Ok(ans)\n    }\n\n    fn eat_word(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        sub: Option<Subscript>,\n        core: &mut ShellCore,\n        append: bool,\n    ) -> bool {\n        if feeder.starts_with(\")\") {\n            return false;\n        }\n\n        let w = match Word::parse(feeder, core, None) {\n            Ok(Some(w)) => w,\n            _ => return false,\n        };\n        ans.text += &w.text;\n        ans.words.push((sub, append, w));\n        true\n    }\n\n    fn eat_subscript(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        ans: &mut Self,\n    ) -> Result<Option<Subscript>, ParseError> {\n        if let Some(s) = Subscript::parse(feeder, core)? {\n            if feeder.starts_with(\"=\") || feeder.starts_with(\"+=\") {\n                ans.text += &s.text.clone();\n                return Ok(Some(s));\n            } else {\n                feeder.replace(0, &s.text);\n            }\n        }\n        Ok(None)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Array>, ParseError> {\n        if !feeder.starts_with(\"(\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            text: feeder.consume(1),\n            ..Default::default()\n        };\n        let mut paren_counter = 1;\n        loop {\n            let mut append = false;\n            command::eat_blank_lines(feeder, core, &mut ans.text)?;\n\n            let sub = Self::eat_subscript(feeder, core, &mut ans)?;\n\n            if sub.is_some() {\n                if feeder.starts_with(\"=\") {\n                    ans.text += &feeder.consume(1);\n                } else if feeder.starts_with(\"+=\") {\n                    append = true;\n                    ans.text += &feeder.consume(2);\n                }\n            }\n\n            if Self::eat_word(feeder, &mut ans, sub, core, append) {\n                continue;\n            }\n\n            if !feeder.is_empty() {\n                if feeder.starts_with(\")\") {\n                    paren_counter -= 1;\n                    if paren_counter == 0 {\n                        ans.text += &feeder.consume(1);\n                        break;\n                    }\n                } else if feeder.starts_with(\"\\n\") {\n                    ans.text += &feeder.consume(1);\n                }\n\n                let len = feeder.scanner_char();\n                let err_char = feeder.consume(len);\n                if &err_char == \"(\" {\n                    paren_counter += 1;\n                }\n                ans.text += &err_char.clone();\n                ans.error_strings.push(err_char);\n                continue;\n            } else if feeder.feed_additional_line(core).is_err() {\n                return Ok(None);\n            }\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/substitution/subscript.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Subscript {\n    pub text: String,\n    data: SubscriptType,\n}\n\n#[derive(Debug, Clone, Default)]\nenum SubscriptType {\n    #[default]\n    None,\n    Arith(ArithmeticExpr),\n    //Evaluated(String),\n    Array(String),\n}\n\nimpl Subscript {\n    pub fn eval(&mut self, core: &mut ShellCore, param_name: &str) -> Result<String, ExecError> {\n        if let SubscriptType::Array(a) = &self.data {\n            return Ok(a.clone());\n        }\n\n        if let SubscriptType::Arith(mut a) = self.data.clone() {\n            if a.text.is_empty() {\n                return Err(ExecError::ArrayIndexInvalid(a.text.clone()));\n            }\n            if !core.db.is_assoc(param_name) {\n                return a.eval(core);\n            }\n\n            if core.valid_assoc_expand_once && core.shopts.query(\"assoc_expand_once\") {\n                return Ok(a.text.clone());\n            }\n\n            let mut f = Feeder::new(&a.text);\n            if let Some(w) = Word::parse(&mut f, core, Some(WordMode::AssocIndex))? {\n                return w.eval_as_assoc_index(core);\n            } else {\n                return Ok(a.text.clone());\n            }\n        }\n\n        Err(ExecError::ArrayIndexInvalid(\"\".to_string()))\n    }\n\n    pub fn reparse(&mut self, core: &mut ShellCore, param_name: &str) -> Result<(), ExecError> {\n        if let SubscriptType::Array(_) = &self.data {\n            return Ok(());\n        }\n\n        let mut text = self.eval(core, param_name)?;\n        text.insert(0, '[');\n        text.push(']');\n\n        let mut f = Feeder::new(&text);\n        match Self::parse(&mut f, core) {\n            Ok(Some(s)) => *self = s,\n            _ => return Err(ExecError::InvalidName(text.clone())),\n        }\n\n        Ok(())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"[\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self::default();\n        ans.text += &feeder.consume(1);\n\n        if feeder.starts_withs(&[\"@\", \"*\"]) {\n            let s = feeder.consume(1);\n            ans.text += &s.clone();\n            ans.data = SubscriptType::Array(s);\n        } else if let Some(a) = ArithmeticExpr::parse(feeder, core, true, \"[\")? {\n            ans.text += &a.text.clone();\n            ans.data = SubscriptType::Arith(a);\n        }\n\n        if !feeder.starts_with(\"]\") {\n            return Ok(None);\n        }\n\n        ans.text += &feeder.consume(1);\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/substitution/value.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::array::Array;\nuse crate::core::database::data::Data;\nuse crate::elements::word::Word;\nuse crate::elements::word::WordMode;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub enum ParsedDataType {\n    #[default]\n    None,\n    Single(Word),\n    Array(Array),\n    Obj(Box::<dyn Data>),\n}\n\n#[derive(Debug, Clone, Default)]\npub struct Value {\n    pub text: String,\n    pub value: ParsedDataType,\n    pub evaluated_string: Option<String>,\n    pub evaluated_array: Option<Vec<(String, bool, String)>>, //bool: true if append\n}\n\nimpl From<Box::<dyn Data>> for Value {\n    fn from(mut d: Box::<dyn Data>) -> Self {\n        Self {\n            text: (*d.get_fmt_string()).to_string(),\n            value: ParsedDataType::Obj(d),\n            ..Default::default()\n        }\n    }\n}\n\nimpl Value {\n    pub fn eval(\n        &mut self,\n        core: &mut ShellCore,\n        name: &str,\n        append: bool,\n    ) -> Result<(), ExecError> {\n        match self.value.clone() {\n            ParsedDataType::Single(v) => self.eval_as_value(&v, core, name),\n            ParsedDataType::Array(mut a) => self.eval_as_array(&mut a, core, name, append),\n            ParsedDataType::Obj(_) => {Ok(())},\n            ParsedDataType::None => {\n                self.evaluated_string = Some(\"\".to_string());\n                Ok(())\n            }\n        }\n    }\n\n    pub fn is_obj(&self) -> bool {\n        match self.value {\n            ParsedDataType::Obj(_) => true,\n            _ => false,\n        }\n    }\n\n    fn eval_as_value(\n        &mut self,\n        w: &Word,\n        core: &mut ShellCore,\n        name: &str,\n    ) -> Result<(), ExecError> {\n        self.evaluated_string = match core.db.has_flag(name, 'i') {\n            true => Some(w.eval_as_integer(core)?),\n            false => Some(w.eval_as_value(core)?),\n        };\n\n        Ok(())\n    }\n\n    fn eval_as_array(\n        &mut self,\n        a: &mut Array,\n        core: &mut ShellCore,\n        name: &str,\n        append: bool,\n    ) -> Result<(), ExecError> {\n        let mut i = match append {\n            false => 0,\n            true => core.db.index_based_len(name) as isize,\n        };\n\n        let mut hash = vec![];\n        let mut vec_assoc = vec![];\n        let i_flag = core.db.has_flag(name, 'i');\n        let mut first = true;\n        let mut assoc_no_index_mode = false;\n        let assoc = core.db.is_assoc(name);\n\n        for (pos, (s, append, v)) in a.eval(core, i_flag, assoc)?.into_iter().enumerate() {\n            if assoc_no_index_mode {\n                vec_assoc.push((v, append));\n                continue;\n            }\n\n            if s.is_none() {\n                if first && assoc {\n                    assoc_no_index_mode = true;\n                    vec_assoc.push((v, append));\n                } else if assoc {\n                    let msg = format!(\n                        \"{}: {}: must use subscript when assigning associative array\",\n                        &name, &a.words[pos].2.text\n                    );\n                    ExecError::Other(msg).print(core);\n                } else {\n                    hash.push((i.to_string(), append, v));\n                }\n                i += 1;\n                first = false;\n                continue;\n            }\n\n            first = false;\n            let index = match s.unwrap().eval(core, name) {\n                Ok(i) => i,\n                Err(ExecError::ArithError(a, b)) => {\n                    self.evaluated_array = Some(vec![]);\n                    ExecError::ArithError(a, b).print(core);\n                    return Ok(());\n                }\n                Err(e) => {\n                    e.print(core);\n                    continue;\n                }\n            };\n\n            if assoc {\n                hash.push((index, append, v));\n            } else {\n                match index.parse::<isize>() {\n                    Ok(j) => i = j,\n                    Err(e) => {\n                        eprintln!(\"{:?}\", &e);\n                        continue;\n                    }\n                }\n                hash.push((index, append, v));\n            }\n            i += 1;\n        }\n\n        if assoc_no_index_mode {\n            let mut key = String::new();\n            for (i, (d, append)) in vec_assoc.iter().enumerate() {\n                match i % 2 {\n                    0 => key = d.clone(),\n                    _ => hash.push((key.clone(), *append, d.clone())),\n                }\n            }\n\n            if vec_assoc.len() % 2 == 1 {\n                hash.push((key.clone(), append, \"\".to_string()));\n            }\n        }\n\n        self.evaluated_array = Some(hash);\n        Ok(())\n    }\n\n    fn reparse_word(&mut self, w: &mut Word, core: &mut ShellCore) -> Result<(), ExecError> {\n        let text = w.eval_as_value(core)?;\n        let mut f = Feeder::new(&text.replace(\"~\", \"\\\\~\"));\n        if let Ok(Some(s)) = Self::parse(&mut f, core, true) {\n            if !f.is_empty() {\n                return Err(ExecError::InvalidName(text));\n            }\n\n            *self = s;\n        }\n        Ok(())\n    }\n\n    pub fn reparse(&mut self, core: &mut ShellCore, quoted: bool) -> Result<(), ExecError> {\n        let v = self.value.clone();\n\n        match v {\n            ParsedDataType::Single(mut w) => self.reparse_word(&mut w, core),\n            ParsedDataType::Array(a) => {\n                if !quoted {\n                    return Ok(());\n                }\n                let txt = \"'\".to_owned() + &a.text + \"'\";\n                let mut w = Word::from(txt.as_str());\n                self.reparse_word(&mut w, core)\n            }\n            _ => Ok(()),\n        }\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        permit_space: bool,\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n\n        let wm = match permit_space {\n            true => WordMode::PermitAnyChar,\n            false => WordMode::Value,\n        };\n\n        if let Some(a) = Array::parse(feeder, core)? {\n            ans.text += &a.text;\n            ans.value = ParsedDataType::Array(a);\n        } else if let Ok(Some(mut w)) = Word::parse(feeder, core, Some(wm)) {\n            w.mode = Some(WordMode::RightOfSubstitution);\n            ans.text += &w.text;\n            ans.value = ParsedDataType::Single(w);\n        }\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/substitution/variable.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::database::data::uninit::Uninit;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::arg;\nuse crate::{Feeder, ShellCore};\nuse super::subscript::Subscript;\n\n#[derive(Debug, Clone, Default)]\npub struct Variable {\n    pub text: String,\n    pub name: String,\n    pub index: Option<Subscript>,\n    pub lineno: usize,\n}\n\nimpl Variable {\n    pub fn check_nameref(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let nameref = match core.db.get_nameref(&self.name)? {\n            Some(nref) => nref,\n            None => return Ok(()),\n        };\n\n        if ! nameref.contains('[') {\n            self.name = nameref;\n            return Ok(());\n        }else {\n            let mut f = Feeder::new(&nameref);\n            if let Some(v) = Self::parse(&mut f, core)? {\n                self.name = v.name;\n                self.index = v.index;\n            }\n        }\n\n        Ok(())\n    }\n\n    pub fn get_index(\n        &mut self,\n        core: &mut ShellCore,\n        right_is_array: bool,\n        append: bool,\n    ) -> Result<Option<String>, ExecError> {\n        if let Some(mut s) = self.index.clone() {\n            if s.text == \"[]\" {\n                return Err(ExecError::ArrayIndexInvalid(\"\".to_string()));\n            }\n            if s.text.chars().all(|c| \" \\n\\t[]\".contains(c)) {\n                if core.db.is_assoc(&self.name) {\n                    let mut index = s.text.clone();\n                    index.remove(0);\n                    index.pop();\n                    return Ok(Some(index));\n                }\n                return Ok(Some(\"0\".to_string()));\n            }\n            let index = s.eval(core, &self.name)?;\n            return Ok(Some(index));\n        }\n\n        if core.db.is_array(&self.name) && !append && !right_is_array {\n            Ok(Some(\"0\".to_string()))\n        } else {\n            Ok(None)\n        }\n    }\n\n    pub fn is_array(&mut self) -> bool {\n        self.is_pos_param_array() || self.is_var_array()\n    }\n\n    pub fn is_pos_param_array(&mut self) -> bool {\n        self.name == \"@\" || self.name == \"*\"\n    }\n\n    pub fn is_var_array(&mut self) -> bool {\n        if self.index.is_none() {\n            return false;\n        }\n        let sub = &self.index.as_ref().unwrap().text;\n        sub == \"[*]\" || sub == \"[@]\"\n    }\n\n    /*\n    pub fn get_value(&mut self, core: &mut ShellCore) -> Result<String, ExecError> {\n        if self.index.is_none() {\n            return core.db.get_param(&self.name);\n        }else{\n            return core.db.get_param(&self.name);\n        }\n    }*/\n\n    pub fn set_value(&mut self, value: &str, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.index.is_none() {\n            return core.db.set_param(&self.name, value, None);\n        }\n\n        let index = self.index.clone().unwrap().eval(core, &self.name)?;\n        core.db.set_param2(&self.name, &index, value, None)\n    }\n\n    pub fn parse_and_set(arg: &str, value: &str, core: &mut ShellCore) -> Result<(), ExecError> {\n        let mut f = Feeder::new(arg);\n        match Self::parse(&mut f, core)? {\n            Some(mut v) => {\n                if !f.is_empty() {\n                    return Err(ExecError::InvalidName(arg.to_string()));\n                }\n                v.set_value(value, core)\n            }\n            None => Err(ExecError::InvalidName(arg.to_string())),\n        }\n    }\n\n    pub fn init_variable(\n        &self,\n        core: &mut ShellCore,\n        scope: Option<usize>,\n        args: &mut Vec<String>,\n    ) -> Result<(), ExecError> {\n        let mut prev = vec![];\n\n        let exists_in_scope = if let Some(l) = scope {\n            core.db.exist_l(&self.name, l)\n        } else {\n            false\n        };\n        if (scope.is_none() && core.db.exist(&self.name)) || exists_in_scope {\n            prev = vec![core.db.get_param(&self.name)?];\n        }\n\n        let i_opt = arg::consume_arg(\"-i\", args);\n        let a_opt = arg::consume_arg(\"-a\", args);\n        let la_opt = arg::consume_arg(\"-A\", args);\n\n        if a_opt || (!la_opt && self.index.is_some()) {\n            let data = match prev.is_empty() {\n                true  => None,\n                false => Some(prev),\n            };\n            return core.db.init_array(&self.name, data, scope, i_opt);\n            //TODO: ^ Maybe, there is a case where an assoc must be\n            //prepared.\n        } else if la_opt {\n            core.db.init_assoc(&self.name, scope, false, i_opt)?;\n            if !prev.is_empty() {\n                core.db.set_assoc_elem(&self.name, \"0\", &prev[0], scope)?;\n            }\n            return Ok(());\n        }\n\n        match prev.len() {\n            0 => {\n                match i_opt {\n                    true =>  core.db.init_as_num(&self.name, \"\", scope),\n                    false => {\n                        let mut opts = String::new();\n                        if a_opt {\n                            opts.push('a');\n                        }\n                        if la_opt {\n                            opts.push('A');\n                        }\n                        let d = Box::new(Uninit::new(&opts));\n                        core.db.set_entry(scope.unwrap_or(0), &self.name, d)\n                    },\n                }\n            },\n            _ => {\n                match i_opt {\n                    true => core.db.init_as_num(&self.name, &prev[0], scope),\n                    false => core.db.set_param(&self.name, &prev[0], scope),\n                }\n            },\n        }\n    }\n\n    pub fn exist(&self, core: &mut ShellCore) -> Result<bool, ExecError> {\n        //used in value_check.rs\n        if core.db.is_array(&self.name) || core.db.is_assoc(&self.name) {\n            if core.db.get_vec(&self.name, false)?.is_empty() {\n                return Ok(false);\n            }\n\n            if self.index.is_none() {\n                return core.db.has_key(&self.name, \"0\");\n            }\n        }\n\n        if let Some(sub) = self.index.clone().as_mut() {\n            let index = sub.eval(core, &self.name)?;\n            return core.db.has_key(&self.name, &index);\n        }\n\n        Ok(core.db.exist(&self.name))\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let len = feeder.scanner_name(core);\n        if len == 0 {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            lineno: feeder.lineno,\n            ..Default::default()\n        };\n\n        let name = feeder.consume(len);\n        ans.name = name.clone();\n        ans.text += &name;\n\n        if let Some(s) = Subscript::parse(feeder, core)? {\n            ans.text += &s.text.clone();\n            ans.index = Some(s);\n        };\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/substitution.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod array;\npub mod subscript;\npub mod value;\npub mod variable;\n\nuse self::value::Value;\nuse self::variable::Variable;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Substitution {\n    pub text: String,\n    pub left_hand: Variable,\n    pub right_hand: Option<Value>,\n    append: bool,\n    lineno: usize,\n    pub quoted: bool,\n    pub reset_nameref: bool,\n}\n\nimpl Substitution {\n    pub fn eval(\n        &mut self,\n        core: &mut ShellCore,\n        scope: Option<usize>,\n        declare: bool,\n    ) -> Result<(), ExecError> {\n        core.db.set_param(\"LINENO\", &self.lineno.to_string(), None)?;\n        if self.right_hand.is_none() {\n            return Ok(());\n        }\n\n        if core.db.exist_nameref(&self.left_hand.name) && ! self.reset_nameref {\n            let mut circular_check_vec = vec![];\n            let org_name = self.left_hand.name.clone();\n            loop {\n                self.left_hand.check_nameref(core)?;\n\n                if circular_check_vec.is_empty() && org_name == self.left_hand.name {\n                    break;\n                }\n\n                if circular_check_vec.contains(&self.left_hand.name) {\n                    return Err(ExecError::CircularNameRef(org_name));\n                }\n                if ! core.db.exist_nameref(&self.left_hand.name) {\n                    break;\n                }\n                circular_check_vec.push(self.left_hand.name.clone());\n            }\n        }\n\n        let r = self.right_hand.as_mut().unwrap();\n        r.eval(core, &self.left_hand.name, self.append)?;\n\n        if r.is_obj() {\n            return Ok(());\n        }\n\n        if declare && r.evaluated_array.is_some() {\n            self.left_hand.index = None;\n        }\n        self.set_to_shell(core, scope)\n    }\n\n    pub fn reparse(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.left_hand.index.is_some() {\n            self.left_hand\n                .index\n                .as_mut()\n                .unwrap()\n                .reparse(core, &self.left_hand.name)?;\n        }\n\n        if let Some(r) = self.right_hand.as_mut() {\n            r.reparse(core, self.quoted)?;\n        }\n        Ok(())\n    }\n\n    pub fn localvar_inherit(&mut self, core: &mut ShellCore) {\n        if self.right_hand.is_some() {\n            return;\n        }\n\n        if let Some(d) = core.db.get_ref(&self.left_hand.name) {\n            self.right_hand = Some(Value::from(d.clone()));\n        }\n    }\n\n    fn restore_flag(core: &mut ShellCore, name: &str,\n                    old_flags: &str, scope: usize) {\n        for flag in old_flags.chars() {\n            if flag == 'A' || flag == 'a' {\n                continue;\n            }\n            if old_flags.contains(flag) {\n                core.db.set_flag(&name, flag, scope);\n            }\n        }\n    }\n\n    fn set_whole_array(&mut self, core: &mut ShellCore, scope: usize) -> Result<(), ExecError> {\n        let r = self.right_hand.as_mut().unwrap();\n        if r.evaluated_array.is_none() {\n            return Err(ExecError::Other(\"no array and no index\".to_string()));\n        }\n\n        let a = r.evaluated_array.as_ref().unwrap();\n        let name = &self.left_hand.name;\n        let old_flags = core.db.get_flags(&name).to_string();\n\n        if a.is_empty() && !self.append {\n            if core.db.is_assoc(name) {\n                core.db.init_assoc(name, Some(scope), true, false)?;\n            } else {\n                core.db\n                    .init_array(name, Some(vec![]), Some(scope), false)?;\n            }\n\n            Self::restore_flag(core, name, &old_flags, scope);\n            return Ok(());\n        } else if !self.append {\n            core.db.init(name, scope);\n            Self::restore_flag(core, name, &old_flags, scope);\n        }\n\n        for e in a {\n            match e.1 {\n                //true if append\n                false => core.db\n                    .set_param2(name, &e.0, &e.2, Some(scope))?,\n                true => core.db\n                    .append_param2(name, &e.0, &e.2, Some(scope))?,\n            }\n        }\n        Ok(())\n    }\n\n    fn set_array_elem(\n        &mut self,\n        core: &mut ShellCore,\n        scope: usize,\n        index: &str,\n    ) -> Result<(), ExecError> {\n        if index.is_empty() {\n            return Err(ExecError::ArrayIndexInvalid(self.left_hand.text.clone()));\n        }\n\n        let r = self.right_hand.as_mut().unwrap();\n        if let Some(v) = &r.evaluated_string {\n            if self.append {\n                return core\n                    .db\n                    .append_param2(&self.left_hand.name, index, v, Some(scope));\n            } else {\n                return core\n                    .db\n                    .set_param2(&self.left_hand.name, index, v, Some(scope));\n            }\n        }\n\n        let msg = format!(\n            \"{}: cannot assign list to array member\",\n            &self.left_hand.text\n        );\n        Err(ExecError::Other(msg))\n    }\n\n    fn init_array(&mut self, core: &mut ShellCore, scope: usize) -> Result<(), ExecError> {\n        let rhs_is_array = match self.right_hand.as_mut() {\n            Some(r) => r.evaluated_array.is_some(),\n            None => false,\n        };\n\n        match self.left_hand.get_index(core, rhs_is_array, self.append)? {\n            Some(index) => self.set_array_elem(core, scope, &index),\n            None => self.set_whole_array(core, scope),\n        }\n    }\n\n    fn set_single(&mut self, core: &mut ShellCore, scope: usize) -> Result<(), ExecError> {\n        let data = match self.right_hand.as_mut() {\n            Some(r) => r.evaluated_string.clone().unwrap(),\n            None => String::new(),\n        };\n\n        if self.append {\n            core.db\n                .append_param(&self.left_hand.name, &data, Some(scope))\n        } else if self.reset_nameref {\n            core.db.set_nameref(&self.left_hand.name, &data, Some(scope))\n        } else {\n            core.db.set_param(&self.left_hand.name, &data, Some(scope))\n        }\n    }\n\n    fn set_to_shell(\n        &mut self,\n        core: &mut ShellCore,\n        scope: Option<usize>,\n    ) -> Result<(), ExecError> {\n        let scope = core.db.get_target_scope(&self.left_hand.name, scope);\n        let r = self.right_hand.as_mut().unwrap();\n\n        if r.evaluated_string.is_some()\n        && self.left_hand.index.is_none() {\n            self.set_single(core, scope)\n        } else {\n            self.init_array(core, scope)\n        }\n    }\n\n    fn eat_equal(&mut self, feeder: &mut Feeder) -> bool {\n        if feeder.starts_with(\"+=\") {\n            self.append = true;\n            self.text += &feeder.consume(2);\n        } else if feeder.starts_with(\"=\") {\n            self.text += &feeder.consume(1);\n        } else {\n            return false;\n        }\n\n        true\n    }\n\n    fn eat_left_hand(\n        &mut self,\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        self.left_hand = match Variable::parse(feeder, core)? {\n            Some(a) => a,\n            None => return Ok(false),\n        };\n        self.text = self.left_hand.text.clone();\n        self.lineno = self.left_hand.lineno;\n        Ok(true)\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        permit_space: bool,\n        permit_no_equal: bool,\n    ) -> Result<Option<Self>, ParseError> {\n        let mut ans = Self::default();\n\n        feeder.set_backup();\n        match ans.eat_left_hand(feeder, core) {\n            Ok(true) => {}\n            Ok(false) => {\n                feeder.rewind();\n                return Ok(None);\n            }\n            Err(e) => {\n                feeder.rewind();\n                return Err(e);\n            }\n        }\n\n        if !ans.eat_equal(feeder) {\n            if permit_no_equal {\n                feeder.pop_backup();\n                return Ok(Some(ans));\n            }\n            feeder.rewind();\n            return Ok(None);\n        }\n        feeder.pop_backup();\n\n        if let Some(a) = Value::parse(feeder, core, permit_space)? {\n            ans.text += &a.text.clone();\n            ans.right_hand = Some(a);\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/ansi_c_quoted.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Subword;\nuse crate::elements::ansi_c_str::{AnsiCString, AnsiCToken};\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct AnsiCQuoted {\n    text: String,\n    tokens: Vec<AnsiCToken>,\n    in_heredoc: bool,\n}\n\nimpl Subword for AnsiCQuoted {\n    fn get_text(&self) -> &str {\n        &self.text\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn make_unquoted_string(&mut self) -> Option<String> {\n        Some(self.make_glob_string())\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        if self.in_heredoc {\n            return self.text.clone();\n        }\n\n        let mut ans = String::new();\n        for t in &mut self.tokens {\n            if let AnsiCToken::EmptyHex = t {\n                break;\n            }\n            ans += &t.render();\n        }\n\n        ans\n    }\n\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n\n    fn set_heredoc_flag(&mut self) {\n        self.in_heredoc = true;\n    }\n}\n\nimpl AnsiCQuoted {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"$'\") {\n            return Ok(None);\n        }\n        let mut ans = Self::default();\n        ans.text += &feeder.consume(2);\n\n        if let Some(ansi_c_str) = AnsiCString::parse(feeder, core, false)? {\n            ans.text += &ansi_c_str.text;\n            ans.tokens = ansi_c_str.tokens;\n        }\n\n        ans.text += &feeder.consume(1);\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/arithmetic.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::command::arithmetic::ArithmeticCommand;\nuse crate::elements::subword::Subword;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone)]\npub struct Arithmetic {\n    pub text: String,\n    com: ArithmeticCommand,\n}\n\nimpl Subword for Arithmetic {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.text = self.com.eval(core)?;\n        Ok(())\n    }\n}\n\nimpl Arithmetic {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"$((\") {\n            return Ok(None);\n        }\n        feeder.set_backup();\n        let dl = feeder.consume(1);\n\n        if let Some(a) = ArithmeticCommand::parse(feeder, core)? {\n            feeder.pop_backup();\n            return Ok(Some(Arithmetic {\n                text: dl + &a.text.clone(),\n                com: a,\n            }));\n        }\n        feeder.rewind();\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/case_conv.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::Variable;\nuse super::OptionalOperation;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::glob;\nuse crate::utils::glob::GlobElem;\nuse crate::{Feeder, ShellCore};\n\nimpl OptionalOperation for CaseConv {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn exec(\n        &mut self,\n        _: &Variable,\n        text: &str,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        self.get_text(text, core)\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct CaseConv {\n    pub text: String,\n    pub pattern: Option<Word>,\n    pub replace_symbol: String,\n}\n\nimpl CaseConv {\n    fn to_string(&self, w: &Option<Word>, core: &mut ShellCore) -> Result<String, ExecError> {\n        if let Some(w) = &w {\n            match w.eval_for_case_word(core) {\n                Some(s) => return Ok(s),\n                None => match w.subwords.len() {\n                    0 => return Ok(\"\".to_string()),\n                    _ => return Err(ExecError::Other(\"parse error\".to_string())),\n                },\n            }\n        }\n\n        Ok(\"\".to_string())\n    }\n\n    fn get_match_length(&self, text: &str, pattern: &[GlobElem], ch: char) -> usize {\n        if pattern.is_empty() {\n            return ch.len_utf8();\n        }\n        glob::longest_match_length(text, pattern)\n    }\n\n    fn conv(&self, ch: char) -> String {\n        if ch.is_ascii_lowercase()\n            && (self.replace_symbol.starts_with(\"^\") || self.replace_symbol.starts_with(\"~\"))\n        {\n            return ch.to_string().to_uppercase();\n        }\n\n        if ch.is_ascii_uppercase()\n            && (self.replace_symbol.starts_with(\",\") || self.replace_symbol.starts_with(\"~\"))\n        {\n            return ch.to_string().to_lowercase();\n        }\n\n        ch.to_string()\n    }\n\n    pub fn get_text(&self, text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let tmp = self.to_string(&self.pattern, core)?;\n        let extglob = core.shopts.query(\"extglob\");\n        let pattern = glob::parse(&tmp, extglob);\n\n        let mut start = 0;\n        let mut ans = String::new();\n        let mut skip = 0;\n        for ch in text.chars() {\n            if skip > 0 {\n                skip -= 1;\n                start += ch.len_utf8();\n                continue;\n            }\n\n            let len = self.get_match_length(&text[start..], &pattern, ch);\n            if len == 0 {\n                ans += &ch.to_string();\n                start += ch.len_utf8();\n                continue;\n            }\n\n            let new_ch = self.conv(ch);\n            ans += &new_ch;\n            if self.replace_symbol.len() != 2 {\n                return Ok(ans + &text[start + len..]);\n            }\n\n            start += ch.len_utf8();\n        }\n        Ok(ans)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"^\") && !feeder.starts_with(\",\") && !feeder.starts_with(\"~\") {\n            return Ok(None);\n        }\n\n        let mut ans = CaseConv::default();\n\n        if feeder.starts_with(\"^^\") || feeder.starts_with(\",,\") || feeder.starts_with(\"~~\") {\n            ans.replace_symbol = feeder.consume(2);\n            ans.text += &ans.replace_symbol;\n        } else if feeder.starts_with(\"^\") || feeder.starts_with(\",\") || feeder.starts_with(\"~\") {\n            ans.replace_symbol = feeder.consume(1);\n            ans.text += &ans.replace_symbol;\n        }\n\n        if let Some(w) = Word::parse(\n            feeder,\n            core,\n            Some(WordMode::ParamOption(vec![\"}\".to_string()])),\n            //Some(WordMode::AlterWord),\n        )? {\n            ans.text += &w.text.clone();\n            ans.pattern = Some(w);\n        } else {\n            ans.pattern = Some(Word::default());\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/escape.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::Variable;\nuse super::OptionalOperation;\nuse crate::error::exec::ExecError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Escape {\n    pub text: String,\n    pub symbol: String,\n}\n\nimpl OptionalOperation for Escape {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn exec(&mut self, _: &Variable, text: &str, _: &mut ShellCore) -> Result<String, ExecError> {\n        self.replace_single_data(text)\n    }\n\n    fn init_array(\n        &mut self,\n        param: &Variable,\n        array: &mut Vec<String>,\n        _: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        if param.name == \"@\" || param.name == \"*\" {\n            *array = core.db.get_position_params();\n            for elem in array.iter_mut() {\n                *elem = self.replace_single_data(elem)?;\n            }\n            return Ok(());\n        }\n\n        if core.db.is_assoc(&param.name) {\n            array.clear();\n            for key in core.db.get_indexes_all(&param.name) {\n                let value = core.db.get_elem(&param.name, &key).unwrap_or_default();\n                array.push(self.replace_array_elem(&key, &value)?);\n            }\n            return Ok(());\n        }\n\n        *array = core.db.get_vec(&param.name, true)?;\n        for (i, elem) in array.iter_mut().enumerate() {\n            *elem = self.replace_array_elem(&i.to_string(), elem)?;\n        }\n\n        Ok(())\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n    fn has_array_replace(&self) -> bool {\n        true\n    }\n}\n\nimpl Escape {\n    pub fn replace_single_data(&self, text: &str) -> Result<String, ExecError> {\n        match self.symbol.as_ref() {\n            \"k\" | \"K\" | \"Q\" => {\n                let text = format!(\"'{text}'\");\n                return Ok(text);\n            }\n            _ => {}\n        }\n        Ok(text.to_string())\n    }\n\n    pub fn replace_array_elem(&self, pos: &str, text: &str) -> Result<String, ExecError> {\n        match self.symbol.as_ref() {\n            \"k\" => {\n                let text = format!(\"{pos} {text}\");\n                return Ok(text);\n            }\n            \"K\" => {\n                let text = format!(\"{pos} \\\"{text}\\\"\");\n                return Ok(text);\n            }\n            \"Q\" => {\n                let text = format!(\"'{text}'\");\n                return Ok(text);\n            }\n            _ => {}\n        }\n        Ok(text.to_string())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Self> {\n        if !feeder.starts_with(\"@\") {\n            return None;\n        }\n\n        let mut ans = Escape::default();\n        if feeder.scanner_escape_directive_in_braced_param(core) == 2 {\n            ans.text = feeder.consume(1);\n            ans.symbol = feeder.consume(1);\n            ans.text += &ans.symbol;\n        } else {\n            return None;\n        }\n\n        Some(ans)\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/remove.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::Variable;\nuse super::OptionalOperation;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::glob;\nuse crate::{Feeder, ShellCore};\n\nimpl OptionalOperation for Remove {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn exec(\n        &mut self,\n        _: &Variable,\n        text: &str,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        self.set(text, core)\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n\n    fn init_array(\n        &mut self,\n        param: &Variable,\n        array: &mut Vec<String>,\n        text: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        *array = match param.name.as_str() {\n            \"@\" | \"*\" => core.db.get_position_params(),\n            _ => core.db.get_vec(&param.name, true)?,\n        };\n\n        for item in array.iter_mut() {\n            *item = self.set(item, core)?;\n        }\n\n        *text = array.join(\" \");\n        Ok(())\n    }\n\n    fn has_array_replace(&self) -> bool {\n        true\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct Remove {\n    pub text: String,\n    pub remove_symbol: String,\n    pub remove_pattern: Option<Word>,\n}\n\nimpl Remove {\n    pub fn set(&mut self, text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let mut text = text.to_string();\n        let pattern = self\n            .remove_pattern\n            .as_mut()\n            .unwrap()\n            .eval_for_case_word(core)\n            .ok_or(ExecError::Other(\"evaluation error\".to_string()))?;\n        let extglob = core.shopts.query(\"extglob\");\n\n        if self.remove_symbol.starts_with(\"##\") {\n            let pat = glob::parse(&pattern, extglob);\n            let len = glob::longest_match_length(&text, &pat);\n            text = text[len..].to_string();\n        } else if self.remove_symbol.starts_with(\"#\") {\n            let pat = glob::parse(&pattern, extglob);\n            let len = glob::shortest_match_length(&text, &pat);\n            text = text[len..].to_string();\n        } else if self.remove_symbol.starts_with(\"%\") {\n            self.percent(&mut text, &pattern, extglob);\n        } else {\n            return Err(ExecError::Other(\"unknown symbol\".to_string()));\n        }\n\n        Ok(text)\n    }\n\n    pub fn percent(&self, text: &mut String, pattern: &str, extglob: bool) {\n        let mut length = text.len();\n        let mut ans_length = length;\n\n        for ch in text.chars().rev() {\n            length -= ch.len_utf8();\n            let s = text[length..].to_string();\n\n            if glob::parse_and_compare(&s, pattern, extglob) {\n                ans_length = length;\n                if self.remove_symbol == \"%\" {\n                    break;\n                }\n            }\n        }\n\n        *text = text[0..ans_length].to_string();\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let len = feeder.scanner_parameter_remove_symbol();\n        if len == 0 {\n            return Ok(None);\n        }\n\n        let mut ans = Remove {\n            remove_symbol: feeder.consume(len),\n            ..Default::default()\n        };\n        ans.text += &ans.remove_symbol.clone();\n\n        if let Some(w) = Word::parse(\n            feeder,\n            core,\n            Some(WordMode::ParamOption(vec![\"}\".to_string()])),\n            //Some(WordMode::AlterWord),\n        )? {\n            ans.text += &w.text.clone();\n            ans.remove_pattern = Some(w);\n        } else {\n            ans.remove_pattern = Some(Word::default());\n        }\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/replace.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::optional_operation::OptionalOperation;\nuse super::super::Variable;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils::glob;\nuse crate::utils::glob::GlobElem;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Replace {\n    pub text: String,\n    pub head_only_replace: bool,\n    pub tail_only_replace: bool,\n    pub all_replace: bool,\n    pub replace_from: Option<Word>,\n    pub replace_to: Option<Word>,\n    pub has_replace_to: bool,\n}\n\nimpl OptionalOperation for Replace {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn exec(\n        &mut self,\n        param: &Variable,\n        text: &str,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        match core.db.exist(&param.name) {\n            true => self.get_text(text, core),\n            false => Ok(\"\".to_string()),\n        }\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n\n    fn init_array(\n        &mut self,\n        param: &Variable,\n        array: &mut Vec<String>,\n        text: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        *array = match param.name.as_str() {\n            \"@\" | \"*\" => core.db.get_position_params(),\n            _ => core.db.get_vec(&param.name, true)?,\n        };\n\n        for item in array.iter_mut() {\n            *item = self.get_text(item, core)?;\n        }\n\n        if param.name == \"@\"\n            || (param.index.is_some() && param.index.as_ref().unwrap().text == \"[@]\")\n        {\n            *text = array.join(\" \");\n            return Ok(());\n        }\n\n        let ifs = core.db.get_ifs_head();\n        *text = array.join(&ifs);\n        Ok(())\n    }\n\n    fn has_array_replace(&self) -> bool {\n        true\n    }\n}\n\nimpl Replace {\n    fn to_string(&self, w: &Option<Word>, core: &mut ShellCore) -> Result<String, ExecError> {\n        if let Some(w) = &w {\n            match w.eval_for_case_word(core) {\n                Some(s) => return Ok(s),\n                None => match w.subwords.len() {\n                    0 => return Ok(\"\".to_string()),\n                    _ => return Err(ExecError::Other(\"parse error\".to_string())),\n                },\n            }\n        }\n\n        Ok(\"\".to_string())\n    }\n\n    fn get_text_head(\n        text: &str,\n        pattern: &[GlobElem],\n        string_to: &str,\n    ) -> Result<String, ExecError> {\n        let len = glob::longest_match_length(text, pattern);\n        if len == 0 && !pattern.is_empty() {\n            return Ok(text.to_string());\n        }\n\n        let ans = string_to.to_string() + &text[len..];\n        Ok(ans)\n    }\n\n    fn get_text_tail(\n        text: &str,\n        pattern: &[GlobElem],\n        string_to: &str,\n    ) -> Result<String, ExecError> {\n        if pattern.is_empty() {\n            let ans = text.to_string() + string_to;\n            return Ok(ans);\n        }\n\n        let mut start = 0;\n        for ch in text.chars() {\n            let len = glob::longest_match_length(&text[start..], pattern);\n\n            if len == text[start..].len() {\n                let ans = text[..start].to_string() + string_to;\n                return Ok(ans);\n            }\n\n            start += ch.len_utf8();\n        }\n\n        Ok(text.to_string())\n    }\n\n    pub fn get_text(&self, text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let extglob = core.shopts.query(\"extglob\");\n        let tmp = self.to_string(&self.replace_from, core)?;\n        let pattern = glob::parse(&tmp, extglob);\n        let string_to = self.to_string(&self.replace_to, core)?;\n\n        if self.head_only_replace {\n            return Self::get_text_head(text, &pattern, &string_to);\n        } else if self.tail_only_replace {\n            return Self::get_text_tail(text, &pattern, &string_to);\n        }\n\n        let mut start = 0;\n        let mut ans = String::new();\n        let mut skip = 0;\n        for ch in text.chars() {\n            if skip > 0 {\n                skip -= 1;\n                start += ch.len_utf8();\n                continue;\n            }\n\n            let len = glob::longest_match_length(&text[start..], &pattern);\n            if len != 0 && self.tail_only_replace {\n                if len == text[start..].len() {\n                    return Ok([&text[..start], &string_to[0..]].concat());\n                } else {\n                    ans += &ch.to_string();\n                    start += ch.len_utf8();\n                    continue;\n                }\n            } else if len != 0 && !self.all_replace {\n                return Ok([&text[..start], &string_to[0..], &text[start + len..]].concat());\n            }\n\n            if len != 0 {\n                skip = text[start..start + len].chars().count() - 1;\n                ans += &string_to.clone();\n            } else {\n                ans += &ch.to_string();\n            }\n            start += ch.len_utf8();\n        }\n\n        Ok(ans)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"/\") {\n            return Ok(None);\n        }\n\n        let mut ans = Replace::default();\n\n        ans.text += &feeder.consume(1);\n        if feeder.starts_with(\"/\") {\n            ans.text += &feeder.consume(1);\n            ans.all_replace = true;\n        } else if feeder.starts_with(\"#\") {\n            ans.text += &feeder.consume(1);\n            ans.head_only_replace = true;\n        } else if feeder.starts_with(\"%\") {\n            ans.text += &feeder.consume(1);\n            ans.tail_only_replace = true;\n        }\n\n        if let Some(w) = Word::parse(\n            feeder,\n            core,\n            Some(WordMode::ParamOption(vec![\n                \"}\".to_string(),\n                \"/\".to_string(),\n            ])),\n        )? {\n            ans.text += &w.text.clone();\n            ans.replace_from = Some(w);\n        } else {\n            ans.replace_from = Some(Word::default());\n        }\n\n        if !feeder.starts_with(\"/\") {\n            return Ok(Some(ans));\n        }\n        ans.text += &feeder.consume(1);\n        ans.has_replace_to = true;\n\n        if let Some(w) = Word::parse(\n            feeder,\n            core,\n            Some(WordMode::ParamOption(vec![\"}\".to_string()])),\n            //Some(WordMode::AlterWord),\n        )? {\n            ans.text += &w.text.clone();\n            ans.replace_to = Some(w);\n        } else {\n            ans.replace_to = Some(Word::default());\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/substr.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::Variable;\nuse super::OptionalOperation;\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Substr {\n    pub text: String,\n    pub offset: Option<ArithmeticExpr>,\n    pub length: Option<ArithmeticExpr>,\n}\n\nimpl OptionalOperation for Substr {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n    fn exec(\n        &mut self,\n        _: &Variable,\n        text: &str,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        self.get(text, core)\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n    fn has_array_replace(&self) -> bool {\n        true\n    }\n\n    fn init_array(\n        &mut self,\n        param: &Variable,\n        array: &mut Vec<String>,\n        text: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        let ifs = core.db.get_ifs_head();\n        match param.name.as_str() {\n            \"@\" => self.set_partial_position_params(array, text, core, \" \"),\n            \"*\" => self.set_partial_position_params(array, text, core, &ifs),\n            _ => self.set_partial_array(&param.name, array, text, core),\n        }\n    }\n}\n\nimpl Substr {\n    fn set_partial_position_params(\n        &mut self,\n        array: &mut Vec<String>,\n        text: &mut String,\n        core: &mut ShellCore,\n        ifs: &str,\n    ) -> Result<(), ExecError> {\n        let offset = self.offset.as_mut().unwrap();\n\n        if offset.text.is_empty() {\n            return Err(ExecError::BadSubstitution(String::new()));\n        }\n\n        *array = core.db.get_vec(\"@\", false)?;\n        let mut n = offset.eval_as_int(core)?;\n        let len = array.len();\n\n        if n < 0 {\n            n += len as i128;\n            if n < 0 {\n                *text = \"\".to_string();\n                *array = vec![];\n                return Ok(());\n            }\n        }\n\n        let mut start = std::cmp::max(0, n) as usize;\n        start = std::cmp::min(start, array.len());\n        *array = array.split_off(start);\n\n        if self.length.is_none() {\n            *text = array.join(ifs);\n            return Ok(());\n        }\n\n        let mut length = match self.length.clone() {\n            None => return Err(ExecError::BadSubstitution(\"\".to_string())),\n            Some(ofs) => ofs,\n        };\n\n        if length.text.is_empty() {\n            return Err(ExecError::BadSubstitution(\"\".to_string()));\n        }\n\n        let n = length.eval_as_int(core)?;\n        if n < 0 {\n            return Err(ExecError::SubstringMinus(n));\n        }\n        let len = std::cmp::min(n as usize, array.len());\n        let _ = array.split_off(len);\n\n        *text = array.join(\" \");\n        Ok(())\n    }\n\n    fn set_partial_array(\n        &mut self,\n        name: &str,\n        array: &mut Vec<String>,\n        text: &mut String,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        let offset = self.offset.as_mut().unwrap();\n\n        if offset.text.is_empty() {\n            return Err(ExecError::BadSubstitution(String::new()));\n        }\n\n        let mut n = offset.eval_as_int(core)?;\n        let len = core.db.index_based_len(name);\n        if n < 0 {\n            n += len as i128;\n            if n < 0 {\n                *text = \"\".to_string();\n                *array = vec![];\n                return Ok(());\n            }\n        }\n\n        //let start = std::cmp::max(0, n) as usize;\n        //*array = core.db.get_vec_from(name, start, true)?;\n        *array = core.db.get_vec_from(name, n as usize, true)?;\n\n        if self.length.is_none() {\n            *text = array.join(\" \");\n            return Ok(());\n        }\n\n        let mut length = match self.length.clone() {\n            None => return Err(ExecError::BadSubstitution(\"\".to_string())),\n            Some(ofs) => ofs,\n        };\n\n        if length.text.is_empty() {\n            return Err(ExecError::BadSubstitution(\"\".to_string()));\n        }\n\n        let n = length.eval_as_int(core)?;\n        if n < 0 {\n            return Err(ExecError::SubstringMinus(n));\n        }\n        let len = std::cmp::min(n as usize, array.len());\n        let _ = array.split_off(len);\n\n        *text = array.join(\" \");\n        Ok(())\n    }\n\n    pub fn get(&mut self, text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let offset = self.offset.as_mut().unwrap();\n\n        if offset.text.is_empty() {\n            let err = ArithError::OperandExpected(\"\".to_string());\n            return Err(ExecError::ArithError(\"\".to_string(), err));\n        }\n\n        let mut ans: String;\n        let mut n = offset.eval_as_int(core)?;\n        let len = text.chars().count();\n\n        if n < 0 {\n            n += len as i128;\n            if n < 0 {\n                return Ok(\"\".to_string());\n            }\n        }\n\n        ans = text\n            .chars()\n            .enumerate()\n            .filter(|(i, _)| (*i as i128) >= n)\n            .map(|(_, c)| c)\n            .collect();\n\n        if self.length.is_some() {\n            ans = self.length(&ans, core)?;\n        }\n\n        Ok(ans)\n    }\n\n    fn length(&mut self, text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let n = self.length.as_mut().unwrap().eval_as_int(core)?;\n        Ok(text\n            .chars()\n            .enumerate()\n            .filter(|(i, _)| (*i as i128) < n)\n            .map(|(_, c)| c)\n            .collect())\n    }\n\n    fn eat_length(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) {\n        if !feeder.starts_with(\":\") {\n            return;\n        }\n        ans.text += &feeder.consume(1);\n        ans.length = match ArithmeticExpr::parse(feeder, core, true, \":\") {\n            Ok(Some(a)) => {\n                ans.text += &a.text.clone();\n                Some(a)\n            }\n            _ => None,\n        };\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Self> {\n        if !feeder.starts_with(\":\") {\n            return None;\n        }\n        let mut ans = Self::default();\n        ans.text += &feeder.consume(1);\n\n        ans.offset = match ArithmeticExpr::parse(feeder, core, true, \":\") {\n            Ok(Some(a)) => {\n                ans.text += &a.text.clone();\n                Self::eat_length(feeder, &mut ans, core);\n                Some(a)\n            }\n            _ => None,\n        };\n\n        Some(ans)\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation/value_check.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::super::Variable;\nuse super::OptionalOperation;\nuse crate::elements::subword::SimpleSubword;\nuse crate::elements::subword::SingleQuoted;\nuse crate::elements::subword::Subword;\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::arith::ArithError;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::utils;\nuse crate::{Feeder, ShellCore};\n\nfn is_special_param(name: &str) -> bool {\n    //\"$?*@#-!0123456789\".contains(name)\n    \"*@\".contains(name)\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ValueCheck {\n    pub text: String,\n    pub symbol: Option<String>,\n    alternative_value: Option<Word>,\n    in_double_quoted: bool,\n}\n\nimpl OptionalOperation for ValueCheck {\n    fn get_text(&self) -> String {\n        self.text.clone()\n    }\n\n    fn exec(\n        &mut self,\n        variable: &Variable,\n        text: &str,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        let sym = self.symbol.clone().unwrap();\n        let mut check_ok = match sym.starts_with(\":\") {\n            true => !text.is_empty(),\n            false => is_special_param(&variable.name) || variable.exist(core)?,\n        };\n\n        if sym.ends_with(\"+\") {\n            check_ok = !check_ok;\n        }\n\n        if check_ok {\n            self.alternative_value = None;\n            return Ok(text.to_string());\n        }\n\n        match sym.as_ref() {\n            \"?\" | \":?\" => self.show_error(&variable.name, core),\n            \"=\" | \":=\" => self.set_value(variable, core),\n            _ => self.replace(core),\n        }\n    }\n\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation> {\n        Box::new(self.clone())\n    }\n    fn is_value_check(&self) -> bool {\n        true\n    }\n\n    fn get_alternative(&self) -> Vec<Box<dyn Subword>> {\n        match &self.alternative_value {\n            Some(w) => w.subwords.to_vec(),\n            None => vec![],\n        }\n    }\n\n    fn set_heredoc_flag(&mut self) {\n        self.alternative_value\n            .iter_mut()\n            .for_each(|e| e.set_heredoc_flag());\n    }\n\n    fn array_to_single(&mut self) -> bool {\n        self.alternative_value.is_some()\n    }\n}\n\nimpl ValueCheck {\n    fn set_alter_word(&mut self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let mut v = match &self.alternative_value {\n            Some(av) => av.clone(),\n            None => return Err(ArithError::OperandExpected(\"\".to_string()).into()),\n        };\n\n        if self.in_double_quoted {\n            for e in v.subwords.iter_mut().filter(|e| e.is_escaped_char()) {\n//                if e.is_escaped_char() {\n                    match e.get_text() {\n                        \"\\\\$\" | \"\\\\\\\\\" | \"\\\\\\\"\" | \"\\\\`\" => {}\n                        txt => {\n                            let sw = SimpleSubword {\n                                text: txt.to_string(),\n                            };\n                            *e = Box::new(sw);\n                        }\n                    }\n //               }\n            }\n\n            self.alternative_value = Some(v.dollar_expansion(core)?);\n        } else {\n            self.alternative_value = Some(v.tilde_and_dollar_expansion(core)?);\n        }\n        if self.in_double_quoted {\n            for sw in self.alternative_value.as_mut().unwrap().subwords.iter_mut() {\n                if sw.get_text().starts_with(\"'\") {\n                    Self::apply_single_quote_rule(sw);\n                }\n            }\n        }\n\n        if v.text.starts_with(\"~\") && !self.in_double_quoted {\n            v.eval_as_value(core)\n        } else {\n            v.eval_as_alter(core)\n        }\n    }\n\n    fn apply_single_quote_rule(sw: &mut Box<dyn Subword>) {\n        let mut escaped = false;\n        let mut ans = String::new();\n        for c in sw.get_text().chars() {\n            if escaped || c == '\\\\' {\n                escaped = !escaped;\n                if c == '\"' {\n                    ans.pop();\n                }\n            } else if c == '\"' {\n                continue;\n            }\n            ans.push(c);\n        }\n\n        ans.insert(0, '\\'');\n        ans.push('\\'');\n        *sw = Box::new(SingleQuoted {\n            text: ans.to_string(),\n        });\n    }\n\n    fn replace(&mut self, core: &mut ShellCore) -> Result<String, ExecError> {\n        self.set_alter_word(core)\n    }\n\n    fn set_value(\n        &mut self,\n        variable: &Variable,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        let mut value = self.set_alter_word(core)?;\n\n        if core.db.is_int(&variable.name) {\n            value = utils::string_to_calculated_string(&value, core)?;\n        }\n        if core.db.has_flag(&variable.name, 'l') {\n            value = value.to_lowercase();\n        } else if core.db.has_flag(&variable.name, 'u') {\n            value = value.to_uppercase();\n        }\n\n        variable.clone().set_value(&value, core)?;\n        self.alternative_value = None;\n        Ok(value)\n    }\n\n    fn show_error(&mut self, name: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n        let value = self.set_alter_word(core)?;\n        let msg = format!(\"{}: {}\", &name, &value);\n        Err(ExecError::Other(msg))\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        let num = feeder.scanner_parameter_alternative_symbol();\n        if num == 0 {\n            return Ok(None);\n        }\n\n        let mut ans = ValueCheck::default();\n\n        let symbol = feeder.consume(num);\n        ans.symbol = Some(symbol.clone());\n        ans.text += &symbol;\n\n        let num = feeder.scanner_blank(core);\n        ans.text += &feeder.consume(num);\n        let mode = WordMode::ParamOption(vec![\"}\".to_string()]);\n        //let mode = WordMode::AlterWord;\n        ans.alternative_value = Some(Word::default());\n\n        if let Some(w) = Word::parse(feeder, core, Some(mode))? {\n            ans.text += &w.text.clone();\n            ans.alternative_value = Some(w);\n        }\n\n        if feeder.nest.iter().any(|e| e.0 == \"\\\"\") {\n            ans.in_double_quoted = true;\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/optional_operation.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod case_conv;\nmod escape;\nmod remove;\nmod replace;\nmod substr;\nmod value_check;\n\nuse self::case_conv::CaseConv;\nuse self::escape::Escape;\nuse self::remove::Remove;\nuse self::replace::Replace;\nuse self::substr::Substr;\nuse self::value_check::ValueCheck;\nuse super::Variable;\nuse crate::elements::subword::Subword;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\nuse core::fmt;\nuse core::fmt::Debug;\n\npub trait OptionalOperation {\n    fn exec(&mut self, _: &Variable, _: &str, _: &mut ShellCore) -> Result<String, ExecError>;\n    fn boxed_clone(&self) -> Box<dyn OptionalOperation>;\n    fn get_text(&self) -> String;\n    fn has_array_replace(&self) -> bool {\n        false\n    }\n    fn is_value_check(&self) -> bool {\n        false\n    }\n    fn init_array(\n        &mut self,\n        _: &Variable,\n        _: &mut Vec<String>,\n        _: &mut String,\n        _: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        Ok(())\n    }\n    fn get_alternative(&self) -> Vec<Box<dyn Subword>> {\n        vec![]\n    }\n\n    fn set_heredoc_flag(&mut self) {}\n    fn array_to_single(&mut self) -> bool {\n        false\n    }\n}\n\npub fn parse(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n) -> Result<Option<Box<dyn OptionalOperation>>, ParseError> {\n    if let Some(a) = Replace::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = ValueCheck::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = CaseConv::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Remove::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Substr::parse(feeder, core) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Escape::parse(feeder, core) {\n        Ok(Some(Box::new(a)))\n    } else {\n        Ok(None)\n    }\n}\n\nimpl Clone for Box<dyn OptionalOperation> {\n    fn clone(&self) -> Box<dyn OptionalOperation> {\n        self.boxed_clone()\n    }\n}\n\nimpl Debug for dyn OptionalOperation {\n    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {\n        fmt.debug_struct(&self.get_text()).finish()\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param/parse.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::optional_operation;\nuse super::{BracedParam, Variable};\nuse crate::elements::substitution::subscript::Subscript;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\nimpl BracedParam {\n    fn eat_subscript(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(s) = Subscript::parse(feeder, core)? {\n            ans.text += &s.text;\n            if s.text.contains('@') && !ans.num {\n                ans.treat_as_array = true;\n            }\n            ans.param.index = Some(s);\n            return Ok(true);\n        }\n\n        Ok(false)\n    }\n\n    fn eat_param(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_name(core);\n        if len != 0 {\n            ans.param = Variable::default();\n            ans.param.name = feeder.consume(len);\n            ans.text += &ans.param.name;\n            return true;\n        }\n\n        let mut len = feeder.scanner_uint(core);\n        if len == 0 {\n            len = feeder.scanner_special_and_positional_param();\n        }\n\n        if len != 0 {\n            ans.param = Variable::default();\n            ans.param.name = feeder.consume(len);\n            ans.treat_as_array = ans.param.name == \"@\" && !ans.num;\n            ans.text += &ans.param.name;\n            return true;\n        }\n\n        feeder.starts_with(\"}\")\n    }\n\n    fn eat_unknown(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<(), ParseError> {\n        if feeder.is_empty() {\n            feeder.feed_additional_line(core)?;\n        }\n\n        let unknown = match feeder.starts_with(\"\\\\}\") {\n            true => feeder.consume(2),\n            false => {\n                let len = feeder.scanner_char(); //feeder.nth(0).unwrap().len_utf8();\n                feeder.consume(len)\n            }\n        };\n\n        ans.unknown += &unknown.clone();\n        ans.text += &unknown;\n        Ok(())\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"${\") {\n            return Ok(None);\n        }\n        let mut ans = Self::default();\n        ans.text += &feeder.consume(2);\n\n        if feeder.starts_with(\"#\") && !feeder.starts_with(\"#}\") {\n            ans.num = true;\n            ans.text += &feeder.consume(1);\n        } else if feeder.starts_with(\"!\") {\n            ans.indirect = true;\n            ans.text += &feeder.consume(1);\n        }\n\n        if Self::eat_param(feeder, &mut ans, core) {\n            Self::eat_subscript(feeder, &mut ans, core)?;\n\n            if let Some(op) = optional_operation::parse(feeder, core)? {\n                ans.text += &op.get_text();\n                ans.optional_operation = Some(op);\n            }\n        }\n        while !feeder.starts_with(\"}\") {\n            Self::eat_unknown(feeder, &mut ans, core)?;\n        }\n\n        ans.text += &feeder.consume(1);\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/braced_param.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod optional_operation;\nmod parse;\n\nuse self::optional_operation::OptionalOperation;\nuse crate::elements::substitution::variable::Variable;\nuse crate::elements::subword::Subword;\nuse crate::error::exec::ExecError;\nuse crate::utils;\nuse crate::utils::splitter;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct BracedParam {\n    text: String,\n    array: Option<Vec<String>>,\n    param: Variable,\n    optional_operation: Option<Box<dyn OptionalOperation>>,\n    unknown: String,\n    treat_as_array: bool,\n    num: bool,\n    indirect: bool,\n}\n\nimpl From<&str> for BracedParam {\n    fn from(s: &str) -> Self {\n        let mut ans: Self = Default::default();\n        ans.text = \"$\".to_owned() + s;\n        ans.param.text = s.to_string();\n        ans.param.name = s.to_string();\n        ans\n    }\n}\n\nimpl Subword for BracedParam {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if core.db.exist_nameref(&self.param.name) && ! self.indirect {\n            let mut circular_check_vec = vec![];\n            let org_name = self.param.name.clone();\n            loop {\n                let bkup = self.param.name.clone();\n                self.param.check_nameref(core)?;\n                if self.param.name == bkup {\n                    self.param.name = utils::gen_not_exist_var(core);\n                }\n\n                if circular_check_vec.contains(&self.param.name) {\n                    ExecError::CircularNameRef(org_name).print(core);\n                    self.param.name = utils::gen_not_exist_var(core);\n                    break;\n                }\n                if ! core.db.exist_nameref(&self.param.name) {\n                    break;\n                }\n                circular_check_vec.push(self.param.name.clone());\n            }\n\n            return self.substitute(core);\n        }\n        self.check()?;\n\n        if self.indirect {\n            if ! self.indirect_preparation(core)? {\n                return Ok(());\n            }\n        }\n\n        if self.param.is_array() {\n            if let Some(op) = self.optional_operation.as_mut() {\n                if op.has_array_replace() {\n                    return self.array_replace(core);\n                }\n            }\n        }\n\n        match self.param.index.is_some() {\n            true => self.subscript_operation(core),\n            false => self.non_subscript_operation(core),\n        }\n    }\n\n    fn set_text(&mut self, text: &str) {\n        self.text = text.to_string();\n    }\n\n    fn is_array(&self) -> bool {\n        self.treat_as_array\n    }\n\n    fn get_elem(&mut self) -> Vec<String> {\n        if let Some(op) = self.optional_operation.as_mut() {\n            if op.array_to_single() {\n                return vec![self.text.clone()];\n            }\n        }\n\n        self.array.clone().unwrap_or_default()\n    }\n\n    fn alter(&mut self) -> Result<Vec<Box<dyn Subword>>, ExecError> {\n        match self.optional_operation.as_mut() {\n            Some(op) => Ok(op.get_alternative()),\n            None => Ok(vec![]),\n        }\n    }\n\n    fn split(&self, ifs: &str, prev_char: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        if self.text.is_empty() {\n            return vec![];\n        }\n\n        let index_is_asterisk =\n            self.param.index.is_some() && self.param.index.as_ref().unwrap().text == \"[*]\";\n\n        if ifs.is_empty() && (self.param.name == \"*\" || index_is_asterisk) {\n            return self.make_split();\n        }\n\n        if (!self.treat_as_array && !index_is_asterisk && self.param.name != \"*\")\n            || ifs.starts_with(\" \")\n            || self.array.is_none()\n        {\n            return splitter::split(&self.text, ifs, prev_char)\n                .iter()\n                .map(|s| (From::from(&s.0), s.1))\n                .collect();\n        }\n\n        self.make_split()\n    }\n\n    fn set_heredoc_flag(&mut self) {\n        self.optional_operation\n            .iter_mut()\n            .for_each(|e| e.set_heredoc_flag());\n    }\n}\n\nimpl BracedParam {\n    fn check(&mut self) -> Result<(), ExecError> {\n        if self.param.name.is_empty() || !utils::is_param(&self.param.name) {\n            return Err(ExecError::BadSubstitution(self.text.clone()));\n        }\n        if !self.unknown.is_empty() && !self.unknown.starts_with(\",\") {\n            return Err(ExecError::BadSubstitution(self.text.clone()));\n        }\n\n        if self.param.index.is_some() && self.param.is_pos_param_array() {\n            return Err(ExecError::BadSubstitution(self.param.name.clone()));\n        }\n        Ok(())\n    }\n\n    fn make_split(&self) -> Vec<(Box<dyn Subword>, bool)> {\n        if self.array.is_none() {\n            return vec![];\n        }\n\n        let mut ans = vec![];\n        for p in self.array.clone().unwrap() {\n            ans.push((From::from(&p), true));\n        }\n        ans\n    }\n\n    fn index_replace(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.optional_operation.is_some() {\n            let msg = core.db.get_vec(&self.param.name, true)?.join(\" \");\n            return Err(ExecError::InvalidName(msg));\n        }\n\n        if !core.db.exist(&self.param.name) {\n            self.text = \"\".to_string();\n            return Ok(());\n        }\n\n        if !core.db.is_array(&self.param.name) && !core.db.is_assoc(&self.param.name) {\n            self.text = \"0\".to_string();\n            return Ok(());\n        }\n\n        let arr = core.db.get_indexes_all(&self.param.name);\n        self.array = Some(arr.clone());\n        self.text = arr.join(\" \");\n\n        Ok(())\n    }\n\n    fn array_replace(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let mut arr = vec![];\n        let op = self.optional_operation.as_mut().unwrap();\n        op.init_array(&self.param, &mut arr, &mut self.text, core)?;\n        self.array = Some(arr.clone());\n        if let Some(index) = &self.param.index {\n            if index.text == \"[*]\" {\n                self.text = arr.join(&core.db.get_ifs_head());\n            }else if index.text == \"[@]\" {\n                self.text = arr.join(\" \");\n            }\n        }\n\n        Ok(())\n    }\n\n    fn indirect_preparation(&mut self, core: &mut ShellCore) -> Result<bool, ExecError> {\n        if ! core.db.exist(&self.param.name)\n        && ! core.db.exist_nameref(&self.param.name) {\n            return Err(ExecError::InvalidIndirectExpansion(self.param.name.to_string()));\n        }\n\n        if core.db.has_flag(&self.param.name, 'n') {\n            if self.text.contains(\"[\") {\n                self.text = String::new();\n            } else if let Some(nameref) = core.db.get_nameref(&self.param.name)? {\n                self.text = nameref;\n            }else{\n                self.text = String::new();\n            }\n            return Ok(false);\n        }\n\n        if self.param.is_var_array() { // ${!name[@]}, ${!name[*]}\n            self.index_replace(core)?;\n            return Ok(false);\n        }\n\n        self.indirect_replace(core)?;\n        self.check()?;\n        Ok(true)\n    }\n\n    fn indirect_replace(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let mut sw = self.clone();\n        sw.indirect = false;\n        sw.unknown = String::new();\n        sw.treat_as_array = false;\n        sw.num = false;\n\n        sw.substitute(core)?;\n\n        if sw.text.contains('[') {\n            let mut feeder = Feeder::new(&(\"${\".to_owned() + &sw.text + \"}\"));\n            if let Ok(Some(mut bp)) = BracedParam::parse(&mut feeder, core) {\n                bp.substitute(core)?;\n                self.param.name = bp.param.name;\n                self.param.index = bp.param.index;\n            } else {\n                return Err(ExecError::InvalidName(sw.text.clone()));\n            }\n        } else {\n            self.param.name = sw.text.clone();\n            self.param.index = None;\n        }\n\n        if !utils::is_param(&self.param.name) {\n            return Err(ExecError::InvalidName(self.param.name.clone()));\n        }\n        Ok(())\n    }\n\n    fn non_subscript_operation(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.param.name == \"*\" || self.param.name == \"@\" {\n            self.array = Some(core.db.get_position_params());\n        }\n\n        let value = core.db.get_param(&self.param.name).unwrap_or_default();\n        self.text = match self.num {\n            true => core.db.get_braced_param_hash_length(&self.param.name)?.to_string(),\n            false => value.to_string(),\n        };\n\n        self.text = self.optional_operation(self.text.clone(), core)?;\n        Ok(())\n    }\n\n    fn subscript_operation(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let index = self\n            .param\n            .index\n            .clone()\n            .unwrap()\n            .eval(core, &self.param.name)?;\n\n        if self.num {\n            self.text = core.db.get_elem_len(&self.param.name, &index)?.to_string();\n            return Ok(());\n        }\n\n        if core.db.is_single(&self.param.name) {\n            let param = core.db.get_param(&self.param.name)?;\n            let tmp = match index.as_str() {\n                //case: a=aaa; echo ${a[@]}; (output: aaa)\n                \"@\" | \"*\" | \"0\" => param, //.unwrap_or(\"\".to_string()),\n                _ => \"\".to_string(),\n            };\n            self.text = self.optional_operation(tmp, core)?;\n            return Ok(());\n        }\n\n        let ifs = core.db.get_ifs_head();\n\n        if index.as_str() == \"@\" {\n            self.atmark_operation(core, \" \")\n        } else if index.as_str() == \"*\" {\n            self.atmark_operation(core, &ifs)\n        } else {\n            let tmp = core.db.get_elem(&self.param.name, &index)?;\n            self.text = self.optional_operation(tmp, core)?;\n            Ok(())\n        }\n    }\n\n    fn atmark_operation(&mut self, core: &mut ShellCore, ifs: &str) -> Result<(), ExecError> {\n        let mut arr = core.db.get_vec(&self.param.name, true)?;\n        self.array = Some(arr.clone());\n        if self.num {\n            self.text = arr.len().to_string();\n            return Ok(());\n        }\n\n        self.text = match self.num {\n            true => core.db.get_var_len(&self.param.name).to_string(),\n            false => core.db.get_vec(&self.param.name, true)?.join(ifs),\n        };\n\n        if arr.len() <= 1 || self.has_value_check() {\n            self.text = self.optional_operation(self.text.clone(), core)?;\n        } else {\n            for item in arr.iter_mut() {\n                *item = self.optional_operation(item.clone(), core)?;\n            }\n            self.text = arr.join(ifs);\n            self.array = Some(arr);\n        }\n        Ok(())\n    }\n\n    fn has_value_check(&mut self) -> bool {\n        match self.optional_operation.as_mut() {\n            Some(op) => op.is_value_check(),\n            _ => false,\n        }\n    }\n\n    fn optional_operation(\n        &mut self,\n        text: String,\n        core: &mut ShellCore,\n    ) -> Result<String, ExecError> {\n        match self.optional_operation.as_mut() {\n            Some(op) => op.exec(&self.param, &text, core),\n            None => Ok(text.clone()),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/command_sub.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::command::paren::ParenCommand;\nuse crate::elements::command::Command;\nuse crate::elements::subword::Subword;\nuse crate::elements::Pipe;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{proc_ctrl, Feeder, ShellCore};\nuse nix::unistd;\nuse std::io::{BufRead, BufReader, Error};\nuse std::os::fd::RawFd;\nuse std::sync::atomic::Ordering::Relaxed;\nuse std::{thread, time};\n\n#[derive(Debug, Clone, Default)]\npub struct CommandSubstitution {\n    pub text: String,\n    command: ParenCommand,\n}\n\nimpl Subword for CommandSubstitution {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let mut pipe = Pipe::new(\"|\".to_string());\n        pipe.set(-1, unistd::getpgrp(), core);\n        let pid = self.command.exec(core, &mut pipe)?;\n        let result = self.read(pipe.recv, core);\n        proc_ctrl::wait_pipeline(core, vec![pid], false, false);\n        result?;\n        self.text = self.text.trim_end_matches(\"\\n\").to_string();\n        Ok(())\n    }\n}\n\nimpl CommandSubstitution {\n    fn set_line(&mut self, line: Result<String, Error>) -> bool {\n        if let Ok(ln) = line {\n            self.text.push_str(&ln);\n            self.text.push('\\n');\n            return true;\n        }\n        false\n    }\n\n    fn interrupted(&mut self, count: usize, core: &mut ShellCore) -> Result<(), ExecError> {\n        if count % 100 == 99 {\n            //To receive Ctrl+C\n            thread::sleep(time::Duration::from_millis(1));\n        }\n        match core.sigint.load(Relaxed) {\n            true => Err(ExecError::Interrupted),\n            false => Ok(()),\n        }\n    }\n\n    fn read(&mut self, fd: RawFd, core: &mut ShellCore) -> Result<(), ExecError> {\n        //let f = unsafe { File::from_raw_fd(fd) };\n        let f = core.fds.get_file(fd);\n        let reader = BufReader::new(f);\n        self.text.clear();\n        for (i, line) in reader.lines().enumerate() {\n            self.interrupted(i, core)?;\n            if !self.set_line(line) {\n                break;\n            }\n        }\n\n        self.text.pop();\n        Ok(())\n    }\n\n    pub fn parse_old_style(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n    ) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"`\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self {\n            text: feeder.consume(1),\n            ..Default::default()\n        };\n        let mut esc = false;\n        while esc || !feeder.starts_with(\"`\") {\n            if feeder.is_empty() {\n                feeder.feed_additional_line(core)?;\n                continue;\n            }\n\n            let len = feeder.scanner_char();\n            let c = feeder.consume(len);\n\n            if esc && (c == \"$\" || c == \"\\\\\" || c == \"`\") {\n                ans.text.pop();\n            }\n\n            ans.text += &c;\n\n            if !esc && c == \"\\\\\" {\n                esc = true;\n                continue;\n            }\n\n            esc = false;\n        }\n\n        ans.text += &feeder.consume(1);\n        let mut paren = ans.text.clone();\n        paren.pop();\n        paren.push(')');\n\n        let mut f = Feeder::new(&paren);\n        if let Some(s) = ParenCommand::parse(&mut f, core, true)? {\n            ans.command = s;\n            return Ok(Some(ans));\n        }\n\n        Ok(None)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if let Some(ans) = Self::parse_old_style(feeder, core)? {\n            return Ok(Some(ans));\n        }\n\n        if !feeder.starts_with(\"$(\") {\n            return Ok(None);\n        }\n        let mut ans = Self {\n            text: feeder.consume(1),\n            ..Default::default()\n        };\n\n        if let Some(pc) = ParenCommand::parse(feeder, core, true)? {\n            ans.text += &pc.get_text();\n            ans.command = pc;\n            Ok(Some(ans))\n        } else {\n            Ok(None)\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/double_quoted.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{BracedParam, EscapedChar, Parameter, Subword, VarName};\nuse crate::elements::subword::{Arithmetic, CommandSubstitution};\nuse crate::elements::word::{substitution, Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct DoubleQuoted {\n    text: String,\n    subwords: Vec<Box<dyn Subword>>,\n    split_points: Vec<usize>,\n    //    quote_substitution: bool,\n}\n\nimpl Subword for DoubleQuoted {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.connect_array(core)?;\n\n        let mut word = match self.subwords.iter().any(|sw| sw.is_array()) {\n            true => Word::from(self.replace_array(core)?),\n            false => Word::from(self.subwords.clone()),\n        };\n\n        substitution::eval(&mut word, core)?;\n        self.subwords = word.subwords;\n        self.text.clear();\n\n        for (i, sw) in self.subwords.iter_mut().enumerate() {\n            if self.split_points.contains(&i) {\n                self.text += \" \";\n            }\n            self.text += sw.get_text();\n        }\n\n        Ok(())\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        self.text\n            .replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(\"*\", \"\\\\*\")\n            .replace(\"?\", \"\\\\?\")\n            .replace(\"[\", \"\\\\[\")\n            .replace(\"]\", \"\\\\]\")\n            .replace(\"@\", \"\\\\@\")\n            .replace(\"+\", \"\\\\+\")\n            .replace(\"!\", \"\\\\!\")\n    }\n\n    fn make_unquoted_string(&mut self) -> Option<String> {\n        let mut text = String::new();\n\n        for (i, sw) in self.subwords.iter_mut().enumerate() {\n            if self.split_points.contains(&i) {\n                text += \" \";\n            }\n\n            if let Some(txt) = sw.make_unquoted_string() {\n                text += &txt;\n            }\n        }\n\n        if text.is_empty() && self.split_points.len() == 1 {\n            return None;\n        }\n\n        Some(text)\n    }\n\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        let mut ans = vec![];\n        let mut last = 0;\n        let mut tmp = Self::default();\n        for p in &self.split_points {\n            tmp.subwords = self.subwords[last..*p].to_vec();\n            ans.push((tmp.boxed_clone(), true));\n            last = *p;\n        }\n        ans\n    }\n}\n\nimpl DoubleQuoted {\n    fn connect_array(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        for sw in self.subwords.iter_mut() {\n            if sw.get_text() == \"$*\" || sw.get_text() == \"${*}\" {\n                let params = core.db.get_position_params();\n                let joint = core.db.get_ifs_head();\n                *sw = From::from(&params.join(&joint));\n            }\n        }\n        Ok(())\n    }\n\n    fn replace_array(&mut self, core: &mut ShellCore) -> Result<Vec<Box<dyn Subword>>, ExecError> {\n        let mut ans = vec![];\n        let mut flg = false;\n\n        for sw in &mut self.subwords {\n            if !sw.is_array() {\n                ans.push(sw.boxed_clone());\n                continue;\n            }\n\n            let array = match sw.get_text() {\n                \"$@\" | \"${@}\" => core.db.get_position_params(),\n                _ => {\n                    sw.substitute(core)?;\n                    sw.get_elem()\n                }\n            };\n\n            if array.len() == 1 {\n                ans.push(sw.boxed_clone());\n                continue;\n            }\n\n            flg = true;\n            for text in array {\n                ans.push(From::from(&text));\n                self.split_points.push(ans.len());\n            }\n            self.split_points.pop();\n        }\n\n        if !flg {\n            self.split_points.clear();\n            return Ok(self.subwords.clone());\n        }\n\n        self.split_points.push(ans.len());\n        Ok(ans)\n    }\n\n    fn eat_element(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        let sw: Box<dyn Subword> = if let Some(a) = BracedParam::parse(feeder, core)? {\n            Box::new(a)\n        } else if let Some(a) = Arithmetic::parse(feeder, core)? {\n            Box::new(a)\n        } else if let Some(a) = CommandSubstitution::parse(feeder, core)? {\n            Box::new(a)\n        }\n        //else if let Some(a) = CommandSubstitutionOld::parse(feeder, core)? {Box::new(a)}\n        else if let Some(a) = Parameter::parse(feeder, core) {\n            Box::new(a)\n        } else if let Some(a) = Self::parse_escaped_char(feeder) {\n            Box::new(a)\n        } else if let Some(a) = Self::parse_name(feeder, core) {\n            Box::new(a)\n        } else {\n            return Ok(false);\n        };\n\n        ans.text += sw.get_text();\n        ans.subwords.push(sw);\n        Ok(true)\n    }\n\n    fn parse_escaped_char(feeder: &mut Feeder) -> Option<EscapedChar> {\n        if feeder.starts_with(\"\\\\$\")\n            || feeder.starts_with(\"\\\\\\\\\")\n            || feeder.starts_with(\"\\\\\\\"\")\n            || feeder.starts_with(\"\\\\`\")\n        {\n            return Some(EscapedChar {\n                text: feeder.consume(2),\n            });\n        }\n        None\n    }\n\n    fn parse_name(feeder: &mut Feeder, core: &mut ShellCore) -> Option<VarName> {\n        match feeder.scanner_name(core) {\n            0 => None,\n            n => Some(VarName {\n                text: feeder.consume(n),\n            }),\n        }\n    }\n\n    fn eat_char(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        let len = feeder.scanner_char();\n        if len == 0 {\n            feeder.feed_additional_line(core)?;\n            return Ok(true);\n        }\n\n        let ch = feeder.consume(len);\n        ans.text += &ch.clone();\n        if ch != \"\\\"\" {\n            ans.subwords.push(From::from(&ch));\n            return Ok(true);\n        }\n        Ok(false)\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        mode: &Option<WordMode>,\n    ) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"\\\"\") && !feeder.starts_with(\"$\\\"\") {\n            return Ok(None);\n        }\n        if let Some(WordMode::Heredoc) = mode {\n            return Ok(None);\n        }\n\n        let mut ans = Self::default();\n        /*\n        if let Some(WordMode::ReparseOfSubstitution) = mode {\n            ans.quote_substitution = true;\n        }*/\n\n        feeder.nest.push((\"\\\"\".to_string(), vec![\"\\\"\".to_string()]));\n\n        let len = if feeder.starts_with(\"\\\"\") { 1 } else { 2 };\n        ans.text = feeder.consume(len);\n\n        while Self::eat_element(feeder, &mut ans, core)? || Self::eat_char(feeder, &mut ans, core)?\n        {\n        }\n\n        feeder.nest.pop();\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/escaped_char.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::Subword;\nuse crate::utils::exit;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone)]\npub struct EscapedChar {\n    pub text: String,\n}\n\nimpl Subword for EscapedChar {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn make_unquoted_string(&mut self) -> Option<String> {\n        match self.text.len() {\n            0 => exit::internal(\"unescaped escaped char\"),\n            1 => None,\n            _ => Some(self.text[1..].to_string()),\n        }\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        if let Some(c) = self.text.chars().nth(1) {\n            if !\"*?[]^!\\\\\".contains(c) {\n                return c.to_string();\n            }\n        }\n        self.text.clone()\n    }\n\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n\n    fn is_escaped_char(&self) -> bool {\n        true\n    }\n}\n\nimpl EscapedChar {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Self> {\n        match feeder.scanner_escaped_char(core) {\n            0 => None,\n            n => Some(EscapedChar {\n                text: feeder.consume(n),\n            }),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/ext_glob.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{BracedParam, EscapedChar, Parameter, Subword, VarName};\nuse crate::elements::subword::CommandSubstitution;\nuse crate::error::parse::ParseError;\nuse crate::utils::exit;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone)]\npub struct ExtGlob {\n    text: String,\n    subwords: Vec<Box<dyn Subword>>,\n}\n\nimpl Subword for ExtGlob {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n    fn get_child_subwords(&self) -> Vec<Box<dyn Subword>> {\n        self.subwords.clone()\n    }\n    fn is_extglob(&self) -> bool {\n        true\n    }\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n}\n\nimpl ExtGlob {\n    pub fn new() -> ExtGlob {\n        ExtGlob {\n            text: String::new(),\n            subwords: vec![],\n        }\n    }\n\n    fn set_simple_subword(feeder: &mut Feeder, ans: &mut Self, len: usize) -> bool {\n        if len == 0 {\n            return false;\n        }\n\n        let txt = feeder.consume(len);\n        ans.text += &txt;\n        ans.subwords.push(From::from(&txt));\n        true\n    }\n\n    fn eat_braced_param(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(a) = BracedParam::parse(feeder, core)? {\n            ans.text += a.get_text();\n            ans.subwords.push(Box::new(a));\n            Ok(true)\n        } else {\n            Ok(false)\n        }\n    }\n\n    fn eat_command_substitution(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(a) = CommandSubstitution::parse(feeder, core)? {\n            ans.text += a.get_text();\n            ans.subwords.push(Box::new(a));\n            Ok(true)\n        } else {\n            Ok(false)\n        }\n    }\n\n    fn eat_special_or_positional_param(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> bool {\n        if let Some(a) = Parameter::parse(feeder, core) {\n            ans.text += a.get_text();\n            ans.subwords.push(Box::new(a));\n            return true;\n        }\n\n        false\n    }\n\n    fn eat_extglob(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if let Some(a) = Self::parse(feeder, core)? {\n            ans.text += a.get_text();\n            ans.subwords.push(Box::new(a));\n            Ok(true)\n        } else {\n            Ok(false)\n        }\n    }\n\n    fn eat_doller(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        match feeder.starts_with(\"$\") {\n            true => Self::set_simple_subword(feeder, ans, 1),\n            false => false,\n        }\n    }\n\n    fn eat_symbol(feeder: &mut Feeder, ans: &mut Self) -> bool {\n        let len = feeder.scanner_subword_symbol();\n        Self::set_simple_subword(feeder, ans, len)\n    }\n\n    fn eat_escaped_char(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        if feeder.starts_with(\"\\\\$\") || feeder.starts_with(\"\\\\\\\\\") {\n            let txt = feeder.consume(2);\n            ans.text += &txt;\n            ans.subwords.push(Box::new(EscapedChar { text: txt }));\n            return true;\n        }\n        let len = feeder.scanner_escaped_char(core);\n        Self::set_simple_subword(feeder, ans, len)\n    }\n\n    fn eat_name(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_name(core);\n        if len == 0 {\n            return false;\n        }\n\n        let txt = feeder.consume(len);\n        ans.text += &txt;\n        ans.subwords.push(Box::new(VarName { text: txt }));\n        true\n    }\n\n    fn eat_other(feeder: &mut Feeder, ans: &mut Self, core: &mut ShellCore) -> bool {\n        let len = feeder.scanner_extglob_subword(core);\n        Self::set_simple_subword(feeder, ans, len)\n    }\n\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !core.shopts.query(\"extglob\") || feeder.scanner_extglob_head() == 0 {\n            return Ok(None);\n        }\n\n        let mut ans = Self::new();\n        ans.text = feeder.consume(2);\n        ans.subwords.push(From::from(&ans.text));\n\n        loop {\n            while Self::eat_braced_param(feeder, &mut ans, core)?\n                || Self::eat_command_substitution(feeder, &mut ans, core)?\n                || Self::eat_extglob(feeder, &mut ans, core)?\n                || Self::eat_special_or_positional_param(feeder, &mut ans, core)\n                || Self::eat_doller(feeder, &mut ans)\n                || Self::eat_escaped_char(feeder, &mut ans, core)\n                || Self::eat_name(feeder, &mut ans, core)\n                || Self::eat_symbol(feeder, &mut ans)\n                || Self::eat_other(feeder, &mut ans, core)\n            {}\n\n            if feeder.starts_with(\")\") {\n                ans.text += &feeder.consume(1);\n                ans.subwords.push(From::from(\")\"));\n                return Ok(Some(ans));\n            } else if feeder.starts_with(\"|\") {\n                ans.text += &feeder.consume(1);\n                ans.subwords.push(From::from(\"|\"));\n            } else if !feeder.is_empty() {\n                exit::internal(\"unknown chars in double quoted word\");\n            } else if feeder.feed_additional_line(core).is_err() {\n                return Ok(None);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/file_input.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::command;\nuse crate::elements::io::redirect::Redirect;\nuse crate::elements::subword::Subword;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\nuse std::fs::File;\nuse std::io::{BufRead, BufReader};\n\n#[derive(Debug, Clone, Default)]\npub struct FileInput {\n    pub text: String,\n    redirect: Redirect,\n}\n\nimpl Subword for FileInput {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let args = self.redirect.right.eval(core)?;\n        if args.len() != 1 {\n            return Err(ExecError::AmbiguousRedirect(\n                self.redirect.right.text.clone(),\n            ));\n        }\n\n        let file = match File::open(&args[0]) {\n            Ok(f) => f,\n            Err(e) => return Err(ExecError::Other(e.to_string())),\n        };\n        let reader = BufReader::new(file);\n        self.text.clear();\n        for line in reader.lines() {\n            match line {\n                Ok(ln) => {\n                    self.text += &ln;\n                    self.text += \" \";\n                }\n                Err(e) => return Err(ExecError::Other(e.to_string())),\n            }\n        }\n\n        self.text.pop();\n        Ok(())\n    }\n}\n\nimpl FileInput {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Result<Option<Self>, ParseError> {\n        if !feeder.starts_with(\"$(\") {\n            return Ok(None);\n        }\n        feeder.set_backup();\n        let mut ans = Self {\n            text: feeder.consume(2),\n            ..Default::default()\n        };\n\n        if let Err(e) = command::eat_blank_lines(feeder, core, &mut ans.text) {\n            feeder.rewind();\n            return Err(e);\n        }\n\n        let mut redirects = vec![];\n        if let Err(e) = command::eat_redirects(feeder, core, &mut redirects, &mut ans.text) {\n            feeder.rewind();\n            return Err(e);\n        }\n\n        if redirects.len() != 1 || redirects[0].symbol != \"<\" {\n            feeder.rewind();\n            return Ok(None);\n        }\n\n        if let Err(e) = command::eat_blank_lines(feeder, core, &mut ans.text) {\n            feeder.rewind();\n            return Err(e);\n        }\n\n        ans.redirect = redirects.pop().unwrap();\n\n        if !feeder.starts_with(\")\") {\n            feeder.rewind();\n            return Ok(None);\n        }\n\n        ans.text += &feeder.consume(1);\n        feeder.pop_backup();\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/filler.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Subword;\n\n/* This subword disappers at word split. */\n#[derive(Debug, Clone)]\npub struct FillerSubword {\n    pub text: String,\n}\n\nimpl Subword for FillerSubword {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn set_text(&mut self, text: &str) {\n        self.text = text.to_string();\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/parameter.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Subword;\n//use crate::elements::substitution::variable::Variable;\nuse crate::error::exec::ExecError;\nuse crate::utils::splitter;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct Parameter {\n    pub text: String,\n    pub array: Option<Vec<String>>,\n}\n\nimpl Subword for Parameter {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if !self.text.starts_with(\"$\") {\n            return Ok(());\n        }\n\n        if self.text == \"$*\" || self.text == \"$@\" {\n            self.array = Some(core.db.get_position_params());\n        }\n        \n        self.text = core.db.get_param(&self.text[1..])?;\n        Ok(())\n    }\n\n    fn is_array(&self) -> bool {\n        self.text == \"$@\"\n    }\n\n    fn split(&self, ifs: &str, prev_char: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        if self.text.is_empty() {\n            return vec![];\n        }\n\n        if ifs.contains(\" \") || self.array.is_none() {\n            //TODO: add \\t and \\n ?\n            return splitter::split(self.get_text(), ifs, prev_char)\n                .iter()\n                .map(|s| (From::from(&s.0), s.1))\n                .collect();\n        }\n\n        let mut ans = vec![];\n        for p in self.array.clone().unwrap() {\n            ans.push((From::from(&p), true));\n        }\n        ans\n    }\n\n    fn is_simple_param(&self) -> bool {\n        true\n    }\n}\n\nimpl Parameter {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Self> {\n        match feeder.scanner_dollar_special_and_positional_param(core) {\n            0 => None,\n            n => {\n                let ans = Self {\n                    text: feeder.consume(n),\n                    ..Default::default()\n                };\n                Some(ans)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/paren.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{BracedParam, EscapedChar, Parameter, Subword, VarName};\nuse crate::elements::subword::{Arithmetic, CommandSubstitution, DoubleQuoted};\nuse crate::elements::word::{Word, WordMode};\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone, Default)]\npub struct EvalLetParen {\n    text: String,\n    subwords: Vec<Box<dyn Subword>>,\n}\n\nimpl Subword for EvalLetParen {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.connect_array(core)?;\n\n        let word = Word::from(self.subwords.clone());\n        self.text = word.eval_as_value(core)?;\n        Ok(())\n    }\n\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n}\n\nimpl EvalLetParen {\n    fn connect_array(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        for sw in self.subwords.iter_mut() {\n            if sw.get_text() == \"$*\" || sw.get_text() == \"${*}\" {\n                let params = core.db.get_position_params();\n                let joint = core.db.get_ifs_head();\n                *sw = From::from(&params.join(&joint));\n            }\n        }\n        Ok(())\n    }\n\n    fn eat_element(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        let sw: Box<dyn Subword> = if let Some(a) = BracedParam::parse(feeder, core)? {\n            Box::new(a)\n        } else if let Some(a) = DoubleQuoted::parse(feeder, core, &None)? {\n            Box::new(a)\n        } else if let Some(a) = Arithmetic::parse(feeder, core)? {\n            Box::new(a)\n        } else if let Some(a) = CommandSubstitution::parse(feeder, core)? {\n            Box::new(a)\n        } else if let Some(a) = Parameter::parse(feeder, core) {\n            Box::new(a)\n        } else if let Some(a) = EscapedChar::parse(feeder, core) {\n            Box::new(a)\n        } else if let Some(a) = Self::parse_name(feeder, core) {\n            Box::new(a)\n        } else {\n            return Ok(false);\n        };\n\n        ans.text += sw.get_text();\n        ans.subwords.push(sw);\n        Ok(true)\n    }\n    /*\n    fn parse_escaped_char(feeder: &mut Feeder) -> Option<EscapedChar> {\n        if feeder.starts_with(\"\\\\$\") || feeder.starts_with(\"\\\\\\\\\")\n        || feeder.starts_with(\"\\\\\\\"\") || feeder.starts_with(\"\\\\`\") {\n            return Some(EscapedChar{ text: feeder.consume(2) });\n        }\n        None\n    }*/\n\n    fn parse_name(feeder: &mut Feeder, core: &mut ShellCore) -> Option<VarName> {\n        match feeder.scanner_name(core) {\n            0 => None,\n            n => Some(VarName {\n                text: feeder.consume(n),\n            }),\n        }\n    }\n\n    fn eat_char(\n        feeder: &mut Feeder,\n        ans: &mut Self,\n        core: &mut ShellCore,\n    ) -> Result<bool, ParseError> {\n        if feeder.starts_with(\")\") {\n            ans.text += &feeder.consume(1);\n            ans.subwords.push(From::from(\")\"));\n            return Ok(false);\n        }\n\n        let len = feeder.scanner_char();\n        if len == 0 {\n            feeder.feed_additional_line(core)?;\n            return Ok(true);\n        }\n\n        let ch = feeder.consume(len);\n        ans.text += &ch.clone();\n        ans.subwords.push(From::from(&ch));\n        Ok(true)\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        mode: &Option<WordMode>,\n    ) -> Result<Option<Self>, ParseError> {\n        match mode {\n            Some(WordMode::EvalLet) => {}\n            _ => return Ok(None),\n        }\n\n        if !feeder.starts_with(\"(\") {\n            return Ok(None);\n        }\n\n        let mut ans = Self::default();\n\n        while Self::eat_element(feeder, &mut ans, core)? || Self::eat_char(feeder, &mut ans, core)?\n        {\n        }\n\n        Ok(Some(ans))\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/process_sub.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::command::paren::ParenCommand;\nuse crate::elements::command::Command;\nuse crate::elements::subword::Subword;\nuse crate::elements::word::WordMode;\nuse crate::elements::Pipe;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{Feeder, ShellCore};\nuse nix::unistd;\n\n#[derive(Debug, Clone, Default)]\npub struct ProcessSubstitution {\n    pub text: String,\n    command: ParenCommand,\n    pub direction: char,\n    pipe: Option<Pipe>,\n}\n\nimpl Subword for ProcessSubstitution {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn substitute(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        if self.direction == '>' {\n            return self.substitute_in(core);\n        }\n\n        let mut pipe = Pipe::new(\"|\".to_string());\n        pipe.set(-1, unistd::getpgrp(), core);\n        let pid = self.command.exec(core, &mut pipe)?.unwrap();\n        core.proc_sub_pid.push(pid);\n        self.text = \"/dev/fd/\".to_owned() + &pipe.recv.to_string();\n        Ok(())\n    }\n\n    fn set_pipe(&mut self, core: &mut ShellCore) {\n        if self.direction == '>' {\n            self.pipe = Some(Pipe::new(\">()\".to_string()));\n            self.pipe.as_mut().unwrap().set(-1, unistd::getpgrp(), core);\n        }\n    }\n\n    fn is_to_proc_sub(&self) -> bool {\n        self.text.starts_with(\">(\")\n    }\n}\n\nimpl ProcessSubstitution {\n    fn substitute_in(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let pipe = self.pipe.as_mut().unwrap();\n        let pid = self.command.exec(core, pipe)?.unwrap();\n        core.proc_sub_pid.push(pid);\n        core.proc_sub_fd.push(pipe.proc_sub_send);\n        self.text = \"/dev/fd/\".to_owned() + &pipe.proc_sub_send.to_string();\n\n        Ok(())\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        mode: &Option<WordMode>,\n    ) -> Result<Option<Self>, ParseError> {\n        if let Some(WordMode::Arithmetic) = mode {\n            return Ok(None);\n        }\n\n        if !feeder.starts_with(\"<(\") && !feeder.starts_with(\">(\") {\n            return Ok(None);\n        }\n        let mut ans = ProcessSubstitution::default();\n        ans.text = feeder.consume(1);\n        ans.direction = ans.text.chars().nth(0).unwrap();\n\n        if let Some(pc) = ParenCommand::parse(feeder, core, true)? {\n            ans.text += &pc.get_text();\n            ans.command = pc;\n            return Ok(Some(ans));\n        }\n\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/simple.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Subword;\nuse crate::Feeder;\n\n#[derive(Debug, Clone)]\npub struct SimpleSubword {\n    pub text: String,\n}\n\nimpl Subword for SimpleSubword {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn set_text(&mut self, text: &str) {\n        self.text = text.to_string();\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n}\n\nimpl SimpleSubword {\n    pub fn parse(feeder: &mut Feeder) -> Option<Self> {\n        let len = feeder.scanner_subword_symbol();\n        if len > 0 {\n            return Some(Self {\n                text: feeder.consume(len),\n            });\n        }\n\n        let len = feeder.scanner_subword();\n        if len > 0 {\n            return Some(Self {\n                text: feeder.consume(len),\n            });\n        }\n\n        None\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/single_quoted.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Subword;\nuse crate::{Feeder, ShellCore};\nuse crate::elements::word::WordMode;\n\n#[derive(Debug, Clone)]\npub struct SingleQuoted {\n    pub text: String,\n}\n\nimpl Subword for SingleQuoted {\n    fn get_text(&self) -> &str {\n        &self.text\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n\n    fn make_unquoted_string(&mut self) -> Option<String> {\n        Some(self.text[1..self.text.len() - 1].to_string())\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        self.text[1..self.text.len() - 1]\n            .replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(\"*\", \"\\\\*\")\n            .replace(\"?\", \"\\\\?\")\n            .replace(\"[\", \"\\\\[\")\n            .replace(\"]\", \"\\\\]\")\n    }\n\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n}\n\nimpl SingleQuoted {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore,\n                 mode: &Option<WordMode>) -> Option<Self> {\n        if let Some(WordMode::ParamOption(_)) = mode {\n            if core.options.query(\"posix\") {\n                return None;\n            }\n        }\n        \n        match feeder.scanner_single_quoted_subword(core) {\n            0 => None,\n            n => {\n                let s = feeder.consume(n);\n                Some(SingleQuoted { text: s })\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword/varname.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::Subword;\nuse crate::{Feeder, ShellCore};\n\n#[derive(Debug, Clone)]\npub struct VarName {\n    pub text: String,\n}\n\nimpl Subword for VarName {\n    fn get_text(&self) -> &str {\n        self.text.as_ref()\n    }\n    fn set_text(&mut self, text: &str) {\n        self.text = text.to_string();\n    }\n    fn boxed_clone(&self) -> Box<dyn Subword> {\n        Box::new(self.clone())\n    }\n    fn is_name(&self) -> bool {\n        true\n    }\n    fn split(&self, _: &str, _: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        vec![]\n    }\n}\n\nimpl VarName {\n    pub fn parse(feeder: &mut Feeder, core: &mut ShellCore) -> Option<Self> {\n        match feeder.scanner_name(core) {\n            0 => None,\n            n => Some(Self {\n                text: feeder.consume(n),\n            }),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements/subword.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod ansi_c_quoted;\npub mod braced_param;\nmod command_sub;\npub mod simple;\npub mod single_quoted;\n//mod command_sub_old;\nmod arithmetic;\nmod double_quoted;\npub mod escaped_char;\nmod ext_glob;\nmod file_input;\npub mod filler;\npub mod parameter;\nmod paren;\nmod process_sub;\nmod varname;\n\nuse self::ansi_c_quoted::AnsiCQuoted;\nuse self::arithmetic::Arithmetic;\nuse self::braced_param::BracedParam;\nuse self::command_sub::CommandSubstitution;\nuse self::simple::SimpleSubword;\nuse crate::elements::word::WordMode;\nuse crate::error::{exec::ExecError, parse::ParseError};\nuse crate::utils::splitter;\nuse crate::{Feeder, ShellCore};\n//use self::command_sub_old::CommandSubstitutionOld;\nuse self::double_quoted::DoubleQuoted;\nuse self::escaped_char::EscapedChar;\nuse self::ext_glob::ExtGlob;\nuse self::file_input::FileInput;\nuse self::filler::FillerSubword;\nuse self::parameter::Parameter;\nuse self::paren::EvalLetParen;\nuse self::process_sub::ProcessSubstitution;\nuse self::single_quoted::SingleQuoted;\nuse self::varname::VarName;\nuse std::fmt;\nuse std::fmt::Debug;\n\nimpl Debug for dyn Subword {\n    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {\n        fmt.debug_struct(self.get_text()).finish()\n    }\n}\n\nimpl Clone for Box<dyn Subword> {\n    fn clone(&self) -> Box<dyn Subword> {\n        self.boxed_clone()\n    }\n}\n\nimpl Default for Box<dyn Subword> {\n    fn default() -> Box<dyn Subword> {\n        Box::new(SimpleSubword {\n            text: \"\".to_string(),\n        })\n    }\n}\n\nimpl From<&String> for Box<dyn Subword> {\n    fn from(s: &String) -> Box<dyn Subword> {\n        Box::new(SimpleSubword { text: s.clone() })\n    }\n}\n\nimpl From<&str> for Box<dyn Subword> {\n    fn from(s: &str) -> Box<dyn Subword> {\n        Box::new(SimpleSubword {\n            text: s.to_string(),\n        })\n    }\n}\n\npub trait Subword {\n    fn get_text(&self) -> &str;\n    fn set_text(&mut self, _: &str) {}\n    fn boxed_clone(&self) -> Box<dyn Subword>;\n    fn substitute(&mut self, _: &mut ShellCore) -> Result<(), ExecError> {\n        Ok(())\n    }\n    fn alter(&mut self) -> Result<Vec<Box<dyn Subword>>, ExecError> {\n        Ok(vec![])\n    }\n\n    fn split(&self, ifs: &str, prev_char: Option<char>) -> Vec<(Box<dyn Subword>, bool)> {\n        //bool: true if it should remain\n        splitter::split(self.get_text(), ifs, prev_char)\n            .iter()\n            .map(|s| (From::from(&s.0), s.1))\n            .collect()\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        self.get_text().to_string()\n    }\n\n    fn make_unquoted_string(&mut self) -> Option<String> {\n        match self.get_text() {\n            \"\" => None,\n            s => Some(s.to_string()),\n        }\n    }\n\n    fn make_regex(&mut self) -> Option<String> {\n        match self.get_text() {\n            \"\" => None,\n            s => Some(s.to_string()),\n        }\n    }\n\n    fn is_name(&self) -> bool {\n        false\n    }\n    fn is_array(&self) -> bool {\n        false\n    }\n    fn get_elem(&mut self) -> Vec<String> {\n        vec![]\n    }\n    fn is_extglob(&self) -> bool {\n        false\n    }\n    fn is_escaped_char(&self) -> bool {\n        false\n    }\n    fn is_to_proc_sub(&self) -> bool {\n        false\n    }\n    fn is_simple_param(&self) -> bool {\n        false\n    }\n    fn get_child_subwords(&self) -> Vec<Box<dyn Subword>> {\n        vec![]\n    }\n    fn set_heredoc_flag(&mut self) {}\n\n    fn set_pipe(&mut self, _: &mut ShellCore) {}\n}\n\nfn replace_history_expansion(feeder: &mut Feeder, core: &mut ShellCore) -> bool {\n    let len = feeder.scanner_history_expansion(core);\n    if len == 0 {\n        return false;\n    }\n\n    let history_len = core.history.len();\n    if history_len < 2 {\n        feeder.replace(len, \"\");\n        return true;\n    }\n\n    let mut his = String::new();\n    for h in &core.history[1..] {\n        let last = h.split(\" \").last().unwrap();\n\n        if !last.starts_with(\"!$\") {\n            his = last.to_string();\n            break;\n        }\n    }\n\n    feeder.replace(len, &his);\n    true\n}\n\nfn last_resort(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    mode: &Option<WordMode>,\n) -> Result<Option<Box<dyn Subword>>, ParseError> {\n    match mode {\n        None => Ok(None),\n        Some(WordMode::ParamOption(v)) => {\n            if feeder.is_empty() || feeder.starts_withs(v) {\n                return Ok(None);\n            }\n\n            let len = feeder.scanner_char();\n            let c = FillerSubword {\n                text: feeder.consume(len),\n            };\n            if feeder.is_empty() {\n                feeder.feed_additional_line(core)?;\n            }\n            Ok(Some(Box::new(c)))\n        }\n        Some(WordMode::ReadCommand) => {\n            if feeder.is_empty() || feeder.starts_withs(&[\"\\n\", \"\\t\", \" \"]) {\n                Ok(None)\n            } else {\n                Ok(Some(From::from(&feeder.consume(1))))\n            }\n        }\n        Some(WordMode::Alias) => {\n            if feeder.starts_with(\"\\t\") {\n                Ok(Some(From::from(&feeder.consume(1))))\n            } else {\n                Ok(None)\n            }\n        }\n        /*\n        Some(WordMode::AlterWord) => {\n            if feeder.is_empty() || feeder.starts_with(\"}\") {\n                return Ok(None);\n            }\n\n            let len = feeder.scanner_char();\n            let c = FillerSubword {\n                text: feeder.consume(len),\n            };\n            if feeder.is_empty() {\n                feeder.feed_additional_line(core)?;\n            }\n            Ok(Some(Box::new(c)))\n        }*/\n        Some(WordMode::AssocIndex) => {\n            if !feeder.starts_with(\"]\") {\n                Ok(Some(From::from(&feeder.consume(1))))\n            } else {\n                Ok(None)\n            }\n        }\n        Some(WordMode::PermitAnyChar) => {\n            if feeder.is_empty() {\n                Ok(None)\n            } else {\n                Ok(Some(From::from(&feeder.consume(1))))\n            }\n        }\n        _ => Ok(None),\n    }\n}\n\npub fn parse(\n    feeder: &mut Feeder,\n    core: &mut ShellCore,\n    mode: &Option<WordMode>,\n) -> Result<Option<Box<dyn Subword>>, ParseError> {\n    if replace_history_expansion(feeder, core) {\n        return parse(feeder, core, mode);\n    }\n\n    if let Some(a) = BracedParam::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = AnsiCQuoted::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Arithmetic::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = FileInput::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = CommandSubstitution::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    }\n    //else if let Some(a) = CommandSubstitutionOld::parse(feeder, core)?{ Ok(Some(Box::new(a))) }\n    else if let Some(a) = ProcessSubstitution::parse(feeder, core, mode)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = SingleQuoted::parse(feeder, core, mode) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = DoubleQuoted::parse(feeder, core, mode)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = ExtGlob::parse(feeder, core)? {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = EscapedChar::parse(feeder, core) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = Parameter::parse(feeder, core) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = VarName::parse(feeder, core) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = SimpleSubword::parse(feeder) {\n        Ok(Some(Box::new(a)))\n    } else if let Some(a) = EvalLetParen::parse(feeder, core, mode)? {\n        Ok(Some(Box::new(a)))\n    } else {\n        last_resort(feeder, core, mode)\n    }\n}\n"
  },
  {
    "path": "src/elements/word/brace_expansion.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::single_quoted::SingleQuoted;\nuse crate::elements::subword::Subword;\nuse crate::elements::word::Word;\n\nenum BraceType {\n    Comma,\n    Range(usize),\n}\n\nfn after_dollar(s: &str) -> bool {\n    s == \"$\" || s == \"$$\"\n}\n\nfn num_to_subword(n: i128) -> Box<dyn Subword> {\n    Box::new(SingleQuoted {\n        text: format!(\"'{n}'\"),\n    })\n}\n\nfn ascii_to_subword(c: char) -> Box<dyn Subword> {\n    let table = vec![\n        \"^?\", \"\\\\M-^@\", \"\\\\M-^A\", \"\\\\M-^B\", \"\\\\M-^C\", \"\\\\M-^D\", \"\\\\M-^E\", \"\\\\M-^F\", \"\\\\M-^G\",\n        \"\\\\M-^H\", \"\\\\M-\\t\", \"\\\\M-\\n\", \"\\\\M-^K\", \"\\\\M-^L\", \"\\\\M-^M\", \"\\\\M-^N\", \"\\\\M-^O\", \"\\\\M-^P\",\n        \"\\\\M-^Q\", \"\\\\M-^R\", \"\\\\M-^S\", \"\\\\M-^T\", \"\\\\M-^U\", \"\\\\M-^V\", \"\\\\M-^W\", \"\\\\M-^X\", \"\\\\M-^Y\",\n        \"\\\\M-^Z\", \"\\\\M-^[\", \"\\\\M-^\\\\\", \"\\\\M-^]\", \"\\\\M-^^\", \"\\\\M-^_\", \" \", \"¡\",\n    ];\n\n    let n = c as usize;\n    let text = if n >= 127 && n < 127 + table.len() {\n        table[n - 127].to_string()\n    } else {\n        c.to_string()\n    };\n\n    Box::new(SingleQuoted {\n        text: format!(\"'{text}'\"),\n    })\n}\n\nfn connect_minus(subwords: &mut Vec<Box<dyn Subword>>) {\n    if subwords.len() < 2 {\n        return;\n    }\n\n    let mut pos = vec![];\n    for (i, sw) in subwords.iter().enumerate() {\n        if sw.get_text() == \"-\" {\n            pos.push(i);\n        }\n    }\n\n    for i in pos {\n        if i + 1 < subwords.len() {\n            let mut num = true;\n            for ch in subwords[i + 1].get_text().chars() {\n                if !ch.is_ascii_digit() {\n                    num = false;\n                    break;\n                }\n            }\n\n            if !num {\n                continue;\n            }\n\n            subwords[i] = Default::default();\n            let m_str = \"-\".to_owned() + subwords[i + 1].get_text();\n            subwords[i + 1] = From::from(&m_str);\n        }\n    }\n\n    subwords.retain(|e| !e.get_text().is_empty());\n}\n\npub fn eval(word: &mut Word, compat_bash: bool) -> Vec<Word> {\n    invalidate_brace(&mut word.subwords);\n    connect_minus(&mut word.subwords);\n\n    let mut skip_until = 0;\n    for i in word.scan_pos(\"{\") {\n        if i < skip_until {\n            //ブレース展開の終わりまで処理をスキップ\n            continue;\n        }\n\n        if let Some(d) = parse(&word.subwords[i..]) {\n            let shift_d: Vec<usize> = d.0.iter().map(|e| e + i).collect();\n\n            if i > 0 && after_dollar(word.subwords[i - 1].get_text()) {\n                skip_until = *shift_d.last().unwrap();\n                continue;\n            }\n\n            return match d.1 {\n                BraceType::Comma => expand_comma_brace(&word.subwords, &shift_d, compat_bash),\n                BraceType::Range(n) => {\n                    expand_range_brace(&mut word.subwords, &shift_d, n, compat_bash)\n                }\n            };\n        }\n    }\n\n    vec![word.clone()]\n}\n\nfn invalidate_brace(subwords: &mut Vec<Box<dyn Subword>>) {\n    if subwords.len() < 2 {\n        return;\n    }\n\n    if subwords[0].get_text() == \"{\" && subwords[1].get_text() == \"}\" {\n        subwords.remove(1);\n        subwords[0].set_text(\"{}\");\n    }\n}\n\nfn parse(subwords: &[Box<dyn Subword>]) -> Option<(Vec<usize>, BraceType)> {\n    let mut stack = vec![];\n    for sw in subwords {\n        stack.push(Some(sw.get_text()));\n        if sw.get_text() == \"}\" {\n            if let Some(found) = get_delimiters(&mut stack) {\n                return Some(found);\n            }\n        }\n    }\n    None\n}\n\nfn get_delimiters(stack: &mut [Option<&str>]) -> Option<(Vec<usize>, BraceType)> {\n    let mut comma_pos = vec![];\n    let mut period_pos = vec![];\n\n    for i in (1..stack.len() - 1).rev() {\n        if stack[i] == Some(\",\") {\n            comma_pos.push(i);\n        } else if stack[i] == Some(\".\") {\n            period_pos.push(i);\n        } else if stack[i] == Some(\"{\") {\n            // find an inner brace expcomma_posion\n            stack[i..].iter_mut().for_each(|e| *e = None);\n            return None;\n        }\n    }\n\n    if !comma_pos.is_empty() {\n        comma_pos.reverse();\n        comma_pos.insert(0, 0); // add \"{\" pos\n        comma_pos.push(stack.len() - 1); // add \"}\" pos\n        return Some((comma_pos, BraceType::Comma));\n    }\n\n    if period_pos.len() == 2 && period_pos[0] == period_pos[1] + 1 {\n        period_pos.reverse();\n        period_pos.insert(0, 0);\n        period_pos.push(stack.len() - 1);\n        return Some((period_pos, BraceType::Range(2)));\n    }\n\n    if period_pos.len() == 4\n        && period_pos[0] == period_pos[1] + 1\n        && period_pos[2] == period_pos[3] + 1\n    {\n        period_pos.reverse();\n        period_pos.insert(0, 0);\n        period_pos.push(stack.len() - 1);\n        return Some((period_pos, BraceType::Range(3)));\n    }\n\n    None\n}\n\nfn comma_brace_to_subwords(\n    subwords: &[Box<dyn Subword>],\n    delimiters: &[usize],\n) -> Vec<Vec<Box<dyn Subword>>> {\n    let mut ans = vec![];\n    let mut from = delimiters[0] + 1;\n    for to in &delimiters[1..] {\n        ans.push(subwords[from..*to].to_vec());\n        from = *to + 1;\n    }\n    ans\n}\n\nfn expand_comma_brace(\n    subwords: &[Box<dyn Subword>],\n    delimiters: &[usize],\n    compat_bash: bool,\n) -> Vec<Word> {\n    let left = subwords[..delimiters[0]].to_vec();\n    let mut right = subwords[(delimiters.last().unwrap() + 1)..].to_vec();\n    invalidate_brace(&mut right);\n\n    let sws = comma_brace_to_subwords(subwords, delimiters);\n    subword_sets_to_words(&sws, &left, &right, compat_bash)\n}\n\nfn expand_range_brace(\n    subwords: &mut Vec<Box<dyn Subword>>,\n    delimiters: &[usize],\n    operand_num: usize,\n    compat_bash: bool,\n) -> Vec<Word> {\n    let start_wrap = subwords[delimiters[0] + 1].make_unquoted_string(); // right of {\n    let end_wrap = subwords[delimiters[2] + 1].make_unquoted_string(); // left of }\n\n    let (start, end) = match (start_wrap, end_wrap) {\n        (Some(s), Some(e)) => (s, e),\n        _ => return subwords_to_word(subwords),\n    };\n\n    let mut skip_num = match operand_num {\n        2 => 1,\n        3 => {\n            let skip = subwords[delimiters[4] + 1].get_text();\n            match skip.parse::<i32>() {\n                Ok(n) => n.unsigned_abs() as usize,\n                _ => return subwords_to_word(subwords),\n            }\n        }\n        _ => return subwords_to_word(subwords),\n    };\n    skip_num = std::cmp::max(skip_num, 1);\n\n    let mut series = gen_nums(&start, &end, skip_num);\n    if series.is_empty() {\n        series = gen_chars(&start, &end, skip_num, compat_bash);\n    }\n    if series.is_empty() {\n        return subwords_to_word(subwords);\n    }\n\n    if compat_bash {\n        for e in series.iter_mut() {\n            if e.get_text() == \"'\\\\'\" {\n                *e = Box::new(SingleQuoted {\n                    text: \"''\".to_string(),\n                });\n            }\n        }\n    }\n\n    let mut series2 = vec![];\n    for e in series {\n        series2.push(vec![e]);\n    }\n\n    let left = &subwords[..delimiters[0]];\n    let mut right = subwords[(delimiters.last().unwrap() + 1)..].to_vec();\n    invalidate_brace(&mut right);\n\n    subword_sets_to_words(&series2, left, &right, compat_bash)\n}\n\nfn gen_nums(start: &str, end: &str, skip: usize) -> Vec<Box<dyn Subword>> {\n    let (start_num, end_num) = match (start.parse::<i128>(), end.parse::<i128>()) {\n        (Ok(s), Ok(e)) => (s, e),\n        _ => return vec![],\n    };\n\n    let min = std::cmp::min(start_num, end_num);\n    let max = std::cmp::max(start_num, end_num);\n\n    let mut ans: Vec<Box<dyn Subword>> = (min..(max + 1)).map(|n| num_to_subword(n)).collect();\n    if start_num > end_num {\n        ans.reverse();\n    }\n    ans.into_iter()\n        .enumerate()\n        .filter(|e| e.0 % skip == 0)\n        .map(|e| e.1)\n        .collect()\n}\n\nfn gen_chars(start: &str, end: &str, skip: usize, compat_bash: bool) -> Vec<Box<dyn Subword>> {\n    let (start_num, end_num) = match (start.chars().next(), end.chars().next()) {\n        (Some(s), Some(e)) => (s, e),\n        _ => return vec![],\n    };\n\n    if start.chars().count() > 1 || end.chars().count() > 1 {\n        return vec![];\n    }\n\n    let min = std::cmp::min(start_num, end_num);\n    let max = std::cmp::max(start_num, end_num);\n\n    if compat_bash {\n        if min.is_ascii_digit() && !max.is_ascii_digit() {\n            return vec![];\n        }\n        if max.is_ascii_digit() && !min.is_ascii_digit() {\n            return vec![];\n        }\n    }\n\n    let mut ans: Vec<Box<dyn Subword>> = (min..max).map(|n| ascii_to_subword(n)).collect();\n    ans.push(ascii_to_subword(max));\n    if start_num > end_num {\n        ans.reverse();\n    }\n\n    ans.into_iter()\n        .enumerate()\n        .filter(|e| e.0 % skip == 0)\n        .map(|e| e.1)\n        .collect()\n}\n\nfn subword_sets_to_words(\n    series: &[Vec<Box<dyn Subword>>],\n    left: &[Box<dyn Subword>],\n    right: &[Box<dyn Subword>],\n    compat_bash: bool,\n) -> Vec<Word> {\n    let mut ws = vec![];\n    for sws in series {\n        let mut w = Word::default();\n        w.subwords = [left, sws, right].concat();\n        w.text = w.subwords.iter().map(|s| s.get_text()).collect();\n        ws.push(w);\n    }\n\n    let mut ans = vec![];\n    for w in ws.iter_mut() {\n        ans.append(&mut eval(w, compat_bash));\n    }\n    ans\n}\n\nfn subwords_to_word(subwords: &[Box<dyn Subword>]) -> Vec<Word> {\n    let mut w = Word::default();\n    w.subwords = subwords.to_vec();\n    w.text = w.subwords.iter().map(|s| s.get_text()).collect();\n    vec![w]\n}\n"
  },
  {
    "path": "src/elements/word/path_expansion.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::options::Options;\nuse crate::elements::word::Word;\nuse crate::utils::directory;\n\npub fn eval(word: &mut Word, shopts: &Options) -> Vec<Word> {\n    let globstr = word.make_glob_string();\n    if no_glob_symbol(&globstr) {\n        return vec![word.clone()];\n    }\n\n    let paths = expand(&globstr, shopts);\n    if paths.is_empty() {\n        if shopts.query(\"nullglob\") {\n            return vec![Word::from(\"\")];\n        }\n        return vec![word.clone()];\n    }\n\n    paths.iter().map(|s| Word::from(s.as_str())).collect()\n}\n\nfn no_glob_symbol(pattern: &str) -> bool {\n    \"*?@+![\".chars().all(|c| !pattern.contains(c))\n}\n\npub fn expand(pattern: &str, shopts: &Options) -> Vec<String> {\n    let mut paths = vec![\"\".to_string()];\n\n    for dir_glob in pattern.split(\"/\") {\n        let mut tmp = paths\n            .iter()\n            .map(|c| directory::glob(c, dir_glob, shopts))\n            .collect::<Vec<Vec<String>>>()\n            .concat();\n\n        if dir_glob == \"**\" && shopts.query(\"globstar\") {\n            tmp.append(&mut paths);\n        }\n        paths = tmp;\n\n        paths.sort();\n        paths.dedup();\n    }\n\n    paths.iter_mut().for_each(|e| {\n        e.pop();\n    });\n\n    if shopts.query(\"globstar\") {\n        if let Some(ptn) = pattern.strip_suffix(\"/**\") {\n            paths.iter_mut().for_each(|p| {\n                if p == ptn {\n                    *p += \"/\";\n                }\n            });\n        }\n    }\n\n    paths.sort();\n    paths\n}\n"
  },
  {
    "path": "src/elements/word/split.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::Subword;\nuse crate::elements::word::Word;\nuse crate::ShellCore;\ntype SplitResult = (usize, Vec<(Box<dyn Subword>, bool)>);\n\npub fn eval(word: &Word, core: &mut ShellCore) -> Vec<Word> {\n    if !core.db.exist(\"IFS\") {\n        let _ = core.db.set_param(\"IFS\", \" \\t\\n\", None);\n    }\n\n    let ifs = core.db.get_param(\"IFS\").unwrap();\n\n    let (pos, mut split) = find_pos(word, &ifs);\n    if split.is_empty() {\n        return vec![word.clone()];\n    }\n\n    if split.len() == 1 {\n        /*\n        if word.subwords[pos].get_text() != split[0].0.get_text() {\n            let mut w = word.clone();\n            w.subwords[pos] = split[0].0.clone();\n            w.do_not_erase = split[0].1;\n            return vec![w];\n        }*/\n\n        return vec![word.clone()];\n    }\n\n    let mut left = word.subwords[..pos].to_vec();\n    let remain = split[0].1;\n    left.push(split.remove(0).0);\n    let mut ans = vec![gen_word(left, remain)];\n\n    while split.len() >= 2 {\n        let remain = split[0].1;\n        ans.push(gen_word(vec![split.remove(0).0], remain));\n    }\n\n    let remain = split[0].1;\n    let mut right = gen_word(word.subwords[pos + 1..].to_vec(), remain);\n    right.subwords.insert(0, split.remove(0).0);\n\n    [ans, eval(&right, core)].concat()\n}\n\nfn gen_word(sws: Vec<Box<dyn Subword>>, remain: bool) -> Word {\n    Word {\n        subwords: sws,\n        do_not_erase: remain,\n        ..Default::default()\n    }\n}\n\npub fn find_pos(word: &Word, ifs: &str) -> SplitResult {\n    let mut prev_char = None;\n    for (i, sw) in word.subwords.iter().enumerate() {\n        let split = sw.split(ifs, prev_char);\n        if split.len() >= 2 {\n            return (i, split);\n        }\n\n        if !sw.get_text().is_empty() {\n            prev_char = sw.get_text().chars().last();\n        }\n    }\n    (0, vec![])\n}\n"
  },
  {
    "path": "src/elements/word/substitution.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::subword::Subword;\nuse crate::elements::subword::braced_param::BracedParam;\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\n\npub fn eval(word: &mut Word, core: &mut ShellCore) -> Result<(), ExecError> {\n    for i in word.scan_pos(\"$\") {\n        connect_names(&mut word.subwords[i..]);\n    }\n    let mut tmp = vec![];\n    for w in word.subwords.iter_mut() {\n        w.substitute(core)?;\n        let mut new_objs = w.alter()?;\n        match new_objs.is_empty() {\n            true => tmp.push(w.clone()),\n            false => tmp.append(&mut new_objs),\n        }\n    }\n\n    word.subwords = tmp;\n    word.text = word\n        .subwords\n        .iter()\n        .map(|sw| sw.get_text().to_string())\n        .collect::<Vec<String>>()\n        .join(\"\");\n    Ok(())\n}\n\nfn connect_names(subwords: &mut [Box<dyn Subword>]) {\n    let mut text = \"$\".to_string();\n    let mut pos = 1;\n    for s in &mut subwords[1..] {\n        if !s.is_name() {\n            break;\n        }\n        text += s.get_text();\n        pos += 1;\n    }\n\n    if pos > 1 {\n        let sw = BracedParam::from(&text[1..]);\n        subwords[0] = Box::new(sw);\n        subwords[1..pos].iter_mut().for_each(|s| s.set_text(\"\"));\n    }\n}\n"
  },
  {
    "path": "src/elements/word/tilde_expansion.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::elements::word::Word;\nuse crate::error::exec::ExecError;\nuse crate::ShellCore;\n//use crate::elements::subword::Subword;\nuse super::WordMode;\nuse nix::unistd::User;\n\npub fn eval(word: &mut Word, core: &mut ShellCore) {\n    if word.subwords.len() > 1\n        && word.subwords[1..].iter().any(|sw| sw.get_text() == \"=\")\n        && !core.options.query(\"posix\")\n    {\n        let permit_equal = word.mode.is_none();\n        return eval_multi(word, core, permit_equal);\n    }\n    if let Some(WordMode::RightOfSubstitution) = word.mode {\n        return eval_multi(word, core, false);\n    }\n\n    eval_single(word, core)\n}\n\nfn eval_single(word: &mut Word, core: &mut ShellCore) {\n    let length = match prefix_length(word) {\n        0 => return,\n        n => n,\n    };\n\n    let text: String = word.subwords[1..length]\n        .iter()\n        .map(|e| e.get_text().to_string())\n        .collect::<Vec<String>>()\n        .concat();\n\n    let value = get_value(&text, core).unwrap_or_default();\n    if value.is_empty() {\n        return;\n    }\n    word.text = value.clone();\n    word.subwords[0] = From::from(&value);\n    word.subwords[1..length]\n        .iter_mut()\n        .for_each(|w| w.set_text(\"\"));\n}\n\npub fn eval_multi(word: &mut Word, core: &mut ShellCore, permit_equal: bool) {\n    let mut ans_sws = vec![];\n    let mut tmp = vec![];\n    let mut equal = 0;\n    for sw in &word.subwords {\n        if sw.get_text() == \"=\" {\n            equal += 1;\n        }\n\n        if sw.get_text() == \":\" || (permit_equal && sw.get_text() == \"=\" && equal < 2) {\n            let mut w = Word::from(tmp.clone());\n            eval_single(&mut w, core);\n            ans_sws.append(&mut w.subwords);\n            tmp.clear();\n            ans_sws.push(sw.clone());\n        } else {\n            tmp.push(sw.clone());\n        }\n    }\n\n    if !tmp.is_empty() {\n        let mut w = Word::from(tmp.clone());\n        eval_single(&mut w, core);\n        ans_sws.append(&mut w.subwords);\n    }\n\n    word.subwords = ans_sws;\n    word.text = word\n        .subwords\n        .iter()\n        .map(|e| e.get_text().to_string())\n        .collect::<Vec<String>>()\n        .concat();\n}\n\nfn prefix_length(word: &Word) -> usize {\n    if word.subwords.is_empty() || word.subwords[0].get_text() != \"~\" {\n        return 0;\n    }\n\n    let len_colon = match word.subwords.iter().position(|e| e.get_text() == \":\") {\n        None => word.subwords.len(),\n        Some(n) => n,\n    };\n\n    let len_slash = match word.subwords.iter().position(|e| e.get_text() == \"/\") {\n        None => word.subwords.len(),\n        Some(n) => n,\n    };\n\n    std::cmp::min(len_colon, len_slash)\n}\n\nfn get_value(text: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n    let key = match text {\n        \"\" => \"HOME\",\n        \"+\" => \"PWD\",\n        \"-\" => \"OLDPWD\",\n        _ => return Ok(get_home_dir(text)),\n    };\n\n    core.db.get_param(key)\n}\n\nfn get_home_dir(user: &str) -> String {\n    match User::from_name(user) {\n        Ok(Some(u)) => u.dir.into_os_string().into_string().unwrap(),\n        _ => String::new(),\n    }\n}\n"
  },
  {
    "path": "src/elements/word.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod brace_expansion;\npub mod path_expansion;\nmod split;\npub mod substitution;\npub mod tilde_expansion;\n\nuse super::subword::Subword;\nuse crate::elements::subword;\nuse crate::error::exec::ExecError;\nuse crate::error::parse::ParseError;\nuse crate::{utils, Feeder, ShellCore};\n\n#[derive(Debug, Clone)]\npub enum WordMode {\n    Alias,\n//    AlterWord,\n    Arithmetic,\n    AssocIndex,\n    EvalLet,\n    CompgenF,\n    ReadCommand,\n    Heredoc,\n    RightOfSubstitution,\n    Value,\n    PermitAnyChar,\n    //ReparseOfSubstitution,\n    ParamOption(Vec<String>),\n}\n\n#[derive(Debug, Clone, Default)]\npub struct Word {\n    pub text: String,\n    pub do_not_erase: bool,\n    pub subwords: Vec<Box<dyn Subword>>,\n    pub mode: Option<WordMode>,\n}\n\nimpl From<&str> for Word {\n    fn from(s: &str) -> Self {\n        Self {\n            text: s.to_string(),\n            subwords: vec![From::from(s)],\n            ..Default::default()\n        }\n    }\n}\n\nimpl From<Box<dyn Subword>> for Word {\n    fn from(subword: Box<dyn Subword>) -> Self {\n        Self {\n            text: subword.get_text().to_string(),\n            subwords: vec![subword],\n            ..Default::default()\n        }\n    }\n}\n\nimpl From<Vec<Box<dyn Subword>>> for Word {\n    fn from(subwords: Vec<Box<dyn Subword>>) -> Self {\n        Self {\n            text: subwords.iter().map(|s| s.get_text()).collect(),\n            subwords,\n            ..Default::default()\n        }\n    }\n}\n\nimpl Word {\n    pub fn eval(&mut self, core: &mut ShellCore) -> Result<Vec<String>, ExecError> {\n        let ws_after_brace_exp = match core.db.flags.contains('B') {\n            true => brace_expansion::eval(&mut self.clone(), core.compat_bash),\n            false => vec![self.clone()],\n        };\n\n        let mut ws = vec![];\n        for w in ws_after_brace_exp {\n            let expanded = w.tilde_and_dollar_expansion(core)?;\n            ws.append(&mut expanded.split_and_path_expansion(core));\n        }\n        Ok(Self::make_args(&mut ws))\n    }\n\n    pub fn eval_as_herestring(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        self.eval_as_value(core)\n    }\n\n    pub fn eval_as_value(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let w = self.tilde_and_dollar_expansion(core)?;\n        let mut ws = w.path_expansion(core);\n        let joint = core.db.get_ifs_head();\n        Ok(Self::make_args(&mut ws).join(&joint))\n    }\n\n    pub fn eval_as_alter(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let w = self.dollar_expansion(core)?;\n        let mut ws = w.path_expansion(core);\n        let joint = core.db.get_ifs_head();\n        Ok(Self::make_args(&mut ws).join(&joint))\n    }\n\n    pub fn eval_as_assoc_index(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let w = self.tilde_and_dollar_expansion(core)?;\n        let joint = core.db.get_ifs_head();\n        Ok(Self::make_args(&mut [w]).join(&joint))\n    }\n\n    pub fn eval_as_integer(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        utils::string_to_calculated_string(&self.text, core)\n    }\n\n    pub fn eval_for_case_word(&self, core: &mut ShellCore) -> Option<String> {\n        match self.tilde_and_dollar_expansion(core) {\n            Ok(mut w) => w.make_unquoted_word(),\n            Err(e) => {\n                e.print(core);\n                None\n            }\n        }\n    }\n\n    pub fn eval_for_regex(&self, core: &mut ShellCore) -> Option<String> {\n        let quoted = self.text.starts_with(\"\\\"\") && self.text.ends_with(\"\\\"\");\n\n        match self.tilde_and_dollar_expansion(core) {\n            Ok(mut w) => {\n                let mut re = w.make_regex()?;\n                if quoted {\n                    re.insert(0, '\"');\n                    re += \"\\\"\";\n                }\n\n                Some(re)\n            }\n            Err(e) => {\n                e.print(core);\n                None\n            }\n        }\n    }\n\n    pub fn eval_for_case_pattern(&self, core: &mut ShellCore) -> Result<String, ExecError> {\n        let mut w = self.tilde_and_dollar_expansion(core)?;\n        Ok(w.make_glob_string())\n    }\n\n    pub fn set_pipe(&mut self, core: &mut ShellCore) {\n        for sw in self.subwords.iter_mut() {\n            sw.set_pipe(core);\n        }\n    }\n\n    pub fn dollar_expansion(&self, core: &mut ShellCore) -> Result<Word, ExecError> {\n        let mut w = self.clone();\n        substitution::eval(&mut w, core)?;\n        Ok(w)\n    }\n\n    pub fn tilde_and_dollar_expansion(&self, core: &mut ShellCore) -> Result<Word, ExecError> {\n        let mut w = self.clone();\n        tilde_expansion::eval(&mut w, core);\n        substitution::eval(&mut w, core)?;\n        Ok(w)\n    }\n\n    pub fn split_and_path_expansion(&self, core: &mut ShellCore) -> Vec<Word> {\n        let mut ans = vec![];\n        let mut splitted = split::eval(self, core);\n\n        let len = splitted.len();\n        if len > 0 {\n            splitted[len - 1].do_not_erase = false;\n        }\n\n        if core.options.query(\"noglob\") || core.db.flags.contains('f') {\n            return splitted;\n        }\n\n        for mut w in splitted {\n            ans.append(&mut path_expansion::eval(&mut w, &core.shopts));\n        }\n        ans\n    }\n\n    fn path_expansion(&self, core: &mut ShellCore) -> Vec<Word> {\n        if core.options.query(\"noglob\") || core.db.flags.contains('f') {\n            return vec![self.clone()];\n        }\n\n        path_expansion::eval(&mut self.clone(), &core.shopts)\n    }\n\n    fn make_args(words: &mut [Word]) -> Vec<String> {\n        words\n            .iter_mut()\n            .filter_map(|w| w.make_unquoted_word())\n            .collect()\n    }\n\n    pub fn make_unquoted_word(&mut self) -> Option<String> {\n        let sw: Vec<Option<String>> = self\n            .subwords\n            .iter_mut()\n            .map(|s| s.make_unquoted_string())\n            .filter(|s| s.is_some())\n            .collect();\n\n        if sw.is_empty() && !self.do_not_erase {\n            return None;\n        }\n\n        Some(sw.into_iter().map(|s| s.unwrap()).collect::<String>())\n    }\n\n    pub fn make_regex(&mut self) -> Option<String> {\n        let sw: Vec<Option<String>> = self\n            .subwords\n            .iter_mut()\n            .map(|s| s.make_regex())\n            .filter(|s| s.is_some())\n            .collect();\n\n        if sw.is_empty() {\n            return None;\n        }\n\n        Some(sw.into_iter().map(|s| s.unwrap()).collect::<String>())\n    }\n\n    fn make_glob_string(&mut self) -> String {\n        self.subwords\n            .iter_mut()\n            .map(|s| s.make_glob_string())\n            .collect::<Vec<String>>()\n            .concat()\n    }\n\n    pub fn set_heredoc_flag(&mut self) {\n        self.subwords.iter_mut().for_each(|e| e.set_heredoc_flag());\n    }\n\n    pub fn is_to_proc_sub(&mut self) -> bool {\n        if self.subwords.len() == 1 {\n            return self.subwords[0].is_to_proc_sub();\n        }\n\n        false\n    }\n\n    fn scan_pos(&self, s: &str) -> Vec<usize> {\n        self.subwords\n            .iter()\n            .enumerate()\n            .filter(|e| e.1.get_text() == s)\n            .map(|e| e.0)\n            .collect()\n    }\n\n    fn push(&mut self, subword: Box<dyn Subword>) {\n        self.text += subword.get_text();\n        self.subwords.push(subword);\n    }\n\n    fn pre_check(feeder: &mut Feeder, mode: &Option<WordMode>) -> bool {\n        if feeder.starts_with(\"#\") && mode.is_none() || feeder.is_empty() {\n            return false;\n        }\n\n        match mode {\n            Some(WordMode::Arithmetic) \n            //| Some(WordMode::AlterWord) \n            | Some(WordMode::CompgenF) => {\n                if feeder.starts_with(\"}\") {\n                    return false;\n                }\n            }\n            Some(WordMode::ParamOption(v)) => {\n                if feeder.starts_withs(v) {\n                    return false;\n                }\n            }\n            _ => {}\n        }\n        true\n    }\n\n    fn post_check(feeder: &mut Feeder, core: &mut ShellCore, mode: &Option<WordMode>) -> bool {\n        if feeder.is_empty() {\n            return false;\n        }\n\n        match mode {\n            Some(WordMode::Arithmetic) | Some(WordMode::CompgenF) => {\n                if feeder.starts_withs(&[\"]\", \"}\"]) || feeder.scanner_math_symbol(core) != 0 {\n                    return false;\n                }\n            },\n            /*\n            Some(WordMode::AlterWord) => {\n                if feeder.starts_with(\"}\") {\n                    return false;\n                }\n            },*/\n            Some(WordMode::ParamOption(v)) => {\n                if feeder.starts_withs(v) {\n                    return false;\n                }\n            }\n            _ => {}\n        }\n        true\n    }\n\n    pub fn parse(\n        feeder: &mut Feeder,\n        core: &mut ShellCore,\n        mode: Option<WordMode>,\n    ) -> Result<Option<Word>, ParseError> {\n        if !Self::pre_check(feeder, &mode) {\n            return Ok(None);\n        }\n\n        let mut ans = Word::default();\n        if let Some(WordMode::Alias) = mode {\n            let len = feeder.scanner_blank(core);\n            ans.text = feeder.consume(len);\n        }\n\n        while let Some(sw) = subword::parse(feeder, core, &mode)? {\n            match sw.is_extglob() {\n                false => ans.push(sw),\n                true => {\n                    ans.text += sw.get_text();\n                    ans.subwords.append(&mut sw.get_child_subwords());\n                }\n            }\n\n            if !Self::post_check(feeder, core, &mode) {\n                break;\n            }\n        }\n\n        match ans.subwords.len() {\n            0 => Ok(None),\n            _ => Ok(Some(ans)),\n        }\n    }\n}\n"
  },
  {
    "path": "src/elements.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod ansi_c_str;\npub mod command;\npub mod expr;\npub mod io;\npub mod job;\npub mod pipeline;\npub mod script;\npub mod substitution;\npub mod subword;\npub mod word;\n\nuse self::io::pipe::Pipe;\n"
  },
  {
    "path": "src/error/arith.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\n#[derive(Debug, Clone)]\npub enum ArithError {\n    AssignmentToNonVariable(String),\n    DivZero(String),\n    Exponent(i128),\n    NoColon(String),\n    ExpressionExpected(String),\n    InvalidBase(String),\n    ValueTooGreatForBase(String),\n    InvalidNumber(String),\n    InvalidIntConst(String),\n    InvalidOperator(String),\n    OperandExpected(String),\n    Recursion(String),\n    SyntaxError(String),\n}\n\nimpl From<ArithError> for String {\n    fn from(e: ArithError) -> String {\n        Self::from(&e)\n    }\n}\n\nimpl From<&ArithError> for String {\n    fn from(e: &ArithError) -> String {\n        match e {\n            ArithError::AssignmentToNonVariable(right) => {\n                error_msg(\"attempted assignment to non-variable\", right)\n            }\n            ArithError::DivZero(token) => error_msg(\"division by 0\", token),\n            ArithError::Exponent(s) => error_msg(\"exponent less than 0\", &s.to_string()),\n            ArithError::NoColon(token) => {\n                error_msg(\"`:' expected for conditional expression\", token)\n            }\n            ArithError::ExpressionExpected(token) => error_msg(\"expression expected\", token),\n            ArithError::InvalidBase(b) => error_msg(\"invalid arithmetic base\", b),\n            ArithError::ValueTooGreatForBase(num) => error_msg(\"value too great for base\", num),\n            ArithError::InvalidNumber(name) => error_msg(\"invalid number\", name),\n            ArithError::InvalidIntConst(tok) => error_msg(\"invalid integer constant\", tok),\n            ArithError::InvalidOperator(tok) => error_msg(\"invalid arithmetic operator\", tok),\n            ArithError::OperandExpected(token) => {\n                error_msg(\"syntax error: operand expected\", token)\n            }\n            ArithError::Recursion(token) => error_msg(\"expression recursion level exceeded\", token),\n            ArithError::SyntaxError(token) => error_msg(\"syntax error in expression\", token),\n        }\n    }\n}\n\nfn error_msg(msg: &str, token: &str) -> String {\n    msg.to_string() + &format!(\" (error token is \\\"{token}\\\")\")\n}\n"
  },
  {
    "path": "src/error/exec.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::arith::ArithError;\nuse crate::error::parse::ParseError;\nuse crate::ShellCore;\nuse nix::errno::Errno;\nuse nix::sys::wait::WaitStatus;\nuse std::num::ParseIntError;\nuse std::os::fd::RawFd;\n\n#[derive(Debug, Clone)]\npub enum ExecError {\n    Internal,\n    ArgListTooLong(String),\n    AmbiguousRedirect(String),\n    ArrayIndexInvalid(String),\n    BadSubstitution(String),\n    BadFd(RawFd),\n    Bug(String),\n    CannotOverwriteExistingFile(String),\n    CircularNameRef(String),\n    CommandNotFound(String),\n    InvalidIndirectExpansion(String),\n    InvalidName(String),\n    InvalidNameRef(String),\n    InvalidOption(String),\n    Interrupted,\n    ValidOnlyInFunction,\n    VariableReadOnly(String),\n    VariableInvalid(String),\n    ParseIntError(String),\n    PermissionDenied(String),\n    SelfRef(String),\n    Silent,\n    SyntaxError(String),\n    Restricted(String),\n    RefCannotBeArray(String),\n    SubstringMinus(i128),\n    UnsupportedWaitStatus(WaitStatus),\n    UnboundVariable(String),\n    Errno(Errno),\n    Other(String),\n\n    ParseError(ParseError),\n    ArithError(String, ArithError),\n}\n\nimpl From<Errno> for ExecError {\n    fn from(e: Errno) -> ExecError {\n        ExecError::Errno(e)\n    }\n}\n\nimpl From<ParseIntError> for ExecError {\n    fn from(e: ParseIntError) -> ExecError {\n        ExecError::ParseIntError(e.to_string())\n    }\n}\n\nimpl From<ParseError> for ExecError {\n    fn from(e: ParseError) -> ExecError {\n        ExecError::ParseError(e)\n    }\n}\n\nimpl From<ArithError> for ExecError {\n    fn from(e: ArithError) -> ExecError {\n        ExecError::ArithError(String::new(), e)\n    }\n}\n\nimpl From<ExecError> for String {\n    fn from(e: ExecError) -> String {\n        Self::from(&e)\n    }\n}\n\n\n//    command_error_exit(command_name, core, \"Arg list too long\", 126)\n//    command_error_exit(command_name, core, \"Permission denied\", 126)\n\nimpl From<&ExecError> for String {\n    fn from(e: &ExecError) -> String {\n        match e {\n            ExecError::Internal => \"INTERNAL ERROR\".to_string(),\n            ExecError::AmbiguousRedirect(name) => format!(\"{name}: ambiguous redirect\"),\n            ExecError::ArrayIndexInvalid(name) => format!(\"[{name}]: bad array subscript\"),\n            ExecError::ArgListTooLong(name) => format!(\"{name}: Arg list too long\"),\n            ExecError::BadSubstitution(s) => format!(\"`{s}': bad substitution\"),\n            ExecError::BadFd(fd) => format!(\"{fd}: bad file descriptor\"),\n            ExecError::CannotOverwriteExistingFile(file) => {\n                format!(\"{file}: cannot overwrite existing file\")\n            }\n            ExecError::CircularNameRef(name) => format!(\"{name}: circular name reference\"),\n            ExecError::CommandNotFound(name) => format!(\"{name}: command not found\"),\n            ExecError::InvalidIndirectExpansion(name) => format!(\"{name}: invalid indirect expansion\"),\n            ExecError::InvalidName(name) => format!(\"`{name}': not a valid identifier\"),\n            ExecError::InvalidNameRef(name) => format!(\"`{name}': invalid variable name for name reference\"),\n            ExecError::InvalidOption(opt) => format!(\"{opt}: invalid option\"),\n            ExecError::Interrupted => \"interrupted\".to_string(),\n            ExecError::ValidOnlyInFunction => \"can only be used in a function\".to_string(),\n            ExecError::VariableReadOnly(name) => format!(\"{name}: readonly variable\"),\n            ExecError::VariableInvalid(name) => format!(\"`{name}': not a valid identifier\"),\n            ExecError::ParseIntError(e) => e.to_string(),\n            ExecError::PermissionDenied(name) => format!(\"{name}: Permission denied\"),\n            ExecError::SelfRef(name) => {\n                format!(\"{name}: nameref variable self references not allowed\")\n            },\n            ExecError::SyntaxError(near) => {\n                format!(\"syntax error near unexpected token `{}'\", &near)\n            }\n            ExecError::Restricted(com) => format!(\"{com}: restricted\"),\n            ExecError::RefCannotBeArray(name) => format!(\"{name}: reference variable cannot be an array\"),\n            ExecError::SubstringMinus(n) => format!(\"{n}: substring expression < 0\"),\n            ExecError::UnsupportedWaitStatus(ws) => format!(\"Unsupported wait status: {ws:?}\"),\n            ExecError::UnboundVariable(name) => format!(\"{name}: unbound variable\"),\n            ExecError::Errno(e) => format!(\"system error {e:?}\"),\n            ExecError::Bug(msg) => format!(\"INTERNAL BUG: {msg}\"),\n            ExecError::Other(name) => name.to_string(),\n\n            ExecError::ArithError(s, a) => format!(\"{}: {}\", s, String::from(a)),\n            ExecError::ParseError(p) => From::from(p),\n            _ => {\"\".to_string()},\n        }\n    }\n}\n\nimpl ExecError {\n    pub fn print(&self, core: &mut ShellCore) {\n        let name = core.db.get_param(\"0\").unwrap();\n        let s: String = From::<&ExecError>::from(self);\n        if s == \"\" {\n            return;\n        }\n\n        if core.db.flags.contains('i') {\n            eprintln!(\"{}: {}\", &name, &s);\n        } else {\n            let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n            eprintln!(\"{}: line {}: {}\", &name, &lineno, s);\n        }\n    }\n}\n"
  },
  {
    "path": "src/error/input.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\n#[derive(Debug, Clone)]\npub enum InputError {\n    NotUtf8,\n    NoSuchFile(String),\n    Interrupt,\n    Eof,\n}\n\nimpl From<&InputError> for String {\n    fn from(e: &InputError) -> String {\n        match e {\n            InputError::NotUtf8 => \"input error: illegal utf-8 character\".to_string(),\n            InputError::NoSuchFile(filename) => format!(\"{filename}: No such file or directory\"),\n            InputError::Eof => \"syntax error: unexpected end of file\".to_string(),\n            InputError::Interrupt => \"interrupted\".to_string(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/error/parse.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::input::InputError;\nuse crate::ShellCore;\n\n#[derive(Debug, Clone)]\npub enum ParseError {\n    UnexpectedSymbol(String),\n    Input(InputError),\n    WrongAlias(String),\n}\n//expected for conditional expression\n\nimpl From<&ParseError> for String {\n    fn from(e: &ParseError) -> String {\n        match e {\n            //ParseError::UnexpectedSymbol(s) => format!(\"Unexpected token: {}\", s),\n            ParseError::UnexpectedSymbol(s) => format!(\"syntax error near unexpected token: {s}\"),\n            ParseError::Input(e) => From::from(e),\n            ParseError::WrongAlias(msg) => format!(\"Someting wrong alias: {msg}\"),\n        }\n    }\n}\n\nimpl ParseError {\n    pub fn print(&self, core: &mut ShellCore) {\n        let name = core.db.get_param(\"0\").unwrap();\n        let s: String = From::<&ParseError>::from(self);\n        if core.db.flags.contains('i') {\n            eprintln!(\"{}: {}\", &name, &s);\n        } else {\n            let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n            eprintln!(\"{}: line {}: {}\", &name, &lineno, s);\n        }\n    }\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod arith;\npub mod exec;\npub mod input;\npub mod parse;\n\nuse crate::ShellCore;\nuse nix::sys::signal::Signal;\nuse nix::unistd::Pid;\n\npub fn print(s: &str, core: &mut ShellCore) {\n    let name = core.db.get_param(\"0\").unwrap();\n    if core.db.flags.contains('i') {\n        eprintln!(\"{}: {}\", &name, &s);\n    } else {\n        let lineno = core.db.get_param(\"LINENO\").unwrap_or(\"\".to_string());\n        eprintln!(\"{}: line {}: {}\", &name, &lineno, s);\n    }\n}\n\npub fn internal(s: &str) -> String {\n    format!(\"SUSH INTERNAL ERROR: {s}\")\n}\n\npub fn exponent(s: &str) -> String {\n    format!(\"exponent less than 0 (error token is \\\"{s}\\\")\")\n}\n\n/* error at wait */\npub fn signaled(pid: Pid, signal: Signal, coredump: bool) -> i32 {\n    match coredump {\n        true => eprintln!(\"Pid: {pid:?}, Signal: {signal:?} (core dumped)\"),\n        false => eprintln!(\"Pid: {pid:?}, Signal: {signal:?}\"),\n    }\n    128 + signal as i32\n}\n"
  },
  {
    "path": "src/feeder/scanner.rs",
    "content": "//SPDX-FileCopyrightText: 2023 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Feeder;\nuse crate::ShellCore;\n\nimpl Feeder {\n    fn feed_and_connect(&mut self, core: &mut ShellCore) {\n        self.remaining.pop();\n        self.remaining.pop();\n        self.lineno += 1;\n        let _ = core.db.set_param(\"LINENO\", &self.lineno.to_string(), None);\n        let _ = self.feed_additional_line_core(core);\n    }\n\n    fn backslash_check_and_feed(&mut self, starts: Vec<&str>, core: &mut ShellCore) {\n        let check = |s: &str| self.remaining.starts_with(&(s.to_owned() + \"\\\\\\n\"));\n        if starts.iter().any(|s| check(s)) {\n            self.feed_and_connect(core);\n        }\n    }\n\n    pub fn scanner_char(&mut self) -> usize {\n        match self.remaining.chars().next() {\n            Some(c) => c.len_utf8(),\n            None => 0,\n        }\n    }\n\n    fn scanner_chars(\n        &mut self,\n        judge: fn(char) -> bool,\n        core: &mut ShellCore,\n        skip_bytes: usize,\n    ) -> usize {\n        loop {\n            let mut ans = 0;\n            for ch in self.remaining[skip_bytes..].chars() {\n                match judge(ch) {\n                    true => ans += ch.len_utf8(),\n                    false => break,\n                }\n            }\n\n            match &self.remaining[skip_bytes + ans..] == \"\\\\\\n\" {\n                true => self.feed_and_connect(core),\n                false => return ans,\n            }\n        }\n    }\n\n    fn scanner_one_of(&self, cands: &[&str]) -> usize {\n        for c in cands {\n            if self.starts_with(c) {\n                return c.len();\n            }\n        }\n        0\n    }\n\n    pub fn scanner_subword_symbol(&self) -> usize {\n        self.scanner_one_of(&[\n            \"{\", \"}\", \",\", \"$\", \"~\", \"/\", \"*\", \"?\", \"%\", \"!\", \"@\", \"!\", \"+\", \"-\", \".\", \":\", \"=\",\n            \"^\", \",\", \"]\",\n        ])\n    }\n\n    pub fn scanner_math_symbol(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"\"], core);\n        self.scanner_one_of(&[\"/\", \"*\", \"?\", \":\", \"+\", \"-\", \"=\", \"^\", \"%\", \",\"])\n    }\n\n    pub fn scanner_unary_operator(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"+\", \"-\", \"!\", \"~\"], core);\n        if let Some('=') = self.remaining.chars().nth(1) {\n            return 0;\n        }\n\n        self.scanner_one_of(&[\"+\", \"-\", \"!\", \"~\"])\n    }\n\n    pub fn scanner_math_output_format(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"[#\", \"[\"], core);\n        if !self.starts_with(\"[#\") {\n            return 0;\n        }\n\n        let mut ans = 2;\n        let mut ok = false;\n        for (i, ch) in self.remaining[2..].chars().enumerate() {\n            if i == 0 && ch == '#' {\n                ans += 1;\n                continue;\n            }\n            if ch.is_ascii_digit() {\n                ok = true;\n                ans += 1;\n                continue;\n            }\n\n            if ch == ']' && ok {\n                return ans + 1;\n            }\n\n            break;\n        }\n        0\n    }\n\n    pub fn scanner_extglob_head(&self) -> usize {\n        self.scanner_one_of(&[\"?(\", \"*(\", \"+(\", \"@(\", \"!(\"])\n    }\n\n    pub fn scanner_escaped_char(&mut self, core: &mut ShellCore) -> usize {\n        if self.starts_with(\"\\\\\\n\") {\n            self.feed_and_connect(core);\n        }\n\n        if !self.starts_with(\"\\\\\") {\n            return 0;\n        }\n\n        match self.remaining.chars().nth(1) {\n            Some(ch) => 1 + ch.len_utf8(),\n            None => 1,\n        }\n    }\n\n    pub fn scanner_ansi_c_oct(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"\\\\\") {\n            return 0;\n        }\n\n        let judge = |ch| ('0'..='7').contains(&ch);\n        self.scanner_chars(judge, core, 1) + 1\n    }\n\n    pub fn scanner_ansi_c_hex(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"\\\\x\") {\n            return 0;\n        }\n\n        let judge = |ch: char| {\n            ch.is_ascii_digit() || ('a'..='f').contains(&ch) || ('A'..='F').contains(&ch)\n        };\n\n        let mut skip = 2;\n        if self.starts_with(\"\\\\x{\") {\n            skip = 3;\n            //let judge = |ch| ch != '}' && ch != '\\'';\n            let len = self.scanner_chars(judge, core, skip) + skip;\n            return len + self.scanner_chars(|c| c == '}', core, len);\n        }\n\n        self.scanner_chars(judge, core, skip) + skip\n    }\n\n    pub fn scanner_ansi_unicode4(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"\\\\u\") {\n            return 0;\n        }\n\n        let judge = |ch: char| {\n            ch.is_ascii_digit() || ('a'..='f').contains(&ch) || ('A'..='F').contains(&ch)\n        };\n        self.scanner_chars(judge, core, 2) + 2\n    }\n\n    pub fn scanner_ansi_unicode8(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"\\\\U\") {\n            return 0;\n        }\n\n        let judge = |ch: char| {\n            ch.is_ascii_digit() || ('a'..='f').contains(&ch) || ('A'..='F').contains(&ch)\n        };\n        self.scanner_chars(judge, core, 2) + 2\n    }\n\n    pub fn scanner_history_expansion(&mut self, _: &mut ShellCore) -> usize {\n        match self.starts_with(\"!$\") {\n            true => 2,\n            false => 0,\n        }\n    }\n\n    pub fn scanner_dollar_special_and_positional_param(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"$\") {\n            return 0;\n        }\n        self.backslash_check_and_feed(vec![\"$\"], core);\n\n        match self.remaining.chars().nth(1) {\n            Some(c) => {\n                if \"$?*@#-!0123456789\".find(c).is_some() {\n                    2\n                } else {\n                    0\n                }\n            }\n            None => 0,\n        }\n    }\n\n    pub fn scanner_special_and_positional_param(&mut self) -> usize {\n        match self.remaining.chars().nth(0) {\n            Some(c) => {\n                if \"$?*@#-!_0123456789\".find(c).is_some() {\n                    1\n                } else {\n                    0\n                }\n            }\n            None => 0,\n        }\n    }\n\n    pub fn scanner_subword(&mut self) -> usize {\n        let mut ans = 0;\n        for ch in self.remaining.chars() {\n            if \" \\t\\n;&|()<>{},\\\\'$/~\\\"*+-?@!.:=^]`%\".find(ch).is_some() {\n                break;\n            }\n            ans += ch.len_utf8();\n        }\n        ans\n    }\n\n    pub fn scanner_double_quoted_subword(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch| \"`\\\"\\\\$\".find(ch).is_none();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_extglob_subword(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch| \")|,}\".find(ch).is_none();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_single_quoted_subword(&mut self, core: &mut ShellCore) -> usize {\n        if !self.starts_with(\"'\") {\n            return 0;\n        }\n\n        loop {\n            if let Some(n) = self.remaining[1..].find(\"'\") {\n                return n + 2;\n            } else if self.feed_additional_line(core).is_err() {\n                return 0;\n            }\n        }\n    }\n\n    pub fn scanner_inner_subscript(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch| \"]\".find(ch).is_none();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_unknown_in_param_brace(&mut self) -> usize {\n        match self.remaining.chars().nth(0) {\n            Some(c) => {\n                if \"'$\".find(c).is_none() {\n                    c.len_utf8()\n                } else {\n                    0\n                }\n            }\n            None => 0,\n        }\n    }\n\n    pub fn scanner_blank(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch| \" \\t\".find(ch).is_some();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_multiline_blank(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch| \" \\t\\n\".find(ch).is_some();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_binary_operator(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(\n            vec![\n                \"<<\", \">>\", \"+\", \"-\", \"/\", \"*\", \"%\", \"<\", \">\", \"=\", \"&\", \"|\", \"^\", \"/\", \"%\",\n            ],\n            core,\n        );\n        self.scanner_one_of(&[\n            \"<<=\", \">>=\", \"&&\", \"||\", \"**\", \"==\", \"!=\", \"*=\", \"/=\", \"%=\", \"+=\", \"-=\", \"&=\", \"^=\",\n            \"|=\", \">>\", \"<<\", \"<=\", \">=\", \"&\", \"^\", \"=\", \"+\", \"-\", \"/\", \"*\", \"%\", \"<\", \">\", \"|\",\n            \"^\", \",\",\n        ])\n    }\n\n    pub fn scanner_substitution(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(\n            vec![\"*\", \"/\", \"%\", \"+\", \"-\", \"<\", \"<<\", \">\", \">>\", \"^\", \"|\"],\n            core,\n        );\n        self.scanner_one_of(&[\n            \"=\", \"*=\", \"/=\", \"%=\", \"+=\", \"-=\", \"<<=\", \">>=\", \"&=\", \"^=\", \"|=\",\n        ])\n    }\n\n    pub fn scanner_uint(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch: char| ch.is_ascii_digit();\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_arith_number(&mut self, core: &mut ShellCore) -> usize {\n        let judge = |ch: char| {\n            ch.is_ascii_digit()\n                || ch.is_ascii_lowercase()\n                || ch.is_ascii_uppercase()\n                || \".#xX_@\".contains(ch)\n        };\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_name(&mut self, core: &mut ShellCore) -> usize {\n        let c = self.remaining.chars().nth(0).unwrap_or('0');\n        if c.is_ascii_digit() {\n            return 0;\n        }\n\n        let judge = |ch: char| {\n            ch == '_' || ch.is_ascii_digit() || ch.is_ascii_lowercase() || ch.is_ascii_uppercase()\n        };\n        self.scanner_chars(judge, core, 0)\n    }\n\n    pub fn scanner_name_and_equal(&mut self, core: &mut ShellCore) -> usize {\n        let name_len = self.scanner_name(core);\n        if name_len == 0 {\n            return 0;\n        }\n\n        if self.remaining[name_len..].starts_with(\"=\") {\n            name_len + 1\n        } else if self.remaining[name_len..].starts_with(\"+=\") {\n            name_len + 2\n        } else {\n            0\n        }\n    }\n\n    pub fn scanner_job_end(&mut self) -> usize {\n        self.scanner_one_of(&[\";\", \"&\", \"\\n\"])\n    }\n\n    pub fn scanner_and_or(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"|\", \"&\"], core);\n        self.scanner_one_of(&[\"||\", \"&&\"])\n    }\n\n    pub fn scanner_pipe(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"|\"], core);\n        if self.starts_with(\"||\") {\n            return 0;\n        }\n        self.scanner_one_of(&[\"|&\", \"|\"])\n    }\n\n    pub fn scanner_comment(&self) -> usize {\n        if !self.remaining.starts_with(\"#\") {\n            return 0;\n        }\n\n        let mut ans = 0;\n        for ch in self.remaining.chars() {\n            if \"\\n\".find(ch).is_some() {\n                break;\n            }\n            ans += ch.len_utf8();\n        }\n        ans\n    }\n\n    pub fn scanner_redirect_symbol(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"<<\", \">\", \"&\", \"<\"], core);\n        self.scanner_one_of(&[\"<<<\", \"<<-\", \"&>\", \">&\", \"<&\", \">>\", \"<<\", \"<\", \">\"])\n    }\n\n    pub fn scanner_parameter_alternative_symbol(&mut self) -> usize {\n        self.scanner_one_of(&[\":-\", \":=\", \":?\", \":+\", \"-\", \"+\", \"?\", \"=\"])\n    }\n\n    pub fn scanner_parameter_remove_symbol(&mut self) -> usize {\n        self.scanner_one_of(&[\"##\", \"#\", \"%%\", \"%\"])\n    }\n\n    pub fn scanner_tabs(&mut self) -> usize {\n        self.scanner_one_of(&[\"\\t\"])\n    }\n\n    pub fn scanner_test_check_option(&mut self, core: &mut ShellCore) -> usize {\n        match self.remaining.chars().nth(0) {\n            Some('-') => {}\n            _ => return 0,\n        }\n        self.backslash_check_and_feed(vec![\"-\"], core);\n\n        if let Some(c) = self.remaining.chars().nth(1) {\n            match \"abcdefghknoprstuvwxzGLNOS\".contains(c) {\n                true => return 2,\n                false => return 0,\n            }\n        }\n        0\n    }\n\n    pub fn scanner_escape_directive_in_braced_param(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"@\"], core);\n        self.scanner_one_of(&[\"@k\", \"@Q\", \"@K\"])\n    }\n\n    pub fn scanner_test_compare_op(&mut self, core: &mut ShellCore) -> usize {\n        self.backslash_check_and_feed(vec![\"-\", \"-e\", \"-n\", \"-o\", \"=\", \"!\"], core);\n        self.scanner_one_of(&[\n            \"-ef\", \"-nt\", \"-ot\", \"==\", \"=~\", \"=\", \"!=\", \"<\", \">\", \"-eq\", \"-ne\", \"-lt\", \"-le\",\n            \"-gt\", \"-ge\",\n        ])\n    }\n\n    pub fn scanner_regex_symbol(&mut self) -> usize {\n        match self.starts_with(\" \") {\n            true => 0,\n            false => 1,\n        }\n    }\n}\n"
  },
  {
    "path": "src/feeder/terminal/completion.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::builtins::compgen;\nuse crate::core::completion::CompletionEntry;\nuse crate::elements::command::simple::SimpleCommand;\nuse crate::elements::io::pipe::Pipe;\nuse crate::error::exec::ExecError;\nuse crate::feeder::terminal::Terminal;\nuse crate::utils::arg;\nuse crate::{file_check, utils, Feeder, ShellCore};\nuse unicode_width::UnicodeWidthStr;\n\nstruct Entry<'a> {\n    list: &'a [String],\n    widths: &'a [usize],\n    row: usize,\n    col: usize,\n    row_num: usize,\n    width: usize,\n    pointed: bool,\n}\n\nfn str_width(s: &str) -> usize {\n    UnicodeWidthStr::width(s)\n}\n\nfn common_length(chars: &[char], s: &str) -> usize {\n    let max_len = chars.len();\n    for (i, c) in s.chars().enumerate() {\n        if i >= max_len || chars[i] != c {\n            return i;\n        }\n    }\n    max_len\n}\n\nfn common_string(paths: &[String]) -> String {\n    if paths.is_empty() {\n        return \"\".to_string();\n    }\n\n    let ref_chars: Vec<char> = paths[0].chars().collect();\n    let mut common_len = ref_chars.len();\n\n    for path in &paths[1..] {\n        let len = common_length(&ref_chars, path);\n        common_len = std::cmp::min(common_len, len);\n    }\n\n    ref_chars[..common_len].iter().collect()\n}\n\nfn is_dir(s: &str, core: &mut ShellCore) -> bool {\n    let tilde_prefix = \"~/\".to_string();\n    let tilde_path = core.db.get_param(\"HOME\").unwrap_or_default() + \"/\";\n\n    file_check::is_dir(&s.replace(&tilde_prefix, &tilde_path))\n}\n\nfn apply_o_options(cand: &mut String, core: &mut ShellCore, o_options: &[String]) {\n    let mut tail = \" \";\n    if is_dir(cand, core) {\n        tail = \"/\";\n    }\n\n    if file_check::exists(cand) {\n        *cand = cand\n            .replace(\" \", \"\\\\ \")\n            .replace(\"(\", \"\\\\(\")\n            .replace(\")\", \"\\\\)\");\n        if !is_dir(cand, core) {\n            tail = tail.trim_end();\n        }\n    }\n\n    if arg::has_option(\"nospace\", o_options) {\n        tail = tail.trim_end();\n    }\n\n    *cand += tail\n}\n\nimpl Terminal {\n    pub fn completion(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        self.escape_at_completion = true;\n        let _ = core.db.init_array(\"COMPREPLY\", Some(vec![]), None, false);\n        self.set_completion_info(core)?;\n\n        if self.set_custom_compreply(core).is_err() && self.set_default_compreply(core).is_err() {\n            self.cloop();\n            return Ok(());\n        }\n\n        let mut cands = core.db.get_vec(\"COMPREPLY\", true)?;\n        cands.retain(|c| !c.is_empty());\n        let o_options = core.completion.current.o_options.clone();\n        for cand in cands.iter_mut() {\n            apply_o_options(cand, core, &o_options);\n        }\n\n        match self.tab_num {\n            1 => self.try_completion(&mut cands, core).unwrap(),\n            _ => self.show_list(&cands),\n        }\n        Ok(())\n    }\n\n    fn exec_complete_function(\n        org_word: &str,\n        prev_pos: i32,\n        cur_pos: i32,\n        core: &mut ShellCore,\n    ) -> Result<(), ExecError> {\n        let prev_word = core.db.get_elem(\"COMP_WORDS\", &prev_pos.to_string())?;\n        let target_word = core.db.get_elem(\"COMP_WORDS\", &cur_pos.to_string())?;\n        let info = &core.completion.current;\n\n        let command = format!(\n            \"{} \\\"{}\\\" \\\"{}\\\" \\\"{}\\\"\",\n            &info.function, &org_word, &target_word, &prev_word\n        );\n        let mut feeder = Feeder::new(&command);\n\n        if let Ok(Some(mut a)) = SimpleCommand::parse(&mut feeder, core) {\n            let mut dummy = Pipe::new(\"\".to_string());\n            a.exec(core, &mut dummy)?;\n        }\n        Ok(())\n    }\n\n    fn exec_action(cur_pos: i32, core: &mut ShellCore) -> Result<(), ExecError> {\n        let target_word = core.db.get_elem(\"COMP_WORDS\", &cur_pos.to_string())?;\n        let info = &core.completion.current;\n\n        let command = format!(\n            \"COMPREPLY=($(compgen -A \\\"{}\\\" \\\"{}\\\"))\",\n            &info.action, &target_word\n        );\n        let mut feeder = Feeder::new(&command);\n\n        if let Ok(Some(mut a)) = SimpleCommand::parse(&mut feeder, core) {\n            let mut dummy = Pipe::new(\"\".to_string());\n            a.exec(core, &mut dummy)?;\n        }\n        Ok(())\n    }\n\n    fn set_custom_compreply(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let cur_pos = Self::get_cur_pos(core);\n        let prev_pos = cur_pos - 1;\n        let word_num = core.db.get_var_len(\"COMP_WORDS\") as i32;\n\n        if prev_pos < 0 || prev_pos >= word_num {\n            return Err(ExecError::Other(\"pos error\".to_string()));\n        }\n\n        let org_word = core.db.get_elem(\"COMP_WORDS\", \"0\")?;\n\n        let info = match core.completion.entries.get(&org_word) {\n            Some(i) => i.clone(),\n            None => CompletionEntry {\n                function: core.completion.default_function.clone(),\n                ..Default::default()\n            },\n        };\n\n        core.completion.current = info.clone();\n        if !info.function.is_empty() {\n            Self::exec_complete_function(&org_word, prev_pos, cur_pos, core)?;\n        } else if !info.action.is_empty() {\n            Self::exec_action(cur_pos, core)?;\n        }\n\n        match core.db.get_var_len(\"COMPREPLY\") {\n            0 => Err(ExecError::Other(\"no completion cand\".to_string())),\n            _ => Ok(()),\n        }\n    }\n\n    fn get_cur_pos(core: &mut ShellCore) -> i32 {\n        core.db\n            .get_param(\"COMP_CWORD\")\n            .unwrap()\n            .parse::<i32>()\n            .unwrap()\n    }\n\n    pub fn set_default_compreply(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let pos = core.db.get_param(\"COMP_CWORD\")?;\n        let last = core.db.get_elem(\"COMP_WORDS\", &pos)?;\n\n        let com = core.db.get_elem(\"COMP_WORDS\", \"0\")?;\n\n        let (tilde_prefix, tilde_path, last_tilde_expanded) =\n            Self::set_tilde_transform(&last, core);\n\n        let args = vec![\n            \"\".to_string(),\n            \"\".to_string(),\n            last_tilde_expanded.to_string(),\n        ];\n\n        let list = self.make_default_compreply(core, &args, &com, &pos);\n        if list.is_empty() {\n            return Err(ExecError::Other(\"empty list\".to_string()));\n        }\n\n        let tmp: Vec<String> = list\n            .iter()\n            .map(|p| p.replacen(&tilde_path, &tilde_prefix, 1))\n            .collect();\n        core.db.init_array(\"COMPREPLY\", Some(tmp), None, false)\n    }\n\n    fn make_default_compreply(\n        &mut self,\n        core: &mut ShellCore,\n        args: &[String],\n        com: &str,\n        pos: &str,\n    ) -> Vec<String> {\n        if core.completion.entries.contains_key(com) {\n            let action = core.completion.entries[com].action.clone();\n            let options = core.completion.entries[com].options.clone();\n\n            if !action.is_empty() {\n                let mut cands = match action.as_ref() {\n                    \"alias\" => compgen::compgen_a(core, args),\n                    \"command\" => compgen::compgen_c(core, args),\n                    \"job\" => compgen::compgen_j(core, args),\n                    \"setopt\" => compgen::compgen_o(core, args),\n                    \"stopped\" => compgen::compgen_stopped(core, args),\n                    \"user\" => compgen::compgen_u(core, args),\n                    \"variable\" => compgen::compgen_v(core, args),\n                    _ => vec![],\n                };\n\n                if options.contains_key(\"-P\") {\n                    let prefix = &options[\"-P\"];\n                    cands = cands.iter().map(|c| prefix.clone() + c).collect();\n                }\n                if options.contains_key(\"-S\") {\n                    let suffix = &options[\"-S\"];\n                    cands = cands\n                        .iter()\n                        .map(|c| c.to_owned() + &suffix.clone())\n                        .collect();\n                }\n                return cands;\n            }\n        }\n\n        if pos == \"0\" {\n            return if core.db.get_var_len(\"COMP_WORDS\") == 0 {\n                self.escape_at_completion = false;\n                compgen::compgen_h(core, args)\n                    .to_vec()\n                    .into_iter()\n                    .filter(|h| !h.is_empty())\n                    .collect()\n            } else {\n                compgen::compgen_c(core, args)\n            };\n        }\n\n        compgen::compgen_f(core, args, false)\n    }\n\n    pub fn try_completion(\n        &mut self,\n        cands: &mut [String],\n        core: &mut ShellCore,\n    ) -> Result<(), String> {\n        let pos = core.db.get_param(\"COMP_CWORD\")?;\n        let target = core.db.get_elem(\"COMP_WORDS\", &pos)?;\n\n        let common = common_string(cands);\n        if common.len() != target.len() && !common.is_empty() {\n            self.replace_input(&common);\n            return Ok(());\n        }\n        self.cloop();\n        Ok(())\n    }\n\n    fn normalize_tab(&mut self, row_num: i32, col_num: i32) {\n        let i = (self.tab_col * row_num + self.tab_row + row_num * col_num) % (row_num * col_num);\n        self.tab_col = i / row_num;\n        self.tab_row = i % row_num;\n    }\n\n    fn show_list(&mut self, list: &[String]) {\n        if list.is_empty() {\n            return;\n        }\n\n        let widths: Vec<usize> = list.iter().map(|s| str_width(s)).collect();\n        let max_entry_width = widths.iter().max().unwrap_or(&1000) + 1;\n        let terminal_row_num = self.size.1;\n        let col_num = std::cmp::min(std::cmp::max(self.size.0 / max_entry_width, 1), list.len());\n        let row_num = std::cmp::min(\n            (list.len() - 1) / col_num + 1,\n            std::cmp::max(terminal_row_num - 2, 1),\n        );\n        self.completion_candidate = String::new();\n\n        if self.tab_num > 2 {\n            self.normalize_tab(row_num as i32, col_num as i32);\n        }\n\n        eprintln!(\"\\r\");\n        for row in 0..row_num {\n            for col in 0..col_num {\n                let tab = self.tab_row == row as i32 && self.tab_col == col as i32;\n                let entry = Entry {\n                    list,\n                    widths: &widths,\n                    row,\n                    col,\n                    row_num,\n                    width: max_entry_width,\n                    pointed: tab,\n                };\n                self.print_an_entry(&entry);\n            }\n            print!(\"\\r\\n\");\n        }\n\n        let (cur_col, cur_row) = self.head_to_cursor_pos(self.head, self.prompt_row);\n\n        self.check_scroll();\n        match cur_row == terminal_row_num {\n            true => {\n                let back_row = std::cmp::max(cur_row as i16 - row_num as i16, 1);\n                self.write(&termion::cursor::Goto(cur_col as u16, back_row as u16).to_string());\n                print!(\"\\x1b[1A\");\n                self.flush();\n            }\n            false => self.rewrite(false),\n        }\n    }\n\n    fn print_an_entry(&mut self, entry: &Entry) {\n        let i = entry.col * entry.row_num + entry.row;\n        let space_num = match i < entry.list.len() {\n            true => entry.width - entry.widths[i],\n            false => entry.width,\n        };\n        let cand = match i < entry.list.len() {\n            true => entry.list[i].clone(),\n            false => \"\".to_string(),\n        };\n\n        let s = String::from_utf8(vec![b' '; space_num]).unwrap();\n        if entry.pointed {\n            print!(\"\\x1b[01;7m{}{}\\x1b[00m\", &cand, &s);\n            self.completion_candidate = cand;\n        } else {\n            print!(\"{}{}\", &cand, &s);\n        }\n    }\n\n    fn shave_existing_word(&mut self) {\n        while self.head > self.prompt.chars().count()\n            && (self.head > 0 && self.chars[self.head - 1] != ' '\n                || (self.head > 1\n                    && self.chars[self.head - 1] == ' '\n                    && self.chars[self.head - 2] == '\\\\'))\n        {\n            self.backspace();\n        }\n        while self.head < self.chars.len() && self.chars[self.head] != ' ' {\n            self.delete();\n        }\n    }\n\n    pub fn replace_input(&mut self, to: &str) {\n        self.shave_existing_word();\n        let to_modified = to.replace(\"↵ \\0\", \"\\n\");\n        for c in to_modified.chars() {\n            self.insert(c);\n            self.check_scroll();\n        }\n        self.rewrite(true);\n    }\n\n    fn set_tilde_transform(last: &str, core: &mut ShellCore) -> (String, String, String) {\n        let tilde_prefix;\n        let tilde_path;\n        let last_tilde_expanded;\n\n        if last.starts_with(\"~/\") {\n            tilde_prefix = \"~/\".to_string();\n            tilde_path = core.db.get_param(\"HOME\").unwrap_or_default() + \"/\";\n            last_tilde_expanded = last.replacen(&tilde_prefix, &tilde_path, 1);\n        } else {\n            tilde_prefix = String::new();\n            tilde_path = String::new();\n            last_tilde_expanded = last.to_string();\n        }\n\n        (tilde_prefix, tilde_path, last_tilde_expanded)\n    }\n\n    fn set_completion_info(&mut self, core: &mut ShellCore) -> Result<(), ExecError> {\n        let prompt_len = self.prompt.chars().count();\n        core.db\n            .set_param(\"COMP_POINT\", &(self.head - prompt_len).to_string(), None)?;\n\n        let all_string = self.get_string(prompt_len);\n        core.db.set_param(\"COMP_LINE\", &all_string, None)?;\n\n        let tp = match self.tab_num {\n            1 => \"\\t\",\n            _ => \"?\",\n        };\n        core.db.set_param(\"COMP_TYPE\", tp, None)?;\n        core.db.set_param(\"COMP_KEY\", \"9\", None)?;\n\n        let mut words_all = utils::split_words(&all_string);\n\n        let left_string: String = self.chars[prompt_len..self.head].iter().collect();\n        let mut words_left = utils::split_words(&left_string);\n        let from = completion_from(&words_left, core);\n\n        words_all = words_all[from..].to_vec();\n        words_left = words_left[from..].to_vec();\n        let _ = core.db.init_array(\"COMP_WORDS\", Some(words_all), None, false);\n\n        let mut num = words_left.len();\n        match left_string.chars().last() {\n            Some(' ') => num -= 1,\n            Some(_) => {\n                num = num.saturating_sub(1);\n            }\n            _ => {}\n        }\n\n        let _ = core.db.set_param(\"COMP_CWORD\", &num.to_string(), None);\n        Ok(())\n    }\n}\n\nfn completion_from(ws: &[String], core: &mut ShellCore) -> usize {\n    for i in 0..ws.len() {\n        if utils::reserved(&ws[i]) {\n            continue;\n        }\n\n        let s = ws[i..].join(\" \");\n        let mut feeder = Feeder::new(&s);\n        if let Ok(Some(_)) = SimpleCommand::parse(&mut feeder, core) {\n            return i;\n        }\n    }\n    ws.len()\n}\n"
  },
  {
    "path": "src/feeder/terminal/key.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::Terminal;\nuse crate::error::input::InputError;\nuse crate::ShellCore;\nuse std::sync::atomic::Ordering::Relaxed;\nuse termion::event;\nuse termion::event::Key;\n\npub fn action(core: &mut ShellCore, term: &mut Terminal, c: &Key) -> Result<bool, InputError> {\n    match c {\n        event::Key::Ctrl(ch) => ctrl(core, term, *ch)?,\n        event::Key::Down | event::Key::Left | event::Key::Right | event::Key::Up => {\n            arrow(term, core, c)\n        }\n        event::Key::Backspace => term.backspace(),\n        event::Key::Delete => term.delete(),\n        event::Key::Char(c) => return char_key(term, core, c),\n        _ => {}\n    }\n    Ok(false)\n}\n\nfn ctrl(core: &mut ShellCore, term: &mut Terminal, c: char) -> Result<(), InputError> {\n    match c {\n        'a' => term.goto_origin(),\n        'b' => term.shift_cursor(-1),\n        'c' => {\n            core.sigint.store(true, Relaxed);\n            term.goto(term.chars.len());\n            term.write(\"^C\\r\\n\");\n            return Err(InputError::Interrupt);\n        }\n        'd' => {\n            if term.chars.len() == term.prompt.chars().count() {\n                term.write(\"\\r\\n\");\n                return Err(InputError::Eof);\n            } else {\n                term.delete();\n            }\n        }\n        'e' => term.goto_end(),\n        'f' => term.shift_cursor(1),\n        _ => {}\n    }\n    Ok(())\n}\n\nfn arrow(term: &mut Terminal, core: &mut ShellCore, key: &event::Key) {\n    if term.tab_num > 1 {\n        match key {\n            event::Key::Down => term.tab_row += 1,\n            event::Key::Up => term.tab_row -= 1,\n            event::Key::Right => term.tab_col += 1,\n            event::Key::Left => term.tab_col -= 1,\n            _ => {}\n        }\n        let _ = term.completion(core);\n    } else {\n        match key {\n            event::Key::Down => term.call_history(-1, core),\n            event::Key::Up => term.call_history(1, core),\n            event::Key::Right => term.shift_cursor(1),\n            event::Key::Left => term.shift_cursor(-1),\n            _ => {}\n        }\n    }\n}\n\nfn char_key(term: &mut Terminal, core: &mut ShellCore, c: &char) -> Result<bool, InputError> {\n    match c {\n        '\\n' => {\n            if !term.completion_candidate.is_empty() {\n                term.set_double_tab_completion(core);\n            } else {\n                term.goto(term.chars.len());\n                term.write(\"\\r\\n\");\n                term.chars.push('\\n');\n                return Ok(true);\n            }\n        }\n        '\\t' => {\n            if term.tab_num == 0 || term.prev_key == event::Key::Char('\\t') {\n                term.tab_num += 1;\n            }\n            if term.tab_num == 2 {\n                term.tab_row = -1;\n                term.tab_col = 0;\n            } else if term.tab_num > 2 {\n                term.tab_row += 1;\n            }\n            let _ = term.completion(core);\n        }\n        c => term.insert(*c),\n    }\n    Ok(false)\n}\n"
  },
  {
    "path": "src/feeder/terminal.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod completion;\nmod key;\n\nuse crate::error::input::InputError;\nuse crate::utils::{arg, file};\nuse crate::{file_check, ShellCore};\nuse nix::unistd;\nuse nix::unistd::User;\nuse std::fs::File;\nuse std::io;\nuse std::io::{Stdout, Write};\nuse std::path::Path;\nuse std::sync::atomic::Ordering::Relaxed;\nuse termion::cursor::DetectCursorPos;\nuse termion::event;\nuse termion::event::Key;\nuse termion::input::TermRead;\nuse termion::raw::{IntoRawMode, RawTerminal};\nuse unicode_width::UnicodeWidthChar;\n\nstruct Terminal {\n    prompt: String,\n    stdout: RawTerminal<Stdout>,\n    prompt_row: usize,\n    chars: Vec<char>,\n    head: usize,\n    hist_ptr: usize,\n    prompt_width_map: Vec<usize>,\n    size: (usize, usize),\n    tab_num: usize,\n    prev_key: Key,\n    /* for extended completion */\n    completion_candidate: String,\n    tab_row: i32,\n    tab_col: i32,\n    escape_at_completion: bool,\n}\n\nfn oct_string(s: &str) -> bool {\n    if !s.starts_with('\\\\') {\n        return false;\n    }\n\n    for i in 1..4 {\n        match s.chars().nth(i) {\n            Some(c) => {\n                if !c.is_ascii_digit() {\n                    return false;\n                }\n            }\n            _ => return false,\n        }\n    }\n\n    true\n}\n\nfn oct_to_hex_in_str(from: &str) -> String {\n    let mut i = 0;\n    let mut pos = vec![];\n\n    for ch in from.chars() {\n        if oct_string(&from[i..]) {\n            pos.push(i);\n        }\n        i += ch.len_utf8();\n    }\n\n    let mut prev = 0;\n    let mut ans = String::new();\n    for p in pos {\n        ans += &from[prev..p];\n        if let Ok(n) = u32::from_str_radix(&from[p + 1..p + 4], 8) {\n            ans += &char::from_u32(n).unwrap().to_string();\n        }\n        prev = p + 4;\n    }\n    ans += &from[prev..];\n    ans\n}\n\nimpl Terminal {\n    pub fn new(core: &mut ShellCore, ps: &str) -> Self {\n        let raw_prompt = core.db.get_param(ps).unwrap_or_default();\n        let ansi_on_prompt = oct_to_hex_in_str(&raw_prompt);\n\n        let replaced_prompt = Self::make_prompt_string(&ansi_on_prompt);\n        let prompt = replaced_prompt\n            .replace(\"\\\\[\", \"\")\n            .replace(\"\\\\]\", \"\")\n            .to_string();\n        print!(\"{prompt}\");\n        io::stdout().flush().unwrap();\n\n        let mut sout = io::stdout().into_raw_mode().unwrap();\n        let row = sout.cursor_pos().unwrap_or((1, 1)).1;\n\n        Terminal {\n            prompt: prompt.to_string(),\n            stdout: sout,\n            prompt_row: row as usize,\n            chars: prompt.chars().collect(),\n            head: prompt.chars().count(),\n            hist_ptr: 0,\n            size: Terminal::size(),\n            prompt_width_map: Self::make_width_map(&replaced_prompt),\n            prev_key: event::Key::Char('a'),\n            tab_num: 0,\n            completion_candidate: String::new(),\n            tab_row: -1,\n            tab_col: -1,\n            escape_at_completion: true,\n        }\n    }\n\n    fn get_branch(cwd: &str) -> String {\n        let mut dirs: Vec<String> = cwd.split(\"/\").map(|s| s.to_string()).collect();\n        while !dirs.is_empty() {\n            let path = dirs.join(\"/\") + \"/.git/HEAD\";\n            dirs.pop();\n\n            if !file_check::is_regular_file(&path) {\n                continue;\n            }\n\n            if let Ok(mut f) = File::open(Path::new(&path)) {\n                return match f.read_line() {\n                    Ok(Some(s)) => s.replace(\"ref: refs/heads/\", \"\") + \"🌵\",\n                    _ => \"\".to_string(),\n                };\n            }\n        }\n\n        \"\".to_string()\n    }\n\n    fn make_prompt_string(raw: &str) -> String {\n        let uid = unistd::getuid();\n        let user = match User::from_uid(uid) {\n            Ok(Some(u)) => u.name,\n            _ => \"\".to_string(),\n        };\n        let hostname = match unistd::gethostname() {\n            Ok(h) => file::oss_to_name(&h),\n            _ => \"\".to_string(),\n        };\n\n        let homedir = match User::from_uid(uid) {\n            Ok(Some(u)) => file::buf_to_name(&u.dir),\n            _ => \"\".to_string(),\n        };\n        let mut cwd = match unistd::getcwd() {\n            Ok(p) => file::buf_to_name(&p),\n            _ => \"\".to_string(),\n        };\n        let branch = Self::get_branch(&cwd);\n\n        if cwd.starts_with(&homedir) {\n            cwd = cwd.replacen(&homedir, \"~\", 1);\n        }\n\n        raw.replace(\"\\\\u\", &user)\n            .replace(\"\\\\h\", &hostname)\n            .replace(\"\\\\w\", &cwd)\n            .replace(\"\\\\b\", &branch)\n            .to_string()\n    }\n\n    fn make_width_map(prompt: &str) -> Vec<usize> {\n        let tmp = prompt\n            .replace(\"\\\\[\", \"\\x01\")\n            .replace(\"\\\\]\", \"\\x02\")\n            .to_string();\n        let mut in_escape = false;\n        let mut ans = vec![];\n        for c in tmp.chars() {\n            if c == '\\x01' || c == '\\x02' {\n                in_escape = c == '\\x01';\n                continue;\n            }\n\n            let wid = match in_escape {\n                true => 0,\n                false => UnicodeWidthChar::width(c).unwrap_or(0),\n            };\n            ans.push(wid);\n        }\n        ans\n    }\n\n    fn write(&mut self, s: &str) {\n        write!(self.stdout, \"{s}\").unwrap();\n    }\n\n    fn flush(&mut self) {\n        self.stdout.flush().unwrap();\n    }\n\n    fn char_width(&self, c: &char, pos: usize) -> usize {\n        if pos < self.prompt.chars().count() {\n            return self.prompt_width_map[pos];\n        }\n\n        UnicodeWidthChar::width(*c).unwrap_or(0)\n    }\n\n    fn size() -> (usize, usize) {\n        let (c, r) = termion::terminal_size().unwrap();\n        (c as usize, r as usize)\n    }\n\n    fn shift_in_range(x: &mut usize, shift: i32, min: usize, max: usize) {\n        *x = if shift < 0 && *x < min + (-shift as usize) {\n            min\n        } else if shift > 0 && *x + (shift as usize) > max {\n            max\n        } else {\n            (*x as isize + shift as isize) as usize\n        };\n    }\n\n    fn head_to_cursor_pos(&self, head: usize, y_origin: usize) -> (usize, usize) {\n        let col = Terminal::size().0;\n        let (mut x, mut y) = (0, y_origin);\n\n        for (i, c) in self.chars[..head].iter().enumerate() {\n            if *c == '\\n' {\n                y += 1;\n                x = 0;\n                continue;\n            }\n\n            let w = self.char_width(c, i);\n            if x + w > col {\n                y += 1;\n                x = w;\n            } else {\n                x += w;\n            }\n        }\n\n        (x + 1, y)\n    }\n\n    fn goto(&mut self, head: usize) {\n        let pos = self.head_to_cursor_pos(head, self.prompt_row);\n        let size = Terminal::size();\n\n        let x: u16 = std::cmp::min(size.0, pos.0).try_into().unwrap();\n        let y: u16 = std::cmp::min(size.1, pos.1).try_into().unwrap();\n        self.write(&termion::cursor::Goto(x, y).to_string());\n    }\n\n    fn rewrite(&mut self, erase: bool) {\n        self.goto(0);\n        if erase {\n            self.write(termion::clear::AfterCursor.as_ref());\n        }\n        self.write(&self.get_string(0).replace(\"\\n\", \"\\n\\r\"));\n        self.goto(self.head);\n        self.flush();\n    }\n\n    pub fn insert(&mut self, c: char) {\n        self.chars.insert(self.head, c);\n        self.head += 1;\n        self.rewrite(false);\n    }\n\n    pub fn backspace(&mut self) {\n        if self.head <= self.prompt.chars().count() {\n            return;\n        }\n        self.head -= 1;\n        self.chars.remove(self.head);\n        self.rewrite(true);\n    }\n\n    pub fn delete(&mut self) {\n        if self.head >= self.chars.len() {\n            return;\n        }\n        self.chars.remove(self.head);\n        self.rewrite(true);\n    }\n\n    pub fn get_string(&self, from: usize) -> String {\n        self.chars[from..].iter().collect()\n    }\n\n    pub fn goto_origin(&mut self) {\n        self.head = self.prompt.chars().count();\n        self.goto(self.head);\n        self.flush();\n    }\n\n    pub fn goto_end(&mut self) {\n        self.head = self.chars.len();\n        self.goto(self.head);\n        self.flush();\n    }\n\n    pub fn shift_cursor(&mut self, shift: i32) {\n        let prev = self.head;\n        Self::shift_in_range(\n            &mut self.head,\n            shift,\n            self.prompt.chars().count(),\n            self.chars.len(),\n        );\n\n        if prev == self.head {\n            self.cloop();\n            return;\n        }\n\n        self.goto(self.head);\n        self.flush();\n    }\n\n    pub fn check_scroll(&mut self) {\n        let extra_lines = self.head_to_cursor_pos(self.chars.len(), 0).1;\n        let row = Terminal::size().1;\n\n        if self.prompt_row + extra_lines > row {\n            let ans = row as isize - extra_lines as isize;\n            self.prompt_row = std::cmp::max(ans, 1) as usize;\n        }\n    }\n\n    pub fn check_terminal_size(&mut self /*, prev_size: &mut (usize, usize)*/) {\n        //if *prev_size == Terminal::size() {\n        if self.size == Terminal::size() {\n            return;\n        }\n\n        let from_under = self.size.1 as isize - self.prompt_row as isize;\n        //*prev_size = Terminal::size();\n        self.size = Terminal::size();\n\n        let cur_row = self.size.1 as isize - from_under;\n        self.prompt_row = std::cmp::max(cur_row, 1) as usize;\n    }\n\n    pub fn call_history(&mut self, inc: i32, core: &mut ShellCore) {\n        let prev = self.hist_ptr;\n        let prev_str = self.get_string(self.prompt.chars().count());\n        Self::shift_in_range(&mut self.hist_ptr, inc, 0, isize::MAX as usize);\n\n        self.chars = self.prompt.chars().collect();\n        self.chars.extend(\n            core.fetch_history(self.hist_ptr, prev, prev_str)\n                .replace(\"↵ \\0\", \"\\n\")\n                .chars(),\n        );\n        self.head = self.chars.len();\n        self.rewrite(true);\n    }\n\n    pub fn set_double_tab_completion(&mut self, core: &ShellCore) {\n        let tail = match arg::has_option(\"nospace\", &core.completion.current.o_options) {\n            true => \"\",\n            false => \" \",\n        };\n\n        let s = self.completion_candidate.clone() + tail;\n        self.replace_input(&s);\n    }\n\n    pub fn cloop(&mut self) {\n        print!(\"\\x07\");\n        self.flush();\n    }\n\n    fn completion_finish_check(&mut self) {\n        match self.prev_key {\n            event::Key::Char('\\t')\n            | event::Key::Left\n            | event::Key::Down\n            | event::Key::Right\n            | event::Key::Up => (),\n            _ => {\n                self.tab_num = 0;\n                self.completion_candidate = String::new();\n            }\n        }\n    }\n}\n\nfn signal_check(core: &mut ShellCore, term: &mut Terminal) -> Result<bool, InputError> {\n    if core.sigint.load(Relaxed) || core.trapped.iter_mut().any(|t| t.0.load(Relaxed)) {\n        term.write(\"\\r\\n\");\n        return Err(InputError::Interrupt);\n    }\n    Ok(true)\n}\n\npub fn read_line(core: &mut ShellCore, prompt: &str) -> Result<String, InputError> {\n    let mut term = Terminal::new(core, prompt);\n    signal_check(core, &mut term)?;\n\n    core.history.insert(0, String::new());\n\n    for c in io::stdin().keys() {\n        let c = c.unwrap();\n\n        if let Err(e) = signal_check(core, &mut term) {\n            core.history.remove(0);\n            return Err(e);\n        }\n\n        term.check_terminal_size();\n        match key::action(core, &mut term, &c) {\n            Ok(true) => break,\n            Ok(false) => term.prev_key = c,\n            Err(e) => {\n                core.history.remove(0);\n                return Err(e);\n            }\n        }\n\n        term.completion_finish_check();\n        term.check_scroll();\n    }\n\n    let ans = term.get_string(term.prompt.chars().count());\n    core.history[0] = ans.trim_end().to_string();\n    Ok(ans)\n}\n\n/*TODO: The following version uses async_stdin and enables\n * the shell be interrupted by signals. However, some bugs\n * found around cursor control. Moreover, this implementation\n * causes a SIGTTIN signal after a command runs.\n */\n/*\npub fn read_line(core: &mut ShellCore, prompt: &str) -> Result<String, InputError>{\n    let mut term = Terminal::new(core, prompt);\n    core.history.insert(0, String::new());\n\n    let mut stdin = termion::async_stdin().keys();\n\n    loop {\n        if let Err(e) = signal_check(core, &mut term) {\n            core.history.remove(0);\n            return Err(e);\n        }\n\n        let c = match stdin.next() {\n            Some(k) => k.as_ref().unwrap().clone(),\n            _ => {\n                thread::sleep(time::Duration::from_millis(10));\n                continue;\n            },\n        };\n\n        term.check_terminal_size();\n        match key::action(core, &mut term, &c) {\n            Ok(true) => break,\n            Ok(false) => term.prev_key = c,\n            Err(e) => {\n                core.history.remove(0);\n                return Err(e)\n            },\n        }\n\n        term.completion_finish_check();\n        term.check_scroll();\n    }\n\n    let ans = term.get_string(term.prompt.chars().count());\n    core.history[0] = ans.trim_end().to_string();\n    Ok(ans)\n}\n*/\n"
  },
  {
    "path": "src/feeder.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod scanner;\nmod terminal;\n\nuse crate::error::input::InputError;\nuse crate::error::parse::ParseError;\nuse crate::{utils, ShellCore};\nuse std::fs::File;\nuse std::io::{BufRead, BufReader, Lines};\nuse std::sync::atomic::Ordering::Relaxed;\n\n#[derive(Debug, Default)]\npub struct Feeder {\n    remaining: String,\n    backup: Vec<String>,\n    pub nest: Vec<(String, Vec<String>)>,\n    pub lineno: usize,\n    pub lineno_addition: usize,\n    script_lines: Option<Lines<BufReader<File>>>,\n    pub main_feeder: bool,\n    c_mode_buffer: Vec<String>,\n    c_mode: bool,\n}\n\nimpl Feeder {\n    pub fn new(s: &str) -> Feeder {\n        Feeder {\n            remaining: s.to_string(),\n            nest: vec![(\"\".to_string(), vec![])],\n            lineno: 1,\n            ..Default::default()\n        }\n    }\n\n    pub fn new_c_mode(s: String) -> Feeder {\n        Feeder {\n            nest: vec![(\"\".to_string(), vec![])],\n            lineno: 1,\n            c_mode_buffer: s.split('\\n').map(|s| s.to_string()).collect(),\n            c_mode: true,\n            ..Default::default()\n        }\n    }\n\n    pub fn set_file(&mut self, s: &str) -> Result<(), InputError> {\n        let file = match File::open(s) {\n            Ok(f) => f,\n            Err(_) => return Err(InputError::NoSuchFile(s.to_string())),\n        };\n        self.script_lines = Some(BufReader::new(file).lines());\n        Ok(())\n    }\n\n    pub fn consume(&mut self, cutpos: usize) -> String {\n        let tail = self.remaining.split_off(cutpos);\n        let ans = self.remaining.to_string();\n        self.remaining = tail;\n        let lineno_org = self.lineno;\n        self.lineno += ans.chars().filter(|c| *c == '\\n').count();\n\n        while self.lineno > lineno_org {\n            if self.lineno_addition == 0 {\n                break;\n            }\n\n            self.lineno_addition -= 1;\n            self.lineno -= 1;\n        }\n\n        ans\n    }\n\n    pub fn refer(&mut self, cutpos: usize) -> &str {\n        &self.remaining[0..cutpos]\n    }\n\n    pub fn set_backup(&mut self) {\n        self.backup.push(self.remaining.clone());\n    }\n\n    pub fn pop_backup(&mut self) {\n        self.backup\n            .pop()\n            .expect(\"SUSHI INTERNAL ERROR (backup error)\");\n    }\n\n    pub fn add_backup(&mut self, line: &str) {\n        for b in self.backup.iter_mut() {\n            if b.ends_with(\"\\\\\\n\") {\n                b.pop();\n                b.pop();\n            }\n            *b += line;\n        }\n    }\n\n    pub fn rewind(&mut self) {\n        self.remaining = self\n            .backup\n            .pop()\n            .expect(\"SUSHI INTERNAL ERROR (backup error)\");\n    }\n\n    fn read_script(&mut self) -> Result<String, InputError> {\n        if self.c_mode {\n            if self.c_mode_buffer.is_empty() {\n                return Err(InputError::Eof);\n            }\n\n            return Ok(self.c_mode_buffer.remove(0) + \"\\n\");\n        }\n\n        if let Some(lines) = self.script_lines.as_mut() {\n            match lines.next() {\n                Some(Ok(line)) => return Ok(line + \"\\n\"),\n                _ => return Err(InputError::Eof),\n            }\n        }\n\n        utils::read_line_stdin_unbuffered(\"\")\n    }\n\n    fn feed_additional_line_core(&mut self, core: &mut ShellCore) -> Result<(), InputError> {\n        if !self.main_feeder {\n            return Err(InputError::Eof);\n        }\n\n        if core.sigint.load(Relaxed) {\n            return Err(InputError::Interrupt);\n        }\n\n        let line = match core.db.flags.contains('i') && self.script_lines.is_none() {\n            true => terminal::read_line(core, \"PS2\"),\n            false => self.read_script(),\n        };\n\n        line.map(|ln| {\n            self.add_line(ln.clone(), core);\n            self.add_backup(&ln);\n        })\n    }\n\n    pub fn feed_additional_line(&mut self, core: &mut ShellCore) -> Result<(), ParseError> {\n        match self.feed_additional_line_core(core) {\n            Ok(()) => Ok(()),\n            Err(InputError::Interrupt) => {\n                core.db.exit_status = 130;\n                Err(ParseError::Input(InputError::Interrupt))\n            }\n            Err(e) => {\n                core.db.exit_status = 2;\n                Err(ParseError::Input(e))\n            }\n        }\n    }\n\n    pub fn feed_line(&mut self, core: &mut ShellCore) -> Result<(), InputError> {\n        let line = match core.db.flags.contains('i') && self.script_lines.is_none() {\n            true => terminal::read_line(core, \"PS1\"),\n            false => self.read_script(),\n        };\n\n        line.map(|ln| self.add_line(ln, core))\n    }\n\n    pub fn add_line(&mut self, line: String, core: &mut ShellCore) {\n        if core.db.flags.contains('v') {\n            eprint!(\"{}\", &line);\n        }\n\n        match self.remaining.len() {\n            0 => self.remaining = line,\n            _ => self.remaining += &line,\n        };\n    }\n\n    pub fn replace(&mut self, num: usize, to: &str) {\n        self.consume(num);\n        self.lineno_addition = to.chars().filter(|c| *c == '\\n').count();\n\n        self.remaining = to.to_string() + &self.remaining;\n    }\n\n    pub fn starts_with(&self, s: &str) -> bool {\n        self.remaining.starts_with(s)\n    }\n\n    pub fn starts_withs<T: AsRef<str>>(&self, vs: &[T]) -> bool {\n        vs.iter().any(|s| self.remaining.starts_with(s.as_ref()))\n    }\n\n    pub fn len(&self) -> usize {\n        self.remaining.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.remaining.is_empty()\n    }\n}\n"
  },
  {
    "path": "src/i18n.rs",
    "content": "//SPDX-FileCopyrightText: 2022 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\n///// Internationalization with Fluent and system language detection /////\n\nuse fluent_bundle::{FluentBundle, FluentResource};\nuse once_cell::unsync::OnceCell;\nuse std::thread_local;\nuse unic_langid::LanguageIdentifier;\n\nthread_local! {\n    pub static FLUENT_BUNDLE: OnceCell<FluentBundle<FluentResource>> = const { OnceCell::new() };\n}\n\n#[cfg(feature = \"lang_ar\")]\npub static FTL_AR: &str = include_str!(\"../i18n/ar.ftl\");\n#[cfg(feature = \"lang_da\")]\npub static FTL_DA: &str = include_str!(\"../i18n/da.ftl\");\n#[cfg(feature = \"lang_de\")]\npub static FTL_DE: &str = include_str!(\"../i18n/de.ftl\");\n#[cfg(feature = \"lang_el\")]\npub static FTL_EL: &str = include_str!(\"../i18n/el.ftl\");\n#[cfg(feature = \"lang_en\")]\npub static FTL_EN: &str = include_str!(\"../i18n/en.ftl\");\n#[cfg(feature = \"lang_es\")]\npub static FTL_ES: &str = include_str!(\"../i18n/es.ftl\");\n#[cfg(feature = \"lang_fi\")]\npub static FTL_FI: &str = include_str!(\"../i18n/fi.ftl\");\n#[cfg(feature = \"lang_fr\")]\npub static FTL_FR: &str = include_str!(\"../i18n/fr.ftl\");\n#[cfg(feature = \"lang_hi\")]\npub static FTL_HI: &str = include_str!(\"../i18n/hi.ftl\");\n#[cfg(feature = \"lang_it\")]\npub static FTL_IT: &str = include_str!(\"../i18n/it.ftl\");\n#[cfg(feature = \"lang_ja\")]\npub static FTL_JA: &str = include_str!(\"../i18n/ja.ftl\");\n#[cfg(feature = \"lang_ko\")]\npub static FTL_KO: &str = include_str!(\"../i18n/ko.ftl\");\n#[cfg(feature = \"lang_nl\")]\npub static FTL_NL: &str = include_str!(\"../i18n/nl.ftl\");\n#[cfg(feature = \"lang_no\")]\npub static FTL_NO: &str = include_str!(\"../i18n/no.ftl\");\n#[cfg(feature = \"lang_pl\")]\npub static FTL_PL: &str = include_str!(\"../i18n/pl.ftl\");\n#[cfg(feature = \"lang_pt\")]\npub static FTL_PT: &str = include_str!(\"../i18n/pt.ftl\");\n#[cfg(feature = \"lang_ru\")]\npub static FTL_RU: &str = include_str!(\"../i18n/ru.ftl\");\n#[cfg(feature = \"lang_sl\")]\npub static FTL_SL: &str = include_str!(\"../i18n/sl.ftl\");\n#[cfg(feature = \"lang_sv\")]\npub static FTL_SV: &str = include_str!(\"../i18n/sv.ftl\");\n#[cfg(feature = \"lang_sw\")]\npub static FTL_SW: &str = include_str!(\"../i18n/sw.ftl\");\n#[cfg(feature = \"lang_uk\")]\npub static FTL_UK: &str = include_str!(\"../i18n/uk.ftl\");\n#[cfg(feature = \"lang_zh\")]\npub static FTL_ZH: &str = include_str!(\"../i18n/zh.ftl\");\n\n#[derive(Debug)]\nstruct LangEntry {\n    code: &'static str,\n    #[cfg(any(\n        feature = \"lang_ar\",\n        feature = \"lang_da\",\n        feature = \"lang_de\",\n        feature = \"lang_el\",\n        feature = \"lang_en\",\n        feature = \"lang_es\",\n        feature = \"lang_fi\",\n        feature = \"lang_fr\",\n        feature = \"lang_hi\",\n        feature = \"lang_it\",\n        feature = \"lang_ja\",\n        feature = \"lang_ko\",\n        feature = \"lang_nl\",\n        feature = \"lang_no\",\n        feature = \"lang_pl\",\n        feature = \"lang_pt\",\n        feature = \"lang_ru\",\n        feature = \"lang_sl\",\n        feature = \"lang_sv\",\n        feature = \"lang_sw\",\n        feature = \"lang_uk\",\n        feature = \"lang_zh\"\n    ))]\n    ftl: &'static str,\n}\n\nconst LANGS: &[LangEntry] = &[\n    #[cfg(feature = \"lang_ar\")]\n    LangEntry {\n        code: \"ar\",\n        ftl: FTL_AR,\n    },\n    #[cfg(feature = \"lang_da\")]\n    LangEntry {\n        code: \"da\",\n        ftl: FTL_DA,\n    },\n    #[cfg(feature = \"lang_de\")]\n    LangEntry {\n        code: \"de\",\n        ftl: FTL_DE,\n    },\n    #[cfg(feature = \"lang_el\")]\n    LangEntry {\n        code: \"el\",\n        ftl: FTL_EL,\n    },\n    #[cfg(feature = \"lang_en\")]\n    LangEntry {\n        code: \"en\",\n        ftl: FTL_EN,\n    },\n    #[cfg(feature = \"lang_es\")]\n    LangEntry {\n        code: \"es\",\n        ftl: FTL_ES,\n    },\n    #[cfg(feature = \"lang_fi\")]\n    LangEntry {\n        code: \"fi\",\n        ftl: FTL_FI,\n    },\n    #[cfg(feature = \"lang_fr\")]\n    LangEntry {\n        code: \"fr\",\n        ftl: FTL_FR,\n    },\n    #[cfg(feature = \"lang_hi\")]\n    LangEntry {\n        code: \"hi\",\n        ftl: FTL_HI,\n    },\n    #[cfg(feature = \"lang_it\")]\n    LangEntry {\n        code: \"it\",\n        ftl: FTL_IT,\n    },\n    #[cfg(feature = \"lang_ja\")]\n    LangEntry {\n        code: \"ja\",\n        ftl: FTL_JA,\n    },\n    #[cfg(feature = \"lang_ko\")]\n    LangEntry {\n        code: \"ko\",\n        ftl: FTL_KO,\n    },\n    #[cfg(feature = \"lang_nl\")]\n    LangEntry {\n        code: \"nl\",\n        ftl: FTL_NL,\n    },\n    #[cfg(feature = \"lang_no\")]\n    LangEntry {\n        code: \"no\",\n        ftl: FTL_NO,\n    },\n    #[cfg(feature = \"lang_pl\")]\n    LangEntry {\n        code: \"pl\",\n        ftl: FTL_PL,\n    },\n    #[cfg(feature = \"lang_pt\")]\n    LangEntry {\n        code: \"pt\",\n        ftl: FTL_PT,\n    },\n    #[cfg(feature = \"lang_ru\")]\n    LangEntry {\n        code: \"ru\",\n        ftl: FTL_RU,\n    },\n    #[cfg(feature = \"lang_sl\")]\n    LangEntry {\n        code: \"sl\",\n        ftl: FTL_SL,\n    },\n    #[cfg(feature = \"lang_sv\")]\n    LangEntry {\n        code: \"sv\",\n        ftl: FTL_SV,\n    },\n    #[cfg(feature = \"lang_sw\")]\n    LangEntry {\n        code: \"sw\",\n        ftl: FTL_SW,\n    },\n    #[cfg(feature = \"lang_uk\")]\n    LangEntry {\n        code: \"uk\",\n        ftl: FTL_UK,\n    },\n    #[cfg(feature = \"lang_zh\")]\n    LangEntry {\n        code: \"zh\",\n        ftl: FTL_ZH,\n    },\n];\n\npub fn get_system_language() -> String {\n    fn extract_lang(s: &str) -> Option<String> {\n        let first_part = s.split(&['_', '.']).next()?;\n        if first_part.is_empty() {\n            None\n        } else {\n            Some(first_part.to_string())\n        }\n    }\n\n    for var in &[\"LC_ALL\", \"LC_MESSAGES\", \"LANG\"] {\n        if let Ok(val) = std::env::var(var) {\n            if let Some(lang) = extract_lang(&val) {\n                return lang;\n            }\n        }\n    }\n\n    \"en\".to_string()\n}\n\npub fn load_fluent_bundle() -> Option<FluentBundle<FluentResource>> {\n    let lang = get_system_language();\n\n    let entry = LANGS\n        .iter()\n        .find(|e| e.code == lang)\n        .or_else(|| LANGS.iter().find(|e| e.code == \"en\"))?;\n\n    let res = FluentResource::try_new(entry.ftl.to_string()).ok()?;\n    let langid: LanguageIdentifier = entry.code.parse().ok()?;\n    let mut bundle = FluentBundle::new(vec![langid]);\n    bundle.add_resource(res).ok()?;\n\n    Some(bundle)\n}\n\npub fn fl(key: &str) -> String {\n    FLUENT_BUNDLE.with(|cell| {\n        let bundle =\n            cell.get_or_init(|| load_fluent_bundle().expect(\"Could not load translation bundle\"));\n\n        let mut errors = vec![];\n        bundle\n            .get_message(key)\n            .and_then(|msg| msg.value())\n            .map(|pattern| {\n                bundle\n                    .format_pattern(pattern, None, &mut errors)\n                    .to_string()\n            })\n            .unwrap_or_else(|| format!(\"{{{key}}}\"))\n    })\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod core;\nmod elements;\nmod error;\nmod feeder;\nmod i18n;\nmod main_c_option;\nmod proc_ctrl;\nmod signal;\nmod utils;\n\n// Externals crates\nuse std::sync::atomic::Ordering::Relaxed;\nuse std::{env, process};\n\n// Internals crates\nuse crate::core::builtins::source;\nuse crate::core::{builtins, ShellCore};\nuse crate::elements::script::Script;\nuse crate::feeder::Feeder;\nuse crate::i18n::FLUENT_BUNDLE;\nuse builtins::option;\nuse error::input::InputError;\nuse utils::{arg, exit, file_check};\n\n///// Main program entry point /////\n\nfn main() {\n    let mut args = arg::dissolve_options_main();\n\n    if args.iter().any(|a| a == \"--version\") {\n        show_version();\n        return;\n    }\n\n    if args.iter().any(|a| a == \"--help\") {\n        show_help();\n        return;\n    }\n\n    let command = args.first().cloned().unwrap_or_else(|| \"sush\".to_string());\n    let script_parts = consume_file_and_subsequents(&mut args);\n\n    let mut c_opt = false;\n    if let Some(opt) = args.last() {\n        if opt == \"-c\" {\n            c_opt = true;\n            args.pop();\n        }\n    }\n\n    let mut core = ShellCore::new();\n\n    let bundle = match i18n::load_fluent_bundle() {\n        Some(b) => b,\n        None => {\n            eprintln!(\"No resources found for language\");\n            std::process::exit(1);\n        }\n    };\n\n    FLUENT_BUNDLE.with(|cell| {\n        cell.set(bundle).ok();\n    });\n\n    set_o_options(&mut args, &mut core);\n    set_short_options(&mut args, &mut core);\n\n    if !c_opt {\n        set_parameters(script_parts, &mut core, &command);\n    } else {\n        main_c_option::set_parameters(&script_parts, &mut core, &args[0]);\n        main_c_option::run_and_exit(&args, &script_parts, &mut core);\n    }\n\n    let _ = core.configure();\n    signal::run_signal_check(&mut core);\n\n    if core.script_name == \"-\" {\n        read_rc_file(&mut core);\n    }\n    main_loop(&mut core, &command);\n}\n\n///// Parses arguments and sets up shell options, parameters, and config. /////\n\nfn consume_file_and_subsequents(args: &mut Vec<String>) -> Vec<String> {\n    let mut skip = false;\n    let mut pos = None;\n\n    for (i, a) in args.iter().enumerate().skip(1) {\n        if skip {\n            skip = false;\n            continue;\n        }\n\n        if a.starts_with(\"-o\") || a.starts_with(\"+o\") {\n            skip = true;\n            continue;\n        }\n\n        if a.starts_with('-') || a.starts_with('+') {\n            continue;\n        }\n\n        pos = Some(i);\n        break;\n    }\n\n    if pos.is_none() {\n        return vec![];\n    }\n\n    args.split_off(pos.unwrap())\n}\n\nfn set_o_options(args: &mut Vec<String>, core: &mut ShellCore) {\n    let mut options = vec![];\n    loop {\n        if let Some(opt) = arg::consume_with_next_arg(\"-o\", args) {\n            options.push((opt, true));\n            continue;\n        }\n        if let Some(opt) = arg::consume_with_next_arg(\"+o\", args) {\n            options.push((opt, false));\n            continue;\n        }\n\n        break;\n    }\n\n    for opt in options {\n        if let Err(e) = core.options.set(&opt.0, opt.1) {\n            e.print(core);\n            process::exit(2);\n        }\n    }\n}\n\nfn set_short_options(args: &mut Vec<String>, core: &mut ShellCore) {\n    if arg::consume_arg(\"-b\", args) {\n        core.compat_bash = true;\n        core.db.flags += \"b\";\n    }\n\n    if arg::consume_arg(\"-n\", args) {\n        core.db.flags += \"n\";\n    }\n\n    if let Err(e) = option::set_options(core, &mut args[1..].to_vec()) {\n        e.print(core);\n        core.db.exit_status = 2;\n        exit::normal(core);\n    }\n}\n\nfn read_rc_file(core: &mut ShellCore) {\n    if !core.db.flags.contains(\"i\") {\n        return;\n    }\n\n    let mut dir = core.db.get_param(\"CARGO_MANIFEST_DIR\").unwrap_or_default();\n    if dir.is_empty() {\n        dir = core.db.get_param(\"HOME\").unwrap_or_default();\n    }\n\n    let rc_file = dir + \"/.sushrc\";\n\n    if file_check::is_regular_file(&rc_file) {\n        core.db.exit_status = source::source(core, &[\".\".to_string(), rc_file]);\n    }\n\n    core.continue_counter = 0; //patch for Ubuntu 25.04 bash completion\n}\n\nfn set_parameters(script_parts: Vec<String>, core: &mut ShellCore, command: &str) {\n    match script_parts.is_empty() {\n        true => {\n            core.db.position_parameters[0] = vec![command.to_string()];\n            core.script_name = \"-\".to_string();\n        }\n        false => {\n            core.db.position_parameters[0] = script_parts;\n            core.script_name = core.db.position_parameters[0][0].clone();\n        }\n    }\n}\n\n///// Core shell flow: input, parsing, execution, and history. /////\n\nfn main_loop(core: &mut ShellCore, command: &str) {\n    let mut feeder = Feeder::new(\"\");\n    feeder.main_feeder = true;\n\n    if core.script_name != \"-\" {\n        core.db.flags.retain(|f| f != 'i');\n        if feeder.set_file(&core.script_name).is_err() {\n            eprintln!(\n                \"{}: {}: No such file or directory\",\n                command, &core.script_name\n            );\n            process::exit(2);\n        }\n    }\n\n    if core.db.flags.contains('i') {\n        show_message();\n    }\n\n    loop {\n        match feed_script(&mut feeder, core) {\n            (true, false) => {}\n            (false, true) => break,\n            _ => parse_and_exec(&mut feeder, core, true),\n        }\n\n        if core.options.query(\"onecmd\") {\n            break;\n        }\n    }\n    core.write_history_to_file();\n    exit::normal(core);\n}\n\nfn feed_script(feeder: &mut Feeder, core: &mut ShellCore) -> (bool, bool) {\n    if let Err(e) = core.jobtable_check_status() {\n        //(continue, break)\n        e.print(core);\n    }\n\n    if core.db.flags.contains('i') && core.options.query(\"monitor\") {\n        core.jobtable_print_status_change();\n    }\n\n    match feeder.feed_line(core) {\n        Ok(()) => (false, false),\n        Err(InputError::Interrupt) => {\n            signal::input_interrupt_check(feeder, core);\n            signal::check_trap(core);\n            (true, false)\n        }\n        _ => (false, true),\n    }\n}\n\nfn parse_and_exec(feeder: &mut Feeder, core: &mut ShellCore, set_hist: bool) {\n    core.sigint.store(false, Relaxed);\n    match Script::parse(feeder, core, false) {\n        Ok(Some(mut s)) => {\n            if core.db.flags.contains('n') {\n                return;\n            }\n            if let Err(e) = s.exec(core) {\n                e.print(core);\n            }\n            if set_hist {\n                set_history(core, &s.get_text());\n            }\n        }\n        Err(e) => {\n            e.print(core);\n            feeder.consume(feeder.len());\n            feeder.nest = vec![(\"\".to_string(), vec![])];\n        }\n        _ => {\n            feeder.consume(feeder.len());\n            feeder.nest = vec![(\"\".to_string(), vec![])];\n        }\n    }\n    core.sigint.store(false, Relaxed);\n}\n\nfn set_history(core: &mut ShellCore, s: &str) {\n    if core.db.flags.contains('i') || core.history.is_empty() {\n        return;\n    }\n\n    core.history[0] = s.trim_end().replace(\"\\n\", \"↵ \\0\").to_string();\n    if core.history[0].is_empty() || (core.history.len() > 1 && core.history[0] == core.history[1])\n    {\n        core.history.remove(0);\n    }\n}\n\n///// Text related functions /////\n\nfn show_message() {\n    eprintln!(\n        \"Sushi shell (a.k.a. Sush), {} {} - {}\",\n        i18n::fl(\"version\"),\n        env!(\"CARGO_PKG_VERSION\"),\n        env!(\"CARGO_BUILD_PROFILE\")\n    );\n}\n\nfn show_version() {\n    eprintln!(\n        \"Sushi shell (a.k.a. Sush), {} {} - {}\\n\\\n         © 2024 Ryuichi Ueda\\n\\\n         {}: BSD 3-Clause\\n\\\n         \\n\\\n         {}\\n\",\n        i18n::fl(\"version\"),\n        env!(\"CARGO_PKG_VERSION\"),\n        env!(\"CARGO_BUILD_PROFILE\"),\n        i18n::fl(\"license\"),\n        i18n::fl(\"text-version\")\n    );\n    process::exit(0);\n}\n\nfn show_help() {\n    eprintln!(\n        \"Sushi shell (a.k.a. Sush), {} {} - {}\\n\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n{}\\n\\n{}\",\n        i18n::fl(\"version\"),\n        env!(\"CARGO_PKG_VERSION\"),\n        env!(\"CARGO_BUILD_PROFILE\"),\n        i18n::fl(\"usage\"),\n        i18n::fl(\"options\"),\n        i18n::fl(\"comp-commands\"),\n        i18n::fl(\"builtins\"),\n        i18n::fl(\"parameters\"),\n        i18n::fl(\"shopt\"),\n        i18n::fl(\"variables-born\"),\n        i18n::fl(\"variables-bash\"),\n        i18n::fl(\"text-help\")\n    );\n    process::exit(0);\n}\n"
  },
  {
    "path": "src/main_c_option.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::{builtins, ShellCore};\nuse crate::feed_script;\nuse crate::feeder::Feeder;\nuse crate::parse_and_exec;\nuse crate::signal;\nuse crate::utils::exit;\nuse builtins::option;\nuse std::process;\n\npub fn set_parameters(c_parts: &[String], core: &mut ShellCore, command: &str) {\n    let parameters = if c_parts.len() > 1 {\n        c_parts[1..].to_vec()\n    } else {\n        vec![command.to_string()]\n    };\n\n    if let Err(e) = option::set_positions_c(core, &parameters) {\n        e.print(core);\n        core.db.exit_status = 2;\n        exit::normal(core);\n    }\n}\n\npub fn run_and_exit(args: &[String], c_parts: &[String], core: &mut ShellCore) {\n    let _ = core.configure_c_mode();\n\n    if c_parts.is_empty() {\n        println!(\"{}: -c: option requires an argument\", &args[0]);\n        process::exit(2);\n    }\n\n    signal::run_signal_check(core);\n    core.db.flags.retain(|f| f != 'i');\n\n    core.db.flags += \"c\";\n    if core.db.flags.contains('v') {\n        eprintln!(\"{}\", &c_parts[0]);\n    }\n\n    let mut feeder = Feeder::new_c_mode(c_parts[0].clone());\n    feeder.main_feeder = true;\n\n    loop {\n        match feed_script(&mut feeder, core) {\n            (true, false) => {}\n            (false, true) => break,\n            _ => parse_and_exec(&mut feeder, core, false),\n        }\n    }\n    exit::normal(core);\n}\n"
  },
  {
    "path": "src/proc_ctrl.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::error::exec::ExecError;\nuse crate::utils::c_string;\nuse crate::{error, exit, signal, Feeder, Script, ShellCore};\nuse nix::errno::Errno;\nuse nix::sys::resource::UsageWho;\nuse nix::sys::signal::Signal;\nuse nix::sys::wait::{WaitPidFlag, WaitStatus};\nuse nix::sys::{resource, wait};\nuse nix::time::{clock_gettime, ClockId};\nuse nix::unistd;\nuse nix::unistd::Pid;\nuse std::ffi::CString;\nuse std::process;\nuse std::sync::atomic::Ordering::Relaxed;\n\npub fn wait_pipeline(\n    core: &mut ShellCore,\n    pids: Vec<Option<Pid>>,\n    exclamation: bool,\n    time: bool,\n) -> Vec<WaitStatus> {\n    if pids.len() == 1 && pids[0].is_none() {\n        if time {\n            show_time(core);\n        }\n        if exclamation {\n            core.flip_exit_status();\n        }\n        close_proc_sub(core);\n        exit::check_e_option(core);\n        return vec![];\n    }\n\n    let last_exit_status = core.db.exit_status;\n    let mut pipestatus = vec![];\n    let mut ans = vec![];\n    for pid in &pids {\n        if pid.is_some() {\n            //None: lastpipe\n            let ws = wait_process(core, pid.unwrap());\n            ans.push(ws);\n            pipestatus.push(core.db.exit_status);\n        } else {\n            pipestatus.push(last_exit_status);\n            core.db.exit_status = last_exit_status;\n        }\n    }\n\n    if time {\n        show_time(core);\n    }\n    let _ = set_foreground(core);\n    let _ = core.db.init_array(\n        \"PIPESTATUS\",\n        Some(pipestatus.iter().map(|e| e.to_string()).collect()),\n        None,\n        false,\n    );\n\n    if core.options.query(\"pipefail\") {\n        pipestatus.retain(|e| *e != 0);\n\n        if !pipestatus.is_empty() {\n            core.db.exit_status = pipestatus[pipestatus.len() - 1];\n        }\n    }\n\n    if exclamation {\n        core.flip_exit_status();\n    }\n\n    close_proc_sub(core);\n    exit::check_e_option(core);\n\n    ans\n}\n\nfn wait_process(core: &mut ShellCore, child: Pid) -> WaitStatus {\n    let waitflags = match core.is_subshell {\n        true => None,\n        false => Some(WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED),\n    };\n\n    let ws = wait::waitpid(child, waitflags);\n\n    core.db.exit_status = match ws {\n        Ok(WaitStatus::Exited(_pid, status)) => status,\n        Ok(WaitStatus::Signaled(pid, signal, coredump)) => error::signaled(pid, signal, coredump),\n        Ok(WaitStatus::Stopped(pid, signal)) => {\n            eprintln!(\"Stopped Pid: {pid:?}, Signal: {signal:?}\");\n            148\n        }\n        Ok(unsupported) => {\n            ExecError::UnsupportedWaitStatus(unsupported).print(core);\n            1\n        }\n        Err(err) => {\n            let msg = format!(\"Error: {err:?}\");\n            exit::internal(&msg);\n        }\n    };\n\n    if core.db.exit_status == 130 {\n        core.sigint.store(true, Relaxed);\n    }\n    ws.expect(\"SUSH INTERNAL ERROR: no wait status\")\n}\n\nfn set_foreground(core: &mut ShellCore) -> Result<(), ExecError> {\n    let fd = match core.tty_fd.as_ref() {\n        Some(fd) => fd,\n        _ => return Ok(()),\n    };\n\n    let pgid = unistd::getpgid(Some(Pid::from_raw(0)))\n        .unwrap_or_else(|_| panic!(\"{}\", error::internal(\"cannot get pgid\")));\n\n    if let Ok(n) = core.fds.tcgetpgrp(*fd) {\n        if n == pgid {\n            return Ok(());\n        }\n    }\n\n    signal::ignore(Signal::SIGTTOU); //SIGTTOUを無視\n    core.fds.tcsetpgrp(*fd, pgid)?;\n    signal::restore(Signal::SIGTTOU); //SIGTTOUを受け付け\n    Ok(())\n}\n\npub fn set_pgid(core: &mut ShellCore, pid: Pid, pgid: Pid) {\n    let _ = unistd::setpgid(pid, pgid);\n    let lastpipe = !core.db.flags.contains('m') && core.shopts.query(\"lastpipe\");\n\n    if !lastpipe && pid.as_raw() == 0 && pgid.as_raw() == 0 {\n        //以下3行追加\n        let _ = set_foreground(core);\n    }\n}\n\nfn show_time(core: &ShellCore) {\n    let real_end_time = clock_gettime(ClockId::CLOCK_MONOTONIC).unwrap();\n\n    let core_usage = resource::getrusage(UsageWho::RUSAGE_SELF).unwrap();\n    let children_usage = resource::getrusage(UsageWho::RUSAGE_CHILDREN).unwrap();\n\n    let real_diff = real_end_time - core.measured_time.real;\n    eprintln!(\n        \"\\nreal\\t{}m{}.{:06}s\",\n        real_diff.tv_sec() / 60,\n        real_diff.tv_sec() % 60,\n        real_diff.tv_nsec() / 1000\n    );\n    let user_diff = core_usage.user_time() + children_usage.user_time() - core.measured_time.user;\n    eprintln!(\n        \"user\\t{}m{}.{:06}s\",\n        user_diff.tv_sec() / 60,\n        user_diff.tv_sec() % 60,\n        user_diff.tv_usec()\n    );\n    let sys_diff = core_usage.system_time() + children_usage.system_time() - core.measured_time.sys;\n    eprintln!(\n        \"sys \\t{}m{}.{:06}s\",\n        sys_diff.tv_sec() / 60,\n        sys_diff.tv_sec() % 60,\n        sys_diff.tv_usec()\n    );\n}\n\npub fn exec_command(args: &[String], core: &mut ShellCore, fullpath: &str) -> ! {\n    let cargs = c_string::to_cargs(args);\n    let cfullpath = CString::new(fullpath.to_string()).unwrap();\n\n    if !fullpath.is_empty() {\n        let _ = unistd::execv(&cfullpath, &cargs);\n    }\n    let result = unistd::execvp(&cargs[0], &cargs);\n\n    match result {\n        Err(Errno::E2BIG) => exit::arg_list_too_long(&args[0], core),\n        Err(Errno::EACCES) => exit::permission_denied(&args[0], core),\n        Err(Errno::ENOENT) => run_command_not_found(&args[0], core),\n        Err(err) => {\n            eprintln!(\"Failed to execute. {err:?}\");\n            process::exit(127)\n        }\n        _ => exit::internal(\"never come here\"),\n    }\n}\n\nfn run_command_not_found(arg: &str, core: &mut ShellCore) -> ! {\n    if core.db.functions.contains_key(\"command_not_found_handle\") {\n        let s = \"command_not_found_handle \".to_owned() + arg;\n        let mut f = Feeder::new(&s);\n        match Script::parse(&mut f, core, false) {\n            Ok(Some(mut script)) => {\n                let _ = script.exec(core);\n            }\n            Err(e) => e.print(core),\n            _ => {}\n        }\n    }\n    exit::not_found(arg, core)\n}\n\nfn close_proc_sub(core: &mut ShellCore) {\n    while let Some(fd) = core.proc_sub_fd.pop() {\n        core.fds.close(fd);\n    }\n\n    while let Some(pid) = core.proc_sub_pid.pop() {\n        let _ = wait::waitpid(pid, None);\n    }\n}\n"
  },
  {
    "path": "src/signal.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nextern crate libc;\nuse libc::{dup2, close};\n\nuse crate::core::ShellCore;\nuse crate::feeder::Feeder;\nuse crate::Script;\nuse nix::sys::signal;\nuse nix::sys::signal::{SigHandler, Signal};\nuse signal_hook::consts;\nuse signal_hook::iterator::Signals;\nuse std::sync::atomic::Ordering::Relaxed;\nuse std::sync::Arc;\nuse std::{thread, time};\n\npub fn ignore(sig: Signal) {\n    unsafe { signal::signal(sig, SigHandler::SigIgn) }.expect(\"sush(fatal): cannot ignore signal\");\n}\n\npub fn restore(sig: Signal) {\n    unsafe { signal::signal(sig, SigHandler::SigDfl) }.expect(\"sush(fatal): cannot restore signal\");\n}\n\npub fn run_signal_check(core: &mut ShellCore) {\n    for fd in 3..10 {\n        //use FD 3~9 to prevent signal-hool from using these FDs\n            let _ = unsafe{dup2(2, fd)};\n    }\n\n    core.sigint.store(true, Relaxed);\n    let sigint = Arc::clone(&core.sigint);\n\n    thread::spawn(move || {\n        let mut signals =\n            Signals::new(vec![consts::SIGINT]).expect(\"sush(fatal): cannot prepare signal data\");\n\n        for fd in 3..10 {\n            // release FD 3~9\n            //nix::unistd::close(fd).expect(\"sush(fatal): init error\");\n            let _ = unsafe{close(fd)};\n        }\n        sigint.store(false, Relaxed);\n\n        loop {\n            thread::sleep(time::Duration::from_millis(100)); //0.1秒周期に変更\n            for signal in signals.pending() {\n                if signal == consts::SIGINT {\n                    sigint.store(true, Relaxed);\n                    eprintln!(\"^C\");\n                }\n            }\n        }\n    });\n\n    while core.sigint.load(Relaxed) {\n        thread::sleep(time::Duration::from_millis(1));\n    }\n} //thanks: https://dev.to/talzvon/handling-unix-kill-signals-in-rust-55g6\n\npub fn input_interrupt_check(feeder: &mut Feeder, core: &mut ShellCore) -> bool {\n    if !core.sigint.load(Relaxed) {\n        //core.input_interrupt {\n        return false;\n    }\n\n    core.sigint.store(false, Relaxed); //core.input_interrupt = false;\n    core.db.exit_status = 130;\n    feeder.consume(feeder.len());\n    true\n}\n\npub fn check_trap(core: &mut ShellCore) {\n    let bkup = core.db.exit_status;\n\n    let mut scripts = vec![];\n    for t in &core.trapped {\n        if t.0.load(Relaxed) {\n            scripts.push(t.1.clone());\n            t.0.store(false, Relaxed);\n        }\n    }\n\n    for s in scripts {\n        let mut feeder = Feeder::new(&s);\n        let mut script = match Script::parse(&mut feeder, core, true) {\n            Ok(None) => {\n                continue;\n            }\n            Ok(s) => s.unwrap(),\n            Err(e) => {\n                e.print(core);\n                continue;\n            }\n        };\n\n        if let Err(e) = script.exec(core) {\n            e.print(core);\n        }\n    }\n\n    core.db.exit_status = bkup;\n}\n"
  },
  {
    "path": "src/utils/arg.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\npub fn has_option(option: &str, args: &[String]) -> bool {\n    args.iter().any(|arg| arg == option)\n}\n\npub fn consume_arg(option: &str, args: &mut Vec<String>) -> bool {\n    let found = args.iter().any(|arg| arg == option);\n    if found {\n        args.retain(|arg| arg != option);\n    }\n    found\n}\n\npub fn consume_option(option: &str, args: &mut Vec<String>) -> bool {\n    for (i, a) in args.iter().enumerate() {\n        if a.starts_with(\"--\") {\n            return false;\n        }\n\n        if a == option {\n            args.remove(i);\n            return true;\n        }\n    }\n\n    false\n}\n\npub fn consume_starts_with(s: &str, args: &mut Vec<String>) -> Vec<String> {\n    let mut ans = args.clone();\n    ans.retain(|a| a.starts_with(s));\n    args.retain(|a| !a.starts_with(s));\n    ans\n}\n\npub fn consume_with_next_arg(prev_opt: &str, args: &mut Vec<String>) -> Option<String> {\n    match args.iter().position(|a| a == prev_opt) {\n        Some(pos) => match pos + 1 >= args.len() {\n            true => None,\n            false => {\n                args.remove(pos);\n                Some(args.remove(pos))\n            }\n        },\n        None => None,\n    }\n}\n\npub fn consume_with_subsequents(prev_opt: &str, args: &mut Vec<String>) -> Vec<String> {\n    match args.iter().position(|a| a == prev_opt) {\n        Some(pos) => {\n            let ans = args[pos..].to_vec();\n            *args = args[..pos].to_vec();\n            ans\n        }\n        None => vec![],\n    }\n}\n\nfn add_prefix(prefix: char, opts: &str) -> Vec<String> {\n    if opts.is_empty() {\n        return vec![prefix.to_string()];\n    }\n\n    opts.chars().map(|c| format!(\"{prefix}{c}\")).collect()\n}\n\npub fn dissolve_option(arg: &str) -> Vec<String> {\n    if arg.starts_with(\"--\") {\n        vec![arg.to_string()]\n    } else if let Some(opts) = arg.strip_prefix('-') {\n        add_prefix('-', opts)\n    } else if let Some(opts) = arg.strip_prefix('+') {\n        add_prefix('+', opts)\n    } else {\n        vec![arg.to_string()]\n    }\n}\n\npub fn dissolve_options(args: &[String]) -> Vec<String> {\n    let mut ans = vec![];\n    let mut stop = false;\n    for a in args {\n        if a == \"--\" {\n            stop = true;\n        }\n\n        match stop {\n            true => ans.push(a.to_string()),\n            false => ans.append(&mut dissolve_option(a)),\n        }\n    }\n\n    ans\n}\n\npub fn dissolve_options_main() -> Vec<String> {\n    let mut ans = vec![];\n    let mut stop = false;\n    for (i, a) in std::env::args().enumerate() {\n        if i != 0 && !a.starts_with(\"-\") || a == \"--\" {\n            stop = true;\n        }\n\n        match stop {\n            true => ans.push(a),\n            false => ans.append(&mut dissolve_option(&a)),\n        }\n    }\n\n    ans\n}\n"
  },
  {
    "path": "src/utils/c_string.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse std::ffi::CString;\n\npub fn to_carg(arg: &str) -> CString {\n    let mut tmp = String::new();\n    let mut unicode8num = 0;\n\n    for c in arg.chars() {\n        if c as u32 >= 0xE080 && c as u32 <= 0xE0FF {\n            let num: u8 = (c as u32 - 0xE000) as u8;\n            let ch = unsafe { String::from_utf8_unchecked(vec![num]) };\n            tmp.push_str(&ch);\n        } else if c as u32 >= 0xE200 && c as u32 <= 0xE4FF {\n            unicode8num <<= 8;\n            unicode8num += c as u32 & 0xFF;\n        } else if c as u32 >= 0xE100 && c as u32 <= 0xE1FF {\n            unicode8num <<= 8;\n            unicode8num += c as u32 & 0xFF;\n            let ch = unsafe { char::from_u32_unchecked(unicode8num) }.to_string();\n            unicode8num = 0; //　^ An error occurs on debug mode.\n            tmp.push_str(&ch);\n        } else {\n            tmp.push(c);\n        }\n    }\n    CString::new(tmp.to_string()).unwrap()\n}\n\npub fn to_cargs(args: &[String]) -> Vec<CString> {\n    args.iter().map(|s| to_carg(s)).collect()\n}\n"
  },
  {
    "path": "src/utils/clock.rs",
    "content": "//SPDX-FileCopyrightText: 2024 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse nix::time;\nuse nix::time::ClockId;\nuse std::time::Duration;\n\npub fn monotonic_time() -> Duration {\n    let now = time::clock_gettime(ClockId::CLOCK_MONOTONIC).unwrap();\n    Duration::new(\n        now.tv_sec().try_into().unwrap(),\n        now.tv_nsec().try_into().unwrap(),\n    )\n}\n\n/*\npub fn set_seconds() -> String {\n    let offset = Duration::seconds(0);\n    let adjusted = monotonic_time() - offset;\n    format!(\"{}.{}\", adjusted.whole_seconds(), adjusted.subsec_nanoseconds())\n}\n\npub fn get_seconds(v: &mut Vec<String>) -> String {\n    if v.is_empty() {\n        v.push(set_seconds());\n    }\n\n    let part: Vec<&str> = v[0].split('.').collect();\n    let sec = i64::from_str(part[0]).unwrap();\n    let nano = i32::from_str(part[1]).unwrap();\n    let offset = Duration::new(sec, nano);\n    let elapsed = monotonic_time() - offset;\n    let ans = elapsed.whole_seconds().to_string();\n    v[0] = format!(\"{}.{}\", sec, nano);\n    ans\n}\n*/\n\npub fn get_epochseconds() -> String {\n    let real = time::clock_gettime(ClockId::CLOCK_REALTIME).unwrap();\n    real.tv_sec().to_string()\n}\n\npub fn get_epochrealtime() -> String {\n    let real = time::clock_gettime(ClockId::CLOCK_REALTIME).unwrap();\n    format!(\"{}.{:06}\", real.tv_sec(), real.tv_nsec() / 1000).to_string()\n}\n"
  },
  {
    "path": "src/utils/directory.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::file_check;\nuse super::glob;\nuse crate::core::options::Options;\nuse std::fs::DirEntry;\nuse std::path::Path;\n\npub fn files(dir: &str) -> Vec<String> {\n    let dir = if dir.is_empty() { \".\" } else { dir };\n\n    let entries = match Path::new(dir).read_dir() {\n        Ok(es) => es,\n        Err(_) => return vec![],\n    };\n\n    let f = |e: DirEntry| e.file_name().to_string_lossy().to_string();\n\n    entries.map(|e| f(e.unwrap())).collect()\n}\n\nfn globstar(dir: &str) -> Vec<String> {\n    let dir = if dir.is_empty() || dir.ends_with(\"/\") {\n        dir\n    } else {\n        &(dir.to_owned() + \"/\")\n    };\n    let mut dirs = vec![dir.to_string()];\n    //    let mut ans = dirs.clone();\n    let mut ans = vec![];\n\n    while !dirs.is_empty() {\n        let mut tmp = vec![];\n        for d in dirs {\n            if file_check::is_symlink(d.trim_end_matches(\"/\")) {\n                continue;\n            }\n            let mut fs = files(&d);\n            fs.iter_mut().for_each(|f| {\n                *f = d.to_string() + f + \"/\";\n            });\n            tmp.append(&mut fs);\n        }\n        ans.extend(tmp.clone());\n        dirs = tmp;\n        dirs.sort();\n        dirs.dedup();\n    }\n\n    ans.sort();\n    ans.dedup();\n    ans\n}\n\npub fn glob(dir: &str, pattern: &str, shopts: &Options) -> Vec<String> {\n    let make_path = |f: &str| dir.to_owned() + f + \"/\";\n\n    if [\"\", \".\", \"..\"].contains(&pattern)\n        || (file_check::is_symlink(dir.trim_end_matches(\"/\")) && shopts.query(\"globstar\"))\n    {\n        let path = make_path(pattern);\n        match file_check::exists(&path) {\n            true => return vec![path],\n            false => return vec![],\n        }\n    }\n\n    if pattern == \"**\" && shopts.query(\"globstar\") {\n        return globstar(dir);\n    }\n\n    let dotglob = shopts.query(\"dotglob\");\n    let extglob = shopts.query(\"extglob\");\n    let pat = glob::parse(pattern, extglob);\n    let mut ans: Vec<String> = files(dir)\n        .iter()\n        .filter(|f| !f.starts_with(\".\") || pattern.starts_with(\".\") || dotglob)\n        .filter(|f| glob::compare(f, &pat))\n        .map(|f| make_path(f))\n        .collect();\n\n    if !shopts.query(\"globskipdots\") {\n        if glob::compare(\"..\", &pat) {\n            ans.push(make_path(\"..\"));\n        }\n        if glob::compare(\".\", &pat) {\n            ans.push(make_path(\".\"));\n        }\n    }\n\n    ans.sort();\n    ans.dedup();\n    ans\n}\n"
  },
  {
    "path": "src/utils/exit.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::{Feeder, Script, ShellCore};\nuse crate::utils::ExecError;\nuse std::process;\n\npub fn normal(core: &mut ShellCore) -> ! {\n    run_script(core);\n\n    core.write_history_to_file();\n    process::exit(core.db.exit_status % 256)\n}\n\nfn run_script(core: &mut ShellCore) {\n    if core.exit_script_run {\n        return;\n    }\n\n    core.exit_script_run = true;\n    if core.exit_script.is_empty() {\n        return;\n    }\n\n    let mut feeder = Feeder::new(&core.exit_script);\n    match Script::parse(&mut feeder, core, true) {\n        Ok(Some(mut s)) => {\n            if let Err(e) = s.exec(core) {\n                e.print(core);\n            }\n        }\n        Err(e) => {\n            e.print(core);\n        }\n        Ok(None) => {}\n    };\n}\n\n/* error at exec */\n/*\nfn command_error_exit(name: &str, core: &mut ShellCore, msg: &str, exit_status: i32) -> ! {\n    let msg = format!(\"{name}: {msg}\");\n    error::print(&msg, core);\n    process::exit(exit_status)\n}*/\n\npub fn arg_list_too_long(command_name: &str, core: &mut ShellCore) -> ! {\n    ExecError::ArgListTooLong(command_name.to_string()).print(core);\n    process::exit(126)\n}\n\npub fn permission_denied(command_name: &str, core: &mut ShellCore) -> ! {\n    ExecError::PermissionDenied(command_name.to_string()).print(core);\n    process::exit(126)\n}\n\npub fn not_found(command_name: &str, core: &mut ShellCore) -> ! {\n    ExecError::CommandNotFound(command_name.to_string()).print(core);\n    process::exit(127)\n}\n\npub fn internal(s: &str) -> ! {\n    panic!(\"SUSH INTERNAL ERROR: {s}\")\n}\n\npub fn check_e_option(core: &mut ShellCore) {\n    if core.db.exit_status != 0 && core.db.flags.contains(\"e\") && !core.suspend_e_option {\n        normal(core);\n    }\n}\n"
  },
  {
    "path": "src/utils/file.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-FileCopyrightText: 2023 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::utils::file_check;\nuse crate::ShellCore;\nuse std::env;\nuse std::ffi::OsString;\nuse std::path::{Component, Path, PathBuf};\n\npub fn oss_to_name(oss: &OsString) -> String {\n    oss.to_string_lossy().to_string()\n}\n\npub fn buf_to_name(path: &Path) -> String {\n    path.to_string_lossy().to_string()\n}\n\npub fn search_command(command: &str) -> Option<String> {\n    let paths = env::var_os(\"PATH\");\n    paths.as_ref()?;\n\n    for path in env::split_paths(&paths.unwrap()) {\n        let compath = buf_to_name(&path) + \"/\" + command;\n        if file_check::exists(&compath) {\n            return Some(compath);\n        }\n    }\n\n    None\n}\n\npub fn make_absolute_path(core: &mut ShellCore, path_str: &str) -> PathBuf {\n    let path = Path::new(&path_str);\n    let mut absolute = PathBuf::new();\n    if !path.is_relative() {\n        absolute.push(path);\n        return absolute;\n    }\n\n    if path.starts_with(\"~\") {\n        // tilde -> $HOME\n        let home_dir = core.db.get_param(\"HOME\").unwrap_or_default();\n        if !home_dir.is_empty() {\n            absolute.push(PathBuf::from(home_dir));\n            let num = match path_str.len() > 1 && path_str.starts_with(\"~/\") {\n                true => 2,\n                false => 1,\n            };\n            absolute.push(PathBuf::from(&path_str[num..]));\n        }\n    } else {\n        // current\n        if let Some(tcwd) = core.get_current_directory() {\n            absolute.push(tcwd);\n            absolute.push(path);\n        };\n    }\n\n    absolute\n}\n\npub fn make_canonical_path(core: &mut ShellCore, path_str: &str) -> PathBuf {\n    let path = make_absolute_path(core, path_str);\n    let mut canonical = PathBuf::new();\n    for component in path.components() {\n        match component {\n            Component::RootDir => canonical.push(Component::RootDir),\n            Component::ParentDir => {\n                canonical.pop();\n            }\n            Component::Normal(c) => canonical.push(c),\n            _ => (),\n        }\n    }\n    canonical\n}\n"
  },
  {
    "path": "src/utils/file_check.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda ryuichiueda@gmail.com\n//SPDX-License-Identifier: BSD-3-Clause\n\nextern crate libc;\nuse libc::isatty;\n\nuse faccess;\nuse faccess::PathExt;\nuse nix::unistd;\nuse std::fs;\nuse std::os::unix::fs::{FileTypeExt, PermissionsExt};\nuse std::os::fd::RawFd;\n\n#[cfg(target_os = \"android\")]\nuse std::os::android::fs::MetadataExt;\n#[cfg(target_os = \"freebsd\")]\nuse std::os::freebsd::fs::MetadataExt;\n#[cfg(target_os = \"linux\")]\nuse std::os::linux::fs::MetadataExt;\n#[cfg(target_os = \"macos\")]\nuse std::os::macos::fs::MetadataExt;\nuse std::os::unix::fs::MetadataExt as UnixMetadataExt;\n\nuse std::path::Path;\n\npub fn exists(name: &str) -> bool {\n    if name.ends_with(\"/\") {\n        //for macOS\n        return is_dir(name);\n    }\n\n    fs::metadata(name).is_ok()\n}\n\npub fn is_regular_file(name: &str) -> bool {\n    Path::new(name).is_file()\n}\n\npub fn is_dir(name: &str) -> bool {\n    Path::new(name).is_dir()\n}\n\npub fn metadata_comp(left: &str, right: &str, tp: &str) -> bool {\n    let (lmeta, rmeta) = match (fs::metadata(left), fs::metadata(right)) {\n        (Ok(lm), Ok(rm)) => (lm, rm),\n        (Ok(_), Err(_)) => return tp == \"-nt\",\n        (Err(_), Ok(_)) => return tp == \"-ot\",\n        (Err(_), Err(_)) => return false,\n    };\n\n    match tp {\n        \"-ef\" => (lmeta.dev(), lmeta.ino()) == (rmeta.dev(), rmeta.ino()),\n        \"-nt\" => lmeta.modified().unwrap() > rmeta.modified().unwrap(),\n        \"-ot\" => lmeta.modified().unwrap() < rmeta.modified().unwrap(),\n        _ => false,\n    }\n}\n\npub fn metadata_check(name: &str, tp: &str) -> bool {\n    let meta = match fs::metadata(name) {\n        Ok(m) => m,\n        _ => return false,\n    };\n\n    match tp {\n        \"-b\" => return meta.file_type().is_block_device(),\n        \"-c\" => return meta.file_type().is_char_device(),\n        \"-p\" => return meta.file_type().is_fifo(),\n        \"-s\" => return meta.len() == 0,\n        \"-G\" => return unistd::getgid() == meta.st_gid().into(),\n        \"-N\" => {\n            let modified_time = match meta.modified() {\n                Ok(t) => t,\n                _ => return false,\n            };\n            let accessed_time = match meta.accessed() {\n                Ok(t) => t,\n                _ => return false,\n            };\n            return modified_time > accessed_time;\n        }\n        \"-O\" => return unistd::getuid() == meta.st_uid().into(),\n        \"-S\" => return meta.file_type().is_socket(),\n        _ => {}\n    }\n\n    let special_mode = (meta.permissions().mode() / 0o1000) % 8;\n    match tp {\n        \"-g\" => (special_mode % 4) >> 1 == 1,\n        \"-k\" => special_mode % 2 == 1,\n        \"-u\" => special_mode / 4 == 1,\n        _ => false,\n    }\n}\n\npub fn is_symlink(name: &str) -> bool {\n    Path::new(name).is_symlink()\n}\n\npub fn is_readable(name: &str) -> bool {\n    Path::new(&name).readable()\n}\n\npub fn is_executable(name: &str) -> bool {\n    Path::new(name).executable()\n}\n\npub fn is_writable(name: &str) -> bool {\n    Path::new(&name).writable()\n}\n\npub fn is_tty(fd: RawFd) -> bool {\n    unsafe{isatty(fd) == 1}\n}\n\npub fn is_tty_str(name: &str) -> bool {\n    let fd = match name.parse::<i32>() {\n        Ok(n) => n,\n        _ => return false,\n    };\n    is_tty(fd)\n  //  unistd::isatty(fd) == Ok(true)\n}\n"
  },
  {
    "path": "src/utils/glob/comparator.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::extglob;\nuse super::{GlobElem, MetaChar};\nuse crate::exit;\n\npub fn shave_word(word: &str, pattern: &[GlobElem]) -> Vec<String> {\n    let mut candidates = vec![word.to_string()];\n    pattern.iter().for_each(|w| shave(&mut candidates, w));\n    candidates\n}\n\npub fn shave(candidates: &mut Vec<String>, w: &GlobElem) {\n    match w {\n        GlobElem::Normal(s) => normal(candidates, s),\n        GlobElem::Symbol('?') => question(candidates),\n        GlobElem::Symbol('*') => asterisk(candidates),\n        GlobElem::OneOf(not, cs) => one_of(candidates, cs, *not),\n        GlobElem::ExtGlob(prefix, ps) => extglob::shave(candidates, *prefix, ps),\n        GlobElem::Symbol(_) => exit::internal(\"Unknown glob symbol\"),\n    }\n}\n\nfn normal(cands: &mut Vec<String>, s: &str) {\n    cands.retain(|c| c.starts_with(s));\n    cands.iter_mut().for_each(|c| {\n        *c = c.split_off(s.len());\n    });\n}\n\nfn question(cands: &mut Vec<String>) {\n    cands.retain(|c| !c.is_empty());\n    let len = |c: &String| c.chars().next().unwrap().len_utf8();\n    cands.iter_mut().for_each(|c| {\n        *c = c.split_off(len(c));\n    });\n}\n\nfn asterisk(cands: &mut Vec<String>) {\n    if cands.is_empty() {\n        return;\n    }\n\n    let mut ans = vec![\"\".to_string()];\n    for cand in cands.iter_mut() {\n        let mut len = 0;\n        for c in cand.chars() {\n            ans.push(cand.clone().split_off(len));\n            len += c.len_utf8();\n        }\n    }\n    *cands = ans;\n}\n\nfn one_of(cands: &mut Vec<String>, cs: &[MetaChar], not_inv: bool) {\n    if cs.is_empty() {\n        if !not_inv {\n            cands.clear();\n        }\n        return;\n    }\n\n    cands.retain(|cand| !cand.is_empty());\n    cands.retain(|cand| cs.iter().any(|c| compare_head(cand, c)) == not_inv);\n    let len = |c: &String| c.chars().next().unwrap().len_utf8();\n    cands.iter_mut().for_each(|c| {\n        *c = c.split_off(len(c));\n    });\n}\n\nfn compare_head(cand: &str, c: &MetaChar) -> bool {\n    let head = cand.chars().next().unwrap();\n    match c {\n        MetaChar::Normal(c) => head == *c,\n        MetaChar::Range(f, t) => range_check(*f, *t, head),\n        MetaChar::CharClass(cls) => charclass_check(cls, head),\n    }\n}\n\nfn range_check(from: char, to: char, c: char) -> bool {\n    if ('0' <= from && from <= to && to <= '9')\n        || ('a' <= from && from <= to && to <= 'z')\n        || ('A' <= from && from <= to && to <= 'Z')\n    {\n        return from <= c && c <= to;\n    }\n    false\n}\n\nfn charclass_check(cls: &str, c: char) -> bool {\n    match cls {\n        //TODO: rough implementation, no test except for [:space:]\n        \"[:alnum:]\" => c.is_ascii_alphanumeric(),\n        \"[:alpha:]\" => c.is_ascii_alphabetic(),\n        \"[:ascii:]\" => c.is_ascii(),\n        \"[:blank:]\" => c == ' ' || c == '\\t',\n        \"[:cntrl:]\" => c.is_ascii_control(),\n        \"[:digit:]\" => c.is_ascii_digit(),\n        \"[:graph:]\" => c.is_ascii_graphic(),\n        \"[:lower:]\" => c.is_ascii_lowercase(),\n        \"[:print:]\" => c.is_ascii() && !c.is_ascii_control(),\n        \"[:punct:]\" => c.is_ascii_punctuation(),\n        \"[:space:]\" => c.is_ascii_whitespace(),\n        \"[:upper:]\" => c.is_ascii_uppercase(),\n        \"[:word:]\" => c.is_ascii_alphanumeric() || c == '_',\n        \"[:xdigit:]\" => c.is_ascii_hexdigit(),\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "src/utils/glob/extglob.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::comparator;\nuse super::parser;\nuse super::GlobElem;\nuse crate::exit;\n\npub fn shave(cands: &mut Vec<String>, prefix: char, patterns: &[String]) {\n    match prefix {\n        '?' => question(cands, patterns),\n        '*' => zero_or_more(cands, patterns),\n        '+' => more_than_zero(cands, patterns),\n        '@' => once(cands, patterns),\n        '!' => not(cands, patterns),\n        _ => exit::internal(\"unknown extglob prefix\"),\n    }\n}\n\nfn question(cands: &mut Vec<String>, patterns: &[String]) {\n    let mut ans = cands.clone();\n    for p in patterns {\n        let mut tmp = cands.clone();\n        parser::parse(p, true)\n            .iter()\n            .for_each(|w| comparator::shave(&mut tmp, w));\n        ans.append(&mut tmp);\n    }\n    *cands = ans;\n}\n\nfn zero_or_more(cands: &mut Vec<String>, patterns: &[String]) {\n    let mut ans = vec![];\n    let mut tmp = cands.clone();\n    let mut len = tmp.len();\n\n    while len > 0 {\n        ans.extend(tmp.clone());\n        once(&mut tmp, patterns);\n        for a in &ans {\n            tmp.retain(|t| a.as_str() != t.as_str());\n        }\n\n        len = tmp.len();\n    }\n    *cands = ans;\n}\n\nfn more_than_zero(cands: &mut Vec<String>, patterns: &[String]) {\n    //TODO: buggy\n    let mut ans: Vec<String> = vec![];\n    let mut tmp: Vec<String> = cands.clone();\n    let mut len = tmp.len();\n\n    while len > 0 {\n        once(&mut tmp, patterns);\n\n        for a in &ans {\n            tmp.retain(|t| a.as_str() != t.as_str());\n        }\n        ans.extend(tmp.clone());\n        len = tmp.len();\n    }\n    *cands = ans;\n}\n\nfn once(cands: &mut Vec<String>, patterns: &[String]) {\n    let mut ans = vec![];\n    for p in patterns {\n        let mut tmp = cands.clone();\n        parser::parse(p, true)\n            .iter()\n            .for_each(|w| comparator::shave(&mut tmp, w));\n        ans.append(&mut tmp);\n    }\n    *cands = ans;\n}\n\nfn not(cands: &mut Vec<String>, patterns: &[String]) {\n    let mut ans = vec![];\n    for cand in cands.iter_mut() {\n        for prefix in make_prefix_strings(cand) {\n            if !once_exact_match(&prefix, patterns) {\n                ans.push(cand[prefix.len()..].to_string());\n            }\n        }\n    }\n    *cands = ans;\n}\n\nfn once_exact_match(cand: &str, patterns: &[String]) -> bool {\n    let mut tmp = vec![cand.to_string()];\n    once(&mut tmp, patterns);\n    tmp.iter().any(|t| t.is_empty())\n}\n\npub fn scan(remaining: &str) -> (usize, Option<GlobElem>) {\n    let prefix = match remaining.chars().next() {\n        Some(c) => c,\n        None => return (0, None),\n    };\n\n    if \"?*+@!\".find(prefix).is_none() || remaining.chars().nth(1) != Some('(') {\n        return (0, None);\n    }\n\n    let mut chars = vec![];\n    let mut len = 2;\n    let mut escaped = false;\n    let mut nest = 0;\n    let mut next_nest = false;\n    let mut patterns = vec![];\n\n    for c in remaining[len..].chars() {\n        len += c.len_utf8();\n\n        if escaped {\n            chars.push(c);\n            escaped = false;\n            continue;\n        }\n        if c == '\\\\' {\n            escaped = true;\n            continue;\n        }\n\n        if c == '|' && nest == 0 {\n            patterns.push(chars.iter().collect());\n            chars.clear();\n            continue;\n        }\n\n        if next_nest && c == '(' {\n            nest += 1;\n        }\n\n        next_nest = \"?*+@!\".find(c).is_some();\n\n        if c == ')' {\n            match nest {\n                0 => {\n                    return {\n                        patterns.push(chars.iter().collect());\n                        (len, Some(GlobElem::ExtGlob(prefix, patterns)))\n                    }\n                }\n                _ => nest -= 1,\n            }\n        }\n\n        chars.push(c);\n    }\n\n    (0, None)\n}\n\nfn make_prefix_strings(s: &str) -> Vec<String> {\n    let mut ans = vec![];\n    let mut prefix = s.to_string();\n\n    ans.push(prefix.clone());\n    while !prefix.is_empty() {\n        prefix.pop();\n        ans.push(prefix.clone());\n    }\n    ans\n}\n"
  },
  {
    "path": "src/utils/glob/parser.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse super::{extglob, GlobElem, MetaChar};\n\nfn eat_one_char(pattern: &mut String, ans: &mut Vec<GlobElem>) -> bool {\n    if pattern.starts_with(\"*\") || pattern.starts_with(\"?\") {\n        ans.push(GlobElem::Symbol(pattern.remove(0)));\n        return true;\n    }\n    false\n}\n\nfn eat_escaped_char(pattern: &mut String, ans: &mut Vec<GlobElem>) -> bool {\n    if !pattern.starts_with(\"\\\\\") {\n        return false;\n    }\n\n    if pattern.len() == 1 {\n        ans.push(GlobElem::Normal(pattern.remove(0).to_string()));\n        return true;\n    }\n    //ans.push( GlobElem::Normal(pattern.remove(0).to_string()) );\n    pattern.remove(0);\n\n    let len = pattern.chars().next().unwrap().len_utf8();\n    ans.push(GlobElem::Normal(consume(pattern, len)));\n    true\n}\n\nfn cut_charclass(pattern: &mut String) -> Option<MetaChar> {\n    for c in vec![\n        \"alnum\", \"alpha\", \"ascii\", \"blank\", \"cntrl\", \"digit\", \"graph\", \"lower\", \"print\", \"punct\",\n        \"space\", \"upper\", \"word\", \"xdigit\",\n    ] {\n        if pattern.starts_with(&(\"[:\".to_owned() + c + \":]\")) {\n            return Some(MetaChar::CharClass(consume(pattern, c.len() + 4)));\n        }\n    }\n\n    None\n}\n\nfn cut_metachar(pattern: &mut String) -> Option<MetaChar> {\n    if pattern.starts_with(\"]\") {\n        return None;\n    }\n\n    if pattern.starts_with(\"[:\") {\n        if let Some(cls) = cut_charclass(pattern) {\n            return Some(cls);\n        }\n    }\n\n    if pattern.starts_with(\"\\\\\") {\n        if pattern.len() > 1 {\n            let ch = pattern.chars().nth(1).unwrap();\n            *pattern = pattern.split_off(ch.len_utf8() + 1);\n            return Some(MetaChar::Normal(ch));\n        } else {\n            *pattern = pattern.split_off(1);\n            return None;\n        }\n    }\n\n    if pattern.len() > 2\n        && pattern.chars().nth(1) == Some('-')\n        && pattern.chars().nth(2) != Some(']')\n    {\n        let f = pattern.chars().next().unwrap();\n        let t = pattern.chars().nth(2).unwrap();\n        *pattern = pattern.split_off(f.len_utf8() + 1 + t.len_utf8());\n        return Some(MetaChar::Range(f, t));\n    }\n\n    if !pattern.is_empty() {\n        let ch = pattern.chars().next().unwrap();\n        *pattern = pattern.split_off(ch.len_utf8());\n        return Some(MetaChar::Normal(ch));\n    }\n\n    None\n}\n\nfn eat_bracket(pattern: &mut String, ans: &mut Vec<GlobElem>) -> bool {\n    if !pattern.starts_with(\"[\") {\n        return false;\n    }\n\n    let bkup = pattern.clone();\n    let not = pattern.starts_with(\"[^\") || pattern.starts_with(\"[!\");\n    let len = if not { 2 } else { 1 };\n    let mut inner = vec![];\n\n    *pattern = pattern.split_off(len);\n    while !pattern.is_empty() {\n        if pattern.starts_with(\"]\") {\n            *pattern = pattern.split_off(1);\n            ans.push(GlobElem::OneOf(!not, inner));\n            return true;\n        }\n\n        if let Some(p) = cut_metachar(pattern) {\n            inner.push(p);\n        }\n    }\n\n    *pattern = bkup;\n    false\n}\n\nfn eat_extglob(pattern: &mut String, ans: &mut Vec<GlobElem>) -> bool {\n    let (len, extparen) = extglob::scan(pattern);\n    if len > 0 {\n        *pattern = pattern.split_off(len);\n        ans.push(extparen.unwrap());\n        return true;\n    }\n    false\n}\n\nfn eat_chars(pattern: &mut String, ans: &mut Vec<GlobElem>) -> bool {\n    let mut len = 0;\n    for c in pattern.chars() {\n        if \"@!+*?[\\\\\".find(c).is_some() {\n            break;\n        }\n        len += c.len_utf8();\n    }\n\n    if len == 0 {\n        return false;\n    }\n\n    let s = consume(pattern, len);\n    ans.push(GlobElem::Normal(s));\n    true\n}\n\npub fn parse(pattern: &str, extglob: bool) -> Vec<GlobElem> {\n    let pattern = pattern.to_string();\n    let mut remaining = pattern.to_string();\n    let mut ans = vec![];\n\n    while !remaining.is_empty() {\n        if (extglob && eat_extglob(&mut remaining, &mut ans))\n            || eat_bracket(&mut remaining, &mut ans)\n            || eat_one_char(&mut remaining, &mut ans)\n            || eat_escaped_char(&mut remaining, &mut ans)\n            || eat_chars(&mut remaining, &mut ans)\n        {\n            continue;\n        }\n\n        let s = consume(&mut remaining, 1);\n        ans.push(GlobElem::Normal(s));\n    }\n\n    ans\n}\n\nfn consume(remaining: &mut String, cutpos: usize) -> String {\n    let cut = remaining[0..cutpos].to_string();\n    *remaining = remaining.split_off(cutpos);\n\n    cut\n}\n"
  },
  {
    "path": "src/utils/glob.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nmod comparator;\nmod extglob;\nmod parser;\n\n#[derive(Debug)]\npub enum GlobElem {\n    Normal(String),\n    Symbol(char),\n    OneOf(bool, Vec<MetaChar>),\n    ExtGlob(char, Vec<String>),\n}\n\n#[derive(Debug)]\npub enum MetaChar {\n    Normal(char),\n    Range(char, char),\n    CharClass(String),\n}\n\npub fn parse_and_compare(word: &str, pattern: &str, extglob: bool) -> bool {\n    let pat = parser::parse(pattern, extglob);\n    compare(word, &pat)\n}\n\npub fn compare(word: &str, pattern: &[GlobElem]) -> bool {\n    comparator::shave_word(word, pattern)\n        .iter()\n        .any(|c| c.is_empty())\n}\n\npub fn longest_match_length(word: &str, pattern: &[GlobElem]) -> usize {\n    word.len()\n        - comparator::shave_word(word, pattern)\n            .iter()\n            .map(|c| c.len())\n            .min()\n            .unwrap_or(word.len())\n}\n\npub fn shortest_match_length(word: &str, pattern: &[GlobElem]) -> usize {\n    word.len()\n        - comparator::shave_word(word, pattern)\n            .iter()\n            .map(|c| c.len())\n            .max()\n            .unwrap_or(word.len())\n}\n\npub fn parse(pattern: &str, extglob: bool) -> Vec<GlobElem> {\n    parser::parse(pattern, extglob)\n}\n"
  },
  {
    "path": "src/utils/restricted_shell.rs",
    "content": "//SPDX-FileCopyrightText: 2025 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\nuse crate::core::database::DataBase;\nuse crate::error::exec::ExecError;\nuse crate::file_check;\n\npub fn check(db: &mut DataBase, name: &str, value: &Option<Vec<String>>) -> Result<(), ExecError> {\n    if !db.flags.contains('r') {\n        return Ok(());\n    }\n\n    if value.is_some() && name == \"BASH_CMDS\" {\n        rsh_cmd_check(value.as_ref().unwrap())?;\n    }\n\n    if [\"SHELL\", \"PATH\", \"ENV\", \"BASH_ENV\"].contains(&name) {\n        return Err(ExecError::VariableReadOnly(name.to_string()));\n    }\n    Ok(())\n}\n\nfn rsh_cmd_check(cmds: &[String]) -> Result<(), ExecError> {\n    for c in cmds {\n        if c.contains('/') {\n            let msg = format!(\"{}: restricted\", &c);\n            return Err(ExecError::Other(msg));\n        }\n\n        if file_check::is_executable(c) {\n            let msg = format!(\"{}: not found\", &c);\n            return Err(ExecError::Other(msg));\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "src/utils/splitter.rs",
    "content": "//SPDX-FileCopyrightText: 2025 @caro@mi.shellgei.org\n//SPDX-License-Identifier: BSD-3-Clause\n\npub fn split(sw: &str, ifs: &str, prev_char: Option<char>) -> Vec<(String, bool)> {\n    //bool: true if it should remain\n    if ifs.is_empty() {\n        return vec![(sw.to_string(), false)];\n    }\n\n    if ifs.chars().all(|c| \" \\t\\n\".contains(c)) {\n        split_str_normal(sw, ifs)\n    } else {\n        split_str_special(sw, ifs, prev_char)\n    }\n}\n\nfn scanner_blank(s: &str, blank: &[char]) -> usize {\n    let mut ans = 0;\n    let mut esc = false;\n\n    for ch in s.chars() {\n        if esc || ch == '\\\\' {\n            esc = !esc;\n            ans += ch.len_utf8();\n            continue;\n        }\n\n        if blank.contains(&ch) {\n            ans += ch.len_utf8();\n        } else {\n            break;\n        }\n    }\n\n    ans\n}\n\nfn scanner_ifs_blank(s: &str, blank: &[char], delim: &[char]) -> usize {\n    let mut ans = 0;\n    let mut esc = false;\n\n    for ch in s.chars() {\n        if esc || ch == '\\\\' {\n            esc = !esc;\n            ans += ch.len_utf8();\n            continue;\n        }\n\n        if delim.contains(&ch) {\n            ans += ch.len_utf8();\n            ans += scanner_blank(&s[ans..], blank);\n            return ans;\n        } else if blank.contains(&ch) {\n            ans += ch.len_utf8();\n        } else {\n            break;\n        }\n    }\n\n    ans\n}\n\nfn split_str_special(s: &str, ifs: &str, prev_char: Option<char>) -> Vec<(String, bool)> {\n    let mut ans = vec![];\n    let mut remaining = s.to_string();\n    let mut shaved = false;\n\n    let shave_prev = match prev_char {\n        None => true,\n        Some(c) => \" \\t\\n\".contains(c),\n    };\n\n    let blank: Vec<char> = ifs.chars().filter(|s| \" \\t\\n\".contains(*s)).collect();\n    let delim: Vec<char> = ifs.chars().filter(|s| !\" \\t\\n\".contains(*s)).collect();\n\n    if shave_prev {\n        let len = scanner_blank(&remaining, &blank);\n        shaved = len > 0;\n        let tail = remaining.split_off(len);\n        remaining = tail;\n    }\n\n    while !remaining.is_empty() {\n        let len = scanner_word(&remaining, ifs);\n        let tail = remaining.split_off(len);\n\n        ans.push((remaining.to_string(), true));\n        remaining = tail;\n\n        let len = scanner_ifs_blank(&remaining, &blank, &delim);\n        if len > 0 {\n            remaining = remaining.split_off(len);\n            if remaining.is_empty() {\n                ans.push((\"\".to_string(), false));\n            }\n        }\n    }\n\n    if ans.is_empty() {\n        ans.push((\"\".to_string(), false));\n        ans.push((\"\".to_string(), false));\n    }\n\n    if shaved && ans.len() < 2 {\n        //if the string is modified, the splitting is applied.\n        ans.push((\"\".to_string(), false));\n    }\n\n    ans\n}\n\nfn split_str_normal(s: &str, ifs: &str) -> Vec<(String, bool)> {\n    let mut esc = false;\n    let mut from = 0;\n    let mut pos = 0;\n    let mut ans = vec![];\n\n    for c in s.chars() {\n        pos += c.len_utf8();\n        if esc || c == '\\\\' {\n            esc = !esc;\n            continue;\n        }\n\n        if ifs.contains(c) {\n            let sw = s[from..pos - c.len_utf8()].to_string();\n            ans.push((sw, false));\n            from = pos;\n        }\n    }\n\n    ans.push((s[from..].to_string(), false));\n\n    ans\n}\n\nfn scanner_word(s: &str, ifs: &str) -> usize {\n    let mut ans = 0;\n    let mut esc = false;\n\n    for ch in s.chars() {\n        if esc || ch == '\\\\' {\n            esc = !esc;\n            ans += ch.len_utf8();\n            continue;\n        }\n\n        if ifs.contains(ch) {\n            return ans;\n        }\n\n        ans += ch.len_utf8();\n    }\n\n    ans\n}\n"
  },
  {
    "path": "src/utils.rs",
    "content": "//SPDX-FileCopyrightText: 2024 Ryuichi Ueda <ryuichiueda@gmail.com>\n//SPDX-License-Identifier: BSD-3-Clause\n\npub mod arg;\npub mod c_string;\npub mod clock;\npub mod directory;\npub mod exit;\npub mod file;\npub mod file_check;\npub mod glob;\npub mod restricted_shell;\npub mod splitter;\n\nuse libc;\nuse crate::{Feeder, ShellCore};\nuse crate::elements::expr::arithmetic::ArithmeticExpr;\nuse crate::error::exec::ExecError;\nuse crate::error::input::InputError;\nuse faccess::PathExt;\nuse io_streams::StreamReader;\nuse std::io::Read;\nuse std::path::Path;\n\npub fn reserved(w: &str) -> bool {\n    matches!(\n        w,\n        \"[[\" | \"]]\"\n            | \"{\"\n            | \"}\"\n            | \"while\"\n            | \"for\"\n            | \"do\"\n            | \"done\"\n            | \"if\"\n            | \"then\"\n            | \"elif\"\n            | \"else\"\n            | \"fi\"\n            | \"case\"\n            | \"coproc\"\n            | \"esac\"\n            | \"repeat\"\n    )\n}\n\npub fn split_words(s: &str) -> Vec<String> {\n    let mut ans = vec![];\n    let mut end_with_space = false;\n\n    let mut in_quote = false;\n    let mut escaped = false;\n    let mut quote = ' ';\n\n    let mut tmp = String::new();\n    for c in s.chars() {\n        end_with_space = false;\n        if escaped || c == '\\\\' {\n            escaped = !escaped;\n            tmp.push(c);\n            continue;\n        }\n\n        if c == '\\'' || c == '\"' {\n            if c == quote {\n                in_quote = !in_quote;\n                quote = ' ';\n            } else if quote == ' ' {\n                in_quote = !in_quote;\n                quote = c;\n            }\n            tmp.push(c);\n            continue;\n        }\n\n        if in_quote {\n            tmp.push(c);\n            continue;\n        }\n\n        if !in_quote && (c == ' ' || c == '\\t') {\n            end_with_space = true;\n            if !tmp.is_empty() {\n                ans.push(tmp.clone());\n                tmp.clear();\n            }\n        } else {\n            tmp.push(c);\n        }\n    }\n\n    if !tmp.is_empty() {\n        ans.push(tmp);\n    }\n\n    if end_with_space {\n        ans.push(\"\".to_string());\n    }\n\n    ans\n}\n\npub fn is_wsl() -> bool {\n    if let Ok(info) = nix::sys::utsname::uname() {\n        let release = info.release().to_string_lossy().to_string();\n        return release.contains(\"WSL\");\n    };\n\n    false\n}\n\npub fn is_name(s: &str, core: &mut ShellCore) -> bool {\n    let mut f = Feeder::new(s);\n    !s.is_empty() && f.scanner_name(core) == s.len()\n}\n\npub fn is_param(s: &str) -> bool {\n    if s.is_empty() {\n        return false;\n    }\n\n    let first_ch = s.chars().next().unwrap();\n    if s.len() == 1 {\n        //special or position param\n        if \"$?*@#-!_0123456789\".find(first_ch).is_some() {\n            return true;\n        }\n    } else if let Ok(n) = s.parse::<usize>() {\n        return n > 0;\n    }\n\n    is_var(s)\n    /*\n    /* variable */\n    if first_ch.is_ascii_digit() {\n        return false;\n    }\n\n    let name_c = |c: char| {\n        c.is_ascii_lowercase() || c.is_ascii_uppercase() || c.is_ascii_digit() || '_' == c\n    };\n    !s.chars().any(|c| !name_c(c))\n        */\n}\n\npub fn is_var(s: &str) -> bool {\n    if s.is_empty() {\n        return false;\n    }\n    let first_ch = s.chars().next().unwrap();\n    if first_ch.is_ascii_digit() {\n        return false;\n    }\n\n    let name_c = |c: char| {\n        c.is_ascii_lowercase() || c.is_ascii_uppercase() || c.is_ascii_digit() || '_' == c\n    };\n    !s.chars().any(|c| !name_c(c))\n}\n\npub fn read_line_stdin_unbuffered(delim: &str) -> Result<String, InputError> {\n    let mut line = vec![];\n    let mut ch: [u8; 1] = Default::default();\n    let mut stdin = StreamReader::stdin().unwrap();\n\n    let mut d = 10; //\\n\n    if let Some(Ok(c)) = delim.as_bytes().bytes().next() {\n        d = c;\n    }\n\n    loop {\n        match stdin.read(&mut ch) {\n            Ok(0) => {\n                if line.is_empty() {\n                    return Err(InputError::Eof);\n                }\n                break;\n            }\n            Ok(_) => {\n                line.push(ch[0]);\n                if d == ch[0] {\n                    break;\n                }\n            }\n            Err(_) => return Err(InputError::Eof),\n        }\n    }\n\n    match String::from_utf8(line) {\n        Ok(s) => Ok(s),\n        Err(_) => Err(InputError::NotUtf8),\n    }\n}\n\npub fn to_ansi_c(s: &str) -> String {\n    let mut ans = String::new();\n    let mut ansi = false;\n    let mut double_quote = false;\n\n    for c in s.chars() {\n        match c as usize {\n            bin @ 0..9 => {\n                ansi = true;\n                let alter = format!(\"\\\\{bin:03o}\");\n                ans.push_str(&alter);\n            },\n            9 => {\n                ansi = true;\n                ans.push_str(\"\\\\t\");\n            },\n            0x0A => {\n                ansi = true;\n                ans.push_str(\"\\\\n\");\n            },\n            0x22 | 0x24 | 0x60 => { // \"\n                double_quote = true;\n                ans.push('\\\\');\n                ans.push(c);\n            },\n            0x20 | /*0x27 |*/ 0x2A | 0x40 | 0x5B | 0x5D => { // space, ' , * , @, [ , ],\n                double_quote = true;\n                ans.push(c);\n            },\n            _ => ans.push(c),\n        }\n    }\n\n    if ansi {\n        ans.insert(0, '\\'');\n        ans.insert(0, '$');\n        ans.push('\\'');\n    } else if double_quote {\n        ans.insert(0, '\"');\n        ans.push('\"');\n    }\n\n    ans\n}\n\npub fn get_command_path(s: &str, core: &mut ShellCore) -> String {\n    for path in core.db.get_param(\"PATH\").unwrap_or_default().split(\":\") {\n        for command in directory::files(path).iter() {\n            let fullpath = path.to_owned() + \"/\" + command;\n            if !Path::new(&fullpath).executable() {\n                continue;\n            }\n\n            if command == s {\n                return fullpath;\n            }\n        }\n    }\n\n    String::new()\n}\n\npub fn string_to_calculated_string(from: &str, core: &mut ShellCore) -> Result<String, ExecError> {\n    let mut f = Feeder::new(from);\n    if let Some(mut a) = ArithmeticExpr::parse(&mut f, core, false, \"\")? {\n        if f.is_empty() {\n            return a.eval(core);\n        }\n    }\n\n    Err(ExecError::SyntaxError(f.consume(f.len())))\n}\n\npub fn gen_not_exist_var(core: &mut ShellCore) -> String {\n    let mut nm = \"fjoeeojwa\".to_string();\n    while core.db.exist(&nm) || core.db.exist_nameref(&nm) {\n        nm.push('a');\n    }\n    nm\n}\n\npub fn groups() -> Vec<String> {\n    let num = unsafe{libc::getgroups(0, ::std::ptr::null_mut())};\n    let mut groups = vec![0; num as usize];\n    unsafe{libc::getgroups(num, groups.as_mut_ptr())};\n    groups.iter().map(|e| e.to_string()).collect()\n}\n"
  },
  {
    "path": "test/README",
    "content": "Test files are moved to https://github.com/shellgei/rusty_bash_test\nbecause it starts containing test cases from Bash repo, which is licensed\nwith GPL. \n"
  }
]