Giter Club home page Giter Club logo

sh's Introduction

sh

Go Reference

A shell parser, formatter, and interpreter. Supports POSIX Shell, Bash, and mksh. Requires Go 1.21 or later.

Quick start

To parse shell scripts, inspect them, and print them out, see the syntax examples.

For high-level operations like performing shell expansions on strings, see the shell examples.

shfmt

go install mvdan.cc/sh/v3/cmd/shfmt@latest

shfmt formats shell programs. See canonical.sh for a quick look at its default style. For example:

shfmt -l -w script.sh

For more information, see its manpage, which can be viewed directly as Markdown or rendered with scdoc.

Packages are available on Alpine, Arch, Debian, Docker, Fedora, FreeBSD, Homebrew, MacPorts, NixOS, OpenSUSE, Scoop, Snapcraft, Void and webi.

gosh

go install mvdan.cc/sh/v3/cmd/gosh@latest

Proof of concept shell that uses interp. Note that it's not meant to replace a POSIX shell at the moment, and its options are intentionally minimalistic.

Fuzzing

We use Go's native fuzzing support. For instance:

cd syntax
go test -run=- -fuzz=ParsePrint

Caveats

  • When indexing Bash associative arrays, always use quotes. The static parser will otherwise have to assume that the index is an arithmetic expression.
$ echo '${array[spaced string]}' | shfmt
1:16: not a valid arithmetic operator: string
$ echo '${array[dash-string]}' | shfmt
${array[dash - string]}
  • $(( and (( ambiguity is not supported. Backtracking would complicate the parser and make streaming support via io.Reader impossible. The POSIX spec recommends to space the operands if $( ( is meant.
$ echo '$((foo); (bar))' | shfmt
1:1: reached ) without matching $(( with ))
  • Some builtins like export and let are parsed as keywords. This allows statically building their syntax tree, as opposed to keeping the arguments as a slice of words. It is also required to support declare foo=(bar). Note that this means expansions like declare {a,b}=c are not supported.

JavaScript

A subset of the Go packages are available as an npm package called mvdan-sh. See the _js directory for more information.

Docker

All release tags are published via Docker, such as v3.5.1. The latest stable release is currently published as v3, and the latest development version as latest. The images only include shfmt; -alpine variants exist on Alpine Linux.

To build a Docker image, run:

docker build -t my:tag -f cmd/shfmt/Dockerfile .

To use a Docker image, run:

docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/mnt" -w /mnt my:tag <shfmt arguments>

Related projects

The following editor integrations wrap shfmt:

Other noteworthy integrations include:

sh's People

Contributors

andreynering avatar arturklauser avatar cclerget avatar dhanusaputra avatar diamondburned avatar ebfe avatar fauust avatar hristiy4n avatar ihar-orca avatar jansorg avatar jounqin avatar kaey avatar lollipopman avatar maienm avatar mikepqr avatar mingrammer avatar morganantonsson avatar mvdan avatar oalders avatar oliv3r avatar parkerduckworth avatar peterdavehello avatar przmv avatar quasilyte avatar raylee avatar reubeno avatar riacataquian avatar scop avatar theclapp avatar zimbatm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sh's Issues

parse: proper support for [[

We don't need to do anything for [ since it's actually just a command with arguments.

On the other hand though, [[ from Bash is a reserved word and should be parsed as a separate node. We parse it as a command right now, just like [.

It is possible to craft a valid Bash program to cause the parser to break because of this:

[[ foo =~ bar(a) ]]

cmd/shfmt: publish to homebrew

Would you consider publishing this to the homebrew OSX package manager? I am able to get it running from go source, but I think making a binary available through homebrew would greatly increase the reach of this package.

Aside: I'm also attempting to write an atom text editor plugin to format a shell script through shfmt.

parser: figure out what to do about bash's extended globbing

If bash's extended globbing is on, some programs are suddenly valid:

shopt -s extglob

echo foo@(bar)

Normally, this fails as ( is a token:

 $ echo 'echo @(foo)' | bash
bash: line 1: syntax error near unexpected token `('
bash: line 1: `echo @(foo)'
 $ echo 'echo @(foo)' | shfmt
1:7: a command can only contain words and redirects

I see two possible options:

  • Change the behaviour of the parser whenever a shopt statement is parsed (ugh)
  • Always parse extended globbing stuff in the bash mode

The first option would make the parser even more complex, as it would change behaviour not only depending on the context, but also depending on what statements it has parsed.

The second option is somewhat cleaner, but I see some minor disadvantages:

  • Some invalid bash programs will be accepted (which is okay for shfmt)
  • If this parser is ever used for anything serious, another package (like an interpreter/executor) would have to take care of erroring if it finds an extended globbing node when not in extglob mode

syntax: full support for a regexp following =~

Right now in extended tests we simply read a word after =~. This works most of the time, but it can be broken with some forms of ERE, by using characters that would otherwise be tokens (like () or spaces.

[[ foo =~ bar(a) ]]
[[ foo =~ ba[ r] ]]

print: forever loop if heredoc word contains heredoc

 $ echo '<<$(<<EOF)' | shfmt
[proceeds to run until it has done too many recursive calls]

What we do is print the word (unquoted) to get the stop line for the heredoc body.

Interestingly enough, it seems that Bash does something different, as any nested statements are kept intact as they were read:

 $ echo '<<foo"bar"' | bash
bash: line 1: warning: here-document at line 1 delimited by end-of-file (wanted `foobar')
 $ echo '<<$(foo   bar)' | bash
bash: line 1: warning: here-document at line 1 delimited by end-of-file (wanted `$(foo   bar)')

parse: index out of range panic with heredocs and logical ops

$ cat x.bash
cat <<FOO|rev&&echo done 
foobar
FOO
$ bash x.bash
raboof
done
$ shfmt x.bash
panic: runtime error: index out of range

goroutine 1 [running]:
panic(0x4e4320, 0xc820010160)
    /home/ebfe/src/go/src/runtime/panic.go:500 +0x18a
github.com/mvdan/sh.(*parser).doHeredocs(0xc82008dae8)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:355 +0x1e3
github.com/mvdan/sh.(*parser).next(0xc82008dae8)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:239 +0x267
github.com/mvdan/sh.(*parser).got(0xc82008dae8, 0xfffffffffffffffd, 0xc82000a700)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:425 +0x42
github.com/mvdan/sh.(*parser).wordPart(0xc82008dae8, 0xb, 0xb)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:647 +0x79e
github.com/mvdan/sh.(*parser).readParts(0xc82008dae8, 0xc82000a7c0)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:599 +0x25
github.com/mvdan/sh.(*parser).gotWord(0xc82008dae8, 0xc82000a7c0, 0x5ba000)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:584 +0x2d
github.com/mvdan/sh.(*parser).cmdOrFunc(0xc82008dae8, 0xffffffffffffffe4, 0x0)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:1306 +0x1fd
github.com/mvdan/sh.(*parser).gotStmtPipe(0xc82008dae8, 0xc82005a540, 0x0)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:1030 +0x95d
github.com/mvdan/sh.(*parser).gotStmtAndOr(0xc82008dae8, 0xc82005a540, 0x0, 0x0, 0x0, 0x5ba040)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:992 +0x2a2
github.com/mvdan/sh.(*parser).gotStmt(0xc82008dae8, 0xc82005a540, 0x0, 0x0, 0x0, 0x409e90)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:965 +0xc4
github.com/mvdan/sh.(*parser).followStmt(0xc82008dae8, 0x1, 0xe, 0x506d35, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:468 +0x8e
github.com/mvdan/sh.(*parser).binaryStmt(0xc82008dae8, 0x58b900, 0xc82004c440, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:1052 +0x17f
github.com/mvdan/sh.(*parser).gotStmtAndOr(0xc82008dae8, 0xc82005a300, 0x0, 0x0, 0x0, 0xc82002a050)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:998 +0x31a
github.com/mvdan/sh.(*parser).gotStmt(0xc82008dae8, 0xc82005a300, 0x0, 0x0, 0x0, 0x1000)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:965 +0xc4
github.com/mvdan/sh.(*parser).stmts(0xc82008dae8, 0x0, 0x0, 0x0, 0x1000, 0x1000, 0x20)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:555 +0x119
github.com/mvdan/sh.Parse(0x58b300, 0xc82002a048, 0x7fff891c654b, 0xf, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
    /home/ebfe/go/src/github.com/mvdan/sh/parse.go:38 +0x119
main.format(0x7fff891c654b, 0xf, 0x58cb40, 0xc82005d6c0)
    /home/ebfe/go/src/github.com/mvdan/sh/cmd/shfmt/main.go:75 +0xbc
main.work(0x7fff891c654b, 0xf, 0x0, 0x0)
    /home/ebfe/go/src/github.com/mvdan/sh/cmd/shfmt/main.go:49 +0xb4
main.main()
    /home/ebfe/go/src/github.com/mvdan/sh/cmd/shfmt/main.go:28 +0x68

syntax: figure out the node structure for indexed assignments

foo[bar]=value

Right now we treat foo[bar] as a literal. This is fine for POSIX, but not bash. Besides being wrong, it can also mean the parser mistakingly errors:

 $ echo 'foo[(1)]=bar' | bash
 $ echo 'foo[(1)]=bar' | shfmt
1:1: "foo(" must be followed by )

print: never collapse "( (" into "(("

shfmt incorrectly reformats

git_lines="$( ( echo "$git_data";
    cat "$(dirname "$0")/foo.txt") | sort |uniq)"

to

git_lines="$((
    echo "$git_data"
    cat "$(dirname "$0")/foo.txt"
) | sort | uniq)"

Which breaks; $((stuff)) is arithmetic expansion, but $( (stuff) ) is a nested subshell inside a command substitution.

parse: support multiline arrays

Should it support multiline arrays? Like this:

var=( 
    1 
    2 
    3
)
echo $var

Right now it's failing:

$ shfmt test.sh
test.sh:3:5: reached literal without matching token ( with )

I'm asking because in the canonical example there is nothing like a multiline array.

By the way, really nice tool!

parse: allow functions with bash reserved words as names

The following should be allowed:

let() { foo; }

Same goes for the rest of bash-only reserved words: eval, local, declare, etc.

This isn't as simple to fix as it may seem at first sight. It's tricky because some statements like let are followed by arithmetic expressions, not regular words. So we cannot tokenize past the first word to see if a ( follows, because we might start tokenizing the following element as a word instead of as an arithmetic expression.

proposal: add simplify (-s) flag to shfmt

Much like gofmt -s, which removes bits of code that aren't necessary such as repeated types.

Rules that come to mind, which would be useful in the shell:

  • Get rid of backticks - `foo` -> $(foo)
  • Remove useless word parts - foo""bar -> foobar (applies to "", '', $() and perhaps more)
  • Force functions to use blocks - foo() bar -> foo() { bar; } (the first works under some shells)

Some more rules that might be too pedantic/opinionated:

  • Force portable functions - function foo() { bar; } -> foo() { bar; }
  • Replace ${foo} with $foo where it means the same
  • Remove quotes where they're unnecessary - "foo" -> foo

Feedback and ideas welcome @focusaurus @ebfe @fwip @mmlb @legionus @mkovacs @seletskiy

BinaryCmd with a nil Y operand inside a LetClause

I understand that this doesn't affect the correctness of shfmt, and also that the AST likely wasn't designed to be used outside of this repository. Still, a separate sh-ast package would be super useful.

To reproduce, parse the following snippet, then inspect the AST.

function generate_piece {
  while true; do
    let pos=RANDOM%fields_total
    let board[$pos] || {
      let value=RANDOM%10?2:4
      board[$pos]=$value
      last_added=$pos
      printf "Generated new piece with value $value at position [$pos]\n" >&3
      break;
    }
  done
  let pieces++
}

I think the offending statement is a LetClause of the form:

let word || {
  stmt
}

proposal: restrict to POSIX if walking a file that isn't bash

When recursively walking a directory, we take all files that either have a valid extension (.sh, .bash) or a valid shebang (any combination of env, sh and bash).

Whether or not we restrict to POSIX is bound by the -p flag. shfmt could be smarter and derive the right restriction depending on whether the extension/shebang is bash or shfmt.

I see various options about this detection:

  • Have this always on by default.
  • Have this enabled by a flag, something like -a. The default would be as now, never restrict to POSIX.

The first option is tricky because bash scripts in general have the .sh extension and a bash shebang. So we would need some extra logic around it.

The second option is a more conservative change, and the behaviour of shfmt will be more clear. At the same time, -a might seem like an unnecessary flag that should always be on.

In any case, -p is here to stay because otherwise there is no way to force POSIX on a file with no extension nor shebang to derive it.

syntax: weird indentation if command 2 empty lines after comment

[manny@manny installer]$ cat <<EOF | shfmt
> #!/usr/bin/env bash
> 
> 
> echo hi
> EOF
#!/usr/bin/env bash

 \
        echo hi
[manny@manny installer]$ cat <<EOF | shfmt
> # some comment
> 
> 
> echo hi
> EOF
# some comment

 \
        echo hi
[manny@manny installer]$ cat <<EOF | shfmt
# some comment

echo hi
> EOF
# some comment

echo hi

parse: (( should equal $(( in bash

[manny@maxwell ~]$ cat t.sh 
((5==5))
[manny@maxwell ~]$ source t.sh; echo $?
0
[manny@maxwell ~]$ cat f.sh 
((5>6))
[manny@maxwell ~]$ source f.sh; echo $?
1

but shfmt is adamant about inserting a space between the leading double parens, probably due to 5979268 which fixed #16

maybe a screenshot

can't tell what this repo does from the README. a visualization would be great?

keep `;` at end of new-line separated lists

I commonly do something like:

foo \
bar \
baz \
another \
;

so that I can just add new entry/entries anywhere and then visually select and sort in vim. Unfortunately shfmt drops the final \ and the ; which then messes up my workflow when something new is sorted at the end. This is very likely a niche annoyance so I'd like to know your thoughts.

incorrectly fmts $((...$()..))

bisected down to commit d1df3bc

[manny@manny shfmt]$ echo 'echo "$(($(echo 5)+5))"' | bash
10
[manny@manny shfmt]$ git checkout -q d1df3bcdec78e0fee97ffb5d19b008e3ab038d81^
[manny@manny shfmt]$ go build &&  echo 'echo "$(($(echo 5)+5))"' | ./shfmt 
echo "$(($(echo 5) + 5))"
[manny@manny shfmt]$ git checkout -q d1df3bcdec78e0fee97ffb5d19b008e3ab038d81
[manny@manny shfmt]$ go build &&  echo 'echo "$(($(echo 5)+5))"' | ./shfmt 
echo "$( ($(echo 5)+5))"

cmd/shfmt: add flags to read/write AST in JSON format to simplify integration with other languages

Quite a few projects out there implement their own shell/bash parsers. Not only the shells themselves, but also some programs that work with shell scripts such as linters.

It's really a little silly for each one of them, especially tooling around shell scripts, to have to implement their own. Sadly, the shells out there don't allow their use as libraries (that I know of).

This is a library, so it does solve that problem. But being written in Go, it's not easily usable from other languages like Python or Shell itself.

Some ideas come to mind:

Both of those could be extended to also include the printer.

Any input will be appreciated, especially from people who would use something like this.

print: needless empty line

input

 #!/bin/sh

 if    [ "x$foo" == "x"   ]; then
TEST=1
 if    [ "y$bar" == "y"   ]; then
TEST=2
else
  fi
else
  fi

output

#!/bin/sh

if [ "x$foo" == "x" ]; then
    TEST=1
    if [ "y$bar" == "y" ]; then
        TEST=2

    fi

fi

parser: solve $(( vs $( ( ambiguity with backtracking

$(( marks the start of an arithmetic expansion, $( marks the start of a command substitution and ( marks the start of a subshell. Examples:

 $ echo -n 'echo $((1+2))' | bash
3
 $ echo -n 'echo $( (echo foo) | cat)' | bash
foo

This is easy, because we know how we should parse what follows the starting tokens directly. In other words, this is a LL(k) language - we don't have to backtrack.

The fun begins here:

 $ echo -n 'echo $((echo foo) | cat)' | bash
foo

Even though this should be parsed as an arithmetic expansion, it's parsed as a subshell inside a command substitution. Our parser fails at this, because it does no backtracking at all and errors on the invalid arithmetic expression:

 $ echo -n 'echo $((echo foo) | cat)' | shfmt
1:14: not a valid arithmetic operator: echo

This is clearly a bash thing, not a POSIX Shell thing:

 $ echo -n 'echo $((echo foo) | cat)' | dash
dash: 1: Syntax error: Missing '))'
 $ echo -n 'echo $((echo foo) | cat)' | busybox ash
ash: syntax error: missing '))'

Sounds to me like POSIX Shell is indeed LL(k), but that bash is LL(*). In other words, we may have to consume an unknown number of characters before we know what we should do with the following token.

Should we make our parser backtrack?

parse: backquotes separated from word

for fastq in $SAMPLE_NAME*.fastq.gz; do
    zcat $fastq \
        >filtered_`basename $fastq .gz`;
done

becomes

for fastq in $SAMPLE_NAME*.fastq.gz; do
    zcat $fastq \
        `basename $fastq .gz` >filtered_
done

I'm not exactly sure what combination triggers this, but it doesn't seem to happen without output redirection. Using $() syntax also seems to be handled correctly, which surprised me.

parse: does not understand case fallthrough tokens

shfmt is totally fine with this code:

#!/bin/bash

do_fallthrough() {

        case $1 in
                'foo')
                        echo foo
                        ;;
                'bar')
                        echo bar
                        ;;
                *)
                        echo baz
                        ;;
        esac
}

echo
do_fallthrough foo

echo
do_fallthrough bar

echo
do_fallthrough blah

But with this code:

#!/bin/bash

do_fallthrough() {

        case $1 in
                'foo')
                        echo foo
                        ;;
                'bar')
                        echo bar
                        ;&
                *)
                        echo baz
                        ;;
        esac
}

echo
do_fallthrough foo

echo
do_fallthrough bar

echo
do_fallthrough blah

I get this (I believe) erroneous error:
sample.sh:11:4: ; can only immediately follow a statement

Relevant Documentation:
https://www.gnu.org/software/bash/manual/bashref.html#index-case

token: token.Position Offset 0 vs 1 based

Maybe a misunderstanding on my part, but the documentation suggest the Offset field of token.Position is 0 based

type Position struct {
        Offset int // offset, starting at 0

while the following example seems to suggest it's 1 based:

$ cat x.sh
ls
$ cat x.go
package main

import (
    "fmt"
    "io/ioutil"

    "github.com/mvdan/sh/parser"
)

func main() {
    buf, err := ioutil.ReadFile("x.sh")
    if err != nil {
        panic(err)
    }

    ast, err := parser.Parse(buf, "x.sh", parser.ParseComments)
    if err != nil {
        panic(err)
    }

    callexpr := ast.Stmts[0].Cmd

    pos := ast.Position(callexpr.Pos())
    end := ast.Position(callexpr.End())

    fmt.Print(string(buf[pos.Offset:end.Offset]))
}
$ go run x.go
s
$

syntax: figure out whether to support associative arrays

This is valid, if the array is associative (like a map from string to x).

 $ echo 'declare -A foo; ${foo[some key]}' | bash
 $ echo 'declare -A foo; ${foo[some key]}' | shfmt
1:28: not a valid arithmetic operator: key

Worth noting that we can't know this statically - bash does this at run-time:

 $ echo '${foo[some key]}' | bash
bash: line 1: some key: syntax error in expression (error token is "key")

This is similar to #37. In both cases, the parser should change behaviour depending on what has been run, not simply what has been parsed. So we have two options:

  • Not support associative arrays fully (i.e. only say things will work well if literals - names - are used)
  • Fall back to accepting the problematic chars to support these associative array keys

The latter will be tricky, because then ${foo[1+2]} will result in the arithmetic expression 1 + 2 even though if it were an associative array it should have been 1+2 as a literal.

It gets even trickier if we start mixing in the printer. In the case above, the printer would probably break programs by introducing or removing spaces.

The first option is the only sane to me. The second will do better in some weird cases, but at the cost of the tool becoming more complex and even unreliable.

parse: heredoc bodies can contain expansions

$ cat <<EOF
> $(echo foo)
> EOF
foo

Right now we're just reading lines and treaging everything as literals, as if it were in between single quotes. This is more like as if it were in between double quotes.

syntax: array indexing should support expressions

 $ foo=(1 2 3); echo ${foo[1+1]}
3

Right now we just read a word. This produces the wrong AST and could lead to errors if we see tokens that are otherwise meaningful inside a parameter expansion, like -.

Thankfully, this doesn't seem to be widely used.

parser: (( should be retried like $((

We retry $(( as $( (. Turns out we need to do the same for ((. I can't find this on the newest posix spec, but even dash supports this.

 $ echo '((echo foo); echo bar)' | bash
foo
bar
 $ echo '((echo foo); echo bar)' | shfmt
1:8: not a valid arithmetic operator: foo

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.