Skip to content

Commit

Permalink
Implement $os:umask
Browse files Browse the repository at this point in the history
Resolves elves#681
  • Loading branch information
krader1961 committed Mar 25, 2020
1 parent 02f5433 commit 996e6c5
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 1 deletion.
94 changes: 94 additions & 0 deletions pkg/eval/unix/umask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// +build !windows,!plan9

package unix

import (
"errors"
"fmt"
"math"
"strconv"
"syscall"

"github.com/elves/elvish/pkg/eval"
"github.com/elves/elvish/pkg/eval/vars"
)

//elvdoc:var umask
//
// The file system creation mask. When used in a string context the output is
// its Elvish octal representation; e.g. 0o027. This makes it possible to use
// it in any context that expects a `$number`.
//
// When assigning a new value a string is implicitly treated as an octal
// number. If that fails the usual rules for interpreting
// [numbers](./language.html#number) are used. This means you can do both
// `unix:umask = 027` and `unix:umask = 0o27`. You can also assign to it a
// `float64` data type that has no fractional component. The legal values are
// in the range [0 .. 0o777].
//
// You can do a temporary assignment to affect a single command; e.g.
// `umask=077 touch a_file`. After the command completes the old umask will be
// restored. **Warning**: Since the umask applies to the entire process, not
// individual threads, changing it temporarily in this manner is dangerous if
// you are doing anything in parallel. Such as via the
// [`peach`](ref/builtin.html#peach) command.
//
// This variable only has meaning on UNIX like operating systems and does not
// exist on other platforms. Do not use this in elvish scripts meant to work
// on non-UNIX compatible systems.

// UmaskVariable is a variable whose value always reflects the current file
// creation permission mask. Setting it changes the current file creation
// permission mask for the process (not an individual thread).
type UmaskVariable struct {
ev *eval.Evaler
}

var _ vars.Var = UmaskVariable{}

// Get returns the current file creation umask as a string.
func (UmaskVariable) Get() interface{} {
// Note: The seemingly redundant syscall is because the syscall.Umask() API
// doesn't allow querying the current value without changing it. So ensure
// we reinstate the curent value.
umask := syscall.Umask(0)
syscall.Umask(umask)
return fmt.Sprintf("0o%03o", umask)
}

// Set changes the current file creation umask. It can be called with a string
// (the usual case) or a float64.
func (UmaskVariable) Set(v interface{}) error {
var umask int

switch v := v.(type) {
case string:
i, err := strconv.ParseInt(v, 8, 0)
if err != nil {
i, err = strconv.ParseInt(v, 0, 0)
if err != nil {
return errors.New("umask value not a valid number")
}
}
umask = int(i)
case float64:
int_part, frac_part := math.Modf(v)
if frac_part != 0 {
return errors.New("umask value must be an integer")
}
umask = int(int_part)
default:
return errors.New("umask value must be a string or float64")
}

if umask < 0 || umask > 0o777 {
return errors.New("umask value outside the range [0..0o777]")
}

syscall.Umask(umask)
return nil
}

func init() {
Ns.Add("umask", UmaskVariable{})
}
41 changes: 41 additions & 0 deletions pkg/eval/unix/umask_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// +build !windows,!plan9

// Note that this unit test assumes a UNIX environment with a POSIX compatible
// /bin/sh program.
package unix

import (
"testing"

"github.com/elves/elvish/pkg/eval"
)

var That = eval.That

func TestUmask(t *testing.T) {
setup := func(ev *eval.Evaler) { ev.Builtin.AddNs("unix", Ns) }
eval.TestWithSetup(t, setup,
// We have to start with a known umask value.
That(`unix:umask = 022`).Puts(),
That(`put $unix:umask`).Puts(`0o022`),
// Now verify that mutating the value and outputing it works.
That(`unix:umask = 23`).Puts(),
That(`put $unix:umask`).Puts(`0o023`),
That(`unix:umask = 0o75`).Puts(),
That(`put $unix:umask`).Puts(`0o075`),
// Verify that a temporary umask change is reverted upon completion of
// the command. Both for builtin and external commands.
That(`unix:umask=012 put $unix:umask`).Puts(`0o012`),
That(`unix:umask=0o23 /bin/sh -c 'umask'`).Prints("0023\n"),
That(`unix:umask=56 /bin/sh -c 'umask'`).Prints("0056\n"),
That(`put $unix:umask`).Puts(`0o075`),
// People won't normally use non-octal bases but make sure these cases
// behave sensibly given that Elvish supports number literals with an
// explicit base.
That(`unix:umask=0x43 /bin/sh -c 'umask'`).Prints("0103\n"),
That(`unix:umask=0b001010100 sh -c 'umask'`).Prints("0124\n"),
// We should be back to our expected umask given the preceding tests
// applied a temporary change to that process attribute.
That(`put $unix:umask`).Puts(`0o075`),
)
}
11 changes: 11 additions & 0 deletions pkg/eval/unix/unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Package `unix` exposes variables and functions that deal with features
// unique to UNIX like operating systems. On other operating systems it will
// be an empty namespace; or have a subset of the features normally available
// on a UNIX like operating system.
package unix

import (
"github.com/elves/elvish/pkg/eval"
)

var Ns = eval.Ns{}
6 changes: 5 additions & 1 deletion pkg/program/shell/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/elves/elvish/pkg/eval/re"
"github.com/elves/elvish/pkg/eval/store"
"github.com/elves/elvish/pkg/eval/str"
"github.com/elves/elvish/pkg/eval/unix"
"github.com/elves/elvish/pkg/eval/platform"
daemonp "github.com/elves/elvish/pkg/program/daemon"
)
Expand Down Expand Up @@ -72,10 +73,13 @@ func InitRuntime(binpath, sockpath, dbpath string) (*eval.Evaler, string) {

ev := eval.NewEvaler()
ev.SetLibDir(filepath.Join(dataDir, "lib"))

ev.InstallModule("math", eval.MathNs)
ev.InstallModule("platform", platform.Ns)
ev.InstallModule("re", re.Ns)
ev.InstallModule("str", str.Ns)
ev.InstallModule("platform", platform.Ns)
ev.InstallModule("unix", unix.Ns)

if sockpath != "" && dbpath != "" {
spawner := &daemonp.Daemon{
BinPath: binpath,
Expand Down
4 changes: 4 additions & 0 deletions website/ref/index.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ title = "The Regular Expression Module (re:)"
name = "str"
title = "The String Module (str:)"

[[articles]]
name = "unix"
title = "The UNIX Module (unix:)"

[[articles]]
name = "epm"
title = "The Elvish Package Manager (epm:)"
Expand Down
10 changes: 10 additions & 0 deletions website/ref/unix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- toc -->

# Introduction

The `unix:` module provides access to features that only make sense on UNIX
compatible operating systems such as Linux, FreeBSD, macOS, etc. Do not use
the features in this module in Elvish scripts that need to work on non-UNIX
operating systems such as MS Windows.

@elvdoc -ns unix: -dir ../pkg/eval/unix

0 comments on commit 996e6c5

Please sign in to comment.