Skip to content

Instantly share code, notes, and snippets.

@psenger
Created June 13, 2025 12:47
Show Gist options
  • Save psenger/7c1baf31e41dc2a8eb1787ded80b0f5b to your computer and use it in GitHub Desktop.
Save psenger/7c1baf31e41dc2a8eb1787ded80b0f5b to your computer and use it in GitHub Desktop.
[Recursive and Pattern-Based File Copy or Duplicate Files with Suffixes] #macos #unix #zsh #shellscript #cli #automation #react #typescript

🧬 Recursive and Pattern-Based File Copy or Duplicate Files with Suffixes (*.tsx)

This guide demonstrates how to programmatically duplicate files with new names based on filename patterns using Zsh scripting.

Common use case: You're working with a codebase using .tsx files (React TypeScript) and need to quickly scaffold variants of each component β€” like *-skeleton.tsx and *-fallback.tsx files.


βœ… Goal

For every *.tsx file in a directory (e.g. some-directory), create one or more new files with a modified name, excluding certain patterns to avoid unnecessary duplication.


πŸ“ Scenario 1: Create *-skeleton.tsx variants for all .tsx files

Original Directory Structure

some-directory/
  β”œβ”€β”€ A.tsx
  β”œβ”€β”€ B.tsx
  β”œβ”€β”€ C.tsx
  └── D.tsx

Desired Outcome

Each .tsx file should have a corresponding *-skeleton.tsx file:

some-directory/
  β”œβ”€β”€ A.tsx
  β”œβ”€β”€ A-skeleton.tsx
  β”œβ”€β”€ B.tsx
  β”œβ”€β”€ B-skeleton.tsx
  β”œβ”€β”€ C.tsx
  β”œβ”€β”€ C-skeleton.tsx
  β”œβ”€β”€ D.tsx
  └── D-skeleton.tsx

βœ… Zsh Command

for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx ]] && touch "${f%.tsx}-skeleton.tsx"
done
for f in *.tsx(.N); do [[ $f != *-skeleton.tsx ]] && touch "${f%.tsx}-skeleton.tsx"; done\n

πŸ” Explanation

  • for f in *.tsx(.N)

    • Iterates over all .tsx files in the current directory.

    • (.N) is a Zsh glob qualifier:

      • .: plain (non-directory) files only.
      • N: nullglob; prevents errors if no matches are found.
  • [[ $f != *-skeleton.tsx ]]

    • Skips files that already have -skeleton in the filename.
  • touch "${f%.tsx}-skeleton.tsx"

    • Creates an empty new file with -skeleton.tsx appended, preserving the original name without .tsx.

πŸ“ Scenario 2: Add *-fallback.tsx files, ignoring already-patterned files

Extended Use Case

Now suppose you already have *-skeleton.tsx files and want to also create *-fallback.tsx versions, but only for the original files (i.e. not for files that are already -skeleton.tsx or -fallback.tsx).

Example Input Directory

some-directory/
  β”œβ”€β”€ A.tsx
  β”œβ”€β”€ A-skeleton.tsx
  β”œβ”€β”€ B.tsx
  β”œβ”€β”€ B-skeleton.tsx
  β”œβ”€β”€ C.tsx
  β”œβ”€β”€ C-skeleton.tsx
  β”œβ”€β”€ D.tsx
  └── D-skeleton.tsx

Desired Outcome

some-directory/
  β”œβ”€β”€ A.tsx
  β”œβ”€β”€ A-skeleton.tsx
  β”œβ”€β”€ A-fallback.tsx
  β”œβ”€β”€ B.tsx
  β”œβ”€β”€ B-skeleton.tsx
  β”œβ”€β”€ B-fallback.tsx
  β”œβ”€β”€ C.tsx
  β”œβ”€β”€ C-skeleton.tsx
  β”œβ”€β”€ C-fallback.tsx
  β”œβ”€β”€ D.tsx
  β”œβ”€β”€ D-skeleton.tsx
  └── D-fallback.tsx

βœ… Zsh Command

for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx && $f != *-fallback.tsx ]] && touch "${f%.tsx}-fallback.tsx"
done
for f in *.tsx(.N); do [[ $f != *-skeleton.tsx && $f != *-fallback.tsx ]] && touch "${f%.tsx}-fallback.tsx"; done\n

πŸ” Explanation

  • Adds a second conditional: [[ $f != *-skeleton.tsx && $f != *-fallback.tsx ]]

    • Ensures you're only acting on original *.tsx files, not previously generated skeleton or fallback versions.
  • Creates *-fallback.tsx files using the same basename as the source file.


πŸ› οΈ Notes

  • These examples assume you are running the command inside the some-directory folder.
  • You can adjust the script to run recursively or across multiple folders with additional scripting logic (e.g., find, zargs, or custom scripts).
  • Ensure you're using Zsh, not Bash, since glob qualifiers like (.N) are specific to Zsh.

πŸ“¦ Bonus: Combine Both Steps

# Create -skeleton.tsx files
for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx ]] && touch "${f%.tsx}-skeleton.tsx"
done

# Create -fallback.tsx files
for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx && $f != *-fallback.tsx ]] && touch "${f%.tsx}-fallback.tsx"
done

πŸ“ Scenario 3: Duplicate *.tsx Files and Copy Content (*-skeleton.tsx, *-fallback.tsx)

Instead of creating empty files, this variation copies the original contents of each .tsx file into its -skeleton.tsx and/or -fallback.tsx counterparts.

βœ… Zsh Command

# Create -skeleton.tsx files with copied content
for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx ]] && cp "$f" "${f%.tsx}-skeleton.tsx"
done

# Create -fallback.tsx files with copied content
for f in *.tsx(.N); do
  [[ $f != *-skeleton.tsx && $f != *-fallback.tsx ]] && cp "$f" "${f%.tsx}-fallback.tsx"
done

πŸ” Explanation

  • cp "$f" "${f%.tsx}-skeleton.tsx": Uses cp instead of touch to copy the file contents into the new filename.
  • Maintains the same logic to skip already existing skeleton or fallback files.

πŸ“ Scenario 4: Recursively Duplicate Files in Subdirectories

In this advanced case, the duplication should occur recursively, affecting all .tsx files in some-directory and its subfolders.

This version uses find and zsh-compatible substitution to replicate the previous behaviour across a nested structure.

βœ… Recursive with touch (empty files)

find some-directory -type f -name '*.tsx' ! -name '*-skeleton.tsx' -exec sh -c 'for f; do touch "${f%.tsx}-skeleton.tsx"; done' _ {} +
find some-directory -type f -name '*.tsx' ! -name '*-skeleton.tsx' ! -name '*-fallback.tsx' -exec sh -c 'for f; do touch "${f%.tsx}-fallback.tsx"; done' _ {} +

βœ… Recursive with cp (copy content)

find some-directory -type f -name '*.tsx' ! -name '*-skeleton.tsx' -exec sh -c 'for f; do cp "$f" "${f%.tsx}-skeleton.tsx"; done' _ {} +
find some-directory -type f -name '*.tsx' ! -name '*-skeleton.tsx' ! -name '*-fallback.tsx' -exec sh -c 'for f; do cp "$f" "${f%.tsx}-fallback.tsx"; done' _ {} +

πŸ” Explanation

  • find some-directory -type f -name '*.tsx': Recursively finds .tsx files.
  • ! -name '*-skeleton.tsx': Excludes skeleton files.
  • -exec sh -c '...' _ {} +: Batches the matched files into a mini shell where you loop over them and apply the copy or touch logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment