├── .gitignore ├── .github ├── utils │ └── init_files.sh └── workflows │ ├── update_man.yml │ ├── run_tests.yml │ └── build.yml ├── src ├── utils.h ├── utils.c ├── set.h ├── mmv.h ├── set.c └── mmv.c ├── man ├── mmv.1.md └── mmv.1.gz ├── .clang-format ├── LICENSE ├── test ├── test_utils.c ├── test_set.c └── test_mmv.c ├── README.md ├── Makefile ├── main.c └── docs └── spec.txt /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/utils/init_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # cheatsheet: https://devhints.io/bash 3 | 4 | main() 5 | { 6 | dir="$1" 7 | num_files=$(test -n "$2" && echo "$2" || echo "10") 8 | 9 | mkdir -p "$dir" 10 | 11 | i=1 12 | while [ "$i" -le "$num_files" ]; do 13 | fname="$dir/test$i.txt" 14 | touch "$fname" 15 | echo "$fname" > "$fname" 16 | 17 | i=$(( i + 1 )) 18 | done 19 | } 20 | 21 | main "$@" 22 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | /** 10 | * @brief move the given source string into the given hash map position 11 | * 12 | * @param array_pos: position in hash map to populate 13 | * @param src_str: str to copy into hash map 14 | */ 15 | char *cpy_str_to_arr(char **array_pos, const char *src_str); 16 | 17 | char *strccat(char **str_arr, unsigned int num_strs); 18 | 19 | #endif // UTILS_H 20 | -------------------------------------------------------------------------------- /.github/workflows/update_man.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | paths: 5 | - "man/mmv.1.md" 6 | pull_request: 7 | branches: 8 | - "main" 9 | paths: 10 | - "man/mmv.1.md" 11 | 12 | jobs: 13 | update_man_page: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Convert Markdown to man page 19 | uses: docker://pandoc/core:3.1 20 | with: 21 | args: "man/mmv.1.md -s -t man -o man/mmv.1.gz" 22 | 23 | - name: Push changes 24 | uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | commit_message: "chore(build): auto-generate man page" 27 | commit_user_name: "github-actions[bot]" 28 | commit_user_email: "github-actions[bot]@users.noreply.github.com" 29 | commit_author: "github-actions[bot] " 30 | -------------------------------------------------------------------------------- /man/mmv.1.md: -------------------------------------------------------------------------------- 1 | % MMV(1) mmv 0.3.2 2 | % Written by Jacob McAuley Penney 3 | % December 2023 4 | 5 | 6 | # NAME 7 | mmv - Rename or move files and directories in $EDITOR 8 | 9 | 10 | # SYNOPSIS 11 | **mmv** [OPTION]... SOURCE(s) 12 | 13 | 14 | # DESCRIPTION 15 | mmv allows the user to open files and directory paths in their $EDITOR program, modify those file paths, and, in doing so, edit the actual path of the original file system item. 16 | 17 | 18 | **-v**, **\--verbose** 19 | : explain what is being done 20 | 21 | **-h**, **\--help** display this help and exit 22 | 23 | **-V**, **\--version** output version information and exit 24 | 25 | 26 | # REPORTING BUGS 27 | Please report any issues at 28 | 29 | 30 | # COPYRIGHT 31 | Copyright © Jacob Penney. License: MIT. 32 | 33 | 34 | # SEE ALSO 35 | **mv(1)** 36 | 37 | Full documentation at 38 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | paths: 6 | - ".github/workflows/*" 7 | - "Makefile" 8 | - "src/**" 9 | - "main.c" 10 | - "test/**" 11 | 12 | pull_request: 13 | branches: 14 | - "main" 15 | paths: 16 | - ".github/workflows/*" 17 | - "Makefile" 18 | - "src/**" 19 | - "main.c" 20 | - "test/**" 21 | 22 | jobs: 23 | run_unit_tests: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Clone mmv 27 | uses: actions/checkout@v3 28 | 29 | - name: Clone test framework 30 | uses: actions/checkout@v3 31 | with: 32 | repository: ThrowTheSwitch/Unity 33 | path: Unity 34 | 35 | - name: Set up directory 36 | run: mv ./Unity/src/* test 37 | 38 | - name: Build test executables 39 | run: make test 40 | 41 | - name: Clean up test executable 42 | run: make test_clean 43 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include "./utils.h" 2 | 3 | char *cpy_str_to_arr(char **arr_dest, const char *src_str) 4 | { 5 | *arr_dest = malloc((strlen(src_str) + 1) * sizeof(char)); 6 | if (arr_dest == NULL) 7 | { 8 | perror("mmv: failed to allocate memory for new map str\n"); 9 | return NULL; 10 | } 11 | 12 | return strcpy(*arr_dest, src_str); 13 | } 14 | 15 | char *strccat(char **str_arr, unsigned int num_strs) 16 | { 17 | if (num_strs < 1) 18 | return NULL; 19 | 20 | unsigned int i; 21 | size_t size = 4200 * sizeof(char); 22 | char *concat_str = malloc(size); 23 | 24 | char *p = memccpy(concat_str, str_arr[0], '\0', size - 1); 25 | 26 | for (i = 1; i < num_strs; i++) 27 | if (p) 28 | p = memccpy(p - 1, str_arr[i], '\0', size - (size_t)p); 29 | else 30 | { 31 | free(concat_str); 32 | return NULL; 33 | } 34 | 35 | return concat_str; 36 | } 37 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Microsoft 3 | AlignAfterOpenBracket: BlockIndent 4 | AlignConsecutiveMacros: 'true' 5 | AlignConsecutiveAssignments: 'true' 6 | AlignConsecutiveDeclarations: 'false' 7 | AlignEscapedNewlines: Left 8 | AlignOperands: 'true' 9 | AlignTrailingComments: 'true' 10 | AllowAllArgumentsOnNextLine: 'true' 11 | AllowAllConstructorInitializersOnNextLine: 'true' 12 | AllowAllParametersOfDeclarationOnNextLine: 'true' 13 | AllowShortBlocksOnASingleLine: 'false' 14 | AllowShortCaseLabelsOnASingleLine: 'true' 15 | AllowShortFunctionsOnASingleLine: None 16 | AllowShortIfStatementsOnASingleLine: Never 17 | AllowShortLoopsOnASingleLine: 'false' 18 | BinPackArguments: 'false' 19 | BinPackParameters: 'false' 20 | BreakBeforeBraces: Allman 21 | ColumnLimit: '120' 22 | IndentCaseLabels: 'true' 23 | IndentPPDirectives: BeforeHash 24 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 25 | MaxEmptyLinesToKeep: '2' 26 | TabWidth: '4' 27 | UseTab: Never 28 | 29 | ... 30 | 31 | # docs: https://clang.llvm.org/docs/ClangFormatStyleOptions.html 32 | # configurator: https://zed0.co.uk/clang-format-configurator/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Jacob M. Penney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/set.h: -------------------------------------------------------------------------------- 1 | #ifndef SET_H 2 | #define SET_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "./utils.h" 12 | 13 | 14 | #define MAX_OPS 50000 15 | 16 | typedef u_int32_t Fnv32_t; 17 | 18 | struct Set 19 | { 20 | char **map; 21 | unsigned long int map_capacity; 22 | unsigned int num_keys; 23 | int keys[MAX_OPS + 1]; 24 | }; 25 | 26 | 27 | /** 28 | * @brief Create a set of strings from argv 29 | * 30 | * @param argv 31 | * @param argc 32 | * @param ***map: pointer to a string array 33 | * @param **key: pointer to MapKeyArr struct 34 | */ 35 | struct Set *set_init(bool resolve_paths, const int arg_count, char *args[], bool track_dupes); 36 | 37 | /** 38 | * @brief completely frees a Map struct 39 | * 40 | * @param map: Map struct to free the nodes of 41 | */ 42 | void set_destroy(struct Set *set); 43 | 44 | /* TODO: */ 45 | int is_duplicate_element(char *cur_str, struct Set *set, long unsigned int *hash); 46 | 47 | int *set_begin(struct Set *set); 48 | 49 | int *set_next(int *iter); 50 | 51 | int *set_end(struct Set *set); 52 | 53 | char **get_set_pos(const struct Set *set, const int *iter); 54 | 55 | int is_valid_key(const int *iter); 56 | 57 | int set_key(int *iter, int new_key); 58 | 59 | #endif // SET_H 60 | -------------------------------------------------------------------------------- /man/mmv.1.gz: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 3.1.1 2 | .\" 3 | .\" Define V font for inline verbatim, using C font in formats 4 | .\" that render this, and otherwise B font. 5 | .ie "\f[CB]x\f[]"x" \{\ 6 | . ftr V B 7 | . ftr VI BI 8 | . ftr VB B 9 | . ftr VBI BI 10 | .\} 11 | .el \{\ 12 | . ftr V CR 13 | . ftr VI CI 14 | . ftr VB CB 15 | . ftr VBI CBI 16 | .\} 17 | .TH "MMV" "1" "December 2023" "mmv 0.3.2" "" 18 | .hy 19 | .SH NAME 20 | .PP 21 | mmv - Rename or move files and directories in $EDITOR 22 | .SH SYNOPSIS 23 | .PP 24 | \f[B]mmv\f[R] [OPTION]\&... 25 | SOURCE(s) 26 | .SH DESCRIPTION 27 | .PP 28 | mmv allows the user to open files and directory paths in their $EDITOR 29 | program, modify those file paths, and, in doing so, edit the actual path 30 | of the original file system item. 31 | .TP 32 | \f[B]-v\f[R], \f[B]--verbose\f[R] 33 | explain what is being done 34 | .PP 35 | \f[B]-h\f[R], \f[B]--help\f[R] display this help and exit 36 | .PP 37 | \f[B]-V\f[R], \f[B]--version\f[R] output version information and exit 38 | .SH REPORTING BUGS 39 | .PP 40 | Please report any issues at 41 | .SH COPYRIGHT 42 | .PP 43 | Copyright © Jacob Penney. 44 | License: MIT. 45 | .SH SEE ALSO 46 | .PP 47 | \f[B]mv(1)\f[R] 48 | .PP 49 | Full documentation at 50 | .SH AUTHORS 51 | Written by Jacob McAuley Penney. 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: compile_and_memcheck 2 | 3 | on: 4 | push: 5 | paths: 6 | - ".github/workflows/*" 7 | - "Makefile" 8 | - "src/**" 9 | - "main.c" 10 | 11 | pull_request: 12 | branches: 13 | - "main" 14 | paths: 15 | - ".github/workflows/*" 16 | - "Makefile" 17 | - "src/**" 18 | - "main.c" 19 | 20 | jobs: 21 | build_executable: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Build primary executable 27 | run: make 28 | 29 | - name: Exercise install 30 | run: sudo make install 31 | 32 | - name: Exercise uninstall 33 | run: sudo make uninstall 34 | 35 | - name: Clean up primary build 36 | run: make clean 37 | 38 | - name: Build debug executable 39 | run: make debug 40 | 41 | - name: Clean up debug build 42 | run: make debug_clean 43 | 44 | run_valgrind: 45 | needs: build_executable 46 | runs-on: ubuntu-latest 47 | env: 48 | test_files_dir: test_dir 49 | utils: .github/utils 50 | steps: 51 | - uses: actions/checkout@v3 52 | 53 | - name: Update packages 54 | run: sudo apt update 55 | 56 | - name: Install dependencies 57 | run: sudo apt-get -y install valgrind 58 | 59 | - name: Create test files 60 | run: sh "$utils"/init_files.sh "$test_files_dir" 10 61 | 62 | - name: Build debug executable 63 | run: make debug 64 | 65 | - name: Execute Valgrind 66 | run: valgrind --leak-check=full --track-origins=yes --show-leak-kinds=all -s ./debug_mmv "$test_files_dir"/test* 67 | -------------------------------------------------------------------------------- /test/test_utils.c: -------------------------------------------------------------------------------- 1 | #include "../src/utils.c" 2 | #include "unity.h" 3 | 4 | void setUp(void) 5 | { 6 | } 7 | 8 | void tearDown(void) 9 | { 10 | } 11 | 12 | void test_cpy_str_to_arr(void) 13 | { 14 | char *test_arr[] = {NULL}; 15 | char *test_str = "TEST STRING\0"; 16 | cpy_str_to_arr(&test_arr[0], test_str); 17 | TEST_ASSERT_EQUAL_STRING("TEST STRING", test_arr[0]); 18 | 19 | free(test_arr[0]); 20 | test_str = "\0"; 21 | cpy_str_to_arr(&test_arr[0], test_str); 22 | TEST_ASSERT_EQUAL_STRING("", test_arr[0]); 23 | } 24 | 25 | void test_strccat_no_args(void) 26 | { 27 | char *parts[7] = {"This", " ", "is", " ", "a", " ", "test"}; 28 | 29 | char *test_str = strccat(parts, 0); 30 | 31 | TEST_ASSERT_NULL(test_str); 32 | } 33 | 34 | void test_strccat_incomplete_args(void) 35 | { 36 | char *parts[7] = {"This", " ", "is", " ", "a", " ", "test"}; 37 | 38 | char *test_str = strccat(parts, 3); 39 | 40 | TEST_ASSERT_EQUAL_STRING("This is", test_str); 41 | } 42 | 43 | void test_strccat_excess_args(void) 44 | { 45 | char *parts[7] = {"This", " ", "is", " ", "a", " ", "test"}; 46 | 47 | char *test_str = strccat(parts, 8); 48 | 49 | TEST_ASSERT_EQUAL_STRING("This is a test", test_str); 50 | } 51 | 52 | void test_strccat_successful(void) 53 | { 54 | char *parts[7] = {"This", " ", "is", " ", "a", " ", "test"}; 55 | 56 | char *test_str = strccat(parts, 7); 57 | 58 | TEST_ASSERT_EQUAL_STRING("This is a test", test_str); 59 | } 60 | 61 | int main(void) 62 | { 63 | UNITY_BEGIN(); 64 | 65 | RUN_TEST(test_cpy_str_to_arr); 66 | RUN_TEST(test_strccat_no_args); 67 | RUN_TEST(test_strccat_incomplete_args); 68 | RUN_TEST(test_strccat_successful); 69 | 70 | return UNITY_END(); 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmv-c 📦 2 | 3 | ![Build](https://github.com/mcauley-penney/mmv-c/actions/workflows/build.yml/badge.svg) 4 | 5 | Edit file and directory names in `$EDITOR`. Inspired by [itchyny/mmv](https://github.com/itchyny/mmv). 6 | 7 | ## usage 8 | 9 | mmv behaves like other commandline tools: it accepts a list of arguments, including patterns like wildcards. See `man mmv` for options. 10 | 11 | ## example 12 | 13 | ![mmv](https://github.com/mcauley-penney/mmv-c/assets/59481467/ecf97305-7847-4878-9ee7-5a86a287634e) 14 | 15 | In the above example, mmv is provided the `verbose` argument so that it will list what renames it conducts and three wildcard arguments that are duplicates of each other: 16 | 1. the set of files in the current directory that begin with `test` 17 | 2. the same set of files as before 18 | 3. the same set of files again, except using an alternate path string 19 | 20 | mmv is capable of removing duplicate arguments even when the input strings don't match, for example `test0.txt` and `~/test_dir/test0.txt`. In the editing buffer, we see only one instance of each unique file, though three were given for each. When cycles between renames are detected, for example renaming `test0.txt` to `test1.txt` even though that destination already exists, mmv will remove the cycles by conducting intermediate renames on only those files which are detected as cycles, i.e. it avoids renaming everything when a cycle is detected. These intermediate rename operations are visible in the verbose output. cat is used here to display that the contents of the files remains the same. 21 | 22 | ## installation 23 | 24 | 1. Clone this repository and enter the repo directory 25 | 2. Issue `make`, then `sudo make install` 26 | 3. Feel free to remove the cloned repo 27 | 28 | In all: 29 | 30 | ``` 31 | $ git clone https://github.com/mcauley-penney/mmv-c.git 32 | $ cd mmv-c 33 | $ make 34 | $ sudo make install 35 | $ cd .. 36 | $ sudo rm -r mmv-c 37 | ``` 38 | 39 | ## credit 40 | 41 | [itchyny/mmv](https://github.com/itchyny/mmv) 42 | 43 | [Glenn Fowler, Landon Curt Noll, and Kiem-Phong Vo](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) 44 | 45 | [Mike Parker, David MacKenzie, Jim Meyering, and all of the contributors to coreutils/mv](https://github.com/coreutils/coreutils/blob/master/src/mv.c) 46 | -------------------------------------------------------------------------------- /src/mmv.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Title : mmv-c 3 | * Description : interactively move files and directories 4 | * Author : Jacob M. Penney 5 | */ 6 | 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "./set.h" 16 | #include "./utils.h" 17 | 18 | 19 | #define PROG_NAME "mmv" 20 | #define PROG_VERSION "version 0.3.2" 21 | #define PROG_REMOTE "https://github.com/mcauley-penney/mmv-c" 22 | 23 | struct Opts 24 | { 25 | bool resolve_paths; 26 | bool verbose; 27 | }; 28 | 29 | struct Set *init_src_set(const int arg_count, char *args[], struct Opts *options); 30 | 31 | /** 32 | * @brief opens temp file at path, writes source strings (old names) 33 | * to it, and closes said temp file 34 | * 35 | * @param path: path to open and write to 36 | * @param map: map of source strings to write 37 | * @param keys: struct containing list of keys to node locations in 38 | * hashmap 39 | */ 40 | int write_strarr_to_tmpfile(struct Set *set, char tmp_path_template[]); 41 | 42 | /** 43 | * @brief gets the user's $EDITOR env variable and opens temp file with it 44 | * 45 | * @param path: file path to open in editor 46 | */ 47 | int edit_tmpfile(char *path); 48 | 49 | struct Set *init_dest_set(unsigned int num_keys, char path[]); 50 | 51 | 52 | /** 53 | * TOOD: 54 | * @brief reads lines out of a file path and renames item at corresponding 55 | * position in hashmap to newly-read line. 56 | * 57 | * For example, the first item in the hashmap will be renamed to the first 58 | * item read from the file. 59 | * 60 | * @param options: struct of user-defined flags 61 | * @param set: set of strings to rename 62 | * @param path: path to temp file containing new names 63 | * @return errno or 0 for success 64 | */ 65 | int read_tmpfile_strs(char **dest_arr, int *dest_size, unsigned int num_keys, char path[]); 66 | 67 | 68 | void free_strarr(char **arr, int arr_size); 69 | 70 | int rename_paths(struct Set *src_set, struct Set *dest_set, struct Opts *options); 71 | 72 | /** 73 | * @brief renames an item in file system 74 | * 75 | * @param options: struct of user-defined flags 76 | * @param src: old name 77 | * @param dest: new name 78 | */ 79 | void rename_path(const char *src, const char *dest, struct Opts *options); 80 | 81 | int rm_unedited_pairs(struct Set *src_set, struct Set *dest_set, struct Opts *opts); 82 | 83 | int rm_cycles(struct Set *src_set, struct Set *dest_set, struct Opts *options); 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Make manual: https://www.gnu.org/software/make/manual/make.html 2 | # GCC Options: https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html 3 | 4 | # mmv 5 | 6 | # ------------------------------------------------------------------- 7 | # files and directories 8 | # ------------------------------------------------------------------- 9 | bin_name = mmv 10 | man_name = $(bin_name).1.gz 11 | 12 | prefix = /usr/local 13 | bin_dir = $(prefix)/bin 14 | man_dir = $(prefix)/man/man1 15 | 16 | src_dir = src 17 | test_dir = test 18 | debug_dir = debug 19 | build_dir = build 20 | 21 | src_files := $(wildcard $(src_dir)/*) 22 | def_files := $(wildcard $(src_dir)/*.c) 23 | test_def_files := $(patsubst $(src_dir)/%.c, $(test_dir)/test_%.c, $(def_files)) 24 | 25 | 26 | # ------------------------------------------------------------------- 27 | # flags 28 | # ------------------------------------------------------------------- 29 | optim_flags = -O2 30 | w-arith = -Wdouble-promotion -Wfloat-equal 31 | w-basic = -pedantic -Wall -Wextra 32 | w-extra = -Wcast-align=strict -Wconversion -Wpadded -Wshadow -Wstrict-prototypes -Wvla 33 | w-fmt = -Wformat=2 -Wformat-overflow=2 -Wformat-truncation 34 | warn_flags = $(w-basic) $(w-extra) $(w-arith) $(w-fmt) 35 | 36 | CFLAGS = $(warn_flags) $(optim_flags) 37 | 38 | 39 | # ------------------------------------------------------------------- 40 | # targets 41 | # ------------------------------------------------------------------- 42 | .PHONY: all test debug clean test_clean install uninstall 43 | 44 | 45 | all: 46 | $(CC) $(CFLAGS) main.c $(src_files) -o $(bin_name) 47 | 48 | 49 | test: 50 | mkdir -p ./test/bin 51 | 52 | $(CC) ./test/test_utils.c ./test/unity.c -o ./test/bin/test_utils 53 | $(CC) ./test/test_set.c ./test/unity.c -o ./test/bin/test_set 54 | $(CC) -D DEBUG=1 ./test/test_mmv.c ./test/unity.c -o ./test/bin/test_mmv 55 | 56 | ./test/bin/test_utils 57 | ./test/bin/test_set 58 | ./test/bin/test_mmv 59 | 60 | 61 | debug: 62 | $(CC) $(CFLAGS) -D DEBUG=1 main.c $(src_files) -o debug_$(bin_name) 63 | 64 | 65 | # clean target: remove all object files and binary 66 | clean: 67 | rm $(bin_name) 68 | 69 | 70 | test_clean: 71 | rm -rf ./test/bin 72 | 73 | 74 | debug_clean: 75 | rm debug_$(bin_name) 76 | 77 | 78 | # install target for "sudo make install" 79 | install: 80 | $(NORMAL_INSTALL) 81 | install -m 007 $(bin_name) $(bin_dir) 82 | cp ./man/$(man_name) $(man_dir)/$(man_name) 83 | 84 | 85 | uninstall: 86 | $(NORMAL_UNINSTALL) 87 | rm $(bin_dir)/$(bin_name) 88 | rm $(man_dir)/$(man_name) 89 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "src/mmv.h" 4 | 5 | /** 6 | * @brief Create struct of possible user flags 7 | * @return Opt struct 8 | */ 9 | static struct Opts *make_opts(void) 10 | { 11 | struct Opts *opts = malloc(sizeof(struct Opts)); 12 | if (opts == NULL) 13 | { 14 | perror("mmv: failed to allocate memory for user flags\n"); 15 | return NULL; 16 | } 17 | 18 | opts->resolve_paths = false; 19 | opts->verbose = false; 20 | 21 | return opts; 22 | } 23 | 24 | /** 25 | * @brief Print program usage information 26 | */ 27 | static void usage(void) 28 | { 29 | printf("Usage: %s [OPTION] SOURCES\n\n", PROG_NAME); 30 | puts("Rename or move SOURCE(s) by editing them in $EDITOR."); 31 | printf("For full documentation, see man %s\n", PROG_NAME); 32 | } 33 | 34 | int main(int argc, char *argv[]) 35 | { 36 | int cur_flag; 37 | char short_opts[] = "rhvV"; 38 | 39 | struct Opts *options = make_opts(); 40 | if (options == NULL) 41 | return EXIT_FAILURE; 42 | 43 | while ((cur_flag = getopt(argc, argv, short_opts)) != -1) 44 | { 45 | switch (cur_flag) 46 | { 47 | case 'r': options->resolve_paths = true; break; 48 | case 'v': options->verbose = true; break; 49 | 50 | case 'h': 51 | free(options); 52 | usage(); 53 | return EXIT_SUCCESS; 54 | 55 | case 'V': 56 | free(options); 57 | puts(PROG_VERSION); 58 | return EXIT_SUCCESS; 59 | 60 | default: 61 | free(options); 62 | puts("Try 'mmv -h'for more information"); 63 | return EXIT_FAILURE; 64 | } 65 | } 66 | 67 | argv += optind; 68 | argc -= optind; 69 | 70 | struct Set *src_set = init_src_set(argc, argv, options); 71 | if (src_set == NULL) 72 | goto free_opts_out; 73 | 74 | char tmpfile[] = "/tmp/mmv_editbuf_XXXXXX"; 75 | 76 | if (write_strarr_to_tmpfile(src_set, tmpfile) != 0) 77 | goto free_src_out; 78 | 79 | if (edit_tmpfile(tmpfile) != 0) 80 | goto rm_path_out; 81 | 82 | struct Set *dest_set = init_dest_set(src_set->num_keys, tmpfile); 83 | if (dest_set == NULL) 84 | goto rm_path_out; 85 | 86 | if (rm_unedited_pairs(src_set, dest_set, options) != 0) 87 | goto free_dest_out; 88 | 89 | if (argc > 1 && rm_cycles(src_set, dest_set, options) != 0) 90 | goto free_dest_out; 91 | 92 | rename_paths(src_set, dest_set, options); 93 | 94 | set_destroy(dest_set); 95 | remove(tmpfile); 96 | set_destroy(src_set); 97 | free(options); 98 | 99 | return EXIT_SUCCESS; 100 | 101 | 102 | free_dest_out: 103 | set_destroy(dest_set); 104 | 105 | rm_path_out: 106 | remove(tmpfile); 107 | 108 | free_src_out: 109 | set_destroy(src_set); 110 | 111 | free_opts_out: 112 | free(options); 113 | return EXIT_FAILURE; 114 | } 115 | -------------------------------------------------------------------------------- /docs/spec.txt: -------------------------------------------------------------------------------- 1 | What do we need: 2 | - cyclically rename 3 | - if cycle is detected, where the hash of a val returns a key, e.g. 4 | 5 | file1.txt ──► file2.txt 6 | hash(file2.txt) != NULL 7 | 8 | we must rename file2.txt, then file1.txt 9 | 10 | - deny rename if item exists and there is no cycle 11 | 12 | 13 | What do we need to do: 14 | 15 | Idea 2: Hashmaps 16 | We iterate over argv and, as we do, we produce a hash of the current string and attempt insertion into an array at the hash index. If the position is occupied, we check if the item at that position is the same. If not, we iterate to next index and check again. We do this until we either see that the string is already in the array or we have reached an empty index. If a matching string is found, reject. If not, insert. This produces a map where the hash of the old name is the index and the item at the index is the old name that made the hash index. We then store the hash in an int array, allowing us to have "positions" that we will use to relate the new names to the old names. An example might be: 17 | 18 | cur_str = argv[i] 19 | cur_str_hash = hash(cur_str) 20 | map[cur_str_hash] = cur_str 21 | key_array[i] = cur_str_hash 22 | 23 | This 24 | 1. gets the string at the current position of argv 25 | - This positional relationship must be maintained 26 | 2. hashes the current string 27 | 3. inserts the current string at it's own hash in the string map 28 | 4. inserts the hash (the key to the old string in the map) into an array of int keys at the current position in argv. 29 | - This allows us to iterate over the keys and know which is first, so that we can map the first item returned from the temp buffer to the first item given in argv 30 | 31 | 2. do not remove non-unique items in new names 32 | - allow rename to fail when items would be overwritten 33 | - safe 34 | 35 | 3. resolve cycles 36 | - scenarios: 37 | 0. we want to rename file1.txt to file2.txt and file2.txt to file3.txt 38 | 1. we want to rename file1.txt to file2.txt and vice versa 39 | - using a map, we can check if each new name has a file in the hashmap 40 | 41 | new_name_hash = hash(new_name) 42 | if map[new_name_hash] != NULL 43 | 44 | 45 | 46 | 47 | f1.txt ─► f2.txt f1.txt ─► f2.txt 48 | f2.txt ─► f3.txt f2.txt ─► f3.txt 49 | f3.txt ─► f1.txt 50 | - check to see if file2 in map 51 | - if yes, rename file2 - check to see if f2 in map 52 | - do temp rename 53 | f1.txt ─► f2.txt 54 | tmp.txt ─► f3.txt f1.txt ─► f2.txt 55 | │ tmp.txt ─► f3.txt 56 | ▼ f3.txt ─► f1.txt 57 | f2.txt │ 58 | tmp.txt ─► f3.txt ▼ 59 | │ f2.txt 60 | ▼ tmp.txt ─► f3.txt 61 | f2.txt f3.txt ─► f1.txt 62 | f3.txt 63 | and so on 64 | 65 | 66 | 1. attempt f1.txt ─► f2.txt 67 | 2. rename fails because f2.txt exists 68 | 3. check if it is cycle 69 | - how: 70 | 1. hash f2 and see if it is at index 71 | 2. if not, iterate until NULL/empty space or end of array (or linked list for chaining) 72 | 4. it exists in array: 73 | - rename to new temp name 74 | - hash temp name 75 | - insert {temp name hash: temp name} into map 76 | - overwrite old position in key array with new key 77 | -------------------------------------------------------------------------------- /src/set.c: -------------------------------------------------------------------------------- 1 | #include "./set.h" 2 | 3 | /** 4 | * @brief Allocate memory for members of a Set struct 5 | * 6 | * @param num_args 7 | * @param map_size 8 | */ 9 | static struct Set *set_alloc(const unsigned long int map_capacity) 10 | { 11 | unsigned int i; 12 | 13 | struct Set *set = malloc(sizeof(struct Set)); 14 | if (set == NULL) 15 | return NULL; 16 | 17 | set->map = malloc(sizeof(char *) * map_capacity); 18 | if (set->map == NULL) 19 | { 20 | free(set); 21 | return NULL; 22 | } 23 | 24 | set->num_keys = 0; 25 | 26 | for (i = 0; i < map_capacity; i++) 27 | set->map[i] = NULL; 28 | 29 | set->map_capacity = map_capacity; 30 | 31 | return set; 32 | } 33 | 34 | /** 35 | * @brief hashes a string with the Fowler–Noll–Vo 1a 32bit hash fn 36 | * 37 | * @param str: string to hash 38 | * @param map_size: size of map; used to modulo the hash to fit into array 39 | * 40 | * @return hash for input string % map_size 41 | */ 42 | static long unsigned int hash_str(char *str, const unsigned long int map_capacity) 43 | { 44 | unsigned char *s = (unsigned char *)str; 45 | Fnv32_t hval = ((Fnv32_t)0x811c9dc5); 46 | 47 | while (*s) 48 | { 49 | hval ^= (Fnv32_t)*s++; 50 | hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); 51 | } 52 | 53 | return (long unsigned int)(hval % map_capacity); 54 | } 55 | 56 | /** 57 | * @brief Insert a string into the given Set struct 58 | * 59 | * @param cur_str 60 | * @param map_size 61 | * @param map 62 | * @return 63 | */ 64 | static int set_insert(char *cur_str, struct Set *set, bool track_dupes) 65 | { 66 | long unsigned int hash = hash_str(cur_str, set->map_capacity); 67 | int is_dupe = is_duplicate_element(cur_str, set, &hash); 68 | 69 | if (is_dupe == 0) 70 | { 71 | if (track_dupes) 72 | { 73 | set->keys[set->num_keys] = -1; 74 | set->num_keys++; 75 | } 76 | 77 | return 0; 78 | } 79 | 80 | if (cpy_str_to_arr(&set->map[hash], cur_str) == NULL) 81 | return -1; 82 | 83 | set->keys[set->num_keys] = (int)hash; 84 | set->num_keys++; 85 | 86 | return 1; 87 | } 88 | 89 | int is_duplicate_element(char *cur_str, struct Set *set, long unsigned int *hash) 90 | { 91 | int dupe_found = -1; 92 | 93 | while (set->map[*hash] != NULL && dupe_found != 0) 94 | { 95 | // strcmp returns 0 if strings are identical 96 | dupe_found = strcmp(set->map[*hash], cur_str); 97 | 98 | if (dupe_found != 0) 99 | *hash = (*hash + 1 < set->map_capacity) ? *hash + 1 : 0; 100 | } 101 | 102 | return dupe_found; 103 | } 104 | 105 | struct Set *set_init(bool resolve_paths, const int arg_count, char *args[], bool track_dupes) 106 | { 107 | if (arg_count > MAX_OPS) 108 | { 109 | fprintf(stderr, "mmv: too many operands, use up to %u\n", MAX_OPS); 110 | return NULL; 111 | } 112 | else if (arg_count == 0) 113 | { 114 | fputs("mmv: missing file operand(s)\n", stderr); 115 | return NULL; 116 | } 117 | 118 | unsigned int i; 119 | const unsigned int u_arg_count = (unsigned int)arg_count; 120 | const unsigned long int map_capacity = (6 * (u_arg_count - 1)) + 1; 121 | 122 | struct Set *set = set_alloc(map_capacity); 123 | if (set == NULL) 124 | { 125 | perror("mmv: failed to allocate memory to initialize string set"); 126 | return NULL; 127 | } 128 | 129 | char *cur_str; 130 | 131 | for (i = 0; i < u_arg_count; i++) 132 | { 133 | cur_str = args[i]; 134 | if (resolve_paths) 135 | cur_str = realpath(cur_str, NULL); 136 | 137 | if (set_insert(cur_str, set, track_dupes) == -1) 138 | { 139 | set_destroy(set); 140 | fprintf(stderr, "mmv: failed to insert \'%s\': %s\n", cur_str, strerror(errno)); 141 | return NULL; 142 | } 143 | } 144 | 145 | return set; 146 | } 147 | 148 | void set_destroy(struct Set *set) 149 | { 150 | unsigned int i; 151 | int key; 152 | 153 | for (i = 0; i < set->num_keys; i++) 154 | { 155 | key = set->keys[i]; 156 | 157 | if (key != -1) 158 | free(set->map[key]); 159 | } 160 | 161 | free(set->map); 162 | free(set); 163 | } 164 | 165 | int *set_begin(struct Set *set) 166 | { 167 | return &set->keys[0]; 168 | } 169 | 170 | int *set_next(int *iter) 171 | { 172 | return ++iter; 173 | } 174 | 175 | int *set_end(struct Set *set) 176 | { 177 | return &set->keys[set->num_keys]; 178 | } 179 | 180 | char **get_set_pos(const struct Set *set, const int *iter) 181 | { 182 | return &(set->map[*iter]); 183 | } 184 | 185 | int is_valid_key(const int *iter) 186 | { 187 | return *iter != -1; 188 | } 189 | 190 | int set_key(int *iter, int new_key) 191 | { 192 | return *iter = new_key; 193 | } 194 | -------------------------------------------------------------------------------- /test/test_set.c: -------------------------------------------------------------------------------- 1 | #include "../src/set.c" 2 | #include "../src/utils.c" 3 | #include "unity.h" 4 | #include 5 | 6 | void setUp(void) 7 | { 8 | } 9 | 10 | void tearDown(void) 11 | { 12 | } 13 | 14 | /* tied to implementation details */ 15 | void test_set_alloc(void) 16 | { 17 | struct Set *test_set = set_alloc(10); 18 | TEST_ASSERT_EQUAL_UINT(10, test_set->map_capacity); 19 | set_destroy(test_set); 20 | 21 | test_set = set_alloc(0); 22 | TEST_ASSERT_EQUAL_UINT(0, test_set->map_capacity); 23 | set_destroy(test_set); 24 | } 25 | 26 | void test_set_begin(void) 27 | { 28 | int *i; 29 | char *cur_str, *test_str = "TEST STRING\0"; 30 | 31 | struct Set *test_set = set_alloc(10); 32 | 33 | set_insert(test_str, test_set, true); 34 | i = set_begin(test_set); 35 | 36 | cur_str = *get_set_pos(test_set, i); 37 | TEST_ASSERT_EQUAL_STRING(test_str, cur_str); 38 | } 39 | 40 | void test_set_next(void) 41 | { 42 | int *i; 43 | char *cur_str, *test_str1 = "TEST STRING1\0", *test_str2 = "TEST STRING2\0"; 44 | 45 | struct Set *test_set = set_alloc(10); 46 | 47 | set_insert(test_str1, test_set, true); 48 | set_insert(test_str2, test_set, true); 49 | i = set_begin(test_set); 50 | i = set_next(i); 51 | 52 | cur_str = *get_set_pos(test_set, i); 53 | TEST_ASSERT_EQUAL_STRING(test_str2, cur_str); 54 | } 55 | 56 | void test_set_end(void) 57 | { 58 | int *i; 59 | char *cur_str, *test_str1 = "TEST STRING1\0", *test_str2 = "TEST STRING2\0"; 60 | struct Set *test_set = set_alloc(10); 61 | 62 | set_insert(test_str1, test_set, true); 63 | set_insert(test_str2, test_set, true); 64 | 65 | for (i = set_begin(test_set); i < set_end(test_set) - 1; i = set_next(i)) 66 | ; 67 | 68 | cur_str = *get_set_pos(test_set, i); 69 | TEST_ASSERT_EQUAL_STRING(test_str2, cur_str); 70 | } 71 | 72 | void test_set_insert_uniq_str(void) 73 | { 74 | char *cur_str, *test_str = "TEST STRING\0"; 75 | 76 | struct Set *test_set = set_alloc(1); 77 | 78 | int insert_outcome = set_insert(test_str, test_set, true); 79 | TEST_ASSERT_EQUAL_INT(1, insert_outcome); 80 | set_destroy(test_set); 81 | } 82 | 83 | void test_set_insert_dup_str(void) 84 | { 85 | char *cur_str, *test_str = "TEST STRING\0"; 86 | 87 | struct Set *test_set = set_alloc(2); 88 | 89 | int insert_outcome = set_insert(test_str, test_set, true); 90 | TEST_ASSERT_EQUAL_INT(1, insert_outcome); 91 | 92 | insert_outcome = set_insert(test_str, test_set, true); 93 | TEST_ASSERT_EQUAL_INT(0, insert_outcome); 94 | 95 | set_destroy(test_set); 96 | } 97 | 98 | void test_set_init_empty(void) 99 | { 100 | // case: NULL because no arguments 101 | int argc = 0; 102 | char *empty_argv[] = {NULL}; 103 | 104 | struct Set *test_set = set_init(false, argc, empty_argv, false); 105 | TEST_ASSERT_NULL(test_set); 106 | } 107 | 108 | void test_set_init_max(void) 109 | { 110 | // case: NULL because too many arguments 111 | int argc = MAX_OPS + 1; 112 | char *empty_argv[] = {NULL}; 113 | 114 | struct Set *test_set = set_init(false, argc, empty_argv, false); 115 | TEST_ASSERT_NULL(test_set); 116 | } 117 | 118 | void test_set_init_onedupe_notrack(void) 119 | { 120 | // case: two keys, one removed duplicate, no sentinal value 121 | int *i, argc = 2; 122 | char *cur_str, *dupe_argv[] = {"TEST STRING", "TEST STRING"}; 123 | 124 | struct Set *test_set = set_init(false, argc, dupe_argv, false); 125 | 126 | // make sure first string is present 127 | i = set_begin(test_set); 128 | cur_str = *get_set_pos(test_set, i); 129 | TEST_ASSERT_EQUAL_STRING("TEST STRING", cur_str); 130 | 131 | // make sure second string has been removed 132 | i = set_next(i); 133 | cur_str = *get_set_pos(test_set, i); 134 | TEST_ASSERT_NULL(cur_str); 135 | set_destroy(test_set); 136 | } 137 | 138 | void test_set_init_onedupe_track(void) 139 | { 140 | // case: two keys, one removed duplicate, sentinal value 141 | int *i, argc = 2; 142 | char *cur_str, *dupe_argv[] = {"TEST STRING", "TEST STRING"}; 143 | 144 | struct Set *test_set = set_init(false, argc, dupe_argv, false); 145 | 146 | // make sure second string's key is sentinal value using predicate 147 | test_set = set_init(false, argc, dupe_argv, true); 148 | i = set_begin(test_set); 149 | i = set_next(i); 150 | TEST_ASSERT_FALSE(is_valid_key(i)); 151 | set_destroy(test_set); 152 | } 153 | 154 | int main(void) 155 | { 156 | UNITY_BEGIN(); 157 | 158 | // Set private helper functions 159 | RUN_TEST(test_set_alloc); 160 | RUN_TEST(test_set_insert_uniq_str); 161 | RUN_TEST(test_set_insert_dup_str); 162 | 163 | // Set iteration 164 | RUN_TEST(test_set_begin); 165 | RUN_TEST(test_set_next); 166 | RUN_TEST(test_set_end); 167 | 168 | // Set initialization 169 | RUN_TEST(test_set_init_empty); 170 | RUN_TEST(test_set_init_max); 171 | RUN_TEST(test_set_init_onedupe_notrack); 172 | RUN_TEST(test_set_init_onedupe_track); 173 | 174 | return UNITY_END(); 175 | } 176 | -------------------------------------------------------------------------------- /test/test_mmv.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../src/mmv.c" 4 | #include "../src/set.c" 5 | #include "../src/utils.c" 6 | #include "unity.h" 7 | 8 | #define DEBUG 1 9 | 10 | void setUp(void) 11 | { 12 | } 13 | 14 | void tearDown(void) 15 | { 16 | } 17 | 18 | static struct Opts *make_opts(void) 19 | { 20 | struct Opts *opts = malloc(sizeof(struct Opts)); 21 | if (opts == NULL) 22 | { 23 | perror("mmv: failed to allocate memory for user flags\n"); 24 | return NULL; 25 | } 26 | 27 | opts->resolve_paths = false; 28 | opts->verbose = false; 29 | 30 | return opts; 31 | } 32 | 33 | void test_rename_path(void) 34 | { 35 | struct Opts *options = make_opts(); 36 | 37 | char *src = "MMV_TEST_FILE1", *dest = "MMV_TEST_FILE2"; 38 | FILE *fptr = fopen(src, "w"); 39 | TEST_ASSERT_NOT_NULL(fptr); 40 | 41 | fclose(fptr); 42 | 43 | rename_path(src, dest, options); 44 | 45 | int exists = access(dest, F_OK); 46 | 47 | free(options); 48 | remove(dest); 49 | 50 | TEST_ASSERT_EQUAL_INT(0, exists); 51 | } 52 | 53 | void test_write_strarr_to_tmpfile(void) 54 | { 55 | int argc = 2; 56 | char *empty_argv[] = {"TEST1", "TEST2"}; 57 | char correct_tmp_path[] = "mmv_XXXXXX"; 58 | 59 | struct Set *test_set = set_init(false, argc, empty_argv, false); 60 | 61 | int ret = write_strarr_to_tmpfile(test_set, correct_tmp_path); 62 | remove(correct_tmp_path); 63 | set_destroy(test_set); 64 | TEST_ASSERT(ret == 0); 65 | } 66 | 67 | void test_edit_tmpfile(void) 68 | { 69 | char path[] = "mmv_XXXXXX"; 70 | int tmp_fd = mkstemp(path); 71 | if (tmp_fd == -1) 72 | fprintf(stderr, "mmv: could not create temporary file \'%s\': %s\n", path, strerror(errno)); 73 | 74 | FILE *fptr = fdopen(tmp_fd, "w"); 75 | fclose(fptr); 76 | 77 | int ret = edit_tmpfile(path); 78 | 79 | remove(path); 80 | TEST_ASSERT_EQUAL_INT(0, ret); 81 | } 82 | 83 | void test_read_tmpfile_strs(void) 84 | { 85 | int argc = 2; 86 | char *argv[] = {"TEST1", "TEST2"}; 87 | char tmp_path[] = "mmv_test_XXXXXX"; 88 | char **dest_arr = malloc(sizeof(char *) * argc); 89 | if (dest_arr == NULL) 90 | TEST_FAIL(); 91 | 92 | struct Set *test_set = set_init(false, argc, argv, false); 93 | write_strarr_to_tmpfile(test_set, tmp_path); 94 | 95 | int dest_size = 0; 96 | 97 | if (read_tmpfile_strs(dest_arr, &dest_size, argc, tmp_path) != 0) 98 | { 99 | free_strarr(dest_arr, dest_size); 100 | remove(tmp_path); 101 | set_destroy(test_set); 102 | TEST_FAIL(); 103 | } 104 | 105 | remove(tmp_path); 106 | TEST_ASSERT_EQUAL_STRING_ARRAY(argv, dest_arr, argc); 107 | free_strarr(dest_arr, dest_size); 108 | set_destroy(test_set); 109 | } 110 | 111 | void test_rm_unedited_pairs_no_matches(void) 112 | { 113 | int *i; 114 | 115 | struct Opts *options = make_opts(); 116 | 117 | // 1. create two sets 118 | int src_argc = 2; 119 | char *src_argv[] = {"TEST_STRING1", "TEST_STRING2"}; 120 | struct Set *src_set = set_init(false, src_argc, src_argv, false); 121 | 122 | int dest_argc = 2; 123 | char *dest_argv[] = {"TEST_STRING2", "TEST_STRING3"}; 124 | struct Set *dest_set = set_init(false, dest_argc, dest_argv, false); 125 | 126 | // 2. subject them to rm_unedited_pairs() 127 | rm_unedited_pairs(src_set, dest_set, options); 128 | 129 | // 3. check their keys 130 | for (i = set_begin(dest_set); i < set_end(dest_set) - 1; i = set_next(i)) 131 | TEST_ASSERT(*i != -1); 132 | } 133 | 134 | void test_rm_unedited_pairs_matches(void) 135 | { 136 | int *i; 137 | 138 | struct Opts *options = make_opts(); 139 | 140 | // 1. create two sets 141 | int src_argc = 2; 142 | char *src_argv[] = {"TEST_STRING1", "TEST_STRING2"}; 143 | struct Set *src_set = set_init(false, src_argc, src_argv, false); 144 | 145 | int dest_argc = 2; 146 | char *dest_argv[] = {"TEST_STRING1", "TEST_STRING3"}; 147 | struct Set *dest_set = set_init(false, dest_argc, dest_argv, false); 148 | 149 | // 2. subject them to rm_unedited_pairs() 150 | rm_unedited_pairs(src_set, dest_set, options); 151 | 152 | // 3. check their keys 153 | i = set_begin(dest_set); 154 | TEST_ASSERT_EQUAL_INT(-1, *i); 155 | } 156 | 157 | void test_rm_cycles(void) 158 | { 159 | struct Opts *options = make_opts(); 160 | 161 | int *i; 162 | char *src_str; 163 | 164 | int src_argc = 2; 165 | char *src_argv[] = {"TEST_STRING1", "TEST_STRING2"}; 166 | struct Set *src_set = set_init(false, src_argc, src_argv, false); 167 | 168 | int dest_argc = 2; 169 | char *dest_argv[] = {"TEST_STRING2", "TEST_STRING3"}; 170 | struct Set *dest_set = set_init(false, dest_argc, dest_argv, false); 171 | 172 | rm_cycles(src_set, dest_set, options); 173 | 174 | for (i = set_begin(src_set); i < set_end(src_set) - 1; i = set_next(i)) 175 | ; 176 | src_str = *get_set_pos(src_set, i); 177 | TEST_ASSERT(strcmp(src_argv[1], src_str) != 0); 178 | 179 | free(options); 180 | set_destroy(src_set); 181 | set_destroy(dest_set); 182 | } 183 | 184 | int main(void) 185 | { 186 | UNITY_BEGIN(); 187 | 188 | RUN_TEST(test_rename_path); // rename_path 189 | RUN_TEST(test_write_strarr_to_tmpfile); // write_strarr_to_tmpfile 190 | RUN_TEST(test_edit_tmpfile); // edit_tmpfile 191 | RUN_TEST(test_read_tmpfile_strs); // read_tmpfile_strs 192 | RUN_TEST(test_rm_unedited_pairs_no_matches); // rm_unedited_pairs 193 | RUN_TEST(test_rm_unedited_pairs_matches); // rm_unedited_pairs 194 | RUN_TEST(test_rm_cycles); // rm_cycles 195 | 196 | return UNITY_END(); 197 | } 198 | -------------------------------------------------------------------------------- /src/mmv.c: -------------------------------------------------------------------------------- 1 | #include "./mmv.h" 2 | 3 | int write_strarr_to_tmpfile(struct Set *set, char tmpfile_template[]) 4 | { 5 | int *i, *set_end_pos = set_end(set); 6 | 7 | int tmp_fd = mkstemp(tmpfile_template); 8 | if (tmp_fd == -1) 9 | { 10 | fprintf(stderr, "mmv: could not create temporary file \'%s\': %s\n", tmpfile_template, strerror(errno)); 11 | return -1; 12 | } 13 | 14 | FILE *tmp_fptr = fdopen(tmp_fd, "w"); 15 | 16 | for (i = set_begin(set); i < set_end_pos; i = set_next(i)) 17 | if (is_valid_key(i)) 18 | fprintf(tmp_fptr, "%s\n", *get_set_pos(set, i)); 19 | 20 | fclose(tmp_fptr); 21 | 22 | return 0; 23 | } 24 | 25 | int edit_tmpfile(char *path) 26 | { 27 | char *editor_name = getenv("EDITOR"); 28 | if (editor_name == NULL) 29 | editor_name = "nano"; 30 | 31 | char *cmd_parts[3] = {editor_name, " ", path}; 32 | char *edit_cmd = strccat(cmd_parts, 3); 33 | if (edit_cmd == NULL) 34 | { 35 | perror("mmv: failed to allocate memory for $EDITOR command string"); 36 | return errno; 37 | } 38 | 39 | #if DEBUG == 0 40 | if (system(edit_cmd) != 0) 41 | { 42 | fprintf(stderr, "mmv: \'%s\' returned non-zero exit status\n", editor_name); 43 | free(edit_cmd); 44 | return errno; 45 | } 46 | #endif 47 | 48 | free(edit_cmd); 49 | 50 | return 0; 51 | } 52 | 53 | struct Set *init_src_set(const int num_keys, char *argv[], struct Opts *options) 54 | { 55 | // prepare for duplicate removal by creating array of absolute paths from commandline args 56 | char **realpath_argv = malloc(sizeof(char *) * (unsigned int)num_keys); 57 | if (realpath_argv == NULL) 58 | { 59 | perror("mmv: failed to allocate memory for absolute path array"); 60 | return NULL; 61 | } 62 | 63 | for (int i = 0; i < num_keys; i++) 64 | if (cpy_str_to_arr(&realpath_argv[i], realpath(argv[i], NULL)) == NULL) 65 | { 66 | free(realpath_argv); 67 | return NULL; 68 | } 69 | 70 | // turn array of absolute paths into a set to rm duplicates 71 | struct Set *realpath_set = set_init(options->resolve_paths, num_keys, realpath_argv, true); 72 | 73 | if (realpath_set == NULL) 74 | { 75 | free(realpath_argv); 76 | return NULL; 77 | } 78 | 79 | struct Set *src_set = realpath_set; 80 | 81 | // if not using the resolve paths opt, give the original arg 82 | // strings, those used on the commandline, back to the user. 83 | if (!options->resolve_paths) 84 | { 85 | int *key, *set_end_pos = set_end(realpath_set), key_num = 0; 86 | for (key = set_begin(realpath_set); key < set_end_pos; key = set_next(key)) 87 | if (is_valid_key(key)) 88 | { 89 | if (cpy_str_to_arr(&realpath_argv[key_num], argv[key_num]) == NULL) 90 | { 91 | free(src_set); 92 | return NULL; 93 | } 94 | 95 | key_num++; 96 | } 97 | 98 | src_set = set_init(false, key_num, realpath_argv, false); 99 | } 100 | 101 | free(realpath_argv); 102 | 103 | return src_set; 104 | } 105 | 106 | struct Set *init_dest_set(unsigned int num_keys, char path[]) 107 | { 108 | // size of destination array only needs to be, at 109 | // maximum, the number of keys in the source set 110 | char **dest_arr = malloc(sizeof(char *) * num_keys); 111 | if (dest_arr == NULL) 112 | return NULL; 113 | 114 | int dest_size = 0; 115 | 116 | if (read_tmpfile_strs(dest_arr, &dest_size, num_keys, path) != 0) 117 | { 118 | free_strarr(dest_arr, dest_size); 119 | return NULL; 120 | } 121 | 122 | struct Set *set = set_init(false, dest_size, dest_arr, true); 123 | 124 | free_strarr(dest_arr, dest_size); 125 | 126 | return set; 127 | } 128 | 129 | int read_tmpfile_strs(char **dest_arr, int *dest_size, unsigned int num_keys, char path[]) 130 | { 131 | char cur_str[PATH_MAX], *read_ptr = ""; 132 | size_t i = 0; 133 | 134 | FILE *tmp_fptr = fopen(path, "r"); 135 | if (tmp_fptr == NULL) 136 | { 137 | fprintf(stderr, "mmv: failed to open \"%s\" in \"r\" mode: %s\n", path, strerror(errno)); 138 | return errno; 139 | } 140 | 141 | while (read_ptr != NULL && i < num_keys) 142 | { 143 | read_ptr = fgets(cur_str, PATH_MAX, tmp_fptr); 144 | 145 | if (read_ptr != NULL && strcmp(cur_str, "\n") != 0) 146 | { 147 | cur_str[strlen(cur_str) - 1] = '\0'; 148 | 149 | cpy_str_to_arr(&dest_arr[(*dest_size)], cur_str); 150 | (*dest_size)++; 151 | 152 | i++; 153 | } 154 | } 155 | 156 | fclose(tmp_fptr); 157 | 158 | return 0; 159 | } 160 | 161 | void free_strarr(char **arr, int arr_size) 162 | { 163 | for (int i = 0; i < arr_size; i++) 164 | free(arr[i]); 165 | 166 | free(arr); 167 | } 168 | 169 | int rename_paths(struct Set *src_set, struct Set *dest_set, struct Opts *opts) 170 | { 171 | int *i, *j; 172 | char *src_str, *dest_str; 173 | 174 | for (i = set_begin(src_set), j = set_begin(dest_set); i < set_end(src_set) && j < set_end(dest_set); 175 | i = set_next(i), j = set_next(j)) 176 | { 177 | src_str = *get_set_pos(src_set, i); 178 | dest_str = *get_set_pos(dest_set, j); 179 | 180 | if (is_valid_key(j)) 181 | rename_path(src_str, dest_str, opts); 182 | } 183 | 184 | return 0; 185 | } 186 | 187 | void rename_path(const char *src, const char *dest, struct Opts *opts) 188 | { 189 | if (rename(src, dest) == -1) 190 | { 191 | fprintf(stderr, "mmv: \'%s\' to \'%s\': %s\n", src, dest, strerror(errno)); 192 | 193 | if (errno == 2) 194 | remove(dest); 195 | } 196 | 197 | else if (opts->verbose) 198 | printf(" '%s' to '%s'\n", src, dest); 199 | } 200 | 201 | int rm_unedited_pairs(struct Set *src_set, struct Set *dest_set, struct Opts *opts) 202 | { 203 | char *src_str, *dest_str; 204 | int *i, *j, *src_end_pos = set_end(src_set), *dest_end_pos = set_end(dest_set); 205 | 206 | for (i = set_begin(src_set), j = set_begin(dest_set); i < src_end_pos && j < dest_end_pos; 207 | i = set_next(i), j = set_next(j)) 208 | { 209 | src_str = *get_set_pos(src_set, i); 210 | dest_str = *get_set_pos(dest_set, j); 211 | 212 | if (strcmp(src_str, dest_str) == 0) 213 | { 214 | set_key(j, -1); 215 | 216 | if (opts->verbose) 217 | printf(" '%s' was not edited. No mv will be conducted.\n", src_str); 218 | } 219 | } 220 | 221 | return 0; 222 | } 223 | 224 | int rm_cycles(struct Set *src_set, struct Set *dest_set, struct Opts *opts) 225 | { 226 | int is_dupe, *i, *j, *src_end_pos = set_end(src_set), *dest_end_pos = set_end(dest_set); 227 | unsigned long int u_key; 228 | char *dest_str, *tmp_path, **cur_src_pos; 229 | 230 | for (i = set_begin(src_set), j = set_begin(dest_set); i < src_end_pos && j < dest_end_pos; 231 | i = set_next(i), j = set_next(j)) 232 | { 233 | if (is_valid_key(j)) 234 | { 235 | dest_str = *get_set_pos(dest_set, j); 236 | u_key = (unsigned int)*j; 237 | is_dupe = is_duplicate_element(dest_str, src_set, &u_key); 238 | 239 | if (is_dupe == 0) 240 | { 241 | cur_src_pos = get_set_pos(src_set, j); 242 | char template[] = "_mmv_XXXXXX"; 243 | char *tmp_path_parts[2] = {*cur_src_pos, template}; 244 | 245 | tmp_path = strccat(tmp_path_parts, 2); 246 | if (tmp_path == NULL) 247 | { 248 | perror("mmv: failed to allocate memory for cycle-removal temporary path"); 249 | return -1; 250 | } 251 | 252 | // create temporary name using the current name 253 | int tmp_fd = mkstemp(tmp_path); 254 | if (tmp_fd == -1) 255 | { 256 | fprintf(stderr, "mmv: could not create temporary file \'%s\': %s\n", tmp_path, strerror(errno)); 257 | return -1; 258 | } 259 | 260 | // rename to temporary name 261 | rename_path(*cur_src_pos, tmp_path, opts); 262 | 263 | // update str in src map to temp_str 264 | free(*cur_src_pos); 265 | cpy_str_to_arr(cur_src_pos, tmp_path); 266 | free(tmp_path); 267 | } 268 | } 269 | } 270 | 271 | return 0; 272 | } 273 | --------------------------------------------------------------------------------