diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2ab768e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# See https://git-scm.com/docs/gitattributes +# See https://help.github.com/articles/dealing-with-line-endings/ + +# Default behavior, if core.autocrlf is unset. +* text=auto + +# Files to be converted to native line endings on checkout. +*.cpp text +*.h text + +# Text files to always have CRLF (dos) line endings on checkout. +*.bat text eol=crlf + +# Text files to always have LF (unix) line endings on checkout. +*.sh text eol=lf +*.zig text eol=lf + diff --git a/.gitignore b/.gitignore index b72f9be..e871e84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ *.swp +zig-cache/ diff --git a/README.md b/README.md index 69287dd..f560284 100644 --- a/README.md +++ b/README.md @@ -43,20 +43,35 @@ git clone https://github.com/ratfactor/ziglings cd ziglings ``` -Then run the `ziglings` script and follow the instructions to begin! +Then run `zig build` and follow the instructions to begin! ```bash -./ziglings +zig build ``` ## Manual Usage -If you can't (or don't want to) use the script, you can manually verify each -exercise with the Zig compiler: +If you want to run a single file for testing, you can do so with this command: ```bash zig run exercises/01_hello.zig ``` +or, alternatively +```bash +zig build 01_test +``` + +To verify a single file, use + +```bash +zig build 01_only +``` + +To prepare an executable for debugging, install it to zig-cache/bin with + +```bash +zig build 01_install +``` ## TODO @@ -66,7 +81,6 @@ the learning resource I wished for. There will be tons of room for improvement: * Wording of explanations * Idiomatic usage of Zig * Additional exercises -* Re-write the `ziglings` script using the Zig build system (or just a Zig application) Planned exercises: diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3d86ece --- /dev/null +++ b/build.zig @@ -0,0 +1,536 @@ +const std = @import("std"); +const Builder = std.build.Builder; +const Step = std.build.Step; +const assert = std.debug.assert; +const print = std.debug.print; + +const Exercise = struct { + /// main_file must have the format key_name.zig. + /// The key will be used as a shorthand to build + /// just one example. + main_file: []const u8, + + /// This is the desired output of the program. + /// A program passes if its output ends with this string. + output: []const u8, + + /// This is an optional hint to give if the program does not succeed. + hint: []const u8 = "", + + /// By default, we verify output against stderr. + /// Set this to true to check stdout instead. + check_stdout: bool = false, + + /// Returns the name of the main file with .zig stripped. + pub fn baseName(self: Exercise) []const u8 { + assert(std.mem.endsWith(u8, self.main_file, ".zig")); + return self.main_file[0 .. self.main_file.len - 4]; + } + + /// Returns the key of the main file, which is the text before the _. + /// For example, "01_hello.zig" has the key "01". + pub fn key(self: Exercise) []const u8 { + const end_index = std.mem.indexOfScalar(u8, self.main_file, '_'); + assert(end_index != null); // main file must be key_description.zig + return self.main_file[0..end_index.?]; + } +}; + +const exercises = [_]Exercise{ + .{ + .main_file = "01_hello.zig", + .output = "Hello world", + .hint = "Note the error: the source file has a hint for fixing 'main'.", + }, + .{ + .main_file = "02_std.zig", + .output = "Standard Library", + }, + .{ + .main_file = "03_assignment.zig", + .output = "55 314159 -11", + .hint = "There are three mistakes in this one!", + }, + .{ + .main_file = "04_arrays.zig", + .output = "Fourth: 7, Length: 8", + .hint = "There are two things to complete here.", + }, + .{ + .main_file = "05_arrays2.zig", + .output = "LEET: 1337, Bits: 100110011001", + .hint = "Fill in the two arrays.", + }, + .{ + .main_file = "06_strings.zig", + .output = "d=d ha ha ha Major Tom", + .hint = "Each '???' needs something filled in.", + }, + .{ + .main_file = "07_strings2.zig", + .output = "Ziggy", + .hint = "Please fix the lyrics!", + }, + .{ + .main_file = "08_quiz.zig", + .output = "Program in Zig", + .hint = "See if you can fix the program!", + }, + .{ + .main_file = "09_if.zig", + .output = "Foo is 1!", + }, + .{ + .main_file = "10_if2.zig", + .output = "price is $17", + }, + .{ + .main_file = "11_while.zig", + .output = "n=1024", + .hint = "You probably want a 'less than' condition.", + }, + .{ + .main_file = "12_while2.zig", + .output = "n=1024", + .hint = "It might help to look back at the previous exercise.", + }, + .{ + .main_file = "13_while3.zig", + .output = "1 2 4 7 8 11 13 14 16 17 19", + }, + .{ + .main_file = "14_while4.zig", + .output = "n=4", + }, + .{ + .main_file = "15_for.zig", + .output = "A Dramatic Story: :-) :-) :-( :-| :-) The End.", + }, + .{ + .main_file = "16_for2.zig", + .output = "13", + }, + .{ + .main_file = "17_quiz2.zig", + .output = "8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16", + .hint = "This is a famous game!", + }, + .{ + .main_file = "18_functions.zig", + .output = "Question: 42", + .hint = "Can you help write the function?", + }, + .{ + .main_file = "19_functions2.zig", + .output = "2 4 8 16", + }, + .{ + .main_file = "20_quiz3.zig", + .output = "32 64 128 256", + .hint = "Unexpected pop quiz! Help!", + }, + .{ + .main_file = "21_errors.zig", + .output = "2<4. 3<4. 4=4. 5>4. 6>4.", + .hint = "What's the deal with fours?", + }, + .{ + .main_file = "22_errors2.zig", + .output = "I compiled", + .hint = "Get the error union type right to allow this to compile.", + }, + .{ + .main_file = "23_errors3.zig", + .output = "a=64, b=22", + }, + .{ + .main_file = "24_errors4.zig", + .output = "a=20, b=14, c=10", + }, + .{ + .main_file = "25_errors5.zig", + .output = "a=0, b=19, c=0", + }, + .{ + .main_file = "26_hello2.zig", + .output = "Hello world", + .hint = "Try using a try!", + .check_stdout = true, + }, + .{ + .main_file = "27_defer.zig", + .output = "One Two", + }, + .{ + .main_file = "28_defer2.zig", + .output = "(Goat) (Cat) (Dog) (Dog) (Goat) (Unknown) done.", + }, + .{ + .main_file = "29_errdefer.zig", + .output = "Getting number...got 5. Getting number...failed!", + }, + .{ + .main_file = "30_switch.zig", + .output = "ZIG?", + }, + .{ + .main_file = "31_switch2.zig", + .output = "ZIG!", + }, + .{ + .main_file = "32_unreachable.zig", + .output = "1 2 3 9 8 7", + }, + .{ + .main_file = "33_iferror.zig", + .output = "2<4. 3<4. 4=4. 5>4. 6>4.", + .hint = "Seriously, what's the deal with fours?", + }, + .{ + .main_file = "34_quiz4.zig", + .output = "my_num=42", + .hint = "Can you make this work?", + .check_stdout = true, + }, + .{ + .main_file = "35_enums.zig", + .output = "1 2 3 9 8 7", + .hint = "This problem seems familiar...", + }, + .{ + .main_file = "36_enums2.zig", + .output = "#0000ff", + .hint = "I'm feeling blue about this.", + }, + .{ + .main_file = "37_structs.zig", + .output = "Your wizard has 90 health and 25 gold.", + }, + .{ + .main_file = "38_structs2.zig", + .output = "Character 2 - G:10 H:100 XP:20", + }, + .{ + .main_file = "39_pointers.zig", + .output = "num1: 5, num2: 5", + .hint = "Pointers aren't so bad.", + }, + .{ + .main_file = "40_pointers2.zig", + .output = "a: 12, b: 12", + }, + .{ + .main_file = "41_pointers3.zig", + .output = "foo=6, bar=11", + }, + .{ + .main_file = "42_pointers4.zig", + .output = "num: 5, more_nums: 1 1 5 1", + }, + .{ + .main_file = "43_pointers5.zig", + .output = "Wizard (G:10 H:100 XP:20)", + }, + .{ + .main_file = "44_quiz5.zig", + .output = "Elephant A. Elephant B. Elephant C.", + .hint = "Oh no! We forgot Elephant B!", + }, +}; + +/// Check the zig version to make sure it can compile the examples properly. +/// This will compile with Zig 0.6.0 and later. +fn checkVersion() bool { + if (!@hasDecl(std.builtin, "zig_version")) { + return false; + } + + const needed_version = std.SemanticVersion.parse("0.8.0-dev.1065") catch unreachable; + const version = std.builtin.zig_version; + const order = version.order(needed_version); + return order != .lt; +} + +pub fn build(b: *Builder) void { + // Use a comptime branch for the version check. + // If this fails, code after this block is not compiled. + // It is parsed though, so versions of zig from before 0.6.0 + // cannot do the version check and will just fail to compile. + // We could fix this by moving the ziglings code to a separate file, + // but 0.5.0 was a long time ago, it is unlikely that anyone who + // attempts these exercises is still using it. + if (comptime !checkVersion()) { + // very old versions of Zig used warn instead of print. + const stderrPrintFn = if (@hasDecl(std.debug, "print")) std.debug.print else std.debug.warn; + stderrPrintFn( + \\Error: Your version of zig is too old. Please download a master build from + \\https://ziglang.org/download/ + \\ + , .{}); + return; + } + + use_color_escapes = false; + switch (b.color) { + .on => use_color_escapes = true, + .off => use_color_escapes = false, + .auto => { + if (std.io.getStdErr().supportsAnsiEscapeCodes()) { + use_color_escapes = true; + } else if (std.builtin.os.tag == .windows) { + const w32 = struct { + const WINAPI = std.os.windows.WINAPI; + const DWORD = std.os.windows.DWORD; + const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + const STD_ERROR_HANDLE = @bitCast(DWORD, @as(i32, -12)); + extern "kernel32" fn GetStdHandle(id: DWORD) callconv(WINAPI) ?*c_void; + extern "kernel32" fn GetConsoleMode(console: ?*c_void, out_mode: *DWORD) callconv(WINAPI) u32; + extern "kernel32" fn SetConsoleMode(console: ?*c_void, mode: DWORD) callconv(WINAPI) u32; + }; + const handle = w32.GetStdHandle(w32.STD_ERROR_HANDLE); + var mode: w32.DWORD = 0; + if (w32.GetConsoleMode(handle, &mode) != 0) { + mode |= w32.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use_color_escapes = w32.SetConsoleMode(handle, mode) != 0; + } + } + }, + } + + if (use_color_escapes) { + red_text = "\x1b[31m"; + green_text = "\x1b[32m"; + bold_text = "\x1b[1m"; + reset_text = "\x1b[0m"; + } + + const header_step = b.addLog( + \\ + \\ _ _ _ + \\ ___(_) __ _| (_)_ __ __ _ ___ + \\ |_ | |/ _' | | | '_ \ / _' / __| + \\ / /| | (_| | | | | | | (_| \__ \ + \\ /___|_|\__, |_|_|_| |_|\__, |___/ + \\ |___/ |___/ + \\ + \\ + , .{}); + + const verify_all = b.step("ziglings", "Verify all ziglings"); + verify_all.dependOn(&header_step.step); + b.default_step = verify_all; + + var prev_chain_verify = verify_all; + + for (exercises) |ex| { + const base_name = ex.baseName(); + const file_path = std.fs.path.join(b.allocator, &[_][]const u8{ + "exercises", ex.main_file, + }) catch unreachable; + const build_step = b.addExecutable(base_name, file_path); + build_step.install(); + + const verify_step = ZiglingStep.create(b, ex); + + const key = ex.key(); + + const named_test = b.step(b.fmt("{s}_test", .{key}), b.fmt("Run {s} without verifying output", .{ex.main_file})); + const run_step = build_step.run(); + named_test.dependOn(&run_step.step); + + const named_install = b.step(b.fmt("{s}_install", .{key}), b.fmt("Install {s} to zig-cache/bin", .{ex.main_file})); + named_install.dependOn(&build_step.install_step.?.step); + + const named_verify = b.step(b.fmt("{s}_only", .{key}), b.fmt("Verify {s} only", .{ex.main_file})); + named_verify.dependOn(&verify_step.step); + + const chain_verify = b.allocator.create(Step) catch unreachable; + chain_verify.* = Step.initNoOp(.Custom, b.fmt("chain {s}", .{key}), b.allocator); + chain_verify.dependOn(&verify_step.step); + + const named_chain = b.step(key, b.fmt("Verify all solutions starting at {s}", .{ex.main_file})); + named_chain.dependOn(&header_step.step); + named_chain.dependOn(chain_verify); + + prev_chain_verify.dependOn(chain_verify); + prev_chain_verify = chain_verify; + } +} + +var use_color_escapes = false; +var red_text: []const u8 = ""; +var green_text: []const u8 = ""; +var bold_text: []const u8 = ""; +var reset_text: []const u8 = ""; + +const ZiglingStep = struct { + step: Step, + exercise: Exercise, + builder: *Builder, + + pub fn create(builder: *Builder, exercise: Exercise) *@This() { + const self = builder.allocator.create(@This()) catch unreachable; + self.* = .{ + .step = Step.init(.Custom, exercise.main_file, builder.allocator, make), + .exercise = exercise, + .builder = builder, + }; + return self; + } + + fn make(step: *Step) anyerror!void { + const self = @fieldParentPtr(@This(), "step", step); + self.makeInternal() catch { + if (self.exercise.hint.len > 0) { + print("\n{s}hint: {s}{s}", .{ bold_text, self.exercise.hint, reset_text }); + } + + print("\n{s}Edit exercises/{s} and run this again.{s}", .{ red_text, self.exercise.main_file, reset_text }); + print("\n{s}To continue from this zigling, use this command:{s}\n {s}zig build {s}{s}\n", .{ red_text, reset_text, bold_text, self.exercise.key(), reset_text }); + std.os.exit(0); + }; + } + + fn makeInternal(self: *@This()) !void { + print("Compiling {s}...\n", .{self.exercise.main_file}); + + const exe_file = try self.doCompile(); + + print("Verifying {s}...\n", .{self.exercise.main_file}); + + const cwd = self.builder.build_root; + + const argv = [_][]const u8{exe_file}; + + const child = std.ChildProcess.init(&argv, self.builder.allocator) catch unreachable; + defer child.deinit(); + + child.cwd = cwd; + child.env_map = self.builder.env_map; + + child.stdin_behavior = .Inherit; + if (self.exercise.check_stdout) { + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Inherit; + } else { + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Pipe; + } + + child.spawn() catch |err| { + print("{s}Unable to spawn {s}: {s}{s}\n", .{ red_text, argv[0], @errorName(err), reset_text }); + return err; + }; + + // Allow up to 1 MB of stdout capture + const max_output_len = 1 * 1024 * 1024; + const output = if (self.exercise.check_stdout) + try child.stdout.?.reader().readAllAlloc(self.builder.allocator, max_output_len) + else + try child.stderr.?.reader().readAllAlloc(self.builder.allocator, max_output_len); + + // at this point stdout is closed, wait for the process to terminate + const term = child.wait() catch |err| { + print("{s}Unable to spawn {s}: {s}{s}\n", .{ red_text, argv[0], @errorName(err), reset_text }); + return err; + }; + + // make sure it exited cleanly. + switch (term) { + .Exited => |code| { + if (code != 0) { + print("{s}{s} exited with error code {d} (expected {d}){s}\n", .{ red_text, self.exercise.main_file, code, 0, reset_text }); + return error.BadExitCode; + } + }, + else => { + print("{s}{s} terminated unexpectedly{s}\n", .{ red_text, self.exercise.main_file, reset_text }); + return error.UnexpectedTermination; + }, + } + + // validate the output + if (std.mem.indexOf(u8, output, self.exercise.output) == null) { + print( + \\ + \\{s}----------- Expected this output -----------{s} + \\{s} + \\{s}----------- but found -----------{s} + \\{s} + \\{s}-----------{s} + \\ + , .{ red_text, reset_text, self.exercise.output, red_text, reset_text, output, red_text, reset_text }); + return error.InvalidOutput; + } + + print("{s}** PASSED **{s}\n", .{ green_text, reset_text }); + } + + // The normal compile step calls os.exit, so we can't use it as a library :( + // This is a stripped down copy of std.build.LibExeObjStep.make. + fn doCompile(self: *@This()) ![]const u8 { + const builder = self.builder; + + var zig_args = std.ArrayList([]const u8).init(builder.allocator); + defer zig_args.deinit(); + + zig_args.append(builder.zig_exe) catch unreachable; + zig_args.append("build-exe") catch unreachable; + + if (builder.color != .auto) { + zig_args.append("--color") catch unreachable; + zig_args.append(@tagName(builder.color)) catch unreachable; + } + + const zig_file = std.fs.path.join(builder.allocator, &[_][]const u8{ "exercises", self.exercise.main_file }) catch unreachable; + zig_args.append(builder.pathFromRoot(zig_file)) catch unreachable; + + zig_args.append("--cache-dir") catch unreachable; + zig_args.append(builder.pathFromRoot(builder.cache_root)) catch unreachable; + + zig_args.append("--enable-cache") catch unreachable; + + const argv = zig_args.items; + var code: u8 = undefined; + const output_dir_nl = builder.execAllowFail(argv, &code, .Inherit) catch |err| { + switch (err) { + error.FileNotFound => { + print("{s}{s}: Unable to spawn the following command: file not found{s}\n", .{ red_text, self.exercise.main_file, reset_text }); + for (argv) |v| print("{s} ", .{v}); + print("\n", .{}); + }, + error.ExitCodeFailure => { + print("{s}{s}: The following command exited with error code {}:{s}\n", .{ red_text, self.exercise.main_file, code, reset_text }); + for (argv) |v| print("{s} ", .{v}); + print("\n", .{}); + }, + error.ProcessTerminated => { + print("{s}{s}: The following command terminated unexpectedly:{s}\n", .{ red_text, self.exercise.main_file, reset_text }); + for (argv) |v| print("{s} ", .{v}); + print("\n", .{}); + }, + else => {}, + } + return err; + }; + const build_output_dir = std.mem.trimRight(u8, output_dir_nl, "\r\n"); + + const target_info = std.zig.system.NativeTargetInfo.detect( + builder.allocator, + .{}, + ) catch unreachable; + const target = target_info.target; + + const file_name = std.zig.binNameAlloc(builder.allocator, .{ + .root_name = self.exercise.baseName(), + .target = target, + .output_mode = .Exe, + .link_mode = .Static, + .version = null, + }) catch unreachable; + + return std.fs.path.join(builder.allocator, &[_][]const u8{ + build_output_dir, file_name, + }); + } +}; diff --git a/ziglings b/ziglings deleted file mode 100755 index 7c2f6a1..0000000 --- a/ziglings +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash - -# ziglings takes one parameter: the exercise number to jump to -jump_to=${1:-0} - -echo -echo " _ _ _ " -echo " ___(_) __ _| (_)_ __ __ _ ___ " -echo " |_ | |/ _' | | | '_ \ / _' / __| " -echo " / /| | (_| | | | | | | (_| \__ \ " -echo " /___|_|\__, |_|_|_| |_|\__, |___/ " -echo " |___/ |___/ " -echo - -# Capture terminal escape sequences (ANSI) for formatting -fmt_err=$( tput setaf 1 ) # red foreground -fmt_yay=$( tput setaf 2 ) # green foreground -fmt_off=$( tput sgr0 ) # reset colors/effects - -exercise_num=0 - -function check_it { - source_file="exercises/$1" - correct_output=$2 - hint=$3 - - # If the current exercise is less than the requested one, skip it - let exercise_num+=1 - if [[ $exercise_num -lt $jump_to ]] - then - return - fi - - # Compile/run the source and capture the result and exit value - cmd="zig run $source_file" - echo "$ $cmd" - result=$($cmd 2>&1) - result_status=$? - - # Echo the result to the screen so user can see what their program does - echo "$result" - if [[ $result_status -ne 0 ]] - then - echo - printf "${fmt_err}Uh oh! Looks like there was an error.${fmt_off}\n" - if [[ ! -z "$hint" ]] - then - echo "$hint" - fi - echo - echo "Edit '$source_file' and run me again." - echo - exit 1 - fi - - # Wildcards to be lenient with anything AROUND the correct output - if [[ "$result" == *"$correct_output"* ]] - then - printf "${fmt_yay}** PASSED **${fmt_off}\n" - else - printf "${fmt_err}It seems to compile, but I wanted to see '$correct_output'.${fmt_off}\n" - if [[ ! -z "$hint" ]] - then - echo "$hint" - fi - echo - exit 1 - fi -} - -# I've chosen to explicitly number AND list each exercise rather than rely -# on sorting. Though it does mean manually renaming things to remove/insert, -# it's worked out well so far. - -check_it 01_hello.zig "Hello world" "Note the error: the source file has a hint for fixing 'main'." -check_it 02_std.zig "Standard Library" -check_it 03_assignment.zig "55 314159 -11" "There are three mistakes in this one!" -check_it 04_arrays.zig "Fourth: 7, Length: 8" "There are two things to complete here." -check_it 05_arrays2.zig "LEET: 1337, Bits: 100110011001" "Fill in the two arrays." -check_it 06_strings.zig "d=d ha ha ha Major Tom" "Each '???' needs something filled in." -check_it 07_strings2.zig "Ziggy" "Please fix the lyrics!" -check_it 08_quiz.zig "Program in Zig" "See if you can fix the program!" -check_it 09_if.zig "Foo is 1!" -check_it 10_if2.zig "price is \$17" -check_it 11_while.zig "n=1024" "You probably want a 'less than' condition." -check_it 12_while2.zig "n=1024" "It might help to look back at the previous exercise." -check_it 13_while3.zig "1 2 4 7 8 11 13 14 16 17 19" -check_it 14_while4.zig "n=4" -check_it 15_for.zig "A Dramatic Story: :-) :-) :-( :-| :-) The End." -check_it 16_for2.zig "13" -check_it 17_quiz2.zig "8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16" "This is a famous game!" -check_it 18_functions.zig "Question: 42" "Can you help write the function?" -check_it 19_functions2.zig "2 4 8 16" -check_it 20_quiz3.zig "32 64 128 256" "Unexpected pop quiz! Help!" -check_it 21_errors.zig "2<4. 3<4. 4=4. 5>4. 6>4." "What's the deal with fours?" -check_it 22_errors2.zig "I compiled" "Get the error union type right to allow this to compile." -check_it 23_errors3.zig "a=64, b=22" -check_it 24_errors4.zig "a=20, b=14, c=10" -check_it 25_errors5.zig "a=0, b=19, c=0" -check_it 26_hello2.zig "Hello world" "Try using a try!" -check_it 27_defer.zig "One Two" -check_it 28_defer2.zig "(Goat) (Cat) (Dog) (Dog) (Goat) (Unknown) done." -check_it 29_errdefer.zig "Getting number...got 5. Getting number...failed!" -check_it 30_switch.zig "ZIG?" -check_it 31_switch2.zig "ZIG!" -check_it 32_unreachable.zig "1 2 3 9 8 7" -check_it 33_iferror.zig "2<4. 3<4. 4=4. 5>4. 6>4." "Seriously, what's the deal with fours?" -check_it 34_quiz4.zig "my_num=42" "Can you make this work?" -check_it 35_enums.zig "1 2 3 9 8 7" "This problem seems familiar..." -check_it 36_enums2.zig "#0000ff" "I'm feeling blue about this." -check_it 37_structs.zig "Your wizard has 90 health and 25 gold." -check_it 38_structs2.zig "Character 2 - G:10 H:100 XP:20" -check_it 39_pointers.zig "num1: 5, num2: 5" "Pointers aren't so bad." -check_it 40_pointers2.zig "a: 12, b: 12" -check_it 41_pointers3.zig "foo=6, bar=11" -check_it 42_pointers4.zig "num: 5, more_nums: 1 1 5 1" -check_it 43_pointers5.zig "Wizard (G:10 H:100 XP:20)" -check_it 44_quiz5.zig "Elephant A. Elephant B. Elephant C." "Oh no! We forgot Elephant B!" -# optional vals (simple scalar) -# optional fields (elephant tail - no longer need circular) -# super-simple struct method -# use struct method for elephant tails -# quiz: add elephant trunk (like tail)! - -echo -echo " __ __ _ " -echo " \ \ / __ _ _ _| | " -echo " \ V / _' | | | | | " -echo " | | (_| | |_| |_| " -echo " |_|\__,_|\__, (_) " -echo " |___/ " -echo -echo "You've completed all of the Ziglings exercises!" -echo " (That have been written so far.)" -echo