Showing posts with label stdout. Show all posts
Showing posts with label stdout. Show all posts

Wednesday, June 20, 2012

UNIX Programming

ทั้ง ๒ entries ที่ผ่านมา คือ program arguments  และ stdin/stdout  นั้น ถือเอาเป็น ตอนที่ I น่ะครับ นั่นเป็นเรื่องของ terminal ล้วนๆ ที่ไม่ต้องลำบากในการ “เปิด” และ “ปิด” อะไรให้ยุ่งยากแต่อย่างใดเลย  ใน entry นี้จะเป็นเรื่องที่ พิเศษ มาอีกนิดหนึ่ง  เชิญครับ

“STANDARD I/O LIBRARY”
standard I/O library (ต่อไปจะเรียกแต่สั้นๆว่า library) เป็นการรวบรวม routines ต่างๆ ไว้ที่เดียวกัน เพื่อให้เกิด ประสิทธิภาพ ของงานบริการด้าน I/O สำหรับโปรแกรมภาษา C และเพื่อให้สามารถใช้งานที่ไหนก็ได้  library นี้มีอยู่ในทุกๆระบบที่สนับสนุนภาษา C ดังนั้นโปรแกรมจึงสามารถโยกย้ายจากระบบหนึ่ง ไปยังอีกระบบอื่นได้โดยไม่ต้องแก้ไขเปลี่ยนแปลงอะไรเลย

“(๒.๑) File Access

โปรแกรมที่ยกมาให้ดูในตอน I นั้นทุกโปรแกรม อ่านข้อมูลจาก stdin และ เขียนผลลัพธ์ลงสู่ stdout ซึ่งได้นิยามเอาไว้ดีแล้ว และ โปรแกรม สามารถติดต่อได้โดยตรง    ในลำดับถัดมาเราจะเขียนโปรแกรมที่ต้องเข้าถึงแฟ้มเอกสาร ที่ยังไม่มีการติดต่อ กับโปรแกรม มาก่อนเป็นที่เรียบร้อยแล้ว เหมือนดั่งในกรณี stdin/stdout นั้น      ตัวอย่างง่ายๆ ได้แก่โปรแกรม wc ซึ่งนับจำนวนอักขระ จำนวนคำ จำนวนบรรทัดในแฟ้มเอกสารต่างๆ  เช่น

% wc x.c y.c

ก็จะพิมพ์ จำนวนบรรทัด คำ และอักขระในแฟ้มทั้งสอง และ จำนวนรวมแต่ละอย่างออกมา เป็นต้น

ปัญหาคือ เราจะให้แฟ้มเหล่านั้นถูกอ่านได้อย่างไรกัน นั่นคือจะเอาชื่อแฟ้ม ไปเชื่อมต่อกับประโยคทาง I/O ที่อ่าน-เขียนข้อมูลจริงๆ ได้อย่างไรกัน

กฏที่วางเอาไว้นั้น ง่ายมาก คือ ก่อนที่แฟ้มจะถูกอ่าน หรือ เขียน แฟ้มเอกสารต้องได้รับ การเปิด ก่อน ด้วยฟังก์ชั่นมาตรฐานใน library นี้ที่ชื่อ fopen.   ฟังก์ชั่นนี้ , fopen, จะเอาชื่อแฟ้มมา, ในที่นี้คือ x.c y.c ,  ดำเนินการบันทึกเป็นเบื้องต้นไว้ แล้วติดต่อ ถกเถียงกับระบบปฏิบัติการ  และ ได้ค่า ชื่อในวงการ มา ซึ่งจะใช้ชื่อนี้แหละ สำหรับการอ่าน และ การเขียนในลำดับต่อๆไป

ชื่อในวงการ นี้จะเป็น pointer, เรียกกันว่า file pointer, ที่ชี้ไปยัง structure ที่มีรายละเอียดของแฟ้ม เช่น ตำแหน่งของ buffer  ตำแหน่งปัจจุบันของอักขระใน buffer  แฟ้มนี้จะเปิดสำหรับอ่าน หรือ เปิดสำหรับเขียน และอื่นๆ  ซึ่งผู้ใช้งาน ไม่จำเป็นต้องรู้เลยแม้แต่น้อยนิดเกี่ยวกับ structure นี้ เพราะแฟ้ม stdio.h ได้ให้นิยามของ structure เอาไว้แล้ว ภายใต้ชื่อ FILE

สิ่งที่จำเป็นต้องระบุไว้ ก็มีเพียง การประกาศใช้งาน FILE เท่านั้น ดังตัวอย่าง

FILE *fp, *fopen();

ซึ่งบอกว่า fp เป็น pointer ไปยัง FILE และ fopen เป็นฟังก์ชั่นที่คืนค่า pointer มายัง FILE  ขอให้ทราบเอาง่ายๆว่า FILE นั้นเป็น ชนิดๆหนึ่ง คล้ายๆกับ int นั่นเอง อย่าคิดมาก

การเรียกใช้งานของฟังก์ชั่น fopen ก็ตรงไปตรงมา ดังนี้

    fp = fopen(name, mode);

โดยที่ name เป็นชื่อแฟ้มเอกสารที่เราต้องการทำงานด้วย   และ   mode นั้นคือลักษณะการทำงานกับแฟ้มเอกสารนั้นว่า เราจะ  อ่าน("r") หรือ เขียน("w") หรือ เขียนต่อท้าย("a") ทั้ง name และ mode เป็นอักขระ

ถ้าแฟ้มที่เราต้องการเปิดเพื่อ เขียน หรือ เขียนต่อท้าย นั้นยังไม่มี แฟ้มก็จะถูกสร้างขึ้นมา  การเปิด แฟ้มเอกสารที่มีอยู่แล้ว เพื่อ เขียน นั้น มีผลให้ สาระของแฟ้มเอกสารนั้น ถูกเขียนทับไป  และความพยายาม ที่จะอ่านแฟ้ม ที่ไม่มีอยู่เลย ยังผลให้ เกิด ความผิดพลาด ขึ้นมา   ในกรณีที่เกิด ความผิดพลาด ขึ้น ฟังก์ชั่น fopen จะคืนค่า NULL ซึ่งเป็น null pointer มาให้ผู้ใช้งาน และค่านี้ ได้รับ การนิยาม ไว้แล้ว ในแฟ้ม stdio.h

ในลำดับถัดมา ที่จำเป็นต้องทำคือ การอ่าน หรือ การเขียน หลังจากแฟ้มเอกสารได้รับ การเปิด ขึ้นมาแล้ว    ในการนี้ สามารถกระทำได้หลายทาง แต่ที่ง่ายที่สุดคือ  getc  และ  putc   โดยที่ getc จะคืนค่าถัดมาจากแฟ้มที่อ่าน มาให้, ฟังก์ชั่นนี้ ต้องการ file pointer เพื่อบอกว่า จะเอาจากแฟ้มไหน ดังนี้แล้ว

    c = getc(fp);

ก็จะนำค่าจากแฟ้มเอกสารที่อ้างอิงถึงโดย file pointer fp นั้น มาไว้ยัง c  และฟังก์ชันนี้ จะคืนค่า EOF ในกรณีที่อ่านไป จนถึงตำแหน่งสุดท้าย ของแฟ้มเอกสารแล้ว    putc  ก็จะกลับกันกับ  getc  กล่าวคือ

    putc(c, fp);

จะนำค่าอักขระ c ไปเก็บไว้ยังแฟ้ม ที่ชี้ไปโดย fp  แล้วคืนค่า c กลับมา    ทั้ง ๒ ฟังก์ชั่น จะคืนค่า  EOF ในกรณีที่เกิดความผิดพลาดขึ้น

อนึ่ง เมื่อโปรแกรมเริ่มทำงานนั้น แฟ้ม ๓ แฟ้มจะ ถูกเปิด ให้โดย อัตโนมัติทันที และ file pointer ก็จะกำหนดให้ ในทันที โดยอัตโนมัติ ด้วยเช่นกัน คือ  standard input, standard output และ standard error และ file pointer ของทั้ง ๓ นี้คือ stdin, stdout, stderr ตามลำดับ  และโดยปกติแล้ว ทั้ง ๓ file pointer นี้จะต่อตรงกับ terminal เสมอ แต่ก็อาจจะ redirect ไปยังที่อื่น หรือ pipe ต่อไปยังโปรแกรมอื่น อีกก็ได้  stdin, stdout, stderr นั้นเป็นวัตถุ ที่นิยามไว้แล้ว ในชนิด FILE * จึงสามารถใช้ได้กับวัตถุอื่นใด ที่เป็นชนิด FILE * ได้

เอาล่ะ เมื่อได้ความรู้มาเท่านี้แล้ว เรามาลองยกร่างโปรแกรมง่ายๆ ที่ชื่อ wc กันดูบ้าง ดังนี้

ถ้าโปรแกรมนี้ มี arguments มันก็จะ process arguments ไปจนหมด ถ้าไม่มี ก็จะรับจาก standard input  ซึ่งโดยลักษณะนี้ ทำให้โปรแกรม สามารถเรียกใช้งาน แบบเดี่ยวๆ ได้ หรือ สามารถใช้ร่วม เป็นส่วนหนึ่ง ของ process ใหญ่ได้    โปรแกม wc มีรายละเอียด, แฟ้มชื่อ wc.c,  และ Makefile ดังนี้

/*
 * wc.c
 */ 

#include <stdio.h>

int main(argc, argv)
int argc;
char *argv[];
{
  int c, i, inword;
  FILE *fp, *fopen();
  long linect, wordct, charct;
  long tlinect = 0, twordct = 0, tcharct = 0;

  i = 1;
  fp = stdin;
  do {
    if (argc > 1 && (fp=fopen(argv[i], "r")) == NULL) {
      fprintf(stderr, "wc: can't open %s\n", argv[i]);
      continue;
    }
    linect = wordct = charct = inword = 0;
    while((c = getc(fp)) != EOF) {
      charct++;
      if (c == '\n')
    linect++;
      if (c ==  ' ' || c == '\t' || c == '\n')
    inword = 0;
      else if (inword == 0) {
    inword = 1;
    wordct++;
      }
    }
    printf("%7ld %7ld %7ld", linect, wordct, charct);
    printf(argc > 1 ? " %s\n" : "\n", argv[i]);
    fclose(fp);
    tlinect += linect;
    twordct += wordct;
    tcharct += charct;
  } while (++i < argc);
  if (argc > 2)
    printf("%7ld %7ld %7ld total\n", tlinect, twordct, tcharct);
  return(0);
}



ฟังก์ชั่น fprintf ก็เหมือนกับฟังก์ชั่น printf เพียงแต่เราระบุ file pointer ชนิด FILE ไว้เป็น argument แรกก่อนเขาอื่น เท่านั้นเอง 

ฟังก์ชั่น fclose ก็เป็นฟังก์ชั่น ที่ตรงข้ามกับ fopen  มันทำหน้าที่ ตัดการเชื่อมต่อ ระหว่าง file pointer และ ชื่อข้างนอก ที่สร้างขึ้นมา โดยฟังก์ชั่น fopen นั้น และ ปลดปล่อย file pointer นั้น เพื่อให้ระบบ สามารถมี file pointer ไว้ใช้งานได้พอเพียง 

ขณะเดียวกัน fclose ก็จะกวาดล้าง buffer ที่ฟังก์ชั่น putc ได้รวบรวมเอาไว้นั้น ออกมาด้วยพร้อมกันเลย ฟังก์ชั่น fclose นี้ จะถูกเรียกมาโดยอัตโนมัติ ในทุกโปรแกรมที่ หยุดการทำงาน

และนี่คือ Makefile ของแฟ้มโปรแกรมที่ใช้ชื่อว่า wc.c

CC=clang
CFLAGS= -g
PROG= wc
SRCS= wc.c

NO_MAN=

.include <bsd.prog.mk>


อนึ่ง หากท่านสนใจในเรื่องของ Makefile ลองเข้าไปอ่านดูได้ที่ /usr/share/mk/bsd.README และแฟ้มอื่นๆ ที่เกี่ยวข้องได้ครับ

ขออนุญาตพัก ตอน II ช่วงแรกไว้ ที่นี่ก่อนครับ  ขอขอบคุณ ที่สละเวลามาอ่าน

Tuesday, June 19, 2012

UNIX Programming


(๑.๒) “Standard Input” และ “Standard Output”

จะไม่ขอแปลล่ะว่า  “Standard Input” คือ ทางเข้ามาตรฐาน และ  “Standard Output” คือ ทางออกมาตรฐาน เพราะหากพินิจพิจารณากันแล้ว อาจจะหมายถึง ทางอาหารใหม่เข้า และ ทางอาหารเก่าออก สำหรับคนธรรมดาทั่วไป แลหากมองไปถึงว่า standard input ของอีกโปรแกรมหนึ่ง จะมาจาก standard output ของอีกโปรแกรมหนึ่งแล้ว ยิ่งพาลไม่อยากกินอาหารไปเสียเลย ก็เป็นได้ แต่ ยังดีที่ว่า ยังไม่พบว่า stdanard output ของโปรแกรมหนึ่งเป็น standard input ของโปรแกรมเดียวกัน

ตกลง เรียกกันง่ายๆว่า stdin  สำหรับ “Standard input” และ stdout สำหรับ “Standard output” น่ะ

กลไกที่ง่ายที่สุดสำหรับการใส่ข้อมูลลงไปคือการอ่านค่าจาก stdin ซึ่งก็ได้แก่ termail ของผู้ใช้งานในขณะนั้นนั่นเอง    ฟังก์ชั่น getchar จะส่งค่าอักขระหนึ่งค่าถัดมา ทุกครั้งที่ฟังก์ชั่นนี้ถูกเรียกใช้งาน    แฟ้มอาจจะใช้แทนที่ terminal ได้โดยการใช้ข้อตกลง <file นี้ นั่นคือ ถ้า โปรแกรม prog เรียกข้อมูลด้วย getchar แล้ว คำสั่งนี้

% prog < file

บังคับให้ prog อ่านจาก file แทนที่  terminal  แลในความเป็นจริงแล้ว prog ไม่จำเป็นที่ต้องรู้เลยว่า ข้อมูลที่ใช้นั้น มาจากไหน  ซึ่งก็เป็นจริง ถ้าว่าข้อมูลนั้น มาจากอีกโปรแกรมหนึ่ง ดังนี้

% otherprog | prog

เพราะ stdin สำหรับ prog นั้นจะมาจาก stdout ของ otherprog

getchar จะคืนค่า EOF เมื่อมันอ่านไปจนจบแฟ้มอะไรก็สุดแล้วแต่ ที่ท่านจะให้เขาอ่าน  หรือ บางทีเมื่อเกิดข้อผิดพลาดขึ้นมา ก็จะส่งคืนค่า EOF นี้มาเช่นกัน   เราไม่จำเป็นต้องไปรู้จักกับ EOF เลยว่ามีค่าเป็นอะไร

ในทำนองเดียวกันนี้ putchar(c) ก็จะนำอักขระ c ไปที่ stdout  ซึ่งโดยค่าปริยายก็คือ terminal ของผู้ใช้งานในชณะนั้น  และผลลัพธ์ ที่ออกมาสามารถ จับไว้ ได้และเก็บในแฟ้ม ด้วยข้อตกลง >otherfile นี้ คล้ายๆกับ getchar นั่นเอง  กล่าวคือ ถ้า prog ใช้ putchar  แล้ว คำสั่ง

% prog > otherfile

ทำให้โปรแกรม prog เขียนหรือส่ง stdout ไปที่แฟ้ม otherfile แทนที่จะเขียนไปที่ terminal ตามค่าปริยายที่ตั้งไว้แล้วนั้น  ในกรณีนี้  otherfile จะถูกสร้างขึ้น ถ้าไม่มีอยู่ก่อน และจะถูกเขียนทับ ในกรณีที่มีอยู่แล้ว  ในขณะเดียวกัน กลไกของ pipe ก็สามารถใช้งานได้ด้วย เช่น

% prog | otherprog

ทำให้ stdout ของ prog กลายเป็น stdin ของ otherprog ไป    ตัวอย่างของโปรแกรมสั้นๆ ที่เขียนขึ้นมา เพื่อแสดงการทำงานของ getchar, putchar ปรากฏในเอกสารอ้างอิง หมายเลข (๒) ในหัวข้อที่ 1.5 ของบทแรกนั้น   ในที่นี้ ใคร่ขอยกตัวอย่าง โปรแกรมที่นับ อักขระ คำ จาก terminal ของผู้ใช้งาน  ในหน้า 20 หัวข้อที่ 1.5.4 มาให้ได้ศึกษากัน อนึ่งสำหรับ printf นั้น ก็เป็นฟังก์ชั่น ที่กำลังจะกล่าวถึง ในย่อหน้าหน้านี้

/*
 *
 * K & R , the C Programming Language
 *
 * knrwc.c
 *
 */
 

#include <stdio.h>

#define IN 1
#define OUT 0

int main()
{
  int c, nl, nw,nc, state;

  state = OUT;
  nl = nw = nc = 0;
  while ((c  = getchar()) != EOF) {
    ++nc;
    if (c  == '\n')
      ++nl;
    if (c == ' ' || c == '\n' || c == '\t')
      state = OUT;
    else if (state == OUT) {
      state = IN;
      ++nw;
    }
  }
  printf("%d %d %d\n", nl, nw, nc);
  return 0;
}


และนี่คือ Makefile ของโปรแกรมข้างบนนั้น

CC=clang
CFLAGS= -g
PROG= knrwc
SRCS= knrwc.c

NO_MAN=

.include
<bsd.prog.mk>

ฟังก์ชั่น printf ซึ่งจัดการรูปแบบของผลลัพธ์ได้หลากหลายนั้น ก็ใช้กลไกการทำงานเดียวกับ putchar  ดังนั้น การเรียกใช้งาน printf และ putchar จึงสามารถสลับสับเปลี่ยนกันไปมาได้ ผลลัพธ์ก็จะปรากฏตามลำดับที่เรียกใช้งาน     ฟังก์ชั่นคู่เหมือนของ pritf คือ scanf ซึ่งจะจัดรูปแบบของ สิ่งที่จะรับเข้ามาทาง stdin  และ  scanf นี้ใช้กลไกเดียวกันกับ getchar

ก่อนจากในตอนนี้ ใคร่ขอยกตัวอย่างชัดๆ ว่า เพียงแค่ getchar, putchar ก็สามารถนำมาใช้งานในชีวิตจริงได้แล้ว ดังโปรแกรมข้างล่างนี้ ซึ่งเขาจะ ขลิป อักขระพิเศษ ที่รับเข้ามา ออกทิ้งเสีย แล้วจึงพิมพ์ออกทาง stdout


/*
 *
 * ccs.c
 *
 */
 
#include <stdio.h>

int main()
{
  int c;
  while ((c = getchar()) != EOF)
    if ((c >= ' ' && c < 0177) || c == '\t' || c == '\n')
      putchar(c);
  return 0;
}


ทั้งสองโปรแกรมที่ยกตัวอย่างมาให้ดูนี้ บรรทัดแรกที่เป็น

#include <stdio.h>

นั้น จะบอก compiler ว่าให้รวมเอาแฟ้ม /usr/include/stdio.h เข้าไว้ด้วย ซึ่งแฟ้มดังกล่าว จะนิยามค่าต่างๆ เช่น EOF เอาไว้แล้ว

และ นี่คือ Makefile ของโปรแกรมขลิปอักขระพิเศษนี้

CC=clang
CFLAGS= -g
PROG= ccs
SRCS= ccs.c

NO_MAN=

.include <bsd.prog.mk>


unix เขาจะมีหน้า manual ให้สำหรับทุกคำสั่ง ดังนั้น FreeBSD จึงสรีางแม่แบบสำหรับหน้า manual ขึ้นมาให้ด้วย สำหรับทุกโปรแกรมที่เราเขียนขึ้น แต่ในครั้งแรกนี้ เพื่อตัดความยุ่งยากออกไปก่อน จึงไม่ต้องให้มีหน้า manual ในโปรแกรมของเราที่เขียน และนี่คือความหมายของบรรทัด

NO_MAN=

ในแฟ้ม Makefile นั้น
ในตอนที่สองนี้ ขอยุติไว้เพียงเท่านี้ ยังมีอีกหลายตอนอยู่น่ะครับ

Friday, July 4, 2008

shell(2)

find

ตั้งใจจะจบเรื่อง shell ไว้เท่าที่เขียนไป แต่มานึกดูอีกที หากขาดรายละเอียดของทั้ง ไฟ, find, และ เสร็จ, sed ที่ใช้ประกอบการบรรยาย ก็เหมือนว่ากินข้าว แล้วไม่ดื่มน้ำตาม ก็ด้วยเหตุนี้แล จึงมีภาคนี้เป็นลำดับถัดมา

find(1) ที่ใช้ค้นหาเอกสารนั้น มีรูปแบบง่ายๆ อย่างที่ยกมานั่นคือ
# find  /usr/ports  -name work  -type d  -print
ซึ่งมีความหมายตรงไปตรงมาคือ ให้หา จาก /usr/ports เอกสารชื่อ work ที่เป็น directory ส่วนที่เหลือนั้นบอกว่า ให้พิมพ์ออกมาเมื่อหาเจอ,  -print
ผลลัพธ์ก็คือ, สำหรับ goffice2,
/usr/ports/converters/fribidi/work
/usr/ports/converters/psiconv/work
/usr/ports/deskutils/glabels/work
/usr/ports/deskutils/planner/work
/usr/ports/devel/goffice/work
/usr/ports/devel/libgsf-gnome/work
/usr/ports/devel/goffice04/work
/usr/ports/editors/abiword/work
/usr/ports/editors/gnome2-office/work
/usr/ports/graphics/dia/work
/usr/ports/graphics/libwmf/work
/usr/ports/math/gnumeric/work
/usr/ports/textproc/wv/work

ซึ่งเรียกกัน หรือ รู้กันว่า คือ temporary working directory, ${WRKDIR}, และจะพิมพ์ออกมาทาง standard output,  stdout ทีละบรรทัด ทีละบรรทัดไป และนี่แหละคือ ไฟ, find(1)

ถ้าเป็นเช่นนี้ คงไม่ใช่ shell ที่เราต้องการ เพราะสิ่งที่เราต้องการคือ, ยกมาจำเพาะผลลัพธ์ บางส่วน,

cd /usr/ports/converters/fribidi && make package
cd /usr/ports/converters/psiconv && make package
....
cd /usr/ports/textproc/wv && make package


ก็แล้วเราจะเปลี่ยนให้ /usr/ports/converters/fribidi/work เป็น cd /usr/ports/converters/fribidi && make package ได้ยังไง ซึ่งก็นี่แหละคือหน้าที่ของ เสร็จ, sed(1) ไงละ

sed
คำสั่งเสร็จที่ใช้ในตัวอย่างที่ยกมาคือ
# sed  -e 's,/usr,cd /usr,'  -e 's,/work, \&\& make package,'
ซึ่งขอยกรูปแบบทั่วไปที่ใช้ตอนนี้มาให้เห็นคือ
# sed   -e  'sDELIMITtextDELIMITnew textDELIMIT'
โดยที่ input ของเสร็จ จะเป็น output ของ ไฟ เพราะส่งกันมาด้วย ท่อ, pipe อีกทีหนึ่ง ก็ต้องเป็นยังงี้แหละ เมื่อ ไฟ ส่งมา ๑ บรรทัด เสร็จ เขาก็จะ process ไป ๑ ที แล้วก็ส่งผลลัพธ์ไปที่ stdout

คราวนี้คงง่ายแล้วใช่ไหม ในการที่จะอ่านสิ่งที่ ได้บอกไว้ตั้งแต่ ports(1) โน้น โดยที่เราเลือกที่จะใช้ตัว คอมม่า เป็นตัว DELIMIT เพื่อแก้ไขตัว text ให้ได้ตามใจ
อ้อ เราต้องแก้ ๒ หนนะ ก็เลยต้องมี สีลบออง ๒ ลบอี sed  -e 's, , ,'  -e 's, , ,' โดยลบอีแรก เราแก้จาก /usr เป็น cd /usr และ ลบอีหลัง เราแก้จาก /work เป็น && make package แต่ สืบเนื่องจากตัว && นั้น มีความหมายพิเศษ เราจึง ยกความหมาย มันออกไปไว้ที่อื่นชั่วคราว ด้วยการใช้ ตัว \ ไงครับ

เสร็จ เอ้ยจบ find & sed และย้ำว่า โปรดศึกษา find(1) & sed(1) เพิ่มเติมด้วย

ขอย้ำว่า เรื่องของ sh ที่นำเสนอนี้ เป็นแต่เพียงสาระเบื้องต้นเท่านั้น และยังหยาบมากอยู่ เพราะยังขาด ในส่วนของการตรวจสอบข้อบกพร่อง ยังขาดการตรวจสอบ return code ของแต่ละคำสั่ง และยังขาดอีก ฯลฯ ขอให้ผู้ใฝ่รู้จงหาความรู้เพิ่มเติมเอาเองจาก sh(1)

ขอให้ประสบความสำเร็จ และ โชคดี

View My Stats