From 494ecdbdaf5574f9a60b29a9d02bc1feb5cb8a30 Mon Sep 17 00:00:00 2001 From: "kwhkim@macm1" Date: Fri, 6 Feb 2026 13:17:04 +0900 Subject: [PATCH] git-issue-mvp, new_filename.sh| MacOS-compatible 1. [[ ]]; -> [ ]; 2. Platform-agnostic timestamp generation - Added fallback logic for date +%s: First tries gdate (GNU date on macOS via Homebrew) Falls back to standard date +%s (works on Linux and modern macOS) Final fallback to perl for maximum compatibility 3. Replaced $(< "$id_file") with $(cat "$id_file") - The $(< file) syntax is bash-specific. Using cat is the portable alternative that works in all POSIX shells. 4. Changed == to = - In [ ... ] tests, the single = is the POSIX standard for string comparison. While == works in bash, = is more portable. 5. especially next_seq - Removed bash-specific features: - Removed shopt -s nullglob (bash-specific) - Removed [[ $f =~ ... ]] regex matching (bash-specific) - Removed BASH_REMATCH array (bash-specific) - Used find instead of globbing - More portable and handles edge cases better across platforms. - Parameter expansion for string extraction - Used ${filename#prefix} and ${temp%suffix} for portable string manipulation. - Portable number validation - Used case statement with pattern matching instead of regex. - Temporary file for max value - Since the while loop with find runs in a subshell (due to the pipe), we use a temporary file to persist the max value across iterations. - Added -print0 and -d '' - For safe handling of filenames with spaces or special characters. - 6. esp. gen_repo_id - Added check for /dev/urandom existence with [ -c /dev/urandom ] for better portability - Added fallback to openssl if /dev/urandom is not available Added final fallback using date, $$ (process ID), and either md5sum (Linux) or md5 (macOS) - More robust error handling - LC_ALL=C prefix - Forces tr to treat input as single-byte characters, preventing UTF-8 interpretation errors on macOS. - Error suppression and validation - Each method: - Redirects stderr to /dev/null with 2>/dev/null - Validates the result is non-empty and has the correct length Falls through to the next method if it fails - Removed bash-specific features: - Removed shopt -s nullglob - Removed [[ $f =~ ... ]] regex matching - Removed BASH_REMATCH array - Used find instead of glob patterns - Added macOS-specific method - jot is a BSD/macOS utility for generating sequences. - Improved hash fallback - Handles both md5 (macOS) and md5sum (Linux) with proper validation. - Guaranteed output - The absolute fallback at the end ensures the function always returns something, even if not ideal. - Better error handling - Each pipeline has || result="" to catch failures gracefully. 7. esp. repo_id_in_use - Replaced grep -q . with [ -n "$(find ...)" ] for simpler, more portable check - Added quotes around ${rid}-*.md for safety - Added 2>/dev/null to suppress errors if directory doesn't exist 8. parse_issue_id - Replaced [[ "$id" == *-* ]] - Changed to echo "$id" | grep -q '-' which works on all platforms. - Replaced regex =~ and BASH_REMATCH - Added validation using case statements - Validates that id_left contains only letters and id_right contains only digits using portable pattern matching. - Replaced sed ... <<< "$var" - Changed from bash-specific here-string to portable pipe: - Added edge case handling - If id_right is all zeros (e.g., "0000"), after removing leading zeros it becomes empty, so we set it to "0". --- git-issue-mvp | 413 ++++++++++++++++++++++++++++++++---------------- new_filename.sh | 191 ++++++++++++++++------ 2 files changed, 427 insertions(+), 177 deletions(-) diff --git a/git-issue-mvp b/git-issue-mvp index cc9d8a8..3bee3c1 100644 --- a/git-issue-mvp +++ b/git-issue-mvp @@ -29,31 +29,46 @@ is_git_scp_like_host_path() { local host path host_part # must contain exactly one colon (first colon matters) - [[ "$url" == *:* ]] || return 1 + case "$url" in + *:*) ;; + *) return 1 ;; + esac host_part="${url%%:*}" path="${url#*:}" # path must not be empty to be scp-like - [[ -n "$path" ]] || return 1 + [ -n "$path" ] || return 1 # reject if '/' appears before the colon - [[ "$host_part" != */* ]] || return 1 + case "$host_part" in + */*) return 1 ;; + esac # strip optional user@ (take part after last @) host="${host_part##*@}" # host must be non-empty - [[ -n "$host" ]] || return 1 + [ -n "$host" ] || return 1 # hostname-legal characters: [A-Za-z0-9.-]+ - [[ "$host" =~ ^[A-Za-z0-9.-]+$ ]] || return 1 + # Use case statement for character validation instead of regex + # First check if it contains any invalid characters + case "$host" in + *[!A-Za-z0-9.-]*) return 1 ;; # Contains invalid chars + "") return 1 ;; # Empty string + esac # Windows drive-letter exclusion (only if running on Windows Git) # Safe to keep on Unix — it never triggers - if [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]]; then - [[ ! "$host" =~ ^[A-Za-z]$ ]] || return 1 - fi + case "${OSTYPE:-}" in + msys*|cygwin*) + # Check if host is a single letter (Windows drive) + case "$host" in + [A-Za-z]) return 1 ;; + esac + ;; + esac return 0 # scp-like } @@ -171,13 +186,13 @@ git_issue_init() { # ref exists on the remote echo '!W: The reference '"$local_ref"' exists on the remote('"$remote"').' while true; do - read -p ' - Do you want to clone it? (Y/N)' ans + read -p ' - Do you want to clone it? (Y/N) ' ans case "$ans" in [Yy]) # Possibly post-commit hook for auto-sync issue-status(open->closed) git_issue_clone "$remote" git config --local extensions.worktreeConfig true || echo 'FAILED: git config --local extensions.worktreeConfig true' - cd $wt && { + cd "$wt" && { git config --worktree advice.detachedHead false git config --worktree core.hooksPath /dev/null || echo 'FAILED: git config --worktree core.hooksPath /dev/null' @@ -186,7 +201,7 @@ git_issue_init() { #echo ".gitignore" >> .gitignore #echo ".local-repo-id" >> .gitignore } - cd - + cd - > /dev/null || return 1 return ;; [Nn]) @@ -209,14 +224,26 @@ git_issue_init() { # git worktree add --detach "$root/.issues" # cd "$root/.issues" || return 1 - tmp_ref="refs/issues/tmp/$(date +%s)-$$" + # Platform-agnostic timestamp generation + if command -v gdate >/dev/null 2>&1; then + # macOS with GNU coreutils installed + timestamp=$(gdate +%s) + elif date +%s >/dev/null 2>&1; then + # Linux or macOS with recent date + timestamp=$(date +%s) + else + # Fallback for older systems + timestamp=$(perl -e 'print time') + fi + + tmp_ref="refs/issues/tmp/${timestamp}-$$" #git worktree add --orphan -B temp_issues $root/.issues - git worktree add --orphan -B $tmp_ref $root/.issues + git worktree add --orphan -B "$tmp_ref" "$root/.issues" - mkdir -p $root/.issues/issues + mkdir -p "$root/.issues/issues" git config --local extensions.worktreeConfig true || echo 'FAILED: git config --local extensions.worktreeConfig true' - cd $wt || { + cd "$wt" || { echo "failed to cd to issue worktree" return 1 } @@ -267,53 +294,88 @@ EOF commit=$(git rev-parse HEAD) #git update-ref "$ISSUE_REF_PREFIX/$issue_id" - git update-ref refs/issues/latest $commit + git update-ref refs/issues/latest "$commit" echo "Initialized issue tracking in .issues/" git checkout --detach #git branch -D temp_issues - git branch -D $tmp_ref - cd - || { + git branch -D "$tmp_ref" + cd - > /dev/null || { echo "failed to restore the directory" return 1 } } parse_issue_id() { - local id=$1 - - if [[ "$id" == *-* ]]; then + local id="$1" + local id_left id_right + if echo "$id" | grep -q '-'; then # Normalize ID format - id_left=${id%-*} - id_right=${id#*-} + id_left="${id%-*}" + id_right="${id#*-}" #echo "$id_left" "$id_right" + if [ "$id_left" = "$id_right" ]; then echo 'usage: git issue edit ' - echo ' should be alphbets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' + echo ' should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi if ! is_int "$id_right"; then echo 'usage: git issue edit ' - echo ' should be alphbets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' + echo ' should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi else - if [[ "$id" =~ ^([[:alpha:]]+)([[:digit:]]+)$ ]]; then - id_left="${BASH_REMATCH[1]}" - id_right="${BASH_REMATCH[2]}" - else + # Extract alphabetic prefix and numeric suffix without regex + # Remove all digits from the end to get prefix + id_left="$id" + while [ "$id_left" != "${id_left%[0-9]}" ]; do + id_left="${id_left%[0-9]}" + done + + # Remove all non-digits from the beginning to get suffix + id_right="$id" + while [ "$id_right" != "${id_right#[!0-9]}" ]; do + id_right="${id_right#[!0-9]}" + done + + # Validate that we have both parts + if [ -z "$id_left" ] || [ -z "$id_right" ]; then echo "usage: git issue edit " - echo ' should be alphbets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' + echo ' should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi + + # Verify id_left contains only alphabets and id_right contains only digits + case "$id_left" in + *[!A-Za-z]*) + echo "usage: git issue edit " + echo ' should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' + return 1 + ;; + esac + + case "$id_right" in + *[!0-9]*) + echo "usage: git issue edit " + echo ' should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' + return 1 + ;; + esac fi #echo "$id_left" "$id_right" - id_right=$(sed 's/^0*//' <<< "$id_right") - # 012은 octal number 12로 인식되므로 10진수로 바꿔줘야 함 + + # Remove leading zeros - platform agnostic + id_right=$(echo "$id_right" | sed 's/^0*//') + + # Handle empty string (if input was all zeros like "0000") + [ -z "$id_right" ] && id_right="0" + + # Format with leading zeros (converts to decimal, avoiding octal interpretation) id_right=$(printf "%04d" "$id_right") #echo "$id_left" "$id_right" @@ -323,20 +385,24 @@ parse_issue_id() { git_issue_edit() { + echo gie1 id="$(parse_issue_id "$1")" || return 1 + echo gie2 fn_md="$id".md root=$(git_project_root) wt=$(git_issue_worktree) path="$wt"/issues/"$fn_md" + if ! safe_cd "$wt"; then + echo '!E: '"Could not change directory to $wt" return 1 fi #fn=$1 [ -z "$id" ] && { echo "usage: git issue edit " - cd - + cd - > /dev/null || return 1 return 1 } @@ -347,33 +413,37 @@ git_issue_edit() { editor=$(git var GIT_EDITOR) || return 1 - #mkdir -p "$(dirname "$file")" + #mkdir -p "$(dirname "$path")" [ -f "$path" ] || { echo "Issue $id not found" - cd - + cd - > /dev/null || return 1 return 1 } - #${EDITOR:-vi} "$file" + #${EDITOR:-vi} "$path" sh -c "$editor \"$path\"" || { echo "editor failed" - cd - + cd - > /dev/null || return 1 return 1 } - # if cmp -s "$file" "$root/.issues/TEMPLATE.md"; then - # echo "No changes made to issue $file" + # if cmp -s "$path" "$root/.issues/TEMPLATE.md"; then + # echo "No changes made to issue $path" # read -p "Do you want to keep the file? (Y/n): " ans - # if [[ $ans == [Nn]* ]]; then - # rm -f "$file" - # echo "Deleted template issue file $file" - # else - # echo "Keeping template issue file $file" - # fi + # case "$ans" in + # [Nn]*) + # rm -f "$path" + # echo "Deleted template issue file $path" + # ;; + # *) + # echo "Keeping template issue file $path" + # ;; + # esac # else # return 0 # fi - cd - + + cd - > /dev/null || return 1 } git_issue_comment() { @@ -383,6 +453,7 @@ git_issue_comment() { root=$(git_project_root) wt=$(git_issue_worktree) path="$wt"/issues/"$fn_md" + if ! safe_cd "$wt"; then return 1 fi @@ -390,13 +461,13 @@ git_issue_comment() { #fn=$1 [ -z "$id" ] && { echo "usage: git issue comment " - cd - + cd - > /dev/null || return 1 return 1 } [ -f "$path" ] || { echo "Issue $id not found" - cd - + cd - > /dev/null || return 1 return 1 } @@ -406,6 +477,7 @@ git_issue_comment() { echo "- comment character set to $comment_char" tmpfile="$(mktemp)"".md" + trap 'rm -f "$tmpfile"' EXIT INT TERM #sed 's/^/# /' file.txt > "$tmpfile" sed 's/^/'"$comment_char "'/' "$path" >> "$tmpfile" @@ -413,11 +485,17 @@ git_issue_comment() { DIR_COMMENT="$wt/issues/$id/comments" echo "- comment directory : $DIR_COMMENT" - comment_file=$(new_filename_comment .md "$id") || { echo '!E: New comment name could not be created'; return 1; } + comment_file=$(new_filename_comment .md "$id") || { + echo '!E: New comment name could not be created' + rm -f "$tmpfile" + trap - EXIT INT TERM + cd - > /dev/null || return 1 + return 1 + } echo '- new comment file named '"$comment_file" mkdir -p "$DIR_COMMENT" - find "$DIR_COMMENT" -type f -print0 | + find "$DIR_COMMENT" -type f -print0 2>/dev/null | while IFS= read -r -d '' other_comment_file; do { echo '' @@ -430,16 +508,30 @@ git_issue_comment() { echo '' >> "$tmpfile" user=$(git config user.name) email=$(git config user.email) - t_now=$(date "+%Y-%m-%d %H:%M:%S") + + # Platform-agnostic date formatting + if date "+%Y-%m-%d %H:%M:%S" >/dev/null 2>&1; then + t_now=$(date "+%Y-%m-%d %H:%M:%S") + else + # Fallback for systems with limited date support + t_now=$(date) + fi echo '*** comment by '"$user($email) at $t_now" >> "$tmpfile" - editor=$(git var GIT_EDITOR) || return 1 + editor=$(git var GIT_EDITOR) || { + rm -f "$tmpfile" + trap - EXIT INT TERM + cd - > /dev/null || return 1 + return 1 + } - #${EDITOR:-vi} "$file" + #${EDITOR:-vi} "$tmpfile" sh -c "$editor \"$tmpfile\"" || { echo '!'"W: editor failed" - cd - + rm -f "$tmpfile" + trap - EXIT INT TERM + cd - > /dev/null || return 1 return 1 } @@ -449,6 +541,7 @@ git_issue_comment() { # else # echo "- no comment to save. aborted" # fi + message="$( sed \ -e "s/[[:space:]]*$//" \ @@ -456,17 +549,19 @@ git_issue_comment() { -e "/^""$comment_char""$/d" \ "$tmpfile" \ | sed '/./,$!d' \ - | sed ':a;/^\n*$/{$d;N;ba;}' + | awk 'NF {p=1} p; END {while (sub(/\n$/,"")) {}}' )" echo "- message : $message" # shellcheck disable=SC2001 - message_trimmed="$(echo "$message" |sed s/'[*][*][*] comment by .* at .*$'// )" + message_trimmed="$(echo "$message" | sed 's/[*][*][*] comment by .* at .*$//')" if [ -z "$message_trimmed" ]; then echo "Aborted: empty message" >&2 rm -f "$tmpfile" + trap - EXIT INT TERM + cd - > /dev/null || return 1 return 1 fi @@ -475,28 +570,33 @@ git_issue_comment() { printf "%s\n" "$message" > "$DIR_COMMENT"/"$comment_file" && echo "- comment file saved to $comment_file" - # if cmp -s "$file" "$root/.issues/TEMPLATE.md"; then - # echo "No changes made to issue $file" - # read -p "Do you want to keep the file? (Y/n): " ans - # if [[ $ans == [Nn]* ]]; then - # rm -f "$file" - # echo "Deleted template issue file $file" - # else - # echo "Keeping template issue file $file" - # fi + # if cmp -s "$path" "$root/.issues/TEMPLATE.md"; then + # echo "No changes made to issue $path" + # printf "Do you want to keep the file? (Y/n): " + # read -r ans + # case "$ans" in + # [Nn]*) + # rm -f "$path" + # echo "Deleted template issue file $path" + # ;; + # *) + # echo "Keeping template issue file $path" + # ;; + # esac # else # return 0 # fi + rm -f "$tmpfile" - cd - - + trap - EXIT INT TERM + cd - > /dev/null || return 1 } - git_issue_new() { root=$(git_project_root) # cd "$root/.issues" || return 1 wt=$(git_issue_worktree) if ! safe_cd "$wt"; then + echo '!E:'" Could not change directory to $wt" return 1 fi @@ -543,19 +643,26 @@ git_issue_new() { return 1 } + echo 1 root=$(git_project_root) path="$wt/issues/$fn_md" - + + echo 2 mkdir -p "$(dirname "$path")" + + echo 3 # Validation (Optional) - if [[ -z "$TITLE" ]]; then + if [ -z "$TITLE" ]; then + echo 4 [ -f "$path" ] || cp "$wt/TEMPLATE.md" "$path" id=$(basename "$fn_md" .md) + echo 5 + echo "- id=$id" git_issue_edit "$id" || { return 1; } - if [ -s $path ]; then + if [ -s "$path" ]; then echo '- issue '"$fn_md"' created' fi - cd - || return 1 + cd - > /dev/null || return 1 else { echo "---" @@ -568,7 +675,7 @@ git_issue_new() { echo "$BODY" } > "$path" echo "Created new issue: issues/$fn_md" - cd - || return 1 + cd - > /dev/null || return 1 fi } @@ -715,30 +822,55 @@ git_issue_show() { echo "- Comments found : $num_comments" echo "" - if [[ "$num_comments" -gt 0 ]]; then + if [ "$num_comments" -gt 0 ]; then while :; do - read -r -p "--->8 Continue to comments? (Y/N) " ans - if [[ $ans =~ ^(N|n)$ ]]; then - return 0 - elif [[ $ans =~ ^(Y|y|N|n)$ ]]; then - break - fi + printf "%s" "--->8 Continue to comments? (Y/N) " + read -r ans + case "$ans" in + [Nn]) + return 0 + ;; + [Yy]) + break + ;; + *) + # Invalid input, loop continues + ;; + esac done DIR_COMMENT="$wt/issues/$id/comments" - if [[ -d $DIR_COMMENT ]]; then - # find "$DIR_COMMENT" -type f -print0 | - # sort by mtime (oldest to newest) - find "$DIR_COMMENT" -type f -printf "%T@ %p\0" | sort -zn | cut -zd' ' -f2- | - # find "$DIR_COMMENT" -type f -printf "%T@ %p\0" | sort -zn | cut -zd' ' -f2- | # newest to oldest - while IFS= read -r -d '' other_comment_file; do - echo '' - echo '=== >8 COMMENT '"$other_comment_file"' ---' - #cat "$other_comment_file" - sed 's/^/ /' "$other_comment_file" - echo '' - done + if [ -d "$DIR_COMMENT" ]; then + # Platform-agnostic approach using a temporary file for sorting + tmp_sorted=$(mktemp) + trap 'rm -f "$tmp_sorted"' EXIT INT TERM + + # Get files with modification times + find "$DIR_COMMENT" -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do + # Get modification time in a platform-agnostic way + if stat -f "%m" "$f" >/dev/null 2>&1; then + # macOS (BSD stat) + mtime=$(stat -f "%m" "$f") + else + # Linux (GNU stat) + mtime=$(stat -c "%Y" "$f") + fi + printf "%s %s\n" "$mtime" "$f" + done | sort -n | cut -d' ' -f2- > "$tmp_sorted" + + # Process each file (oldest to newest) + while IFS= read -r other_comment_file; do + [ -f "$other_comment_file" ] || continue + echo '' + echo '=== >8 COMMENT '"$other_comment_file"' ---' + #cat "$other_comment_file" + sed 's/^/ /' "$other_comment_file" + echo '' + done < "$tmp_sorted" + + rm -f "$tmp_sorted" + trap - EXIT INT TERM fi fi } @@ -748,53 +880,68 @@ git_issue_show() { git_issue_list() { root=$(git_project_root) issues_dir="$root/.issues/issues" || return 1 - + # check the folder .issues/ [ ! -d "$root/.issues" ] && echo ".issue not found. Use \`git issue init\` to create new one and \`git issue clone\` to clone the remote issues" && return 1 - + printf "%-13s %-8s %-8s %-3s %-4s %s\n" "ID" "STATUS" "TYPE" "PRI" "#CMT" "TITLE" - - while IFS= read -r -d '' f; do + + # Platform-agnostic approach: use a temporary file for sorting + tmp_file=$(mktemp) + trap 'rm -f "$tmp_file"' EXIT + + # Find all markdown files and get their modification times + find "$issues_dir" -maxdepth 1 -name "*.md" -type f | while IFS= read -r f; do + # Get modification time in a platform-agnostic way + if stat -f "%m" "$f" >/dev/null 2>&1; then + # macOS (BSD stat) + mtime=$(stat -f "%m" "$f") + else + # Linux (GNU stat) + mtime=$(stat -c "%Y" "$f") + fi + printf "%s %s\n" "$mtime" "$f" + done | sort -rn | cut -d' ' -f2- > "$tmp_file" + + # Process each file + while IFS= read -r f; do [ -f "$f" ] || continue - id=$(basename "$f" .md) + id=$(basename "$f" .md) num_comments=$(count_comments "$id") - num_comments=$(printf "%4d" "$num_comments") - + awk -v id="$id" -v num_comments="$num_comments" ' - BEGIN { - in_yaml = 0 - title = type = pri = status = "" - } - - /^---$/ { - in_yaml = !in_yaml - next - } - - in_yaml { - if ($1 == "title:") { - sub(/^title:[[:space:]]*/, "") - title = $0 - } - else if ($1 == "type:") type = substr($2, 1, 8) - else if ($1 == "priority:") pri = substr($2, 1, 8) - else if ($1 == "status:") status = substr($2, 1, 8) - } - - END { - printf "%-13s %-8s %-8s %-3s %-4s %s\n", - id, - status ? status : "-", - type ? type : "-", - pri ? pri : "-", - num_comments, - title ? title : "-" - } - ' "$f" - done < <(find "$issues_dir" -maxdepth 1 -name "*.md" -type f -printf '%T@ %p\0' | sort -zrn | cut -zd' ' -f2-) - + BEGIN { + in_yaml = 0 + title = type = pri = status = "" + } + /^---$/ { + in_yaml = !in_yaml + next + } + in_yaml { + if ($1 == "title:") { + sub(/^title:[[:space:]]*/, "") + title = $0 + } + else if ($1 == "type:") type = substr($2, 1, 8) + else if ($1 == "priority:") pri = substr($2, 1, 8) + else if ($1 == "status:") status = substr($2, 1, 8) + } + END { + printf "%-13s %-8s %-8s %-3s %-4s %s\n", + id, + status ? status : "-", + type ? type : "-", + pri ? pri : "-", + num_comments, + title ? title : "-" + } + ' "$f" + done < "$tmp_file" + + rm -f "$tmp_file" } upper() { @@ -1498,7 +1645,7 @@ git_issue_status() { issue_gitdir=$(git rev-parse --git-dir) if [ -f "$issue_gitdir"/MERGE_HEAD ]; then echo "Issue merge in progress" - git status --short + git status --short exit 0 fi @@ -1543,7 +1690,7 @@ git_issue_status() { # [ -n "$(git ls-files --others --exclude-standard)" ]; then # echo "No changes since last commit" # else - git status --short + git status --short -- issues/ # fi ) } diff --git a/new_filename.sh b/new_filename.sh index 8bdefd7..2545c0e 100755 --- a/new_filename.sh +++ b/new_filename.sh @@ -7,13 +7,75 @@ id_len=6 pad=4 gen_repo_id() { - tr -dc "$alphabet" < /dev/urandom | head -c "$id_len" || true - # || true is to avoid non-zero exit code if head cuts tr output + local result + + # Method 1: Try /dev/urandom with LC_ALL=C to handle binary data + if [ -c /dev/urandom ]; then + result=$(LC_ALL=C tr -dc "$alphabet" < /dev/urandom 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + if [ -n "$result" ] && [ ${#result} -eq "$id_len" ]; then + printf '%s' "$result" + return 0 + fi + fi + + # Method 2: Try openssl + if command -v openssl >/dev/null 2>&1; then + result=$(openssl rand -base64 48 2>/dev/null | tr -dc "$alphabet" 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + if [ -n "$result" ] && [ ${#result} -eq "$id_len" ]; then + printf '%s' "$result" + return 0 + fi + fi + + # Method 3: Try /dev/random (slower but more portable on some systems) + if [ -c /dev/random ]; then + result=$(LC_ALL=C tr -dc "$alphabet" < /dev/random 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + if [ -n "$result" ] && [ ${#result} -eq "$id_len" ]; then + printf '%s' "$result" + return 0 + fi + fi + + # Method 4: Use jot (available on macOS and BSD) + if command -v jot >/dev/null 2>&1; then + # Generate random characters using jot + result=$(jot -r -c "$id_len" 0 127 2>/dev/null | tr -dc "$alphabet" 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + if [ -n "$result" ] && [ ${#result} -eq "$id_len" ]; then + printf '%s' "$result" + return 0 + fi + fi + + # Method 5: Last resort - use date, PID, and hash + if command -v md5 >/dev/null 2>&1; then + # macOS + result=$(printf '%s%s%s' "$(date +%s%N 2>/dev/null || date +%s)" "$$" "${RANDOM:-$(awk 'BEGIN{srand();print int(rand()*32768)}')}" | \ + md5 2>/dev/null | tr -dc "$alphabet" 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + elif command -v md5sum >/dev/null 2>&1; then + # Linux + result=$(printf '%s%s%s' "$(date +%s%N 2>/dev/null || date +%s)" "$$" "${RANDOM:-$(awk 'BEGIN{srand();print int(rand()*32768)}')}" | \ + md5sum 2>/dev/null | cut -d' ' -f1 | tr -dc "$alphabet" 2>/dev/null | head -c "$id_len" 2>/dev/null) || result="" + fi + + if [ -n "$result" ] && [ ${#result} -ge "$id_len" ]; then + printf '%s' "$result" | head -c "$id_len" + return 0 + fi + + # Absolute fallback: generate from timestamp and PID + printf '%s%s' "$(date +%s)" "$$" | head -c "$id_len" + return 0 } repo_id_in_use() { local rid="$1" - if find . -maxdepth 1 -name "$rid-*.md" -print -quit | grep -q .; then + # Use find with -print and check if output is non-empty + if [ -n "$(find . -maxdepth 1 -name "${rid}-*.md" -print -quit 2>/dev/null)" ]; then + wc_rid=$(cat "$id_file" | wc -c) + if [ $wc_rid -lt $id_len ]; then + echo '!'"E: id_file $id_file seems broken" + return 1 + fi echo "yes" else echo "no" @@ -21,13 +83,13 @@ repo_id_in_use() { } secure_repo_id() { - if [[ -f $id_file ]]; then - repo_id=$(< "$id_file") + if [ -f "$id_file" ]; then + repo_id=$(cat "$id_file") echo "$repo_id" else while :; do repo_id=$(gen_repo_id) - if [[ $(repo_id_in_use "$repo_id") == no ]]; then + if [ "$(repo_id_in_use "$repo_id")" = "no" ]; then printf '%s\n' "$repo_id" > "$id_file" break fi @@ -39,18 +101,40 @@ next_seq() { local rid="$1" local ext="$2" local max=0 - local n n_dec - - shopt -s nullglob - for f in "issues/""$rid-"*"$ext"; do - [[ $f =~ ^issues/$rid-([0-9]+)$ext$ ]] || continue - n="${BASH_REMATCH[1]}" - n_dec=$((10#$n)) # force decimal, strips padding safely - ((n_dec > max)) && max="$n_dec" - done - shopt -u nullglob + local n n_dec f filename temp tmp_max + + # Create a temporary file to store the max value + tmp_max=$(mktemp) + + echo "0" > "$tmp_max" + + if [ -d "issues" ]; then + find "issues" -maxdepth 1 -name "${rid}-*${ext}" -type f -print0 2>/dev/null | \ + while IFS= read -r -d '' f; do + filename=$(basename "$f") + temp="${filename#${rid}-}" + n="${temp%${ext}}" + + case "$n" in + ''|*[!0-9]*) continue ;; + esac + + n_dec=$((10#$n)) + max=$(cat "$tmp_max") + + if [ "$n_dec" -gt "$max" ]; then + echo "$n_dec" > "$tmp_max" + fi + done + + max=$(cat "$tmp_max") + fi - printf "%0*d" "$pad" $((max + 1)) + # Clean up and return result + local result + result=$(printf "%0*d" "$pad" $((max + 1))) + rm -f "$tmp_max" + printf '%s' "$result" } next_seq_comment() { @@ -58,35 +142,50 @@ next_seq_comment() { local ext="$2" local issue_id="$3" local max=0 - local n n_dec - shopt -s nullglob - for f in "issues/$issue_id/comments/"*; do - [[ $f =~ ^issues/"$issue_id"/comments/$rid-([0-9]+)$ext$ ]] || continue - n="${BASH_REMATCH[1]}" - n_dec=$((10#$n)) # force decimal, strips padding safely - ((n_dec > max)) && max="$n_dec" - done - shopt -u nullglob + if [ -d "issues/$issue_id/comments" ]; then + # Get all matching files and extract the max sequence + for f in "issues/$issue_id/comments/${rid}-"*"${ext}"; do + # Check if glob matched anything (file exists) + [ -f "$f" ] || continue + + # Extract filename + filename=$(basename "$f") + # Remove prefix and suffix + temp="${filename#${rid}-}" + n="${temp%${ext}}" + + # Validate it's a number + case "$n" in + ''|*[!0-9]*) continue ;; + esac + + # Convert to decimal + n_dec=$((10#$n)) + + # Update max + [ "$n_dec" -gt "$max" ] && max="$n_dec" + done + fi printf "%0*d" "$pad" $((max + 1)) } new_filename_comment() { - if [[ $# -lt 2 ]]; then + if [ $# -lt 2 ]; then echo "new_comment_file " + return 1 fi - ext=$1 - issue_id=$2 + ext="$1" + issue_id="$2" - - if [[ -f $id_file ]]; then - repo_id=$(< "$id_file") + if [ -f "$id_file" ]; then + repo_id=$(cat "$id_file") else while :; do repo_id=$(gen_repo_id) - if [[ $(repo_id_in_use "$repo_id") == no ]]; then + if [ "$(repo_id_in_use "$repo_id")" = "no" ]; then printf '%s\n' "$repo_id" > "$id_file" break fi @@ -97,8 +196,6 @@ new_filename_comment() { filename="$repo_id-$seq$ext" printf '%s\n' "$filename" - - } # ---- main ---- @@ -106,31 +203,37 @@ new_filename_comment() { new_filename() { local ext - #if [[ -n $1 ]]; then + #if [ -n "$1" ]; then # ext="$1" #else # ext=".md" #fi - if [[ $# -eq 0 ]]; then + if [ $# -eq 0 ]; then ext=".md" else ext="$1" fi - # if [[ -n $1 ]]; then + # if [ -n "$1" ]; then # ext="" # else - # ext=$1 + # ext="$1" # fi - #echo $ext + #echo "$ext" - if [[ -f $id_file ]]; then - repo_id=$(< "$id_file") + if [ -f "$id_file" ]; then + # echo nf1 + wc_rid=$(cat "$id_file" | wc -c) + if [ $wc_rid -lt $id_len ]; then + echo '!'"E: id_file $id_file seems broken" + return 1 + fi + repo_id=$(cat "$id_file") else while :; do repo_id=$(gen_repo_id) - if [[ $(repo_id_in_use "$repo_id") == no ]]; then + if [ "$(repo_id_in_use "$repo_id")" = "no" ]; then printf '%s\n' "$repo_id" > "$id_file" break fi @@ -141,4 +244,4 @@ new_filename() { filename="$repo_id-$seq$ext" printf '%s\n' "$filename" -} +} \ No newline at end of file -- 2.51.2