Skip to content

C Command-Line Arguments and Lab 01

Overview

This hands-on lab session walks through the full development workflow you will use all semester: writing C programs on the BeagleV machines, compiling them with gcc, organizing builds with a Makefile, and checking your work with the class autograder. The technical heart of the session is understanding how a C program receives command-line arguments through argc and argv, and how the compiler turns a .c source file into a runnable executable. By the end you will have written and submitted the two Lab 01 programs, hello and argslens, and demonstrated that your development environment is fully set up.

Learning Objectives

  • Compile and run C programs on the BeagleV machines using gcc
  • Explain the difference between one-step compilation and the two-step compile-then-link pipeline (.c.o → executable)
  • Read and accept command-line arguments in a C program using argc and argv
  • Convert string arguments to integers with atoi() and measure string length with strlen()
  • Write, build, and test the hello and argslens programs for Lab 01
  • Read a basic Makefile and add a new program target to it
  • Install, configure, and run the autograder (grade) on the BeagleV machines
  • Use git to add, commit, and push your code to your Lab 01 GitHub repository

Prerequisites

  • Passwordless SSH access to the BeagleV machines from your laptop
  • A terminal-based editor other than nano (micro, vim, or emacs)
  • A GitHub account whose ID has been added to the class spreadsheet
  • Basic command-line navigation (cd, ls, mkdir, pwd)
  • Familiarity with the contents of Lab 01

1. The Lab Workflow at a Glance

Lab 01 is the first time you put the entire toolchain together. Nothing here is conceptually hard, but each step has to work for the next one to succeed. The whole loop looks like this:

flowchart LR
    A[SSH to BeagleV] --> B[Clone Lab01 repo]
    B --> C[Edit .c files<br/>with micro/vim]
    C --> D[make]
    D --> E[Run program<br/>./hello World]
    E --> F{Output correct?}
    F -- no --> C
    F -- yes --> G[grade test]
    G --> H{Tests pass?}
    H -- no --> C
    H -- yes --> I[git add / commit / push]
    I --> J[Demo dev setup to TA]

There are two graded pieces in Lab 01:

Piece How it is graded What it checks
Lab01 Tests Autograder (grade test) hello and argslens produce exactly the expected output
Lab01 Dev Setup Interactive demo to Greg or a TA You can SSH, edit, compile, run, autograde, and use git

The rest of these notes follow the workflow above, in order.


2. Compiling a C Program with gcc

The first thing demonstrated in class was compiling the classic "Hello, World" program. The single most important command of the day is:

gcc -o hello hello.c

This reads as: "compile the source file hello.c and write the output executable to a file named hello." The -o hello part names the output. If you leave off -o, gcc writes the executable to the default name a.out, which is rarely what you want.

$ gcc -o hello hello.c     # produces an executable named "hello"
$ ./hello World
Hello, World!

Notice the leading ./ when you run the program. The ./ tells the shell to look for the executable in the current directory. Without it, the shell only searches the directories listed in your PATH, and your freshly built hello is not on that list.

A complete hello.c

Here is a minimal program that satisfies the Lab 01 hello requirement (we will revisit the argument handling in detail in Section 5):

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("usage: ./hello <str>\n");
        return 1;
    }
    printf("Hello, %s!\n", argv[1]);
    return 0;
}
$ gcc -o hello hello.c
$ ./hello World
Hello, World!
$ ./hello CS315
Hello, CS315!

The handwritten notes contrast two ways of getting from source code to an executable. Understanding the difference is essential for reading the Lab 01 Makefile.

The one-step view

When you run gcc -o hello hello.c, it looks like one operation, but gcc is actually doing several things behind the scenes: preprocessing, compiling to assembly, assembling to machine code, and linking. For a single source file, you never see the intermediate results.

   hello.c
      |
      |  gcc -o hello hello.c
      v
   hello          <-- runnable executable

You can ask gcc to stop after producing an object file (.o) and then link separately. This is the model make uses. The -c flag means "compile only, do not link."

   hello.c
      |
      |  gcc -c hello.c        (compile only)
      v
   hello.o        <-- object file (machine code, not yet runnable)
      |
      |  gcc -o hello hello.o  (link)
      v
   hello          <-- runnable executable
$ gcc -c hello.c          # produces hello.o (note: no -o needed, defaults to hello.o)
$ gcc -o hello hello.o    # links hello.o into the executable hello
$ ./hello World
Hello, World!

Why bother with two steps?

A single program with one source file does not benefit much. The payoff arrives in Project 01 and beyond, when programs share code across multiple files:

  • Faster rebuilds: if you change only argslens.c, only that file needs to be recompiled into argslens.o. Unchanged object files are reused.
  • Code sharing: a helper module compiled once into a .o can be linked into several different programs.

The object file (.o) is real machine code, but it is not runnable on its own. It is missing the C runtime startup code and, for multi-file programs, the definitions of functions it calls. Linking resolves those references and produces the final executable.

flowchart TD
    subgraph compile["Compile (per source file)"]
        A[hello.c] -->|gcc -c| B[hello.o]
    end
    subgraph link["Link (combine objects)"]
        B -->|gcc -o hello| C[hello executable]
        D[C runtime startup] --> C
    end

4. The Lab 01 Makefile

Typing the compile and link commands by hand gets tedious fast, and it is easy to forget a step. make automates it. You write a Makefile that declares what to build and how, and make figures out the when by comparing file timestamps.

Mental model

  • A target is a file you want to build (e.g., hello).
  • A target has prerequisites (the files it depends on, e.g., hello.o).
  • A target has a recipe (the shell commands that build it).
  • make rebuilds a target only if a prerequisite is newer than the target, or the target is missing.

The Makefile for Lab 01

.PHONY: all clean
CC      := gcc
CFLAGS  := -g -O2 -Wall -Wextra
LDFLAGS := -g

PROGS := hello argslens
HELLO_OBJS := hello.o
ARGSLENS_OBJS := argslens.o
OBJS := ${HELLO_OBJS} ${ARGSLENS_OBJS}

%.o: %.c
    ${CC} -c ${CFLAGS} -o $@ $<

all: ${PROGS}

hello: ${HELLO_OBJS}
    ${CC} ${LDFLAGS} -o $@ $^

argslens: ${ARGSLENS_OBJS}
    ${CC} ${LDFLAGS} -o $@ $^

clean:
    rm -rf ${PROGS} ${OBJS}

Reading it piece by piece

Line / block What it does
.PHONY: all clean Declares all and clean as targets that are not real files, so make always runs them when asked
CC := gcc The C compiler to use
CFLAGS := -g -O2 -Wall -Wextra Compile flags: debug info, optimization, and lots of warnings
LDFLAGS := -g Link flags: keep debug symbols in the executable
PROGS := hello argslens The two executables to build
%.o: %.c A pattern rule: how to turn any .c into the matching .o
all: ${PROGS} The default goal: build everything

The recipe lines use automatic variables. These are the most confusing part of make for newcomers, so commit them to memory:

Variable Meaning In hello: hello.o
$@ The target being built hello
$< The first prerequisite hello.o
$^ All prerequisites hello.o

So the pattern rule ${CC} -c ${CFLAGS} -o $@ $< expands, when building hello.o, to:

gcc -c -g -O2 -Wall -Wextra -o hello.o hello.c

And the link rule ${CC} ${LDFLAGS} -o $@ $^ expands to:

gcc -g -o hello hello.o

Tabs, not spaces. Every recipe line must begin with a real tab character. If your editor inserts spaces, make reports the cryptic error Makefile:NN: *** missing separator. Stop. This is the single most common Makefile mistake.

What make actually runs

$ make
gcc -c -g -O2 -Wall -Wextra -o hello.o hello.c
gcc -g -o hello hello.o
gcc -c -g -O2 -Wall -Wextra -o argslens.o argslens.c
gcc -g -o argslens argslens.o

Run make again immediately and nothing happens, because every target is up to date:

$ make
make: Nothing to be done for 'all'.

Edit argslens.c, and only that program rebuilds:

$ make
gcc -c -g -O2 -Wall -Wextra -o argslens.o argslens.c
gcc -g -o argslens argslens.o

Adding a new program

This is the modification you will make most often. To add a program foo:

  1. Write foo.c.
  2. Add foo to PROGS.
  3. Add FOO_OBJS := foo.o.
  4. Add a link rule:
foo: ${FOO_OBJS}
    ${CC} ${LDFLAGS} -o $@ $^

The pattern rule already knows how to compile foo.c into foo.o, so you do not need a compile rule.

Handy make commands

make            # build the default goal (all)
make clean      # delete executables and object files
make clean && make   # full rebuild from scratch
make -n         # dry run: print commands without running them

For more depth see the make guide.


5. Command-Line Arguments: argc and argv

This is the conceptual core of the session. Every C program's main can receive the words you type on the command line. The signature is:

int main(int argc, char *argv[])
  • argc ("argument count") is the number of command-line words, including the program name itself.
  • argv ("argument vector") is an array of strings holding those words. argv[0] is always the program name; argv[1], argv[2], ... are the user's arguments.

You may also see argv written as char **argv; for our purposes char *argv[] and char **argv mean the same thing.

A concrete example

Suppose you run:

$ ./args foo cs315 computer architecture

The shell splits that line on whitespace and hands the pieces to your program like this:

argc = 5

argv[0] ──> "./args"
argv[1] ──> "foo"
argv[2] ──> "cs315"
argv[3] ──> "computer"
argv[4] ──> "architecture"
argv[5] ──> NULL          (the array is NULL-terminated)

A useful mental picture: argv is an array of pointers, each pointing at a NUL-terminated string somewhere in memory.

   argv
  +------+        +-------------------+
  |  [0] | -----> | '.' '/' 'a' 'r' 'g' 's' '\0' |
  +------+        +-------------------+
  |  [1] | -----> | 'f' 'o' 'o' '\0' |
  +------+        +-------------------+
  |  [2] | -----> | 'c' 's' '3' '1' '5' '\0' |
  +------+        +-------------------+
  |  [3] | -----> | 'c' 'o' 'm' 'p' 'u' 't' 'e' 'r' '\0' |
  +------+        +-------------------+
  |  [4] | -----> | 'a' 'r' 'c' 'h' ... '\0' |
  +------+        +-------------------+
  |  [5] | -----> NULL
  +------+

The args.c program

This is the program developed in class (available in the inclass repo). It prints each argument on its own line:

#include <stdio.h>

int main(int argc, char *argv[]) {
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}
$ gcc -o args args.c
$ ./args foo cs315 computer architecture
argv[0] = ./args
argv[1] = foo
argv[2] = cs315
argv[3] = computer
argv[4] = architecture

The loop runs from 0 to argc - 1. Using i < argc (not i <= argc) is critical: index argc holds the terminating NULL, and printing it as a %s string is undefined behavior.

Checking the argument count

Most real programs require a specific number of arguments and should fail gracefully otherwise. The hello program needs exactly one user argument (so argc == 2, counting the program name):

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("usage: ./hello <str>\n");
        return 1;          // non-zero return signals an error
    }
    printf("Hello, %s!\n", argv[1]);
    return 0;              // zero return signals success
}

Returning 0 from main means success; any non-zero value means an error. The autograder and shell scripts rely on this convention.


6. Converting Argument Strings to Numbers

Everything in argv is a string, even when it looks like a number. If a user types ./repeat 3, then argv[1] is the two-byte string "3" (the character '3' followed by '\0'), not the integer 3.

To use it as a number, convert it with atoi() ("ASCII to integer") from <stdlib.h>:

#include <stdio.h>
#include <stdlib.h>     // for atoi

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("usage: ./echorepeat <str> <count>\n");
        return 1;
    }
    int count = atoi(argv[2]);   // convert the string "3" to the int 3
    for (int i = 0; i < count; i++) {
        printf("%s\n", argv[1]);
    }
    return 0;
}
$ gcc -o echorepeat echorepeat.c
$ ./echorepeat hi 3
hi
hi
hi

This echorepeat example was demonstrated in class. The instructor noted that the current version hard-codes some assumptions and will be improved in a later session, but it illustrates the essential idea: read a string argument, convert it, and use it.

Gotcha: atoi returns 0 for input it cannot parse (e.g., atoi("abc") is 0), and it does not report errors. For robust input validation, programs use strtol instead, but atoi is fine for the labs.


7. Measuring String Length with strlen

The argslens program extends args.c by also printing the length in characters of each argument. The length is computed with strlen() from <string.h>.

strlen counts the characters in a string up to, but not including, the terminating '\0'. So strlen("foo") is 3.

Building argslens.c

Start from args.c, add #include <string.h>, and append the length to each line:

#include <stdio.h>
#include <string.h>      // for strlen

int main(int argc, char *argv[]) {
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = %s (%zu)\n", i, argv[i], strlen(argv[i]));
    }
    return 0;
}
$ gcc -o argslens argslens.c
$ ./argslens foo cs315 computer architecture
argv[0] = ./argslens (10)
argv[1] = foo (3)
argv[2] = cs315 (5)
argv[3] = computer (8)
argv[4] = architecture (12)

A note on the format specifier

strlen returns a value of type size_t (an unsigned integer type). The matching printf conversion is %zu. Using %d (for int) instead will compile, but with -Wall -Wextra the compiler warns you, and on some systems it prints the wrong value. Two acceptable approaches:

// Option A: use the correct specifier
printf("argv[%d] = %s (%zu)\n", i, argv[i], strlen(argv[i]));

// Option B: cast to int and use %d
printf("argv[%d] = %s (%d)\n", i, argv[i], (int)strlen(argv[i]));

Either matches the required output exactly. The point is to make the compiler warning go away, because the autograder expects clean, exact output.


8. Running the Autograder

The autograder, invoked as grade, runs your programs against a set of saved test cases and reports a score. It is the same tool that determines your Lab01 Tests grade, so running it yourself before submitting tells you exactly where you stand.

Installing the autograder on BeagleV

The autograder works out of the box on the BeagleV machines. You only need to clone it and put it on your PATH:

$ ssh beagle
$ mkdir cs315
$ cd cs315
$ git clone https://github.com/phpeterson-usf/autograder

Then add it to your PATH by editing ~/.bash_profile. If the file does not exist, create it with your editor (micro ~/.bash_profile):

export PATH=~/cs315/autograder:$PATH

Log out and back in (or source ~/.bash_profile) so the change takes effect. Confirm it worked:

$ which grade
/home/you/cs315/autograder/grade

Pointing the autograder at the tests repo

You also need to clone the class tests repo and configure the autograder to find it. The tests live at https://github.com/USF-CS315-F25/tests.

Running the tests

From inside your lab01 directory (where your built hello and argslens live), run:

$ grade test
. 01(25/25) 02(25/25) 03(25/25) 04(25/25) 100/100

A perfect score is 100/100. Each test compares your program's output byte for byte against the expected output. This is why exact formatting matters: a missing exclamation point, a wrong space, or a trailing blank line will fail a test even though the output "looks right."

flowchart LR
    A[grade test] --> B[Build your programs]
    B --> C[Run with test inputs]
    C --> D[Compare output<br/>to expected]
    D --> E{Match exactly?}
    E -- yes --> F[Test passes]
    E -- no --> G[Test fails<br/>shows diff]

9. Submitting with Git

Once your programs pass, commit and push them to your Lab 01 GitHub repo. The basic cycle:

$ git add Makefile hello.c argslens.c
$ git commit -m "Lab01: hello and argslens"
$ git push

What to submit (and what NOT to submit)

Submit Do NOT submit
Makefile executables (hello, argslens)
hello.c object files (*.o)
argslens.c anything make generates
README.md (optional) a.out

In short: submit source, never build products. A make clean before committing is a good habit so you do not accidentally git add a binary. If you already committed binaries, run make clean, then git add -A, commit, and push.


10. Common Mistakes and How to Fix Them

These are the issues that come up most often in lab. Scan this list before asking for help; the fix is usually here.

Symptom Likely cause Fix
command not found: hello Ran hello instead of ./hello Prefix with ./ to run from current directory
Makefile:5: *** missing separator. Stop. Recipe indented with spaces Replace the leading spaces with a real tab
undefined reference to 'main' Compiled with -c but tried to run the .o Link the object: gcc -o hello hello.o
implicit declaration of 'strlen' Missing #include <string.h> Add the header
implicit declaration of 'atoi' Missing #include <stdlib.h> Add the header
Autograder fails a passing-looking test Output not byte-exact (spacing, !, newline) Match the spec's output character for character
Segfault printing arguments Loop ran to i <= argc, hit NULL Use i < argc
which grade finds nothing PATH not updated Edit ~/.bash_profile, then re-login
Wrong length printed in argslens Used %d for size_t Use %zu or cast (int)strlen(...)
Committed hello/*.o to GitHub Forgot make clean make clean, then commit the deletions

Key Concepts

Concept Definition Example
argc Count of command-line arguments, including the program name ./args a bargc == 3
argv Array of argument strings; argv[0] is the program name argv[1] == "a"
Object file Compiled machine code for one source file, not yet runnable hello.o
Linking Combining object files + runtime into an executable gcc -o hello hello.o
-c flag Compile only; produce a .o, do not link gcc -c hello.c
-o flag Name the output file gcc -o hello hello.c
strlen Returns the number of characters before '\0' strlen("foo") == 3
atoi Converts a numeric string to an int atoi("42") == 42
Pattern rule Generic Makefile rule for a class of files %.o: %.c
$@ / $< / $^ Make automatic vars: target / first prereq / all prereqs In hello: hello.o, $@=hello, $<=hello.o
Phony target A Make target that is not a file .PHONY: all clean
Exit status Return value of main; 0 = success return 1; on error

Practice Problems

Problem 1: Predict argc and argv

A user runs:

$ ./greet Hello there CS315

What is the value of argc, and what does each element of argv hold?

Click to reveal solution
argc = 4

argv[0] = "./greet"
argv[1] = "Hello"
argv[2] = "there"
argv[3] = "CS315"
argv[4] = NULL
The shell splits the line on whitespace. `argv[0]` is always the program name, so four words means `argc == 4`. Index `4` holds the terminating `NULL` and must not be printed as a string.

Problem 2: Spot the off-by-one bug

This loop is supposed to print every argument but crashes. Why?

for (int i = 0; i <= argc; i++) {
    printf("argv[%d] = %s\n", i, argv[i]);
}
Click to reveal solution The condition should be `i < argc`, not `i <= argc`. When `i == argc`, the loop accesses `argv[argc]`, which is the terminating `NULL` pointer. Passing `NULL` to `printf` with `%s` dereferences a null pointer, which is undefined behavior and typically causes a segmentation fault. **Fix:**
for (int i = 0; i < argc; i++) {
    printf("argv[%d] = %s\n", i, argv[i]);
}

Problem 3: Convert one-step into two steps

You currently build with one command:

gcc -o argslens argslens.c

Rewrite this as the two-step compile-then-link process, and explain what intermediate file is produced.

Click to reveal solution
gcc -c argslens.c          # produces argslens.o (compile only)
gcc -o argslens argslens.o # links argslens.o into the executable
The intermediate file is the **object file** `argslens.o`. It contains compiled machine code but is not runnable on its own because it still needs the C runtime startup code, which is added during the link step. This two-step model is exactly what the Makefile automates: a pattern rule for compiling and an explicit link rule per program.

Problem 4: Add a program to the Makefile

You wrote a new program wordcount.c. Show the lines you must add to the Lab 01 Makefile so that make wordcount works.

Click to reveal solution
PROGS := hello argslens wordcount        # add wordcount here
WORDCOUNT_OBJS := wordcount.o            # new object list

wordcount: ${WORDCOUNT_OBJS}             # new link rule
    ${CC} ${LDFLAGS} -o $@ $^
You do **not** need a compile rule. The existing pattern rule `%.o: %.c` already knows how to build `wordcount.o` from `wordcount.c`. Remember that the recipe line under `wordcount:` must start with a real tab.

Problem 5: Fix the argslens length output

A student's argslens prints the right strings but the lengths come out as garbage numbers, and the compiler warns about a format mismatch. Their line is:

printf("argv[%d] = %s (%d)\n", i, argv[i], strlen(argv[i]));

What is wrong, and how do you fix it two different ways?

Click to reveal solution `strlen` returns a `size_t` (unsigned), but `%d` expects an `int`. The mismatch causes the warning and can produce wrong output. **Fix A — use the correct specifier:**
printf("argv[%d] = %s (%zu)\n", i, argv[i], strlen(argv[i]));
**Fix B — cast to int:**
printf("argv[%d] = %s (%d)\n", i, argv[i], (int)strlen(argv[i]));
Both compile cleanly under `-Wall -Wextra` and produce the exact output the autograder expects.

Problem 6: Why does the autograder fail a "correct" program?

Your hello prints Hello, World and you think that is right, but grade test fails the test. The spec shows Hello, World!. What general lesson does this teach about the autograder?

Click to reveal solution The output is missing the trailing `!`. The autograder compares your program's output to the expected output **byte for byte**. Any difference fails the test, even one that looks trivial to a human: - a missing or extra `!` - a wrong number of spaces - a missing or extra newline (`\n`) - printing to `stderr` instead of `stdout` **Lesson:** match the spec's sample output exactly, including punctuation and whitespace. When a test fails, compare your output against the expected output character by character (the autograder shows the diff). **Fix:**
printf("Hello, %s!\n", argv[1]);

Further Reading


Summary

  1. gcc -o hello hello.c compiles and links in one command; ./hello runs the result from the current directory.

  2. Compilation is really two steps: a source file compiles to an object file (gcc -c.o), and object files link into a runnable executable. A .o is machine code but is not runnable by itself.

  3. make automates the compile-and-link pipeline; it rebuilds only what changed, using pattern rules and the automatic variables $@, $<, and $^. Recipe lines must start with a tab.

  4. main(int argc, char *argv[]) receives command-line arguments: argc is the count (including the program name), and argv is the array of argument strings, with argv[0] being the program name.

  5. Arguments are always strings. Use atoi() (from <stdlib.h>) to convert a numeric argument to an int, and strlen() (from <string.h>) to measure a string's length, printed with %zu.

  6. The two Lab 01 programs are hello (prints Hello, <str>!) and argslens (prints each argument with its length), both built from the provided Makefile.

  7. The autograder (grade test) scores your programs against byte-exact expected output; install it on your PATH and run it before submitting.

  8. Submit source only (Makefile, hello.c, argslens.c, optional README.md) to your Lab 01 GitHub repo, never executables or object files, and demonstrate your dev setup to a TA.