Compare commits

..
Author SHA1 Message Date
Chen, Tai-Lin aebe587ad3 update 2024-04-30 01:40:42 +08:00
Chen, Tai-Lin c96ee83fa3 update quest\permission 2024-04-08 22:39:23 +08:00
116 changed files with 3210 additions and 5578 deletions

No files matched your search

+20
View File
@@ -35,3 +35,23 @@ jobs:
- name: Test
run: yarn test
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Build Test Environment, Stage 1
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
- name: Build Test Quests
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
- name: Run Cypress Tests
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
run: docker-compose -f docker-compose.testing.yml run cypress
-24
View File
@@ -1,24 +0,0 @@
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Build Test Environment, Stage 1
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
- name: Build Test Quests
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
- name: Run Cypress Tests
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
run: docker-compose -f docker-compose.testing.yml run cypress
+1 -1
View File
@@ -19,7 +19,7 @@
Run mongoDB first:
docker compose up -d
docker compose -f docker-compose.dev.yml up -d db
The db will automatically restarts by Docker.
+2 -4
View File
@@ -6,11 +6,9 @@
"dev": "vite",
"dev:docker": "API_TARGET=http://backend:3000 vite",
"build": "vue-tsc -b && vite build",
"type-check": "vue-tsc -b",
"preview": "vite preview"
},
"dependencies": {
"@formbricks/js": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -18,13 +16,13 @@
"@fortawesome/vue-fontawesome": "^3.0.3",
"@linux-odyssey/constants": "*",
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/models": "*",
"@linux-odyssey/utils": "*",
"@linux-odyssey/models": "*",
"@types/dompurify": "^3.0.5",
"axios": "^1.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.4.3",
"marked": "^10.0.0",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
-17
View File
@@ -83,16 +83,6 @@ const handleChange = () => {
errorMessage.value = 'Something went wrong'
}
}
const registerGuest = async () => {
try {
await api.post('/auth/register-guest')
router.push({ name: 'map' })
} catch (err) {
console.error(err)
toast.error('Something went wrong.')
}
}
</script>
<template>
<!-- login page -->
@@ -229,13 +219,6 @@ const registerGuest = async () => {
</RouterLink></span
>
</p>
<button
id="register-guest"
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
@click="registerGuest()"
>
以訪客身分繼續
</button>
</form>
</div>
</template>
@@ -2,6 +2,6 @@
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
<div id="cheatsheets">
<p class="text-text-secondary">-- --指令列表-- --</p>
<p class="text-text-secondary">--指令列表--</p>
</div>
</template>
@@ -4,16 +4,14 @@ import { useToast } from 'vue-toastification'
import { ref } from 'vue'
import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config'
import CompleteAnimation from '../../img/complete-animation.json'
import CompleteAnimation from '../img/complete-animation.json'
const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const closeModal = () => {
showModal.value = false
}
const defineComponent = () => ({
components: {
Vue3Lottie,
@@ -24,11 +22,11 @@ const defineComponent = () => ({
}
},
})
const backtoMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
@@ -62,6 +60,17 @@ setTimeout(() => {
<p class="text-base text-text text-center">
接下來回到地圖,迎接新的挑戰吧!
</p>
<!-- <p class="text-base text-text">
您已成功完成挑戰,真的非常感謝您的參與!<br />
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
</p> -->
<!-- <a
id="SurveyButton"
:href="surveyUrl"
target="_blank"
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
>填寫問卷</a
> -->
<a
id="BacktoMap"
target="_blank"
@@ -70,21 +79,6 @@ setTimeout(() => {
>回到地圖</a
>
</div>
<div style="position: relative; height: 100vh; overflow: auto">
<iframe
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
frameborder="0"
style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 0;
"
>
</iframe>
</div>
</div>
</template>
+59
View File
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { createSession } from '../store/session'
import { surveyUrl } from '../config'
const router = useRouter()
const reset = async () => {
console.log('resetting...')
await createSession()
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
useToast().error('無法讀取地圖')
console.error(err)
}
}
</script>
<template>
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">問卷</p>
</a>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled"
/>
<p class="text-text-disabled inline ml-1">解答</p>
</button>
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">重來</p>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">繼續</p>
</button>
</section>
</template>
+1 -15
View File
@@ -20,21 +20,7 @@ function nodeClass() {
</script>
<template>
<a href="#" :class="nodeClass()">
<font-awesome-icon
v-if="node.type === 'folder'"
:icon="['far', 'folder']"
class="text-text-primary size-8"
/>
<font-awesome-icon
v-else
:icon="['far', 'file']"
class="text-text-primary size-8"
/>
<br />
{{ node.discovered ? node.name : '???' }}
{{ node.name === '' && node.discovered ? '/' : '' }}
</a>
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
<FileNode :node="child" :pwd="pwd" />
+142
View File
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { defineProps, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../utils/auth'
import { bugReportUrl, surveyUrl } from '../config'
import { reset } from '../store/session'
import userProfileStore, {
loadUserProfile,
resetUserProfile,
} from '../store/userProfile'
const router = useRouter()
const toast = useToast()
const handleLogout = async () => {
try {
await logout()
resetUserProfile()
reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('登出失敗')
console.error(err)
}
}
defineProps({
title: {
type: String,
required: false,
},
})
onMounted(async () => {
try {
await loadUserProfile()
} catch (err) {
console.error(err)
toast.error('無法讀取使用者資料')
}
})
</script>
<template>
<div id="header" class="bg-bg-secondary content-center h-full w-full p-1">
<div class="h-full flex items-center justify-center mx-3">
<div class="flex items-center gap-3">
<!-- <button id="stream" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="backward" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'arrow-left']"
class="text-border h-full w-full"
/>
</button> -->
<img src="../img/icon_totem.svg" class="h-5" />
<button
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
Linux Odyssey
</button>
<p
id="QuestTitle"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p>
</div>
<div class="h-full w-full flex gap-3 items-center justify-end">
<!-- <button id="warning" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'triangle-exclamation']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="setting" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'gear']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="fullscreen" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'expand']"
class="text-text-primary h-full w-full"
/>
</button> -->
<p
id="UsernameText"
class="text-text inline-block whitespace-nowrap"
style="font-size: 2vh"
>
{{ userProfileStore.username }}
</p>
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary h-full w-full"
/>
</a>
<RouterLink title="LeaderBoard" to="/leaderboard" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'trophy']"
class="text-text-primary h-full w-full"
/>
</RouterLink>
<RouterLink title="Map" to="/map" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'map']"
class="text-text-primary h-full w-full"
/>
</RouterLink>
<a
title="Bug Report"
:href="bugReportUrl"
target="_blank"
class="h-5 w-5"
>
<font-awesome-icon
:icon="['fas', 'bug']"
class="text-text-primary h-full w-full"
/>
</a>
<button title="Sign Out" @click="handleLogout" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'arrow-right-from-bracket']"
class="text-text-primary h-full w-full"
/>
</button>
</div>
</div>
</div>
</template>
@@ -1,21 +1,16 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import useSession from '../../store/session'
import MarkdownText from '../MarkdownText.vue'
import { ref, watch } from 'vue'
import sessionStore from '../store/session'
import MarkdownText from './MarkdownText.vue'
const sessionStore = useSession()
const current = ref(-1)
const length = computed(() => {
return sessionStore.session?.responses.length ?? 0
})
const disabled = computed(() => {
return current.value === length.value - 1
})
watch(length, () => {
current.value = length.value - 1
})
watch(
() => sessionStore.session.hints,
(hints) => {
current.value = hints.length - 1
},
{ deep: true }
)
const left = () => {
if (current.value > 0) {
@@ -24,7 +19,7 @@ const left = () => {
}
const right = () => {
if (current.value < length.value - 1) {
if (current.value < sessionStore.session.hints.length - 1) {
current.value += 1
}
}
@@ -32,12 +27,16 @@ const right = () => {
<template>
<div class="h-full flex flex-col">
<!-- Hint Header -->
<div class="flex items-center p-2 h-10">
<font-awesome-icon
:icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center"
/>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
<!-- Hint -->
提示
</h1>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
@@ -54,13 +53,13 @@ const right = () => {
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">
{{ length }}
{{ sessionStore.session.hints.length }}
</p>
<button
class="p-2 m-1 w-1/8"
id="Rbutton"
@click="right"
:disabled="disabled"
:disabled="current === sessionStore.session.hints.length - 1"
>
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
</button>
@@ -69,7 +68,7 @@ const right = () => {
<div id="hint" class="bg-bg p-8 overflow-y-auto">
<ul>
<li
v-for="response in sessionStore.session?.responses[current]"
v-for="response in sessionStore.session.responses[current]"
:key="response.type"
class="text-text font-xl whitespace-pre-wrap"
>
@@ -78,7 +77,7 @@ const right = () => {
</div>
</li>
<li
v-for="hint in sessionStore.session?.hints[current]"
v-for="hint in sessionStore.session.hints[current]"
:key="hint"
class="text-text-primary font-xl whitespace-pre-wrap"
>
-3
View File
@@ -25,9 +25,6 @@ const html = computed(() => {
line-height: 1;
}
.markdown-content :deep(p) {
line-height: 1.5;
}
.markdown-content :deep(ol) {
list-style: decimal;
padding-left: 1em;
-121
View File
@@ -1,121 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref, defineProps, onMounted } from 'vue'
import { getQuest } from '../utils/api'
import MarkdownText from './MarkdownText.vue'
const props = defineProps({
questTitle: {
type: String,
required: true,
},
questId: {
type: String,
required: true,
},
questUnlocked: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['closeIntro'])
const showIntro = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const { questTitle, questId, questUnlocked } = props
const questInstruction = ref('')
const questColor = ref('')
const questTextColor = ref('')
const closeModal = () => {
showIntro.value = false
emit('closeIntro', true)
router.push({ name: 'map' })
}
const handleQuests = (id: string) => {
if (questUnlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
}
}
const handleColour = () => {
if (questUnlocked) {
questColor.value = '#00ff00'
questTextColor.value = '#000000'
} else {
questColor.value = '#505050'
questTextColor.value = '#a0a0a0'
}
}
onMounted(async () => {
const quest = await getQuest(questId)
questInstruction.value = quest.instruction
handleColour()
})
</script>
<template>
<div v-if="showIntro" class="modal">
<div class="modal-content bg-bg-secondary rounded-lg">
<span class="close text-text content-between" @click="closeModal"
>&times;</span
>
<h2
id="QuestTitle"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
>
{{ questTitle }}
</h2>
<div class="w-full h-3/5 p-8">
<div class="px-5 whitespace-pre-wrap">
<MarkdownText
class="text-text markdown-content"
:content="questInstruction"
/>
</div>
</div>
<a
id="StartAdventure"
target="_blank"
@click="handleQuests(questId)"
:style="{ backgroundColor: questColor, color: questTextColor }"
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
>開始冒險</a
>
</div>
</div>
</template>
<style scoped>
.modal {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.modal-content {
position: relative;
margin: 10% auto;
padding: 20px;
width: 60%;
height: 50%;
}
.close {
position: absolute;
top: 0;
right: 10px;
font-size: 30px;
cursor: pointer;
}
.markdown-content {
text-align: left;
}
</style>
+153 -218
View File
@@ -1,254 +1,189 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { use, init } from 'echarts/core'
import { GraphChart } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import {
TitleComponent,
TooltipComponent,
DataZoomComponent,
} from 'echarts/components'
import { DAG } from '@linux-odyssey/utils'
import { getQuests } from '../utils/api'
import useUserProfile from '../store/userProfile'
import QuestIntro from './QuestIntro.vue'
import api from '../utils/api'
import { NodeImage } from '../img/svg.js'
const store = useUserProfile()
const toast = useToast()
const marginX = 500
const marginY = 300
const chartContainer = ref(null)
let chartInstance = null
const fullwidth = window.screen.width
const svgWidth = ref(window.innerWidth)
const svgHeight = ref(window.innerHeight)
use([
GraphChart,
SVGRenderer,
TitleComponent,
TooltipComponent,
DataZoomComponent,
])
const marginX = 120
const marginY = 120
async function getQuests() {
try {
const res = await api.get('/quests')
return res.data
} catch (err) {
console.error(err)
const nodeWidth = 160
const nodeHeight = 50
const opened = ref<Node | null>(null)
type Node = {
id: string
title: string
x: number
y: number
index: number
completed: boolean
unlocked: boolean
throw err
}
}
type Edge = {
source: Node
target: Node
async function getProgress() {
try {
const res = await api.get('/users/me')
return res.data.progress
} catch (err) {
console.error(err)
throw err
}
}
const graphData = ref<{ nodes: Node[]; edges: Edge[] }>({
nodes: [],
edges: [],
const genOption = (nodes: any, edges: any) => ({
renderer: 'svg',
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
// dataZoom: [
// {
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
// orient: 'vertical', // This makes the data zoom component vertical
// start: 0, // The starting percentage of the window out of the data extent
// end: 100, // The ending percentage of the window out of the data extent
// show: true,
// },
// ],
series: [
{
type: 'graph',
layout: 'none',
symbol: () => {
return NodeImage
},
symbolSize: [fullwidth / 13, fullwidth / 30],
roam: 'move',
zoom: 1.5,
center: ['120%', '30%'],
label: {
show: true,
fontSize: fullwidth / 90,
},
edgeSymbol: ['pin', 'arrow'],
edgeSymbolSize: [2, 15],
edgeLabel: {
fontSize: 25,
},
data: nodes,
// links: [],
links: edges,
lineStyle: {
width: 3,
color: 'gray',
opacity: 4,
curveness: 0.08,
},
itemStyle: {
color: (params: any) => {
const {
data: { completed, unlocked },
} = params
if (completed) {
return '#00ff00'
}
if (unlocked) {
return '#ADADB5'
}
return '#505050'
},
},
},
],
})
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const offsetX = ref(0)
const offsetY = ref(0)
function startDrag(event: MouseEvent) {
isDragging.value = true
dragStartX.value = event.clientX - offsetX.value
dragStartY.value = event.clientY - offsetY.value
}
function drag(event: MouseEvent) {
if (!isDragging.value) return
offsetX.value = event.clientX - dragStartX.value
offsetY.value = event.clientY - dragStartY.value
}
function endDrag() {
isDragging.value = false
}
async function computeGraphData() {
if (!store.progress) return
try {
const quests = await getQuests()
function getOption(quests: any, progress: any) {
const dag = new DAG(quests)
const nodesValues = dag.getNodes()
const nodes = nodesValues.map((node) => ({
const nodes = dag.getNodes().map((node) => ({
id: node._id,
title: node.title,
x: marginX * node.layer * 2,
y:
marginY * node.index -
(marginX * dag.getLayer(node._id)) / 2 +
svgHeight.value / 2,
completed: store.progress[node._id]?.completed || false,
unlocked: node.requirements.every(
(req: string) => store.progress[req]?.completed
),
index: node.index,
name: node.title,
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
x: marginY * node.layer * 2,
completed: progress[node._id]?.completed || false,
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
}))
const edges = dag
.getEdgesArray()
.map((edge) => ({
source: nodes.find((n) => n.id === edge[0]),
target: nodes.find((n) => n.id === edge[1]),
const lineAppearence = (node: any) => {
try {
const { unlocked } = node
return {
type: unlocked ? 'line' : 'dashed',
color: unlocked ? '#ADADB5' : '#454552',
}
} catch (err) {
console.error(err)
throw err
}
}
const edges = dag.getEdgesArray().map((edge: any) => ({
source: edge[0],
target: edge[1],
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
}))
.filter(
(edge): edge is Edge =>
edge.source !== undefined && edge.target !== undefined
)
return genOption(nodes, edges)
}
graphData.value = { nodes, edges }
} catch (error) {
console.error('Error computing graph data:', error)
toast.error('Failed to load quest data')
}
}
function handleNodeClick(node: Node) {
opened.value = node
}
function closeIntro(close: boolean) {
if (close) {
opened.value = null
const router = useRouter()
const toast = useToast()
function initChart(option: any) {
chartInstance = init(chartContainer.value)
chartInstance.setOption(option)
chartInstance.on('click', (params: any) => {
const {
data: { id, unlocked },
} = params
if (unlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
}
})
}
onMounted(async () => {
await store.loadUserProfile()
await computeGraphData()
})
watch(() => store.progress, computeGraphData, { deep: true })
const curvedPath = computed(() => {
return (source: Node, target: Node) => {
// const dx = target.x - source.x
// const dy = target.y - source.y
// const dr = Math.sqrt(dx * dx + dy * dy)
const dr = 0
const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index
return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}`
}
})
const nodeStyle = computed(() => {
return (node: Node) => {
if (node.completed) return 'completed'
if (node.unlocked) return 'unlocked'
return 'locked'
}
})
const edgeStyle = computed(() => {
return (edge: Edge) => {
return edge.target.unlocked ? 'unlocked' : 'locked'
}
const quests = await getQuests()
const progress = await getProgress()
const option = getOption(quests, progress)
initChart(option)
})
</script>
<template>
<div class="relative bg-black w-full h-full flex">
<div
class="flex place-content-center w-full bg-catelogbg bg-cover bg-scroll"
>
<img
src="../img/catelogbg.svg"
alt="bg"
class="p-2 stroke-2 scale-y-120 justify-center items-center"
/>
<h1
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
>
踏上你的Linux冒險之旅吧!
</h1>
<svg
:width="svgWidth"
:height="svgHeight"
class="absolute"
@mousedown="startDrag"
@mousemove="drag"
@mouseup="endDrag"
@mouseleave="endDrag"
>
<g :transform="`translate(${offsetX}, ${offsetY})`">
<g
v-for="edge in graphData.edges"
:key="`${edge.source.id}-${edge.target.id}`"
>
<path
:d="curvedPath(edge.source, edge.target)"
:class="['edge', edgeStyle(edge)]"
/>
</g>
<g
v-for="node in graphData.nodes"
:key="node.id"
@click="handleNodeClick(node)"
>
<rect
:x="node.x - nodeWidth / 2"
:y="node.y - nodeHeight / 2"
:width="nodeWidth"
:height="nodeHeight"
rx="10"
ry="10"
:class="['node', nodeStyle(node)]"
/>
<text
:x="node.x"
:y="node.y"
text-anchor="middle"
alignment-baseline="middle"
:class="['node-text', nodeStyle(node)]"
font-size="18"
>
{{ node.title }}
</text>
</g>
</g>
</svg>
<QuestIntro
:questTitle="opened.title"
:questId="opened.id"
:questUnlocked="opened.unlocked"
v-if="opened"
@close-intro="closeIntro"
/>
<div class="flex flex-wrap absolute w-full h-full z-1">
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
</div>
</div>
</template>
<style scoped>
svg {
overflow: visible;
}
.node {
&.completed {
fill: #6cf76c;
}
&.unlocked {
fill: #8c8c92;
}
&.locked {
fill: #505050;
}
}
.node-text {
&.completed {
fill: #1d1d1d;
}
&.unlocked {
fill: #ffffff;
}
&.locked {
fill: #a0a0a0;
}
}
.edge {
stroke-width: 3;
fill: none;
&.unlocked {
stroke: #adadb5;
stroke-dasharray: none;
}
&.locked {
stroke: #454552;
stroke-dasharray: 5, 5;
}
}
</style>
@@ -1,12 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownText from '../MarkdownText.vue'
import useSession from '../../store/session'
import { Task } from '../../types'
import MarkdownText from './MarkdownText.vue'
import sessionStore from '../store/session'
import { Task } from '../types'
const sessionStore = useSession()
const tasks = computed(() => {
return sessionStore.session?.tasks.map((task: Task) => {
return sessionStore.session.tasks.map((task: Task) => {
const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text'
return {
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useTerminal } from '../../store/session'
import { useTerminal } from '../store/session'
const terminal = ref<HTMLElement | null>(null)
@@ -1,8 +1,6 @@
<script setup lang="ts">
import FileNode from '../FileNode.vue'
import useSession from '../../store/session'
const sessionStore = useSession()
import FileNode from './FileNode.vue'
import sessionStore from '../store/session'
</script>
<template>
@@ -10,7 +8,6 @@ const sessionStore = useSession()
<ul>
<li>
<FileNode
v-if="sessionStore.session"
:node="sessionStore.session.graph"
:pwd="sessionStore.session.pwd"
/>
@@ -48,14 +45,14 @@ const sessionStore = useSession()
position: absolute;
top: 0;
right: 50%;
border-top: 2px solid #00ff00;
border-top: 1px solid #00ff00;
width: 50%;
height: 20px;
}
.tree li::after {
right: auto;
left: 50%;
border-left: 2px solid #00ff00;
border-left: 1px solid #00ff00;
}
.tree li:only-child::after,
@@ -73,7 +70,7 @@ const sessionStore = useSession()
}
.tree li:last-child::before {
border-right: 2px solid #00ff00;
border-right: 1px solid #00ff00;
border-radius: 0 5px 0 0;
-webkit-border-radius: 0 5px 0 0;
-moz-border-radius: 0 5px 0 0;
@@ -89,7 +86,7 @@ const sessionStore = useSession()
position: absolute;
top: 0;
left: 50%;
border-left: 2px solid #00ff00;
border-left: 1px solid #00ff00;
width: 0;
height: 20px;
}
@@ -100,7 +97,7 @@ const sessionStore = useSession()
text-decoration: none;
color: #ffffff;
font-family: arial, verdana, tahoma;
font-size: 12px;
font-size: 11px;
display: inline-block;
border-radius: 5px;
@@ -112,10 +109,18 @@ const sessionStore = useSession()
-moz-transition: all 0.5s;
}
.tree li a.pwd {
border: 3px solid #00ff00;
background: #008080;
font-weight: bold;
font-size: 13px;
a.discovered {
background: #526b4e;
color: #000;
border: 1px solid #94a0b4;
}
ul.discovered,
li.discovered {
border-color: #94a0b4;
}
a.pwd {
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
}
</style>
@@ -1,70 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import useSession from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
const router = useRouter()
const sessionStore = useSession()
const reset = async () => {
console.log('resetting...')
await sessionStore.createSession()
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
useToast().error('無法讀取地圖')
console.error(err)
}
}
</script>
<template>
<section class="mt-2 flex flex-row gap-3 justify-center">
<button
id="survey"
class="bg-bg-secondary rounded-lg p-2"
@click="openQuestSurvey"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline md:flex">問卷</p>
</button>
<button
title="Bug Report"
@click="openBugReport"
class="bg-bg-secondary rounded-lg p-2"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<p class="text-text-primary inline ml-1 md:flex">回報</p>
</button>
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled px-1"
/>
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
</button> -->
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">重來</p>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
</button>
</section>
</template>
-37
View File
@@ -1,37 +0,0 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { computed } from 'vue'
import useSession from '../../store/session'
const toast = useToast()
const sessionStore = useSession()
const showCover = computed(() => sessionStore.session === null)
const startSession = async () => {
try {
await sessionStore.createSession()
} catch (err: any) {
console.error(err)
toast.error(err.message)
}
}
</script>
<template>
<div
v-if="showCover"
class="bg-bg-secondary h-full w-1/2 fixed bg-opacity-50 flex justify-center items-center"
>
<button
id="start"
class="bg-bg-secondary rounded-lg p-3"
@click="startSession"
>
<font-awesome-icon
:icon="['fas', 'play']"
class="text-text-primary h-full px-3"
/>
<p class="text-text-primary inline ml-1 md:flex">開始</p>
</button>
</div>
</template>
@@ -1,107 +0,0 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../../utils/auth'
import { surveyUrl } from '../../config'
import { openBugReport } from '../../utils/formbricks'
import useSession from '../../store/session'
import useUserProfile from '../../store/userProfile'
const userStore = useUserProfile()
const sessionStore = useSession()
const router = useRouter()
const toast = useToast()
const handleLogout = async () => {
try {
await logout()
userStore.resetUserProfile()
sessionStore.reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('登出失敗')
console.error(err)
}
}
onMounted(async () => {
try {
await userStore.loadUserProfile()
} catch (err) {
toast.error('無法讀取使用者資料')
console.error(err)
}
})
</script>
<template>
<!-- <button id="warning" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'triangle-exclamation']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="setting" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'gear']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="fullscreen" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'expand']"
class="text-text-primary h-full w-full"
/>
</button> -->
<p
id="UsernameText"
class="text-text inline-block whitespace-nowrap px-1.5"
style="font-size: 2vh"
>
{{ userStore.username }}
</p>
<a
title="Survey"
:href="surveyUrl"
target="_blank"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary inline"
/>
<span class="text-text sm:hidden px-2">Survey</span>
</a>
<RouterLink
title="LeaderBoard"
to="/leaderboard"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'trophy']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Leader Board</span>
</RouterLink>
<RouterLink title="Map" to="/map" class="md:h-5 md:w-5 px-1.5 w-auto">
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Map</span>
</RouterLink>
<button
title="Bug Report"
@click="openBugReport"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Bug Report</span>
</button>
<button
title="Sign Out"
@click="handleLogout"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon
:icon="['fas', 'arrow-right-from-bracket']"
class="text-text-primary"
/>
<span class="text-text sm:hidden px-2">Sign Out</span>
</button>
</template>
-66
View File
@@ -1,66 +0,0 @@
<script setup lang="ts">
import { defineProps, ref } from 'vue'
defineProps({
title: {
type: String,
required: false,
},
headerComponent: {
type: Object,
required: false,
},
})
const menuOpen = ref(false)
const windowWidth = window.innerWidth < 768
const toggleMenu = () => {
menuOpen.value = !menuOpen.value
}
</script>
<template>
<div
class="w-full top-0 px-5 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
>
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
<RouterLink
to="/"
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1 transition ease-in-out hover:scale-110 duration-300"
style="font-size: 2vh"
>
Linux Odyssey
</RouterLink>
<p
id="QuestTitle"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p>
</div>
<div
class="w-full flex flex-col items-center justify-end sm:w-auto sm:block sm:ml-6"
:class="{ hidden: !menuOpen }"
>
<component :is="headerComponent" />
</div>
<div
v-if="windowWidth"
:class="{ 'w-full flex justify-end pb-3': menuOpen }"
>
<button
type="button"
class="flex items-center h-5 w-5 rounded-md sm:hidden"
aria-controls="mobile-menu"
aria-expanded="false"
@click="toggleMenu"
>
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-text-primary h-full w-full"
/>
</button>
</div>
</div>
</template>
@@ -1,62 +0,0 @@
<script setup lang="ts">
import { bugReportUrl } from '../../config'
const scrollToSection = (sectionId: string) => {
const section = document.getElementById(sectionId)
if (section) {
const container = document.querySelector('.overflow-y-auto') as HTMLElement // Replace '.overflow-y-auto' with the appropriate class or selector for your container
if (container) {
container.scrollTo({
top: section.offsetTop - container.offsetTop,
behavior: 'smooth', // Add smooth scrolling behavior if desired
})
}
}
}
</script>
<template>
<button
id="Intro"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('intro')"
style="font-size: 2vh"
>
前言
</button>
<button
id="Feature"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('feature')"
style="font-size: 2vh"
>
特色</button
><button
id="Info"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('about')"
style="font-size: 2vh"
>
關於
</button>
<button
id="Contact"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('contact')"
style="font-size: 2vh"
>
聯絡我們
</button>
<RouterLink
title="Sign In"
id="SignIn"
to="/register"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
style="font-size: 2vh"
>
登入
</RouterLink>
<a title="Bug Report" :href="bugReportUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
</a>
</template>
File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+27 -33
View File
@@ -3,30 +3,27 @@ import { library } from '@fortawesome/fontawesome-svg-core'
/* import specific icons */
import {
faArrowLeft,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faArrowLeft,
faList,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faBug,
faCircleExclamation,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faList,
faMap,
faPlay,
faTerminal,
faTriangleExclamation,
faTrophy,
faFileInvoice,
faEnvelope,
} from '@fortawesome/free-solid-svg-icons'
import {
faLightbulb,
faCircleQuestion,
faCircleRight,
faFile,
faFolder,
faLightbulb,
} from '@fortawesome/free-regular-svg-icons'
import {
faFacebook,
@@ -36,29 +33,26 @@ import {
/* add icons to the library */
library.add(
faArrowLeft,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faBug,
faCircleExclamation,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faArrowLeft,
faList,
faMap,
faPlay,
faTerminal,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faBug,
faMap,
faTrophy,
faCircleExclamation,
faLightbulb,
faCircleQuestion,
faCircleRight,
faFile,
faFolder,
faLightbulb,
faFacebook,
faGithub,
faGoogle
faGoogle,
faFileInvoice,
faFacebook,
faEnvelope
)
+1 -11
View File
@@ -1,6 +1,4 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
/* import font awesome icon component */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
@@ -10,24 +8,16 @@ import Toast, { POSITION } from 'vue-toastification'
/* import Vue Lottie */
import Vue3Lottie from 'vue3-lottie'
import 'vue-toastification/dist/index.css'
import App from './App.vue'
import router from './router'
import formbricks from './utils/formbricks'
const app = createApp(App)
const pinia = createPinia()
app.component('font-awesome-icon', FontAwesomeIcon)
app.use(pinia)
app.use(router)
router.afterEach(() => {
if (typeof formbricks !== 'undefined') {
formbricks.registerRouteChange()
}
})
const options = {
position: POSITION.TOP_RIGHT,
}
+120 -69
View File
@@ -1,82 +1,142 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { useToast } from 'vue-toastification'
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
import { IQuest } from '@linux-odyssey/models'
import { createSession, getActiveSession, getQuest } from '../utils/api'
import api from '../utils/api'
import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import { LoadQuestError, LoadSessionError } from '../utils/errors'
import { Session, StageResponse } from '../types'
const socket = new Socket()
const term = new SocketTerminal(40, 80)
let hasSetup = false
const toast = useToast()
function newSession() {
const graph = new FileGraph({
path: '/',
type: 'folder',
discovered: false,
})
return {
_id: '',
status: 'inactive',
graph,
pwd: '',
hints: [],
tasks: [],
responses: [],
}
}
interface Store {
session: Session | null
session: Session
questId: string
quest: IQuest | null
}
const useSession = defineStore('session', {
state: (): Store => ({
session: null,
const store = reactive<Store>({
session: newSession(),
questId: '',
quest: null,
}),
actions: {
async setQuest(questId: string) {
this.quest = await getQuest(questId)
this.questId = questId
},
async setSession(session: Session) {
this.session = session
this.session.graph = new FileGraph(session.graph)
})
const socket = new Socket()
const term = new SocketTerminal(40, 80)
function setQuest(questId: string) {
return api.get(`/quests/${questId}`).then((res) => {
store.questId = res.data._id
store.quest = res.data
})
}
async function setSession(session: Session) {
try {
console.log('Setting session...', session)
store.session = session
store.session.graph = new FileGraph(session.graph)
term.reset()
await socket.connect(session)
term.focus()
},
async createSession() {
const session = await createSession(this.questId)
await this.setSession(session)
socket.emit('terminal', 'echo start\n')
},
async getActiveSession() {
const session = await getActiveSession(this.questId)
if (session) {
this.setSession(session)
} catch (err: any) {
console.error(err)
if (err instanceof Error) {
toast.error(err.message)
} else {
toast.error('Failed to connect to the session. Please try again later.')
}
},
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
if (!this.session) return
throw err
}
}
export async function createSession() {
try {
console.log('Creating a new session...')
const res = await api.post('/sessions', { questId: store.questId })
const { data } = res
await setSession(data)
} catch (err) {
console.error(err)
toast.error('Failed to create a new session. Please try again later.')
}
}
async function getActiveSession(questId: string) {
try {
const res = await api.post('/sessions/active', { questId })
await setSession(res.data)
} catch (err) {
console.error(err)
toast.warning('Failed to connect previous session. Creating a new one...')
await createSession()
}
}
function updateGraph(event: { discover: FileObject[]; pwd: string }) {
if (event.discover) {
this.session.graph.discover(event.discover)
store.session.graph.discover(event.discover)
}
if (event.pwd) {
this.session.pwd = event.pwd
store.session.pwd = event.pwd
}
},
newResponse(response: StageResponse) {
if (!this.session) return
this.session.responses.push(response.responses)
this.session.hints.push(response.hints)
if (response.tasks) {
this.session.tasks = response.tasks
}
if (
response.status === 'finished' &&
this.session.status !== 'finished'
) {
this.finish()
}
this.session.status = response.status
},
finish() {},
reset() {
}
function newResponse(response: StageResponse) {
console.log('newResponse', response, 'session', store.session)
store.session.responses.push(response.responses)
store.session.hints.push(response.hints)
store.session.tasks = response.tasks
store.session.status = response.status
}
export function useTerminal() {
return term
}
export function reset() {
socket.reset()
term.reset()
this.$reset()
},
setup() {
if (hasSetup) return
store.session = newSession()
store.quest = null
}
export async function init(questId: string) {
if (!questId) throw new Error('No quest ID provided')
reset()
try {
await setQuest(questId)
} catch (err) {
console.error(err)
throw new LoadQuestError('Failed to load quest', questId)
}
try {
await getActiveSession(questId)
} catch (err) {
console.error(err)
throw new LoadSessionError('Failed to load session', questId)
}
}
function setup() {
socket.on('terminal', (data: string) => {
term.write(data)
})
@@ -84,22 +144,13 @@ const useSession = defineStore('session', {
socket.emit('terminal', data)
})
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
this.updateGraph(event)
updateGraph(event)
})
socket.on('response', (response: StageResponse) => {
this.newResponse(response)
newResponse(response)
})
hasSetup = true
},
setStatus(status: string) {
if (!this.session) return
this.session.status = status
},
},
})
export function useTerminal() {
return term
}
export default useSession
setup()
export default store
+24 -29
View File
@@ -1,35 +1,30 @@
import { defineStore } from 'pinia'
import { getUserProfile } from '../utils/api'
import { reactive } from 'vue'
import api from '../utils/api'
interface IProgress {
quest: string
sessions: string[]
completed: boolean
startedAt: Date
finishedAt?: Date
}
export interface UserProfile {
username: string
email: string
progress: Record<string, IProgress>
}
const useUserProfile = defineStore('userProfile', {
state: (): UserProfile => ({
const store = reactive({
username: '',
email: '',
progress: {},
}),
actions: {
async loadUserProfile() {
const res = await getUserProfile()
this.$patch(res)
},
async resetUserProfile() {
this.$reset()
},
},
})
export default useUserProfile
export async function loadUserProfile() {
try {
const res = await api.get('/users/me')
const { username, email, progress } = res.data
store.username = username
store.email = email
store.progress = progress
return store
} catch (err: any) {
console.error(err)
throw new Error(`Failed to load user profile: ${err.message}`)
}
}
export function resetUserProfile() {
store.username = ''
store.email = ''
store.progress = {}
}
export default store
-40
View File
@@ -1,8 +1,4 @@
import axios from 'axios'
import { IQuest } from '@linux-odyssey/models'
import { GetSessionsRequest } from '@linux-odyssey/constants'
import { Session } from '../types'
import { UserProfile } from '../store/userProfile'
const api = axios.create({
baseURL: '/api/v1',
@@ -12,39 +8,3 @@ const api = axios.create({
withCredentials: true,
})
export default api
export async function getUserProfile(): Promise<UserProfile> {
const res = await api.get<UserProfile>('/users/me')
return res.data
}
export async function getQuest(id: string): Promise<IQuest> {
const res = await api.get<IQuest>(`/quests/${id}`)
return res.data
}
export async function createSession(questId: string): Promise<Session> {
const res = await api.post<Session>('/sessions', { questId })
return res.data
}
export async function getSessions(
request: GetSessionsRequest
): Promise<Session[]> {
const res = await api.get<Session[]>('/sessions', { params: request })
return res.data
}
export async function getActiveSession(
questId: string
): Promise<Session | null> {
const res = await api.get<Session | null>('/sessions/active', {
params: { questId },
})
return res.data
}
export async function getQuests(): Promise<IQuest[]> {
const res = await api.get<IQuest[]>('/quests')
return res.data
}
-40
View File
@@ -1,40 +0,0 @@
import formbricks from '@formbricks/js/website'
import useSession from '../store/session'
import useUserProfile from '../store/userProfile'
if (typeof window !== 'undefined') {
formbricks.init({
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
apiHost: 'https://app.formbricks.com',
})
}
export function openQuestSurvey() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('quest_completed', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export function openBugReport() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('bug_report', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export default formbricks
+1
View File
@@ -46,6 +46,7 @@ class SocketTerminal {
mount(reference: HTMLElement): void {
this.term.open(reference)
this.term.focus()
this.resizeScreen()
}
+35 -38
View File
@@ -1,30 +1,19 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import useSession from '../store/session'
import useUserProfile from '../store/userProfile'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import DescriptionPart from '../components/game/DescriptionPart.vue'
import QuestPart from '../components/game/QuestPart.vue'
import TerminalPart from '../components/game/TerminalPart.vue'
import VisualizationPart from '../components/game/VisualizationPart.vue'
import ControlPalette from '../components/game/ControlPalette.vue'
import CompleteModal from '../components/game/CompleteModal.vue'
import StartButton from '../components/game/StartButton.vue'
import { openQuestSurvey } from '../utils/formbricks'
const sessionStore = useSession()
const userStore = useUserProfile()
import { computed, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import sessionStore, { init } from '../store/session'
import GameHeaderPart from '../components/GameHeaderPart.vue'
// import CommandlistPart from '../components/CommandlistPart.vue'
import HintPart from '../components/HintPart.vue'
import QuestPart from '../components/QuestPart.vue'
import TerminalPart from '../components/TerminalPart.vue'
import VisualizationPart from '../components/VisualizationPart.vue'
import ControlPalette from '../components/ControlPalette.vue'
import CompleteModal from '../components/CompleteModal.vue'
import { LoadQuestError, LoadSessionError } from '../utils/errors'
const completed = computed(() => {
return sessionStore.session?.status === 'finished'
})
watch(completed, (newValue, oldValue) => {
if (newValue && !oldValue) {
console.log('finish', userStore)
openQuestSurvey()
}
return sessionStore.session.status === 'finished'
})
const props = defineProps({
@@ -34,30 +23,39 @@ const props = defineProps({
},
})
const toast = useToast()
onMounted(async () => {
sessionStore.reset()
sessionStore.setup()
await sessionStore.setQuest(props.questId)
await sessionStore.getActiveSession()
try {
await init(props.questId)
} catch (err: any) {
if (err instanceof LoadQuestError) {
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
return
}
if (err instanceof LoadSessionError) {
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
return
}
console.error(err)
toast.error(err.message)
}
})
</script>
<template>
<!-- game header -->
<div class="w-full">
<HeaderPart
:title="sessionStore.quest?.title"
:headerComponent="GameHeaderComponents"
/>
<div class="h-[6vh] w-full">
<GameHeaderPart :title="sessionStore.quest?.title" />
</div>
<!-- current status indicator -->
<input
type="hidden"
id="currentStatus"
:value="sessionStore.session?.status"
:value="sessionStore.session.status"
/>
<!-- main -->
<div id="main" class="h-full pt-10 w-full flex p-3 space-x-3">
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
<!-- Topic and Command List -->
<div class="bg-bg-secondary h-full w-1/4 rounded-lg">
<section id="quest" class="h-full p-3 overflow-auto">
@@ -69,10 +67,9 @@ onMounted(async () => {
</section> -->
</div>
<!-- Terminal and Hint -->
<div class="bg-bg h-full rounded-lg w-1/2">
<StartButton :questId="$props.questId" />
<div class="bg-bg h-full w-1/2 rounded-lg">
<section id="hint" class="h-3/5">
<DescriptionPart />
<HintPart />
</section>
<hr class="border-border border" />
<section id="terminal" class="h-2/5 overflow-hidden">
+101 -27
View File
@@ -1,19 +1,97 @@
<script setup lang="ts">
import ContactButton from '../components/ContactButton.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import LandingHeaderComponents from '../components/header/LandingHeaderComponents.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import { bugReportUrl, facebookUrl, githubUrl } from '../config'
import { facebookUrl, githubUrl } from '../config'
const scrollToSection = (sectionId: string) => {
const section = document.getElementById(sectionId)
if (section) {
const container = document.querySelector('.overflow-y-auto') // Replace '.overflow-y-auto' with the appropriate class or selector for your container
if (container) {
container.scrollTo({
top: section.offsetTop,
behavior: 'smooth', // Add smooth scrolling behavior if desired
})
}
}
}
</script>
<template>
<div class="w-screen h-screen bg-scroll bg-bg">
<DynamicBackground class="w-full h-full" />
<div class="h-screen w-screen absolute top-0 left-0">
<!-- game header -->
<HeaderPart :headerComponent="LandingHeaderComponents" class="z-50" />
<div
class="w-full h-[6vh] px-5 flex flex-row items-center justify-between bg-bg-secondary"
>
<div class="flex flex-row items-center gap-3">
<img src="../img/icon_totem.svg" class="h-8 justify-self-start" />
<button
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1"
style="font-size: 2vh"
>
Linux Odyssey
</button>
</div>
<div class="flex flex-row items-center gap-3">
<button
id="Intro"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('intro')"
style="font-size: 2vh"
>
Intro
</button>
<button
id="Feature"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('feature')"
style="font-size: 2vh"
>
Feature</button
><button
id="Info"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('about')"
style="font-size: 2vh"
>
About
</button>
<button
id="Contact"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
@click="scrollToSection('contact')"
style="font-size: 2vh"
>
Contact Us
</button>
<RouterLink
title="Sign In"
id="SignIn"
to="/register"
class="text-text inline-block font-bold whitespace-nowrap pt-1"
style="font-size: 2vh"
>
Sign In
</RouterLink>
<a
title="Bug Report"
:href="bugReportUrl"
target="_blank"
class="h-5 w-5"
>
<font-awesome-icon
:icon="['fas', 'bug']"
class="text-text-primary h-full w-full"
/>
</a>
</div>
</div>
<!-- main content -->
<main class="w-full h-full overflow-y-auto">
<main class="w-full h-[94vh] overflow-y-auto">
<div class="w-full flex flex-col items-center">
<div
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
@@ -26,6 +104,7 @@ import { facebookUrl, githubUrl } from '../config'
Odyssey
</h1>
</div>
<!-- intro -->
<div class="w-full md:w-2/3 px-5">
<section
@@ -149,43 +228,40 @@ import { facebookUrl, githubUrl } from '../config'
鈦坦敏捷開發特別獎 第二名
</h1>
</div>
<div class="container mx-auto">
<div
class="justify-items-center grid grid-cols-1 sm:grid-cols-3 gap-5"
>
<h1
class="text-text-primary text-3xl font-extrabold font-mono sm:col-span-3 gap-6"
<!-- <h1
class="text-text-primary p-5 font-extrabold font-mono col-span-3 gap-3"
style="font-size: 3.5vh"
>
玩家感想
<!--可以做成翻頁卡牌一次一個-->
</h1>
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"很好玩很像益智遊戲"
</h1>
<h1 class="text-text p-3" style="font-size: 2.5vh">
"我從來不知道自由軟體的存在,謝謝你們。"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"還蠻好玩的,讓程式變好玩"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前不喜歡電腦課現在覺得喜歡一點點"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"程式真的對新手很友善,讓我學到了很多東西"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"感覺做得很好很簡明易懂關卡設計很用心"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前我覺得寫程式很難不過現在覺得很有趣"
</h1>
<h1 class="text-xl text-center">
<h1 class="text-text p-3" style="font-size: 2.5vh">
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
</h1>
</div>
</div>
</h1> -->
</section>
</div>
<section
@@ -195,9 +271,7 @@ import { facebookUrl, githubUrl } from '../config'
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
聯絡我們
</h1>
<div
class="flex flex-row items-center justify-around mt-5 gap-5 mb-5"
>
<div class="flex flex-row items-center justify-around mt-5 gap-5">
<ContactButton
:url="facebookUrl"
:icons="['fab', 'facebook']"
+3 -4
View File
@@ -2,8 +2,7 @@
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import api from '../utils/api'
const leaderboard = ref<
@@ -36,8 +35,8 @@ onMounted(async () => {
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="w-full">
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
<div class="h-[6vh] w-full">
<GameHeaderPart title="排行榜" />
</div>
<div class="w-fit h-[94vh] m-3">
<div
+2 -4
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
import AuthForm from '../components/AuthForm.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import Background from '../components/DynamicBackground.vue'
import {
TooManyRequestsError,
@@ -46,11 +45,10 @@ const handleLogin = async ({
<template>
<div class="w-screen h-screen">
<DynamicBackground class="w-full h-full" />
<Background class="w-full h-full" />
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<HeaderPart />
<div class="w-fit">
<AuthForm @onSubmit="handleLogin" type="login" />
</div>
+4 -9
View File
@@ -1,18 +1,13 @@
<script setup lang="ts">
import QuestMap from '../components/QuestMap.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
</script>
<template>
<div class="w-fit">
<HeaderPart
title="關卡地圖"
:headerComponent="GameHeaderComponents"
class="z-50"
/>
<div class="h-[6vh] w-full">
<GameHeaderPart title="關卡地圖" />
</div>
<div class="flex flex-col justify-center items-center w-full h-full pt-5">
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
<QuestMap />
</div>
</template>
+4 -5
View File
@@ -3,8 +3,8 @@ import validator from 'validator'
import { isValidUsername } from '@linux-odyssey/utils'
import { passwordPolicy } from '@linux-odyssey/constants'
import AuthForm from '../components/AuthForm.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import Background from '../components/DynamicBackground.vue'
import { checkUsername, register } from '../utils/auth'
import {
UnauthorizedError,
@@ -89,12 +89,11 @@ async function check({
<template>
<div class="w-screen h-screen">
<DynamicBackground class="w-full h-full" />
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<HeaderPart />
<div class="w-fit mt-8">
<div class="w-fit">
<AuthForm
@onSubmit="handleRegister"
@onChange="check"
-3
View File
@@ -24,9 +24,6 @@ export default {
DEFAULT: '#FF0000',
},
},
backgroundImage: {
catelogbg: "url('/catelogbg.svg')",
},
},
},
plugins: [],
+95
View File
@@ -0,0 +1,95 @@
version: '3'
services:
app:
depends_on:
- backend
build:
context: .
dockerfile: app/Dockerfile
target: dev
ports:
- 5173:5173
networks:
- internal
volumes:
- ./app:/app/app
- ./packages:/app/packages
backend:
depends_on:
- db
build:
context: .
dockerfile: server/Dockerfile
target: dev
networks:
- internal
- players
ports:
- '3000:3000'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./server/src:/app/server/src
- ./server/dist:/app/server/dist
- ./server/swagger.yaml:/app/server/swagger.yaml
- ./packages:/app/packages
- ./quests:/app/quests
- ./logs:/app/logs
environment:
- BASE_URL=http://localhost:5173
- MONGO_URL=mongodb://db:27017/odyssey-test
- DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- SURVEY_URL=${SURVEY_URL}
- BUG_REPORT_URL=${BUG_REPORT_URL}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- TESTING=true
- TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234
- HOST_PWD=${PWD}
- MOUNT_QUEST=${MOUNT_QUEST}
analytics:
build:
context: .
dockerfile: packages/analytics/Dockerfile
target: dev
depends_on:
- db
networks:
- internal
ports:
- '3001:3001'
environment:
- MONGO_URL=mongodb://db:27017/odyssey-test
- ADMIN_USERNAME=alex
- ADMIN_PASSWORD=Alex1234
volumes:
- ./packages/analytics:/app/packages/analytics
- ./packages/models:/app/packages/models
db:
image: mongo
restart: always
networks:
- internal
ports:
- '27017:27017'
volumes:
- ./data:/data/db
base:
image: linuxodyssey/quest-base
build:
context: .
dockerfile: ./quests/Dockerfile
networks:
internal:
players:
name: linux-odyssey-players
internal: true
-23
View File
@@ -1,23 +0,0 @@
version: '3'
services:
db:
image: mongo
restart: always
networks:
- internal
ports:
- '27017:27017'
volumes:
- ./data:/data/db
base:
image: linuxodyssey/quest-base
build:
context: .
dockerfile: ./quests/Dockerfile
networks:
internal:
players:
name: linux-odyssey-players
internal: true
-1
View File
@@ -54,7 +54,6 @@
"typescript": "^5.3.3"
},
"dependencies": {
"latest": "^0.2.0",
"typescript-eslint": "^7.4.0"
}
}
+3 -10
View File
@@ -1,5 +1,3 @@
import { z } from 'zod'
export const passwordPolicy = {
minLength: 8,
minNumbers: 1,
@@ -8,11 +6,6 @@ export const passwordPolicy = {
minSymbols: 0,
}
export const getSessionsRequestSchema = z.object({
questId: z.string().optional(),
status: z.enum(['active', 'inactive', 'finished']).optional(),
limit: z.coerce.number().optional(),
order: z.enum(['asc', 'desc']).optional(),
})
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
export default {
passwordPolicy,
}
-3
View File
@@ -10,8 +10,5 @@
"scripts": {
"build": "tsc",
"dev": "tsc -w"
},
"dependencies": {
"zod": "^3.23.8"
}
}
-6
View File
@@ -28,7 +28,6 @@ export interface IUser {
socialLogins: Map<string, ISocialLogin>
createdAt: Date
updatedAt: Date
isGuest: boolean
}
export const User = model<IUser>(
@@ -52,11 +51,6 @@ export const User = model<IUser>(
default: {},
required: true,
},
isGuest: {
type: Boolean,
default: false,
required: true,
},
},
{
timestamps: true,
+3 -3
View File
@@ -5,12 +5,12 @@ interface Node {
export default class DAG {
private nodes = new Map()
private edges = new Set<[string, string]>()
private edges = new Set()
private layers: number[] = []
constructor(nodes: Node[]) {
this.nodes = new Map(nodes.map((node) => [node._id, node]))
this.edges = new Set<[string, string]>()
this.edges = new Set()
this.nodes.forEach((_, id) => {
this.setLayer(id)
this.addEdges(id)
@@ -42,7 +42,7 @@ export default class DAG {
if (!node) {
throw new Error(`Node ${id} not found`)
}
return this.layers[node.layer - 1] || 0
return this.layers[node.layer - 1]
}
private setLayer(id: string) {
+22 -12
View File
@@ -1,23 +1,25 @@
title: 輸出文字
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `echo` 在終端機上顯示文字。
1. 如何使用 `echo` 指令
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始教程
condition:
command:
- ^echo start$
output:
- ^start$
responses:
- content: |
還記得第一關使用的指令 `echo` 嗎?
`echo` 指令可以將後方接續的文字輸出到終端機上。
例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
你可以試著輸入 `echo ` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
- content:
- 你剛才使用的指令叫做 `echo`。
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
hints:
- ➤ 輸入 `echo` 並在後面加上一些文字
- ➤ 例如:`echo example`
@@ -41,8 +43,8 @@ stages:
requirements:
- echo
condition:
command:
- ^echo [Hh]ow are you\??$
output:
- ^[Hh]ow are you\??$
exceptions:
- condition:
command:
@@ -57,9 +59,17 @@ stages:
responses:
- type: narrative
content:
- 在 `echo ` 後面還需要加上 `How are you?`
- 在 `echo` 後面還需要加上一些文字
responses:
- content:
- 你成功的在終端機上顯示了 `How are you?`。
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
exceptions:
- repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
+131
View File
@@ -0,0 +1,131 @@
title: 輸出文字
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `echo` 指令
2. `echo` 指令在遊戲中的用途
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 你剛才使用的指令叫做 `echo`。
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
hints:
- ➤ 輸入 `echo` 並在後面加上一些文字
- ➤ 例如:`echo example`
- id: echo
task: 在終端機上顯示顯示任意文字
requirements:
- start
condition:
command:
- ^echo .*
responses:
- content:
- 你應該可以看到終端機上顯示了你在 `echo` 指令後面的文字。
- 接下來,請你試著在終端機上顯示 `How are you?`。
hints:
- ➤ 利用 `echo` 指令來在終端機上顯示 `How are you?`
- id: how-are-you
task: 在終端機上顯示 `How are you?`
requirements:
- echo
condition:
output:
- ^[Hh]ow are you\??$
exceptions:
- condition:
command:
- ^[Hh]ow are you\??$
responses:
- type: narrative
content:
- 在 `How are you?` 前面必須加上 `echo`
- condition:
command:
- ^echo$
responses:
- type: narrative
content:
- 在 `echo` 後面還需要加上一些文字
responses:
- content:
- 你成功的在終端機上顯示了 `How are you?`。
- 相信你已經了解 `echo` 指令的用法了。
- 接下來,要像你介紹 `echo` 在遊戲中的特殊功能,那就是詠唱咒語。
- 你可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
- 但是要注意,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
- 在真實的 Linux 環境中,詠唱咒語並不會有任何特殊效果。
- 現在,有一段咒語叫做 `HELP`,請你試著使用 `echo` 詠唱這段咒語。
hints:
- ➤ 試著使用 `echo` 來詠唱 `HELP` 咒語
- ➤ 注意:咒語要使用大寫
- id: help
task: 詠唱 `HELP` 咒語
requirements:
- how-are-you
condition:
output:
- ^HELP$
responses:
- content:
- 你詠唱了 `HELP` 咒語,一位精靈出現了。
- type: dialogue
speaker: Unix 精靈
content:
- 你好,我是 Unix 精靈。
- 既然你使用了 `HELP` 咒語呼喚我,就讓我來教你一個咒語吧。
- 讓我想想... 好了,我想到了一個咒語,叫做 `WATER`。
- 這是施展水魔法的咒語。詠唱這個咒語可以施展水魔法,產生少量的水。
- 試著使用 `echo` 詠唱這個咒語吧。
hints:
- ➤ 詠唱 `WATER` 咒語
- id: END
task: 詠唱 `WATER` 咒語
requirements:
- help
condition:
output:
- ^WATER$
responses:
- content:
- 你施展了水魔法,一個水球出現在你的手中。
- type: dialogue
speaker: Unix 精靈
content:
- 很好,你已經學會了水魔法。
- content:
- 真厲害,你已經了解如何使用 `echo` 指令,並且學會了詠唱咒語的方法。
- 最後複習一下,你可以通過輸入 `echo [文字]` 來在終端機上顯示文字。
- 你也可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
- 不要忘了,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
- 在真實的 Linux 環境中,咒語只會被視為一串文字顯示出來而已。
- 以上就是 `echo` 指令的用法,請繼續努力,完成接下來的關卡吧!
hints: |
複習:
通過輸入 `echo [文字]` 來在終端機上顯示文字
通過輸入 `echo [咒語]` 來詠唱咒語
咒語只有在 Linux Odyssey 中才會有特殊效果
-3
View File
@@ -1,3 +0,0 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
-7
View File
@@ -1,7 +0,0 @@
echo start
echo next
ls
cd folder03
pwd
cd ../folder02
ls ../folder01
-110
View File
@@ -1,110 +0,0 @@
title: 測驗一
instruction: |
在這個關卡中,將會考驗你是否理解以下內容:
1. `echo`:用來輸出文字
2. `ls`:用來查看資料夾的內容
3. `cd`:用來在資料夾之間移動
4. `pwd`:用來顯示目前的路徑
requirements:
- path
stages:
- id: start
task: 按下 `開始` 按鈕開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content: |
這個關卡將會測試你是否理解之前的教學內容。
因此不會告訴你要輸入什麼指令。
請思考並應用正確的指令來完成任務。
現在,請讓終端機輸出 `next` 來繼續。
- id: next
task: 讓終端機輸出 `next`
requirements:
- start
condition:
output:
- ^next$
responses:
- type: narrative
content: |
現在請你查看目前資料夾的內容。
- id: ls-~
task: 查看目前資料夾的內容
requirements:
- next
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- type: narrative
content: |
請進入 `folder03` 資料夾。
- id: cd-folder03
task: 進入 `folder03` 資料夾
requirements:
- ls-~
condition:
pwd:
- ^/home/commander/folder03$
responses:
- type: narrative
content: |
請在終端機顯示目前的路徑。
- id: pwd-folder03
task: 顯示目前的路徑
requirements:
- cd-folder03
condition:
command:
- ^pwd$
output:
- ^/home/commander/folder03$
responses:
- type: narrative
content: |
現在請你進入 `folder02` 資料夾,
並查看 `folder02` 資料夾的內容。
- id: ls-folder02
task: 進入並查看 `folder02` 資料夾的內容
requirements:
- pwd-folder03
condition:
pwd:
- ^/home/commander/folder02$
command:
- ^ls$
responses:
- type: narrative
content: |
最後,請你留在 `folder02` 資料夾,
並查看 `folder01` 資料夾的內容。
- id: END
requirements:
- ls-folder02
task: 留在 `folder02` 資料夾,查看 `folder01` 資料夾的內容。
condition:
pwd:
- ^/home/commander/folder02$
command:
- ^ls
output:
- file1.txt
responses:
- type: narrative
content: |
恭喜你通過了這個測驗!
你可以繼續學習之後的教學內容了。
Whitespace-only changes.
Whitespace-only changes.
-3
View File
@@ -1,3 +0,0 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
-7
View File
@@ -1,7 +0,0 @@
echo start
echo next
ls
cat hello.txt
cat pneumonoultramicroscopicsilicovolcanoconiosis.txt
mkdir folder
touch folder/file.txt
-108
View File
@@ -1,108 +0,0 @@
title: 測驗二
instruction: |
在這個關卡中,將會考驗你是否理解以下內容:
1. `cat`:用來查看檔案的內容
2. `touch`:用來建立檔案
3. `mkdir`:用來建立資料夾
4. 用 `tab` 鍵來自動補全
5. 之前測驗包含的內容
requirements:
- create
stages:
- id: start
task: 按下 `開始` 按鈕開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content: |
這個關卡將會測試你是否理解之前的教學內容。
因此不會告訴你要輸入什麼指令。
請思考並應用正確的指令來完成任務。
現在,請讓終端機輸出 `next` 來繼續。
- id: next
task: 讓終端機輸出 `next`
requirements:
- start
condition:
output:
- ^next$
responses:
- type: narrative
content: |
現在請你查看目前資料夾的內容。
- id: ls-~
task: 查看目前資料夾的內容
requirements:
- next
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- type: narrative
content: |
請查看 `hello.txt` 檔案的內容。
- id: cat-hello
task: 查看 `hello.txt` 檔案內容
requirements:
- ls-~
condition:
command:
- ^cat hello.txt$
output:
- ^This is `hello.txt`.$
responses:
- type: narrative
content: |
請查看另一個有著很長檔名的檔案內容。
- id: cat-long-filename
task: 查看另一個檔案的內容
requirements:
- cat-hello
condition:
command:
- ^cat pneumonoultramicroscopicsilicovolcanoconiosis.txt$
output:
- ^A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.$
responses:
- type: narrative
content: |
現在請你建立一個名稱為 `folder` 的資料夾。
- id: mkdir-folder
task: 建立名稱為 `folder` 的資料夾
requirements:
- cat-long-filename
condition:
files:
- path: /home/commander/folder
type: folder
responses:
- type: narrative
content: |
最後,請你在 `folder` 資料夾中,
建立檔名為 `file.txt` 的檔案。
- id: END
requirements:
- mkdir-folder
task: 在 `folder` 中建立檔名為 `file.txt` 的檔案
condition:
files:
- path: /home/commander/folder/file.txt
type: file
responses:
- type: narrative
content: |
恭喜你通過了這個測驗!
你可以繼續學習之後的教學內容了。
-1
View File
@@ -1 +0,0 @@
This is `hello.txt`.
@@ -1 +0,0 @@
A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.
-7
View File
@@ -1,7 +0,0 @@
FROM linuxodyssey/quest-base
USER root
COPY home .
RUN chown -R ${USERNAME}:${USERNAME} .
USER ${USERNAME}
-12
View File
@@ -1,12 +0,0 @@
echo start
echo next
ls
cd folder
cp hello.txt world.txt
rm hello.txt
mv world.txt ..
cd ..
mv world.txt hello.txt
cat hello.txt
rmdir folder
rm -r trash_can
-153
View File
@@ -1,153 +0,0 @@
title: 測驗三
instruction: |
在這個關卡中,將會考驗你是否理解以下內容:
1. `cp`:用來複製檔案和資料夾
2. `mv`:用來移動檔案和資料夾或更改名稱
3. `rm`:用來刪除檔案和資料夾
4. `rmdir`:用來刪除空的資料夾
5. 透過選項讓指令達成不同的功能,例如 `rm -r`
6. 之前測驗包含的內容
requirements:
- remove
stages:
- id: start
task: 按下 `開始` 按鈕開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content: |
這個關卡將會測試你是否理解之前的教學內容。
因此不會告訴你要輸入什麼指令。
請思考並應用正確的指令來完成任務。
現在,請讓終端機輸出 `next` 來繼續。
- id: next
task: 讓終端機輸出 `next`
requirements:
- start
condition:
output:
- ^next$
responses:
- type: narrative
content: |
現在請你查看目前資料夾的內容。
- id: ls-~
task: 查看目前資料夾的內容
requirements:
- next
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- type: narrative
content: |
請複製 `folder` 資料夾中的檔案到 `folder` 中,並命名為 `world.txt` 。
- id: cp-hello-world
task: 複製 `folder` 中的檔案並命名為 `world.txt`
requirements:
- ls-~
condition:
command:
- ^cp .*hello.txt .*world.txt$
files:
- path: /home/commander/folder/hello.txt
type: file
- path: /home/commander/folder/world.txt
type: file
responses:
- type: narrative
content: |
請刪除 `hello.txt` 檔案。
- id: rm-hello
task: 刪除 `hello.txt`
requirements:
- cp-hello-world
condition:
command:
- ^rm .*hello.txt$
files:
- path: /home/commander/folder/hello.txt
type: file
exists: false
responses:
- type: narrative
content: |
現在請將 `world.txt` 移出 `folder` 資料夾。
(移到 `commander` 資料夾中)
- id: mv-world-..
task: 將 `world.txt` 移出 `folder` 資料夾
requirements:
- rm-hello
condition:
files:
- path: /home/commander/folder/world.txt
type: file
exists: false
- path: /home/commander/world.txt
type: file
responses:
- type: narrative
content: |
將 `world.txt` 的名稱改回 `hello.txt` ,
並查看檔案的內容。
- id: cat-hello
task: 將 `world.txt` 的名稱改回 `hello.txt` 後查看檔案內容
requirements:
- mv-world-..
condition:
command:
- ^cat hello.txt$
files:
- path: /home/commander/world.txt
type: file
exists: false
- path: /home/commander/hello.txt
type: file
output:
- ^This is `hello.txt`.$
responses:
- type: narrative
content: |
刪除 `folder` 資料夾。
- id: rmdir-folder
task: 刪除 `folder` 資料夾
requirements:
- cat-hello
condition:
files:
- path: /home/commander/folder
type: folder
exists: false
responses:
- type: narrative
content: |
刪除 `trash_can` 資料夾。
- id: END
requirements:
- rmdir-folder
task: 刪除 `trash_can` 資料夾
condition:
files:
- path: /home/commander/trash_can
type: folder
exists: false
responses:
- type: narrative
content: |
恭喜你通過了這個測驗!
你可以繼續學習之後的教學內容了。
@@ -1 +0,0 @@
This is `hello.txt`.
Whitespace-only changes.
Whitespace-only changes.
@@ -1 +0,0 @@
aHR0cHM6Ly9yZXVybC5jYy9YUjZZMDA=
Whitespace-only changes.
+1 -2
View File
@@ -1,8 +1,7 @@
echo start
ls
cat hello.txt
cat world.txt
echo next
echo continue
cp hello.txt hello2.txt
cat hello2.txt
cp world.txt world2.txt
+83 -230
View File
@@ -1,186 +1,122 @@
title: 複製檔案
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `cp` 複製檔案
1. 如何用 `cp` 指令複製檔案
準備好就輸入 `echo start` 開始吧!
requirements:
- create
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content: |
今天我們要學習如何複製檔案。
先用 `ls` 指令查看目前資料夾裡有哪些檔案。
hints:
- ➤ 使用 `ls` 來查看目前資料夾裡有哪些檔案
- id: ls
task: 查看目前資料夾裡的檔案
requirements:
- start
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
output:
- (^|\s)hello.txt($|\s)
- (^|\s)world.txt($|\s)
- start
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
command:
- ^echo$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
content:
- 你忘記輸入 `start` 了嗎?
- 請輸入 `echo start` 來開始
- catchAll: true
responses:
- type: narrative
content: |
可以看到現在資料夾裡有 `hello.txt` 跟 `world.txt`。
請使用 `cat` 來查看它們的內容。
看完之後,使用 `echo next` 來繼續吧!
content:
- 請輸入 `echo start` 來開始
responses:
- type: narrative
content:
- 今天我們要學習如何複製檔案。
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,請使用 `ls` 與 `cat` 來查看它們的內容。
- 看完之後,使用 `echo continue` 來繼續吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
- ➤ 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
- ➤ 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
- id: cat-hello
task: 查看 `hello.txt` 的內容
requirements:
- ls
- start
condition:
pwd:
- ^/home/commander$
command:
- ^cat hello.txt$
output:
- Hello
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
- condition:
command:
- ^cat$
responses:
- type: narrative
content: |
你忘記輸入 `cat` 了嗎?
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
content:
- 你忘記輸入 `cat` 了嗎?
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
- condition:
command:
- ^cat hello$
responses:
- type: narrative
content: |
你忘記輸入 `.txt` 了嗎?
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
content:
- 你忘記輸入 `.txt` 了嗎?
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
responses:
- type: narrative
content: |
你成功查看到 `hello.txt` 的內容了!
content:
- 你成功查看到 `hello.txt` 的內容了!
- id: cat-world
task: 查看 `world.txt` 的內容
requirements:
- ls
- start
condition:
pwd:
- ^/home/commander$
command:
- ^cat world.txt$
output:
- World
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
- condition:
command:
- ^cat$
responses:
- type: narrative
content: |
你忘記輸入 `cat` 了嗎?
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
- condition:
command:
- ^cat world$
responses:
- type: narrative
content: |
你忘記輸入 `.txt` 了嗎?
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
responses:
- type: narrative
content: |
你成功查看到 `world.txt` 的內容了!
content:
- 你成功查看到 `world.txt` 的內容了!
- id: cat-next
task: 輸入 `echo next` 來繼續
- id: cat-continue
task: 使用 `echo continue` 來繼續
requirements:
- cat-hello
- cat-world
condition:
command:
- ^echo next$
- echo
responses:
- type: narrative
content: |
接下來我們要來學習如何複製檔案。
`cp` 這個指令,是 copy 的縮寫,就是複製的意思。
我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
請你使用 `cp` 來複製 `hello.txt` 的內容到 `hello2.txt`。
content:
- 接下來我們要來學習複製檔案。
- '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
- 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
- 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
hints:
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
- id: cp-hello
task: 複製 `hello.txt` 到 `hello2.txt`
requirements:
- cat-next
- cat-continue
condition:
pwd:
- ^/home/commander$
command:
- ^cp hello.txt hello2.txt$
files:
- path: hello2.txt
type: file
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功複製 `hello.txt` 到 `hello2.txt` 了!
我們打開來確認一下 `hello2.txt` 的內容吧!
content:
- 你成功複製 `hello.txt` 到 `hello2.txt` 了!
- 我們打開來確認一下 `hello2.txt` 的內容吧!
hints:
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
@@ -189,28 +125,16 @@ stages:
- cp-hello
task: 查看 `hello2.txt` 的內容
condition:
pwd:
- ^/home/commander$
command:
- ^cat hello2.txt$
output:
- Hello
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功查看到 `hello2.txt` 的內容了!
正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
content:
- 你成功查看到 `hello2.txt` 的內容了!
- 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
- 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
hints:
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
@@ -219,28 +143,16 @@ stages:
- cat-hello2
task: 複製 `world.txt` 到 `world2.txt`
condition:
pwd:
- ^/home/commander$
command:
- ^cp world.txt world2.txt$
files:
- path: world2.txt
type: file
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功複製 `world.txt` 到 `world2.txt` 了!
我們打開來確認一下 `world2.txt` 的內容吧!
content:
- 你成功複製 `world.txt` 到 `world2.txt` 了!
- 我們打開來確認一下 `world2.txt` 的內容吧!
hints:
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
@@ -249,35 +161,23 @@ stages:
- cp-world
task: 查看 `world2.txt` 的內容
condition:
pwd:
- ^/home/commander$
command:
- ^cat world2.txt$
output:
- World
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功查看到 `world2.txt` 的內容了!
`world2.txt` 的內容跟 `world.txt` 一模一樣。
接下來我們要來學習如何複製資料夾。
現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
選項的格式通常是 `-` 加上一個字母,例如 `-r`。
選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
content:
- 你成功查看到 `world2.txt` 的內容了!
- '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
- 接下來我們要來學習如何複製資料夾。
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
hints:
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
@@ -287,8 +187,6 @@ stages:
- cat-world2
task: 複製 `notes` 資料夾到 `notes2`
condition:
pwd:
- ^/home/commander$
command:
- ^cp
files:
@@ -296,59 +194,38 @@ stages:
type: directory
- path: notes2/note.txt
type: file
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
content:
- 你成功複製 `notes` 資料夾到 `notes2` 了!
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
hints:
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
exceptions:
- condition:
error:
- 'cp: omitting directory'
responses:
- type: narrative
content: |
你忘記加上 `-r` 的參數了嗎?
請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
responses:
- type: narrative
content: |
你成功複製 `notes` 資料夾到 `notes2` 了!
我們打開來確認一下 `notes2` 資料夾的內容吧!
hints:
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
content:
- 你忘記加上 `-r` 的參數了嗎?
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- id: ls-notes2
requirements:
- cp-notes
task: 查看 `notes2` 資料夾的內容
condition:
pwd:
- ^/home/commander$
command:
- ^ls notes2$
output:
- note.txt
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功查看到 `notes2` 資料夾的內容了!
我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
content:
- 你成功查看到 `notes2` 資料夾的內容了!
- 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
- 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
hints:
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
@@ -357,25 +234,13 @@ stages:
- ls-notes2
task: 複製 `hello.txt` 到 `world.txt`
condition:
pwd:
- ^/home/commander$
command:
- ^cp hello.txt world.txt$
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
你成功複製 `hello.txt` 到 `world.txt` 了!
我們打開來確認一下 `world.txt` 的內容吧!
content:
- 你成功複製 `hello.txt` 到 `world.txt` 了!
- 我們打開來確認一下 `world.txt` 的內容吧!
hints:
- ➤ 使用 `cat world.txt`
@@ -384,26 +249,14 @@ stages:
- cp-overwrite
task: 檢查 `world.txt` 的內容
condition:
pwd:
- ^/home/commander$
command:
- ^cat world.txt$
output:
- Hello
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
你不在 `/home/commander` 資料夾裡。
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
`world.txt` 的內容變成 `Hello` 了!
這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
以上就是複製檔案的基本操作!
content:
- '`world.txt` 的內容變成 `Hello` 了!'
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
- 以上就是複製檔案的基本操作!
-1
View File
@@ -1,5 +1,4 @@
echo start
ls
touch hello.txt
ls
cat hello.txt
+53 -89
View File
@@ -4,64 +4,40 @@ instruction: |
1. 如何使用 `touch` 建立檔案
2. 如何使用 `mkdir` 建立資料夾
準備好就輸入 `echo start` 開始吧!
requirements:
- tab
- read
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start$
- ^echo start
responses:
- type: narrative
content: |
這次要學習的指令是 `touch` 指令。
請你先用 `ls` 指令查看目前所在資料夾中有哪些檔案。
你應該會發現目前所在資料夾中沒有任何檔案。
hints:
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
- id: ls
task: 查看資料夾中的檔案
requirements:
- start
condition:
command:
- ^ls$
responses:
- type: narrative
content: |
你應該可以看到終端機上沒有任何回應,這表示目前所在資料夾中沒有任何檔案。
而我們可以使用 `touch [檔案名稱]` 指令來建立一個檔案。
接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
content:
- 這次要學習的指令是 `touch` 指令。
- '`touch` 指令可以建立一個檔案。'
- 接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
hints:
- ➤ 輸入 `touch hello.txt` 來建立一個檔案
- id: touch
task: 使用 `touch` 指令來建立一個檔案
requirements:
- ls
- start
condition:
files:
- path: hello.txt
type: file
exceptions:
- condition:
files:
- path: hello$
type: file
responses:
- type: narrative
content: |
你似乎忘記輸入副檔名 `txt` 了。
請再試一次,並確保檔名為 `hello.txt`。
responses:
- type: narrative
content: |
你應該可以看到終端機上沒有任何回應。
然而實際上 `touch` 指令已經成功建立了一個檔案。
請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
content:
- 你應該可以看到終端機上沒有任何回應。
- 這表示 `touch` 指令已經成功建立了一個檔案。
- 接下來,請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
hints:
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
@@ -76,28 +52,26 @@ stages:
- hello.txt
responses:
- type: narrative
content: |
你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
我們來看看 `hello.txt` 檔案的內容吧!
我們可以使用之前學過的 `cat` 指令來查看檔案內容。
content:
- 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
- 我們來看看 `hello.txt` 檔案的內容吧!
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
hints:
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
- id: cat
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
requirements:
- ls-touch
condition:
command:
- ^cat hello.txt$
responses:
- type: narrative
content: |
咦?竟然什麼都沒有!
沒錯,使用 `touch` 指令建立的檔案是空的。
我們在未來會學習如何編輯檔案的內容。
再來試試建立其他的檔案吧!
請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
content:
- 咦?竟然什麼都沒有!
- 沒錯,使用 `touch` 指令建立的檔案是空的。
- 我們在未來會學習如何編輯檔案的內容。
- 再來試試建立其他的檔案吧!
- 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
hints:
- ➤ 輸入 `touch world.txt` 來建立一個檔案
@@ -109,21 +83,11 @@ stages:
files:
- path: world.txt
type: file
exceptions:
- condition:
files:
- path: world$
type: file
responses:
- type: narrative
content: |
你似乎忘記輸入副檔名 `txt` 了。
請再試一次,並確保檔名為 `world.txt`。
responses:
- type: narrative
content: |
非常好,你已經成功建立了第二個檔案。
接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
content:
- 非常好,你已經成功建立了第二個檔案。
- 接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
hints:
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
@@ -135,16 +99,16 @@ stages:
command:
- ^ls$
output:
- (^|\s)hello.txt($|\s)
- (^|\s)world.txt($|\s)
- hello.txt
- world.txt
responses:
- type: narrative
content: |
你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
接下來,我們來試試看建立資料夾吧!
`mkdir` 指令可以建立一個空的資料夾。
使用方式是 `mkdir [資料夾名稱]`。
請你試著在終端機上建立一個資料夾,名稱為 `notes`。
content:
- 你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
- 接下來,我們來試試看建立資料夾吧!
- '`mkdir` 指令可以建立一個空的資料夾。'
- 使用方式是 `mkdir [資料夾名稱]`。
- 請你試著在終端機上建立一個資料夾,名稱為 `notes`。
hints:
- ➤ 輸入 `mkdir notes` 來建立一個資料夾
@@ -158,9 +122,9 @@ stages:
type: folder
responses:
- type: narrative
content: |
非常好,你已經成功建立了 `notes` 資料夾。
接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
content:
- 非常好,你已經成功建立了 `notes` 資料夾。
- 接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
- id: ls-mkdir
task: 使用 `ls` 查看剛建立的資料夾
@@ -173,11 +137,11 @@ stages:
- notes
responses:
- type: narrative
content: |
你應該可以看到終端機上顯示了 `notes` 這個資料夾。
接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
首先,我們要先進入 `notes` 資料夾。
還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
content:
- 你應該可以看到終端機上顯示了 `notes` 這個資料夾。
- 接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
- 首先,我們要先進入 `notes` 資料夾。
- 還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
hints:
- ➤ 輸入 `cd notes` 來進入 `notes` 資料夾
- ➤ 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
@@ -191,9 +155,9 @@ stages:
- ^/home/commander/notes$
responses:
- type: narrative
content: |
你已經成功進入了 `notes` 資料夾。
接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
content:
- 你已經成功進入了 `notes` 資料夾。
- 接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
hints:
- ➤ 輸入 `touch note.txt` 來建立一個檔案
@@ -207,9 +171,9 @@ stages:
type: file
responses:
- type: narrative
content: |
非常好,你已經成功建立了 `note.txt` 檔案。
跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
content:
- 非常好,你已經成功建立了 `note.txt` 檔案。
- 跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
- id: END
task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
@@ -222,7 +186,7 @@ stages:
- note.txt
responses:
- type: narrative
content: |
你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
恭喜你完成了這個關卡!
在下一個關卡中,你將學習如何複製檔案與資料夾。
content:
- 你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
- 恭喜你完成了這個關卡!
- 在下一個關卡中,你將學習如何複製檔案與資料夾。
+78 -35
View File
@@ -4,96 +4,139 @@ exceptions:
- '[\uff21-\uff3a\uff41-\uff5a]'
responses:
- type: narrative
content: |
!! 請使用半形英文字母 !!
content:
- '!! 請使用半形英文字母 !!'
- condition:
command:
- ' '
responses:
- type: narrative
content: |
!! 請使用半形空格 !!
content:
- '!! 請使用半形空格 !!'
- condition:
or:
- command:
- '`'
- command:
- \'
responses:
- type: narrative
content:
- '!! 不需要輸入上下引號 !!'
- condition:
command:
- ^echo\S
- ^echo\S$
responses:
- type: narrative
content: |
!! `echo` 後面要加空格 !!
content:
- '!! `echo` 後面要加空格 !!'
- condition:
command:
- ^cd\S
- ^cd\S$
responses:
- type: narrative
content: |
!! `cd` 後面要加空格 !!
content:
- '!! `cd` 後面要加空格 !!'
- condition:
command:
- ^ls\S
- ^ls\S$
responses:
- type: narrative
content: |
!! `ls` 後面要加空格 !!
content:
- '!! `ls` 後面要加空格 !!'
- condition:
command:
- ^cat\S
- ^cat\S$
responses:
- type: narrative
content: |
!! `cat` 後面要加空格 !!
content:
- '!! `cat` 後面要加空格 !!'
- condition:
command:
- ^touch\S
- ^touch\S$
responses:
- type: narrative
content: |
!! `touch` 後面要加空格 !!
content:
- '!! `touch` 後面要加空格 !!'
- condition:
not:
command:
- ^rmdir
- ^rmdir$
command:
- ^rm\S
- ^rm\S$
responses:
- type: narrative
content: |
!! `rm` 後面要加空格 !!
content:
- '!! `rm` 後面要加空格 !!'
- condition:
command:
- ^mv\S
- ^mv\S$
responses:
- type: narrative
content: |
!! `mv` 後面要加空格 !!
content:
- '!! `mv` 後面要加空格 !!'
- condition:
command:
- ^mkdir\S
- ^mkdir\S$
responses:
- type: narrative
content: |
!! `mkdir` 後面要加空格 !!
content:
- '!! `mkdir` 後面要加空格 !!'
- condition:
command:
- ^rmdir\S
- ^rmdir\S$
responses:
- type: narrative
content: |
!! `rmdir` 後面要加空格 !!
content:
- '!! `rmdir` 後面要加空格 !!'
- condition:
command:
- ^cp\S
- ^cp\S$
responses:
- type: narrative
content: |
!! `cp` 後面要加空格 !!
content:
- '!! `cp` 後面要加空格 !!'
- condition:
command:
- ^[Ii1]s$
responses:
- type: narrative
content:
- '!! `ls` 的首字母是小寫的 `L` !!'
- condition:
command:
- 嗨
responses:
- type: narrative
content:
- '嗨~'
- condition:
command:
- folderl
responses:
- type: narrative
content:
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
- condition:
command:
- filel
responses:
- type: narrative
content:
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
-6
View File
@@ -1,6 +0,0 @@
FROM linuxodyssey/quest-base
USER root
COPY home ${HOME}
USER commander
-80
View File
@@ -1,80 +0,0 @@
# 執行檔案
## Outline
1. 什麼是可執行檔
2. 使用 `which` 指令查看執行檔的位置
3. 如何執行可執行檔
---
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
要查看一個指令它的檔案位置,可以使用 `which` 指令。
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
> 使用 `which ls` 來查看 `ls` 的路徑
---
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
> 使用 `which cat` 查看 `cat` 的路徑
---
我們要執行一個可執行檔時,只要直接輸入它的路徑就可以執行它。
我們來試看看使用執行檔的路徑來執行指令。
> 輸入 `/bin/ls` 來查看當前的資料夾
---
你會看到它列出了我們資料夾內的檔案,你可以自己比較看看,跟單純打 `ls` 的結果會一模一樣。
它一樣可以使用參數。我們來用 `cat` 試試看。
> 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
---
你會看到了 `hello.txt` 的內容,效果跟單純用 `cat` 一模一樣。
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
其實指令,就是由 **一個執行檔** + **參數** 所構成。
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
我們也可以執行不是內建的指令,像是現在資料夾中的 `dog` 檔案。
> 使用 `cat dog` 來查看 `dog` 的內容
---
你會看到這是一個用 `echo` 和 `cat` 所組成的簡單程式。
詳細的內容不用了解沒關係,功能是和 `cat` 一樣,用來印出檔案內容的程式。
但是這支程式,在沒有給要印出的檔案時,不會停留在輸入狀態。
當我們要執行一個檔案時,就是用指定路徑的方式。
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`,我們也可以用簡寫 `~/dog`。
> 輸入 `~/dog` 來執行 `dog` 檔案
---
你看到了終端機輸出這隻傲嬌的狗。
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
`.` 在路徑中,代表的就是當前的資料夾。
那我們再試試看使用相對路徑,並讓牠接收一個檔案作為參數。
> 輸入 `./dog hello.txt` 來印出 `hello.txt` 的內容
---
你看到這隻狗在講了幾句話後也印出了 `hello.txt` 的內容了。
這個範例,其實就是一個簡單的自訂指令。
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
我們在使用別人寫的程式時,也常會用到這個技巧。
最後留一個小挑戰給你,請試著移動到 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
-187
View File
@@ -1,187 +0,0 @@
title: 執行檔案
instruction: |
在這個關卡中,你將會學到:
1. 什麼是可執行檔
2. 使用 `which` 指令查看執行檔的位置
3. 如何執行可執行檔
requirements:
- remove
stages:
- id: start
task: 按下開始按鈕
type: start
condition:
command: ^echo start$
responses:
- content: |
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
要查看一個指令它的檔案位置,可以使用 `which` 指令。
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
hints:
- ➤ 使用 `which ls` 來查看 `ls` 的路徑
- id: which-ls
requirements:
- start
task: 使用 `which` 指令查看 `ls` 的路徑
condition:
command: ^which ls$
responses:
- content: |
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
hints:
- ➤ 使用 `which cat` 查看 `cat` 的路徑
- id: which-cat
requirements:
- which-ls
task: 使用 `which` 指令查看 `cat` 的路徑
condition:
command: ^which cat$
responses:
- content: |
你看到了 `/bin/cat` 這個路徑,它就是 `cat` 指令它檔案存在的位置。
我們要執行一個可執行檔時,只要 **直接輸入它的路徑** 就可以執行它。
我們來試看看使用執行檔的路徑來執行指令。
在此之前,我們先試試看一般執行 `ls` 指令的結果。
hints:
- ➤ 輸入 `ls` 來查看當前的資料夾
- id: ls
requirements:
- which-cat
task: 使用 `ls` 指令查看當前的資料夾
condition:
command: ^ls$
responses:
- content: |
你看到了當前的資料夾內有 `commands` 資料夾和 `dog`, `hello.txt` 這兩個檔案。
我們再來試看看用 `ls` 的路徑來執行它。
hints:
- ➤ 輸入 `/bin/ls` 來查看當前的資料夾
- id: bin-ls
requirements:
- ls
task: 使用執行檔的絕對路徑來執行 `ls`
condition:
command: ^/bin/ls$
responses:
- content: |
你會看到它列出了我們資料夾內的檔案,你會發現它的輸出跟單純打 `ls` 的結果一模一樣。
它一樣可以使用參數。我們來用 `cat` 試試看。
hints:
- ➤ 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
- id: bin-cat
requirements:
- bin-ls
task: 使用執行檔的絕對路徑來執行 `cat`
condition:
command: ^/bin/cat hello.txt$
responses:
- content: |
你看到了 `hello.txt` 的內容,跟單純使用 `cat` 一模一樣。
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
因為一個指令,就是由 `執行檔 參數1 參數2 ...` 所構成。
`執行檔` 就是我們要執行的程式,像是 `ls`, `cat`, `which` 等。
`參數` 則是我們要提供給程式的資訊,像是接在 `cat` 後面的 `hello.txt`。
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
接著我們來試著執行不是內建的指令。
現在資料夾中的 `dog` 檔案,是一個在這門課程所提供的自訂指令。
我們先來看看它是怎麼寫的。
hints:
- ➤ 使用 `cat dog` 來查看 `dog` 的內容
- id: cat-dog
requirements:
- bin-cat
task: 使用 `cat` 指令查看 `dog` 的內容
condition:
command: ^cat dog$
responses:
- content: |
你看到了這是一個用 `echo` 和 `sleep` 所組成的簡單程式。
詳細的內容不用了解沒關係,只是輸出一些文字而已。
當我們要執行一個檔案時,就是用指定路徑的方式。
也就是說 `執行檔` 就是 `dog` 的路徑,`參數` 在這個範例中不會使用到。
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`。
那在 `執行檔 參數` 這個格式下,我們要打的指令就會是
```
/home/commander/dog
```
hints:
- ➤ 輸入 `/home/commander/dog` 來執行 `dog` 檔案
- id: dog-abs
requirements:
- cat-dog
task: 使用絕對路徑來執行 `dog`
condition:
or:
- command: ^/home/commander/dog$
- command: ^~/dog$
responses:
- content: |
你看到了終端機輸出這隻傲嬌的狗。
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
`.` 在路徑中,代表的就是當前的資料夾。
那我們再試試看使用相對路徑來執行 `dog` 檔案。
hints:
- ➤ 輸入 `./dog` 來執行 `dog` 檔案
- id: dog-rel
requirements:
- dog-abs
task: 使用相對路徑來執行 `dog`,並加上參數
condition:
pwd: /home/commander
command: ^./dog$
responses:
- content: |
你看到這隻狗執行了跟剛才一樣的結果。
這個範例,其實就是一個簡單的自訂指令。
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
我們在使用別人寫的程式時,也常會用到這個技巧。
最後留一個小挑戰給你,請試著先進入 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
hints:
- ➤ 使用 `cd` 來進入資料夾
- ➤ 使用 `ls` 查看檔案
- ➤ 輸入 `./檔案名稱` 來執行檔案
- id: END
requirements:
- dog-rel
task: 使用相對路徑來執行 `lucky`
condition:
pwd: /home/commander/commands
command: ^./lucky$
responses:
- content: |
你看到了終端機輸出 `You are lucky!` 這個字串。
恭喜你,你已經完成了這個課程!
exceptions:
condition:
command: ^echo help$
responses:
- content: |
你需要先移動到 `commands` 資料夾,再使用相對路徑執行 `lucky` 檔案。
請先輸入 `cd commands` 來移動到 `commands` 資料夾,再使用相對路徑 `./lucky` 來執行 `lucky` 檔案。
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
echo "I am SO LUCKY!!!"
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
echo "你執行了 dog 檔案"
echo "汪汪汪"
sleep 1
echo "你知道嗎?"
sleep 1
echo "我比貓還厲害"
-1
View File
@@ -1 +0,0 @@
Hello! This is a file hello.txt
+56 -60
View File
@@ -4,22 +4,24 @@ instruction: |
1. 如何使用 `ls` 查看資料夾的內容
2. 如何使用 `cd` 在資料夾之間移動
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- id: start
task: 按下 `開始` 按鈕開始教程
task: 輸入 `echo start` 來開始教程
condition:
command:
- ^echo start$
output:
- ^start$
responses:
- content: |
在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
但是你可以透過 `ls` 指令來查看資料夾的內容。
`ls` 指令(第一個字是小寫L)是 list 的簡寫,也就是列出檔案的意思。
在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾。
現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
- content:
- 在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
- 但是你可以透過 `ls` 指令來查看資料夾的內容。
- '`ls` 指令是 list 的縮寫,也就是列出檔案的意思。'
- 在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾,右邊的檔案樹也會隨之改變。
- 現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
hints:
- ➤ 輸入 `ls` 並查看資料夾的內容
@@ -34,23 +36,24 @@ stages:
- condition:
not:
pwd:
- /home/commander
- ^/home/commander$
responses:
- type: narrative
content: |
請先回到 `/home/commander/` 資料夾
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
content:
- 請先回到 `/home/commander/` 資料夾
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
responses:
- content: |
你應該能看到資料夾中的內容了。
在畫面右邊的檔案樹也有了改變。
可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2` 。
- content: |
接下來,我要教你如何進入資料夾。
`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。
你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
例如你要進入 `folder1` 資料夾,你可以輸入 `cd folder1` 。
現在,請你進入 `folder1` 資料夾。
- content:
- 你應該能看到資料夾中的內容了。
- 在畫面右邊的檔案樹也有了改變。
- 可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2`。
- content:
- 接下來,我要教你如何進入資料夾。
- '`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。'
- 你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
- 例如你要進入 `folder1` 資料夾,你可以輸入
- '`cd folder1`。'
- 現在,請你進入 `folder1` 資料夾。
hints:
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
@@ -62,37 +65,30 @@ stages:
pwd:
- ^/home/commander/folder1$
exceptions:
- condition:
command:
- folderl
responses:
- type: narrative
content: |
`folder1` 的最後是數字 `1` 不是字母 `l`。
- condition:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: 輸入 `cd folder1` 來進入 `folder1` 資料夾
content:
- 輸入 `cd folder1` 來進入 `folder1` 資料夾
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content: |
請先回到 `/home/commander/` 資料夾
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
再輸入 `cd folder1` 來進入 `folder1` 資料夾
content:
- 請先回到 `/home/commander/` 資料夾
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
responses:
- content: |
你成功的進入 `folder1` 資料夾了。
你可以發現指令提示符從原本的
`commander:~ $` 變成了
`commander:~/folder1 $` 。
這表示你現在正位在 `folder1` 資料夾中。
現在,請查看 `folder1` 資料夾的內容。
- content:
- 你成功的進入 `folder1` 資料夾了。
- 你可以發現指令提示符從原本的
- '`commander:~ $` 變成了'
- '`commander:~/folder1 $` 。'
- 這表示你現在正位在 `folder1` 資料夾中。
- 現在,請查看 `folder1` 資料夾的內容。
hints:
- ➤ 輸入 `ls` 查看資料夾的內容
@@ -106,11 +102,11 @@ stages:
command:
- ^ls$
responses:
- content: |
你現在可以看到 `folder1` 資料夾中有兩個檔案,分別是 `file1.txt` 跟 `file2.txt`。
現在我們可以離開 `folder1` 回到原本的資料夾了。
你可以使用 `cd ..` 回到上一層資料夾。
這裡的 `..` 代表的是上一層的資料夾。
- content:
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
- 現在我們可以離開 `folder1` 回到原本的資料夾了。
- 你可以使用 `cd ..` 回到上一層資料夾。
- 這裡的 `..` 代表的是上一層的資料夾。
hints:
- ➤ 輸入 `cd ..` 回到上一層資料夾
@@ -122,9 +118,9 @@ stages:
pwd:
- ^/home/commander$
responses:
- content: |
你成功的回到上一層資料夾了。
現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
- content:
- 你成功的回到上一層資料夾了。
- 現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
hints:
- ➤ 使用 `cd` 指令進出資料夾
- ➤ 使用 `ls` 指令查看資料夾的內容
@@ -137,9 +133,9 @@ stages:
pwd:
- ^/home/commander/folder2$
responses:
- content: |
你成功的進入 `folder2` 資料夾了。
現在,請你查看 `folder2` 資料夾的內容。
- content:
- 你成功的進入 `folder2` 資料夾了。
- 現在,請你查看 `folder2` 資料夾的內容。
hints:
- ➤ 使用 `ls` 查看資料夾的內容
@@ -151,11 +147,11 @@ stages:
pwd:
- ^/home/commander/folder2$
command:
- ^ls$
- ls
responses:
- content: |
你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
現在你可以離開 `folder2` 回到原本的資料夾了。
- content:
- 你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
- 現在你可以離開 `folder2` 回到原本的資料夾了。
hints:
- ➤ 使用 `cd ..` 回到上一層資料夾
@@ -167,9 +163,9 @@ stages:
pwd:
- ^/home/commander$
responses:
- content: |
你成功的回到原本的資料夾了。
恭喜你學會了 `ls` 和 `cd` 指令的用法。
- content:
- 你成功的回到原本的資料夾了。
- 恭喜你學會了 `ls` 和 `cd` 指令的用法。
hints:
- ➤ 使用 `cd` 指令進出資料夾
- ➤ 使用 `ls` 指令查看資料夾的內容
-1
View File
@@ -1,4 +1,3 @@
echo start
echo hello
echo next
echo finish
+46 -39
View File
@@ -5,20 +5,30 @@ instruction: |
1. 如何使用這個網站
2. 如何使用終端機
在畫面的中間可以看到終端機和提示兩個區塊。
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
!! 注意:不需要輸入上下引號 !!
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始教程
condition:
command:
- ^echo start$
output:
- ^start$
responses:
- content: |
在畫面的中下方的區塊是終端機,你可以在終端機裡輸入指令來操作電腦。
在終端機中,你所看到的 `commander:~ $` 叫做命令提示字串。
命令提示字串最後的錢字號 `$` 叫命令提示符,你必須將指令輸入在它的後方。
當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令,並且會從終端機的下一行開始輸出指令的執行結果。
直到你看到新的 `commander:~ $` 出現時,代表你可以輸入下一個指令了。
現在請你輸入 `echo hello` 來看看會發生什麼事。
- content:
- 你好,我是你的 Unix 助手,我叫做 Ada。我將會在之後協助你了解如何使用終端機。
- 現在所在的這個區塊就是終端機,你可以在這裡輸入指令來操作電腦。
- 在終端機中,你所看到的
- '`commander:~ $` 是指令提示符號,它能讓你知道現在可以輸入指令了。'
- 你必須將指令輸入在它的後方。
- 當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令。
- 並且會將執行結果輸出在終端機的下一行。
- 這個終端機的功能和你在 Linux 電腦上看到的終端機是一樣的。
- 唯一的差別是,在這裡會有劇情和提示來協助你學習如何使用終端機。
- 在輸入指令之後,如果你看到用綠色 `=====` 組成的分隔線,代表在分隔線下方的內容是添加的內容,在真實的終端機中不會出現。
- 而終端機的輸出,是在綠色 `=====` 分隔線上方的白色內容。
- 現在請你輸入 `echo hello` 來看看會發生什麼事。
hints:
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
@@ -30,41 +40,38 @@ stages:
command:
- ^echo hello$
responses:
- content: |
你應該可以看到終端機上顯示了以下內容:
```
commander:~ $ echo hello
hello
```
第一行的 `commander:~ $` 是命令提示字串,接在後方的 `echo hello` 是你輸入的指令。
第二行的 `hello` 則是終端機的輸出結果。
你現在知道了如何在終端機上輸入指令,以及查看終端機輸出的位置。
現在請輸入 `echo next` 來繼續下一個步驟。
- content:
- 你應該可以看到終端機上顯示了:
- commander:~ $ echo hello
- hello
- 第一行的 `commander:~ $` 是指令提示符, `echo hello` 是你輸入的指令。
- 第二行的 `hello` 則是終端機的輸出。
- content:
- 現在你知道如何在終端機上輸入指令,以及查看終端機輸出的位置。
- 指令必須輸入在指令提示符號的後面,然而當指令提示符消失時,你就無法輸入指令了。
- 這時候你可以按下 `Ctrl + C` 來嘗試解決這個問題。
- 在終端機中, `Ctrl + C` 並不能用來複製,而是用來中斷目前正在執行的指令。
- 因此,如果你前一次的指令執行時卡住了,或是執行很久沒有反應,你可以按下 `Ctrl + C` 來中斷指令,讓你可以繼續輸入指令。
- 如果 `Ctrl + C` 也沒有辦法解決問題,可以按按看 `esc` 或 `q` 鍵。如果都沒用的話,按下畫面右下角的 `重來` 按鈕重新開始關卡。
- content:
- 在終端機下方會顯示提示,你可以用提示右上角的箭頭來查看之前的提示。
- 在畫面左邊的區域是關卡說明,你可以在這裡看到關卡的介紹,以及現在需要完成的任務。
- 在畫面右邊的區域是檔案樹,它會根據你所探索的資料夾發生變化,讓你更清楚檔案之間的位置關係。
- 畫面右上角的圖示分別是 `查看地圖`、`回報錯誤` 以及 `登出` 按鈕。
- content:
- 第一關的文字內容比較多,可以慢慢讀沒關係。
- 如果忘記了也可以隨時回來複習。
- 需要的話可以按下 `Enter` 來快速跳過劇情說明。
- 如果你已經讀完了上述內容,請輸入 `echo finish` 來結束這一關。
hints:
- ➤ 輸入 `echo next` 繼續下一個步驟
- id: echo-next
task: 在終端機輸入 `echo next`
requirements:
- echo-hello
condition:
command:
- ^echo next$
responses:
- content: |
`Ctrl + C` 在終端機中是一個很重要的快捷鍵,它可以用來中斷正在執行的指令,並產生一個新的命令提示字串,讓你可以繼續輸入指令。
如果指令執行很久沒有反應,或你因為其他原因想要中斷指令,你可以按下 `Ctrl + C` 來中斷指令。
又或者你輸入了錯誤的指令,卻懶得慢慢刪除,你也可以按下 `Ctrl + C` 來產生新的命令提示字串重新輸入指令。
如果你已經理解了剛才的說明,請輸入 `echo finish` 來結束這一關。
hints:
- ➤ 按下 `Ctrl + C` 能夠中斷正在執行的指令
- ➤ 按下 `Ctrl + C` 能夠產生新的命令提示字串
- ➤ 按下 `Ctrl + C` 能夠中斷指令
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
- ➤ 輸入 `echo finish` 來結束這一關
- id: END
task: 結束關卡
requirements:
- echo-next
- echo-hello
condition:
command:
- ^echo finish$
+31 -31
View File
@@ -21,17 +21,17 @@ stages:
- 你聽到了一個聲音:
- type: dialogue
speaker: Unix 精靈
content: |
你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
首先,你必須要學會如何使用指令。
你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
hints:
- ➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
content:
- 你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
- 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
- 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
- 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
- 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
- 首先,你必須要學會如何使用指令。
- 你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
- 現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
hints: |
➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
- id: find-scroll
task: 搜索卷軸
@@ -44,20 +44,20 @@ stages:
- ^ls$
responses:
- type: narrative
content: |
一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
content:
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
- type: dialogue
speaker: Unix 精靈
content: |
你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
hints:
- ➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
- ➤ 使用 `Tab` 鍵來取得指令提示。
- ➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
content:
- 你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
- 使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
- 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
- 例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
hints: |
➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
➤ 使用 `Tab` 鍵來取得指令提示。
➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
- id: read-forgotten-scroll
task: 查看卷軸
@@ -71,11 +71,11 @@ stages:
responses:
- type: dialogue
speaker: Unix 精靈
content: |
你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
hints:
- ➤ 輸入 `echo Hello World` 來詠唱咒語。
content:
- 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
- 使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
hints: |
➤ 輸入 `echo Hello World` 來詠唱咒語。
- id: END
task: 解除封印
@@ -92,6 +92,6 @@ stages:
- 突然之間,你感覺到一股強大的力量從你的身體中湧出,你的身體開始發光,你的手中出現了一把魔法之劍。
- type: dialogue
speaker: Unix 精靈
content: |
恭喜您,我的指令官,您已成功解除了封印。
請跟著在接下來的關卡,一步步取回 Linux 之根吧!
content:
- 恭喜您,我的指令官,您已成功解除了封印。
- 請跟著在接下來的關卡,一步步取回 Linux 之根吧!
+3 -2
View File
@@ -1,11 +1,12 @@
echo start
echo next
echo continue
mv hello.txt notes
ls
ls notes
echo next
echo continue
mv world.txt wow.txt
ls
mv notes docs
ls
echo hint
mv docs/hello.txt hi.txt
+63 -51
View File
@@ -1,49 +1,51 @@
title: 移動檔案
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `mv` 移動檔案與資料夾
1. 如何用 `mv` 移動檔案與資料夾
準備好就輸入 `echo start` 開始吧!
requirements:
- create
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content: |
今天我們要學習如何移動檔案與資料夾。
現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
請先用 `ls` 與 `cat` 來查看它們的內容。
看完之後,使用 `echo next` 來繼續吧!
content:
- 今天我們要學習如何移動檔案與資料夾。
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
- 請先用 `ls` 與 `cat` 來查看它們的內容。
- 看完之後,使用 `echo continue` 來繼續吧!
hints:
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
- ➤ 完成後輸入 `echo next` 來繼續
- ➤ 完成後輸入 `echo continue` 來繼續
- id: next
- id: continue
requirements:
- start
task: 查看檔案內容,完成後輸入 `echo next` 來繼續
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
condition:
command:
- ^echo next$
- ^echo continue$
responses:
- type: narrative
content: |
現在我們要來學習如何移動檔案與資料夾。
`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。
使用方式為 `mv [來源] [目的地]`。
我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
content:
- 現在我們要來學習如何移動檔案與資料夾。
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
- 使用方式為 `mv [來源] [目的地]`。
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
hints:
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
- id: mv-hello
requirements:
- next
- continue
task: 將 `hello.txt` 移動到 `notes` 資料夾中
condition:
command:
@@ -55,9 +57,9 @@ stages:
exists: false
responses:
- type: narrative
content: |
你做得很好!
我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
content:
- 你做得很好!
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
@@ -72,8 +74,8 @@ stages:
- ^ls$
responses:
- type: narrative
content: |
`hello.txt` 已經不在資料夾中了!
content:
- '`hello.txt` 已經不在資料夾中了!'
- id: ls-notes-hello
requirements:
@@ -88,19 +90,29 @@ stages:
- hello.txt
responses:
- type: narrative
content: |
`hello.txt` 出現在 `notes` 資料夾中了!
content:
- '`hello.txt` 出現在 `notes` 資料夾中了!'
- id: continue-mv
requirements:
- ls-no-hello
- ls-notes-hello
task: 輸入 `echo continue` 來繼續
condition:
command:
- ^echo continue$
responses:
- type: narrative
content: |
`mv` 指令也可以用來更改檔案名稱。
只要將目的地改成新的檔案名稱就可以了。
現在我們要將 `world.txt` 更名為 `wow.txt`。
content:
- '`mv` 指令也可以用來更改檔案名稱。'
- '只要將目的地改成新的檔案名稱就可以了。'
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
hints:
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
- id: mv-world
requirements:
- ls-notes-hello
- continue-mv
task: 將 `world.txt` 更名為 `wow.txt`
condition:
command:
@@ -112,9 +124,9 @@ stages:
exists: false
responses:
- type: narrative
content: |
你做得很好!
我們用 `ls` 來確認一下資料夾內的檔案吧!
content:
- 你做得很好!
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
@@ -131,12 +143,12 @@ stages:
- wow.txt
responses:
- type: narrative
content: |
`world.txt` 現在已經變成 `wow.txt` 了。
接下來我們來學習如何移動資料夾吧!
移動資料夾非常簡單,方法跟移動檔案一樣。
`mv` 指令會自動將資料夾內的所有檔案一起移動
我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
content:
- '`world.txt` 現在已經變成 `wow.txt` 了。'
- 接下來我們來學習如何移動資料夾吧!
- 移動資料夾非常簡單,方法跟移動檔案一樣。
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
hints:
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
@@ -154,9 +166,9 @@ stages:
exists: false
responses:
- type: narrative
content: |
你做得很好!
我們用 `ls` 來確認一下資料夾內的檔案吧!
content:
- 你做得很好!
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
@@ -173,11 +185,11 @@ stages:
- docs
responses:
- type: narrative
content: |
`notes` 資料夾已經變成 `docs` 了。
那麼最後來考考你:
試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
content:
- '`notes` 資料夾已經變成 `docs` 了。'
- 那麼最後來考考你:
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
- id: END
requirements:
@@ -197,12 +209,12 @@ stages:
- ^echo hint$
responses:
- type: narrative
content: |
你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
content:
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
hints:
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
responses:
- type: narrative
content: |
你做得很好!
你已經學會如何移動檔案與資料夾了!
content:
- 你做得很好!
- 你已經學會如何移動檔案與資料夾了!
+53 -51
View File
@@ -4,22 +4,24 @@ instruction: |
1. 絕對路徑和相對路徑
2. 如何使用 `pwd` 顯示目前的路徑
準備好就輸入 `echo start` 開始吧!
requirements:
- cast
- explore
stages:
- id: start
task: 按下 `開始` 按鈕開始教程
task: 輸入 `echo start` 來開始教程
condition:
command:
- ^echo start$
output:
- ^start$
responses:
- content: |
在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
而路徑又分為相對路徑和絕對路徑。
相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
請你先用 `ls` 指令查看這個資料夾中的檔案。
- content:
- 在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
- 而路徑又分為相對路徑和絕對路徑。
- 相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
- 請你先用 `ls` 指令查看這個資料夾中的檔案。
- id: ls
task: 查看資料夾中的檔案
@@ -31,8 +33,8 @@ stages:
command:
- ^ls$
responses:
- content: |
現在你使用 `cd` 指令進入 `folder1` 資料夾。
- content:
- 現在你使用 `cd` 指令進入 `folder1` 資料夾。
- id: cd-folder1
task: 進入 `folder1` 資料夾
@@ -42,14 +44,14 @@ stages:
pwd:
- ^/home/commander/folder1$
responses:
- content: |
你進入了 `folder1` 資料夾。
在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
相同的道理 `cd ..` 中的 `..` 也是相對路徑。
相對路徑可以不只有一層資料夾。
例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
現在試著用這個方式進入 `folder2` 資料夾看看吧。
- content:
- 你進入了 `folder1` 資料夾。
- 在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
- 相同的道理 `cd ..` 中的 `..` 也是相對路徑。
- 相對路徑可以不只有一層資料夾。
- 例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
- 然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
- 現在試著用這個方式進入 `folder2` 資料夾看看吧。
hints:
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
@@ -66,14 +68,14 @@ stages:
- ^/home/commander$
responses:
- type: narrative
content: |
輸入 `cd folder2` 來進入 `folder2` 資料夾
content:
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
responses:
- content: |
你成功的用相對路徑進入了 `folder2` 資料夾。
而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
試著用這個方式查看 `folder1` 資料夾中的檔案吧。
- content:
- 你成功的用相對路徑進入了 `folder2` 資料夾。
- 而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
- 例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
- 試著用這個方式查看 `folder1` 資料夾中的檔案吧。
hints:
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
@@ -91,16 +93,16 @@ stages:
- ^/home/commander/folder2$
responses:
- type: narrative
content: |
進到 `folder1` 資料夾並用 `ls` 查看內容。
content:
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
responses:
- content: |
很好,你現在知道如何運用相對路徑了。
接下來要介紹的是絕對路徑。
絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
你可以使用 `pwd` 來查看目前所在的絕對路徑。
`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。
現在請你使用 `pwd` 來查看目前所在的絕對路徑。
- content:
- 很好,你現在知道如何運用相對路徑了。
- 接下來要介紹的是絕對路徑。
- 絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
- 你可以使用 `pwd` 來查看目前所在的絕對路徑。
- '`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。'
- 現在請你使用 `pwd` 來查看目前所在的絕對路徑。
hints:
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
@@ -120,17 +122,17 @@ stages:
- ^/home/commander/folder2$
responses:
- type: narrative
content: |
請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
content:
- 請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
responses:
- content: |
你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
而絕對路徑其實也顯示在你的命令提示符中。
`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。
而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
`ls` 和 `cd` 指令也可以使用絕對路徑。
例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
現在請你使用 `ls ~` 來查看家目錄的內容。
- content:
- 你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
- 而絕對路徑其實也顯示在你的命令提示符中。
- '`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。'
- 而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
- '`ls` 和 `cd` 指令也可以使用絕對路徑。'
- 例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
- 現在請你使用 `ls ~` 來查看家目錄的內容。
hints:
- ➤ 輸入 `ls ~` 來查看家目錄的內容
@@ -142,8 +144,8 @@ stages:
command:
- ^ls ~$
responses:
- content: |
現在請使用 `cd ~` 回到家目錄。
- content:
- 現在請使用 `cd ~` 回到家目錄。
hints:
- ➤ 輸入 `cd ~` 來回到家目錄
@@ -160,9 +162,9 @@ stages:
- ^/home/commander$
responses:
- type: narrative
content: |
請使用 `cd ~` 回到家目錄
content:
- 請使用 `cd ~` 回到家目錄
responses:
- content: |
你回到家目錄了。
恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
- content:
- 你回到家目錄了。
- 恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
File renamed without changes.
File renamed without changes.
+278
View File
@@ -0,0 +1,278 @@
title: 查看檔案存取權限
instruction: |
在這個關卡中,你將會學到:
1. 用`ls -l`指令查看檔案的存取權限
2. 了解檔案存取權限的意義
準備好就輸入 `echo start` 開始吧!
requirements:
- helloworld
stages:
- id: start
task: 輸入 `echo start` 來開始
condition:
output:
- start
exceptions:
- condition:
command:
- ^echo$
responses:
- type: narrative
content:
- 你忘記輸入 `start` 了嗎?
- 請輸入 `echo start` 來開始
- catchAll: true
responses:
- type: narrative
content:
- 請輸入 `echo start` 來開始
responses:
- type: narrative
content: 現在我們要學習如何查看檔案的存取權限。
使用`ls -l`指令來查看不同檔案的存取權限吧!
hints:
- ➤ 使用 `ls -l` 來查看檔案的存取權限
- id: ls-l
task: 查看檔案的存取權限
requirements:
- start
condition:
command:
- ls -l
pwd:
- /home/commander
responses:
- type: narrative
content:
- 你可以看到檔案的存取權限的左邊類似 `-rw-r--r--` 的字串。
- 這個字串由十個字元組成,第一個字元代表檔案的類型,後面九個字元分別代表檔案的存取權限。
- 第一個字元如果是 `-` 代表這是一個檔案,如果是 `d` 代表這是一個資料夾。
- 後面九個字元中以三個字元一組,分別代表檔案的擁有者、群組、以及其他人的存取權限。
- 每一組中的三個字元分別代表讀取(`r`)、寫入(`w`)、執行(`x`)的權限。
- 如果該欄位是 `-` 則表示沒有該權限。
- 例如 `-w-` 代表擁有寫入的權限,但沒有讀取和執行的權限;
- 現在試著查看 `file1` 、 `file2` 、 `file3` 的內容吧!
- 如果沒有權限將會顯示 `Permission denied` 的訊息。
- id: read-file1
task: 嘗試查看 `file1` 的內容
requirements:
- ls-l
condition:
pwd: /home/commander
command:
- cat file1
- id: read-file2
task: 嘗試查看 `file2` 的內容
requirements:
- ls-l
condition:
pwd: /home/commander
command:
- cat file2
- id: read-file3
task: 嘗試查看 `file3` 的內容
requirements:
- ls-l
condition:
pwd: /home/commander
command:
- cat file3
- id: read-files
requirements:
- read-file1
- read-file2
- read-file3
responses:
- type: narrative
content:
- 你已經試著查看 `file1` 、 `file2` 、 `file3` 的內容了!
- 現在請用 `echo [檔案名稱]` 回答現在有哪的檔案的讀取權限。
# {unfinished}
- id: cat-continue
task: 使用 `echo continue` 來繼續
requirements:
- cat-hello
- cat-world
condition:
command:
- echo
responses:
- type: narrative
content:
- 接下來我們要來學習複製檔案。
- '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
- 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
- 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
hints:
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
- id: cp-hello
task: 複製 `hello.txt` 到 `hello2.txt`
requirements:
- cat-continue
condition:
command:
- ^cp hello.txt hello2.txt$
files:
- path: hello2.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `hello.txt` 到 `hello2.txt` 了!
- 我們打開來確認一下 `hello2.txt` 的內容吧!
hints:
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
- id: cat-hello2
requirements:
- cp-hello
task: 查看 `hello2.txt` 的內容
condition:
command:
- ^cat hello2.txt$
output:
- Hello
responses:
- type: narrative
content:
- 你成功查看到 `hello2.txt` 的內容了!
- 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
- 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
hints:
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
- id: cp-world
requirements:
- cat-hello2
task: 複製 `world.txt` 到 `world2.txt`
condition:
command:
- ^cp world.txt world2.txt$
files:
- path: world2.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `world.txt` 到 `world2.txt` 了!
- 我們打開來確認一下 `world2.txt` 的內容吧!
hints:
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
- id: cat-world2
requirements:
- cp-world
task: 查看 `world2.txt` 的內容
condition:
command:
- ^cat world2.txt$
output:
- World
responses:
- type: narrative
content:
- 你成功查看到 `world2.txt` 的內容了!
- '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
- 接下來我們要來學習如何複製資料夾。
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
hints:
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
- id: cp-notes
requirements:
- cat-world2
task: 複製 `notes` 資料夾到 `notes2`
condition:
command:
- ^cp
files:
- path: notes2
type: directory
- path: notes2/note.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `notes` 資料夾到 `notes2` 了!
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
hints:
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
exceptions:
- condition:
error:
- 'cp: omitting directory'
responses:
- type: narrative
content:
- 你忘記加上 `-r` 的參數了嗎?
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- id: ls-notes2
requirements:
- cp-notes
task: 查看 `notes2` 資料夾的內容
condition:
command:
- ^ls notes2$
output:
- note.txt
responses:
- type: narrative
content:
- 你成功查看到 `notes2` 資料夾的內容了!
- 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
- 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
hints:
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
- id: cp-overwrite
requirements:
- ls-notes2
task: 複製 `hello.txt` 到 `world.txt`
condition:
command:
- ^cp hello.txt world.txt$
responses:
- type: narrative
content:
- 你成功複製 `hello.txt` 到 `world.txt` 了!
- 我們打開來確認一下 `world.txt` 的內容吧!
hints:
- ➤ 使用 `cat world.txt`
- id: END
requirements:
- cp-overwrite
task: 檢查 `world.txt` 的內容
condition:
command:
- ^cat world.txt$
output:
- Hello
responses:
- type: narrative
content:
- '`world.txt` 的內容變成 `Hello` 了!'
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
- 以上就是複製檔案的基本操作!
+1
View File
@@ -0,0 +1 @@
你有閱讀這個檔案的權限
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo 你有執行這個檔案的權限
+12
View File
@@ -0,0 +1,12 @@
# 關卡名稱
## stage 名稱 {stage-id}
> {speaker=角色名稱}
> 對話內容
### exception1
content1
### exception2
content2
+30 -30
View File
@@ -3,22 +3,24 @@ instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `cat` 查看檔案的內容
準備好就輸入 `echo start` 開始吧!
requirements:
- path
stages:
- id: start
task: 按下 `開始` 按鈕開始教程
task: 輸入 `echo start` 來開始教程
condition:
command:
- ^echo start$
output:
- ^start$
responses:
- content: |
在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
但是你可以透過 `cat` 指令來查看檔案的內容。
`cat` 指令是 concatenate 的縮寫,也就是串接的意思。
在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
請你先查看這個資料夾中哪些檔案。
- content:
- 在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
- 但是你可以透過 `cat` 指令來查看檔案的內容。
- '`cat` 指令是 concatenate 的縮寫,也就是串接的意思。'
- 在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
- 請你先查看這個資料夾中哪些檔案。
hints:
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
@@ -32,9 +34,9 @@ stages:
command:
- ^ls$
responses:
- content: |
這個資料夾中有一個檔案和一個資料夾。
現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
- content:
- 這個資料夾中有一個檔案和一個資料夾。
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
hints:
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
@@ -48,10 +50,10 @@ stages:
command:
- ^cat file1.txt$
responses:
- content: |
你應該能看到檔案的內容了。
`cat` 指令也可以使用路徑來查看檔案內容
請你先用 `ls folder` 查看資料夾內的檔案。
- content:
- 你應該能看到檔案的內容了。
- '`cat` 指令也可以使用路徑來查看檔案內容'
- 請你先用 `ls folder` 查看資料夾內的檔案。
hints:
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
@@ -69,13 +71,13 @@ stages:
not:
pwd:
- ^/home/commander$
content: |
請留在 `/home/commander` 資料夾中。
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
content:
- 請留在 `/home/commander` 資料夾中。
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
responses:
- content: |
你現在知道 `folder` 資料夾的內容了。
請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
- content:
- 你現在知道 `folder` 資料夾的內容了。
- 請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
hints:
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
@@ -93,12 +95,10 @@ stages:
not:
pwd:
- ^/home/commander$
content:
- 請留在 `/home/commander` 資料夾中。
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
responses:
- type: narrative
content: |
請留在 `/home/commander` 資料夾中。
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
responses:
- content: |
你成功地查看到 `file.txt` 的內容了。
你已經會使用 `cat` 指令查看檔案的內容了。
- content:
- 你成功地查看到 `file.txt` 的內容了。
- 你已經會使用 `cat` 指令查看檔案的內容了。
-1
View File
@@ -1,5 +1,4 @@
echo start
ls
rm hello.txt
ls
rm world.txt wow.txt
+78 -80
View File
@@ -1,9 +1,12 @@
title: 刪除檔案與資料夾
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `rm` 指令刪除檔案
2. 如何使用 `rmdir` 刪除空資料夾
3. 如何使用 `rm -r` 刪除資料夾與其中的所有檔案
1. 用 `rm` 指令刪除檔案
2. 用 `rmdir` 與資料夾
3. 用 `rm -r` 刪除資料夾與它的所有檔案
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
準備好就輸入 `echo start` 開始吧!
requirements:
- copy
@@ -11,44 +14,33 @@ requirements:
stages:
- id: start
task: 按下 `開始` 按鈕開始
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start$
responses:
- content: |
今天我們要學習如何刪除檔案與資料夾。
請先使用 `ls` 指令查看資料夾內容。
hints:
- ➤ 使用 `ls` 來查看資料夾內容。
- id: ls
requirements:
- ^echo
output:
- start
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls$
responses:
- content: |
首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。
現在請你使用 `rm` 指令刪除 `hello.txt`。
- content:
- 今天我們要學習如何刪除檔案與資料夾。
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
hints:
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
- id: rm-hello
requirements:
- ls
- start
task: 使用 `rm` 刪除 `hello.txt`
condition:
command:
- ^rm\s
- ^rm
files:
- path: hello.txt
exists: false
responses:
- content: |
很好!現在請你使用 `ls` 指令來查看資料夾內容。
- content:
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
- id: ls-no-hello
requirements:
@@ -56,15 +48,15 @@ stages:
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls$
- ^ls
files:
- path: hello.txt
exists: false
responses:
- content: |
你會發現 `hello.txt` 已經不見了。
`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。
現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
- content:
- '你會發現 `hello.txt` 已經不見了。'
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
hints:
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
- ➤ 檔案名稱之間要用空白隔開。
@@ -83,8 +75,8 @@ stages:
- path: wow.txt
exists: false
responses:
- content: |
很好!我們一樣用 `ls` 指令確認一下資料夾內容。
- content:
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
hints:
- ➤ 使用 `ls` 來查看資料夾內容。
@@ -96,10 +88,10 @@ stages:
command:
- ^ls$
responses:
- content: |
你會發現 `world.txt` 與 `wow.txt` 已經不見了。
現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。
現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。
- content:
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
hints:
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
@@ -109,13 +101,13 @@ stages:
task: 使用 `rmdir` 刪除 `empty` 資料夾
condition:
command:
- ^rmdir empty$
- ^rmdir
files:
- path: empty
exists: false
responses:
- content: |
很好!現在請你使用 `ls` 指令來查看資料夾內容。
- content:
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
- id: ls-no-empty
requirements:
@@ -125,9 +117,9 @@ stages:
command:
- ^ls$
responses:
- content: |
你會發現 `empty` 資料夾已經不見了。
現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。
- content:
- '你會發現 `empty` 資料夾已經不見了。'
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
hints:
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
@@ -143,12 +135,12 @@ stages:
type: folder
exists: true
responses:
- content: |
指令執行失敗了!
你會發現 `notes` 資料夾還在。
這是因為 `rmdir` 指令只能刪除空的資料夾。
我們先來將 `notes` 資料夾的檔案刪除再來試一次。
現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。
- content:
- 指令執行失敗了!
- '你會發現 `notes` 資料夾還在。'
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
hints:
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
@@ -171,29 +163,31 @@ stages:
command:
- ^rm notes/\[.*\]
responses:
- content: |
你好像理解錯了。
這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
實際打指令的時候,不要打中括號 `[]` 喔!
例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
- content:
- '你好像理解錯了。'
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
- 實際打指令的時候,不要打中括號 `[]` 喔!
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
- condition:
command:
- ^rmdir notes$
- ^rm
error:
- 'No such file or directory'
responses:
- content: |
這個錯誤的意思是找不到檔案
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
- content:
- 這個錯誤的意思是找不到檔案
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
- condition:
command:
- ^ls notes$
responses:
- content: |
`notes` 資料夾中有著 `note.txt` 檔案。
那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
- content:
- '`notes` 資料夾中有著 `note.txt` 檔案。'
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
responses:
- content: |
很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。
- content:
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
hints:
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
@@ -202,14 +196,16 @@ stages:
- rm-notes-content
task: 使用 `rmdir` 刪除 `notes` 資料夾
condition:
command:
- ^rmdir
files:
- path: notes
exists: false
responses:
- content: |
很好!`notes` 資料夾已經被刪除了。
接下來我們來刪除 `trash` 資料夾。
先使用 `ls` 指令查看一下 `trash` 資料夾內容。
- content:
- '很好!`notes` 資料夾已經被刪除了。'
- 接下來我們來刪除 `trash` 資料夾。
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
hints:
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
@@ -219,14 +215,16 @@ stages:
task: 使用 `ls` 查看 `trash` 資料夾內容
condition:
command:
- ^ls trash$
- ^ls
output:
- garbage.txt
responses:
- content: |
天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。
我們來試試看吧!
- content:
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
- 我們來試試看吧!
hints:
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
@@ -236,13 +234,13 @@ stages:
task: 使用 `rm -r` 刪除 `trash` 資料夾
condition:
command:
- ^rm -r trash$
- ^rm -r
files:
- path: trash
exists: false
responses:
- content: |
很好!現在請你使用 `ls` 指令查看資料夾內容。
- content:
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
hints:
- ➤ 使用 `ls` 來查看資料夾內容。
@@ -254,6 +252,6 @@ stages:
command:
- ^ls$
responses:
- content: |
`trash` 資料夾已經不見了。
恭喜你!你已經學會如何刪除檔案與資料夾了!
- content:
- '`trash` 資料夾已經不見了。'
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
-196
View File
@@ -1,196 +0,0 @@
title: Tab 自動補全
instruction: |
在這個關卡,你將會學到
1. 如何使用 Tab 自動補全路徑
2. 出現衝突時如何處理
按下中間的按鈕來開始這個關卡。
requirements:
- read
stages:
- id: start
task: 按下開始按鈕開始關卡
condition:
command:
- ^echo start$
responses:
- content: |
在終端機裡,所有的操作都是用文字進行。
然而,打字很花時間,而且只要一個字打錯,指令就會執行失敗。
因些終端機提供「自動補全」的功能,自動幫你完成檔案路徑、指令名稱、參數名稱,節省你的時間,也避免輸入錯誤。
在開始練習之前,我們先來看看家目錄下有哪些檔案。
hints:
- ➤ 使用 `ls` 查看檔案列表
- id: ls
task: 使用 `ls` 查看檔案列表
requirements:
- start
condition:
command:
- ^ls$
responses:
- content: |
你看到了這個資料夾中充滿了超級長和有錯字的檔案名稱,如果這些都要我們一個一個字輸入,肯定會很痛苦。
但不要緊,只要使用自動補全,檔名不論多長都能輕鬆輸入。
使用自動補全的方法是,**先輸入部分的檔名,再按下 Tab**
(Tab 是 Q 左邊的按鍵)
例如要補全 `SuperSecretPlansForWorldDomination.txt`,你可以先輸入 `Su` 再按下 Tab,終端機就會幫你打完剩下的檔名了。
我們試看看印出 `SuperSecretPlansForWorldDomination.txt` 這個檔案的內容。
hints:
- ➤ 使用 `cat SuperSecretPlansForWorldDomination.txt` 來印出檔案內容
- ➤ 在輸入 `cat Su` 之後按下 Tab 來自動補全檔名,完成後按 Enter 就能執行指令
- id: cat_super_secret
task: 使用 `cat` 印出 SuperSecretPlansForWorldDomination.txt 的內容
requirements:
- ls
condition:
pwd:
- ^/home/commander$
command:
- ^cat SuperSecretPlansForWorldDomination.txt$
responses:
- content: |
你看到了終端機自動幫你補全了 `SuperSecretPlansForWorldDomination.txt`,就算是很長的檔名,也不用怕打錯字。
開頭不論你打了幾個字都無所謂,你可以只打 `S`、`Su`、`Sup`、`Super` 或甚至完全不打字都沒關係。
但是你打的字越少,就越容易跟其他的檔案重複,這時候就要做衝突處理。
關於衝突處理,我們會在待會練習。
自動補全也可以用來對付拼字錯誤,像是 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 這個檔案充滿了錯字,任誰來打都難保不會失手。
這一次,請你一樣用 `cat` 來試著印出 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 的內容。
hints:
- ➤ 輸入 `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 來印出檔案內容
- ➤ 輸入 `cat Thsi` 之後按下 Tab 做自動補全
- ➤ 試試看輸入更少的字元就按 Tab 會有什麼樣的結果
- id: cat_typo
task: 使用 `cat` 印出 ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt 的內容
requirements:
- cat_super_secret
condition:
pwd:
- ^/home/commander$
command:
- ^cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt$
responses:
- content: |
你看到了這個充滿錯字的檔案了。
只要使用自動補全,不管是錯字、大小寫變化、還是隨機亂碼都不再是困擾了。
讓我們再練習一個,這次請你查看 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾中的檔案。
一樣,輸入前幾個字,然後按 Tab 自動補全。
hints:
- ➤ 輸入 `ls ThingsIWillOrganizeLaterWhenIHaveTime` 查看資料夾中的檔案
- ➤ 輸入 `ls Thing` 之後按下 Tab 做自動補全
- id: ls_things
task: 使用 `ls` 查看 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾中的檔案
requirements:
- cat_typo
condition:
pwd:
- ^/home/commander$
command:
- ^ls ThingsIWillOrganizeLaterWhenIHaveTime$
responses:
- content: |
你看到了這個資料夾裡有著三個檔案:一個如何做三明治的教學跟兩個在半夜隨機下載的東西。
假如我們要打開三明治教學 `HowToProperlyMakeASandwich.txt`,但是它位在資料夾中,我們沒辦法只按一次 Tab 就補全它的檔名。
這次我們就要來練習使用**連續的自動補全**。
像這樣檔案位在其他資料夾的情況,我們只要先輸入資料夾名稱的開頭,按一次 Tab,再輸入檔案的開頭,再按一次 Tab,就能夠補全 `資料夾/檔名` 了。
而且當你補全的對象是資料夾時,終端機還會自動幫你在結尾加上 `/`,省去你自己輸入的時間。
那麼我們就來打開 `HowToProperlyMakeASandwich.txt`。
hints:
- ➤ 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` 來印出檔案內容
- ➤ 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
- ➤ 再輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/How` 按 Tab 補全至完整路徑
- id: cat_sandwich
task: 使用 `cat` 印出 ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt 的內容
requirements:
- ls_things
condition:
command:
- ^cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt$
responses:
- content: |
你看到了這份嚴謹的三明治製作教學。
那我們剩下來要看的就是資料夾裡的兩個檔案:`RandomStuffThatIDownloadedAt3AM.txt` 和 `RandomStuffThatIDownloadedAt4AM.txt`
兩個檔名非常相似,只有最後一個是 3AM 另一個是 4AM。
這樣我們只打前面幾個字的時候,沒有辦法分辦到底選的是哪一個檔案。
這就是**發生衝突**。
遇到衝突的解決方法,其實跟使用資料夾是一樣的。
在輸入 `Ran` 按下 Tab 後,終端機會補全到衝突發生前的位置,也就是 `RandomStuffThatIDownloadedAt`。
我們只要根據我們想開啟的檔案,輸入 `3` 或是 `4`,再按一下 Tab,就能夠補全剩下的檔名了。
_如果我們把完整的路徑視作補全的對象,那資料夾其實就是一種衝突的情況。_
hints:
- ➤ 印出 `ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` 的檔案內容
- ➤ 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
- ➤ 再接續輸入 `Ran` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt`
- ➤ 再接續輸入 `3` 後按 Tab 補全至完整的路徑。
- id: cat_3am
task: 使用 `cat` 印出 ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt 的內容
requirements:
- cat_sandwich
condition:
pwd:
- ^/home/commander$
command:
- ^cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt$
responses:
- content: |
你看到了一份凌晨三點下載的檔案列表。嗯…我今天網路用的夠多了。
最後留下一個挑戰給你,請先進入到 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾後,再印出 `RandomStuffThatIDownloadedAt4AM.txt`。
練習使用今天教的自動補全來完成任務。
hints:
- ➤ 先使用 `cd` 進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾
- ➤ 再使用 `cat` 印出 RandomStuffThatIDownloadedAt4AM.txt 的內容
- ➤ 記得使用 Tab 自動補全來節省時間和避免錯誤
- id: cd_things
task: 使用 `cd` 進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾
requirements:
- cat_3am
condition:
pwd:
- ^/home/commander/ThingsIWillOrganizeLaterWhenIHaveTime$
responses:
- content: |
很好,你已經成功進入 ThingsIWillOrganizeLaterWhenIHaveTime 資料夾了。
現在請使用 `cat` 指令來印出 `RandomStuffThatIDownloadedAt4AM.txt` 的內容。
記得運用 Tab 自動補全功能!
- id: END
task: 使用 `cat` 印出 RandomStuffThatIDownloadedAt4AM.txt 的內容
requirements:
- cd_things
condition:
pwd:
- ^/home/commander/ThingsIWillOrganizeLaterWhenIHaveTime$
command:
- ^cat RandomStuffThatIDownloadedAt4AM.txt$
responses:
- content: |
恭喜你!你已經成功完成了所有的任務,並且熟練地運用了 Tab 自動補全功能。
這個技能將會在你未來的終端機操作中帶來極大的便利。
記住,不論是長檔名、複雜路徑,還是難以拼寫的檔案名稱,Tab 自動補全都是你的好幫手!
hints:
- ➤ 你已經完成了所有任務!可以繼續探索或是結束這個關卡。
@@ -1,10 +0,0 @@
1. 收集全世界的襪子,讓所有人都找不到另一支襪子。
2. 訓練一支企鵝軍隊,用可愛征服世界。
3. 發明一種能讓所有人講話都變成雞叫聲的裝置。
4. 把所有的咖啡替換成無咖啡因的,看著世界陷入混亂。
5. 用巨大的橡皮擦擦掉所有國家的邊界,宣布地球是一個巨大的遊樂場。
6. 讓所有政治家在演講時必須穿著小丑服裝。
7. 把所有的數學課本改成烹飪食譜,看看會發生什麼。
8. 發明一種能讓人說出真心話的香水,然後在聯合國大會上噴灑。
9. 用巨大的磁鐵吸走地球上所有的金屬,建造一個太空迪士尼樂園。
10. 最後,如果以上都失敗了,就用可愛貓咪影片轟炸全世界的社交媒體,直到所有人都投降為止。
@@ -1,9 +0,0 @@
如何"正確"製作一個三明治:
1. 用力拍醒你的麵包。它們整天都在發懶!
2. 仔細選擇配料,確保每種都有自己的個性。例如,挑選一片特別愛說笑話的火腿。
3. 用尺精確測量每片配料的厚度,誤差不得超過1毫米。
4. 在組裝三明治時,必須戴上白手套和實驗室防護鏡,以確保最高級別的衛生。
5. 完成後,用放大鏡仔細檢查每一寸三明治,確保完美無缺。
記住:製作三明治是一門精確的科學,絕不容許有任何差錯或創意!
@@ -1,5 +0,0 @@
我在凌晨 3 點開始下載的東西,但我一直沒有時間去整理。
1. 一個可以把所有食物變成披薩的魔法棒設計圖
2. 如何在五分鐘內學會十種外星語言的秘密指南
3. 用舊襪子製作時尚帽子的教程視頻(連結已失效)
@@ -1,5 +0,0 @@
我在凌晨 4 點開始下載的東西,但我一直沒有時間去整理。
1. 一份詳細解釋為什麼貓咪才是地球真正統治者的論文
2. 如何用口香糖和迴紋針修理國際太空站的WikiHow頁面
3. 訓練水母成為水下偵探的完整課程大綱
@@ -1,3 +0,0 @@
Congrats! You fond a flie namd by somone who cleerly had a strok. If you can raed this, you mght be a geenius or jsut realy gud at Wheel of Fortnue.
Remeber: freinds don't let freinds name files aftr drinkng too much. But hey, at lest you used tab compltion to get here!
-95
View File
@@ -1,95 +0,0 @@
# 使用 Tab 自動補全
在這個關卡,你將會學到
1. 如何使用 Tab 自動補全路徑
2. 出現衝突時如何處理
按下中間的按鈕來開始這個關卡。
---
在終端機裡,所有的操作都是用文字進行。
然而,打字很花時間,而且只要一個字打錯,指令就會執行失敗。
因此終端機提供「自動補全」的功能,自動幫你完成檔案路徑、指令名稱、參數名稱,節省你的時間,也避免輸入錯誤。
在開始練習之前,我們先來看看家目錄下有哪些檔案。
> 使用 `ls` 查看檔案列表
---
你看到了這個資料夾中充滿了超級長和有錯字的檔案名稱,如果這些都要我們一個一個字輸入,肯定會很痛苦。
但不要緊,只要使用自動補全,檔名不論多長都能輕鬆輸入。
使用自動補全的方法是,**先輸入部分的檔名,再按下 Tab**
(Tab 是 Q 左邊的按鍵)
例如要補全 `SuperSecretPlansForWorldDomination.txt`,你可以先輸入 `Su` 再按下 Tab,終端機就會幫你打完剩下的檔名了。
我們試看看印出 `SuperSecretPlansForWorldDomination.txt` 這個檔案的內容。
> 使用 `cat SuperSecretPlansForWorldDomination.txt` 來印出檔案內容
> 在輸入 `cat Su` 之後按下 Tab 來自動補全檔名,完成後按 Enter 就能執行指令
---
你看到了終端機自動幫你補全了 `SuperSecretPlansForWorldDomination.txt`,就算是很長的檔名,也不用怕打錯字。
開頭不論你打了幾個字都無所謂,你可以只打 `S`、`Su`、`Sup`、`Super` 或甚至完全不打字都沒關係。
但是你打的字越少,就越容易跟其他的檔案重複,這時候就要做衝突處理。
關於衝突處理,我們會在待會練習。
自動補全也可以用來對付拼字錯誤,像是 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 這個檔案充滿了錯字,任誰來打都難保不會失手。
這一次,請你一樣用 `cat` 來試著印出 `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 的內容。
> 輸入 `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` 來印出檔案內容
> 輸入 `cat Thsi` 之後按下 Tab 做自動補全
> 試試看輸入更少的字元就按 Tab 會有什麼樣的結果
---
你看到了這個充滿錯字的檔案了。
只要使用自動補全,不管是錯字、大小寫變化、還是隨機亂碼都不再是困擾了。
讓我們再練習一個,這次請你查看 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾中的檔案。
一樣,輸入前幾個字,然後按 Tab 自動補全。
> 輸入 `ls ThingsIWillOrganizeLaterWhenIHaveTime` 查看資料夾中的檔案
> 輸入 `ls Thing` 之後按下 Tab 做自動補全
---
你看到了這個資料夾裡有著三個檔案:一個如何做三明治的教學跟兩個在半夜隨機下載的東西。
假如我們要打開三明治教學 `HowToProperlyMakeASandwich.txt`,但是它位在資料夾中,我們沒辦法只按一次 Tab 就補全它的檔名。
這次我們就要來練習使用**連續的自動補全**。
像這樣檔案位在其他資料夾的情況,我們只要先輸入資料夾名稱的開頭,按一次 Tab,再輸入檔案的開頭,再按一次 Tab,就能夠補全 `資料夾/檔名` 了。
而且當你補全的對象是資料夾時,終端機還會自動幫你在結尾加上 `/`,省去你自己輸入的時間。
那麼我們就來打開 `HowToProperlyMakeASandwich.txt`。
> 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` 來印出檔案內容
> 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
> 再輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/How` 按 Tab 補全至完整路徑
---
你看到了這份嚴謹的三明治製作教學。
那我們剩下來要看的就是資料夾裡的兩個檔案:`RandomStuffThatIDownloadedAt3AM.txt` 和 `RandomStuffThatIDownloadedAt4AM.txt`
兩個檔名非常相似,只有最後一個是 3AM 另一個是 4AM。
這樣我們只打前面幾個字的時候,沒有辦法分辦到底選的是哪一個檔案。
這就是**發生衝突**。
遇到衝突的解決方法,其實跟使用資料夾是一樣的。
在輸入 `Ran` 按下 Tab 後,終端機會補全到衝突發生前的位置,也就是 `RandomStuffThatIDownloadedAt`。
我們只要根據我們想開啟的檔案,輸入 `3` 或是 `4`,再按一下 Tab,就能夠補全剩下的檔名了。
_如果我們把完整的路徑視作補全的對象,那資料夾其實就是一種衝突的情況。_
> 輸入 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` 來印出檔案內容
> 先輸入 `cat Thin` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/`
> 再接續輸入 `Ran` 按 Tab 補全至 `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt`
> 再接續輸入 `3` 後按 Tab 補全至完整的路徑。
---
你看到了一份凌晨三點下載的檔案列表。嗯…我今天網路用的夠多了。
最後留下一個挑戰給你,請先進入到 `ThingsIWillOrganizeLaterWhenIHaveTime` 資料夾後,再印出 `RandomStuffThatIDownloadedAt4AM.txt`。
練習使用今天教的自動補全來完成任務。
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more