├── Version ├── .github ├── Speed.png ├── samples │ ├── horse.mp4 │ ├── document.doc │ ├── blackhole.jpg │ └── add.py ├── scripts │ ├── debian-package │ │ ├── commit.sh │ │ ├── update-installer.sh │ │ └── update-code.sh │ ├── vm-testing │ │ ├── gen-config.sh │ │ └── verify-files.sh │ └── release │ │ └── build.sh └── workflows │ ├── release.yml │ └── autoscript.yml ├── DEBIAN └── control ├── conf ├── qsort.conf ├── extensions.json └── qsort-completion.bash ├── LICENSE ├── CMakeLists.txt ├── CHANGELOG.md ├── lib ├── user.hpp └── inicpp.h ├── README.md └── main.cpp /Version: -------------------------------------------------------------------------------- 1 | 0.3.3-beta -------------------------------------------------------------------------------- /.github/Speed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiltuDas1/qsort/HEAD/.github/Speed.png -------------------------------------------------------------------------------- /.github/samples/horse.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiltuDas1/qsort/HEAD/.github/samples/horse.mp4 -------------------------------------------------------------------------------- /.github/samples/document.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiltuDas1/qsort/HEAD/.github/samples/document.doc -------------------------------------------------------------------------------- /.github/samples/blackhole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BiltuDas1/qsort/HEAD/.github/samples/blackhole.jpg -------------------------------------------------------------------------------- /DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: qsort 2 | Version: 0.3.3-beta 3 | Installed-Size: 708 4 | Depends: libmagic-dev 5 | Maintainer: BiltuDas1 6 | Architecture: amd64 7 | Description: Quicker way to sort file according to user settings 8 | For More information visit https://github.com/BiltuDas1/qsort 9 | -------------------------------------------------------------------------------- /.github/scripts/debian-package/commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 4 | git config --local user.name "github-actions[bot]" 5 | git add . 6 | git commit -m "Updated debian control information" && echo File Updated || echo Already up-to-date -------------------------------------------------------------------------------- /conf/qsort.conf: -------------------------------------------------------------------------------- 1 | ; The path where all files will be moved 2 | [Path] 3 | General=$HOME 4 | Videos=$VIDEOS 5 | Pictures=$PICTURES 6 | Documents=$DOCUMENTS 7 | Music=$MUSIC 8 | 9 | ; Exception of those files/extensions, which you don't want to move 10 | [Exclude] 11 | Extensions=py, txt 12 | Filenames=requirements.txt 13 | -------------------------------------------------------------------------------- /.github/samples/add.py: -------------------------------------------------------------------------------- 1 | print("Input number one: ") 2 | try: 3 | a=int(input()) 4 | except ValueError: 5 | print("Invalid input, please enter a numeric value") 6 | exit(1) 7 | print("Input number two: ") 8 | try: 9 | b=int(input()) 10 | except ValueError: 11 | print("Invalid input, please enter a numeric value") 12 | exit(1) 13 | result=a+b 14 | print("Addition of two numbers are: {}".format(result)) 15 | -------------------------------------------------------------------------------- /.github/scripts/debian-package/update-installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ENVIRONMENT 4 | VER=$1 5 | 6 | # Updates the version into control 7 | default=$(cat DEBIAN/control | grep "Version" | awk '{print $2}') 8 | if [[ "$default" != "$VER" ]]; then 9 | sed -i 's/Version: '"$default"'/Version: '"$VER"'/' DEBIAN/control 10 | fi 11 | 12 | # Updates total installed size 13 | default=$(cat DEBIAN/control | grep "Installed-Size" | awk '{print $2}') 14 | compiled=$(ls -l build/qsort | awk '{print $5}') 15 | compiled=$((compiled/1024)) 16 | if [[ "$compiled" -ne "$default" ]]; then 17 | sed -i 's/Installed-Size: '"$default"'/Installed-Size: '"$compiled"'/' DEBIAN/control 18 | fi -------------------------------------------------------------------------------- /.github/scripts/vm-testing/gen-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ~ 4 | mkdir Documents Pictures Videos Music 5 | echo XDG_DESKTOP_DIR="$HOME/Desktop">>$HOME/.config/user-dirs.dirs 6 | echo XDG_DOCUMENTS_DIR="$HOME/Documents">>$HOME/.config/user-dirs.dirs 7 | echo XDG_DOWNLOAD_DIR="$HOME/Downloads">>$HOME/.config/user-dirs.dirs 8 | echo XDG_MUSIC_DIR="$HOME/Music">>$HOME/.config/user-dirs.dirs 9 | echo XDG_PICTURES_DIR="$HOME/Pictures">>$HOME/.config/user-dirs.dirs 10 | echo XDG_PUBLICSHARE_DIR="$HOME/Public">>$HOME/.config/user-dirs.dirs 11 | echo XDG_TEMPLATES_DIR="$HOME/Templates">>$HOME/.config/user-dirs.dirs 12 | echo XDG_VIDEOS_DIR="$HOME/Videos">>$HOME/.config/user-dirs.dirs -------------------------------------------------------------------------------- /.github/scripts/vm-testing/verify-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | if [[ -f "$HOME/Documents/document.doc" ]]; then 5 | echo "Document Verification Complete" 6 | else 7 | echo "Document Verification Failed" 8 | exit 1 9 | fi 10 | 11 | if [[ -f "$HOME/Pictures/blackhole.jpg" ]]; then 12 | echo "Picture Verification Complete" 13 | else 14 | echo "Picture Verification Failed" 15 | exit 1 16 | fi 17 | 18 | if [[ -f "$HOME/Videos/horse.mp4" ]]; then 19 | echo "Video Verification Complete" 20 | else 21 | echo "Video Verification Failed" 22 | exit 1 23 | fi 24 | 25 | if [[ ! -f "$HOME/add.py" ]]; then 26 | echo "General Files Verification Complete" 27 | else 28 | echo "General Files Verification Failed" 29 | exit 1 30 | fi -------------------------------------------------------------------------------- /.github/scripts/debian-package/update-code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ENVIRONMENT 4 | VER=$1 5 | 6 | ver=$(echo $VER | cut -f1 -d-) 7 | vertype=$(echo $VER | cut -f2 -d-) 8 | userver=$(grep -n "const string ver = \"" lib/user.hpp | head -1 | awk '{print $5}' | cut -f2 -d\") 9 | uservertype=$(grep -n "const string vertype = \"" lib/user.hpp | head -1 | awk '{print $5}' | cut -f2 -d\") 10 | 11 | # Updates the version information into user.hpp 12 | if [[ "$ver" != "$userver" ]]; then 13 | sed -i 's/const string ver = "'"$userver"'";/const string ver = "'"$ver"'";/' lib/user.hpp 14 | fi 15 | 16 | # Updates the version type into user.hpp 17 | if [[ "$vertype" != "$uservertype" ]]; then 18 | sed -i 's/const string vertype = "'"$uservertype"'";/const string vertype = "'"$vertype"'";/' lib/user.hpp 19 | fi -------------------------------------------------------------------------------- /conf/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "Documents":[ 3 | "doc", "docx", "xls", "xlsx", "txt", "pdf","pptx","rtf","odt","ott","odm","docm","dotx","dotm","csv","tsv","xlsm","xltm","xltx","ods","ots","ppt","ppsx","odp","otp" 4 | ], 5 | 6 | "Pictures":[ 7 | "jpg", "jpeg", "png", "ico","gif","bmp","tiff","svg","webp","eps","hdr" 8 | ], 9 | 10 | "Musics":[ 11 | "mp3", "aac", "wav", "wma","flac","ogg","m4a","aiff","alac","dsd","ape","opus","midi","kar","m3u","pls","cue","sib","mus","gpx","ra","ram","ac3","dts","wv","tta","vox","snd","amr","au","dct","gsm","ivs","m4r","mpc","ofr","w64" 12 | ], 13 | 14 | "Videos":[ 15 | "mp4", "avi","mkv","mov","wmv","flv","mpeg","mpg","3gp","3g2","vob","m4v","rm","rmvb","webm","mts","ts","divx","ogv","asf","f4v","h264","hevc","ogm","yuv" 16 | ] 17 | } -------------------------------------------------------------------------------- /conf/qsort-completion.bash: -------------------------------------------------------------------------------- 1 | # https://opensource.com/article/18/3/creating-bash-completion-script 2 | _qsort_completions(){ 3 | if [ ${#COMP_WORDS[@]} -eq "2" ]; then 4 | COMPREPLY=($(compgen -W "--help --version --edit-conf --thread --custom-conf -cc --mime" -- "${COMP_WORDS[1]}")) 5 | else 6 | if [[ ${COMP_WORDS[1]} == "--edit-conf" && ${#COMP_WORDS[@]} -eq "3" ]]; then 7 | COMPREPLY=($(compgen -W "cli" -- "${COMP_WORDS[2]}")) 8 | fi 9 | if [[ ${COMP_WORDS[1]} == "--thread" && ${#COMP_WORDS[@]} -eq "3" ]]; then 10 | for((i=1;i<=$(nproc);i++)); do 11 | thread[$i-1]=$i 12 | done 13 | show_thread=${thread[@]} 14 | COMPREPLY=($(compgen -W "$show_thread" -- "${COMP_WORDS[2]}")) 15 | elif [[ ${COMP_WORDS[1]} == "--custom-conf" || ${COMP_WORDS[1]} == "-cc" && ${#COMP_WORDS[@]} -eq "3" ]]; then 16 | list=$(ls -p | grep -v /) 17 | COMPREPLY=($(compgen -W "$list" -- "${COMP_WORDS[2]}")) 18 | fi 19 | fi 20 | } 21 | 22 | complete -F _qsort_completions qsort -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BiltuDas1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22) 2 | 3 | if(UNIX) 4 | project(qsort CXX) 5 | set(CMAKE_CXX_STANDARD 20) 6 | add_executable(qsort main.cpp) 7 | target_link_libraries(qsort magic) 8 | install( 9 | PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/qsort 10 | DESTINATION /usr/local/bin 11 | ) 12 | install( 13 | FILES ${CMAKE_SOURCE_DIR}/conf/extensions.json conf/qsort.conf 14 | DESTINATION /etc/qsort 15 | ) 16 | install( 17 | FILES ${CMAKE_SOURCE_DIR}/conf/qsort-completion.bash 18 | DESTINATION /etc/bash_completion.d 19 | ) 20 | add_custom_target(deb 21 | DEPENDS qsort 22 | COMMAND mkdir -p qsort-debian/usr/local/bin 23 | COMMAND cp -fr DEBIAN qsort-debian/ 24 | COMMAND cp -f ${CMAKE_CURRENT_BINARY_DIR}/qsort qsort-debian/usr/local/bin/ 25 | COMMAND mkdir -p qsort-debian/etc/qsort qsort-debian/etc/bash_completion.d 26 | COMMAND cp -f conf/* qsort-debian/etc/qsort/ 27 | COMMAND mv qsort-debian/etc/qsort/qsort-completion.bash qsort-debian/etc/bash_completion.d 28 | COMMAND dpkg-deb --build qsort-debian 29 | COMMAND rm -drf qsort-debian 30 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 31 | ) 32 | endif() 33 | -------------------------------------------------------------------------------- /.github/scripts/release/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ENVIRONMENT 4 | VER=$1 5 | OLD_VER=$(cat Version) 6 | 7 | # Updates the CHANGELOG.md 8 | if ! grep -c "\[$VER\]" CHANGELOG.md >/dev/null; then 9 | sed -i 's/\[unreleased\]: https:\/\/github.com\/BiltuDas1\/qsort\/compare\/0.3.2-beta...pre\/beta/\['$VER'\]: https:\/\/github.com\/BiltuDas1\/qsort\/compare\/'$OLD_VER'...'$VER'/' CHANGELOG.md 10 | sed -i 's/\[unreleased\]/\['$VER'\] - '$(date +%F)'/' CHANGELOG.md 11 | sed -i 's/'$OLD_VER'/'$VER'/' Version 12 | 13 | fp=$(grep -n "##" CHANGELOG.md | head -1 | cut -f1 -d:) 14 | cat CHANGELOG.md | head -$((fp-1)) > CHANGELOG.md.swap 15 | echo \#\# \[unreleased\] >> CHANGELOG.md.swap 16 | echo - There\'s Nothing but Crickets ¯\\\\_\(ツ\)_/¯ >> CHANGELOG.md.swap 17 | echo >> CHANGELOG.md.swap 18 | 19 | sp=$(grep -n "\-\-\-" CHANGELOG.md | cut -f1 -d:) 20 | cat CHANGELOG.md | head -$sp | tail +$fp >> CHANGELOG.md.swap 21 | echo >> CHANGELOG.md.swap 22 | echo \[unreleased\]: https://github.com/BiltuDas1/qsort/compare/$VER...pre/beta >> CHANGELOG.md.swap 23 | cat CHANGELOG.md | tail +$((sp+2)) >> CHANGELOG.md.swap 24 | 25 | rm CHANGELOG.md 26 | mv CHANGELOG.md.swap CHANGELOG.md 27 | fi 28 | 29 | echo qsort_v"$VER"_1_amd64.deb >.gitignore 30 | echo build >>.gitignore 31 | echo .gitignore >>.gitignore 32 | 33 | cmake -B build . 34 | cd build/ 35 | make deb 36 | cd .. 37 | mv "qsort-debian.deb" "qsort_v"$VER"_1_amd64.deb" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | run-name: Releasing Packages from main branch 3 | 4 | on: 5 | workflow_run: 6 | workflows: [AutoCheck] 7 | branches: [main] 8 | types: [completed] 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 14 | steps: 15 | - uses: actions/checkout@master 16 | with: 17 | fetch-depth: 0 18 | 19 | - shell: bash 20 | name: Building packages 21 | run: | 22 | bash .github/scripts/release/build.sh "${{ vars.VER }}" 23 | 24 | # Signing Files 25 | - name: Updating CHANGELOG 26 | run: | 27 | bash .github/scripts/debian-package/commit.sh 28 | 29 | # Pushing CHANGELOG 30 | - name: Releasing CHANGELOG 31 | uses: ad-m/github-push-action@master 32 | with: 33 | force: true 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | branch: ${{ github.ref }} 36 | 37 | - if: ${{ contains(vars.VER, 'beta') }} 38 | uses: marvinpinto/action-automatic-releases@latest 39 | with: 40 | repo_token: ${{ secrets.GITHUB_TOKEN }} 41 | automatic_release_tag: "${{ vars.VER }}" 42 | prerelease: true 43 | files: | 44 | qsort_v${{ vars.VER }}_1_amd64.deb 45 | 46 | - if: ${{ contains(vars.VER, 'stable') }} 47 | uses: marvinpinto/action-automatic-releases@latest 48 | with: 49 | repo_token: ${{ secrets.GITHUB_TOKEN }} 50 | automatic_release_tag: "${{ vars.VER }}" 51 | prerelease: false 52 | files: | 53 | qsort_v${{ vars.VER }}_1_amd64.deb 54 | 55 | skip-task: 56 | runs-on: ubuntu-latest 57 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 58 | steps: 59 | - shell: bash 60 | run: | 61 | echo "Error: AutoCheck Failed, So no Packages are released." 62 | exit -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [unreleased] 4 | - There's Nothing but Crickets ¯\\_(ツ)_/¯ 5 | 6 | ## [0.3.3-beta] - 2023-05-12 7 | ### Removed 8 | - Removed the obsolete Operations.md file 9 | 10 | ### Added 11 | - Added support for passing a custom configuration file (qsort.conf) 12 | - Added beta support for sorting according to file mime type instead of extensions 13 | - Added support for reading path names from [xdg-user-dirs](https://wiki.archlinux.org/title/XDG_user_directories) for qsort.conf 14 | - Added libmagic-dev as a requirement for qsort to work 15 | 16 | ### Changed 17 | - Updated README.md to reflect new features 18 | - Changed Continuous Integration (CI) process to improve stability 19 | 20 | ### Fixed 21 | - Fixed a bug that prevented users from passing multiple parameters to qsort at the same time 22 | 23 | ## [0.3.2-beta] - 2023-05-02 24 | ### Changed 25 | - Updated Debian package information to improve compatibility 26 | - Implemented building using CMake for better cross-platform support 27 | 28 | ### Fixed 29 | - Fixed an error in the AutoCheck feature 30 | 31 | ### Removed 32 | - Removed Makefile as it is no longer necessary 33 | 34 | ## [0.3.1-beta] - 2023-05-01 35 | ### Changed 36 | - Updated Debian package information to improve compatibility 37 | 38 | ### Added 39 | - Added AutoRelease feature to automate the release process 40 | 41 | ## [0.3-beta] - 2023-04-29 42 | ### Added 43 | - Added the [MIT License](https://github.com/BiltuDas1/qsort/blob/main/LICENSE) to the project 44 | - Improved control for Debian packages 45 | - Added multithreading feature to improve performance 46 | 47 | ## [0.2-beta] - unknown 48 | ### Added 49 | - Added support for Debian packages 50 | - Implemented building using Makefile 51 | - Added --help parameter to provide usage instructions 52 | 53 | ## [0.1-beta] - unknown 54 | - Initial release of qsort. 55 | --- 56 | 57 | [unreleased]: https://github.com/BiltuDas1/qsort/compare/0.3.3-beta...pre/beta 58 | [0.3.3-beta]: https://github.com/BiltuDas1/qsort/compare/0.3.2-beta...0.3.3-beta 59 | [0.3.2-beta]: https://github.com/BiltuDas1/qsort/compare/0.3.1-beta...0.3.2-beta 60 | [0.3.1-beta]: https://github.com/BiltuDas1/qsort/compare/0.3-beta...0.3.1-beta 61 | [0.3-beta]: https://github.com/BiltuDas1/qsort/releases/tag/v0.3-beta 62 | [0.2-beta]: about:blank 63 | [0.1-beta]: about:blank -------------------------------------------------------------------------------- /.github/workflows/autoscript.yml: -------------------------------------------------------------------------------- 1 | name: AutoCheck 2 | run-name: Verifying Pushed Codes of ${{ github.actor }} 3 | 4 | on: [push, pull_request] 5 | jobs: 6 | 7 | vm-testing: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | with: 13 | fetch-depth: 0 14 | 15 | - shell: bash 16 | 17 | # Building from Source Code 18 | name: Building from Source Files 19 | run: | 20 | cmake -B build . 21 | cd build/ 22 | sudo make install 23 | 24 | # Generating Configuration Files 25 | - name: Generating Configuration Files 26 | run: | 27 | bash .github/scripts/vm-testing/gen-config.sh 28 | 29 | # Executing Binary 30 | - name: Executing Binary 31 | run: | 32 | cd .github/samples 33 | qsort 34 | 35 | # Checking If Everything Working Perfectly 36 | - name: Verifying Files 37 | run: | 38 | bash .github/scripts/vm-testing/verify-files.sh 39 | 40 | debian-package: 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@master 45 | with: 46 | persist-credentials: false 47 | fetch-depth: 0 48 | 49 | - shell: bash 50 | 51 | # Updating the Source Code Information 52 | name: Updating the Source Code 53 | run: | 54 | # Updates the Source Code Version 55 | bash .github/scripts/debian-package/update-code.sh "${{ vars.VER }}" 56 | 57 | # Generating Files 58 | - name: Compiling Files 59 | run: | 60 | echo .gitignore>.gitignore 61 | echo build/>>.gitignore 62 | cmake -B build . 63 | cd build/ 64 | make 65 | 66 | # Modifying Debian Installer Information 67 | - name: Modifying Debian Installer Information 68 | run: | 69 | bash .github/scripts/debian-package/update-installer.sh "${{ vars.VER }}" 70 | 71 | # Signing Files 72 | - name: Updating Files 73 | run: | 74 | bash .github/scripts/debian-package/commit.sh 75 | 76 | # Pushing Files 77 | - name: Pushing Files 78 | uses: ad-m/github-push-action@master 79 | with: 80 | force: true 81 | github_token: ${{ secrets.GITHUB_TOKEN }} 82 | branch: ${{ github.ref }} -------------------------------------------------------------------------------- /lib/user.hpp: -------------------------------------------------------------------------------- 1 | // <----- Header files -----> 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include // libmagic-dev 13 | #include "inicpp.h" // https://github.com/Rookfighter/inifile-cpp 14 | #include "json.hpp" // https://github.com/nlohmann/json 15 | namespace fs = std::filesystem; 16 | using namespace std; 17 | 18 | // <--- Version Information Start ---> 19 | // <--- Don't modify the text here ---> 20 | 21 | const string ver = "0.3.3"; 22 | const string vertype = "beta"; 23 | 24 | // <--- Version Information End ---> 25 | 26 | // <-------- Main user.hpp starts here --------> 27 | 28 | // A dynamic String for storing temporary data 29 | string *tempstr = new string; 30 | 31 | // A dynamic String which contains the current path location 32 | string *current_path = new string(fs::current_path()); 33 | 34 | // A dynamic String for storing total arguments passed through this binary 35 | string *args = new string; 36 | 37 | // An integer for storing temporary long integers 38 | unsigned long long int tempint; 39 | 40 | // An integer variable which contains the program exit code 41 | int errorcode = 0; 42 | 43 | // Configuration file location 44 | string *confL = new string("/etc/qsort/qsort.conf"); 45 | 46 | // [exclude] in qsort.conf 47 | namespace exclude 48 | { 49 | string *extensions = new string; 50 | string *filenames = new string; 51 | } 52 | 53 | // [path] in qsort.conf 54 | namespace path 55 | { 56 | string *general = new string; 57 | string *documents = new string; 58 | string *pictures = new string; 59 | string *videos = new string; 60 | string *music = new string; 61 | } 62 | 63 | // extensions.json data 64 | namespace json 65 | { 66 | string *documents = new string; 67 | string *pictures = new string; 68 | string *musics = new string; 69 | string *videos = new string; 70 | } 71 | 72 | // Function to execute Commands into Host System 73 | // And redirect output to a string or std::ostream 74 | string execute_cmd(const string& cmd) 75 | { 76 | string result; 77 | FILE* output = popen(cmd.c_str(), "r"); 78 | if (!output) 79 | throw std::runtime_error("Error: Can't read qsort.conf, Syntax Error"); 80 | 81 | char buffer[1024]; 82 | while (fgets(buffer, sizeof(buffer), output) != nullptr) 83 | result += buffer; 84 | 85 | pclose(output); 86 | result.erase(result.find_last_not_of("\n") + 1); 87 | return result; 88 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Downloads](https://img.shields.io/github/downloads/BIltuDas1/qsort/total?style=social) 2 | # Quick File Sorter 3 | Quick File Sorter is a command-line tool for automatically sorting files based on their file extension. It can sort various types of files, such as documents, images, music, and videos, into their respective folders. This tool can save you a lot of time and effort in manually sorting your files. 4 | 5 | ## Features 6 | * This tool automatically sorts files based on their file extensions and mime types. 7 | * It supports various types of files including images, music, and videos, as well as documents. 8 | * Configuration is easy through the use of the qsort.conf file. 9 | * This tool is available as a command-line tool. 10 | 11 | ## How it works 12 | Quick File Sorter reads all extension data from extensions.json(only for sorting based on extensions) and loads them into different groups using C++ strings. It then reads the qsort.conf file and moves files to a specific path based on the matched extensions using the C++ std::filesystem::rename function. 13 | 14 | ## Requirements 15 | 16 | * CMake 3.22 or higher 17 | * libmagic-dev 18 | 19 | ## Building & Installing 20 | You can build the source code yourself by cloning the repository and running the following commands: 21 | 22 | ``` 23 | git clone https://github.com/BiltuDas1/qsort.git 24 | cd qsort/ 25 | cmake -B build . 26 | cd build/ 27 | sudo make install 28 | ``` 29 | Alternatively, you can download pre-built binaries from the releases page. 30 | 31 | ### Source Code Building 32 | |Task Name|`main`|`pre/beta`| 33 | |---------|------|----------| 34 | |AutoCheck|![AutoCheck](https://github.com/BiltuDas1/qsort/actions/workflows/autoscript.yml/badge.svg?branch=main)|![AutoCheck](https://github.com/BiltuDas1/qsort/actions/workflows/autoscript.yml/badge.svg?branch=pre/beta)| 35 | |Release|![Release](https://github.com/BiltuDas1/qsort/actions/workflows/release.yml/badge.svg?branch=main)| Not Applicable :x: | 36 | 37 | ## Usage 38 | Once installed, simply navigate to the directory containing the files you want to sort and enter the `qsort` command. Quick File Sorter will automatically sort files to their respective type of documents (i.e doc, ppt, pdf all those file will be moved into Document Folder). 39 | 40 | 41 | ### Parameters 42 | |Parameter|Description| 43 | |---------|-----------| 44 | |--version|Prints the version information of the qsort program.| 45 | |--help|Shows the help window, which lists all the available parameters and their descriptions.| 46 | |--edit-conf [cli]|Opens the qsort configuration file. If the optional cli parameter is specified, then it will force the program to open the configuration file in command-line interface mode. This parameter requires sudo privileges.| 47 | |--thread [count]|Specifies the number of threads that the qsort program will use for sorting. The value of count can be any positive integer between 1 and the max core your pc have. If this parameter is not specified, then the program will use a single thread for sorting.| 48 | |--custom-conf [filename] or -cc|Allows the user to choose a custom configuration file for the qsort program. The filename parameter specifies the name of the custom configuration file that the user wants to use. The -cc option is a shorthand for this parameter.| 49 | |--mime|(Beta) This parameter forces the qsort program to sort files according to their MIME type. By default, the program sorts files based on their file extensions. This parameter is still in beta, and its functionality may be limited or unstable.| 50 | 51 | # Configuration 52 | Quick File Sorter uses a configuration file to determine where to move the files. The default configuration file is located at /etc/qsort/qsort.conf. To modify the configuration, you can edit this file using your favorite text editor. 53 | 54 | The qsort.conf file allows you to specify the paths where files should be moved to, and to exclude certain files or file extensions from being moved. 55 | 56 | 57 | ## Path Configuration 58 | The Path section specifies the destination path for each file type. You can use predefined variables or provide custom paths. 59 | 60 | To specify the path where a particular file type should be moved to, use the following format in the `[Path]` section of the qsort.conf file: 61 | 62 | ``` 63 | [type]=$PATH 64 | ``` 65 | For example, to specify that all image files should be moved to the Pictures directory, use: 66 | ``` 67 | Pictures=$PICTURES 68 | ``` 69 | Note that the `$PICTURES` variable is obtained from the xdg-user-dirs system, which is set to `/home/user/Pictures`. While you can use the direct path `/home/user/Pictures` in your configuration file. 70 | 71 | You can also refer to the [xdg-user-dirs](https://wiki.archlinux.org/title/XDG_user_directories) documentation to learn more about using predefined variables for directory paths. 72 | 73 | Currently The following types are supported: 74 | 75 | * General 76 | * Videos 77 | * Pictures 78 | * Documents 79 | * Music 80 | 81 | 82 | ## Exclusion Configuration 83 | The Exclude section allows you to exclude certain files or file types from being moved. You can exclude files by their extensions or specific filenames. 84 | 85 | To exclude certain files or file extensions from being moved, use the following format in the [Exclude] section of the qsort.conf file: 86 | 87 | ``` 88 | [Exclude] 89 | Extensions=ext1,ext2 90 | Filenames=file1,file2 91 | ``` 92 | 93 | For example, to exclude all files with the .txt and .py extensions, as well as a file named requirements.txt, use: 94 | 95 | ``` 96 | [Exclude] 97 | Extensions=py,txt 98 | Filenames=requirements.txt 99 | ``` 100 | In the example above, the extensions py and txt are excluded, meaning files with these extensions will not be moved. Additionally, the file named requirements.txt will be excluded. 101 | 102 | Please note that the configuration file can be customized to suit your specific needs. Make sure to follow the format specified in the examples above and adjust the paths and exclusions accordingly. 103 | 104 | 105 | Here is a Final Example of qsort.conf file: 106 | 107 | ``` 108 | [Path] 109 | General=$HOME 110 | Videos=$VIDEOS 111 | Pictures=$PICTURES 112 | Documents=$DOCUMENTS 113 | Music=$MUSIC 114 | 115 | [Exclude] 116 | Extensions=py,txt 117 | Filenames=requirements.txt 118 | ``` 119 | 120 | Note: Incorrect Configuration might crash the program without any warning. 121 | 122 | ### Speed 123 | I can't gurentee that the speed will be same into your own Condition, but here is the speed for my personal laptop 124 | 125 | ![Sorting Speed](.github/Speed.png) 126 | 127 | It took about 7 second to sort 100k + 2 files 128 | 129 | Processor: Intel Core i5 M460 @ 2.53GHz 130 | RAM: 6GiB 131 | Hard Disk: HS-SSD-E100 256G 132 | 133 | ## License 134 | This project is licensed under [MIT License](/LICENSE) 135 | 136 | ## Contributing 137 | If you would like to add a new feature to Quick File Sorter, please create an issue first to discuss the feature with the project owner and gain approval before proceeding with any changes. If you encounter an issue or bug that you would like to fix, you are welcome to create a pull request with your proposed changes directly. Contributions of any kind are greatly appreciated! -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "lib/user.hpp" 2 | 3 | #define yes true 4 | #define no false 5 | 6 | // Initialization 7 | class init 8 | { 9 | static void xdg_user_dir(string& value, const string optional){ 10 | value = execute_cmd("xdg-user-dir " + optional); 11 | } 12 | static void xdg_user_dir(string& value){ 13 | int firstpath, secondpath; 14 | string str; 15 | while(value.find("$") != string::npos){ 16 | firstpath = value.find_first_of("$"); 17 | str = value.substr(firstpath + 1); 18 | // If the val doesn't contains any slash 19 | if(str.find_first_of("/") != string::npos){ 20 | secondpath = str.find_first_of("/") + 1; 21 | } else { 22 | secondpath = str.length() + 1; 23 | } 24 | str.erase(secondpath - 1); 25 | str = execute_cmd("xdg-user-dir " + str); 26 | if(!value.find_first_of("/")){ 27 | if(str.substr(0, 1) == "/" && value.substr(firstpath - 1, 1) == "/"){ 28 | value.replace(firstpath - 1, secondpath + 1, str); 29 | return; 30 | } 31 | } 32 | value.replace(firstpath, secondpath, str); 33 | } 34 | } 35 | 36 | public: 37 | static void getconfig() 38 | { 39 | ini::IniFile conf; 40 | conf.load(*confL); 41 | 42 | *path::general = conf["Path"]["General"].as(); 43 | *path::documents = conf["Path"]["Documents"].as(); 44 | *path::pictures = conf["Path"]["Pictures"].as(); 45 | *path::videos = conf["Path"]["Videos"].as(); 46 | *path::music = conf["Path"]["Music"].as(); 47 | 48 | // If no value is set then 49 | if(path::general->empty()) 50 | xdg_user_dir(*path::general, "HOME"); 51 | else 52 | xdg_user_dir(*path::general); 53 | if(path::documents->empty()) 54 | xdg_user_dir(*path::documents, "DOCUMENTS"); 55 | else 56 | xdg_user_dir(*path::documents); 57 | if(path::pictures->empty()) 58 | xdg_user_dir(*path::pictures, "PICTURES"); 59 | else 60 | xdg_user_dir(*path::pictures); 61 | if(path::videos->empty()) 62 | xdg_user_dir(*path::videos, "VIDEOS"); 63 | else 64 | xdg_user_dir(*path::videos); 65 | if(path::music->empty()) 66 | xdg_user_dir(*path::music, "MUSIC"); 67 | else 68 | xdg_user_dir(*path::music); 69 | 70 | *exclude::extensions = conf["Exclude"]["Extensions"].as(); 71 | *exclude::filenames = conf["Exclude"]["Filenames"].as(); 72 | } 73 | 74 | static void getjson() 75 | { 76 | using nljson = nlohmann::json; 77 | 78 | ifstream json_file("/etc/qsort/extensions.json"); 79 | nljson jsondata = nljson::parse(json_file); 80 | 81 | // Access the values in the JSON file 82 | auto documents = jsondata["Documents"]; 83 | for (auto& document : documents) { 84 | json::documents->append(" "); 85 | json::documents->append(document); 86 | } 87 | 88 | auto pictures = jsondata["Pictures"]; 89 | for (auto& picture : pictures) { 90 | json::pictures->append(" "); 91 | json::pictures->append(picture); 92 | } 93 | 94 | auto musics = jsondata["Musics"]; 95 | for (auto& music : musics) { 96 | json::musics->append(" "); 97 | json::musics->append(music); 98 | } 99 | 100 | auto videos = jsondata["Videos"]; 101 | for (auto& video : videos) { 102 | json::videos->append(" "); 103 | json::videos->append(video); 104 | } 105 | } 106 | 107 | }; 108 | 109 | 110 | // Main Program 111 | class base : protected init 112 | { 113 | unsigned short thread_count = std::thread::hardware_concurrency(); 114 | bool ismime = false; 115 | 116 | void version() 117 | { 118 | cout << "qsort v" << ver << "-" << vertype << endl; 119 | } 120 | 121 | void help(string exec) 122 | { 123 | cout << exec << " [--thread] [count]\n\n"; 124 | cout << "Parameters of qsort are:\n"; 125 | cout << "--version Prints the version information\n"; 126 | cout << "--help Shows this window\n"; 127 | cout << "--edit-conf [cli] Opens qsort configuration file(requires sudo)\n"; 128 | cout << " cli Force to cli mode\n"; 129 | cout << "--thread [count] Threads count which will be used\n"; 130 | cout << " The count range only can between [1-" << thread_count << "]\n"; 131 | cout << "--custom-conf [filename] Choose a Custom qsort.conf file\n"; 132 | cout << " -cc Same as --custom-conf\n"; 133 | cout << "--mime (Beta) This parameter force qsort to sort\n"; 134 | cout << " files according to their mime type"; 135 | cout << endl; 136 | } 137 | 138 | void error(string err) 139 | { 140 | cerr << "Error: Unrecognized parameter " << err << endl; 141 | errorcode = 1; 142 | } 143 | 144 | static void operations(const long long int count, const long long int start_point, const bool ismime){ 145 | // Listing all folder/files 146 | auto iter = fs::directory_iterator{*current_path}; 147 | auto subrange = iter | ranges::views::drop(start_point); 148 | 149 | magic_t magic_cookie = magic_open(MAGIC_MIME_TYPE); 150 | 151 | if (ismime){ 152 | magic_load(magic_cookie, NULL); 153 | } 154 | 155 | for (auto const &files : subrange | ranges::views::take(count)) 156 | { 157 | if (fs::is_regular_file(files.path())) 158 | { 159 | fs::path pathfile{files.path()}; 160 | string filename = files.path().filename().string(); 161 | string extension; 162 | 163 | // Storing File Names into filename 164 | filename.erase(0, filename.find_last_of("/") + 1); 165 | if (filename.find_last_of(".") != string::npos) 166 | { 167 | extension = filename.substr(filename.find_last_of(".") + 1); 168 | 169 | // Check If extension exist into configuration 170 | if (exclude::extensions->find(extension) != string::npos) 171 | continue; 172 | 173 | // If filename exist into configuration 174 | if (exclude::filenames->find(filename) != string::npos) 175 | continue; 176 | 177 | // Check if extension exist into Catagories 178 | string mvpath; 179 | 180 | if(ismime){ 181 | string type = magic_file(magic_cookie, files.path().filename().c_str()); 182 | type.erase(type.find("/")); 183 | if (type == "image") 184 | mvpath = *path::pictures; 185 | else if (type == "video") 186 | mvpath = *path::videos; 187 | else if (type == "audio") 188 | mvpath = *path::music; 189 | else 190 | mvpath = *path::general; 191 | } else { 192 | if (json::documents->find(extension) != string::npos) 193 | mvpath = *path::documents; 194 | 195 | else if (json::pictures->find(extension) != string::npos) 196 | mvpath = *path::pictures; 197 | 198 | else if (json::musics->find(extension) != string::npos) 199 | mvpath = *path::music; 200 | 201 | else if (json::videos->find(extension) != string::npos) 202 | mvpath = *path::videos; 203 | else 204 | mvpath = *path::general; 205 | } 206 | 207 | try { 208 | fs::rename(pathfile, mvpath + "/" + filename); 209 | } 210 | catch(...){ 211 | cout << "Unable to rename: " << pathfile << endl; 212 | } 213 | 214 | } 215 | else 216 | { 217 | // If no extension and file not found into Exclude list then move to General 218 | if (exclude::filenames->find(filename) == string::npos) 219 | { 220 | fs::path temp_path = *path::general + "/" + filename; 221 | fs::rename(pathfile, temp_path); 222 | } 223 | } 224 | } 225 | } 226 | 227 | if(ismime){ 228 | magic_close(magic_cookie); 229 | } 230 | } 231 | 232 | void work_thread(const bool ismime){ 233 | // If no arguments passed then do main operation 234 | // Initializing Variables/Settings 235 | thread g_config(init::getconfig); 236 | thread g_json(init::getjson); 237 | g_config.join(); 238 | g_json.join(); 239 | 240 | const long long unsigned int file_count = distance(fs::directory_iterator(*current_path), fs::directory_iterator{}); 241 | 242 | thread threads[thread_count]; 243 | const long long int thread_process = file_count/thread_count; 244 | 245 | bool taskDone = no; 246 | 247 | if (thread_process > thread_count){ 248 | tempint = 0; 249 | 250 | cout << "Using " << thread_count << " Threads\n"; 251 | // Creating threads 252 | for(unsigned short i = 0; i < thread_count; ++i){ 253 | threads[i] = thread(&base::operations, thread_process, tempint, ismime); 254 | tempint = tempint + thread_process; 255 | if(tempint > file_count){ 256 | tempint = file_count; 257 | } 258 | } 259 | 260 | // Waiting for thread to complete 261 | for(auto &thread : threads){ 262 | thread.join(); 263 | } 264 | 265 | taskDone = yes; 266 | } 267 | 268 | // Checks if any remaining files 269 | tempint = 0; 270 | const unsigned int remaining_count = distance(fs::directory_iterator(*current_path), fs::directory_iterator{}); 271 | if (remaining_count){ 272 | operations(remaining_count, tempint, ismime); 273 | } 274 | 275 | // If operation completed 276 | if(taskDone){ 277 | cout << "Operation Completed" << endl; 278 | } 279 | } 280 | 281 | 282 | public: 283 | base(const int arg, const char **argv) 284 | { 285 | // If arguments passed into the executable 286 | if (arg > 1) 287 | { 288 | // Version Information 289 | *tempstr = argv[1]; 290 | if (!tempstr->compare("--version")) 291 | { 292 | if (arg == 2) 293 | version(); 294 | else 295 | error(argv[2]); 296 | } 297 | // Help 298 | else if (!tempstr->compare("--help")) 299 | { 300 | if (arg == 2) 301 | help(argv[0]); 302 | else 303 | error(argv[2]); 304 | } 305 | // Edit Configuration 306 | else if (!tempstr->compare("--edit-conf")) 307 | { 308 | const char *editor; 309 | if (arg == 2){ 310 | if(access("/usr/bin/xdg-open", X_OK)){ 311 | editor = "editor"; 312 | } else { 313 | editor = "xdg-open"; 314 | } 315 | } else { 316 | *tempstr = argv[2]; 317 | if (!tempstr->compare("cli")){ 318 | editor = "editor"; 319 | } 320 | } 321 | 322 | char* const args[] = {const_cast(editor), const_cast("/etc/qsort/qsort.conf"), nullptr}; 323 | 324 | if (geteuid() != 0){ 325 | cerr << "Error: Sudo permission required\n"; 326 | } else { 327 | execvp(editor, args); // Open default text editor 328 | cerr << "Error: Could not open text editor\n"; 329 | } 330 | errorcode = 1; 331 | } 332 | // For Multiple types arguments 333 | else { 334 | // Appending the whole parameters list 335 | for(int i=1; iappend("\\"); 339 | args->append(argv[i]); 340 | } 341 | args->append("\\"); 342 | 343 | // Thread Parameter 344 | if (args->find("--thread") != string::npos){ 345 | if (arg == 2) 346 | { 347 | work_thread(false); 348 | } else { 349 | *tempstr = args->substr(args->find("--thread") + 8 + 1); 350 | tempstr->erase(tempstr->find_first_of("\\")); 351 | if (stoi(*tempstr) <= thread_count && stoi(*tempstr) >= 1){ 352 | this->thread_count = stoi(*tempstr); 353 | } else { 354 | error("Error: Out of range. You can only use any thread between [1-" + to_string(thread_count) + "]."); 355 | } 356 | } 357 | } 358 | 359 | // Custom qsort.conf 360 | if (args->find("--custom-conf") != string::npos || args->find("-cc") != string::npos){ 361 | if (arg == 2){ 362 | error("No configuration file name specified"); 363 | } else { 364 | if(args->find("-cc") != string::npos) 365 | *tempstr = args->substr(args->find("-cc") + 3 + 1); 366 | else 367 | *tempstr = args->substr(args->find("--custom-conf") + 13 + 1); 368 | tempstr->erase(tempstr->find_first_of("\\")); 369 | if(fs::exists(*tempstr)){ 370 | if(fs::is_regular_file(*tempstr) && !fs::is_empty(*tempstr)) 371 | *confL = *tempstr; 372 | else 373 | error("Error: " + *tempstr + " is not a valid configuration file"); 374 | } else { 375 | error("Error: Configuration file not found"); 376 | } 377 | } 378 | } 379 | 380 | // Mime type sorter 381 | if (args->find("--mime") != string::npos){ 382 | this->ismime = true; 383 | } 384 | 385 | // Load the task 386 | work_thread(this->ismime); 387 | } 388 | } 389 | else 390 | { 391 | work_thread(false); 392 | } 393 | } 394 | 395 | ~base() 396 | { 397 | delete tempstr, current_path, confL, args; 398 | delete exclude::extensions, exclude::filenames; 399 | delete path::documents, path::general, path::music, path::pictures, path::videos; 400 | delete json::documents, json::musics, json::pictures, json::videos; 401 | } 402 | }; 403 | 404 | int main(const int arg, const char **argv) 405 | { 406 | // Passing parameters to the base class 407 | base *b = new base(arg, argv); 408 | delete b; 409 | return errorcode; 410 | } 411 | -------------------------------------------------------------------------------- /lib/inicpp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * inicpp.h 3 | * 4 | * Created on: 26 Dec 2015 5 | * Author: Fabian Meyer 6 | * License: MIT 7 | */ 8 | 9 | #ifndef INICPP_H_ 10 | #define INICPP_H_ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace ini 22 | { 23 | /************************************************ 24 | * Helper Functions 25 | ************************************************/ 26 | 27 | /** Returns a string of whitespace characters. */ 28 | constexpr const char *whitespaces() 29 | { 30 | return " \t\n\r\f\v"; 31 | } 32 | 33 | /** Returns a string of indentation characters. */ 34 | constexpr const char *indents() 35 | { 36 | return " \t"; 37 | } 38 | 39 | /** Trims a string in place. 40 | * @param str string to be trimmed in place */ 41 | inline void trim(std::string &str) 42 | { 43 | // first erasing from end should be slighty more efficient 44 | // because erasing from start potentially moves all chars 45 | // multiple indices towards the front. 46 | 47 | auto lastpos = str.find_last_not_of(whitespaces()); 48 | if(lastpos == std::string::npos) 49 | { 50 | str.clear(); 51 | return; 52 | } 53 | 54 | str.erase(lastpos + 1); 55 | str.erase(0, str.find_first_not_of(whitespaces())); 56 | } 57 | 58 | /************************************************ 59 | * Conversion Functors 60 | ************************************************/ 61 | 62 | inline bool strToLong(const std::string &value, long &result) 63 | { 64 | char *endptr; 65 | // check if decimal 66 | result = std::strtol(value.c_str(), &endptr, 10); 67 | if(*endptr == '\0') 68 | return true; 69 | // check if octal 70 | result = std::strtol(value.c_str(), &endptr, 8); 71 | if(*endptr == '\0') 72 | return true; 73 | // check if hex 74 | result = std::strtol(value.c_str(), &endptr, 16); 75 | if(*endptr == '\0') 76 | return true; 77 | 78 | return false; 79 | } 80 | 81 | inline bool strToULong(const std::string &value, unsigned long &result) 82 | { 83 | char *endptr; 84 | // check if decimal 85 | result = std::strtoul(value.c_str(), &endptr, 10); 86 | if(*endptr == '\0') 87 | return true; 88 | // check if octal 89 | result = std::strtoul(value.c_str(), &endptr, 8); 90 | if(*endptr == '\0') 91 | return true; 92 | // check if hex 93 | result = std::strtoul(value.c_str(), &endptr, 16); 94 | if(*endptr == '\0') 95 | return true; 96 | 97 | return false; 98 | } 99 | 100 | template 101 | struct Convert 102 | {}; 103 | 104 | template<> 105 | struct Convert 106 | { 107 | void decode(const std::string &value, bool &result) 108 | { 109 | std::string str(value); 110 | std::transform(str.begin(), str.end(), str.begin(), [](const char c){ 111 | return static_cast(::toupper(c)); 112 | }); 113 | 114 | if(str == "TRUE") 115 | result = true; 116 | else if(str == "FALSE") 117 | result = false; 118 | else 119 | throw std::invalid_argument("field is not a bool"); 120 | } 121 | 122 | void encode(const bool value, std::string &result) 123 | { 124 | result = value ? "true" : "false"; 125 | } 126 | }; 127 | 128 | template<> 129 | struct Convert 130 | { 131 | void decode(const std::string &value, char &result) 132 | { 133 | assert(value.size() > 0); 134 | result = value[0]; 135 | } 136 | 137 | void encode(const char value, std::string &result) 138 | { 139 | result = value; 140 | } 141 | }; 142 | 143 | template<> 144 | struct Convert 145 | { 146 | void decode(const std::string &value, unsigned char &result) 147 | { 148 | assert(value.size() > 0); 149 | result = value[0]; 150 | } 151 | 152 | void encode(const unsigned char value, std::string &result) 153 | { 154 | result = value; 155 | } 156 | }; 157 | 158 | template<> 159 | struct Convert 160 | { 161 | void decode(const std::string &value, short &result) 162 | { 163 | long tmp; 164 | if(!strToLong(value, tmp)) 165 | throw std::invalid_argument("field is not a short"); 166 | result = static_cast(tmp); 167 | } 168 | 169 | void encode(const short value, std::string &result) 170 | { 171 | std::stringstream ss; 172 | ss << value; 173 | result = ss.str(); 174 | } 175 | }; 176 | 177 | template<> 178 | struct Convert 179 | { 180 | void decode(const std::string &value, unsigned short &result) 181 | { 182 | unsigned long tmp; 183 | if(!strToULong(value, tmp)) 184 | throw std::invalid_argument("field is not an unsigned short"); 185 | result = static_cast(tmp); 186 | } 187 | 188 | void encode(const unsigned short value, std::string &result) 189 | { 190 | std::stringstream ss; 191 | ss << value; 192 | result = ss.str(); 193 | } 194 | }; 195 | 196 | template<> 197 | struct Convert 198 | { 199 | void decode(const std::string &value, int &result) 200 | { 201 | long tmp; 202 | if(!strToLong(value, tmp)) 203 | throw std::invalid_argument("field is not an int"); 204 | result = static_cast(tmp); 205 | } 206 | 207 | void encode(const int value, std::string &result) 208 | { 209 | std::stringstream ss; 210 | ss << value; 211 | result = ss.str(); 212 | } 213 | }; 214 | 215 | template<> 216 | struct Convert 217 | { 218 | void decode(const std::string &value, unsigned int &result) 219 | { 220 | unsigned long tmp; 221 | if(!strToULong(value, tmp)) 222 | throw std::invalid_argument("field is not an unsigned int"); 223 | result = static_cast(tmp); 224 | } 225 | 226 | void encode(const unsigned int value, std::string &result) 227 | { 228 | std::stringstream ss; 229 | ss << value; 230 | result = ss.str(); 231 | } 232 | }; 233 | 234 | template<> 235 | struct Convert 236 | { 237 | void decode(const std::string &value, long &result) 238 | { 239 | if(!strToLong(value, result)) 240 | throw std::invalid_argument("field is not a long"); 241 | } 242 | 243 | void encode(const long value, std::string &result) 244 | { 245 | std::stringstream ss; 246 | ss << value; 247 | result = ss.str(); 248 | } 249 | }; 250 | 251 | template<> 252 | struct Convert 253 | { 254 | void decode(const std::string &value, unsigned long &result) 255 | { 256 | if(!strToULong(value, result)) 257 | throw std::invalid_argument("field is not an unsigned long"); 258 | } 259 | 260 | void encode(const unsigned long value, std::string &result) 261 | { 262 | std::stringstream ss; 263 | ss << value; 264 | result = ss.str(); 265 | } 266 | }; 267 | 268 | template<> 269 | struct Convert 270 | { 271 | void decode(const std::string &value, double &result) 272 | { 273 | result = std::stod(value); 274 | } 275 | 276 | void encode(const double value, std::string &result) 277 | { 278 | std::stringstream ss; 279 | ss << value; 280 | result = ss.str(); 281 | } 282 | }; 283 | 284 | template<> 285 | struct Convert 286 | { 287 | void decode(const std::string &value, float &result) 288 | { 289 | result = std::stof(value); 290 | } 291 | 292 | void encode(const float value, std::string &result) 293 | { 294 | std::stringstream ss; 295 | ss << value; 296 | result = ss.str(); 297 | } 298 | }; 299 | 300 | template<> 301 | struct Convert 302 | { 303 | void decode(const std::string &value, std::string &result) 304 | { 305 | result = value; 306 | } 307 | 308 | void encode(const std::string &value, std::string &result) 309 | { 310 | result = value; 311 | } 312 | }; 313 | 314 | template<> 315 | struct Convert 316 | { 317 | void encode(const char* const &value, std::string &result) 318 | { 319 | result = value; 320 | } 321 | 322 | void decode(const std::string &value, const char* &result) 323 | { 324 | result = value.c_str(); 325 | } 326 | }; 327 | 328 | template<> 329 | struct Convert 330 | { 331 | void encode(const char* const &value, std::string &result) 332 | { 333 | result = value; 334 | } 335 | }; 336 | 337 | template 338 | struct Convert 339 | { 340 | void encode(const char *value, std::string &result) 341 | { 342 | result = value; 343 | } 344 | }; 345 | 346 | class IniField 347 | { 348 | private: 349 | std::string value_; 350 | 351 | public: 352 | IniField() : value_() 353 | {} 354 | 355 | IniField(const std::string &value) : value_(value) 356 | {} 357 | IniField(const IniField &field) : value_(field.value_) 358 | {} 359 | 360 | ~IniField() 361 | {} 362 | 363 | template 364 | T as() const 365 | { 366 | Convert conv; 367 | T result; 368 | conv.decode(value_, result); 369 | return result; 370 | } 371 | 372 | template 373 | IniField &operator=(const T &value) 374 | { 375 | Convert conv; 376 | conv.encode(value, value_); 377 | return *this; 378 | } 379 | 380 | IniField &operator=(const IniField &field) 381 | { 382 | value_ = field.value_; 383 | return *this; 384 | } 385 | }; 386 | 387 | struct StringInsensitiveLess 388 | { 389 | bool operator()(std::string lhs, std::string rhs) const 390 | { 391 | std::transform(lhs.begin(), lhs.end(), lhs.begin(), [](const char c){ 392 | return static_cast(::tolower(c)); 393 | }); 394 | std::transform(rhs.begin(), rhs.end(), rhs.begin(), [](const char c){ 395 | return static_cast(::tolower(c)); 396 | }); 397 | return lhs < rhs; 398 | } 399 | }; 400 | 401 | template 402 | class IniSectionBase : public std::map 403 | { 404 | public: 405 | IniSectionBase() 406 | {} 407 | ~IniSectionBase() 408 | {} 409 | }; 410 | 411 | using IniSection = IniSectionBase>; 412 | using IniSectionCaseInsensitive = IniSectionBase; 413 | 414 | template 415 | class IniFileBase : public std::map, Comparator> 416 | { 417 | private: 418 | char fieldSep_ = '='; 419 | char esc_ = '\\'; 420 | std::vector commentPrefixes_ = { "#" , ";" }; 421 | bool multiLineValues_ = false; 422 | 423 | void eraseComment(const std::string &commentPrefix, 424 | std::string &str, 425 | std::string::size_type startpos = 0) 426 | { 427 | size_t prefixpos = str.find(commentPrefix, startpos); 428 | if(std::string::npos == prefixpos) 429 | return; 430 | // Found a comment prefix, is it escaped? 431 | if(0 != prefixpos && str[prefixpos - 1] == esc_) 432 | { 433 | // The comment prefix is escaped, so just delete the escape char 434 | // and keep erasing after the comment prefix 435 | str.erase(prefixpos - 1, 1); 436 | eraseComment( 437 | commentPrefix, str, prefixpos - 1 + commentPrefix.size()); 438 | } 439 | else 440 | { 441 | str.erase(prefixpos); 442 | } 443 | } 444 | 445 | void eraseComments(std::string &str) 446 | { 447 | for(const std::string &commentPrefix : commentPrefixes_) 448 | eraseComment(commentPrefix, str); 449 | } 450 | 451 | /** Tries to find a suitable comment prefix for the string data at the given 452 | * position. Returns commentPrefixes_.end() if not match was found. */ 453 | std::vector::const_iterator findCommentPrefix(const std::string &str, 454 | const std::size_t startpos) const 455 | { 456 | // if startpos is invalid simply return "not found" 457 | if(startpos >= str.size()) 458 | return commentPrefixes_.end(); 459 | 460 | for(size_t i = 0; i < commentPrefixes_.size(); ++i) 461 | { 462 | const std::string &prefix = commentPrefixes_[i]; 463 | // if this comment prefix is longer than the string view itself 464 | // then skip 465 | if(prefix.size() + startpos > str.size()) 466 | continue; 467 | 468 | bool match = true; 469 | for(size_t j = 0; j < prefix.size() && match; ++j) 470 | match = str[startpos + j] == prefix[j]; 471 | 472 | if(match) 473 | return commentPrefixes_.begin() + i; 474 | } 475 | 476 | return commentPrefixes_.end(); 477 | } 478 | 479 | void writeEscaped(std::ostream &os, const std::string &str) const 480 | { 481 | for(size_t i = 0; i < str.length(); ++i) 482 | { 483 | auto prefixpos = findCommentPrefix(str, i); 484 | // if no suitable prefix was found at this position 485 | // then simply write the current character 486 | if(prefixpos != commentPrefixes_.end()) 487 | { 488 | const std::string &prefix = *prefixpos; 489 | os.put(esc_); 490 | os.write(prefix.c_str(), prefix.size()); 491 | i += prefix.size() - 1; 492 | } 493 | else if (multiLineValues_ && str[i] == '\n') 494 | os.write("\n\t", 2); 495 | else 496 | os.put(str[i]); 497 | } 498 | } 499 | 500 | public: 501 | IniFileBase() = default; 502 | 503 | IniFileBase(const char fieldSep, const char comment) 504 | : fieldSep_(fieldSep), commentPrefixes_(1, std::string(1, comment)) 505 | {} 506 | 507 | IniFileBase(const std::string &filename) 508 | { 509 | load(filename); 510 | } 511 | 512 | IniFileBase(std::istream &is) 513 | { 514 | decode(is); 515 | } 516 | 517 | IniFileBase(const char fieldSep, 518 | const std::vector &commentPrefixes) 519 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 520 | {} 521 | 522 | IniFileBase(const std::string &filename, 523 | const char fieldSep, 524 | const std::vector &commentPrefixes) 525 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 526 | { 527 | load(filename); 528 | } 529 | 530 | IniFileBase(std::istream &is, 531 | const char fieldSep, 532 | const std::vector &commentPrefixes) 533 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 534 | { 535 | decode(is); 536 | } 537 | 538 | ~IniFileBase() 539 | {} 540 | 541 | /** Sets the separator charactor for fields in the INI file. 542 | * @param sep separator character to be used. */ 543 | void setFieldSep(const char sep) 544 | { 545 | fieldSep_ = sep; 546 | } 547 | 548 | /** Sets the character that should be interpreted as the start of comments. 549 | * Default is '#'. 550 | * Note: If the inifile contains the comment character as data it must be prefixed with 551 | * the configured escape character. 552 | * @param comment comment character to be used. */ 553 | void setCommentChar(const char comment) 554 | { 555 | commentPrefixes_ = {std::string(1, comment)}; 556 | } 557 | 558 | /** Sets the list of strings that should be interpreted as the start of comments. 559 | * Default is [ "#" ]. 560 | * Note: If the inifile contains any comment string as data it must be prefixed with 561 | * the configured escape character. 562 | * @param commentPrefixes vector of comment prefix strings to be used. */ 563 | void setCommentPrefixes(const std::vector &commentPrefixes) 564 | { 565 | commentPrefixes_ = commentPrefixes; 566 | } 567 | 568 | /** Sets the character that should be used to escape comment prefixes. 569 | * Default is '\'. 570 | * @param esc escape character to be used. */ 571 | void setEscapeChar(const char esc) 572 | { 573 | esc_ = esc; 574 | } 575 | 576 | /** Sets whether or not to parse multi-line field values. 577 | * Default is false. 578 | * @param enable enable or disable? */ 579 | void setMultiLineValues(bool enable) 580 | { 581 | multiLineValues_ = enable; 582 | } 583 | 584 | /** Tries to decode a ini file from the given input stream. 585 | * @param is input stream from which data should be read. */ 586 | void decode(std::istream &is) 587 | { 588 | this->clear(); 589 | int lineNo = 0; 590 | IniSectionBase *currentSection = nullptr; 591 | std::string mutliLineValueFieldName = ""; 592 | std::string line; 593 | // iterate file line by line 594 | while(!is.eof() && !is.fail()) 595 | { 596 | std::getline(is, line, '\n'); 597 | eraseComments(line); 598 | bool hasIndent = line.find_first_not_of(indents()) != 0; 599 | trim(line); 600 | ++lineNo; 601 | 602 | // skip if line is empty 603 | if(line.size() == 0) 604 | continue; 605 | 606 | if(line[0] == '[') 607 | { 608 | // line is a section 609 | // check if the section is also closed on same line 610 | std::size_t pos = line.find("]"); 611 | if(pos == std::string::npos) 612 | { 613 | std::stringstream ss; 614 | ss << "l." << lineNo 615 | << ": ini parsing failed, section not closed"; 616 | throw std::logic_error(ss.str()); 617 | } 618 | // check if the section name is empty 619 | if(pos == 1) 620 | { 621 | std::stringstream ss; 622 | ss << "l." << lineNo 623 | << ": ini parsing failed, section is empty"; 624 | throw std::logic_error(ss.str()); 625 | } 626 | 627 | // retrieve section name 628 | std::string secName = line.substr(1, pos - 1); 629 | currentSection = &((*this)[secName]); 630 | 631 | // clear multiline value field name 632 | // a new section means there is no value to continue 633 | mutliLineValueFieldName = ""; 634 | } 635 | else 636 | { 637 | // line is a field definition 638 | // check if section was already opened 639 | if(currentSection == nullptr) 640 | { 641 | std::stringstream ss; 642 | ss << "l." << lineNo 643 | << ": ini parsing failed, field has no section"; 644 | throw std::logic_error(ss.str()); 645 | } 646 | 647 | // find key value separator 648 | std::size_t pos = line.find(fieldSep_); 649 | if (multiLineValues_ && hasIndent && mutliLineValueFieldName != "") 650 | { 651 | // extend a multi-line value 652 | IniField previous_value = (*currentSection)[mutliLineValueFieldName]; 653 | std::string value = previous_value.as() + "\n" + line; 654 | (*currentSection)[mutliLineValueFieldName] = value; 655 | } 656 | else if(pos == std::string::npos) 657 | { 658 | std::stringstream ss; 659 | ss << "l." << lineNo 660 | << ": ini parsing failed, no '" 661 | << fieldSep_ 662 | << "' found"; 663 | if (multiLineValues_) 664 | ss << ", and not a multi-line value continuation"; 665 | throw std::logic_error(ss.str()); 666 | } 667 | else 668 | { 669 | // retrieve field name and value 670 | std::string name = line.substr(0, pos); 671 | trim(name); 672 | std::string value = line.substr(pos + 1, std::string::npos); 673 | trim(value); 674 | (*currentSection)[name] = value; 675 | // store last field name for potential multi-line values 676 | mutliLineValueFieldName = name; 677 | } 678 | } 679 | } 680 | } 681 | 682 | /** Tries to decode a ini file from the given input string. 683 | * @param content string to be decoded. */ 684 | void decode(const std::string &content) 685 | { 686 | std::istringstream ss(content); 687 | decode(ss); 688 | } 689 | 690 | /** Tries to load and decode a ini file from the file at the given path. 691 | * @param fileName path to the file that should be loaded. */ 692 | void load(const std::string &fileName) 693 | { 694 | std::ifstream is(fileName.c_str()); 695 | decode(is); 696 | } 697 | 698 | /** Encodes this inifile object and writes the output to the given stream. 699 | * @param os target stream. */ 700 | void encode(std::ostream &os) const 701 | { 702 | // iterate through all sections in this file 703 | for(const auto &filePair : *this) 704 | { 705 | os.put('['); 706 | writeEscaped(os, filePair.first); 707 | os.put(']'); 708 | os.put('\n'); 709 | 710 | // iterate through all fields in the section 711 | for(const auto &secPair : filePair.second) 712 | { 713 | writeEscaped(os, secPair.first); 714 | os.put(fieldSep_); 715 | writeEscaped(os, secPair.second.template as()); 716 | os.put('\n'); 717 | } 718 | } 719 | } 720 | 721 | /** Encodes this inifile object as string and returns the result. 722 | * @return encoded infile string. */ 723 | std::string encode() const 724 | { 725 | std::ostringstream ss; 726 | encode(ss); 727 | return ss.str(); 728 | } 729 | 730 | /** Saves this inifile object to the file at the given path. 731 | * @param fileName path to the file where the data should be stored. */ 732 | void save(const std::string &fileName) const 733 | { 734 | std::ofstream os(fileName.c_str()); 735 | encode(os); 736 | } 737 | }; 738 | 739 | using IniFile = IniFileBase>; 740 | using IniSection = IniSectionBase>; 741 | using IniFileCaseInsensitive = IniFileBase; 742 | using IniSectionCaseInsensitive = IniSectionBase; 743 | } 744 | 745 | #endif 746 | --------------------------------------------------------------------------------