Skip to content

File and Directory Commands

Server administration is full of ordinary file operations: checking a log directory, copying a configuration file, moving a release into place, or removing temporary files. The risk is rarely that the command is mysterious. The risk is that the right command is run in the wrong directory, against the wrong path, or with options that change more than intended.

Work through this article only inside ~/lab/file-commands. The concepts from How Shell and Bash Work and Command Syntax and Help Documentation are assumed: quoting, type, paths, and exit status.

Prepare a Safe Lab

mkdir -p ~/lab/file-commands
cd ~/lab/file-commands
pwd

Expected output:

/home/<username>/lab/file-commands

Replace <username> mentally with your real account name. These exercises do not need sudo. If you use sudo, you may create root-owned files in your home directory and make the lab harder to clean up.

Paths and Navigation

An absolute path starts at /:

ls -ld /var/log

A relative path starts from the current working directory:

cd /var
ls -ld log

Both can refer to the same object, but the relative form depends on where the process is standing.

Useful navigation:

pwd
cd /var/log
pwd
cd -
pwd
cd ~/lab/file-commands

cd - returns to the previous working directory. It is not the same as cd .., which moves to the parent directory.

Common path shortcuts:

  • . means the current directory.
  • .. means the parent directory.
  • ~ is expanded by Bash to your home directory.
  • / is the directory separator and the filesystem root.

Linux filenames are case-sensitive on common server filesystems. Reports, reports, and REPORTS can be three different names.

Listing and Inspecting Files

Start with:

ls
ls -lah
ls -ld .

ls -lah means:

  • -l: long listing.
  • -a: include hidden names beginning with ..
  • -h: human-readable sizes when used with long output.

ls -ld . lists the directory itself, not its contents.

Sample long output:

drwxr-xr-x 3 admin admin 4096 Jul 29 10:20 .
-rw-r--r-- 1 admin admin   24 Jul 29 10:21 app.conf

Read it from left to right:

  • First character: object type, such as d for directory or - for regular file.
  • Permission bits: owner, group, and other access.
  • Link count.
  • Owner and group.
  • Size.
  • Modification time.
  • Name.

The link count is the number of directory entries (names) that point to the same inode, not a count of copies of the file. For an ordinary file that has never been hard-linked, this is 1. For a directory, it starts at 2 even when empty: one entry is the directory's name in its parent, the other is . inside the directory itself, pointing back to the same inode. Each subdirectory adds one more, because that subdirectory's .. also points back to the parent:

mkdir -p project/count-demo/child
stat -c 'links=%h name=%n' project/count-demo
links=3 name=project/count-demo

count-demo shows 3: itself, its own ., and child/... A file's link count above 1 means more than one filename refers to the identical data — see Hard Links vs Symbolic Links below.

Color is convenient but not authoritative. It depends on aliases and LS_COLORS. For more reliable inspection:

file /etc/hosts
stat /etc/hosts

file guesses content type. stat shows metadata such as inode, ownership, mode, size, and timestamps.

Hidden Files

Names beginning with . are hidden from plain ls output:

touch .env.example
ls
ls -A

ls -A shows hidden names but omits the implied . and .. entries. Hidden files are not protected by special security rules; they are hidden by naming convention only.

Creating Files and Directories

Create a small project tree:

mkdir -p project/{config,logs,backup}
touch project/config/app.conf
find project -maxdepth 2 -print

Expected shape:

project
project/config
project/config/app.conf
project/logs
project/backup

mkdir -p creates missing parent directories and does not fail when the target directory already exists. The {config,logs,backup} part is Bash brace expansion; Bash turns it into three arguments before mkdir runs.

touch creates an empty file if it does not exist. If the file already exists, it normally updates timestamps.

Add content:

printf 'port=8080\n' > project/config/app.conf
printf 'mode=lab\n' >> project/config/app.conf
cat project/config/app.conf

Output:

port=8080
mode=lab

Warning

> truncates an existing file before the command writes new output. That is useful in a lab and dangerous on production configuration files. Make a backup and verify the target before using redirection on important files.

Remove an empty directory:

mkdir project/empty
rmdir project/empty
test ! -e project/empty && echo "empty directory removed"

rmdir refuses to remove non-empty directories. That limitation is a safety feature.

Copying with cp

The common forms are:

cp [OPTION]... SOURCE DEST
cp [OPTION]... SOURCE... DIRECTORY

Copy a file and verify it:

cp -i project/config/app.conf project/backup/app.conf
cmp project/config/app.conf project/backup/app.conf
echo "cmp status=$?"

If cmp prints nothing and returns 0, the two files are byte-for-byte identical.

Copy a directory tree:

cp -a project/config project/backup/config-snapshot
find project/backup -maxdepth 3 -print

GNU cp -a means archive mode. It copies recursively and preserves attributes where possible. cp -r is recursive too, but it does not express the same preservation intent.

Destination rules matter:

mkdir source existing-directory
touch source/file.txt
cp -a source new-name
cp -a source existing-directory/
find . -maxdepth 3 -print

If new-name did not exist, it becomes a copy of source. If the destination is an existing directory, source is copied inside it. Check with ls -ld -- <path> before copying into a path that might already exist.

Moving and Renaming with mv

Rename a file:

mv -i project/backup/app.conf project/backup/app.conf.old
test -f project/backup/app.conf.old && echo "new name exists"
test ! -e project/backup/app.conf && echo "old name is gone"

mv can rename within one directory or move to another directory. If the destination exists, it may overwrite it; -i asks first.

On one filesystem, a rename is usually fast and atomic. Across filesystems, mv behaves more like copy-then-delete. For large data, consider available disk space, interruption, and rollback before moving.

cp makes a second, independent copy of the data. A link makes a second name for the same data, and Linux gives you two different kinds with different failure behavior.

A hard link is a second directory entry pointing at the same inode:

printf 'v1\n' > project/config/app.conf
ln project/config/app.conf project/config/app.conf.hardlink
stat -c 'links=%h inode=%i name=%n' project/config/app.conf project/config/app.conf.hardlink
links=2 inode=... name=project/config/app.conf
links=2 inode=... name=project/config/app.conf.hardlink

Both names now report the same inode number and a link count of 2. Writing through either name changes the same underlying data:

printf 'v2\n' > project/config/app.conf
cat project/config/app.conf.hardlink
v2

Deleting one name only removes that directory entry; the data survives under the remaining name until the link count reaches 0. This is exactly why rm is described as removing a name, not "deleting a file" in the strict sense — the underlying data is only freed once its last link and every process holding it open are gone.

A symbolic link is a different kind of object: a small file whose content is a path, resolved by the kernel each time it is accessed:

ln -s app.conf project/config/app.conf.symlink
ls -l project/config/app.conf.symlink
stat -c 'links=%h inode=%i name=%n' project/config/app.conf.symlink
lrwxrwxrwx 1 admin admin 9 Jul 29 10:22 project/config/app.conf.symlink -> app.conf
links=1 inode=... name=project/config/app.conf.symlink

The leading l in ls -l and the -> target are the signal that this is a symlink, not a regular file. Its own link count and inode are unrelated to the target's.

The practical differences, and the ones most likely to come up in an interview or bite you in production:

  • Deleting the target. Remove app.conf and the hard link app.conf.hardlink still works — it is not "the target," it is an equal name for the same data. The symlink app.conf.symlink becomes a dangling link: ls still lists it, but opening it fails with No such file or directory.
  • Crossing filesystems. A hard link must point to an inode on the same filesystem, because inode numbers are only unique within one filesystem; ln refuses across filesystems. A symlink is just a stored path string, so it can point anywhere, including across filesystems or to a path that does not exist yet.
  • Directories. You cannot hard-link a directory on Linux (this is disallowed to prevent loops in the directory tree). You can symlink a directory freely.
  • What rm needs to know. Removing a symlink with plain rm symlink-name removes only the link, never the target. Adding a trailing slash or trying to treat a symlink to a directory as if it were the directory itself is a common source of confusion — verify with ls -l -- <path> or readlink -f -- <path> before scripting around a link.
readlink project/config/app.conf.symlink
readlink -f project/config/app.conf.symlink
app.conf
/home/<username>/lab/file-commands/project/config/app.conf

readlink without -f shows the stored target text exactly as written. readlink -f resolves it to a full absolute path, following the link — the version you want when a script needs to know what a symlink actually points to right now.

Names with Spaces, Wildcards, and Leading Dashes

Always quote paths that may contain spaces:

touch 'project/monthly report.txt'
ls -l -- 'project/monthly report.txt'

Use -- when a filename may begin with -:

touch -- -draft
ls -l -- -draft

Preview wildcard expansion before using a destructive command:

touch project/logs/app.log project/logs/error.log
echo project/logs/*.log

Bash expands *.log before the command starts. If no files match, the pattern may remain literal under default Bash settings. Do not assume a wildcard matched what you intended.

Removing Files

Remove one lab file interactively:

touch project/logs/test.log
ls -l -- project/logs/test.log
rm -i -- project/logs/test.log
test ! -e project/logs/test.log && echo "file removed"

Danger

Recursive deletion can permanently remove a directory tree. Before removing a directory, verify the full path with pwd, printf '<%s>\n' "$target", and find "$target" -maxdepth 2 -print. Do not use recursive forced deletion on a production server without a tested backup or snapshot and a rollback plan.

For the lab tree, inspect first:

target="$HOME/lab/file-commands/project"
printf 'target=<%s>\n' "$target"
find "$target" -maxdepth 3 -print

Then, only if the target is correct and inside your lab:

rm -rI -- "$target"
test ! -e "$target" && echo "lab project removed"

-r removes recursively. GNU -I asks once before removing many files and is less noisy than -i, but it is still a destructive command.

Reading Common File-Operation Errors

Most file-command failures fall into a small set of error messages. Recognizing which one you got narrows the diagnosis immediately.

cat project/does-not-exist.conf
cat: project/does-not-exist.conf: No such file or directory

The path does not exist at all — check for a typo, a wrong working directory (pwd first), or a step that failed earlier without you noticing.

touch /etc/example-not-allowed.conf
touch: cannot touch '/etc/example-not-allowed.conf': Permission denied

The path exists, or its parent directory does, but the current user's permission bits (or an ACL, if one is set) do not allow the operation. Check ownership and mode with ls -ld -- <path> and stat -- <path> before reaching for sudo — confirm you understand why access is denied, since routinely running as root to bypass this defeats the purpose of the permission system.

cat project
cat: project: Is a directory

The command expected a file but was given a directory. This is a common copy-paste mistake when a variable that should hold a file path ends up holding a directory path instead; file -- <path> or test -f -- <path> catches it before running a command that assumes a regular file.

rmdir project
rmdir: failed to remove 'project': Directory not empty

rmdir only removes empty directories, by design — see Remove an Empty Directory above. A non-empty target needs rm -r (or rm -rI), used only after previewing its contents with find.

Practical Scenario: Safely Update a Configuration Copy

Problem: you need to prepare a changed application config while preserving the original.

Create the original:

mkdir -p ~/lab/file-commands/scenario/{current,backup}
printf 'port=8080\nmode=production\n' > ~/lab/file-commands/scenario/current/app.conf

Back it up and verify:

cp -a ~/lab/file-commands/scenario/current/app.conf \
      ~/lab/file-commands/scenario/backup/app.conf.before-change
cmp ~/lab/file-commands/scenario/current/app.conf \
    ~/lab/file-commands/scenario/backup/app.conf.before-change
echo "backup status=$?"

Prepare a modified copy:

cp -a ~/lab/file-commands/scenario/current/app.conf \
      ~/lab/file-commands/scenario/current/app.conf.new
printf 'port=9090\nmode=production\n' > ~/lab/file-commands/scenario/current/app.conf.new
cat ~/lab/file-commands/scenario/current/app.conf.new

Replace only after inspection:

mv -i ~/lab/file-commands/scenario/current/app.conf.new \
      ~/lab/file-commands/scenario/current/app.conf
cat ~/lab/file-commands/scenario/current/app.conf

Conclusion: the workflow separates backup, edit, verification, and replacement. Real production services also need application-specific validation before reload or restart.

Common Mistakes

  • Running file commands from an unknown current directory.
  • Using sudo in a home-directory lab and creating root-owned files.
  • Confusing cd - with cd ...
  • Believing hidden files are protected. They are only hidden from default listing.
  • Copying a directory without checking whether the destination already exists.
  • Using unquoted variables for paths.
  • Running recursive deletion before previewing the target.
  • Assuming a symlink and its target are removed together, or that rm on a symlink deletes the data it points to.
  • Reacting to Permission denied by reaching for sudo before checking ownership and mode with ls -l.

Exercises

  1. Build ~/lab/file-commands/ex1/{input,output,tmp} with one command, then verify it with find.
  2. Create a file whose name contains a space. Show one failing unquoted command and one correct quoted command.
  3. Copy a directory with cp -a, verify with find, then rename the copy with mv -i.
  4. Create three .log files and preview the wildcard expansion with echo before deleting one of them interactively.
  5. Create a hard link and a symbolic link to the same file. Delete the original file and use cat on each link to show which one still works and which one is now dangling.
  6. Trigger No such file or directory, Permission denied, and Is a directory deliberately in your lab, and write one sentence for each explaining the cause before you fix it.

Verification criterion: after these exercises, you should be able to explain what object each command will modify before you run it.

References

  • GNU Coreutils manual: https://www.gnu.org/software/coreutils/manual/coreutils.html
  • ls invocation: https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html
  • cp invocation: https://www.gnu.org/software/coreutils/manual/html_node/cp-invocation.html
  • mv invocation: https://www.gnu.org/software/coreutils/manual/html_node/mv-invocation.html
  • rm invocation: https://www.gnu.org/software/coreutils/manual/html_node/rm-invocation.html
  • ln invocation: https://www.gnu.org/software/coreutils/manual/html_node/ln-invocation.html
  • stat invocation: https://www.gnu.org/software/coreutils/manual/html_node/stat-invocation.html
  • Filesystem Hierarchy Standard: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html